| 1 | // Run: tsx src/__tests__/config-load-warnings.test.ts |
| 2 | |
| 3 | import { JSDOM } from "jsdom"; |
| 4 | import React, { act } from "react"; |
| 5 | import { createRoot } from "react-dom/client"; |
| 6 | import type { AppBindings } from "../lib/bridge"; |
| 7 | import { |
| 8 | configLoadWarningsKey, |
| 9 | normalizeConfigLoadWarnings, |
| 10 | normalizeConfigLoadWarningsEvent, |
| 11 | normalizeConfigLoadWarningsRevision, |
| 12 | subscribeConfigLoadWarnings, |
| 13 | useConfigLoadWarnings, |
| 14 | } from "../lib/useConfigLoadWarnings"; |
| 15 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 16 | |
| 17 | let passed = 0; |
| 18 | let failed = 0; |
| 19 | |
| 20 | function equal(actual: unknown, expected: unknown, label: string) { |
| 21 | if (JSON.stringify(actual) === JSON.stringify(expected)) { |
| 22 | process.stdout.write(` PASS ${label}\n`); |
| 23 | passed += 1; |
| 24 | } else { |
| 25 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`); |
| 26 | failed += 1; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | console.log("\nconfig load warnings"); |
| 31 | |
| 32 | equal( |
| 33 | normalizeConfigLoadWarnings([" warning one ", "", 42, "warning one", "warning two"]), |
| 34 | ["warning one", "warning two"], |
| 35 | "bridge payload normalization keeps unique non-empty strings", |
| 36 | ); |
| 37 | equal(configLoadWarningsKey(["a", "b"]), '["a","b"]', "warning fingerprints are stable"); |
| 38 | equal(configLoadWarningsKey([]), "", "empty warning lists have no fingerprint"); |
| 39 | equal(normalizeConfigLoadWarningsRevision(7), 7, "safe event revisions are preserved"); |
| 40 | equal(normalizeConfigLoadWarningsRevision(Number.MAX_SAFE_INTEGER + 1), 0, "unsafe event revisions fall back safely"); |
| 41 | equal( |
| 42 | normalizeConfigLoadWarningsEvent({ warnings: [" current warning ", null, "current warning"], revision: 7 }), |
| 43 | { warnings: ["current warning"], revision: 7 }, |
| 44 | "versioned bridge event payloads are normalized", |
| 45 | ); |
| 46 | equal( |
| 47 | normalizeConfigLoadWarningsEvent(["versioned warning"], 8), |
| 48 | { warnings: ["versioned warning"], revision: 8 }, |
| 49 | "array event payloads accept an additive revision argument", |
| 50 | ); |
| 51 | |
| 52 | const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', { url: "http://localhost/" }); |
| 53 | (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; |
| 54 | globalThis.window = dom.window as unknown as Window & typeof globalThis; |
| 55 | globalThis.document = dom.window.document; |
| 56 | const desktopStub = installDesktopHostStub(({ main: { App: {} as AppBindings } }).main.App); |
| 57 | |
| 58 | function emitWarnings(payload: unknown, revision: number) { |
| 59 | desktopStub.emit("config:load-warnings", payload, revision); |
| 60 | } |
| 61 | |
| 62 | const received: Array<{ warnings: string[]; revision: number }> = []; |
| 63 | const stop = subscribeConfigLoadWarnings((snapshot) => received.push(snapshot)); |
| 64 | desktopStub.emit("config:load-warnings", [" current warning ", null, "current warning"], 7); |
| 65 | desktopStub.emit("config:load-warnings", [], 8); |
| 66 | equal(received, [{ warnings: ["current warning"], revision: 7 }], "runtime bridge forwards normalized non-empty warnings"); |
| 67 | stop(); |
| 68 | equal(desktopStub.events.get("config:load-warnings")?.size ?? 0, 0, "runtime warning subscription is disposable"); |
| 69 | |
| 70 | type WarningHook = ReturnType<typeof useConfigLoadWarnings>; |
| 71 | let warningHook: WarningHook | undefined; |
| 72 | function Probe() { |
| 73 | warningHook = useConfigLoadWarnings(); |
| 74 | return null; |
| 75 | } |
| 76 | |
| 77 | const rootElement = document.getElementById("root"); |
| 78 | if (!rootElement) throw new Error("missing test root"); |
| 79 | const root = createRoot(rootElement); |
| 80 | await act(async () => { root.render(React.createElement(Probe)); }); |
| 81 | |
| 82 | await act(async () => { emitWarnings(["runtime warning"], 2); }); |
| 83 | await act(async () => { warningHook?.applySnapshot([], 1); }); |
| 84 | equal(warningHook?.configLoadWarnings, ["runtime warning"], "stale startup snapshots cannot clear runtime warnings"); |
| 85 | |
| 86 | await act(async () => { warningHook?.dismiss(); }); |
| 87 | await act(async () => { emitWarnings(["runtime warning"], 3); }); |
| 88 | equal(warningHook?.configLoadWarnings, [], "dismissed warnings stay hidden across repeated session builds"); |
| 89 | |
| 90 | await act(async () => { warningHook?.reload([], 4); }); |
| 91 | await act(async () => { emitWarnings(["runtime warning"], 3); }); |
| 92 | equal(warningHook?.configLoadWarnings, [], "events started before a successful reload cannot revive stale warnings"); |
| 93 | |
| 94 | await act(async () => { emitWarnings(["runtime warning"], 5); }); |
| 95 | equal(warningHook?.configLoadWarnings, ["runtime warning"], "a newer recurrence can reuse the same warning fingerprint"); |
| 96 | await act(async () => { root.unmount(); }); |
| 97 | |
| 98 | if (failed > 0) { |
| 99 | process.stdout.write(`\n${failed} failed, ${passed} passed\n`); |
| 100 | process.exit(1); |
| 101 | } |
| 102 | process.stdout.write(`\n${passed} passed\n`); |
| 103 |