// hookHarness — each simulated semantic proved by its own test (#409). // // The tests that rely on the harness can only be as true as it is. Here it is // driven directly: a harness of its own, a fake React built over the real one, // and small components written for the test. import { describe, it, expect, vi, beforeEach } from "vitest"; import { readFileSync } from "node:fs"; import * as actualReact from "react"; import { createHookHarness, fakeReact, hookHarness, type HookHarness } from "./hookHarness"; let harness: HookHarness; let R: typeof actualReact; beforeEach(() => { harness = createHookHarness(); R = fakeReact(actualReact, harness); }); describe("hookHarness — the module itself", () => { it("imports React only as a type: a value import would load the mock it builds", () => { // The code only: the header comment shows the pattern of use. const source = readFileSync(new URL("./hookHarness.ts", import.meta.url), "utf8") .split("\n") .filter((line) => !line.trimStart().startsWith("//")) .join("\n"); const reactImports = source.split("\n").filter((line) => /from\s+["']react["']/.test(line)); expect(reactImports.length).toBeGreaterThan(0); for (const line of reactImports) expect(line).toMatch(/^import type /); expect(source).not.toMatch(/require\(["']react["']\)/); expect(source).not.toMatch(/import\(["']react["']\)/); }); it("shares one harness through its accessor", () => { expect(hookHarness()).toBe(hookHarness()); }); it("leaves the rest of React real", () => { expect(R.isValidElement).toBe(actualReact.isValidElement); expect(R.Fragment).toBe(actualReact.Fragment); expect(R.createElement).toBe(actualReact.createElement); }); }); describe("hookHarness — useState", () => { it("calls the initializer once, at the first render", () => { const init = vi.fn(() => 1); const Counter = () => R.useState(init)[0]; expect(harness.render(Counter)).toBe(1); expect(harness.render(Counter)).toBe(1); expect(init).toHaveBeenCalledTimes(1); }); it("keeps a stable setter, applies functional updates, and does not re-render", () => { let renders = 0; const Counter = () => { renders++; return R.useState(0); }; const [, set] = harness.render(Counter); set(5); set((n: number) => n + 1); expect(renders).toBe(1); const [value, again] = harness.render(Counter); expect(value).toBe(6); expect(again).toBe(set); }); it("keeps each slot apart, by call order", () => { const Pair = () => [R.useState("a")[0], R.useState("b")[0]]; expect(harness.render(Pair)).toEqual(["a", "b"]); }); it("reads a slot the test preset before the first render", () => { harness.slots[0] = "preset"; expect(harness.render(() => R.useState("init")[0])).toBe("preset"); }); }); describe("hookHarness — useReducer", () => { const reducer = (state: number, action: { by: number }) => state + action.by; it("applies the reducer, keeps a stable dispatch, and records every action in the spy", () => { const Sum = () => R.useReducer(reducer, 10); const [state, dispatch] = harness.render(Sum); expect(state).toBe(10); dispatch({ by: 2 }); dispatch({ by: 3 }); const [next, again] = harness.render(Sum); expect(next).toBe(15); expect(again).toBe(dispatch); expect(harness.dispatch.mock.calls.map((c) => c[0])).toEqual([{ by: 2 }, { by: 3 }]); }); it("calls the init function once", () => { const init = vi.fn((n: number) => n * 2); const Sum = () => R.useReducer(reducer, 4, init)[0]; expect(harness.render(Sum)).toBe(8); expect(harness.render(Sum)).toBe(8); expect(init).toHaveBeenCalledTimes(1); }); }); describe("hookHarness — useRef", () => { it("keeps one object from render to render", () => { const Ref = () => R.useRef(0); const first = harness.render(Ref); first.current = 7; const second = harness.render(Ref); expect(second).toBe(first); expect(second.current).toBe(7); }); }); describe("hookHarness — useMemo and useCallback", () => { it("useMemo keeps its value while the dependencies are equal, recomputes when one changes", () => { const compute = vi.fn((n: number) => ({ n })); let dep = 1; const Memo = () => R.useMemo(() => compute(dep), [dep]); const first = harness.render(Memo); expect(harness.render(Memo)).toBe(first); expect(compute).toHaveBeenCalledTimes(1); dep = 2; const second = harness.render(Memo); expect(second).not.toBe(first); expect(second).toEqual({ n: 2 }); expect(compute).toHaveBeenCalledTimes(2); }); it("compares the dependencies with Object.is", () => { let dep: unknown = NaN; const compute = vi.fn(() => ({})); const Memo = () => R.useMemo(compute, [dep]); harness.render(Memo); harness.render(Memo); expect(compute).toHaveBeenCalledTimes(1); dep = { same: true }; harness.render(Memo); dep = { same: true }; harness.render(Memo); expect(compute).toHaveBeenCalledTimes(3); }); it("recomputes when the dependency array changes length, even over an equal prefix", () => { // React warns on such an array; the harness still treats it as a change. // Shrinking keeps every remaining item equal, and growing by `undefined` // matches the missing item read as `undefined`: only the length differs. let deps: unknown[] = [1, 2]; const compute = vi.fn(() => ({})); const Memo = () => R.useMemo(compute, deps); harness.render(Memo); deps = [1]; harness.render(Memo); expect(compute).toHaveBeenCalledTimes(2); deps = [1, undefined]; harness.render(Memo); expect(compute).toHaveBeenCalledTimes(3); }); it("useMemo without dependencies recomputes at every render", () => { const compute = vi.fn(() => ({})); const Memo = () => R.useMemo(compute, undefined as unknown as []); harness.render(Memo); harness.render(Memo); expect(compute).toHaveBeenCalledTimes(2); }); it("useCallback keeps the first function while the dependencies are equal", () => { let dep = "a"; const Callback = () => R.useCallback(() => dep, [dep]); const first = harness.render(Callback); expect(harness.render(Callback)).toBe(first); dep = "b"; const second = harness.render(Callback); expect(second).not.toBe(first); expect(second()).toBe("b"); }); }); describe("hookHarness — useEffect", () => { const log: string[] = []; let dep = 1; const Effects = () => { const d = dep; R.useEffect(() => { log.push(`run ${d}`); return () => { log.push(`cleanup ${d}`); }; }, [d]); R.useEffect(() => { log.push("once"); }, []); }; beforeEach(() => { log.length = 0; dep = 1; }); it("captures the effects during the render, without running them", () => { harness.render(Effects); expect(log).toEqual([]); }); it("commit runs them all at the first commit, then only those whose dependencies changed", () => { harness.render(Effects); harness.commit(); expect(log).toEqual(["run 1", "once"]); harness.render(Effects); harness.commit(); expect(log).toEqual(["run 1", "once"]); dep = 2; harness.render(Effects); harness.commit(); // The cleanup of the previous run, with its own closure, before the new run. expect(log).toEqual(["run 1", "once", "cleanup 1", "run 2"]); }); it("commits a render once", () => { const fn = vi.fn(); const Always = () => R.useEffect(fn); harness.render(Always); harness.commit(); harness.commit(); expect(fn).toHaveBeenCalledTimes(1); harness.render(Always); harness.commit(); expect(fn).toHaveBeenCalledTimes(2); }); it("runs every cleanup before any effect of the same commit", () => { const order: string[] = []; let n = 0; const Two = () => { const k = n; R.useEffect(() => { order.push(`a${k}`); return () => { order.push(`~a${k}`); }; }, [k]); R.useEffect(() => { order.push(`b${k}`); return () => { order.push(`~b${k}`); }; }, [k]); }; harness.render(Two); harness.commit(); n = 1; harness.render(Two); harness.commit(); expect(order).toEqual(["a0", "b0", "~a0", "~b0", "a1", "b1"]); }); it("replayEffects plays StrictMode's double run: every cleanup, then every effect again", () => { harness.render(Effects); harness.commit(); harness.replayEffects(); expect(log).toEqual(["run 1", "once", "cleanup 1", "run 1", "once"]); }); it("unmount runs every cleanup, once", () => { harness.render(Effects); harness.commit(); harness.unmount(); harness.unmount(); expect(log).toEqual(["run 1", "once", "cleanup 1"]); }); it("clear starts a fresh mount", () => { harness.render(Effects); harness.commit(); harness.dispatch("x"); harness.clear(); expect(harness.slots).toEqual([]); expect(harness.dispatch).not.toHaveBeenCalled(); harness.render(Effects); harness.commit(); expect(log).toEqual(["run 1", "once", "run 1", "once"]); }); }); describe("hookHarness — hooks it does not simulate", () => { it.each(["useContext", "useLayoutEffect", "useSyncExternalStore", "useId", "useTransition", "use"])( "%s throws instead of reaching the real React", (name) => { const hook = (R as unknown as Record unknown>)[name]; expect(hook).toBeTypeOf("function"); expect(() => hook()).toThrow(`hookHarness: ${name} is not simulated`); }, ); it("on the default export too", () => { const fake = R as unknown as { default: Record unknown> }; expect(() => fake.default.useContext()).toThrow("hookHarness: useContext is not simulated"); expect(fake.default.useState).toBe((R as unknown as Record).useState); }); });