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
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
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.19–21) 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,
|
||
});
|
||
});
|