From 952636c1e61b57d52f2e70e6fe5745fa60667280 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Fri, 24 Jul 2026 11:54:28 +0800 Subject: [PATCH 1/7] fix(draw): restore table cell resize origin --- .../src/plugins/with-table-resize.spec.ts | 114 ++++++++++++++++++ .../draw/src/plugins/with-table-resize.ts | 38 +++--- 2 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 packages/draw/src/plugins/with-table-resize.spec.ts diff --git a/packages/draw/src/plugins/with-table-resize.spec.ts b/packages/draw/src/plugins/with-table-resize.spec.ts new file mode 100644 index 000000000..1e2eb9837 --- /dev/null +++ b/packages/draw/src/plugins/with-table-resize.spec.ts @@ -0,0 +1,114 @@ +import { fakeAsync, tick } from '@angular/core/testing'; +import { + BOARD_TO_HOST, + BOARD_TO_ELEMENT_HOST, + IS_BOARD_ALIVE, + PlaitBoard, + cacheSelectedElements, + clearNodeWeakMap, + createG, + createPointerEvent, + createTestingBoard, + fakeNodeWeakMap +} from '@plait/core'; +import { PlaitTable } from '../interfaces/table'; +import { withDraw } from './with-draw'; + +describe('withTableResize', () => { + let board: PlaitBoard; + let table: PlaitTable; + + beforeEach(() => { + table = { + id: 'table', + type: 'table', + points: [ + [0, 0], + [200, 100] + ], + rows: [{ id: 'row-1' }], + columns: [ + { id: 'column-1', width: 100 }, + { id: 'column-2', width: 100 } + ], + cells: [ + { id: 'cell-1-1', rowId: 'row-1', columnId: 'column-1' }, + { id: 'cell-1-2', rowId: 'row-1', columnId: 'column-2' } + ] + }; + board = createTestingBoard([withDraw], [table]); + fakeNodeWeakMap(board); + fakeElementHost(board); + fakeBoardHost(board); + IS_BOARD_ALIVE.set(board, true); + cacheSelectedElements(board, [table]); + }); + + afterEach(() => { + clearNodeWeakMap({ children: [table] } as unknown as PlaitBoard); + clearNodeWeakMap(board); + BOARD_TO_ELEMENT_HOST.delete(board); + BOARD_TO_HOST.delete(board); + IS_BOARD_ALIVE.delete(board); + }); + + it('should restore the original column size when the pointer returns to the resize origin', fakeAsync(() => { + board.pointerDown(createPointerEvent('pointerdown', 100, 50)); + + board.pointerMove(createPointerEvent('pointermove', 120, 50)); + tick(16); + + expect((board.children[0] as PlaitTable).columns[0].width).toBe(120); + expect((board.children[0] as PlaitTable).points).toEqual([ + [0, 0], + [220, 100] + ]); + + board.pointerMove(createPointerEvent('pointermove', 100, 50)); + tick(16); + board.globalPointerUp(createPointerEvent('pointerup', 100, 50)); + + expect((board.children[0] as PlaitTable).columns[0].width).toBe(100); + expect((board.children[0] as PlaitTable).points).toEqual([ + [0, 0], + [200, 100] + ]); + })); +}); + +function fakeElementHost(board: PlaitBoard) { + BOARD_TO_ELEMENT_HOST.set(board, { + lowerHost: createG(), + host: createG(), + upperHost: createG(), + topHost: createG(), + activeHost: createG(), + container: document.createElement('div'), + viewportContainer: document.createElement('div') + }); +} + +function fakeBoardHost(board: PlaitBoard) { + const host = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement; + Object.defineProperty(host, 'viewBox', { + value: { + baseVal: { + x: 0, + y: 0, + width: 1000, + height: 1000 + } + } + }); + spyOn(host, 'getBoundingClientRect').and.returnValue({ + x: 0, + y: 0, + width: 1000, + height: 1000, + top: 0, + right: 1000, + bottom: 1000, + left: 0 + } as DOMRect); + BOARD_TO_HOST.set(board, host); +} diff --git a/packages/draw/src/plugins/with-table-resize.ts b/packages/draw/src/plugins/with-table-resize.ts index ca51bdc6c..386bad23c 100644 --- a/packages/draw/src/plugins/with-table-resize.ts +++ b/packages/draw/src/plugins/with-table-resize.ts @@ -92,26 +92,24 @@ export function withTableResize(board: PlaitTableBoard) { const sizeOffset = edge === 'start' ? -pointerOffset : pointerOffset; const targetSize = Math.max(MIN_CELL_SIZE, currentSize + sizeOffset); const appliedOffset = targetSize - currentSize; - if (appliedOffset !== 0) { - if (isRow) { - const { rows, points } = updateRows( - resizeRef.element, - resizeRef.element.rows[targetIndex].id, - targetSize, - appliedOffset, - edge - ); - Transforms.setNode(board, { rows, points }, path); - } else { - const { columns, points } = updateColumns( - resizeRef.element, - resizeRef.element.columns[targetIndex].id, - targetSize, - appliedOffset, - edge - ); - Transforms.setNode(board, { columns, points }, path); - } + if (isRow) { + const { rows, points } = updateRows( + resizeRef.element, + resizeRef.element.rows[targetIndex].id, + targetSize, + appliedOffset, + edge + ); + Transforms.setNode(board, { rows, points }, path); + } else { + const { columns, points } = updateColumns( + resizeRef.element, + resizeRef.element.columns[targetIndex].id, + targetSize, + appliedOffset, + edge + ); + Transforms.setNode(board, { columns, points }, path); } } } else if (isCornerHandle(board, resizeRef.handle)) { From 6b2214ef59885370a3da3734c272f2b3d1b9b373 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Fri, 24 Jul 2026 18:56:03 +0800 Subject: [PATCH 2/7] refactor: unit test --- .../core/src/plugins/with-selection.spec.ts | 12 +-- .../core/src/testing/core/create-board.ts | 20 ---- .../core/src/testing/core/fake-weak-map.ts | 40 ------- packages/core/src/testing/core/index.ts | 2 - packages/core/src/testing/fixture/index.ts | 3 + .../core/src/testing/fixture/mock-board.ts | 102 ++++++++++++++++++ .../core/src/testing/fixture/mock-host.ts | 76 +++++++++++++ .../core/src/testing/fixture/mock-weak-map.ts | 21 ++++ packages/core/src/testing/index.ts | 4 +- .../event-objects.ts | 2 +- .../{fake-events => mock-events}/index.ts | 0 .../testing/{ => mock-events}/test-element.ts | 0 .../src/plugins/with-table-resize.spec.ts | 73 ++----------- packages/draw/src/transforms/swimlane.spec.ts | 17 ++- packages/draw/src/utils/fill-style.spec.ts | 29 +++-- 15 files changed, 250 insertions(+), 151 deletions(-) delete mode 100644 packages/core/src/testing/core/create-board.ts delete mode 100644 packages/core/src/testing/core/fake-weak-map.ts delete mode 100644 packages/core/src/testing/core/index.ts create mode 100644 packages/core/src/testing/fixture/index.ts create mode 100644 packages/core/src/testing/fixture/mock-board.ts create mode 100644 packages/core/src/testing/fixture/mock-host.ts create mode 100644 packages/core/src/testing/fixture/mock-weak-map.ts rename packages/core/src/testing/{fake-events => mock-events}/event-objects.ts (99%) rename packages/core/src/testing/{fake-events => mock-events}/index.ts (100%) rename packages/core/src/testing/{ => mock-events}/test-element.ts (100%) diff --git a/packages/core/src/plugins/with-selection.spec.ts b/packages/core/src/plugins/with-selection.spec.ts index b32f28cb2..2231ab7b0 100644 --- a/packages/core/src/plugins/with-selection.spec.ts +++ b/packages/core/src/plugins/with-selection.spec.ts @@ -1,6 +1,6 @@ import { fakeAsync, tick } from '@angular/core/testing'; import { PlaitBoard, PlaitElement, PlaitPointerType } from '../interfaces'; -import { clearBoardElementHost, clearNodeWeakMap, createTestingBoard, fakeBoardElementHost, fakeNodeWeakMap } from '../testing'; +import { setupTestingBoard, TestingBoardFixture } from '../testing'; import { Transforms } from '../transforms'; import { cacheSelectedElements, createG, getSelectedElements } from '../utils'; import { withOptions } from './with-options'; @@ -15,18 +15,18 @@ describe('withSelection', () => { let board: PlaitBoard; let activeHost: SVGGElement; let drawSelectionRectangle: jasmine.Spy; + let fixture: TestingBoardFixture; beforeEach(() => { - board = createTestingBoard([withOptions, withSelection], children); - fakeNodeWeakMap(board); - activeHost = fakeBoardElementHost(board).activeHost; + fixture = setupTestingBoard([withOptions, withSelection], children); + board = fixture.board; + activeHost = PlaitBoard.getActiveHost(board); drawSelectionRectangle = jasmine.createSpy('drawSelectionRectangle').and.callFake(() => createG()); board.drawSelectionRectangle = drawSelectionRectangle; }); afterEach(() => { - clearNodeWeakMap(board); - clearBoardElementHost(board); + fixture.destroy(); }); it('should refresh the multi-selection rectangle after viewport changes in hand mode', fakeAsync(() => { diff --git a/packages/core/src/testing/core/create-board.ts b/packages/core/src/testing/core/create-board.ts deleted file mode 100644 index a512274fb..000000000 --- a/packages/core/src/testing/core/create-board.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PlaitBoardOptions, PlaitElement, PlaitPlugin } from '../../interfaces'; -import { createBoard } from '../../plugins/create-board'; -import { KEY_TO_ELEMENT_MAP } from '../../utils'; - -/** - * 1.create board instance - * 2.build fake node weak map - */ -export const createTestingBoard = ( - plugins: PlaitPlugin[], - children: PlaitElement[], - options: PlaitBoardOptions = { readonly: false, hideScrollbar: true, disabledScrollOnNonFocus: false } -) => { - let board = createBoard(children, options); - plugins.forEach((plugin) => { - board = plugin(board); - }); - KEY_TO_ELEMENT_MAP.set(board, new Map()); - return board; -}; diff --git a/packages/core/src/testing/core/fake-weak-map.ts b/packages/core/src/testing/core/fake-weak-map.ts deleted file mode 100644 index 8ea59053f..000000000 --- a/packages/core/src/testing/core/fake-weak-map.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PlaitBoard } from '../../interfaces/board'; -import { PlaitNode } from '../../interfaces/node'; -import { createG } from '../../utils/dom/common'; -import { BOARD_TO_ELEMENT_HOST, NODE_TO_INDEX, NODE_TO_PARENT } from '../../utils/weak-maps'; - -export const fakeNodeWeakMap = (object: PlaitNode | PlaitBoard) => { - const children = object.children || []; - children.forEach((value, index: number) => { - NODE_TO_PARENT.set(value, object); - NODE_TO_INDEX.set(value, index); - fakeNodeWeakMap(value); - }); -}; - -export const clearNodeWeakMap = (object: PlaitNode | PlaitBoard) => { - const children = object.children || []; - children.forEach((value) => { - NODE_TO_PARENT.delete(value); - NODE_TO_INDEX.delete(value); - clearNodeWeakMap(value); - }); -}; - -export const fakeBoardElementHost = (board: PlaitBoard) => { - const elementHost = { - lowerHost: createG(), - host: createG(), - upperHost: createG(), - topHost: createG(), - activeHost: createG(), - container: document.createElement('div'), - viewportContainer: document.createElement('div') - }; - BOARD_TO_ELEMENT_HOST.set(board, elementHost); - return elementHost; -}; - -export const clearBoardElementHost = (board: PlaitBoard) => { - BOARD_TO_ELEMENT_HOST.delete(board); -}; diff --git a/packages/core/src/testing/core/index.ts b/packages/core/src/testing/core/index.ts deleted file mode 100644 index a5d8c1dee..000000000 --- a/packages/core/src/testing/core/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './create-board'; -export * from './fake-weak-map'; diff --git a/packages/core/src/testing/fixture/index.ts b/packages/core/src/testing/fixture/index.ts new file mode 100644 index 000000000..626f5a69d --- /dev/null +++ b/packages/core/src/testing/fixture/index.ts @@ -0,0 +1,3 @@ +export * from './mock-board'; +export * from './mock-host'; +export * from './mock-weak-map'; diff --git a/packages/core/src/testing/fixture/mock-board.ts b/packages/core/src/testing/fixture/mock-board.ts new file mode 100644 index 000000000..97a35053d --- /dev/null +++ b/packages/core/src/testing/fixture/mock-board.ts @@ -0,0 +1,102 @@ +import { PlaitBoard, PlaitBoardOptions, PlaitElement, PlaitPlugin } from '../../interfaces'; +import { createBoard } from '../../plugins/create-board'; +import { cacheSelectedElements, KEY_TO_ELEMENT_MAP } from '../../utils'; +import { IS_BOARD_ALIVE } from '../../utils/weak-maps'; +import { + clearBoardElementHost, + clearBoardHost, + clearBoardRoughSVG, + fakeBoardElementHost, + fakeBoardHost, + fakeBoardRoughSVG +} from './mock-host'; +import { clearNodeWeakMap, fakeNodeWeakMap } from './mock-weak-map'; + +export interface TestingBoardFixture { + board: PlaitBoard; + destroy: () => void; +} + +/** + * 1.create board instance + * 2.build fake node weak map + */ +export const createTestingBoard = ( + plugins: PlaitPlugin[], + children: PlaitElement[], + options: PlaitBoardOptions = { readonly: false, hideScrollbar: true, disabledScrollOnNonFocus: false } +) => { + let board = createBoard(children, options); + plugins.forEach((plugin) => { + board = plugin(board); + }); + KEY_TO_ELEMENT_MAP.set(board, new Map()); + return board; +}; + +export interface SetupTestingBoardOptions { + boardOptions?: PlaitBoardOptions; + selectedElements?: any[]; + withNodeWeakMap?: boolean; + withElementHost?: boolean; + withHost?: boolean; + withRoughSVG?: boolean; + alive?: boolean; +} + +export const setupTestingBoard = ( + plugins: PlaitPlugin[], + children: PlaitElement[], + options: SetupTestingBoardOptions = {} +): TestingBoardFixture => { + const { + boardOptions, + selectedElements, + withNodeWeakMap = true, + withElementHost = true, + withHost = true, + withRoughSVG = false, + alive = true + } = options; + + const board = createTestingBoard(plugins as PlaitPlugin[], children as PlaitElement[], boardOptions); + + if (withNodeWeakMap) { + fakeNodeWeakMap(board); + } + if (withElementHost) { + fakeBoardElementHost(board); + } + if (withHost) { + fakeBoardHost(board); + } + if (withRoughSVG) { + fakeBoardRoughSVG(board); + } + if (selectedElements) { + cacheSelectedElements(board, selectedElements as PlaitElement[]); + } + if (alive) { + IS_BOARD_ALIVE.set(board, true); + } + + const destroy = () => { + if (withNodeWeakMap) { + clearNodeWeakMap(board); + } + if (withElementHost) { + clearBoardElementHost(board); + } + if (withHost) { + clearBoardHost(board); + } + if (withRoughSVG) { + clearBoardRoughSVG(board); + } + if (alive) { + IS_BOARD_ALIVE.delete(board); + } + }; + + return { board, destroy }; +}; diff --git a/packages/core/src/testing/fixture/mock-host.ts b/packages/core/src/testing/fixture/mock-host.ts new file mode 100644 index 000000000..92629f7f5 --- /dev/null +++ b/packages/core/src/testing/fixture/mock-host.ts @@ -0,0 +1,76 @@ +import { RoughSVG } from 'roughjs/bin/svg'; +import { PlaitBoard } from '../../interfaces/board'; +import { createG, createSVG } from '../../utils/dom/common'; +import { BOARD_TO_ELEMENT_HOST, BOARD_TO_HOST, BOARD_TO_ROUGH_SVG } from '../../utils/weak-maps'; + +export const fakeBoardElementHost = (board: PlaitBoard) => { + const elementHost = { + lowerHost: createG(), + host: createG(), + upperHost: createG(), + topHost: createG(), + activeHost: createG(), + container: document.createElement('div'), + viewportContainer: document.createElement('div') + }; + BOARD_TO_ELEMENT_HOST.set(board, elementHost); + return elementHost; +}; + +export const clearBoardElementHost = (board: PlaitBoard) => { + BOARD_TO_ELEMENT_HOST.delete(board); +}; + +export const fakeBoardHost = (board: PlaitBoard) => { + const host = createSVG(); + Object.defineProperty(host, 'viewBox', { + value: { + baseVal: { + x: 0, + y: 0, + width: 1000, + height: 1000 + } + } + }); + Object.defineProperty(host, 'getBoundingClientRect', { + value: () => + ({ + x: 0, + y: 0, + width: 1000, + height: 1000, + top: 0, + right: 1000, + bottom: 1000, + left: 0 + } as DOMRect) + }); + BOARD_TO_HOST.set(board as PlaitBoard, host); + return host; +}; + +export const clearBoardHost = (board: PlaitBoard) => { + BOARD_TO_HOST.delete(board); +}; + +export const createTestingRoughSVG = () => { + const roughSVG = { + path: () => createG(), + polygon: () => createG(), + circle: () => createG(), + rectangle: () => createG(), + linearPath: () => createG() + }; + return roughSVG as unknown as RoughSVG; +}; + +export const fakeBoardRoughSVG = (board: PlaitBoard) => { + const roughSVG = createTestingRoughSVG(); + BOARD_TO_ROUGH_SVG.set(board, roughSVG); + return roughSVG; +}; + +export const clearBoardRoughSVG = (board: PlaitBoard) => { + BOARD_TO_ROUGH_SVG.delete(board); +}; diff --git a/packages/core/src/testing/fixture/mock-weak-map.ts b/packages/core/src/testing/fixture/mock-weak-map.ts new file mode 100644 index 000000000..9bc55049c --- /dev/null +++ b/packages/core/src/testing/fixture/mock-weak-map.ts @@ -0,0 +1,21 @@ +import { PlaitBoard } from '../../interfaces/board'; +import { PlaitNode } from '../../interfaces/node'; +import { NODE_TO_INDEX, NODE_TO_PARENT } from '../../utils/weak-maps'; + +export const fakeNodeWeakMap = (object: PlaitNode | PlaitBoard) => { + const children = object.children || []; + children.forEach((value, index: number) => { + NODE_TO_PARENT.set(value, object); + NODE_TO_INDEX.set(value, index); + fakeNodeWeakMap(value); + }); +}; + +export const clearNodeWeakMap = (object: PlaitNode | PlaitBoard) => { + const children = object.children || []; + children.forEach((value) => { + NODE_TO_PARENT.delete(value); + NODE_TO_INDEX.delete(value); + clearNodeWeakMap(value); + }); +}; diff --git a/packages/core/src/testing/index.ts b/packages/core/src/testing/index.ts index 8dd94ef46..412389f2f 100644 --- a/packages/core/src/testing/index.ts +++ b/packages/core/src/testing/index.ts @@ -1,2 +1,2 @@ -export * from './core'; -export * from './fake-events'; +export * from './fixture'; +export * from './mock-events'; diff --git a/packages/core/src/testing/fake-events/event-objects.ts b/packages/core/src/testing/mock-events/event-objects.ts similarity index 99% rename from packages/core/src/testing/fake-events/event-objects.ts rename to packages/core/src/testing/mock-events/event-objects.ts index cfdd1258a..81638ed5a 100644 --- a/packages/core/src/testing/fake-events/event-objects.ts +++ b/packages/core/src/testing/mock-events/event-objects.ts @@ -7,7 +7,7 @@ */ import { IS_MAC } from '../../utils/dom/environment'; -import { ModifierKeys } from '../test-element'; +import { ModifierKeys } from './test-element'; /** Used to generate unique IDs for events. */ let uniqueIds = 0; diff --git a/packages/core/src/testing/fake-events/index.ts b/packages/core/src/testing/mock-events/index.ts similarity index 100% rename from packages/core/src/testing/fake-events/index.ts rename to packages/core/src/testing/mock-events/index.ts diff --git a/packages/core/src/testing/test-element.ts b/packages/core/src/testing/mock-events/test-element.ts similarity index 100% rename from packages/core/src/testing/test-element.ts rename to packages/core/src/testing/mock-events/test-element.ts diff --git a/packages/draw/src/plugins/with-table-resize.spec.ts b/packages/draw/src/plugins/with-table-resize.spec.ts index 1e2eb9837..1fd47a030 100644 --- a/packages/draw/src/plugins/with-table-resize.spec.ts +++ b/packages/draw/src/plugins/with-table-resize.spec.ts @@ -1,22 +1,13 @@ import { fakeAsync, tick } from '@angular/core/testing'; -import { - BOARD_TO_HOST, - BOARD_TO_ELEMENT_HOST, - IS_BOARD_ALIVE, - PlaitBoard, - cacheSelectedElements, - clearNodeWeakMap, - createG, - createPointerEvent, - createTestingBoard, - fakeNodeWeakMap -} from '@plait/core'; +import { PlaitBoard, TestingBoardFixture, createPointerEvent } from '@plait/core'; +import { setupTestingBoard } from '@plait/core'; import { PlaitTable } from '../interfaces/table'; import { withDraw } from './with-draw'; describe('withTableResize', () => { let board: PlaitBoard; let table: PlaitTable; + let fixture: TestingBoardFixture; beforeEach(() => { table = { @@ -36,38 +27,29 @@ describe('withTableResize', () => { { id: 'cell-1-2', rowId: 'row-1', columnId: 'column-2' } ] }; - board = createTestingBoard([withDraw], [table]); - fakeNodeWeakMap(board); - fakeElementHost(board); - fakeBoardHost(board); - IS_BOARD_ALIVE.set(board, true); - cacheSelectedElements(board, [table]); + fixture = setupTestingBoard([withDraw], [table], { + selectedElements: [table], + withRoughSVG: true + }); }); afterEach(() => { - clearNodeWeakMap({ children: [table] } as unknown as PlaitBoard); - clearNodeWeakMap(board); - BOARD_TO_ELEMENT_HOST.delete(board); - BOARD_TO_HOST.delete(board); - IS_BOARD_ALIVE.delete(board); + fixture.destroy(); }); it('should restore the original column size when the pointer returns to the resize origin', fakeAsync(() => { + board = fixture.board; board.pointerDown(createPointerEvent('pointerdown', 100, 50)); - board.pointerMove(createPointerEvent('pointermove', 120, 50)); tick(16); - expect((board.children[0] as PlaitTable).columns[0].width).toBe(120); expect((board.children[0] as PlaitTable).points).toEqual([ [0, 0], [220, 100] ]); - board.pointerMove(createPointerEvent('pointermove', 100, 50)); tick(16); board.globalPointerUp(createPointerEvent('pointerup', 100, 50)); - expect((board.children[0] as PlaitTable).columns[0].width).toBe(100); expect((board.children[0] as PlaitTable).points).toEqual([ [0, 0], @@ -75,40 +57,3 @@ describe('withTableResize', () => { ]); })); }); - -function fakeElementHost(board: PlaitBoard) { - BOARD_TO_ELEMENT_HOST.set(board, { - lowerHost: createG(), - host: createG(), - upperHost: createG(), - topHost: createG(), - activeHost: createG(), - container: document.createElement('div'), - viewportContainer: document.createElement('div') - }); -} - -function fakeBoardHost(board: PlaitBoard) { - const host = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement; - Object.defineProperty(host, 'viewBox', { - value: { - baseVal: { - x: 0, - y: 0, - width: 1000, - height: 1000 - } - } - }); - spyOn(host, 'getBoundingClientRect').and.returnValue({ - x: 0, - y: 0, - width: 1000, - height: 1000, - top: 0, - right: 1000, - bottom: 1000, - left: 0 - } as DOMRect); - BOARD_TO_HOST.set(board, host); -} diff --git a/packages/draw/src/transforms/swimlane.spec.ts b/packages/draw/src/transforms/swimlane.spec.ts index 396033e36..026a4c3ba 100644 --- a/packages/draw/src/transforms/swimlane.spec.ts +++ b/packages/draw/src/transforms/swimlane.spec.ts @@ -1,10 +1,17 @@ -import { createTestingBoard, fakeNodeWeakMap } from '@plait/core'; +import { TestingBoardFixture, setupTestingBoard } from '@plait/core'; import { withDraw } from '../plugins/with-draw'; import { PlaitSwimlane, SwimlaneDrawSymbols } from '../interfaces'; import { createDefaultSwimlane } from '../utils/swimlane'; import { addSwimlaneColumn, addSwimlaneRow } from './swimlane'; describe('swimlane transforms', () => { + let fixture: TestingBoardFixture | null = null; + + afterEach(() => { + fixture?.destroy(); + fixture = null; + }); + const expectCellsInRowMajorOrder = (swimlane: PlaitSwimlane) => { const expectedPositions = swimlane.rows.flatMap((row) => swimlane.columns.map((column) => ({ rowId: row.id, columnId: column.id })) @@ -17,8 +24,8 @@ describe('swimlane transforms', () => { [0, 0], [600, 300] ]); - const board = createTestingBoard([withDraw], [swimlane]); - fakeNodeWeakMap(board); + fixture = setupTestingBoard([withDraw], [swimlane]); + const board = fixture.board; addSwimlaneRow(board, swimlane, 1); @@ -30,8 +37,8 @@ describe('swimlane transforms', () => { [0, 0], [600, 300] ]); - const board = createTestingBoard([withDraw], [swimlane]); - fakeNodeWeakMap(board); + fixture = setupTestingBoard([withDraw], [swimlane]); + const board = fixture.board; addSwimlaneColumn(board, swimlane, 1); diff --git a/packages/draw/src/utils/fill-style.spec.ts b/packages/draw/src/utils/fill-style.spec.ts index 13e3aa2ea..0962a45ad 100644 --- a/packages/draw/src/utils/fill-style.spec.ts +++ b/packages/draw/src/utils/fill-style.spec.ts @@ -1,4 +1,5 @@ -import { BOARD_TO_ROUGH_SVG, createTestingBoard, PlaitBoard, RectangleClient } from '@plait/core'; +import { PlaitBoard, RectangleClient, TestingBoardFixture } from '@plait/core'; +import { setupTestingBoard } from '@plait/core'; import { withDraw } from '../plugins/with-draw'; import { Options } from 'roughjs/bin/core'; import { GeometryShapeGenerator } from '../generators/geometry-shape.generator'; @@ -9,23 +10,24 @@ import { drawGeometry } from './geometry'; describe('fillStyle', () => { let board: PlaitBoard; + let fixture: TestingBoardFixture; beforeEach(() => { - board = createTestingBoard([withDraw], []); - BOARD_TO_ROUGH_SVG.set(board, createTestingRoughSVG()); + fixture = setupTestingBoard([withDraw], [], { + withNodeWeakMap: false, + withElementHost: false, + withHost: false, + withRoughSVG: true + }); }); - const createTestingRoughSVG = () => { - const createG = () => document.createElementNS('http://www.w3.org/2000/svg', 'g') as SVGGElement; - return { - rectangle: () => createG(), - path: () => createG(), - linearPath: () => createG() - } as any; - }; + afterEach(() => { + fixture.destroy(); + }); describe('GeometryShapeGenerator', () => { it('should use solid fillStyle by default', () => { + board = fixture.board; const roughSVG = PlaitBoard.getRoughSVG(board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const element: PlaitGeometry = { @@ -46,6 +48,7 @@ describe('fillStyle', () => { }); it('should pass element fillStyle to shape engine', () => { + board = fixture.board; const roughSVG = PlaitBoard.getRoughSVG(board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const element: PlaitGeometry = { @@ -69,6 +72,7 @@ describe('fillStyle', () => { describe('drawGeometry', () => { it('should use solid fillStyle by default when called directly', () => { + board = fixture.board; const roughSVG = PlaitBoard.getRoughSVG(board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const rectangle: RectangleClient = { @@ -89,6 +93,7 @@ describe('fillStyle', () => { }); it('should support all fill styles when called directly', () => { + board = fixture.board; const roughSVG = PlaitBoard.getRoughSVG(board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const rectangle: RectangleClient = { @@ -112,6 +117,7 @@ describe('fillStyle', () => { }); it('should preserve fillStyle for path based geometry engines', () => { + board = fixture.board; const roughSVG = PlaitBoard.getRoughSVG(board); const pathSpy = spyOn(roughSVG, 'path').and.callThrough(); const rectangle: RectangleClient = { @@ -145,6 +151,7 @@ describe('fillStyle', () => { describe('drawShape', () => { it('should use solid fillStyle by default for table cell fills', () => { + board = fixture.board; const roughSVG = PlaitBoard.getRoughSVG(board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const rectangle: RectangleClient = { From 976c1d0c70e03202242b729f1d0cd585db0c7b0a Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Sat, 25 Jul 2026 16:59:22 +0800 Subject: [PATCH 3/7] refactor: improve unit test by setupTestingBoard --- .../core/src/plugins/with-selection.spec.ts | 22 +++++------ .../src/plugins/with-table-resize.spec.ts | 20 +++++----- packages/draw/src/transforms/swimlane.spec.ts | 15 ++------ packages/draw/src/utils/fill-style.spec.ts | 38 +++++++------------ packages/draw/src/utils/swimlane.spec.ts | 3 -- 5 files changed, 37 insertions(+), 61 deletions(-) diff --git a/packages/core/src/plugins/with-selection.spec.ts b/packages/core/src/plugins/with-selection.spec.ts index 2231ab7b0..e415580f5 100644 --- a/packages/core/src/plugins/with-selection.spec.ts +++ b/packages/core/src/plugins/with-selection.spec.ts @@ -12,17 +12,15 @@ const children: PlaitElement[] = [ ]; describe('withSelection', () => { - let board: PlaitBoard; let activeHost: SVGGElement; let drawSelectionRectangle: jasmine.Spy; let fixture: TestingBoardFixture; beforeEach(() => { fixture = setupTestingBoard([withOptions, withSelection], children); - board = fixture.board; - activeHost = PlaitBoard.getActiveHost(board); + activeHost = PlaitBoard.getActiveHost(fixture.board); drawSelectionRectangle = jasmine.createSpy('drawSelectionRectangle').and.callFake(() => createG()); - board.drawSelectionRectangle = drawSelectionRectangle; + fixture.board.drawSelectionRectangle = drawSelectionRectangle; }); afterEach(() => { @@ -30,13 +28,13 @@ describe('withSelection', () => { }); it('should refresh the multi-selection rectangle after viewport changes in hand mode', fakeAsync(() => { - cacheSelectedElements(board, children); - board.afterChange(); + cacheSelectedElements(fixture.board, children); + fixture.board.afterChange(); const initialRectangle = activeHost.firstElementChild; expect(activeHost.contains(initialRectangle)).toBeTrue(); - board.pointer = PlaitPointerType.hand; - Transforms.setViewport(board, { zoom: 1, origination: [10, 10] }); + fixture.board.pointer = PlaitPointerType.hand; + Transforms.setViewport(fixture.board, { zoom: 1, origination: [10, 10] }); tick(); expect(drawSelectionRectangle).toHaveBeenCalledTimes(2); @@ -46,15 +44,15 @@ describe('withSelection', () => { })); it('should not update selected elements from selection operations in hand mode', fakeAsync(() => { - cacheSelectedElements(board, children); - board.pointer = PlaitPointerType.hand; + cacheSelectedElements(fixture.board, children); + fixture.board.pointer = PlaitPointerType.hand; - Transforms.setSelection(board, { + Transforms.setSelection(fixture.board, { anchor: [100, 100], focus: [100, 100] }); tick(); - expect(getSelectedElements(board)).toEqual(children); + expect(getSelectedElements(fixture.board)).toEqual(children); })); }); diff --git a/packages/draw/src/plugins/with-table-resize.spec.ts b/packages/draw/src/plugins/with-table-resize.spec.ts index 1fd47a030..d2a7e83df 100644 --- a/packages/draw/src/plugins/with-table-resize.spec.ts +++ b/packages/draw/src/plugins/with-table-resize.spec.ts @@ -1,11 +1,10 @@ import { fakeAsync, tick } from '@angular/core/testing'; -import { PlaitBoard, TestingBoardFixture, createPointerEvent } from '@plait/core'; +import { TestingBoardFixture, createPointerEvent } from '@plait/core'; import { setupTestingBoard } from '@plait/core'; import { PlaitTable } from '../interfaces/table'; import { withDraw } from './with-draw'; describe('withTableResize', () => { - let board: PlaitBoard; let table: PlaitTable; let fixture: TestingBoardFixture; @@ -38,20 +37,19 @@ describe('withTableResize', () => { }); it('should restore the original column size when the pointer returns to the resize origin', fakeAsync(() => { - board = fixture.board; - board.pointerDown(createPointerEvent('pointerdown', 100, 50)); - board.pointerMove(createPointerEvent('pointermove', 120, 50)); + fixture.board.pointerDown(createPointerEvent('pointerdown', 100, 50)); + fixture.board.pointerMove(createPointerEvent('pointermove', 120, 50)); tick(16); - expect((board.children[0] as PlaitTable).columns[0].width).toBe(120); - expect((board.children[0] as PlaitTable).points).toEqual([ + expect((fixture.board.children[0] as PlaitTable).columns[0].width).toBe(120); + expect((fixture.board.children[0] as PlaitTable).points).toEqual([ [0, 0], [220, 100] ]); - board.pointerMove(createPointerEvent('pointermove', 100, 50)); + fixture.board.pointerMove(createPointerEvent('pointermove', 100, 50)); tick(16); - board.globalPointerUp(createPointerEvent('pointerup', 100, 50)); - expect((board.children[0] as PlaitTable).columns[0].width).toBe(100); - expect((board.children[0] as PlaitTable).points).toEqual([ + fixture.board.globalPointerUp(createPointerEvent('pointerup', 100, 50)); + expect((fixture.board.children[0] as PlaitTable).columns[0].width).toBe(100); + expect((fixture.board.children[0] as PlaitTable).points).toEqual([ [0, 0], [200, 100] ]); diff --git a/packages/draw/src/transforms/swimlane.spec.ts b/packages/draw/src/transforms/swimlane.spec.ts index 026a4c3ba..c70394b8a 100644 --- a/packages/draw/src/transforms/swimlane.spec.ts +++ b/packages/draw/src/transforms/swimlane.spec.ts @@ -9,7 +9,6 @@ describe('swimlane transforms', () => { afterEach(() => { fixture?.destroy(); - fixture = null; }); const expectCellsInRowMajorOrder = (swimlane: PlaitSwimlane) => { @@ -25,11 +24,8 @@ describe('swimlane transforms', () => { [600, 300] ]); fixture = setupTestingBoard([withDraw], [swimlane]); - const board = fixture.board; - - addSwimlaneRow(board, swimlane, 1); - - expectCellsInRowMajorOrder(board.children[0] as PlaitSwimlane); + addSwimlaneRow(fixture.board, swimlane, 1); + expectCellsInRowMajorOrder(fixture.board.children[0] as PlaitSwimlane); }); it('should keep cells ordered after adding a column in the middle', () => { @@ -38,10 +34,7 @@ describe('swimlane transforms', () => { [600, 300] ]); fixture = setupTestingBoard([withDraw], [swimlane]); - const board = fixture.board; - - addSwimlaneColumn(board, swimlane, 1); - - expectCellsInRowMajorOrder(board.children[0] as PlaitSwimlane); + addSwimlaneColumn(fixture.board, swimlane, 1); + expectCellsInRowMajorOrder(fixture.board.children[0] as PlaitSwimlane); }); }); diff --git a/packages/draw/src/utils/fill-style.spec.ts b/packages/draw/src/utils/fill-style.spec.ts index 0962a45ad..3976e3eeb 100644 --- a/packages/draw/src/utils/fill-style.spec.ts +++ b/packages/draw/src/utils/fill-style.spec.ts @@ -9,7 +9,6 @@ import { PlaitGeometry, BasicShapes, FlowchartSymbols, FILL_STYLES, UMLSymbols } import { drawGeometry } from './geometry'; describe('fillStyle', () => { - let board: PlaitBoard; let fixture: TestingBoardFixture; beforeEach(() => { @@ -27,8 +26,7 @@ describe('fillStyle', () => { describe('GeometryShapeGenerator', () => { it('should use solid fillStyle by default', () => { - board = fixture.board; - const roughSVG = PlaitBoard.getRoughSVG(board); + const roughSVG = PlaitBoard.getRoughSVG(fixture.board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const element: PlaitGeometry = { id: 'test-default-fill-style', @@ -40,16 +38,13 @@ describe('fillStyle', () => { ], fill: '#FF5733' }; - - new GeometryShapeGenerator(board).draw(element, {}); - + new GeometryShapeGenerator(fixture.board).draw(element, {}); const options = rectangleSpy.calls.mostRecent().args[4] as Options; expect(options.fillStyle).toBe('solid'); }); it('should pass element fillStyle to shape engine', () => { - board = fixture.board; - const roughSVG = PlaitBoard.getRoughSVG(board); + const roughSVG = PlaitBoard.getRoughSVG(fixture.board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const element: PlaitGeometry = { id: 'test-custom-fill-style', @@ -63,8 +58,8 @@ describe('fillStyle', () => { fillStyle: 'hachure' }; - new GeometryShapeGenerator(board).draw(element, {}); - + new GeometryShapeGenerator(fixture.board).draw(element, {}); + new GeometryShapeGenerator(fixture.board).draw(element, {}); const options = rectangleSpy.calls.mostRecent().args[4] as Options; expect(options.fillStyle).toBe('hachure'); }); @@ -72,8 +67,7 @@ describe('fillStyle', () => { describe('drawGeometry', () => { it('should use solid fillStyle by default when called directly', () => { - board = fixture.board; - const roughSVG = PlaitBoard.getRoughSVG(board); + const roughSVG = PlaitBoard.getRoughSVG(fixture.board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const rectangle: RectangleClient = { x: 0, @@ -82,7 +76,7 @@ describe('fillStyle', () => { height: 100 }; - drawGeometry(board, rectangle, BasicShapes.rectangle, { + drawGeometry(fixture.board, rectangle, BasicShapes.rectangle, { stroke: '#000000', strokeWidth: 2, fill: '#FF5733' @@ -93,8 +87,7 @@ describe('fillStyle', () => { }); it('should support all fill styles when called directly', () => { - board = fixture.board; - const roughSVG = PlaitBoard.getRoughSVG(board); + const roughSVG = PlaitBoard.getRoughSVG(fixture.board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const rectangle: RectangleClient = { x: 0, @@ -104,7 +97,7 @@ describe('fillStyle', () => { }; FILL_STYLES.forEach((fillStyle) => { - drawGeometry(board, rectangle, BasicShapes.rectangle, { + drawGeometry(fixture.board, rectangle, BasicShapes.rectangle, { stroke: '#000000', strokeWidth: 2, fill: '#FF5733', @@ -117,8 +110,7 @@ describe('fillStyle', () => { }); it('should preserve fillStyle for path based geometry engines', () => { - board = fixture.board; - const roughSVG = PlaitBoard.getRoughSVG(board); + const roughSVG = PlaitBoard.getRoughSVG(fixture.board); const pathSpy = spyOn(roughSVG, 'path').and.callThrough(); const rectangle: RectangleClient = { x: 0, @@ -142,7 +134,7 @@ describe('fillStyle', () => { ]; shapes.forEach((shape) => { - drawGeometry(board, rectangle, shape, roughOptions); + drawGeometry(fixture.board, rectangle, shape, roughOptions); const options = pathSpy.calls.mostRecent().args[1] as Options; expect(options.fillStyle).toBe('hachure'); }); @@ -151,8 +143,7 @@ describe('fillStyle', () => { describe('drawShape', () => { it('should use solid fillStyle by default for table cell fills', () => { - board = fixture.board; - const roughSVG = PlaitBoard.getRoughSVG(board); + const roughSVG = PlaitBoard.getRoughSVG(fixture.board); const rectangleSpy = spyOn(roughSVG, 'rectangle').and.callThrough(); const rectangle: RectangleClient = { x: 0, @@ -171,10 +162,10 @@ describe('fillStyle', () => { columns: [{ id: 'column-1' }], cells: [{ id: 'cell-1', rowId: 'row-1', columnId: 'column-1', fill: '#FF5733' }] }; - (board as any).buildTable = (element: PlaitTable) => element; + (fixture.board as any).buildTable = (element: PlaitTable) => element; drawShape( - board, + fixture.board, rectangle, TableSymbols.table, { @@ -187,6 +178,5 @@ describe('fillStyle', () => { const options = rectangleSpy.calls.mostRecent().args[4] as Options; expect(options.fillStyle).toBe('solid'); }); - }); }); diff --git a/packages/draw/src/utils/swimlane.spec.ts b/packages/draw/src/utils/swimlane.spec.ts index e4a1af996..f9cd95757 100644 --- a/packages/draw/src/utils/swimlane.spec.ts +++ b/packages/draw/src/utils/swimlane.spec.ts @@ -12,11 +12,8 @@ describe('isSwimlanePointers', () => { it('uses the effective pointer by default', () => { board = createTestingBoard([], []); board.pointer = SwimlaneDrawSymbols.swimlaneHorizontal; - expect(isSwimlanePointers(board)).toBeTrue(); - BOARD_TO_TEMPORARY_POINTER.set(board, PlaitPointerType.hand); - expect(isSwimlanePointers(board)).toBeFalse(); }); }); From 2e45e4e66d35aa6eef4aad95c7acb6645bd5781f Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Mon, 27 Jul 2026 19:13:29 +0800 Subject: [PATCH 4/7] feat: improve test fixture and fill-style.spec.ts --- packages/core/src/testing/fixture/mock-board.ts | 4 +++- packages/core/src/testing/fixture/mock-host.ts | 14 ++++---------- packages/core/src/testing/fixture/mock-weak-map.ts | 6 +++++- packages/draw/src/utils/fill-style.spec.ts | 1 - 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/core/src/testing/fixture/mock-board.ts b/packages/core/src/testing/fixture/mock-board.ts index 97a35053d..04bb146c5 100644 --- a/packages/core/src/testing/fixture/mock-board.ts +++ b/packages/core/src/testing/fixture/mock-board.ts @@ -10,7 +10,7 @@ import { fakeBoardHost, fakeBoardRoughSVG } from './mock-host'; -import { clearNodeWeakMap, fakeNodeWeakMap } from './mock-weak-map'; +import { clearNodeWeakMap, clearNodeWeakMapByNodes, fakeNodeWeakMap } from './mock-weak-map'; export interface TestingBoardFixture { board: PlaitBoard; @@ -60,6 +60,7 @@ export const setupTestingBoard = ( } = options; const board = createTestingBoard(plugins as PlaitPlugin[], children as PlaitElement[], boardOptions); + const initialChildren = board.children; if (withNodeWeakMap) { fakeNodeWeakMap(board); @@ -82,6 +83,7 @@ export const setupTestingBoard = ( const destroy = () => { if (withNodeWeakMap) { + clearNodeWeakMapByNodes(initialChildren); clearNodeWeakMap(board); } if (withElementHost) { diff --git a/packages/core/src/testing/fixture/mock-host.ts b/packages/core/src/testing/fixture/mock-host.ts index 92629f7f5..9087771f1 100644 --- a/packages/core/src/testing/fixture/mock-host.ts +++ b/packages/core/src/testing/fixture/mock-host.ts @@ -23,16 +23,7 @@ export const clearBoardElementHost = (board: PlaitBoard) => { export const fakeBoardHost = (board: PlaitBoard) => { const host = createSVG(); - Object.defineProperty(host, 'viewBox', { - value: { - baseVal: { - x: 0, - y: 0, - width: 1000, - height: 1000 - } - } - }); + host.setAttribute('viewBox', '0 0 1000 1000'); Object.defineProperty(host, 'getBoundingClientRect', { value: () => ({ @@ -59,6 +50,9 @@ export const createTestingRoughSVG = () => { path: () => createG(), polygon: () => createG(), circle: () => createG(), + ellipse: () => createG(), + line: () => createG(), + curve: () => createG(), rectangle: () => createG(), linearPath: () => createG() }; diff --git a/packages/core/src/testing/fixture/mock-weak-map.ts b/packages/core/src/testing/fixture/mock-weak-map.ts index 9bc55049c..b2e898b58 100644 --- a/packages/core/src/testing/fixture/mock-weak-map.ts +++ b/packages/core/src/testing/fixture/mock-weak-map.ts @@ -13,7 +13,11 @@ export const fakeNodeWeakMap = (object: PlaitNode | PlaitBoard) => { export const clearNodeWeakMap = (object: PlaitNode | PlaitBoard) => { const children = object.children || []; - children.forEach((value) => { + clearNodeWeakMapByNodes(children); +}; + +export const clearNodeWeakMapByNodes = (nodes: PlaitNode[]) => { + nodes.forEach((value) => { NODE_TO_PARENT.delete(value); NODE_TO_INDEX.delete(value); clearNodeWeakMap(value); diff --git a/packages/draw/src/utils/fill-style.spec.ts b/packages/draw/src/utils/fill-style.spec.ts index 3976e3eeb..4df962c27 100644 --- a/packages/draw/src/utils/fill-style.spec.ts +++ b/packages/draw/src/utils/fill-style.spec.ts @@ -58,7 +58,6 @@ describe('fillStyle', () => { fillStyle: 'hachure' }; - new GeometryShapeGenerator(fixture.board).draw(element, {}); new GeometryShapeGenerator(fixture.board).draw(element, {}); const options = rectangleSpy.calls.mostRecent().args[4] as Options; expect(options.fillStyle).toBe('hachure'); From c5292deb39ff8fb6f8bfd8e5640e8a2860ea86fd Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Tue, 28 Jul 2026 19:05:01 +0800 Subject: [PATCH 5/7] feat: improve test fixture --- .../core/src/testing/fixture/mock-board.ts | 36 +++++++++++++++---- .../core/src/testing/fixture/mock-host.ts | 12 +------ .../core/src/testing/fixture/mock-weak-map.ts | 5 +-- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/core/src/testing/fixture/mock-board.ts b/packages/core/src/testing/fixture/mock-board.ts index 04bb146c5..b99d673c8 100644 --- a/packages/core/src/testing/fixture/mock-board.ts +++ b/packages/core/src/testing/fixture/mock-board.ts @@ -1,7 +1,7 @@ import { PlaitBoard, PlaitBoardOptions, PlaitElement, PlaitPlugin } from '../../interfaces'; import { createBoard } from '../../plugins/create-board'; -import { cacheSelectedElements, KEY_TO_ELEMENT_MAP } from '../../utils'; -import { IS_BOARD_ALIVE } from '../../utils/weak-maps'; +import { cacheSelectedElements, findElements, getSelectedElements, KEY_TO_ELEMENT_MAP } from '../../utils'; +import { BOARD_TO_SELECTED_ELEMENT, IS_BOARD_ALIVE } from '../../utils/weak-maps'; import { clearBoardElementHost, clearBoardHost, @@ -10,7 +10,7 @@ import { fakeBoardHost, fakeBoardRoughSVG } from './mock-host'; -import { clearNodeWeakMap, clearNodeWeakMapByNodes, fakeNodeWeakMap } from './mock-weak-map'; +import { clearNodeWeakMapByNodes, fakeNodeWeakMap } from './mock-weak-map'; export interface TestingBoardFixture { board: PlaitBoard; @@ -60,10 +60,10 @@ export const setupTestingBoard = ( } = options; const board = createTestingBoard(plugins as PlaitPlugin[], children as PlaitElement[], boardOptions); - const initialChildren = board.children; + const mappedNodes = new Set(); if (withNodeWeakMap) { - fakeNodeWeakMap(board); + fakeNodeWeakMap(board, mappedNodes); } if (withElementHost) { fakeBoardElementHost(board); @@ -81,10 +81,15 @@ export const setupTestingBoard = ( IS_BOARD_ALIVE.set(board, true); } + const { apply } = board; + board.apply = (operation) => { + apply(operation); + refreshNodeWeakMapsAndSelection(board, mappedNodes, withNodeWeakMap); + }; + const destroy = () => { if (withNodeWeakMap) { - clearNodeWeakMapByNodes(initialChildren); - clearNodeWeakMap(board); + clearNodeWeakMapByNodes([...mappedNodes]); } if (withElementHost) { clearBoardElementHost(board); @@ -98,7 +103,24 @@ export const setupTestingBoard = ( if (alive) { IS_BOARD_ALIVE.delete(board); } + KEY_TO_ELEMENT_MAP.delete(board); + BOARD_TO_SELECTED_ELEMENT.delete(board); }; return { board, destroy }; }; + +const refreshNodeWeakMapsAndSelection = (board: PlaitBoard, mappedNodes: Set, withNodeWeakMap: boolean) => { + if (withNodeWeakMap) { + fakeNodeWeakMap(board, mappedNodes); + } + + const selectedElementIds = new Set(getSelectedElements(board).map((element) => element.id)); + if (selectedElementIds.size > 0) { + const selectedElements = findElements(board, { + match: (element) => selectedElementIds.has(element.id), + recursion: () => true + }); + cacheSelectedElements(board, selectedElements); + } +}; diff --git a/packages/core/src/testing/fixture/mock-host.ts b/packages/core/src/testing/fixture/mock-host.ts index 9087771f1..72ccc367d 100644 --- a/packages/core/src/testing/fixture/mock-host.ts +++ b/packages/core/src/testing/fixture/mock-host.ts @@ -46,17 +46,7 @@ export const clearBoardHost = (board: PlaitBoard) => { }; export const createTestingRoughSVG = () => { - const roughSVG = { - path: () => createG(), - polygon: () => createG(), - circle: () => createG(), - ellipse: () => createG(), - line: () => createG(), - curve: () => createG(), - rectangle: () => createG(), - linearPath: () => createG() - }; - return roughSVG as unknown as RoughSVG; + return new RoughSVG(createSVG()); }; export const fakeBoardRoughSVG = (board: PlaitBoard) => { diff --git a/packages/core/src/testing/fixture/mock-weak-map.ts b/packages/core/src/testing/fixture/mock-weak-map.ts index b2e898b58..934e57640 100644 --- a/packages/core/src/testing/fixture/mock-weak-map.ts +++ b/packages/core/src/testing/fixture/mock-weak-map.ts @@ -2,12 +2,13 @@ import { PlaitBoard } from '../../interfaces/board'; import { PlaitNode } from '../../interfaces/node'; import { NODE_TO_INDEX, NODE_TO_PARENT } from '../../utils/weak-maps'; -export const fakeNodeWeakMap = (object: PlaitNode | PlaitBoard) => { +export const fakeNodeWeakMap = (object: PlaitNode | PlaitBoard, mappedNodes?: Set) => { const children = object.children || []; children.forEach((value, index: number) => { NODE_TO_PARENT.set(value, object); NODE_TO_INDEX.set(value, index); - fakeNodeWeakMap(value); + mappedNodes?.add(value); + fakeNodeWeakMap(value, mappedNodes); }); }; From bc0690a173d1015c4d5024fc38406b4996a475f5 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Tue, 28 Jul 2026 19:44:28 +0800 Subject: [PATCH 6/7] feat: support isEqualData --- packages/core/src/utils/helper.ts | 45 +++++++++++++++++++ .../draw/src/plugins/with-table-resize.ts | 11 +++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index 5ef9c68ac..6d072610a 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -4,6 +4,51 @@ export function isNullOrUndefined(value: any) { return value === null || value === undefined; } +/** + * Compare base data recursively. + * + * Supported recursive types: + * - primitive values compared by `Object.is` + * - arrays + * - plain objects created by object literal or `Object.create(null)` + * + * Unsupported recursive types: + * - Date, Map, Set, RegExp and class instances + * - circular references + */ +export function isEqualData(value: unknown, otherValue: unknown): boolean { + if (Object.is(value, otherValue)) { + return true; + } + + if (Array.isArray(value) && Array.isArray(otherValue)) { + return value.length === otherValue.length && value.every((item, index) => isEqualData(item, otherValue[index])); + } + + if (isPlainObject(value) && isPlainObject(otherValue)) { + const keys = Object.keys(value); + const otherKeys = Object.keys(otherValue); + return ( + keys.length === otherKeys.length && + keys.every( + (key) => + Object.prototype.hasOwnProperty.call(otherValue, key) && isEqualData(value[key], otherValue[key]) + ) + ); + } + + return false; +} + +function isPlainObject(value: unknown): value is Record { + if (value === null || typeof value !== 'object') { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + /** * get {x,y} point * @param point diff --git a/packages/draw/src/plugins/with-table-resize.ts b/packages/draw/src/plugins/with-table-resize.ts index 386bad23c..96b3960a2 100644 --- a/packages/draw/src/plugins/with-table-resize.ts +++ b/packages/draw/src/plugins/with-table-resize.ts @@ -1,4 +1,4 @@ -import { PlaitBoard, Point, RectangleClient, Transforms, getSelectedElements, hasValidAngle } from '@plait/core'; +import { PlaitBoard, PlaitNode, Point, RectangleClient, Transforms, getSelectedElements, hasValidAngle, isEqualData } from '@plait/core'; import { PlaitBaseTable, PlaitTableBoard, PlaitTableCell, PlaitTableCellWithPoints } from '../interfaces/table'; import { getIndexByResizeHandle, @@ -92,6 +92,7 @@ export function withTableResize(board: PlaitTableBoard) { const sizeOffset = edge === 'start' ? -pointerOffset : pointerOffset; const targetSize = Math.max(MIN_CELL_SIZE, currentSize + sizeOffset); const appliedOffset = targetSize - currentSize; + const table = PlaitNode.get(board, path) as PlaitBaseTable; if (isRow) { const { rows, points } = updateRows( resizeRef.element, @@ -100,7 +101,9 @@ export function withTableResize(board: PlaitTableBoard) { appliedOffset, edge ); - Transforms.setNode(board, { rows, points }, path); + if (!isEqualData(rows, table.rows) || !isEqualData(points, table.points)) { + Transforms.setNode(board, { rows, points }, path); + } } else { const { columns, points } = updateColumns( resizeRef.element, @@ -109,7 +112,9 @@ export function withTableResize(board: PlaitTableBoard) { appliedOffset, edge ); - Transforms.setNode(board, { columns, points }, path); + if (!isEqualData(columns, table.columns) || !isEqualData(points, table.points)) { + Transforms.setNode(board, { columns, points }, path); + } } } } else if (isCornerHandle(board, resizeRef.handle)) { From c36fb4688f83da5a726f5e9324946cd5e0eb3c51 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Tue, 28 Jul 2026 21:23:54 +0800 Subject: [PATCH 7/7] chore: update changeset for table resize origin fix --- .changeset/fresh-table-resize.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/fresh-table-resize.md b/.changeset/fresh-table-resize.md index 6dbf3acc9..4570ddbe4 100644 --- a/.changeset/fresh-table-resize.md +++ b/.changeset/fresh-table-resize.md @@ -1,5 +1,6 @@ --- +'@plait/core': patch '@plait/draw': patch --- -Adjust table cell edge resize behavior +restore table cell resize origin and add isEqualData for base data comparisons