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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user