perf(movimento): sem tempo morto, vídeos regravados e 3D sem React Three Fiber
Legenda do mergulho até Projetos cobrir a cena; transição de página sem blur e com fundo escuro; foco no conteúdo ao trocar de página; listas com marcador e dois pesos de título; vídeos da SEV e da SC Plus regravados sem quadros brancos; scrub a 12-15 fps (-30%); vitrine com uma parada de Tab e setas, contador no celular e CSS sob demanda; âncoras num módulo só (fragment-nav); FramePulse com setInterval; CSS morto removido.
This commit is contained in:
1 parent
f8545cd442
commit
6ac87824f3
68 files changed
+5735
-3300
No files matched your search
@@ -5,11 +5,19 @@ import { renderToString } from "react-dom/server";
|
||||
import { MOBILE_QUERY } from "@/lib/device";
|
||||
import { getFeaturedProjects } from "@/lib/portfolio-data";
|
||||
import { getShowcaseConfig } from "@/lib/project-showcase";
|
||||
import { CAPTION_OUT_MS, ProjectReel } from "./ProjectReel";
|
||||
import { ProjectReel } from "./ProjectReel";
|
||||
import { ShowcaseProvider } from "./ShowcaseProvider";
|
||||
import { reelOffset, videoProgress } from "./showcase-math";
|
||||
import { CAPTION_OUT_MS, reelOffset, videoProgress } from "./showcase-math";
|
||||
import { PINNED_QUERY } from "./useShowcaseMedia";
|
||||
|
||||
// o pacote da vitrine fixa e o da janela ampliada chegam sob demanda (React.lazy): aqui entram já, para o teste não esperar
|
||||
jest.mock("./PinnedReelLazy", () => ({
|
||||
PinnedReelLazy: jest.requireActual("./PinnedReel").default,
|
||||
}));
|
||||
jest.mock("./ProjectLightboxLazy", () => ({
|
||||
ProjectLightboxLazy: jest.requireActual("./ProjectLightbox").ProjectLightbox,
|
||||
}));
|
||||
|
||||
let reducedMotion = false;
|
||||
jest.mock("@/app/hooks/useReducedMotion", () => ({
|
||||
useReducedMotion: () => reducedMotion,
|
||||
@@ -699,13 +707,94 @@ describe("ProjectReel: a troca de projeto não duplica nada na tela", () => {
|
||||
for (const element of caption().querySelectorAll("*"))
|
||||
expect((element as HTMLElement).style.transform).toBe("");
|
||||
|
||||
const css = fs.readFileSync(path.join(__dirname, "showcase.css"), "utf8");
|
||||
const css = ["reel-list.css", "pinned-reel.css"]
|
||||
.map((file) => fs.readFileSync(path.join(__dirname, file), "utf8"))
|
||||
.join("\n");
|
||||
const rule = /\.ds-reel-caption \{[\s\S]*?\n\}/.exec(css)?.[0] ?? "";
|
||||
expect(rule).toContain("opacity");
|
||||
expect(rule).not.toMatch(/transform|translate/);
|
||||
expect(rule).toContain("0.12s");
|
||||
const layer = /\.ds-reel-layer \{[\s\S]*?\n\}/.exec(css)?.[0] ?? "";
|
||||
expect(layer).toContain("opacity");
|
||||
expect(layer).not.toMatch(/transform|translate/);
|
||||
|
||||
// comportamento: a legenda sai (atributo, sem estilo inline) e volta com o conteúdo novo sem nunca se deslocar
|
||||
expect(caption()).toHaveAttribute("data-leaving");
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(CAPTION_OUT_MS + 40);
|
||||
});
|
||||
expect(caption()).not.toHaveAttribute("data-leaving");
|
||||
expect(caption().getAttribute("style")).toBeNull();
|
||||
for (const element of caption().querySelectorAll("*"))
|
||||
expect((element as HTMLElement).style.transform).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectReel: teclado do índice (roving tabindex)", () => {
|
||||
const index = () =>
|
||||
screen.getByRole("navigation", { name: "Projetos da vitrine" });
|
||||
const stops = () =>
|
||||
within(index())
|
||||
.getAllByRole("button")
|
||||
.filter((button) => button.getAttribute("tabindex") === "0");
|
||||
const focusOn = (element: HTMLElement) =>
|
||||
act(() => {
|
||||
element.focus();
|
||||
});
|
||||
|
||||
it("makes the whole index ONE tab stop (the current project), however many projects and chapters it lists", () => {
|
||||
renderReel();
|
||||
const buttons = within(index()).getAllByRole("button");
|
||||
// 5 projetos e os 3 trechos do projeto em cena: 8 botões, uma parada
|
||||
expect(buttons).toHaveLength(5 + 3);
|
||||
expect(stops()).toHaveLength(1);
|
||||
expect(stops()[0]).toHaveAttribute("aria-current", "true");
|
||||
expect(stops()[0]).toHaveAccessibleName("SEV Moda Íntima");
|
||||
});
|
||||
|
||||
it("walks projects and the chapters of the current one with the arrows, in the order they appear", () => {
|
||||
renderReel();
|
||||
const names = () =>
|
||||
within(index())
|
||||
.getAllByRole("button")
|
||||
.map((button) => button.textContent?.replace(/\s+/g, " ").trim());
|
||||
focusOn(stops()[0]);
|
||||
const seen: Array<string | null | undefined> = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
fireEvent.keyDown(document.activeElement as Element, {
|
||||
key: "ArrowDown",
|
||||
});
|
||||
seen.push(
|
||||
document.activeElement?.textContent?.replace(/\s+/g, " ").trim(),
|
||||
);
|
||||
}
|
||||
// depois do projeto em cena vêm os trechos dele, e então os outros projetos
|
||||
expect(seen.slice(0, 3)).toEqual(names().slice(1, 4));
|
||||
// as setas não passam da última parada
|
||||
fireEvent.keyDown(document.activeElement as Element, { key: "End" });
|
||||
const last = document.activeElement;
|
||||
fireEvent.keyDown(last as Element, { key: "ArrowDown" });
|
||||
expect(document.activeElement).toBe(last);
|
||||
});
|
||||
|
||||
it("moves the tab stop to where the focus went, and Enter picks the project like a click", () => {
|
||||
renderReel();
|
||||
focusOn(stops()[0]);
|
||||
fireEvent.keyDown(document.activeElement as Element, { key: "End" });
|
||||
const target = document.activeElement as HTMLElement;
|
||||
expect(stops()).toEqual([target]);
|
||||
// o botão do último projeto leva a rolagem até ele (o teclado ativa o clique nativo do botão)
|
||||
fireEvent.click(target);
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user