Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
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(<FramePulse active />);
|
|
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(<FramePulse active={false} />);
|
|
advance(1000);
|
|
expect(mockInvalidate).not.toHaveBeenCalled();
|
|
|
|
rerender(<FramePulse active />);
|
|
advance(200);
|
|
const running = mockInvalidate.mock.calls.length;
|
|
expect(running).toBeGreaterThan(0);
|
|
|
|
rerender(<FramePulse active={false} />);
|
|
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(<FramePulse active />);
|
|
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(<FramePulse active />);
|
|
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(<FramePulse active />);
|
|
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(<FramePulse active idleMs={1000} />);
|
|
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(<FramePulse active />);
|
|
rerender(<FramePulse active={false} />);
|
|
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();
|
|
});
|
|
});
|