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

@@ -1,8 +1,17 @@
import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { getFeaturedProjects } from "@/lib/portfolio-data";
import { ProjectList } from "./ProjectList";
import { carouselIndex, ProjectList } from "./ProjectList";
import { buildShowcaseItems, ShowcaseProvider } from "./ShowcaseProvider";
// 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", () => ({
// o ProjectLightbox deste teste é o dublê logo abaixo
ProjectLightboxLazy: jest.requireMock("./ProjectLightbox").ProjectLightbox,
}));
/**
* Testes de caminhos-base de ProjectList (e do ListItem dentro dela): sem aparelho (servidor), celular x desktop,
* longe x perto da janela (pôster), com e sem subtítulo, rótulo "pendente" e o botão de ampliar.
@@ -171,3 +180,150 @@ describe("ProjectList", () => {
);
});
});
describe("ProjectList: carrossel do celular", () => {
/** Dá ao carrossel as medidas que o jsdom não tem: largura de cada cartão, passo e rolagem. */
function lay(
list: HTMLElement,
{ scrollLeft = 0, card = 300, gap = 16 } = {},
) {
const cards = Array.from(list.children) as HTMLElement[];
Object.defineProperty(list, "clientWidth", {
configurable: true,
value: 360,
});
Object.defineProperty(list, "scrollWidth", {
configurable: true,
value: cards.length * (card + gap),
});
list.scrollLeft = scrollLeft;
cards.forEach((element, index) => {
element.getBoundingClientRect = () =>
({ left: index * (card + gap) - scrollLeft }) as DOMRect;
});
}
const list = (container: HTMLElement) =>
container.querySelector(".ds-reel-items") as HTMLElement;
const count = (container: HTMLElement) =>
container.querySelector(".ds-reel-count");
it("path: on the phone, shows '1/N' and one dot per project (decorative: the list already says how many there are)", () => {
const { container } = renderList({ device: "mobile" });
const counter = count(container) as HTMLElement;
expect(counter).toHaveAttribute("aria-hidden", "true");
expect(counter.textContent).toBe(`1/${items.length}`);
expect(counter.querySelectorAll("i")).toHaveLength(items.length);
expect(counter.querySelectorAll("i[data-on]")).toHaveLength(1);
});
it("path: no counter on the desktop grid, before mount, or with one project only", () => {
expect(count(renderList({ device: "desktop" }).container)).toBeNull();
expect(count(renderList({ device: null }).container)).toBeNull();
expect(
count(
renderList({ device: "mobile", items: items.slice(0, 1) }).container,
),
).toBeNull();
});
it("path: scrolling the strip moves the counter and the dot (one animation frame at a time)", () => {
jest.useFakeTimers();
try {
const { container } = renderList({ device: "mobile" });
const strip = list(container);
lay(strip, { scrollLeft: 316 * 2 });
act(() => {
strip.dispatchEvent(new Event("scroll"));
strip.dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(40);
});
expect((count(container) as HTMLElement).textContent).toBe(
`3/${items.length}`,
);
const dots = [...(count(container) as HTMLElement).querySelectorAll("i")];
expect(dots.findIndex((dot) => dot.hasAttribute("data-on"))).toBe(2);
} finally {
jest.useRealTimers();
}
});
it("path: the strip scrolled to its end -> the last project, even if its start never reaches the edge", () => {
jest.useFakeTimers();
try {
const { container } = renderList({ device: "mobile" });
const strip = list(container);
lay(strip);
// fim da rolagem: scrollLeft + clientWidth = scrollWidth
strip.scrollLeft = strip.scrollWidth - strip.clientWidth;
act(() => {
strip.dispatchEvent(new Event("scroll"));
jest.advanceTimersByTime(40);
});
expect((count(container) as HTMLElement).textContent).toBe(
`${items.length}/${items.length}`,
);
} finally {
jest.useRealTimers();
}
});
it("carouselIndex: 0 without horizontal scroll, never past the ends, and tolerant of a degenerate stride", () => {
const strip = document.createElement("ul");
for (let i = 0; i < 4; i++) strip.append(document.createElement("li"));
// sem rolagem para o lado (grade das telas maiores)
Object.defineProperty(strip, "scrollWidth", {
configurable: true,
value: 300,
});
Object.defineProperty(strip, "clientWidth", {
configurable: true,
value: 300,
});
expect(carouselIndex(strip)).toBe(0);
lay(strip, { scrollLeft: 316 });
expect(carouselIndex(strip)).toBe(1);
lay(strip, { scrollLeft: 316 * 10 });
expect(carouselIndex(strip)).toBe(3);
// cartões sobrepostos (passo zero): não divide por zero
for (const child of Array.from(strip.children))
(child as HTMLElement).getBoundingClientRect = () =>
({ left: 0 }) as DOMRect;
strip.scrollLeft = 10;
Object.defineProperty(strip, "scrollWidth", {
configurable: true,
value: 2000,
});
Object.defineProperty(strip, "clientWidth", {
configurable: true,
value: 360,
});
expect(carouselIndex(strip)).toBe(0);
// um cartão só
const single = document.createElement("ul");
single.append(document.createElement("li"));
expect(carouselIndex(single)).toBe(0);
});
it("path: 'Ampliar vídeo' keeps its accessible name (visually hidden on the phone, by CSS) and 'Ver case' is the first action", () => {
const { container } = renderList({ device: "mobile" });
const first = container.querySelector(".ds-reel-actions") as HTMLElement;
const names = [...first.querySelectorAll("a, button")].map((element) =>
element.textContent?.replace(/\s+/g, " ").trim(),
);
expect(names[0]).toMatch(/^Ver case/);
expect(
within(first).getByRole("button", { name: /^Ampliar vídeo de / }),
).toBeInTheDocument();
expect(first.querySelector(".ds-reel-expand-label")?.textContent).toBe(
"Ampliar vídeo",
);
});
});