test(core): make the unsubscribe test actually verify unsubscription

Copilot review round 3.

"stops listening on unmount" asserted that the rendered value stopped
changing after unmount, which proves nothing: React drops a store
notification for an unmounted component whether or not the listener is
still attached, so the test passed with the leak it existed to catch.

Spy the store's add/removeEventListener and assert the exact function
attached is the one detached. Verified it discriminates by deleting the
cleanup, watching this test fail, and restoring.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-29 02:55:27 -04:00
commit b088b94019

View file

@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from "vitest"; import { describe, it, expect, beforeEach, vi } from "vitest";
import { act, renderHook } from "@testing-library/react"; import { act, renderHook } from "@testing-library/react";
import { TypedEventTarget } from "@inspector/core/mcp/typedEventTarget"; import { TypedEventTarget } from "@inspector/core/mcp/typedEventTarget";
import { useStoreSnapshot } from "@inspector/core/react/useStoreSnapshot"; import { useStoreSnapshot } from "@inspector/core/react/useStoreSnapshot";
@ -257,16 +257,25 @@ describe("useStoreSnapshot", () => {
}); });
it("stops listening on unmount", () => { it("stops listening on unmount", () => {
const { result, unmount } = renderHook(() => // Assert on the listener, not on the rendered value. Watching
// `result.current` stay put after unmount proves nothing: React drops a
// store notification for an unmounted component either way, so that test
// passes with the listener still attached — which is the leak it was
// supposed to catch.
const addSpy = vi.spyOn(store, "addEventListener");
const removeSpy = vi.spyOn(store, "removeEventListener");
const { unmount } = renderHook(() =>
useStoreSnapshot(store, "itemsChange", readItems, NO_ITEMS), useStoreSnapshot(store, "itemsChange", readItems, NO_ITEMS),
); );
const before = result.current; expect(addSpy).toHaveBeenCalledWith("itemsChange", expect.any(Function));
const subscribed = addSpy.mock.calls[0]?.[1];
unmount(); unmount();
// No act(): after unmount there is nothing to re-render, and a listener // The exact function that was attached is the one detached — a cleanup
// still attached here would be a leak rather than an update. // that removed some other listener, or none, fails here.
store.add("a"); expect(removeSpy).toHaveBeenCalledWith("itemsChange", subscribed);
expect(result.current).toBe(before);
}); });
it("unmounts cleanly with no store to unsubscribe from", () => { it("unmounts cleanly with no store to unsubscribe from", () => {