Legenda do mergulho até Projetos cobrir a cena; transição de página sem blur e com fundo escuro; foco no conteúdo ao trocar de página; listas com marcador e dois pesos de título; vídeos da SEV e da SC Plus regravados sem quadros brancos; scrub a 12-15 fps (-30%); vitrine com uma parada de Tab e setas, contador no celular e CSS sob demanda; âncoras num módulo só (fragment-nav); FramePulse com setInterval; CSS morto removido.
142 lines
4.0 KiB
TypeScript
142 lines
4.0 KiB
TypeScript
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<typeof startFramePulse>[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);
|
|
});
|
|
});
|