mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-03 12:27:55 +08:00
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:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user