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
109 lines
4.4 KiB
TypeScript
109 lines
4.4 KiB
TypeScript
import type { Page } from '@playwright/test';
|
|
import { test, expect } from './fixtures';
|
|
import { clickTreeItem, findAllTreeItems, waitUntil, stableShot } from '../e2e/utils/element-tracker';
|
|
|
|
/**
|
|
* PCB Calculator WASM E2E Tests
|
|
*
|
|
* The calculator is a wxFrame containing a wxTreebook with ~14 calculator
|
|
* panels grouped under four section pages. There's no GAL canvas, no
|
|
* toolbars-as-tested, and no setup wizard for the calculator itself.
|
|
*
|
|
* calculator.html seeds a default KiCad config in preRun (like eeschema.html /
|
|
* pl_editor.html), so KiCad's first-run setup wizard never opens — the frame
|
|
* comes straight up and we wait deterministically for its panels to register.
|
|
* No wizard click-through, no fixed sleeps.
|
|
*
|
|
* Panel labels are sourced from pcb_calculator_frame.cpp:170-192 (kicad fork).
|
|
*/
|
|
|
|
async function waitForCalculatorReady(page: Page): Promise<void> {
|
|
await expect(page.locator('#canvas')).toBeVisible({ timeout: 90000 });
|
|
// "Calculate" is a unique button on the default Regulator panel — a reliable
|
|
// witness that the calculator frame is live and its panels have registered.
|
|
await waitUntil(
|
|
page,
|
|
() => {
|
|
const r = window.wxElementRegistry;
|
|
return !!r && r.findByLabel('Calculate', {}).length > 0;
|
|
},
|
|
'calculator Regulator panel live (Calculate button registered)',
|
|
{ timeout: 90000 }
|
|
);
|
|
}
|
|
|
|
async function getRegistryLabels(page: Page): Promise<string[]> {
|
|
return await page.evaluate(() => {
|
|
const registry = window.wxElementRegistry;
|
|
if (!registry) return [];
|
|
return registry.findAll({})
|
|
.map((el) => (el && el.label ? String(el.label) : ''))
|
|
.filter((l: string) => l.length > 0);
|
|
});
|
|
}
|
|
|
|
test.describe('PCB Calculator WASM', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/kicad/calculator.html');
|
|
});
|
|
|
|
test('loads calculator frame', async ({ page, testLogger }) => {
|
|
void testLogger;
|
|
await waitForCalculatorReady(page);
|
|
|
|
const labels = await getRegistryLabels(page);
|
|
const hasRegulatorPanel = labels.some(l => l === 'Calculate');
|
|
expect(hasRegulatorPanel, `expected the calculator's default Regulator panel to be live (Calculate button registered). Got: ${JSON.stringify(labels.slice(0, 30))}`).toBe(true);
|
|
|
|
await stableShot(page, 'calculator-loaded.png');
|
|
});
|
|
|
|
test('treebook lists expected panels', async ({ page, testLogger }) => {
|
|
void testLogger;
|
|
await waitForCalculatorReady(page);
|
|
|
|
const treeItems = await findAllTreeItems(page);
|
|
const treeLabels = treeItems.map(i => i.label).filter((l): l is string => typeof l === 'string');
|
|
|
|
const requiredSubset = [
|
|
'Regulators',
|
|
'Resistor Calculator',
|
|
'Via Size',
|
|
'Track Width',
|
|
'Color Code',
|
|
'RF Attenuators',
|
|
'Transmission Lines',
|
|
];
|
|
const missing = requiredSubset.filter(req => !treeLabels.includes(req));
|
|
expect(missing, `treebook is missing expected panels: ${JSON.stringify(missing)} (tree items: ${JSON.stringify(treeLabels)})`).toEqual([]);
|
|
});
|
|
|
|
test('switch to Color Code panel', async ({ page, testLogger }) => {
|
|
void testLogger;
|
|
await waitForCalculatorReady(page);
|
|
|
|
await stableShot(page, 'calculator-before-switch.png');
|
|
|
|
const clicked = await clickTreeItem(page, 'Color Code');
|
|
expect(clicked, 'expected to find and click the Color Code tree item').toBe(true);
|
|
|
|
// The Color Code panel exposes a unique "Tolerance" label the Regulator
|
|
// panel does not — wait for it as proof the panel swapped in (replaces
|
|
// waitForTimeout(800)).
|
|
await waitUntil(
|
|
page,
|
|
() => {
|
|
const r = window.wxElementRegistry;
|
|
if (!r) return false;
|
|
return r.findAll({}).some((el) => /Tolerance/i.test(el.label || ''));
|
|
},
|
|
'Color Code panel active (Tolerance label present)',
|
|
);
|
|
|
|
const labelsAfter = await getRegistryLabels(page);
|
|
const onColorCodePanel = labelsAfter.some(l => /Tolerance/i.test(l));
|
|
expect(onColorCodePanel, `expected Color Code panel to be active after click; labels: ${JSON.stringify(labelsAfter.slice(0, 40))}`).toBe(true);
|
|
|
|
await stableShot(page, 'calculator-color-code.png');
|
|
});
|
|
});
|