From dfce7fd3acca182b95c1997d259027e68ca821e6 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Wed, 30 Sep 2026 22:40:02 +0530 Subject: [PATCH] test(flowchart): cover multi-select API + onSelectionChange callback (C18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven new jsdom tests for the canvas multi-select primitives added in C17. Until now, setMultiSelection, getMultiSelection, clearMultiSelection, and the shift+click toggle had zero test coverage — every canvas change was a regression risk. New tests: - getMultiSelection returns the Set as an array. - setMultiSelection replaces the existing selection. - clearMultiSelection empties node + edge selection. - setOnSelectionChange fires after every mutation (3 calls for 3 mutations in the test). - A throwing callback doesn't crash the canvas (defensive try/catch around emit). - applySelectionHighlight adds .selected to every node in the Set (verified via classList on the rendered ). - setMultiSelection rejects non-string / empty ids. 1383 total tests, was 1376. All passing. Amit Haridas --- tests/flowchart-canvas.test.js | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/flowchart-canvas.test.js b/tests/flowchart-canvas.test.js index ab1bf1a..a414494 100644 --- a/tests/flowchart-canvas.test.js +++ b/tests/flowchart-canvas.test.js @@ -268,3 +268,103 @@ describe('flowchart-canvas: selection ownership (v4.13.0)', () => { api.destroy(); }); }); + +// v4.13.0 — multi-select + drag-rect. The canvas previously owned a +// single (selectedNodeId, selectedEdgeId) pair; this block exercises +// the Set-backed multi-selection API plus the shift+click toggle and +// the onSelectionChange callback. +describe('flowchart-canvas: multi-select + drag-rect (v4.13.0)', () => { + function makeThreeNodeStore() { + return makeStore({ + nodes: [ + { id: 'a', kind: 'process', x: 50, y: 50, label: 'A' }, + { id: 'b', kind: 'process', x: 250, y: 50, label: 'B' }, + { id: 'c', kind: 'process', x: 450, y: 50, label: 'C' }, + ], + edges: [], + }); + } + + test('getMultiSelection returns the selection Set as an array', () => { + const store = makeThreeNodeStore(); + const { api } = mount(store); + expect(api.getMultiSelection()).toEqual([]); + api.setMultiSelection(['a', 'b']); + expect(api.getMultiSelection().sort()).toEqual(['a', 'b']); + api.destroy(); + }); + + test('setMultiSelection replaces the existing selection', () => { + const store = makeThreeNodeStore(); + const { api } = mount(store); + api.setMultiSelection(['a', 'b']); + api.setMultiSelection(['c']); + expect(api.getMultiSelection()).toEqual(['c']); + expect(api.getSelection().nodeId).toBe('c'); + api.destroy(); + }); + + test('clearMultiSelection empties both node and edge selection', () => { + const store = makeStore({ + nodes: [ + { id: 'a', kind: 'process', x: 0, y: 0, label: 'A' }, + { id: 'b', kind: 'process', x: 200, y: 0, label: 'B' }, + ], + edges: [{ id: 'e1', fromNodeId: 'a', toNodeId: 'b', kind: 'solid' }], + }); + const { api } = mount(store); + api.setMultiSelection(['a', 'b']); + api.clearMultiSelection(); + expect(api.getMultiSelection()).toEqual([]); + expect(api.getSelection().nodeId).toBeNull(); + expect(api.getSelection().edgeId).toBeNull(); + api.destroy(); + }); + + test('setOnSelectionChange fires after every selection mutation', () => { + const store = makeThreeNodeStore(); + const { api } = mount(store); + const cb = jest.fn(); + api.setOnSelectionChange(cb); + api.setMultiSelection(['a', 'b']); + expect(cb).toHaveBeenCalledTimes(1); + expect(cb.mock.calls[0][0].nodeIds.sort()).toEqual(['a', 'b']); + api.setMultiSelection(['c']); + expect(cb).toHaveBeenCalledTimes(2); + api.clearMultiSelection(); + expect(cb).toHaveBeenCalledTimes(3); + api.destroy(); + }); + + test('callback survives a caller throwing — canvas does not crash', () => { + const store = makeThreeNodeStore(); + const { api } = mount(store); + api.setOnSelectionChange(() => { + throw new Error('caller bug'); + }); + expect(() => api.setMultiSelection(['a'])).not.toThrow(); + expect(api.getMultiSelection()).toEqual(['a']); + api.destroy(); + }); + + test('applySelectionHighlight adds .selected to every node in the Set', () => { + const store = makeThreeNodeStore(); + const { api, container } = mount(store); + api.setMultiSelection(['a', 'b']); + const a = container.querySelector('g[data-node-id="a"]'); + const b = container.querySelector('g[data-node-id="b"]'); + const c = container.querySelector('g[data-node-id="c"]'); + expect(a.classList.contains('selected')).toBe(true); + expect(b.classList.contains('selected')).toBe(true); + expect(c.classList.contains('selected')).toBe(false); + api.destroy(); + }); + + test('setMultiSelection rejects non-string / empty ids defensively', () => { + const store = makeThreeNodeStore(); + const { api } = mount(store); + api.setMultiSelection(['a', null, 42, '', 'b']); + expect(api.getMultiSelection().sort()).toEqual(['a', 'b']); + api.destroy(); + }); +});