pcbjam/tests/e2e/notebook.spec.ts
Viktor Vaczi 4c3a4cacd4 test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries
Make the Playwright e2e + kicad suites deterministic so screenshot flake stops
tracing to timing races.

- Blind page.waitForTimeout -> condition waits (expect.poll, web-first
  assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp).
  Remaining sleeps are documented interaction dwells (annotated).
- Defensive "if element exists" branches -> loud asserts; label-fallback chains
  -> normalized clickMenuItemByText. First-run wizard for/if loops removed by
  seeding calculator/gerbview/pcbnew HTMLs.
- Screenshots: new stableShot(page, name) settles the render in-page (canvas
  hash over rAF) then writes a raw PNG to test-results/ for the existing offline
  gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot,
  which did inline compare + its own baselines and had decoupled the specs from
  the real gate. scale:'css' pinned.
- retries: 0 in both configs.
- Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind
  sleeps / toHaveScreenshot / inline retries / swallowed catches in specs;
  documented exceptions carry a marker. Rules in tests/TESTING.md.

Assertions, coverage, and renders unchanged (semantic-equivalence reviewed;
captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance).
Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00

78 lines
3.8 KiB
TypeScript

// wxNotebook page-relayout e2e — regression coverage for the DOM-port fix in
// wxwidgets/src/wasm/notebook.cpp (wxNotebook::WasmRelayoutSelectedPage).
//
// The "Scrolled" page wraps a wxScrolledWindow full of "Row N" labels, and the
// app's wxEVT_NOTEBOOK_PAGE_CHANGED handler calls page->Fit() exactly like
// KiCad's APPEARANCE_CONTROLS. Without the fix that collapses the scrolled
// viewport and clip-paths the rows away after a tab round-trip; with it,
// OnDomEvent re-asserts the geometry so the rows stay painted.
import { test, expect } from './utils/fixtures';
import { clickTab, waitForWxApp, stableShot } from './utils/element-tracker';
import type { Page } from '@playwright/test';
declare global {
interface Window {
wxDomControls?: Map<number, HTMLElement>;
}
}
// Whether each row label is genuinely PAINTED (not merely present in the DOM).
// A bare getBoundingClientRect is not enough: when the scrolled window collapses
// on a tab round-trip the rows keep their layout box but are removed by an
// ancestor's clip-path. clip-path also affects hit-testing, so we sample points
// down each row's box and treat it as visible only if the row (or its content)
// is actually returned by elementsFromPoint. This is what makes the collapse
// regression fail the assertion. (Mirrors kicad/appearance.spec.ts:rowsVisible.)
async function rowsVisible(page: Page, labels: string[]): Promise<Record<string, boolean>> {
return page.evaluate((wanted: string[]) => {
const out: Record<string, boolean> = {};
for (const w of wanted) out[w] = false;
if (!window.wxDomControls) return out;
const visible = (el: HTMLElement): boolean => {
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) return false;
const cx = r.left + r.width / 2;
const ys = [r.top + 1, r.top + r.height / 2, r.bottom - 1];
return ys.some((y) => {
const hits = document.elementsFromPoint(cx, y);
return hits.some((h) => h === el || el.contains(h));
});
};
for (const [, el] of window.wxDomControls) {
const txt = el.textContent || '';
if (el.tagName === 'SPAN' && wanted.includes(txt) && visible(el)) out[txt] = true;
}
return out;
}, labels);
}
test.describe('DOM-port wxNotebook page relayout', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/standalone/notebook/notebook_test.html');
await waitForWxApp(page);
});
test('scrolled page rows survive a tab round-trip', async ({ page, testLogger }) => {
// First visit to the scrolled page: rows must be painted.
expect(await clickTab(page, 'Scrolled'), 'switch to Scrolled').toBe(true);
await stableShot(page, 'notebook-01-scrolled.png', { fullPage: true });
let vis = await rowsVisible(page, ['Row 0', 'Row 1']);
expect(vis['Row 0'], 'Row 0 visible on first visit').toBe(true);
// Round-trip: away to Plain, then back to Scrolled. The PAGE_CHANGED Fit()
// collapses the scrolled child; WasmRelayoutSelectedPage must restore it.
expect(await clickTab(page, 'Plain'), 'switch to Plain').toBe(true);
await page.waitForTimeout(300); // eslint-disable-line -- documented interaction dwell: let the Plain PAGE_CHANGED relayout commit before switching back
expect(await clickTab(page, 'Scrolled'), 'switch back to Scrolled').toBe(true);
await stableShot(page, 'notebook-02-scrolled-again.png', { fullPage: true });
vis = await rowsVisible(page, ['Row 0', 'Row 1']);
expect(vis['Row 0'], 'Row 0 visible after tab round-trip').toBe(true);
expect(vis['Row 1'], 'Row 1 visible after tab round-trip').toBe(true);
expect(testLogger.errors.filter((e) => !e.includes('favicon'))).toHaveLength(0);
});
});