import { startFramePulse } from "./FramePulse"; const invalidate = jest.fn(); let stops: Array<() => void> = []; /** Liga a batida e guarda o desligamento para o afterEach não deixar ouvinte para trás. */ function start(options?: Parameters[1]) { const stop = startFramePulse(invalidate, options); stops.push(stop); return stop; } beforeEach(() => { jest.useFakeTimers(); invalidate.mockClear(); stops = []; }); afterEach(() => { for (const stop of stops) stop(); jest.useRealTimers(); }); const advance = (ms: number) => jest.advanceTimersByTime(ms); describe("startFramePulse", () => { it("asks for frames at about 30 per second, not 60", () => { start(); advance(1000); expect(invalidate.mock.calls.length).toBeGreaterThan(20); expect(invalidate.mock.calls.length).toBeLessThanOrEqual(34); }); it("asks for the first frame right away", () => { start(); expect(invalidate).toHaveBeenCalledTimes(1); }); it("is a single setInterval, never a second animation-frame loop", () => { const frames = jest.spyOn(window, "requestAnimationFrame"); start(); expect(jest.getTimerCount()).toBe(1); advance(1000); expect(frames).not.toHaveBeenCalled(); frames.mockRestore(); }); it("does nothing once it is stopped (the hero is off screen)", () => { const stop = start(); advance(200); const running = invalidate.mock.calls.length; expect(running).toBeGreaterThan(0); stop(); advance(1000); expect(invalidate.mock.calls.length).toBe(running); expect(jest.getTimerCount()).toBe(0); }); it("removes its listeners when it stops", () => { const stop = start(); stop(); invalidate.mockClear(); window.dispatchEvent(new Event("pointermove")); advance(500); expect(invalidate).not.toHaveBeenCalled(); expect(jest.getTimerCount()).toBe(0); }); it("can be stopped twice", () => { const stop = start(); stop(); expect(() => stop()).not.toThrow(); }); }); describe("startFramePulse: stops when nobody is looking", () => { it("stops drawing, and has no timer left, after 5 s without a cursor or scroll", () => { start(); advance(4000); const drawn = invalidate.mock.calls.length; expect(drawn).toBeGreaterThan(80); // ainda batendo, ~30 por segundo advance(1500); // passou dos 5 s const stopped = invalidate.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(invalidate.mock.calls.length).toBe(stopped); expect(jest.getTimerCount()).toBe(0); }); it.each([["pointermove"], ["scroll"], ["wheel"], ["keydown"]])( "wakes up again with %s and keeps drawing", (type) => { start(); advance(7000); const stopped = invalidate.mock.calls.length; window.dispatchEvent(new Event(type)); advance(500); expect(invalidate.mock.calls.length).toBeGreaterThan(stopped + 8); }, ); it("wakes up once, not once per gesture (one timer, however many events arrive)", () => { start(); for (let i = 0; i < 20; i++) window.dispatchEvent(new Event("pointermove")); expect(jest.getTimerCount()).toBe(1); }); it("each gesture buys another 5 s: it does not stop while there is movement", () => { start(); for (let i = 0; i < 4; i++) { advance(3000); window.dispatchEvent(new Event("pointermove")); } const before = invalidate.mock.calls.length; advance(1000); expect(invalidate.mock.calls.length).toBeGreaterThan(before + 20); }); it("lets the idle time and the rate be configured", () => { start({ idleMs: 1000 }); advance(1500); const stopped = invalidate.mock.calls.length; advance(2000); expect(invalidate.mock.calls.length).toBe(stopped); invalidate.mockClear(); start({ fps: 10 }); advance(1000); expect(invalidate.mock.calls.length).toBeLessThanOrEqual(11); expect(invalidate.mock.calls.length).toBeGreaterThanOrEqual(9); }); });