Skip to content

Commit 74c017d

Browse files
serpentbladeclaude
andcommitted
test(quick-260610-jrk): connector/socket alignment proof + rebless FlowCanvas baseline
Adds a per-target rete-flow-align cell that measures every drawn connection path's START/END screen points (getPointAtLength + getScreenCTM) against every socket center and asserts the worst-case VERTICAL offset to the nearest socket is <=6px. Pre-fix this is ~13.9px (the inline-SVG baseline pushed endpoints to the node bottom); post-fix it collapses to «1px. The horizontal offset is only sanity-bounded (<=20px) because getDOMSocketPosition intentionally shifts the stored position 12px outward. Deep-queries open shadow roots so the Lit cell sees its sockets/paths. 6/6 green; the 18 existing rete-flow* cells stay green. Reblessed the shared FlowCanvasScreenshot.png (geometry shifted onto the sockets); 6/6 against the single baseline (D-10 byte-identity held). Visually confirmed: connectors now run through the node-center sockets, not the bottoms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1984d21 commit 74c017d

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

90 Bytes
Loading

tests/visual-regression/specs/rete-flow.spec.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,162 @@ for (const target of TARGETS) {
406406
.toBe(3);
407407
});
408408
}
409+
410+
/**
411+
* Connector / socket vertical-alignment proof (quick-260610-jrk continuation #2).
412+
*
413+
* THE BUG: connection lines anchored ~14px BELOW each socket, at the node BOTTOM,
414+
* instead of on the socket. ROOT CAUSE (DOM-evidence-confirmed): the connection
415+
* `<svg>` was `display:inline` (the SVG default), so the 1px-tall SVG sat on the
416+
* connection element's TEXT BASELINE. With the engine container's default
417+
* line-height that baseline is ~14px below the connection element's top — and the
418+
* connection element IS the area-transform origin, so the offset is in screen space
419+
* and pushes EVERY endpoint ~14px down. The socket positions reported by
420+
* `getDOMSocketPosition` (offsetTop within the node-view) were already correct; the
421+
* inline-SVG baseline was the sole vertical drift. FIX: `display:block` on
422+
* `.rozie-flow-connection__svg` removes the baseline gap (CSS-only, in FlowCanvas's
423+
* scoped `:root {}` engine-DOM block — no script/emitter change).
424+
*
425+
* THE PROOF (load-bearing — must FAIL pre-fix, PASS post-fix): every drawn
426+
* connection path's START and END screen point must sit within tolerance of SOME
427+
* socket center VERTICALLY. Pre-fix worst dy ≈ 13.9px (node bottom); post-fix it
428+
* collapses to «1px (on the socket). The HORIZONTAL offset is NOT asserted tightly:
429+
* `getDOMSocketPosition.calculatePosition` intentionally returns the socket center
430+
* shifted 12px OUTWARD (`position.x + 12 * (side==='input' ? -1 : 1)`), so a correct
431+
* endpoint is ~12px horizontally from the socket center BY DESIGN — only a loose
432+
* sanity bound (≤ 20px) is checked horizontally. Tolerance rationale for the
433+
* vertical proof: cross-target sub-pixel kerning / AA / curvature-handle rounding is
434+
* « 6px, while the bug's node-bottom offset (~14px) is well outside it. Holds on all
435+
* 6 targets — they share the one vanilla render pipe + the same scoped connection CSS.
436+
*/
437+
const ALIGN_DY_TOLERANCE_PX = 6;
438+
const ALIGN_DX_SANITY_PX = 20; // 12px intentional outward offset + AA/rounding slack
439+
440+
for (const target of TARGETS) {
441+
const built = existsSync(
442+
resolve(__dirname, `../dist/${target}/host/entry.${target}.html`),
443+
);
444+
const runner = !built || KNOWN_FAILING.has(target) ? test.fixme : test;
445+
runner(`rete-flow-align [${target}]: connectors sit on the node sockets`, async ({
446+
page,
447+
}) => {
448+
await page.goto(`/?example=FlowCanvas&target=${target}`);
449+
const mount = page.getByTestId('rozie-mount');
450+
await expect(mount).toBeVisible();
451+
452+
const canvas = page.locator('.rozie-flow-canvas').first();
453+
await expect(canvas).toBeVisible({ timeout: 15_000 });
454+
await expect
455+
.poll(async () => page.locator('.rozie-flow-node').count(), {
456+
timeout: 15_000,
457+
})
458+
.toBeGreaterThanOrEqual(3);
459+
// both config-array edges (a→b, b→c) drawn before we measure.
460+
await expect
461+
.poll(async () => page.locator('.rozie-flow-connection__path').count(), {
462+
timeout: 10_000,
463+
})
464+
.toBeGreaterThanOrEqual(2);
465+
466+
// Give the watcher-driven redraw a moment to settle after mount/fit.
467+
await page.waitForTimeout(1200);
468+
469+
// For every DRAWN path, compute its START + END screen points (via the path's
470+
// own getPointAtLength + getScreenCTM, so transforms/zoom are accounted for),
471+
// collect every socket's screen-center, and report the worst-case offset of any
472+
// endpoint from its NEAREST socket center. The bug-specific signal is VERTICAL
473+
// (worstDy): pre-fix ~14px (node bottom), post-fix «1px (on the socket). The
474+
// horizontal offset is loose (the lib intentionally shifts the stored position
475+
// 12px outward), so worstDx is only sanity-bounded.
476+
const result = await page.evaluate(() => {
477+
// Deep query across the document AND every open shadow root (Lit renders the
478+
// canvas + sockets + connections inside a shadow root; plain querySelectorAll
479+
// does NOT pierce shadow DOM, so we recurse). Returns all matches everywhere.
480+
const deepQueryAll = (selector: string): Element[] => {
481+
const out: Element[] = [];
482+
const walk = (root: Document | ShadowRoot) => {
483+
out.push(...Array.from(root.querySelectorAll(selector)));
484+
for (const el of Array.from(root.querySelectorAll('*'))) {
485+
const sr = (el as HTMLElement).shadowRoot;
486+
if (sr) walk(sr);
487+
}
488+
};
489+
walk(document);
490+
return out;
491+
};
492+
493+
const sockets = deepQueryAll('.rozie-flow-socket').map((s) => {
494+
const r = (s as HTMLElement).getBoundingClientRect();
495+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
496+
});
497+
498+
const paths = deepQueryAll('.rozie-flow-connection__path').filter(
499+
(p) => ((p as SVGPathElement).getAttribute('d') || '').trim().length > 0,
500+
) as SVGPathElement[];
501+
502+
const screenPoint = (p: SVGPathElement, len: number) => {
503+
const pt = p.getPointAtLength(len);
504+
const m = p.getScreenCTM();
505+
if (!m) return null;
506+
return {
507+
x: pt.x * m.a + pt.y * m.c + m.e,
508+
y: pt.x * m.b + pt.y * m.d + m.f,
509+
};
510+
};
511+
512+
let worstDx = 0;
513+
let worstDy = 0;
514+
const endpoints: Array<{ dx: number; dy: number }> = [];
515+
for (const p of paths) {
516+
const total = p.getTotalLength();
517+
const ends = [screenPoint(p, 0), screenPoint(p, total)];
518+
for (const e of ends) {
519+
if (!e) continue;
520+
// nearest socket center to this endpoint
521+
let best = Infinity;
522+
let bestDx = Infinity;
523+
let bestDy = Infinity;
524+
for (const s of sockets) {
525+
const dx = Math.abs(e.x - s.x);
526+
const dy = Math.abs(e.y - s.y);
527+
const d = Math.hypot(dx, dy);
528+
if (d < best) {
529+
best = d;
530+
bestDx = dx;
531+
bestDy = dy;
532+
}
533+
}
534+
endpoints.push({ dx: bestDx, dy: bestDy });
535+
if (bestDx > worstDx) worstDx = bestDx;
536+
if (bestDy > worstDy) worstDy = bestDy;
537+
}
538+
}
539+
return {
540+
socketCount: sockets.length,
541+
pathCount: paths.length,
542+
endpointCount: endpoints.length,
543+
worstDx,
544+
worstDy,
545+
endpoints,
546+
};
547+
});
548+
549+
// Sanity: we actually measured drawn edges + sockets.
550+
expect(result.socketCount).toBeGreaterThanOrEqual(3);
551+
expect(result.pathCount).toBeGreaterThanOrEqual(2);
552+
expect(result.endpointCount).toBeGreaterThanOrEqual(4);
553+
554+
// THE PROOF (vertical): every endpoint sits on a socket center within tolerance
555+
// VERTICALLY — pre-fix worstDy ~14px (node bottom), post-fix «1px (on the socket).
556+
expect(
557+
result.worstDy,
558+
`worst vertical endpoint→socket offset ${result.worstDy.toFixed(2)}px (tol ${ALIGN_DY_TOLERANCE_PX}px) — pre-fix ~14px (node bottom); per-endpoint=${JSON.stringify(result.endpoints)}`,
559+
).toBeLessThanOrEqual(ALIGN_DY_TOLERANCE_PX);
560+
// SANITY (horizontal): each endpoint terminates near a socket (the lib shifts the
561+
// stored position 12px outward by design, so this is a loose bound, not the proof).
562+
expect(
563+
result.worstDx,
564+
`worst horizontal endpoint→socket offset ${result.worstDx.toFixed(2)}px (sanity ${ALIGN_DX_SANITY_PX}px; ~12px is the lib's intentional outward offset); per-endpoint=${JSON.stringify(result.endpoints)}`,
565+
).toBeLessThanOrEqual(ALIGN_DX_SANITY_PX);
566+
});
567+
}

0 commit comments

Comments
 (0)