Files
portifolio-evandrodevbr/app/components/showcase/ProjectLightbox.ready.paths.test.tsx
Evandro Fonseca Junior 204ebd5fa5 fix(navegacao): foco em âncora de outra página, vitrine acessível e 3D mais leve
Foco cai no destino ao chegar de outra página; âncoras ignoram ?tipo=; movimento reduzido salta em todos os cliques; cronômetro da janela ampliada; degradê no topo de Projetos e legendas do mergulho com intervalos testados; vitrine acima da barra de cookies e com 2 paradas por projeto; reel-list.css no global; 3D montado em tarefas curtas, limitado a ~30 qps e estático com WebGL por software; pôsteres da vitrine de 330 para 158 KB; SC Plus regravado; scripts sem funções acima de complexidade 10.
2026-10-14 19:14:48 -03:00

161 lines
5.5 KiB
TypeScript

import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { getFeaturedProjects } from "@/lib/portfolio-data";
import { getShowcaseConfig, mobileFractionAt } from "@/lib/project-showcase";
import { ShowcaseProvider, useShowcaseOpen } from "./ShowcaseProvider";
/**
* Caminhos-base de readMetadata (CC 3) e do efeito de `readyState` no ProjectLightbox (complemento de
* ProjectLightbox.test.tsx): o vídeo que já chega com os metadados (cache, preload) não dispara `loadedmetadata`
* depois de montado, então o efeito lê a duração e aplica o ponto pendente da troca de aparelho.
* O que o teste principal já cobre: pronto na abertura sem ponto pendente; troca de aparelho com o evento.
* Aqui: troca de aparelho com o vídeo novo JÁ pronto (o ponto pendente é aplicado sem evento algum), troca de
* projeto com o vídeo novo já pronto (sem ponto pendente, o vídeo começa do zero), e vídeo ainda sem metadados.
*/
jest.mock("./ProjectLightboxLazy", () => ({
ProjectLightboxLazy: jest.requireActual("./ProjectLightbox").ProjectLightbox,
}));
jest.mock("@/app/hooks/useReducedMotion", () => ({
useReducedMotion: () => false,
}));
const DURATION = 30;
const lavacao = getShowcaseConfig("lavacao-047");
const projects = getFeaturedProjects(null);
/** O ponto de cada <video> (jsdom não tem reprodução). */
const positions = new WeakMap<object, number>();
let readyState = 0;
function Card({ id }: { id: string }) {
const openProject = useShowcaseOpen();
return (
<button type="button" onClick={() => openProject(id)}>
ampliar {id}
</button>
);
}
function open(id = "lavacao-047") {
render(
<ShowcaseProvider projects={projects}>
<Card id="sev-modaintima" />
<Card id="lavacao-047" />
<Card id="nutriscan" />
<Card id="as-servicos" />
</ShowcaseProvider>,
);
fireEvent.click(screen.getByRole("button", { name: `ampliar ${id}` }));
return screen.getByRole("dialog");
}
const video = (dialog: HTMLElement) =>
dialog.querySelector("video") as HTMLVideoElement;
beforeAll(() => {
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener() {},
removeEventListener() {},
}));
HTMLDialogElement.prototype.showModal = function showModal(
this: HTMLDialogElement,
) {
this.setAttribute("open", "");
};
HTMLDialogElement.prototype.close = function close(this: HTMLDialogElement) {
this.removeAttribute("open");
this.dispatchEvent(new Event("close"));
};
});
beforeEach(() => {
readyState = 0;
jest
.spyOn(HTMLMediaElement.prototype, "readyState", "get")
.mockImplementation(() => readyState);
jest
.spyOn(HTMLMediaElement.prototype, "duration", "get")
.mockImplementation(() => DURATION);
jest
.spyOn(HTMLMediaElement.prototype, "currentTime", "get")
.mockImplementation(function currentTime(this: HTMLMediaElement) {
return positions.get(this) ?? 0;
});
jest
.spyOn(HTMLMediaElement.prototype, "currentTime", "set")
.mockImplementation(function currentTime(
this: HTMLMediaElement,
value: number,
) {
positions.set(this, value);
});
jest
.spyOn(HTMLMediaElement.prototype, "play")
.mockImplementation(() => Promise.resolve());
jest.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
document.documentElement.classList.remove("ds-lb-open");
});
describe("ProjectLightbox: vídeo que já chega com os metadados", () => {
it("path: switching device and the new video is already ready -> the pending point is applied without any loadedmetadata event", () => {
if (!lavacao) throw new Error("config ausente");
const dialog = open();
const desktop = video(dialog);
positions.set(desktop, DURATION * 0.5);
readyState = 1;
fireEvent.click(within(dialog).getByRole("button", { name: "Celular" }));
const phone = video(dialog);
expect(phone).not.toBe(desktop);
expect(positions.get(phone)).toBeCloseTo(
mobileFractionAt(lavacao, 0.5) * DURATION,
5,
);
expect(dialog.querySelector(".ds-lb-time")?.textContent).toMatch(
/ \/ 0:30$/,
);
});
it("path: switching device, the new video is NOT ready yet -> nothing is applied until loadedmetadata, then the point is applied once", () => {
if (!lavacao) throw new Error("config ausente");
const dialog = open();
positions.set(video(dialog), DURATION * 0.5);
fireEvent.click(within(dialog).getByRole("button", { name: "Celular" }));
const phone = video(dialog);
expect(positions.has(phone)).toBe(false);
act(() => {
phone.dispatchEvent(new Event("loadedmetadata"));
});
const applied = positions.get(phone);
expect(applied).toBeCloseTo(mobileFractionAt(lavacao, 0.5) * DURATION, 5);
// um segundo loadedmetadata (o vídeo recarrega) não puxa o ponto de volta: o pendente já foi consumido
positions.set(phone, 7);
act(() => {
phone.dispatchEvent(new Event("loadedmetadata"));
});
expect(positions.get(phone)).toBe(7);
});
it("path: moving to another project whose video is already ready -> it starts from zero, with the duration shown", () => {
const dialog = open("lavacao-047");
positions.set(video(dialog), DURATION * 0.5);
readyState = 1;
fireEvent.click(within(dialog).getByRole("button", { name: /próximo/i }));
expect(positions.has(video(dialog))).toBe(false);
expect(dialog.querySelector(".ds-lb-time")?.textContent).toMatch(
/^0:00 \/ 0:30$/,
);
});
});