Files
markdown-converter/tests/flowchart-shapes.test.js
amitwh 23046967c3 feat(flowchart): discoverable connect form + per-node color + save-to-file export; v4.12.0
User feedback on v4.11.0: the Add Connection form was buried below the
node/edge lists and they couldn't find it; they also asked for per-node
color and Save to File export. v4.12.0 reorganises the #fc-nodelist panel
to put the connect form right after the Add Node buttons, adds a native
<input type="color"> per node row that drives a new store.setNodeColor
mutator (with serialize/deserialize round-trip), and wires a Save to File
button that pops a system save dialog via a new 'save-text-file' IPC
channel.

- src/flowchart-generator.html — panel order is now Add Node /
  Add Connection / Nodes / Edges / Export (Insert · Save · Reset).
  Removed the legacy top toolbar; status moved into the panel.
  Added .fc-help, per-node color-picker CSS, .fc-toolbar-row.
- src/flowchart/flowchart-shapes.js — shapeSvg() accepts an optional
  6th 'color' arg; emits fill=... on rects and polygons. Falls back to
  #ffffff for empty/null/undefined so old callers keep working.
- src/flowchart/flowchart-store.js — setNodeColor(id, color) mutator
  with snapshot/emit, normalizeColor() helper (hex validation),
  addNode accepts color, deserialize normalises missing color.
- src/renderer/flowchart-bundle.js — mirrors all of the above inline
  (the bundle is loaded as a single <script>), wires fc-btn-save to
  api.saveFile, passes node.color to shapeSvg in the canvas render,
  and renders <input type='color'> per node row.
- src/main.js — new ipcMain.handle('save-text-file', ...) using a
  system Save dialog (mirrors the ascii:save handler shape).
- src/preload.js — added 'save-text-file' to ALLOWED_SEND_CHANNELS
  and a saveFile(content, defaultName) helper on the flowchart bridge.

Tests: 92/92 suites, 1165/1165 tests pass on this run
- +9 store tests (color defaults, setNodeColor, undo, hex validation,
  round-trip, deserialize normalisation)
- +7 shapes tests (fill color on all 5 kinds, fallback to #ffffff)
- +6 controller tests (color picker wires setNodeColor, canvas rect
  reflects color, Save to File calls api.saveFile with the fenced
  Mermaid source and 'flowchart.mmd', cancel + error paths surface in
  status)

Verification:
- npm run lint          clean
- npm run format:check  clean
- npm test              92/92 suites, 1165/1165 tests pass

Amit Haridas
2026-09-15 09:23:58 +05:30

112 lines
3.7 KiB
JavaScript

/**
* @jest-environment node
*/
const {
shapeSvg,
SHAPE_KINDS,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
} = require('../src/flowchart/flowchart-shapes');
describe('flowchart-shapes: shapeSvg', () => {
test('process emits a <rect> at the given coordinates', () => {
const svg = shapeSvg('process', 10, 20, 100, 50);
expect(svg).toMatch(/<rect/);
expect(svg).toMatch(/x="10"/);
expect(svg).toMatch(/y="20"/);
expect(svg).toMatch(/width="100"/);
expect(svg).toMatch(/height="50"/);
});
test('decision emits a <polygon> diamond', () => {
const svg = shapeSvg('decision', 0, 0, 100, 60);
expect(svg).toMatch(/<polygon/);
// 4 points (diamond)
const match = /points="([^"]+)"/.exec(svg);
expect(match).not.toBeNull();
expect(match[1].split(/\s+/).filter(Boolean)).toHaveLength(4);
});
test('terminator emits a <rect> with rx (stadium)', () => {
const svg = shapeSvg('terminator', 0, 0, 120, 40);
expect(svg).toMatch(/<rect/);
expect(svg).toMatch(/rx="/);
});
test('subroutine emits two concentric <rect> elements (double border)', () => {
const svg = shapeSvg('subroutine', 0, 0, 100, 50);
const rects = svg.match(/<rect/g) || [];
expect(rects.length).toBeGreaterThanOrEqual(2);
});
test('document emits a <polygon> parallelogram', () => {
const svg = shapeSvg('document', 0, 0, 120, 60);
expect(svg).toMatch(/<polygon/);
const match = /points="([^"]+)"/.exec(svg);
expect(match[1].split(/\s+/).filter(Boolean)).toHaveLength(4);
});
test('unknown kind throws', () => {
expect(() => shapeSvg('hexagon', 0, 0, 100, 50)).toThrow(/hexagon/);
});
// v4.12.0 — optional fill color. Defaults to #ffffff when omitted, used
// when provided, and applied to every emitted element (rect/polygon and
// both rects of the subroutine double-border).
test('process emits a <rect> with default fill #ffffff when color is omitted', () => {
const svg = shapeSvg('process', 10, 20, 100, 50);
expect(svg).toMatch(/fill="#ffffff"/);
});
test('process emits a <rect> with the given fill color', () => {
const svg = shapeSvg('process', 0, 0, 100, 50, '#ff0000');
expect(svg).toMatch(/fill="#ff0000"/);
expect(svg).toMatch(/<rect/);
});
test('decision (polygon) honors the fill color', () => {
const svg = shapeSvg('decision', 0, 0, 100, 60, '#336699');
expect(svg).toMatch(/<polygon/);
expect(svg).toMatch(/fill="#336699"/);
});
test('subroutine paints both concentric <rect>s with the fill color', () => {
const svg = shapeSvg('subroutine', 0, 0, 100, 50, '#aabbcc');
const fills = svg.match(/fill="#aabbcc"/g) || [];
expect(fills.length).toBeGreaterThanOrEqual(2);
});
test('terminator honors the fill color', () => {
const svg = shapeSvg('terminator', 0, 0, 120, 40, '#123456');
expect(svg).toMatch(/fill="#123456"/);
expect(svg).toMatch(/rx="/);
});
test('document (parallelogram) honors the fill color', () => {
const svg = shapeSvg('document', 0, 0, 120, 60, '#abcdef');
expect(svg).toMatch(/<polygon/);
expect(svg).toMatch(/fill="#abcdef"/);
});
test('empty / falsy color falls back to #ffffff', () => {
expect(shapeSvg('process', 0, 0, 100, 50, '')).toMatch(/fill="#ffffff"/);
expect(shapeSvg('process', 0, 0, 100, 50, null)).toMatch(/fill="#ffffff"/);
expect(shapeSvg('process', 0, 0, 100, 50, undefined)).toMatch(/fill="#ffffff"/);
});
test('SHAPE_KINDS lists all 5 shapes', () => {
expect(SHAPE_KINDS.sort()).toEqual([
'decision',
'document',
'process',
'subroutine',
'terminator',
]);
});
test('DEFAULT_WIDTH and DEFAULT_HEIGHT are positive numbers', () => {
expect(DEFAULT_WIDTH).toBeGreaterThan(0);
expect(DEFAULT_HEIGHT).toBeGreaterThan(0);
});
});