import { act, render } from "@testing-library/react"; import FramePulse from "./FramePulse"; const mockInvalidate = jest.fn(); jest.mock("@react-three/fiber", () => ({ useThree: (select: (state: { invalidate: () => void }) => unknown) => select({ invalidate: mockInvalidate }), })); beforeEach(() => { jest.useFakeTimers(); mockInvalidate.mockClear(); }); afterEach(() => { jest.useRealTimers(); }); const advance = (ms: number) => act(() => { jest.advanceTimersByTime(ms); }); describe("FramePulse", () => { it("asks for frames at about 30 per second while active, not 60", () => { render(); advance(1000); expect(mockInvalidate.mock.calls.length).toBeGreaterThan(20); expect(mockInvalidate.mock.calls.length).toBeLessThanOrEqual(34); }); it("draws nothing at all when it is not active (off screen)", () => { const { rerender } = render(); advance(1000); expect(mockInvalidate).not.toHaveBeenCalled(); rerender(); advance(200); const running = mockInvalidate.mock.calls.length; expect(running).toBeGreaterThan(0); rerender(); advance(1000); expect(mockInvalidate.mock.calls.length).toBe(running); }); }); describe("FramePulse: stops when nobody is looking", () => { it("stops drawing, and stops asking for animation frames, after 5 s without a cursor or scroll", () => { render(); advance(4000); const drawn = mockInvalidate.mock.calls.length; expect(drawn).toBeGreaterThan(80); // ainda batendo, ~30 por segundo advance(1500); // passou dos 5 s const stopped = mockInvalidate.mock.calls.length; // parou perto dos 5 s: no máximo 5 s de batidas a ~30 por segundo expect(stopped).toBeLessThanOrEqual(5 * 32); advance(3000); expect(mockInvalidate.mock.calls.length).toBe(stopped); // sem laço nenhum: nenhum quadro de animação pendente expect(jest.getTimerCount()).toBe(0); }); it.each([["pointermove"], ["scroll"], ["wheel"], ["keydown"]])( "wakes up again with %s and keeps drawing", (type) => { render(); advance(7000); const stopped = mockInvalidate.mock.calls.length; act(() => { window.dispatchEvent(new Event(type)); }); advance(500); expect(mockInvalidate.mock.calls.length).toBeGreaterThan(stopped + 8); }, ); it("each gesture buys another 5 s: it does not stop while there is movement", () => { render(); for (let i = 0; i < 4; i++) { advance(3000); act(() => { window.dispatchEvent(new Event("pointermove")); }); } const before = mockInvalidate.mock.calls.length; advance(1000); expect(mockInvalidate.mock.calls.length).toBeGreaterThan(before + 20); }); it("lets the idle time be configured", () => { render(); advance(1500); const stopped = mockInvalidate.mock.calls.length; advance(2000); expect(mockInvalidate.mock.calls.length).toBe(stopped); }); it("removes its listeners when it unmounts or goes inactive", () => { const { rerender, unmount } = render(); rerender(); mockInvalidate.mockClear(); act(() => { window.dispatchEvent(new Event("pointermove")); }); advance(500); expect(mockInvalidate).not.toHaveBeenCalled(); unmount(); act(() => { window.dispatchEvent(new Event("scroll")); }); advance(500); expect(mockInvalidate).not.toHaveBeenCalled(); }); });