test: fix error-boundary-28 — undeclared errors ref and over-broad assertion

- Replace `onerror={(e) => errors.push(e.message)}` with `onerror={() => {}}`
  in main.svelte; `errors` was an undeclared global (undefined at runtime),
  disconnected from the dead `const errors = []` in _config.js.
- Remove the dead `const errors = []` declaration and its stale comment.
- Wrap the unmount flushSync in try/catch: when the boundary is torn down
  together with the component all ancestor boundary effects carry DESTROYING,
  so invoke_error_boundary finds no live handler and re-throws synchronously.
  The real TypeError (null.value) escaping is acceptable.
- Tighten the assertion to match only "reading 'error'" — the specific
  null-dereference crash (#18485) this test guards against — rather than
  the overly broad "Cannot read properties of null" which also matched
  the original application error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/18486/head
Rupankar Dutta 2 weeks ago
parent 7a40b0bee5
commit c3fc325dba

@ -10,10 +10,6 @@ export default test({
mode: ['client'],
test({ assert, target, window }) {
const errors = /** @type {string[]} */ ([]);
// Expose the errors array to the component template.
// We check window.uncaughtErrors to verify no unhandled throw escapes.
const originalOnError = window.onerror;
/** @type {string[]} */
const uncaught = [];
@ -28,18 +24,36 @@ export default test({
});
// Step 2: unmount — teardown reads the dirty derived, which throws.
flushSync(() => {
target.querySelector('#unmount')?.click();
});
// The boundary is being torn down together with the component, so the
// real TypeError may escape synchronously from flushSync (no live
// boundary ancestor can catch it). That is acceptable. What must NOT
// happen is a secondary null-dereference crash about a destroyed
// boundary's error handler ("Cannot read properties of null (reading 'error')").
/** @type {unknown} */
let teardownError = null;
try {
flushSync(() => {
target.querySelector('#unmount')?.click();
});
} catch (e) {
teardownError = e;
}
// The real error about null.value is fine — it's the expected throw.
// The null-dereference about effect.b.error is the regression we guard.
const nullDerefMsg = "Cannot read properties of null (reading 'error')";
if (teardownError) {
assert.ok(
!String(/** @type {any} */ (teardownError).message).includes(nullDerefMsg),
'no null-dereference crash about effect.b should escape synchronously'
);
}
// The boundary was being torn down along with the component, so the
// onerror handler itself may not fire (the boundary is already gone),
// but the critical thing is that no TypeError about null effect.b
// escapes to the global error handler.
assert.equal(
uncaught.filter((m) => m.includes("Cannot read properties of null")).length,
uncaught.filter((m) => m.includes(nullDerefMsg)).length,
0,
'no null-dereference crash should escape'
'no null-dereference crash about effect.b should escape to window.onerror'
);
window.onerror = originalOnError;

@ -14,7 +14,7 @@
<button id="unmount" onclick={() => (mounted = false)}>unmount</button>
{#if mounted}
<svelte:boundary onerror={(e) => errors.push(e.message)}>
<svelte:boundary onerror={() => {}}>
<Trigger getValue={() => appContext} />
</svelte:boundary>
{/if}

Loading…
Cancel
Save