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:
Evandro Fonseca Junior committed 2026-10-14 17:01:37 -03:00
1 parent f8545cd442
commit 6ac87824f3
68 files changed
+5735 -3300

No files matched your search

+93 -4
View File
@@ -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);
});
});