diff --git a/.gitignore b/.gitignore index 388cb3b4..b239677f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode node_modules dist -*.tgz \ No newline at end of file +*.tgz +src/tests/*/__screenshots__ diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index c07a12f3..f890d136 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -11,6 +11,7 @@ const ComposedRelations = mixins(Element, Linksable, Relationstyleable); export class Relations extends ComposedRelations { static isSelectable = true; static hitScope = 'children'; + linkPoints = []; _renderDirty = true; @@ -25,7 +26,7 @@ export class Relations extends ComposedRelations { initPath() { const path = new Graphics(); - Object.assign(path, { type: 'path', links: [], allowContainsPoint: true }); + Object.assign(path, { type: 'path', allowContainsPoint: true }); this.addChild(path); return path; } @@ -49,6 +50,7 @@ export class Relations extends ComposedRelations { const { links } = this.props; if (!this.path) return; this.path.clear(); + this.linkPoints.length = 0; let lastPoint = null; for (const link of links) { @@ -82,6 +84,7 @@ export class Relations extends ComposedRelations { } this.path.lineTo(...targetPoint); lastPoint = targetPoint; + this.linkPoints.push({ sourcePoint, targetPoint }); } this.path.stroke(); } diff --git a/src/tests/render/Relations.test.js b/src/tests/render/Relations.test.js new file mode 100644 index 00000000..b01c92b5 --- /dev/null +++ b/src/tests/render/Relations.test.js @@ -0,0 +1,124 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { setupPatchmapTests } from './patchmap.setup'; + +describe('Relations Component Rendering Tests', () => { + const { getPatchmap } = setupPatchmapTests(); + + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + const baseMapData = [ + { type: 'item', id: 'item-A', size: 50, attrs: { x: 100, y: 100 } }, + { type: 'item', id: 'item-B', size: 50, attrs: { x: 300, y: 100 } }, + { type: 'item', id: 'item-C', size: 50, attrs: { x: 200, y: 300 } }, + { + type: 'relations', + id: 'rel-1', + links: [{ source: 'item-A', target: 'item-B' }], + style: { width: 2, color: 'primary.default' }, // 0x0c73bf + }, + ]; + + const getRelations = (patchmap) => { + return patchmap.selector('$..[?(@.id=="rel-1")]')[0]; + }; + + const getPath = (patchmap) => { + return getRelations(patchmap).children[0]; + }; + + it('should render correctly with initial properties', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + const relations = getRelations(patchmap); + const path = getPath(patchmap); + + expect(relations).toBeDefined(); + expect(path).toBeDefined(); + expect(path.type).toBe('path'); + + expect(relations.props.links).toHaveLength(1); + expect(relations.props.style.width).toBe(2); + + expect(relations.linkPoints).toHaveLength(1); + const points = relations.linkPoints[0]; + const itemA = patchmap.selector('$..[?(@.id=="item-A")]')[0]; + const itemB = patchmap.selector('$..[?(@.id=="item-B")]')[0]; + + expect(points.sourcePoint).toEqual([itemA.x, itemA.y]); + expect(points.targetPoint).toEqual([itemB.x, itemB.y]); + }); + + it('should update path style when the "style" property changes', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + patchmap.update({ + path: '$..[?(@.id=="rel-1")]', + changes: { style: { width: 5, color: 'primary.accent' } }, // 0xef4444 + }); + + const path = getPath(patchmap); + expect(path.strokeStyle.width).toBe(5); + expect(path.strokeStyle.color).toBe(0xef4444); + }); + + it('should recalculate points and redraw when "links" property changes', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + const relations = getRelations(patchmap); + const originalPointsLength = relations.linkPoints.length; + expect(originalPointsLength).toBe(1); + + patchmap.update({ + path: '$..[?(@.id=="rel-1")]', + changes: { + links: [{ source: 'item-B', target: 'item-C' }], + }, + }); + await vi.advanceTimersByTimeAsync(100); + + const updatedRelations = getRelations(patchmap); + expect(updatedRelations.props.links).toHaveLength(2); + expect(updatedRelations.linkPoints).toHaveLength(2); + + const newPoints = updatedRelations.linkPoints[1]; + const itemB = patchmap.selector('$..[?(@.id=="item-B")]')[0]; + const itemC = patchmap.selector('$..[?(@.id=="item-C")]')[0]; + expect(newPoints.sourcePoint).toEqual([itemB.x, itemB.y]); + expect(newPoints.targetPoint).toEqual([itemC.x, itemC.y]); + }); + + it('should recalculate points and redraw when a linked item is moved', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + const relations = getRelations(patchmap); + const originalPoint = [...relations.linkPoints[0].targetPoint]; + expect(originalPoint).toEqual([300, 100]); + + patchmap.update({ + path: '$..[?(@.id=="item-B")]', + changes: { attrs: { x: 400, y: 250 } }, + }); + + await vi.advanceTimersByTimeAsync(100); + + const updatedPoints = relations.linkPoints[0].targetPoint; + expect(updatedPoints).not.toEqual(originalPoint); + expect(updatedPoints).toEqual([400, 250]); + + const path = getPath(patchmap); + expect(path.getBounds().width).toBeGreaterThan(0); + }); +});