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:
Evandro Fonseca Junior committed 2026-10-14 19:14:48 -03:00
1 parent 9e2d65a926
commit 204ebd5fa5
49 files changed
+2197 -606

No files matched your search

+125 -22
View File
@@ -44,6 +44,10 @@ function mockViewport({ phone = false }: { phone?: boolean } = {}) {
}));
}
/** O arquivo do pôster que a imagem dentro de `element` pede (decodificado); "" sem imagem. */
const posterIn = (element: Element | null | undefined) =>
decodeURIComponent(element?.querySelector("img")?.getAttribute("src") ?? "");
const projects = getFeaturedProjects(null);
const weights = [
"sev-modaintima",
@@ -294,10 +298,9 @@ describe("ProjectReel", () => {
// o celular da moldura só tem o pôster: nenhum vídeo dentro dele
expect(layer.querySelector(".ds-phone")).toBeInTheDocument();
expect(layer.querySelector(".ds-phone video")).toBeNull();
expect(
(layer.querySelector(".ds-phone-screen") as HTMLElement).style
.backgroundImage,
).toContain("lavacao-047/mobile.webp");
expect(posterIn(layer.querySelector(".ds-phone-screen"))).toContain(
"lavacao-047/mobile.webp",
);
});
it("names the chapter on screen and counts them", () => {
@@ -412,17 +415,48 @@ describe("ProjectReel", () => {
it("asks for posters only near the reel, and only for the project on screen and its neighbours", () => {
reelTop = 6000;
renderReel();
const background = (index: number) =>
(document.querySelectorAll(".ds-reel-screen")[index] as HTMLElement).style
.backgroundImage;
expect(background(0)).toBe("");
const poster = (index: number) =>
posterIn(document.querySelectorAll(".ds-reel-screen")[index]);
expect(poster(0)).toBe("");
expect(document.querySelectorAll("video[poster]")).toHaveLength(0);
scrollTo(0); // chegou a menos de uma tela
expect(background(0)).toContain("sev-modaintima/desktop.webp");
expect(background(1)).toContain("lavacao-047/desktop.webp");
expect(background(3)).toBe("");
expect(background(4)).toBe("");
expect(poster(0)).toContain("sev-modaintima/desktop.webp");
expect(poster(1)).toContain("lavacao-047/desktop.webp");
expect(poster(3)).toBe("");
expect(poster(4)).toBe("");
});
it("downloads each poster once, as an optimized image sized for where it shows, never the 1440 px original twice", () => {
renderReel();
scrollTo(0);
// nenhum pôster vai por imagem de fundo nem pelo atributo do vídeo (cada um era uma requisição do arquivo original)
for (const screen of document.querySelectorAll<HTMLElement>(
".ds-reel-screen, .ds-phone-screen",
))
expect(screen.style.backgroundImage).toBe("");
expect(document.querySelectorAll("video[poster]")).toHaveLength(0);
const posters = [...document.querySelectorAll(".ds-reel img")];
// o do projeto em cena e o do vizinho, no notebook e no celular
expect(posters).toHaveLength(4);
for (const image of posters) {
// otimizado (next/image: o arquivo é redimensionado pelo servidor) e com o tamanho em que aparece
expect(image.getAttribute("src")).toContain("/_next/image");
expect(image.getAttribute("sizes")).toBeTruthy();
expect(image).toHaveAttribute("alt", "");
}
const laptops = posters.filter((image) => image.closest(".ds-reel-screen"));
const phones = posters.filter((image) => image.closest(".ds-phone-screen"));
expect(laptops).toHaveLength(2);
expect(phones).toHaveLength(2);
for (const image of laptops)
expect(image.getAttribute("sizes")).toContain("58vw");
for (const image of phones)
expect(image.getAttribute("sizes")).toContain("13vw");
// cada pôster é uma URL só (não há dois pedidos do mesmo arquivo)
const urls = posters.map((image) => image.getAttribute("src"));
expect(new Set(urls).size).toBe(urls.length);
});
it("respects the data saver: no scroll video is downloaded", () => {
@@ -437,10 +471,9 @@ describe("ProjectReel", () => {
});
expect(sources().every((src) => src === null)).toBe(true);
// o pôster continua lá
expect(
(document.querySelectorAll(".ds-reel-screen")[0] as HTMLElement).style
.backgroundImage,
).toContain("desktop.webp");
expect(posterIn(document.querySelectorAll(".ds-reel-screen")[0])).toContain(
"desktop.webp",
);
});
it("gives the server the project list (text and links, no media) and holds the pinned height before mounting", () => {
@@ -790,11 +823,81 @@ describe("ProjectReel: teclado do índice (roving tabindex)", () => {
expect(window.scrollTo).toHaveBeenCalled();
});
it("leaves the caption actions as their own tab stops (Ver case, Ampliar vídeo, Abrir site)", () => {
renderReel();
const actions = [...caption().querySelectorAll("a, button")].filter(
(element) => element.getAttribute("tabindex") !== "-1",
);
expect(actions.length).toBeGreaterThanOrEqual(2);
describe("the caption actions", () => {
const tabStopsAll = (root: Element) => [
...root.querySelectorAll<HTMLElement>("a[href], button"),
];
const tabStops = (root: Element) =>
tabStopsAll(root).filter(
(element) => element.getAttribute("tabindex") !== "-1",
);
it("are ONE tab stop (Ver case, Ampliar vídeo and Abrir site as a toolbar), so a project costs two stops with the index", () => {
renderReel();
const toolbar = within(caption()).getByRole("toolbar", {
name: "Ações de SEV Moda Íntima",
});
expect(tabStopsAll(toolbar)).toHaveLength(3);
expect(tabStops(toolbar)).toHaveLength(1);
expect(tabStops(toolbar)[0]).toHaveAccessibleName(/Ver case/);
// o índice (uma parada) + as ações (uma parada): a vitrine inteira custa duas paradas, não oito
const reel = document.querySelector(".ds-reel") as HTMLElement;
const stops = tabStops(reel).filter(
(element) => !element.closest("[inert]"),
);
expect(stops).toHaveLength(2);
});
it("keep Ampliar vídeo and Abrir site reachable with the arrows, and Enter on Ampliar opens it", () => {
renderReel();
const toolbar = within(caption()).getByRole("toolbar");
act(() => {
tabStops(toolbar)[0].focus();
});
fireEvent.keyDown(document.activeElement as Element, {
key: "ArrowRight",
});
expect(document.activeElement).toHaveAccessibleName(/Ampliar vídeo/);
// a parada do Tab foi para onde o foco está
expect(tabStops(toolbar)).toEqual([document.activeElement]);
fireEvent.keyDown(document.activeElement as Element, {
key: "ArrowRight",
});
expect(document.activeElement).toHaveAccessibleName(/Abrir site/);
fireEvent.keyDown(document.activeElement as Element, { key: "End" });
expect(document.activeElement).toHaveAccessibleName(/Abrir site/);
fireEvent.keyDown(document.activeElement as Element, { key: "Home" });
expect(document.activeElement).toHaveAccessibleName(/Ver case/);
fireEvent.keyDown(document.activeElement as Element, {
key: "ArrowRight",
});
fireEvent.click(document.activeElement as Element);
expect(
screen.getByRole("dialog", { name: "SEV Moda Íntima" }),
).toBeInTheDocument();
});
it("follow a project without a site: two actions, and the stop never lands on the missing one", () => {
renderReel();
const toolbar = () => within(caption()).getByRole("toolbar");
act(() => {
tabStops(toolbar())[0].focus();
});
fireEvent.keyDown(document.activeElement as Element, { key: "End" });
expect(document.activeElement).toHaveAccessibleName(/Abrir site/);
scrollTo(1); // AS Serviços: sem site
act(() => {
jest.advanceTimersByTime(CAPTION_OUT_MS + 40);
});
expect(caption()).toHaveTextContent("AS Serviços");
expect(tabStopsAll(toolbar())).toHaveLength(2);
expect(tabStops(toolbar())).toHaveLength(1);
});
});
});