pcbjam/tests/web/fatal-overlay.spec.ts
Gergő Törcsvári eff5befd5d
fix(editor): DOM-level blue-screen floor — React can no longer white-screen a crash
v0.1.22's WasmErrorBoundary was still not enough: a commit-phase throw in
WasmTool's OWN effects unmounts the root, and no boundary below it helps.
fatal-screen.ts is the floor: plain-DOM blue screen with its own mirrored
log ring (append feeds recordFatalLog), installed at module import in
main.tsx — before and independent of React. It cooperates with the React
overlay: hidden while [data-testid="fatal-overlay"] exists, takes over via a
1Hz ensure-loop the moment it disappears. Fatal promotions also append the
asyncify flight-recorder dump so whichever screen survives carries the
targeting data.

fatal-overlay.spec.ts now also rips out the React root after the fatal and
asserts the DOM floor takes over with the mirrored [fatal] log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:07:09 +02:00

65 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '@playwright/test';
/**
* The blue screen of death must actually appear (WSOD hardening, async/16
* companion). Three prod crash reports in a row (v0.1.1921) ended in a WHITE
* page because the crash killed the very UI built to report it: the final
* trap landed inside a React effect, React unmounted the root, and the fatal
* overlay + console panel died with it. The fix is layered — window/worker
* error listeners promote a `fatal` state rendered OUTSIDE a WasmErrorBoundary
* that absorbs descendant render/effect throws — and this spec pins the
* user-visible contract:
*
* a terminal-looking uncaught error after boot ⇒ the blue fatal overlay is
* VISIBLE and the console log panel is OPEN (the log is the only account of
* what was loading; a collapsed console behind a mystery screen is useless).
*
* The throw is synthesized (a real wasm trap is not deterministically
* available), but it exercises the identical promotion path: uncaught error →
* terminal() match → promote → overlay + console.
*/
const SCOPE = 'default';
test('a terminal uncaught error raises the blue fatal overlay with the console open', async ({
page,
}) => {
test.setTimeout(300000); // one full pcbnew wasm boot
await page.goto(`/${SCOPE}/projects/demo/demo.kicad_pcb?user=fatal-probe`);
await expect(page.locator('#canvas')).toBeVisible({ timeout: 120000 });
await expect
.poll(() => page.title(), {
message: 'editor never reached the expected title',
timeout: 120000,
intervals: [1000],
})
.toMatch(/demo — PCB Editor/i);
// Synthesize the trap: an uncaught error whose message matches the
// terminal signature set (Firefox's bare wasm trap spelling included).
await page.evaluate(() => {
setTimeout(() => {
throw new Error('index out of bounds');
}, 0);
});
await expect(page.getByTestId('fatal-overlay')).toBeVisible({ timeout: 10000 });
// The console panel auto-opened and carries the promotion record.
await expect(page.locator('pre').filter({ hasText: '[fatal]' })).toBeVisible({
timeout: 10000,
});
// While React's overlay is alive, the DOM floor stays out of the way.
await expect(page.getByTestId('fatal-screen-dom')).toHaveCount(0);
// Simulate what prod actually did three releases in a row: React unmounts
// its whole root after the fatal. The DOM-level floor must take over —
// blue screen + the mirrored log — instead of a white page.
await page.evaluate(() => {
document.querySelector('[data-testid="fatal-overlay"]')?.closest('#root, body > div')?.remove();
});
await expect(page.getByTestId('fatal-screen-dom')).toBeVisible({ timeout: 5000 });
await expect(page.locator('#pcbjam-fatal-screen pre')).toContainText('[fatal]', {
timeout: 5000,
});
});