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.
This commit is contained in:
1 parent
9e2d65a926
commit
204ebd5fa5
49 files changed
+2197
-606
No files matched your search
@@ -0,0 +1,160 @@
|
||||
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$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user