2026-05-29 09:51:18 +02:00
import type { Page } from '@playwright/test' ;
import { test , expect } from './fixtures' ;
import {
clickMenuBarItem ,
clickMenuItem ,
2026-07-07 10:50:24 +02:00
waitForEditorReady ,
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
waitForRenderedByLabel ,
waitUntil , stableShot , shotPath } from '../e2e/utils/element-tracker' ;
2026-05-29 09:51:18 +02:00
import { injectFromSubmodule } from './utils/fs-inject' ;
import { waitForBoardLoaded } from './utils/board-ready' ;
/ * *
* Spike test : load real . kicad_pcb demos through pcbnew ' s File → Open .
*
* Flow ( parametrized for each demo below ) :
* 1 . Wait for pcbnew to come up ( dismiss the first - run KiCad Setup wizard
* if it appears — depends on whether MEMFS already has config ) .
* 2 . Inject the demo ' s . kicad_pcb ( + . kicad_pro for completeness ) into
* MEMFS at PATHS : : GetDefaultUserProjectsPath ( ) — confirmed by
2026-06-23 21:23:20 +02:00
* load - pcb - probe . spec . ts to be / home / kicad / documents / kicad / 10.0 / projects / .
2026-05-29 09:51:18 +02:00
* 3 . Drive File → Open via menu helpers .
* 4 . Click on the file row in wxFileListCtrl ( wasm port doesn ' t register
* listctrl rows in wxElementRegistry , so we click by the filelist ' s
* bounding box ) , then focus the filename text input and press Enter to
* trigger wxGenericFileDialog ' s accept path . ( A direct OK button click
* doesn ' t dismiss the dialog in the wasm port ; the listctrl row click
* populates the text input but doesn ' t mark the row selected enough for
* OK to satisfy validation . )
* 5 . Dismiss any post - load info dialogs ( missing - libs etc . — pic_programmer
* uses local footprint libs and may pop one of these ) .
* 6 . Wait until the file dialog and the LoadBoard progress dialog both go
* away . Screenshot the loaded board , named per - demo .
*
* The board load itself was crashing in ` Classify ` inside ` rtree.h ` until we
* fixed the wasm - only ` ELEMTYPEREAL = intptr_t ` ( 32 - bit on wasm32 ) typo at
* ` kicad/libs/kimath/src/geometry/shape_poly_set.cpp:1927 ` . See
* ` features/<branch>/rtree-debug-findings.md ` for the full trail .
* /
2026-06-23 21:23:20 +02:00
// KiCad 10 stores projects under /home/kicad/documents/kicad/10.0/projects
// (GetMajorMinorVersion() = "10.0"); the old WASM port used "9.99".
const KICAD_VERSION_DIR = '10.0' ;
2026-05-29 09:51:18 +02:00
const PROJECT_DIR_MEMFS = ` /home/kicad/documents/kicad/ ${ KICAD_VERSION_DIR } /projects ` ;
type DemoCfg = {
name : string ; // appears in test name & screenshot filename
dir : string ; // folder under kicad/demos/
stem : string ; // file stem (kicad_pcb / kicad_pro)
} ;
const DEMOS : DemoCfg [ ] = [
{
// RF-polygon-heavy board — small, no external libs, was the case that
// tripped the rtree overflow in the first place.
name : 'microwave' ,
dir : 'microwave' ,
stem : 'microwave' ,
} ,
{
// "Normal" through-hole/SMD design — larger (~698 KB), uses local
// footprint libs from its own project tree (may pop a missing-libs
// warning, which we dismiss before screenshotting).
name : 'pic_programmer' ,
dir : 'pic_programmer' ,
stem : 'pic_programmer' ,
} ,
] ;
function runLoadPcbTest ( demo : DemoCfg ) : void {
const pcbFilename = ` ${ demo . stem } .kicad_pcb ` ;
const proFilename = ` ${ demo . stem } .kicad_pro ` ;
test ( ` opens ${ demo . name } demo from MEMFS through the wxFileDialog ` , async ( { page , testLogger } ) = > {
await page . goto ( '/kicad/pcbnew.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForEditorReady ( page ) ;
await stableShot ( page , ` load-pcb- ${ demo . name } -00-pcbnew-ready.png ` ) ;
2026-05-29 09:51:18 +02:00
// ── Inject .kicad_pcb + .kicad_pro into the dialog's start dir. ──
await injectFromSubmodule (
page ,
` kicad/demos/ ${ demo . dir } / ${ pcbFilename } ` ,
` ${ PROJECT_DIR_MEMFS } / ${ pcbFilename } ` ,
) ;
await injectFromSubmodule (
page ,
` kicad/demos/ ${ demo . dir } / ${ proFilename } ` ,
` ${ PROJECT_DIR_MEMFS } / ${ proFilename } ` ,
) ;
// ── Drive the menu. ────────────────────────────────────────────
const fileClicked = await clickMenuBarItem ( page , 'File' ) ;
expect ( fileClicked , 'File menu should be findable' ) . toBe ( true ) ;
2026-07-07 10:50:24 +02:00
await waitUntil (
page ,
( ) = > {
const r = window . wxElementRegistry ;
if ( ! r ? . findAllRendered ) return false ;
return r . findAllRendered ( { elementType : 'menuitem' } ) . length > 3 ;
} ,
'File menu items rendered' ,
) ;
2026-05-29 09:51:18 +02:00
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
// Items register progressively while the popup paints — wait for the one
// we click (clickMenuItem is single-shot; the >3-items gate isn't enough).
await waitForRenderedByLabel ( page , 'Open...' , { elementType : 'menuitem' } ) ;
2026-05-29 09:51:18 +02:00
const openClicked = await clickMenuItem ( page , 'Open...' ) ;
expect ( openClicked , 'Open… menu item should be findable' ) . toBe ( true ) ;
// ── Wait for the wxFileDialog to appear, file list to paint. ───
await page . waitForFunction ( ( ) = > {
const registry = window . wxElementRegistry ;
if ( ! registry ) return false ;
return registry . findAll ( { visible : true } )
. some ( ( el ) = > el . typeName === 'wxFileDialog' ) ;
} , null , { timeout : 15000 } ) ;
2026-07-07 10:50:24 +02:00
// stableShot stabilizes the file-list paint before comparing — deterministically
// replacing a fixed 1000ms that used to catch the dialog mid-paint (black rectangle).
await stableShot ( page , ` load-pcb- ${ demo . name } -01-dialog-open.png ` ) ;
2026-05-29 09:51:18 +02:00
2026-06-04 13:20:55 +02:00
// ── Focus the filename text field, type the name, accept. ──────
// The Open dialog gives default keyboard focus to the file LIST,
// where keystrokes act as type-ahead (and Enter on the highlighted
// ".." row navigates up) rather than filename entry. So we must click
// the wxTextCtrl to focus it first, located via the element registry
// rather than a fixed pixel offset. No filelist row click is needed —
// the earlier version's extra row + offset clicks were what left focus
// off the field, so the typed name never registered, the field stayed
// empty, and Enter was a no-op (the board never loaded).
2026-05-29 09:51:18 +02:00
const filenameInput = await page . evaluate ( ( ) = > {
const registry = window . wxElementRegistry ;
if ( ! registry ) return null ;
const text = registry . findAll ( { visible : true } )
. find ( ( el ) = > el . typeName === 'wxTextCtrl' && el . name === 'text' ) ;
return text ? { x : text.centerX , y : text.centerY } : null ;
} ) ;
expect ( filenameInput , 'filename text input should be visible' ) . not . toBeNull ( ) ;
if ( ! filenameInput ) throw new Error ( 'filename text input not found' ) ;
await page . mouse . click ( filenameInput . x , filenameInput . y ) ;
2026-07-07 10:50:24 +02:00
// Small settle so the focus click lands before typing — no JS-observable "input
// focused" signal here (documented interaction wait).
await page . waitForTimeout ( 200 ) ; // eslint-disable-line -- documented interaction dwell
2026-05-29 09:51:18 +02:00
await page . keyboard . type ( pcbFilename ) ;
2026-07-07 10:50:24 +02:00
await page . waitForTimeout ( 300 ) ; // eslint-disable-line -- documented interaction dwell
2026-05-29 09:51:18 +02:00
await page . keyboard . press ( 'Enter' ) ;
// ── Wait for the load to complete (no dialogs visible). The
// wxInfoBar that pcbnew shows for "older format" PCBs is not a
// wxDialog, so waitForBoardLoaded doesn't get blocked by it. ──
// If we ever need to dismiss post-load wxMessageDialogs (missing
// libs etc.), do it INSIDE waitForBoardLoaded so the dismiss
// side-effect lives with the polling loop — calling page.evaluate
// from the test driver hangs once the post-load asyncify clipboard
// runtime error breaks the wasm event loop. ───────────────────
// ── Wait for the load to complete (no dialogs visible). ───────
const result = await waitForBoardLoaded ( page , testLogger , 60000 ) ;
console . log ( ` [TEST] ${ demo . name } board-ready result: ${ result } ` ) ;
// Take the screenshot immediately. Don't wait — KiCad's post-load
// clipboard-polling path can hit a wasm RuntimeError that occasionally
// closes the page entirely on Firefox; if we sleep first we sometimes
// lose the page before page.screenshot runs. The canvas is already
// fully painted by the time waitForBoardLoaded returns.
await page . screenshot ( {
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
path : shotPath ( page , ` load-pcb- ${ demo . name } .png ` ) ,
2026-07-01 18:01:15 +02:00
scale : 'css' ,
2026-05-29 09:51:18 +02:00
} ) ;
const allLines = [ . . . testLogger . consoleLogs , . . . testLogger . errors ] ;
const rtreeDiag = allLines . filter ( ( l ) = > l . includes ( '[RTREE-DIAG]' ) ) ;
expect (
rtreeDiag ,
` RTree Classify duplicate-index reappeared for ${ demo . name } ; this means the wasm-layer fix at shape_poly_set.cpp:1927 has regressed: \ n ${ rtreeDiag . join ( '\n\n' ) } ` ,
) . toEqual ( [ ] ) ;
const aborts = allLines . filter ( ( l ) = > l . includes ( 'Aborted(' ) ) ;
expect (
aborts ,
` WASM aborted during ${ demo . name } load: \ n ${ aborts . join ( '\n\n' ) } ` ,
) . toEqual ( [ ] ) ;
2026-06-12 16:59:07 +02:00
// ── Clean-console gate: NO asyncify corruption may surface anywhere in
// the load — not before, not after the board renders. The formerly
// tolerated post-load clipboard/unwind RuntimeErrors are fixed
// (sync clipboard IsSupported in wx; "unwind" sentinel handling in
// scripts/common/shims/handlesleep.js; see docs/features/asyncify-arbiter/).
const asyncifySignatures = [
'index out of bounds' ,
'indirect call to null' ,
'uncaught exception: unwind' ,
'invalid state' ,
'is not a function' ,
] ;
const asyncifyErrors = allLines . filter ( ( l ) = >
asyncifySignatures . some ( ( sig ) = > l . toLowerCase ( ) . includes ( sig ) ) ,
) ;
expect (
asyncifyErrors ,
` Asyncify corruption surfaced during ${ demo . name } load: \ n ${ asyncifyErrors . join ( '\n\n' ) } ` ,
) . toEqual ( [ ] ) ;
2026-05-29 09:51:18 +02:00
} ) ;
}
test . describe ( 'Load real PCB via File → Open' , ( ) = > {
// Two 187 MB wasm runtimes loaded in parallel saturate Firefox's memory
// and slow each test enough that the 180s per-test budget runs out before
// the post-load canvas-settle wait completes. Run serially.
test . describe . configure ( { mode : 'serial' } ) ;
test . setTimeout ( 240000 ) ;
for ( const demo of DEMOS ) {
runLoadPcbTest ( demo ) ;
}
} ) ;