perf(movimento): vitrine e 3D mais leves, WebGL por software lembrado na sessão e suporte ao WebKit

This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 22:54:27 -03:00
1 parent 9fd1570426
commit 8903f3c57e
28 files changed
+854 -406

No files matched your search

+29 -24
View File
@@ -1,10 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import { act, fireEvent, render, screen, within } from "@testing-library/react";
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 {
getShowcaseConfig,
videoFractionAtScroll,
} from "@/lib/project-showcase";
import { ProjectReel } from "./ProjectReel";
import { ShowcaseProvider } from "./ShowcaseProvider";
import { CAPTION_OUT_MS, reelOffset, videoProgress } from "./showcase-math";
@@ -290,11 +291,18 @@ describe("ProjectReel", () => {
const layer = activeLayer();
expect(layer.querySelector(".ds-reel-title")).toBeNull();
expect(caption()).toHaveTextContent("Lavação 047");
const p = videoProgress(0.5);
// a rolagem do projeto é repartida por igual entre os capítulos: o meio dela é onde o 2º capítulo começa no vídeo
const lavacao = getShowcaseConfig("lavacao-047");
if (!lavacao) throw new Error("projeto fora da vitrine");
const p = videoFractionAtScroll(lavacao, videoProgress(0.5));
const desktop = layer.querySelector(
".ds-reel-screen video",
) as HTMLVideoElement;
expect(desktop.currentTime).toBeCloseTo(p * 30, 1);
expect(p).toBeCloseTo(
lavacao.chapters[1].desktop / lavacao.durations.desktop,
9,
);
// 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();
@@ -312,7 +320,7 @@ describe("ProjectReel", () => {
expect(lavacao).toBeDefined();
expect(
caption().querySelector(".ds-reel-chapter-count")?.textContent,
).toMatch(/^0\d\/03$/);
).toMatch(/^0\d\/02$/);
const labels = lavacao?.chapters.map((chapter) => chapter.label) ?? [];
expect(labels).toContain(
caption().querySelector(".ds-reel-chapter strong")?.textContent,
@@ -512,16 +520,16 @@ describe("ProjectReel", () => {
expect(list.querySelector("h2, h3")?.tagName).toBe("H2");
});
it("limits each project to three chapters, in the index and in the caption", () => {
it("limits each project to two chapters, in the index and in the caption", () => {
renderReel();
for (const layer of document.querySelectorAll(".ds-reel-layer"))
expect(
layer.querySelectorAll(".ds-reel-ticks li").length,
).toBeLessThanOrEqual(3);
).toBeLessThanOrEqual(2);
const chapters = screen.getByRole("list", {
name: "Trechos de SEV Moda Íntima",
});
expect(within(chapters).getAllByRole("button")).toHaveLength(3);
expect(within(chapters).getAllByRole("button")).toHaveLength(2);
});
it("shows the real domain in the address bar, or just the project name, never github.com", () => {
@@ -586,7 +594,9 @@ describe("ProjectReel", () => {
it("scrolls to a chapter inside the project when its link is pressed", () => {
renderReel();
const nav = screen.getByRole("navigation", { name: "Projetos da vitrine" });
fireEvent.click(within(nav).getByRole("button", { name: /As favoritas/ }));
fireEvent.click(
within(nav).getByRole("button", { name: /Coleção completa/ }),
);
act(() => {
jest.advanceTimersByTime(1200);
});
@@ -595,6 +605,11 @@ describe("ProjectReel", () => {
const last = calls[calls.length - 1][0] as { top: number };
expect(last.top).toBeGreaterThan(reelOffset(weights, 0, 0) * SCROLLABLE);
expect(last.top).toBeLessThan(reelOffset(weights, 1, 0) * SCROLLABLE);
// o 2º capítulo começa na metade da rolagem do projeto (que vai de REEL_HOLD a 1 - REEL_HOLD do trecho dele)
expect(last.top).toBeCloseTo(
reelOffset(weights, 0, 0.5) * SCROLLABLE + 2,
0,
);
});
});
@@ -729,10 +744,10 @@ describe("ProjectReel: a troca de projeto não duplica nada na tela", () => {
expect(titles()).toEqual(["SEV Moda Íntima"]);
expect(
caption().querySelector(".ds-reel-chapter-count")?.textContent,
).not.toBe("01/03");
).not.toBe("01/02");
});
it("never moves the caption text: no inline transform, and the CSS only fades it", () => {
it("never moves the caption text: no inline transform, while it leaves and when it comes back with the new content", () => {
renderReel();
moveTo(1, 60);
@@ -740,17 +755,7 @@ 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 = ["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/);
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
// 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);
@@ -777,8 +782,8 @@ describe("ProjectReel: teclado do índice (roving tabindex)", () => {
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);
// 5 projetos e os 2 trechos do projeto em cena: 7 botões, uma parada
expect(buttons).toHaveLength(5 + 2);
expect(stops()).toHaveLength(1);
expect(stops()[0]).toHaveAttribute("aria-current", "true");
expect(stops()[0]).toHaveAccessibleName("SEV Moda Íntima");