2026-07-07 10:50:24 +02:00
import { test , expect , MAIN_CANVAS , waitForWxApp } from './utils/fixtures' ;
2025-11-29 20:54:54 +01:00
import { Page } from '@playwright/test' ;
2025-12-30 14:17:51 +01:00
import {
clickTab ,
clickByLabel ,
2026-01-01 20:18:19 +01:00
clickMenuBarItem ,
2025-12-30 14:17:51 +01:00
clickSlider ,
dragSliderTo ,
findSliderTrack ,
clickTextCtrl ,
findSingleLineTextCtrl ,
findMultiLineTextCtrl ,
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
clickListboxItemByIndex , stableShot , shotPath } from './utils/element-tracker' ;
2025-11-28 14:24:53 +01:00
// Capture console events with [EVENT] prefix
function captureEvents ( page : Page ) : string [ ] {
const events : string [ ] = [ ] ;
page . on ( 'console' , msg = > {
const text = msg . text ( ) ;
if ( text . startsWith ( '[EVENT]' ) ) {
events . push ( text . replace ( '[EVENT] ' , '' ) ) ;
}
} ) ;
return events ;
}
// Known non-critical warnings (wxWidgets and Emscripten)
function isKnownWarning ( error : string ) : boolean {
return error . includes ( 'unsupported bitmap depth' ) ||
error . includes ( 'error creating bitmap' ) ||
error . includes ( 'Failed to create line wrap XBM' ) ||
error . includes ( 'invalid bitmap' ) ||
error . includes ( 'assert' ) ||
error . includes ( 'HEAPU8' ) || // Emscripten export warning
2025-11-28 20:14:59 +01:00
error . includes ( 'showError' ) || // Template function
error . includes ( 'emscripten GL emulation' ) || // GL emulation warnings
error . includes ( 'GL immediate mode emulation' ) || // GL immediate mode warning
error . includes ( 'WebGL' ) || // WebGL version warnings
error . includes ( 'EndModal' ) || // wxWidgets debug messages
error . includes ( 'Debug:' ) ; // wxWidgets debug prefix
2025-11-28 14:24:53 +01:00
}
2026-06-12 10:18:16 +02:00
// Click at specific canvas coordinates. Uses page.mouse (no actionability
// check): in the DOM port real elements legitimately cover the canvas, and
// the click goes through the app's normal input routing either way.
2025-11-28 14:24:53 +01:00
async function clickCanvas ( page : Page , x : number , y : number ) {
const canvas = page . locator ( MAIN_CANVAS ) ;
2026-06-12 10:18:16 +02:00
const box = await canvas . boundingBox ( ) ;
if ( ! box ) throw new Error ( 'Canvas not found' ) ;
await page . mouse . click ( box . x + x , box . y + y ) ;
2025-11-28 14:24:53 +01:00
}
// Drag on canvas from one point to another
async function dragCanvas ( page : Page , startX : number , startY : number , endX : number , endY : number ) {
const canvas = page . locator ( MAIN_CANVAS ) ;
const box = await canvas . boundingBox ( ) ;
if ( ! box ) throw new Error ( 'Canvas not found' ) ;
await page . mouse . move ( box . x + startX , box . y + startY ) ;
await page . mouse . down ( ) ;
await page . mouse . move ( box . x + endX , box . y + endY , { steps : 10 } ) ;
await page . mouse . up ( ) ;
}
test . describe ( 'wxWidgets WASM - Diagnostics' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'comprehensive UI interaction test' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
test(screenshots): restore screenshot-neutrality after the determinism refactor
The determinism conversion silently dropped/renamed screenshots vs the committed
baselines, so CI flagged ~35 baselines as "removed". Restore them:
- pl_editor-*: re-add the "pl_editor-" prefix the conversion dropped (5).
- Buffer screenshots that lost their path: (eeschema draw-wires/loaded-css,
pcbnew draw-lines/loaded-css, pcbnew-move, dark-mode, eeschema-crosshair,
aui-resize) re-add path:'test-results/<name>' so they SAVE and still feed the
in-test pixel-diff analysis (17).
- Dropped animating/loading captures re-added via stableShot / raw screenshot:
timer-03/05/06, dialogs-timer-running/started, wxgrid-02/03, 01-loading (7).
Delete 5 obsolete wizard baselines (eeschema/gerbview/pl_editor/symbol_editor/
generic -wizard-00-initial): every app is now seeded to bypass the first-run
setup wizard (that was the point of removing the wizard for/if loop), so those
states are no longer produced — pl_editor.spec asserts "no wizard appears"
instead.
Net: every committed baseline has a producer again (0 "removed"); determinism
guard green; affected specs pass (e2e 40, kicad 12) and emit the restored PNGs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 11:35:39 +02:00
// Loading-state capture: raw + immediate (no settle) on purpose — this shot documents the
// app mid-load, before the canvas is up, so it must NOT wait for the render to finish.
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
await page . screenshot ( { path : shotPath ( page , '01-loading.png' ) , fullPage : true } ) ;
test(screenshots): restore screenshot-neutrality after the determinism refactor
The determinism conversion silently dropped/renamed screenshots vs the committed
baselines, so CI flagged ~35 baselines as "removed". Restore them:
- pl_editor-*: re-add the "pl_editor-" prefix the conversion dropped (5).
- Buffer screenshots that lost their path: (eeschema draw-wires/loaded-css,
pcbnew draw-lines/loaded-css, pcbnew-move, dark-mode, eeschema-crosshair,
aui-resize) re-add path:'test-results/<name>' so they SAVE and still feed the
in-test pixel-diff analysis (17).
- Dropped animating/loading captures re-added via stableShot / raw screenshot:
timer-03/05/06, dialogs-timer-running/started, wxgrid-02/03, 01-loading (7).
Delete 5 obsolete wizard baselines (eeschema/gerbview/pl_editor/symbol_editor/
generic -wizard-00-initial): every app is now seeded to bypass the first-run
setup wizard (that was the point of removing the wizard for/if loop), so those
states are no longer produced — pl_editor.spec asserts "no wizard appears"
instead.
Net: every committed baseline has a producer again (0 "removed"); determinism
guard green; affected specs pass (e2e 40, kicad 12) and emit the restored PNGs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 11:35:39 +02:00
2026-07-07 10:50:24 +02:00
// Deterministic app readiness: canvas visible + wx registry populated.
// Fails loudly (replaces the try/catch waitForSelector + 1s settle).
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
const canvas = page . locator ( '#canvas' ) ;
const box = await canvas . boundingBox ( ) ;
if ( ! box ) throw new Error ( 'Canvas not found' ) ;
// Screenshot after load
2026-07-07 10:50:24 +02:00
await stableShot ( page , '03-after-load.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
// === TAB 1: Controls ===
2025-12-30 14:17:51 +01:00
// Click Controls tab using element registry
await clickTab ( page , 'Controls' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click "Click Me" button using element registry
await clickByLabel ( page , 'Click Me' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click "Toggle" button using element registry
await clickByLabel ( page , 'Toggle' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click checkbox "Enable feature" using element registry
await clickByLabel ( page , 'Enable feature' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click radio button "Option B" using element registry
await clickByLabel ( page , 'Option B' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click radio button "Option C" using element registry
await clickByLabel ( page , 'Option C' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Interact with slider using element tracking
await clickSlider ( page ) ;
// Drag slider to 80% position
await dragSliderTo ( page , 0.8 ) ;
2025-11-28 14:24:53 +01:00
2026-07-07 10:50:24 +02:00
await stableShot ( page , '04-controls-tab.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
// === TAB 2: Text Input ===
2025-12-30 14:17:51 +01:00
// Click Text Input tab using element registry
await clickTab ( page , 'Text Input' ) ;
2025-11-28 14:24:53 +01:00
2026-07-07 10:50:24 +02:00
await stableShot ( page , '05-text-input-tab.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click in single-line text field using element tracking
const singleLine = await findSingleLineTextCtrl ( page ) ;
expect ( singleLine , 'Single-line text control should be tracked' ) . not . toBeNull ( ) ;
await page . mouse . click ( singleLine ! . centerX , singleLine ! . centerY ) ;
2026-07-07 10:50:24 +02:00
await page . waitForTimeout ( 200 ) ; // eslint-disable-line -- documented interaction dwell: let focus land before typing (no registry/console observable for focus commit)
2025-11-28 14:24:53 +01:00
await page . keyboard . type ( 'Hello World' ) ;
2025-12-30 14:17:51 +01:00
// Click multiline text area using element tracking
const multiLine = await findMultiLineTextCtrl ( page ) ;
expect ( multiLine , 'Multi-line text control should be tracked' ) . not . toBeNull ( ) ;
await page . mouse . click ( multiLine ! . centerX , multiLine ! . centerY ) ;
2026-07-07 10:50:24 +02:00
await page . waitForTimeout ( 200 ) ; // eslint-disable-line -- documented interaction dwell: let focus land before typing (no registry/console observable for focus commit)
2025-11-28 14:24:53 +01:00
await page . keyboard . type ( 'Line 1\nLine 2\nLine 3' ) ;
2026-07-07 10:50:24 +02:00
await stableShot ( page , '06-text-input-typed.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
// === TAB 3: Drawing ===
2025-12-30 14:17:51 +01:00
// Click Drawing tab using element registry
await clickTab ( page , 'Drawing' ) ;
2025-11-28 14:24:53 +01:00
2026-07-07 10:50:24 +02:00
await stableShot ( page , '07-drawing-tab.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
// Draw on the canvas - multiple strokes
for ( let i = 0 ; i < 3 ; i ++ ) {
const startX = 100 + i * 50 ;
const startY = 100 + i * 30 ;
await page . mouse . move ( box . x + startX , box . y + startY ) ;
await page . mouse . down ( ) ;
await page . mouse . move ( box . x + startX + 100 , box . y + startY + 50 , { steps : 10 } ) ;
await page . mouse . up ( ) ;
}
2026-07-07 10:50:24 +02:00
await stableShot ( page , '08-drawing-done.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
// === TAB 4: Lists ===
2025-12-30 14:17:51 +01:00
// Click Lists tab using element registry
await clickTab ( page , 'Lists' ) ;
2025-11-28 14:24:53 +01:00
2026-07-07 10:50:24 +02:00
await stableShot ( page , '09-lists-tab.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click on listbox items using element registry (wxListBox uses listboxitem type)
2025-11-28 14:24:53 +01:00
for ( let i = 0 ; i < 5 ; i ++ ) {
2025-12-30 14:17:51 +01:00
await clickListboxItemByIndex ( page , i ) ;
2025-11-28 14:24:53 +01:00
}
2026-07-07 10:50:24 +02:00
await stableShot ( page , '10-lists-clicked.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
2026-06-12 10:18:16 +02:00
// Test the wxChoice dropdown. It renders as a native <select> (the
// browser owns the popup, so it cannot be driven by coordinate
// clicks) — drive it through the DOM and verify the value sticks.
const choice = page
. locator ( 'select:not([multiple])' )
. filter ( { has : page.locator ( 'option' , { hasText : 'Red' } ) } )
. first ( ) ;
expect ( await choice . count ( ) , 'Choice <select> should exist' ) . toBe ( 1 ) ;
2025-11-29 10:08:42 +01:00
2026-06-12 10:18:16 +02:00
await choice . selectOption ( { label : 'Green' } ) ;
2025-11-29 10:08:42 +01:00
2026-07-07 10:50:24 +02:00
await stableShot ( page , '10b-choice-selected.png' , { fullPage : true } ) ;
2025-11-29 10:08:42 +01:00
2026-06-12 10:18:16 +02:00
expect ( await choice . inputValue ( ) , 'Choice should now be Green' ) . toBe ( 'Green' ) ;
2025-11-29 10:08:42 +01:00
2025-11-28 14:24:53 +01:00
// === Menu interaction ===
2025-12-30 14:17:51 +01:00
// Click File menu using element registry
2026-01-01 20:18:19 +01:00
await clickMenuBarItem ( page , 'File' ) ;
2026-07-07 10:50:24 +02:00
await stableShot ( page , '11-file-menu.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Close menu with Escape
await page . keyboard . press ( 'Escape' ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Click Help menu using element registry
2026-01-01 20:18:19 +01:00
await clickMenuBarItem ( page , 'Help' ) ;
2026-07-07 10:50:24 +02:00
await stableShot ( page , '12-help-menu.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
2025-12-30 14:17:51 +01:00
// Close menu with Escape
await page . keyboard . press ( 'Escape' ) ;
2025-11-28 14:24:53 +01:00
// === Rapid interactions ===
// Click rapidly on various areas
for ( let i = 0 ; i < 30 ; i ++ ) {
const x = 50 + ( i * 37 ) % 400 ;
const y = 50 + ( i * 23 ) % 350 ;
await page . mouse . click ( box . x + x , box . y + y ) ;
}
2026-07-07 10:50:24 +02:00
await stableShot ( page , '13-final.png' , { fullPage : true } ) ;
2025-11-28 14:24:53 +01:00
} ) ;
} ) ;
test . describe ( 'wxWidgets WASM - Loading' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'app loads without JavaScript errors' , async ( { page , testLogger } ) = > {
2025-11-28 13:10:05 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 13:10:05 +01:00
// Filter out known non-critical errors
2025-11-29 20:54:54 +01:00
const criticalErrors = testLogger . errors . filter ( e = >
2025-11-28 14:24:53 +01:00
! e . includes ( 'SharedArrayBuffer' ) &&
! e . includes ( 'cross-origin' ) &&
! isKnownWarning ( e )
2025-11-28 13:10:05 +01:00
) ;
expect ( criticalErrors ) . toHaveLength ( 0 ) ;
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'canvas element is rendered with dimensions' , async ( { page , testLogger } ) = > {
2025-11-28 13:10:05 +01:00
await page . goto ( '/minimal_test.html' ) ;
2025-11-28 14:24:53 +01:00
const canvas = page . locator ( MAIN_CANVAS ) ;
2025-11-28 13:10:05 +01:00
await expect ( canvas ) . toBeVisible ( { timeout : 30000 } ) ;
const box = await canvas . boundingBox ( ) ;
expect ( box ) . not . toBeNull ( ) ;
expect ( box ? . width ) . toBeGreaterThan ( 0 ) ;
expect ( box ? . height ) . toBeGreaterThan ( 0 ) ;
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'loading progress completes' , async ( { page , testLogger } ) = > {
2025-11-28 13:10:05 +01:00
await page . goto ( '/minimal_test.html' ) ;
await page . waitForFunction ( ( ) = > {
const status = document . getElementById ( 'progress-text' ) ;
if ( ! status ) return true ;
const text = status . textContent || '' ;
return text . toLowerCase ( ) . includes ( 'complete' ) ||
text === '' ||
status . style . display === 'none' ;
} , { timeout : 30000 } ) ;
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'WASM module initializes successfully' , async ( { page , testLogger } ) = > {
2025-11-28 13:10:05 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 13:10:05 +01:00
const moduleExists = await page . evaluate ( ( ) = > {
return typeof ( window as any ) . Module !== 'undefined' ;
} ) ;
expect ( moduleExists ) . toBe ( true ) ;
} ) ;
2025-11-28 14:24:53 +01:00
2025-11-29 20:54:54 +01:00
test ( 'application started event is logged' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
// Deterministically wait for the startup event to be logged (same assertion,
// no blind settle).
await expect
. poll ( ( ) = > testLogger . consoleLogs . some ( e = > e . includes ( 'Application started' ) ) , {
message : 'Application started event should be logged'
} )
. toBe ( true ) ;
2025-11-28 14:24:53 +01:00
} ) ;
} ) ;
test . describe ( 'wxWidgets WASM - Canvas Interaction' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'canvas receives click events' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
const events = captureEvents ( page ) ;
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
// Click on the canvas (somewhere in the middle)
await clickCanvas ( page , 320 , 240 ) ;
// Should have received the startup event at minimum
// Additional click events may or may not be logged depending on what's clicked
2026-07-07 10:50:24 +02:00
await expect
. poll ( ( ) = > events . length , { message : 'At least one [EVENT] should be logged' } )
. toBeGreaterThanOrEqual ( 1 ) ;
2025-11-28 14:24:53 +01:00
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'canvas receives keyboard events' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
// Focus the canvas
const canvas = page . locator ( MAIN_CANVAS ) ;
await canvas . focus ( ) ;
// Type some text
await page . keyboard . type ( 'test' ) ;
2026-07-07 10:50:24 +02:00
await page . waitForTimeout ( 300 ) ; // eslint-disable-line -- documented interaction dwell: keystroke commit; test asserts no errors surface during keyboard input, no observable to poll
2025-11-28 14:24:53 +01:00
// The test passes if no errors occur during keyboard input
} ) ;
} ) ;
test . describe ( 'wxWidgets WASM - Mouse Drawing' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'mouse drag creates drawing stroke' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
const events = captureEvents ( page ) ;
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
// First, switch to the Drawing tab (Tab 3)
2025-12-30 14:17:51 +01:00
// Use element registry to click the tab reliably
await clickTab ( page , 'Drawing' ) ;
2025-11-28 14:24:53 +01:00
2026-07-07 10:50:24 +02:00
// Deterministically confirm we're on the Drawing tab before drawing
// (replaces the blind 500ms sleep + the defensive `if (onDrawingTab)`
// guard that used to silently skip the assertion).
await expect
. poll ( ( ) = > events . some ( e = > e . includes ( 'Tab changed to: Drawing' ) ) , {
message : 'Should switch to the Drawing tab'
} )
. toBe ( true ) ;
// Now do a mouse drag in the drawing area
await dragCanvas ( page , 100 , 150 , 300 , 250 ) ;
// Should have mouse down and mouse up events
await expect
. poll (
( ) = >
events . some ( e = > e . includes ( 'Mouse down' ) ) ||
events . some ( e = > e . includes ( 'Mouse up' ) ) ,
{ message : 'Drag should produce a mouse down or mouse up event' }
)
. toBe ( true ) ;
2025-11-28 14:24:53 +01:00
} ) ;
} ) ;
test . describe ( 'wxWidgets WASM - Event Logging' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'events are logged to console with [EVENT] prefix' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
const events = captureEvents ( page ) ;
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
// Should have at least the startup event
2026-07-07 10:50:24 +02:00
await expect
. poll ( ( ) = > events . length , { message : 'Startup event should be logged' } )
. toBeGreaterThan ( 0 ) ;
2025-11-28 14:24:53 +01:00
expect ( events [ 0 ] ) . toContain ( 'Application started' ) ;
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'multiple interactions produce multiple log entries' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
const events = captureEvents ( page ) ;
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
const initialCount = events . length ;
// Perform multiple clicks
await clickCanvas ( page , 100 , 100 ) ;
await clickCanvas ( page , 200 , 100 ) ;
await clickCanvas ( page , 300 , 100 ) ;
// Should have more events now
2026-07-07 10:50:24 +02:00
await expect
. poll ( ( ) = > events . length , { message : 'Interactions should not lose log entries' } )
. toBeGreaterThanOrEqual ( initialCount ) ;
2025-11-28 14:24:53 +01:00
} ) ;
} ) ;
test . describe ( 'wxWidgets WASM - Visual Rendering' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'frame renders with visible content' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
// Take a screenshot for visual verification
const screenshot = await page . screenshot ( ) ;
expect ( screenshot . length ) . toBeGreaterThan ( 0 ) ;
// The canvas should have more than just a blank color
// (This is a basic check - real visual testing would use image comparison)
const canvas = page . locator ( MAIN_CANVAS ) ;
const box = await canvas . boundingBox ( ) ;
expect ( box ? . width ) . toBeGreaterThan ( 100 ) ;
expect ( box ? . height ) . toBeGreaterThan ( 100 ) ;
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'window has reasonable dimensions' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
const canvas = page . locator ( MAIN_CANVAS ) ;
const box = await canvas . boundingBox ( ) ;
// The test frame is set to 640x480 in the C++ code
// Canvas might be slightly different but should be close
expect ( box ? . width ) . toBeGreaterThanOrEqual ( 400 ) ;
expect ( box ? . height ) . toBeGreaterThanOrEqual ( 300 ) ;
} ) ;
} ) ;
test . describe ( 'wxWidgets WASM - Stability' , ( ) = > {
2025-11-29 20:54:54 +01:00
test ( 'app remains stable after multiple interactions' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
// Perform many rapid interactions
for ( let i = 0 ; i < 20 ; i ++ ) {
await clickCanvas ( page , 100 + i * 20 , 100 + i * 10 ) ;
}
// App should still be responsive
const canvas = page . locator ( MAIN_CANVAS ) ;
await expect ( canvas ) . toBeVisible ( ) ;
// No JavaScript errors should have occurred
2025-11-29 20:54:54 +01:00
const criticalErrors = testLogger . errors . filter ( e = >
2025-11-28 14:24:53 +01:00
! e . includes ( 'SharedArrayBuffer' ) &&
! e . includes ( 'cross-origin' ) &&
! isKnownWarning ( e )
) ;
expect ( criticalErrors ) . toHaveLength ( 0 ) ;
} ) ;
2025-11-29 20:54:54 +01:00
test ( 'app handles rapid mouse movements' , async ( { page , testLogger } ) = > {
2025-11-28 14:24:53 +01:00
await page . goto ( '/minimal_test.html' ) ;
2026-07-07 10:50:24 +02:00
await waitForWxApp ( page ) ;
2025-11-28 14:24:53 +01:00
const canvas = page . locator ( MAIN_CANVAS ) ;
const box = await canvas . boundingBox ( ) ;
if ( ! box ) throw new Error ( 'Canvas not found' ) ;
// Rapid mouse movements
for ( let i = 0 ; i < 50 ; i ++ ) {
const x = box . x + 100 + Math . sin ( i * 0.3 ) * 100 ;
const y = box . y + 200 + Math . cos ( i * 0.3 ) * 100 ;
await page . mouse . move ( x , y ) ;
}
// App should still be responsive
await expect ( page . locator ( MAIN_CANVAS ) ) . toBeVisible ( ) ;
} ) ;
2025-11-28 13:10:05 +01:00
} ) ;