Files
markdown-converter/tests/flowchart-align.test.js
amitwh bf12996a40 feat(flowchart): visual polish — selection ring, hover, resize grip (C16)
Adds the visual feedback that was missing from the C10 resize + C2
selection commits. The canvas previously set the .selected class but
no CSS rule styled it, so a selected node looked identical to an
unselected one. Same for hover and the resize handle.

CSS additions (src/flowchart-generator.html):

  - .flowchart-node: cursor:grab + 0.12s filter transition for the
    hover tint (drop-shadow blue glow).
  - .flowchart-node:hover .flowchart-node-shape: blue glow.
  - .flowchart-node.selected .flowchart-node-shape: 2.25px blue
    stroke — replaces the (previously invisible) .selected class.
  - .flowchart-edge:hover and .flowchart-edge.selected: blue
    stroke + 2.5px width for the same reason.
  - .flowchart-resize-handle-hit: 24px transparent rect for an
    enlarged hit area (the visible grip stays 10px).
  - .flowchart-resize-handle-grip: blue square with stroke, styled
    via CSS instead of inline fill/stroke (which were overriding
    the rest of the file's theme).
  - .flowchart-edge-label-bg: opaque white pill behind edge labels,
    so labels stay readable when an edge curves under them.
  - .fc-modal / .fc-modal-overlay: fade + scale-in keyframes
    (0.15s ease-out) so the promptInline / confirmInline modals
    animate in instead of popping in instantly.

Canvas (src/flowchart/flowchart-canvas.js): the resize handle now
emits two rects — a 24px transparent hit area (data-resize-node
attribute preserved) and a smaller 10px visible grip styled via
.flowchart-resize-handle-grip. Cursor stays nwse-resize via CSS.

Tests: no new tests (visual); existing 14 canvas tests still pass.
1376 total.

Amit Haridas
2026-09-30 22:29:35 +05:30

113 lines
4.0 KiB
JavaScript

/**
* @jest-environment node
*
* Pure tests for the alignment + distribution helpers.
* Each function takes nodes and returns a new array with positions
* adjusted — no store interaction, no DOM.
*/
const {
alignLeft,
alignRight,
alignTop,
alignBottom,
alignCenterHorizontal,
alignCenterVertical,
distributeHorizontally,
distributeVertically,
} = require('../src/flowchart/flowchart-align');
const n = (id, x, y, w = 120) => ({ id, x, y, width: w, label: id });
describe('flowchart-align: alignLeft', () => {
test('aligns all nodes to the leftmost x', () => {
const out = alignLeft([n('a', 100, 50), n('b', 200, 80), n('c', 300, 30)]);
expect(out.map((node) => node.x)).toEqual([100, 100, 100]);
});
test('preserves y positions and other fields', () => {
const out = alignLeft([n('a', 100, 50), n('b', 200, 80)]);
expect(out[0]).toMatchObject({ id: 'a', x: 100, y: 50 });
expect(out[1]).toMatchObject({ id: 'b', x: 100, y: 80 });
});
test('no-op for 0 or 1 nodes', () => {
expect(alignLeft([])).toEqual([]);
expect(alignLeft([n('a', 100, 50)])).toEqual([n('a', 100, 50)]);
});
test('returns a new array — does not mutate inputs', () => {
const inputs = [n('a', 100, 50), n('b', 200, 80)];
const snapshot = JSON.parse(JSON.stringify(inputs));
alignLeft(inputs);
expect(inputs).toEqual(snapshot);
});
});
describe('flowchart-align: alignRight', () => {
test('aligns all nodes to the rightmost (x + width)', () => {
const out = alignRight([n('a', 100, 50), n('b', 200, 80), n('c', 250, 30, 200)]);
// maxRight = 250 + 200 = 450
expect(out.map((node) => node.x + node.width)).toEqual([450, 450, 450]);
});
test('no-op for single node', () => {
expect(alignRight([n('a', 100, 50)])).toEqual([n('a', 100, 50)]);
});
});
describe('flowchart-align: alignTop / alignBottom', () => {
test('alignTop pins to the topmost y', () => {
const out = alignTop([n('a', 50, 100), n('b', 80, 200), n('c', 30, 50)]);
expect(out.map((node) => node.y)).toEqual([50, 50, 50]);
});
test('alignBottom pins to the bottommost y + 60', () => {
const out = alignBottom([n('a', 50, 100), n('b', 80, 200), n('c', 30, 150)]);
expect(out.map((node) => node.y)).toEqual([200, 200, 200]);
});
});
describe('flowchart-align: alignCenterHorizontal / alignCenterVertical', () => {
test('alignCenterHorizontal averages centers', () => {
const out = alignCenterHorizontal([
n('a', 0, 0, 100),
n('b', 100, 0, 100),
n('c', 200, 0, 100),
]);
// centers: 50, 150, 250 → avg = 150
expect(out.map((node) => node.x + node.width / 2)).toEqual([150, 150, 150]);
});
test('alignCenterVertical averages centers', () => {
const out = alignCenterVertical([n('a', 0, 0), n('b', 0, 60), n('c', 0, 120)]);
// centers: 30, 90, 150 → avg = 90
expect(out.map((node) => node.y + 30)).toEqual([90, 90, 90]);
});
});
describe('flowchart-align: distributeHorizontally', () => {
test('spaces 4 nodes evenly between leftmost and rightmost', () => {
const out = distributeHorizontally([
n('a', 0, 0),
n('b', 100, 0),
n('c', 500, 0),
n('d', 900, 0),
]);
// gap = (900 - 0) / 3 = 300
expect(out.map((node) => node.x)).toEqual([0, 300, 600, 900]);
});
test('handles unsorted input by sorting first', () => {
const out = distributeHorizontally([n('a', 500, 0), n('b', 0, 0), n('c', 900, 0)]);
// sorted: 0, 500, 900 → gap = 450 → x = [0, 450, 900]
expect(out.map((node) => node.x)).toEqual([0, 450, 900]);
});
test('no-op for fewer than 3 nodes', () => {
expect(distributeHorizontally([n('a', 0, 0), n('b', 100, 0)])).toEqual([
n('a', 0, 0),
n('b', 100, 0),
]);
});
});
describe('flowchart-align: distributeVertically', () => {
test('spaces 4 nodes evenly between topmost and bottommost', () => {
const out = distributeVertically([n('a', 0, 0), n('b', 0, 60), n('c', 0, 300), n('d', 0, 900)]);
// gap = 900 / 3 = 300
expect(out.map((node) => node.y)).toEqual([0, 300, 600, 900]);
});
});