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.
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { createElement } from "react";
|
|
import { renderToString } from "react-dom/server";
|
|
import { useReducedMotion } from "./useReducedMotion";
|
|
|
|
type Listener = () => void;
|
|
|
|
/** matchMedia que responde só à consulta de movimento reduzido e deixa o teste mudar a preferência depois. */
|
|
function mockPreference(initial: boolean) {
|
|
const listeners = new Set<Listener>();
|
|
const query = {
|
|
matches: initial,
|
|
media: "(prefers-reduced-motion: reduce)",
|
|
addEventListener: jest.fn((_: string, listener: Listener) =>
|
|
listeners.add(listener),
|
|
),
|
|
removeEventListener: jest.fn((_: string, listener: Listener) =>
|
|
listeners.delete(listener),
|
|
),
|
|
};
|
|
window.matchMedia = jest.fn().mockReturnValue(query);
|
|
return {
|
|
query,
|
|
change(value: boolean) {
|
|
query.matches = value;
|
|
for (const listener of listeners) listener();
|
|
},
|
|
};
|
|
}
|
|
|
|
const original = window.matchMedia;
|
|
|
|
afterEach(() => {
|
|
window.matchMedia = original;
|
|
});
|
|
|
|
describe("useReducedMotion", () => {
|
|
it("is false when the system does not ask for reduced motion", () => {
|
|
mockPreference(false);
|
|
|
|
const { result } = renderHook(() => useReducedMotion());
|
|
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it("is true when the system asks for reduced motion", () => {
|
|
mockPreference(true);
|
|
|
|
const { result } = renderHook(() => useReducedMotion());
|
|
|
|
expect(result.current).toBe(true);
|
|
expect(window.matchMedia).toHaveBeenCalledWith(
|
|
"(prefers-reduced-motion: reduce)",
|
|
);
|
|
});
|
|
|
|
it("follows the preference when it changes while the page is open", () => {
|
|
const preference = mockPreference(false);
|
|
const { result } = renderHook(() => useReducedMotion());
|
|
expect(result.current).toBe(false);
|
|
|
|
act(() => preference.change(true));
|
|
expect(result.current).toBe(true);
|
|
|
|
act(() => preference.change(false));
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it("stops listening when the component unmounts", () => {
|
|
const preference = mockPreference(false);
|
|
const { unmount } = renderHook(() => useReducedMotion());
|
|
|
|
unmount();
|
|
|
|
expect(preference.query.removeEventListener).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("is false on the server and in browsers without matchMedia, so the HTML always hydrates the same", () => {
|
|
mockPreference(true);
|
|
function Probe() {
|
|
return createElement("i", { "data-reduced": String(useReducedMotion()) });
|
|
}
|
|
expect(renderToString(createElement(Probe))).toContain(
|
|
'data-reduced="false"',
|
|
);
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: simula um navegador sem a API
|
|
(window as any).matchMedia = undefined;
|
|
const { result } = renderHook(() => useReducedMotion());
|
|
expect(result.current).toBe(false);
|
|
});
|
|
});
|