perf(movimento): sem tela vazia, passagens em CSS e vitrine leve

Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:50 -03:00
1 parent 10f170abf4
commit 318a764190
77 files changed
+4293 -4951

No files matched your search

@@ -1,557 +1,166 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { getFeaturedProjects } from "@/lib/portfolio-data";
import {
buildShowcaseItems,
type ShowcaseItem,
ShowcaseProvider,
useShowcaseCard,
useShowcaseItems,
useShowcaseOpen,
} from "./ShowcaseProvider";
/**
* Testes de caminhos-base de ShowcaseProvider (complemento de ShowcaseProvider.test.tsx): register() sem
* IntersectionObserver, limiar de visibilidade, quem devolve o mouse, desmontagem do card em destaque, foco
* que sai para dentro ou para fora do card, aba oculta, pausa do usuário e a janela ampliada.
* Testes de caminhos-base de ShowcaseProvider (complemento de ShowcaseProvider.test.tsx). O provedor só guarda a
* lista da vitrine e qual projeto está ampliado: aqui, a janela aberta quando a lista muda, a troca de projeto
* com a janela já aberta e a estabilidade do que ele entrega (lista e função de abrir) entre renderizações.
* A janela ampliada é um dublê que só mostra o índice.
*/
let reducedMotion = false;
jest.mock("@/app/hooks/useReducedMotion", () => ({
useReducedMotion: () => reducedMotion,
}));
jest.mock("./ProjectLightbox", () => ({
ProjectLightbox: ({
index,
items,
onClose,
onNavigate,
}: {
index: number;
items: unknown[];
onClose: () => void;
onNavigate: (index: number) => void;
}) => (
<div role="dialog" aria-label="ampliado">
aberto:{index} de {items.length}
<button type="button" onClick={onClose}>
fechar
</button>
<button type="button" onClick={() => onNavigate(0)}>
ir para o primeiro
</button>
<button type="button" onClick={() => onNavigate(99)}>
ir para um que não existe
</button>
</div>
),
}));
type FakeObserver = {
callback: IntersectionObserverCallback;
targets: Set<Element>;
};
let observers: FakeObserver[] = [];
const OriginalObserver = global.IntersectionObserver;
class FakeIntersectionObserver {
private readonly state: FakeObserver;
constructor(callback: IntersectionObserverCallback) {
this.state = { callback, targets: new Set() };
observers.push(this.state);
}
observe(element: Element) {
this.state.targets.add(element);
}
unobserve(element: Element) {
this.state.targets.delete(element);
}
disconnect() {
this.state.targets.clear();
}
takeRecords() {
return [];
}
}
function report(target: Element, ratio: number) {
act(() => {
for (const observer of observers) {
observer.callback(
[
{
target,
intersectionRatio: ratio,
isIntersecting: ratio > 0,
} as IntersectionObserverEntry,
],
observer as unknown as IntersectionObserver,
);
}
});
}
function place(element: Element, top: number) {
element.getBoundingClientRect = () =>
({
top,
height: 300,
bottom: top + 300,
left: 0,
right: 400,
width: 400,
x: 0,
y: top,
}) as DOMRect;
}
function flush() {
act(() => {
jest.advanceTimersByTime(40);
});
}
function Card({ id }: { id: string }) {
const { frameRef, active, open, toggleAutoplay, userPaused } =
useShowcaseCard(id);
return (
<article className="ds-card">
<div ref={frameRef} data-testid={id} data-active={active} />
<button type="button" onClick={open}>
ampliar {id}
</button>
<button type="button" onClick={toggleAutoplay}>
pausa {id} {String(userPaused)}
</button>
</article>
);
}
const projects = getFeaturedProjects(null);
const clientIds = ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"];
const clients = projects.filter((project) => clientIds.includes(project.id));
function Cards({ ids }: { ids: string[] }) {
function Opener({ id }: { id: string }) {
const open = useShowcaseOpen();
return (
<ShowcaseProvider projects={projects}>
{ids.map((id) => (
<Card key={id} id={id} />
))}
</ShowcaseProvider>
<button type="button" onClick={() => open(id)}>
abrir {id}
</button>
);
}
function setup(ids = ["sev-modaintima", "lavacao-047"]) {
const view = render(<Cards ids={ids} />);
const frame = (id: string) => screen.getByTestId(id);
// tela de 768px de altura no jsdom: o meio fica em 384
if (ids.includes("sev-modaintima")) place(frame("sev-modaintima"), 100); // centro 250
if (ids.includes("lavacao-047")) place(frame("lavacao-047"), 230); // centro 380
return { view, frame };
describe("ShowcaseProvider: janela ampliada e mudança da lista", () => {
it("path: window open and its project leaves the list -> the window goes away", () => {
const { rerender } = render(
<ShowcaseProvider projects={clients}>
<Opener id="sc-plus" />
</ShowcaseProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "abrir sc-plus" }));
expect(screen.getByRole("dialog")).toHaveTextContent("aberto:2 de 4");
rerender(
<ShowcaseProvider
projects={clients.filter((project) => project.id !== "sc-plus")}
>
<Opener id="sc-plus" />
</ShowcaseProvider>,
);
expect(screen.queryByRole("dialog")).toBeNull();
});
it("path: window open and another project is opened -> it moves to the new project, one window only", () => {
render(
<ShowcaseProvider projects={clients}>
<Opener id="sev-modaintima" />
<Opener id="as-servicos" />
</ShowcaseProvider>,
);
fireEvent.click(
screen.getByRole("button", { name: "abrir sev-modaintima" }),
);
expect(screen.getByRole("dialog")).toHaveTextContent("aberto:0 de 4");
fireEvent.click(screen.getByRole("button", { name: "abrir as-servicos" }));
expect(screen.getAllByRole("dialog")).toHaveLength(1);
expect(screen.getByRole("dialog")).toHaveTextContent("aberto:3 de 4");
});
it("path: a project outside the showcase is asked while another window is open -> no window is left", () => {
render(
<ShowcaseProvider projects={projects}>
<Opener id="sev-modaintima" />
<Opener id="reddit-explorer" />
</ShowcaseProvider>,
);
fireEvent.click(
screen.getByRole("button", { name: "abrir sev-modaintima" }),
);
fireEvent.click(
screen.getByRole("button", { name: "abrir reddit-explorer" }),
);
// o id desconhecido não acha posição na lista: a janela não mostra projeto nenhum
expect(screen.queryByRole("dialog")).toBeNull();
});
it("path: closed window -> reopening the same project works again", () => {
render(
<ShowcaseProvider projects={clients}>
<Opener id="lavacao-047" />
</ShowcaseProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "abrir lavacao-047" }));
fireEvent.click(screen.getByRole("button", { name: "fechar" }));
expect(screen.queryByRole("dialog")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "abrir lavacao-047" }));
expect(screen.getByRole("dialog")).toHaveTextContent("aberto:1 de 4");
});
});
type Captured = {
items: ShowcaseItem[];
open: (id: string) => void;
};
function Probe({ onRender }: { onRender: (captured: Captured) => void }) {
onRender({ items: useShowcaseItems(), open: useShowcaseOpen() });
return null;
}
const isActive = (element: Element) =>
element.getAttribute("data-active") === "true";
beforeEach(() => {
jest.useFakeTimers();
reducedMotion = false;
observers = [];
global.IntersectionObserver =
FakeIntersectionObserver as unknown as typeof IntersectionObserver;
});
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
global.IntersectionObserver = OriginalObserver;
Reflect.deleteProperty(document, "hidden");
});
describe("ShowcaseProvider: IntersectionObserver e visibilidade", () => {
it("path: browser without IntersectionObserver -> cards register fine but none ever plays", () => {
// @ts-expect-error simula um navegador sem IntersectionObserver
global.IntersectionObserver = undefined;
const { view, frame } = setup();
flush();
expect(isActive(frame("sev-modaintima"))).toBe(false);
expect(isActive(frame("lavacao-047"))).toBe(false);
expect(() => view.unmount()).not.toThrow();
});
it.each([
[0.32, false],
[0.33, true],
[1, true],
])("path: %f of the card on screen -> visible: %s", (ratio, expected) => {
const { frame } = setup(["sev-modaintima"]);
report(frame("sev-modaintima"), ratio);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(expected);
});
it("path: a visibility report about an element that is not a card -> ignored", () => {
const { frame } = setup(["sev-modaintima"]);
const stranger = document.createElement("div");
report(frame("sev-modaintima"), 0.8);
flush();
report(stranger, 1);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
});
it("path: the card leaves the screen -> it stops being the active one", () => {
const { frame } = setup(["sev-modaintima"]);
report(frame("sev-modaintima"), 0.8);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
report(frame("sev-modaintima"), 0);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(false);
});
it("path: hidden tab -> nobody plays; back to visible -> the closest card plays again", () => {
const { frame } = setup();
report(frame("sev-modaintima"), 0.8);
report(frame("lavacao-047"), 0.8);
flush();
expect(isActive(frame("lavacao-047"))).toBe(true);
Object.defineProperty(document, "hidden", {
configurable: true,
value: true,
});
act(() => {
document.dispatchEvent(new Event("visibilitychange"));
});
flush();
expect(isActive(frame("lavacao-047"))).toBe(false);
expect(isActive(frame("sev-modaintima"))).toBe(false);
Object.defineProperty(document, "hidden", {
configurable: true,
value: false,
});
act(() => {
document.dispatchEvent(new Event("visibilitychange"));
});
flush();
expect(isActive(frame("lavacao-047"))).toBe(true);
});
});
describe("ShowcaseProvider: mouse e foco", () => {
function hoverSetup() {
const result = setup();
report(result.frame("sev-modaintima"), 0.8);
report(result.frame("lavacao-047"), 0.8);
flush();
return result;
}
it("path: a pen counts as a pointer that hovers", () => {
const { frame } = hoverSetup();
fireEvent.pointerEnter(
frame("sev-modaintima").closest(".ds-card") as HTMLElement,
{ pointerType: "pen" },
describe("ShowcaseProvider: o que entrega a quem está dentro", () => {
it("path: re-render with the same projects -> the same list and the same open function", () => {
const seen: Captured[] = [];
const tree = () => (
<ShowcaseProvider projects={clients}>
<Probe onRender={(captured) => seen.push(captured)} />
</ShowcaseProvider>
);
flush();
const { rerender } = render(tree());
expect(isActive(frame("sev-modaintima"))).toBe(true);
rerender(tree());
expect(seen.length).toBeGreaterThan(1);
expect(seen[seen.length - 1].items).toBe(seen[0].items);
expect(seen[seen.length - 1].open).toBe(seen[0].open);
});
it("path: another card's pointer leaves -> the hovered card keeps the video", () => {
const { frame } = hoverSetup();
const sev = frame("sev-modaintima").closest(".ds-card") as HTMLElement;
const lavacao = frame("lavacao-047").closest(".ds-card") as HTMLElement;
fireEvent.pointerEnter(sev, { pointerType: "mouse" });
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
fireEvent.pointerLeave(lavacao, { pointerType: "mouse" });
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
});
it("path: the hovered card is not on screen -> the closest visible card plays instead", () => {
const { frame } = hoverSetup();
report(frame("sev-modaintima"), 0);
fireEvent.pointerEnter(
frame("sev-modaintima").closest(".ds-card") as HTMLElement,
{ pointerType: "mouse" },
it("path: a different list of projects -> a new list of items", () => {
const seen: Captured[] = [];
const probe = <Probe onRender={(captured) => seen.push(captured)} />;
const { rerender } = render(
<ShowcaseProvider projects={clients}>{probe}</ShowcaseProvider>,
);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(false);
expect(isActive(frame("lavacao-047"))).toBe(true);
});
it("path: focus moves to another element inside the same card -> hover is kept", () => {
const { frame } = hoverSetup();
const card = frame("sev-modaintima").closest(".ds-card") as HTMLElement;
const inside = screen.getByRole("button", {
name: /ampliar sev-modaintima/,
});
fireEvent.focusIn(inside);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
fireEvent.focusOut(card, { relatedTarget: inside });
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
});
it.each([
[
"to another card",
() => screen.getByRole("button", { name: /ampliar lavacao-047/ }),
],
["to nothing", () => null],
])(
"path: focus leaves the card %s -> hover is released",
(_label, target) => {
const { frame } = hoverSetup();
const card = frame("sev-modaintima").closest(".ds-card") as HTMLElement;
fireEvent.focusIn(
screen.getByRole("button", { name: /ampliar sev-modaintima/ }),
);
flush();
expect(isActive(frame("sev-modaintima"))).toBe(true);
fireEvent.focusOut(card, { relatedTarget: target() });
flush();
expect(isActive(frame("sev-modaintima"))).toBe(false);
expect(isActive(frame("lavacao-047"))).toBe(true);
},
);
it("path: the hovered card is removed from the page -> its hover is dropped and another card plays", () => {
function Swap() {
const [showFirst, setShowFirst] = useState(true);
return (
<ShowcaseProvider projects={projects}>
{showFirst && <Card id="sev-modaintima" />}
<Card id="lavacao-047" />
<button type="button" onClick={() => setShowFirst(false)}>
remover o primeiro
</button>
</ShowcaseProvider>
);
}
render(<Swap />);
const sev = screen.getByTestId("sev-modaintima");
const lavacao = screen.getByTestId("lavacao-047");
place(sev, 100);
place(lavacao, 230);
report(sev, 0.8);
report(lavacao, 0.8);
fireEvent.pointerEnter(sev.closest(".ds-card") as HTMLElement, {
pointerType: "mouse",
});
flush();
expect(isActive(sev)).toBe(true);
fireEvent.click(screen.getByRole("button", { name: "remover o primeiro" }));
flush();
expect(screen.queryByTestId("sev-modaintima")).toBeNull();
expect(isActive(lavacao)).toBe(true);
});
});
describe("ShowcaseProvider: pausa e janela ampliada", () => {
it("path: the visitor pauses and resumes -> no card plays while paused", () => {
const { frame } = setup(["lavacao-047"]);
report(frame("lavacao-047"), 0.8);
flush();
expect(isActive(frame("lavacao-047"))).toBe(true);
fireEvent.click(
screen.getByRole("button", { name: /pausa lavacao-047 false/ }),
);
flush();
expect(isActive(frame("lavacao-047"))).toBe(false);
fireEvent.click(
screen.getByRole("button", { name: /pausa lavacao-047 true/ }),
);
flush();
expect(isActive(frame("lavacao-047"))).toBe(true);
});
it("path: opening a project that is not part of the showcase -> no window", () => {
function OpenOutsider() {
const { open } = useShowcaseCard("reddit-explorer");
return (
<button type="button" onClick={open}>
ampliar reddit-explorer
</button>
);
}
render(
<ShowcaseProvider projects={projects}>
<OpenOutsider />
rerender(
<ShowcaseProvider projects={clients.slice(0, 2)}>
{probe}
</ShowcaseProvider>,
);
fireEvent.click(
screen.getByRole("button", { name: /ampliar reddit-explorer/ }),
);
flush();
expect(screen.queryByRole("dialog")).toBeNull();
});
it("path: navigating in the window to a valid index -> changes project; to an invalid one -> closes", () => {
setup();
fireEvent.click(
screen.getByRole("button", { name: /ampliar lavacao-047/ }),
);
expect(screen.getByRole("dialog")).toHaveTextContent("aberto:1");
fireEvent.click(screen.getByRole("button", { name: "ir para o primeiro" }));
expect(screen.getByRole("dialog")).toHaveTextContent("aberto:0");
fireEvent.click(
screen.getByRole("button", { name: "ir para um que não existe" }),
);
expect(screen.queryByRole("dialog")).toBeNull();
});
it("path: reduced motion -> pausing and resuming never makes a card play", () => {
reducedMotion = true;
const { frame } = setup(["lavacao-047"]);
report(frame("lavacao-047"), 1);
flush();
fireEvent.click(
screen.getByRole("button", { name: /pausa lavacao-047 false/ }),
);
flush();
fireEvent.click(
screen.getByRole("button", { name: /pausa lavacao-047 true/ }),
);
flush();
expect(isActive(frame("lavacao-047"))).toBe(false);
});
});
describe("ShowcaseProvider: trabalho por quadro", () => {
it("path: no card registered -> scroll and resize schedule no frame", () => {
const raf = jest.spyOn(window, "requestAnimationFrame");
render(
<ShowcaseProvider projects={projects}>
<p>sem cards</p>
</ShowcaseProvider>,
);
flush();
raf.mockClear();
act(() => {
window.dispatchEvent(new Event("scroll"));
window.dispatchEvent(new Event("resize"));
document.dispatchEvent(new Event("visibilitychange"));
});
flush();
expect(raf).not.toHaveBeenCalled();
});
it("path: several scrolls inside one frame -> evaluated once", () => {
const { frame } = setup(["lavacao-047"]);
report(frame("lavacao-047"), 0.8);
flush();
const raf = jest.spyOn(window, "requestAnimationFrame");
raf.mockClear();
act(() => {
window.dispatchEvent(new Event("scroll"));
window.dispatchEvent(new Event("scroll"));
window.dispatchEvent(new Event("scroll"));
});
expect(raf).toHaveBeenCalledTimes(1);
});
});
describe("ShowcaseProvider: ganchos fora do provedor e cartão sem .ds-card", () => {
it("path: hooks used outside the provider -> empty list, and open/toggle do nothing", () => {
function Outsider() {
const items = useShowcaseItems();
const open = useShowcaseOpen();
const { toggleAutoplay, open: openCard, active } = useShowcaseCard("x");
return (
<button
type="button"
onClick={() => {
open("sev-modaintima");
openCard();
toggleAutoplay();
}}
>
itens:{items.length} ativo:{String(active)}
</button>
);
}
render(<Outsider />);
const button = screen.getByRole("button", { name: "itens:0 ativo:false" });
expect(() => fireEvent.click(button)).not.toThrow();
expect(screen.queryByRole("dialog")).toBeNull();
});
it("path: a card whose frame has no .ds-card ancestor -> the frame itself listens for the pointer", () => {
function Bare() {
const { frameRef, active } = useShowcaseCard("lavacao-047");
return <div ref={frameRef} data-testid="bare" data-active={active} />;
}
function Other() {
const { frameRef, active } = useShowcaseCard("sev-modaintima");
return <div ref={frameRef} data-testid="other" data-active={active} />;
}
render(
<ShowcaseProvider projects={projects}>
<Other />
<Bare />
</ShowcaseProvider>,
);
const bare = screen.getByTestId("bare");
const other = screen.getByTestId("other");
place(other, 230); // mais perto do meio da tela
place(bare, 100);
report(other, 0.8);
report(bare, 0.8);
flush();
expect(isActive(other)).toBe(true);
fireEvent.pointerEnter(bare, { pointerType: "mouse" });
flush();
expect(isActive(bare)).toBe(true);
expect(isActive(other)).toBe(false);
});
it("path: projects missing from the list -> only showcase projects that exist are built, in showcase order", () => {
const only = projects.filter((project) =>
["as-servicos", "sev-modaintima"].includes(project.id),
);
const items = buildShowcaseItems(only);
expect(items.map((item) => item.config.id)).toEqual([
"sev-modaintima",
"as-servicos",
]);
expect(buildShowcaseItems([])).toEqual([]);
expect(seen[0].items).toHaveLength(4);
expect(seen[seen.length - 1].items).toHaveLength(2);
});
});