From 52cf61133c3df059bca1afdaf5d6768a4643c864 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:31:49 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Added=20tests=20for=20useSquawk?= =?UTF-8?q?=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: johnstrand <11484777+johnstrand@users.noreply.github.com> --- src/__tests__/Squawk.proxy.test.ts | 7 +- src/__tests__/Squawk.useSquawk.test.ts | 133 +++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/Squawk.useSquawk.test.ts diff --git a/src/__tests__/Squawk.proxy.test.ts b/src/__tests__/Squawk.proxy.test.ts index 9841a78..cb7aacf 100644 --- a/src/__tests__/Squawk.proxy.test.ts +++ b/src/__tests__/Squawk.proxy.test.ts @@ -1,15 +1,16 @@ -import createStore from "../Squawk"; import * as React from "react"; +import createStore from "../Squawk"; + // Mock React hooks to simulate hook environment jest.mock("react", () => { const actualReact = jest.requireActual("react"); return { ...actualReact, - useState: jest.fn((init) => [typeof init === 'function' ? init() : init, jest.fn()]), + useState: jest.fn((init) => [typeof init === "function" ? init() : init, jest.fn()]), useRef: jest.fn((init) => ({ current: init })), useMemo: jest.fn((factory) => factory()), - useEffect: jest.fn(), + useEffect: jest.fn() }; }); diff --git a/src/__tests__/Squawk.useSquawk.test.ts b/src/__tests__/Squawk.useSquawk.test.ts new file mode 100644 index 0000000..e40f38a --- /dev/null +++ b/src/__tests__/Squawk.useSquawk.test.ts @@ -0,0 +1,133 @@ +import * as React from "react"; + +import createStore from "../Squawk"; + +// Mock variables to track state across hook calls +// eslint-disable-next-line immutable/no-let +let stateDispatchers: jest.Mock[] = []; +// eslint-disable-next-line immutable/no-let +let effectCallbacks: (() => (() => void) | void)[] = []; +// eslint-disable-next-line immutable/no-let +let unmountCallbacks: (() => void)[] = []; + +// Mock React hooks to simulate hook environment without external libraries +jest.mock("react", () => { + return { + useState: jest.fn((init) => { + const state = typeof init === "function" ? init() : init; + const dispatcher = jest.fn(); + stateDispatchers.push(dispatcher); + return [state, dispatcher]; + }), + useRef: jest.fn((init) => { + return { current: init }; + }), + useMemo: jest.fn((factory) => factory()), + useEffect: jest.fn((cb) => { + effectCallbacks.push(cb); + }) + }; +}); + +describe("useSquawk", () => { + beforeEach(() => { + stateDispatchers = []; + effectCallbacks = []; + unmountCallbacks = []; + jest.clearAllMocks(); + }); + + // Test setup helper to simulate component mount and return cleanups + const simulateMount = () => { + effectCallbacks.forEach((cb) => { + const cleanup = cb(); + if (cleanup) { + unmountCallbacks.push(cleanup); + } + }); + effectCallbacks = []; // clear after mounting + }; + + const simulateUnmount = () => { + unmountCallbacks.forEach((cb) => cb()); + unmountCallbacks = []; + }; + + it("should return the initial state", () => { + const store = createStore({ foo: "bar", baz: 123 }); + const state = store.useSquawk(); + expect(state.foo).toBe("bar"); + expect(state.baz).toBe(123); + }); + + it("should track contexts implicitly via proxy", () => { + const store = createStore({ foo: "bar", baz: 123 }); + const state = store.useSquawk(); + + // Access property to trigger proxy getter + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + state.foo; + + // Check that contexts ref has "foo" + const useRefCalls = (React.useRef as jest.Mock).mock.calls; + // useIfMounted has 2 useRef calls, useSquawk has 1 + const contextsRef = useRefCalls[2][0]; + + expect(contextsRef).toBeInstanceOf(Set); + expect(contextsRef.has("foo")).toBe(true); + expect(contextsRef.has("baz")).toBe(false); + }); + + it("should track contexts explicitly passed as arguments", () => { + const store = createStore({ foo: "bar", baz: 123 }); + store.useSquawk("baz"); + + const useRefCalls = (React.useRef as jest.Mock).mock.calls; + const contextsRef = useRefCalls[2][0]; + + expect(contextsRef).toBeInstanceOf(Set); + expect(contextsRef.has("baz")).toBe(true); + expect(contextsRef.has("foo")).toBe(false); + }); + + it("should receive updates when subscribed contexts change", () => { + const store = createStore({ foo: "bar", baz: 123 }); + const state = store.useSquawk(); + + // Access foo to subscribe to it + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + state.foo; + + // Simulate component mount to run useEffect and set up subscription + simulateMount(); + + // Trigger update + store.update({ foo: "newBar" }); + + // Check if dispatcher was called with updated state + expect(stateDispatchers.length).toBe(1); + expect(stateDispatchers[0]).toHaveBeenCalledWith(expect.objectContaining({ foo: "newBar", baz: 123 })); + }); + + it("should not receive updates when unsubscribed (component unmounts)", () => { + const store = createStore({ foo: "bar", baz: 123 }); + const state = store.useSquawk(); + + // Access foo to subscribe + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + state.foo; + + // Simulate mount then unmount + simulateMount(); + simulateUnmount(); + + // Clear dispatcher calls from any initial setup + stateDispatchers[0].mockClear(); + + // Trigger update + store.update({ foo: "newBar" }); + + // Dispatcher should not be called since component unmounted + expect(stateDispatchers[0]).not.toHaveBeenCalled(); + }); +});