import { act, render, waitFor } from "@testing-library/react"; import createGlobe from "cobe"; import { ControlRoomGlobe } from "./ControlRoomGlobe"; const mockDestroy = jest.fn(); jest.mock("cobe", () => ({ __esModule: true, default: jest.fn(() => ({ destroy: mockDestroy })), })); jest.mock("motion/react", () => ({ useMotionValue: (initial: number) => ({ get: () => initial, set: jest.fn(), }), useSpring: (value: unknown) => value, })); let intersectionCallback: IntersectionObserverCallback | undefined; let resizeCallback: ResizeObserverCallback | undefined; let measuredWidth = 400; function setDocumentHidden(hidden: boolean) { Object.defineProperty(document, "hidden", { configurable: true, value: hidden, }); } function setGlobeIntersects(isIntersecting: boolean) { const entry = { isIntersecting } as IntersectionObserverEntry; act(() => intersectionCallback?.([entry], {} as IntersectionObserver)); } function renderCallback() { expect(createGlobe).toHaveBeenCalledTimes(1); const call = jest.mocked(createGlobe).mock.calls[0]; if (!call) throw new Error("Globe was not initialized"); return call[1].onRender; } describe("ControlRoomGlobe lifecycle", () => { beforeEach(() => { jest.clearAllMocks(); setDocumentHidden(false); intersectionCallback = undefined; resizeCallback = undefined; measuredWidth = 400; jest .spyOn(HTMLElement.prototype, "offsetWidth", "get") .mockImplementation(() => measuredWidth); jest .spyOn(HTMLCanvasElement.prototype, "getContext") .mockReturnValue({} as unknown as GPUCanvasContext); class MockIntersectionObserver { readonly root = null; readonly rootMargin = ""; readonly thresholds = []; constructor(callback: IntersectionObserverCallback) { intersectionCallback = callback; } disconnect = jest.fn(); observe = jest.fn(); takeRecords = () => []; unobserve = jest.fn(); } global.IntersectionObserver = MockIntersectionObserver as unknown as typeof IntersectionObserver; class MockResizeObserver { constructor(callback: ResizeObserverCallback) { resizeCallback = callback; } disconnect = jest.fn(); observe = jest.fn(); unobserve = jest.fn(); } global.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver; }); afterEach(() => { jest.restoreAllMocks(); }); it("pauses for a hidden tab without losing the intersection flag", async () => { render(); await waitFor(() => expect(createGlobe).toHaveBeenCalledTimes(1)); const renderFrame = renderCallback(); const state = { phi: 0, theta: 0, width: 0, height: 0 }; setGlobeIntersects(true); renderFrame(state); expect(state.phi).not.toBe(0); setDocumentHidden(true); const pausedPhi = state.phi; renderFrame(state); expect(state.phi).toBe(pausedPhi); setDocumentHidden(false); renderFrame(state); expect(state.phi).not.toBe(pausedPhi); setGlobeIntersects(false); const offscreenPhi = state.phi; setDocumentHidden(true); setDocumentHidden(false); renderFrame(state); expect(state.phi).toBe(offscreenPhi); }); it("keeps one Cobe instance while ResizeObserver updates render dimensions", async () => { render(); await waitFor(() => expect(createGlobe).toHaveBeenCalledTimes(1)); measuredWidth = 520; const entry = { contentRect: { width: measuredWidth }, } as ResizeObserverEntry; act(() => resizeCallback?.([entry], {} as ResizeObserver)); setGlobeIntersects(true); const state = { phi: 0, theta: 0, width: 0, height: 0 }; renderCallback()(state); expect(createGlobe).toHaveBeenCalledTimes(1); expect(state.width).toBe(1040); expect(state.height).toBe(1040); }); it("keeps the reduced-motion globe static while tracking dimensions", async () => { render(); await waitFor(() => expect(createGlobe).toHaveBeenCalled()); setGlobeIntersects(true); const state = { phi: 0, theta: 0, width: 0, height: 0 }; renderCallback()(state); expect(state).toEqual({ phi: 0, theta: 0, width: 800, height: 800 }); }); });