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