test(web): Phase 15 toggle hook failure-mode coverage (PR #1320)

lefarcen P2 on PR #1320 flagged that the PR body claimed safe
behavior for disabled localStorage, non-object JSON, and missing
CustomEvent shim, but the suite only covered corrupt JSON plus
happy-path storage events. Added four failure-mode tests so the
swallowed errors are not silently traded for a throw in a future
refactor:

1. Returns false on a stored JSON value that parses to an array
   (non-object). Catches a regression where the guard treats
   anything truthy as a config blob.
2. Returns false on a stored JSON value of literal 'null'.
   typeof null === 'object' in JS, so the guard has to check null
   explicitly; this test pins that check.
3. Returns false when localStorage.getItem throws (private mode /
   disabled storage / SecurityError). The hook must swallow and
   return false so the rest of the app keeps rendering.
4. setCritiqueTheaterEnabled still dispatches the same-tab
   CustomEvent when localStorage.setItem throws (quota exceeded /
   disabled storage). The dispatch path is the in-session
   broadcast that keeps every mounted hook coherent even when
   persistence is unavailable; verified by mounting two probes
   and asserting both flip after the setter is called with a
   throwing setItem.

10/10 vitest cases green (6 existing + 4 new).
This commit is contained in:
Nagendhra
2026-05-11 19:33:36 -04:00
parent 6094858548
commit affcdd27a6

View File

@@ -106,4 +106,98 @@ describe('useCritiqueTheaterEnabled (Phase 15.3)', () => {
});
expect(sink.enabled).toBe(true);
});
// ---------------------------------------------------------------------------
// Failure-mode coverage (lefarcen P2 on PR #1320). The hook + setter both
// intentionally swallow these errors in production so a private-mode browser
// or a stripped CustomEvent shim does not crash the React tree. The tests
// below pin that behavior so the swallow is not silently traded for a throw
// in a future refactor.
// ---------------------------------------------------------------------------
it('returns false and does not throw when stored JSON is a non-object value (array)', () => {
// `JSON.parse('[1,2,3]')` is a valid array, not an object. The hook must
// not treat that as a config blob; the `critiqueTheaterEnabled` lookup
// would fall through to `undefined` and the function should return false.
window.localStorage.setItem('open-design:config', '[1,2,3]');
const sink: { enabled?: boolean } = {};
render(<Probe sink={sink} />);
expect(sink.enabled).toBe(false);
});
it('returns false and does not throw when stored JSON parses to null', () => {
// `null` is JSON-valid and `typeof null === 'object'` in JS, so the
// guard has to check for null explicitly. If it did not, `null.critique...`
// would throw on read.
window.localStorage.setItem('open-design:config', 'null');
const sink: { enabled?: boolean } = {};
render(<Probe sink={sink} />);
expect(sink.enabled).toBe(false);
});
it('returns false when localStorage.getItem throws (private mode / disabled storage)', () => {
const original = window.localStorage.getItem;
const restore = () => {
Object.defineProperty(window.localStorage, 'getItem', {
configurable: true,
value: original,
});
};
try {
Object.defineProperty(window.localStorage, 'getItem', {
configurable: true,
value: () => {
throw new DOMException(
'storage disabled (synthetic)',
'SecurityError',
);
},
});
const sink: { enabled?: boolean } = {};
render(<Probe sink={sink} />);
// Hook swallows the throw; UI sees `false` and the rest of the app
// keeps running.
expect(sink.enabled).toBe(false);
} finally {
restore();
}
});
it('still emits the same-tab CustomEvent when localStorage.setItem throws (quota / disabled storage)', () => {
// setCritiqueTheaterEnabled writes localStorage first and then dispatches
// the same-tab event. If the write throws (quota exceeded, private mode),
// the production code falls through to the dispatch so every mounted
// hook in the session still updates even though the value cannot
// persist across reloads. Verify the dispatch still fires.
const original = window.localStorage.setItem;
const restore = () => {
Object.defineProperty(window.localStorage, 'setItem', {
configurable: true,
value: original,
});
};
try {
Object.defineProperty(window.localStorage, 'setItem', {
configurable: true,
value: () => {
throw new DOMException(
'quota exceeded (synthetic)',
'QuotaExceededError',
);
},
});
const sink: { enabled?: boolean } = {};
render(<Probe sink={sink} />);
expect(sink.enabled).toBe(false);
act(() => {
setCritiqueTheaterEnabled(true);
});
// The dispatch path is exercised even though localStorage rejected
// the write: every mounted hook updates from the in-session event.
expect(sink.enabled).toBe(true);
} finally {
restore();
}
});
});