From f7282afe921b8ccd3fe3163bf33cd71c2722110c Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 5 Jun 2026 21:52:10 +0800 Subject: [PATCH] test: add visibility recovery tests for connection monitor --- tests/unit/web/visibility-recovery.test.ts | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/unit/web/visibility-recovery.test.ts diff --git a/tests/unit/web/visibility-recovery.test.ts b/tests/unit/web/visibility-recovery.test.ts new file mode 100644 index 00000000..43439c25 --- /dev/null +++ b/tests/unit/web/visibility-recovery.test.ts @@ -0,0 +1,101 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const fetchMock = vi.fn(); +vi.stubGlobal("fetch", fetchMock); + +vi.stubGlobal("localStorage", { + getItem: vi.fn(() => null), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + get length() { + return 0; + }, + key: vi.fn(() => null), +}); + +function okHealth() { + return Promise.resolve(new Response(JSON.stringify({ status: "healthy" }), { status: 200 })); +} + +function simulateVisible() { + Object.defineProperty(document, "visibilityState", { + value: "visible", + writable: true, + configurable: true, + }); + document.dispatchEvent(new Event("visibilitychange")); +} + +function simulateHidden() { + Object.defineProperty(document, "visibilityState", { + value: "hidden", + writable: true, + configurable: true, + }); + document.dispatchEvent(new Event("visibilitychange")); +} + +describe("visibility recovery", () => { + beforeEach(() => { + vi.useFakeTimers(); + fetchMock.mockReset(); + fetchMock.mockImplementation(() => okHealth()); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("connection monitor", () => { + it("calls checkHealth when tab becomes visible", async () => { + const { useConnectionStore } = await import("@/stores/connection-store"); + const { useConnectionMonitor } = await import("@/hooks/use-connection-monitor"); + const { renderHook } = await import("@testing-library/react"); + + useConnectionStore.setState({ + status: "connected", + failedSince: null, + lastHealthCheck: null, + }); + fetchMock.mockReset(); + fetchMock.mockImplementation(() => okHealth()); + + const { unmount } = renderHook(() => useConnectionMonitor()); + + await vi.advanceTimersByTimeAsync(0); + const initialCalls = fetchMock.mock.calls.length; + + simulateVisible(); + await vi.advanceTimersByTimeAsync(0); + + expect(fetchMock.mock.calls.length).toBeGreaterThan(initialCalls); + unmount(); + }); + + it("does not call checkHealth when tab becomes hidden", async () => { + const { useConnectionStore } = await import("@/stores/connection-store"); + const { useConnectionMonitor } = await import("@/hooks/use-connection-monitor"); + const { renderHook } = await import("@testing-library/react"); + + useConnectionStore.setState({ + status: "connected", + failedSince: null, + lastHealthCheck: null, + }); + fetchMock.mockReset(); + fetchMock.mockImplementation(() => okHealth()); + + const { unmount } = renderHook(() => useConnectionMonitor()); + await vi.advanceTimersByTimeAsync(0); + const callsAfterMount = fetchMock.mock.calls.length; + + simulateHidden(); + await vi.advanceTimersByTimeAsync(0); + + expect(fetchMock.mock.calls.length).toBe(callsAfterMount); + unmount(); + }); + }); +});