perf(movimento): uma transição, 3D leve e vídeo certo por aparelho
Passagens com fade e 32 px, cena anterior termina em opacidade 0, sem visibility nem clip-path. 3D do hero como fundo ambiente, sob demanda e desligado em toque, aparelho fraco, saveData e movimento reduzido. Lenis fora do toque e salto direto em âncoras distantes. Vitrine só de clientes, até 3 capítulos, fixa só no desktop e carrossel no celular; vídeo só do projeto em cena com preload metadata. Rótulos mono a partir de 12 px e limão só no contato.
This commit is contained in:
1 parent
8d07b8c6bb
commit
c9a0b80656
48 files changed
+7210
-1241
No files matched your search
@@ -0,0 +1,173 @@
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { getFeaturedProjects } from "@/lib/portfolio-data";
|
||||
import { ProjectList } from "./ProjectList";
|
||||
import { buildShowcaseItems, ShowcaseProvider } from "./ShowcaseProvider";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock("./ProjectLightbox", () => ({
|
||||
ProjectLightbox: ({ index }: { index: number }) => (
|
||||
<div role="dialog" aria-label="ampliado">
|
||||
aberto:{index}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
type Callback = IntersectionObserverCallback;
|
||||
/** Cada observador criado, com o elemento que observa. */
|
||||
const observers: Array<{ callback: Callback; target?: Element }> = [];
|
||||
const Original = global.IntersectionObserver;
|
||||
|
||||
const projects = getFeaturedProjects(null);
|
||||
const items = buildShowcaseItems(projects);
|
||||
|
||||
beforeEach(() => {
|
||||
observers.length = 0;
|
||||
global.IntersectionObserver = class {
|
||||
private readonly record: { callback: Callback; target?: Element };
|
||||
constructor(callback: Callback) {
|
||||
this.record = { callback };
|
||||
observers.push(this.record);
|
||||
}
|
||||
observe(target: Element) {
|
||||
this.record.target = target;
|
||||
}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
} as unknown as typeof IntersectionObserver;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.IntersectionObserver = Original;
|
||||
});
|
||||
|
||||
function renderList(props: Partial<Parameters<typeof ProjectList>[0]> = {}) {
|
||||
return render(
|
||||
<ShowcaseProvider projects={projects}>
|
||||
<ProjectList items={items} device="desktop" {...props} />
|
||||
</ShowcaseProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function bringNear(target: Element) {
|
||||
act(() => {
|
||||
for (const observer of observers)
|
||||
if (observer.target === target)
|
||||
observer.callback(
|
||||
[{ isIntersecting: true } as IntersectionObserverEntry],
|
||||
{} as IntersectionObserver,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function bringAllNear(container: HTMLElement) {
|
||||
for (const item of container.querySelectorAll("li.ds-reel-item"))
|
||||
bringNear(item);
|
||||
}
|
||||
|
||||
describe("ProjectList", () => {
|
||||
it("path: device unknown (server / before mount) -> text and links only, no media and no expand button", () => {
|
||||
const { container } = renderList({ device: null });
|
||||
|
||||
expect(container.querySelectorAll("li")).toHaveLength(items.length);
|
||||
expect(container.querySelector("[data-device]")).toBeNull();
|
||||
expect(container.querySelector(".ds-reel-item-media")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: /Ampliar vídeo/ })).toBeNull();
|
||||
expect(screen.getAllByRole("link", { name: /Ver case/ })).toHaveLength(
|
||||
items.length,
|
||||
);
|
||||
});
|
||||
|
||||
it("path: desktop -> browser frame, poster only once the item is near the window", () => {
|
||||
const { container } = renderList({ device: "desktop" });
|
||||
|
||||
expect(container.querySelectorAll(".ds-bframe")).toHaveLength(items.length);
|
||||
expect(container.querySelector(".ds-phone")).toBeNull();
|
||||
expect(container.querySelector("img")).toBeNull();
|
||||
|
||||
bringAllNear(container);
|
||||
|
||||
const images = [...container.querySelectorAll("img")];
|
||||
expect(images).toHaveLength(items.length);
|
||||
expect(images[0]).toHaveAttribute(
|
||||
"alt",
|
||||
`Captura de tela de ${items[0].project.title}`,
|
||||
);
|
||||
expect(decodeURIComponent(images[0].getAttribute("src") ?? "")).toContain(
|
||||
"desktop.webp",
|
||||
);
|
||||
expect(images[0]).toHaveAttribute("sizes", "50vw");
|
||||
});
|
||||
|
||||
it("path: mobile -> phone frame with the phone poster and the phone sizes", () => {
|
||||
const { container } = renderList({ device: "mobile" });
|
||||
bringAllNear(container);
|
||||
|
||||
expect(container.querySelectorAll(".ds-phone")).toHaveLength(items.length);
|
||||
expect(container.querySelector(".ds-bframe")).toBeNull();
|
||||
const image = container.querySelector("img") as HTMLImageElement;
|
||||
expect(decodeURIComponent(image.getAttribute("src") ?? "")).toContain(
|
||||
"mobile.webp",
|
||||
);
|
||||
expect(image).toHaveAttribute("sizes", "(max-width: 767px) 70vw");
|
||||
});
|
||||
|
||||
it("path: an item that is not near yet keeps its frame but no image", () => {
|
||||
const { container } = renderList({ device: "desktop" });
|
||||
bringNear(container.querySelector("li.ds-reel-item") as Element);
|
||||
|
||||
expect(container.querySelectorAll("img")).toHaveLength(1);
|
||||
expect(container.querySelectorAll(".ds-bframe")).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it("path: project with a subtitle -> shown; without -> no subtitle paragraph", () => {
|
||||
const withSubtitle = items.map((item, index) => ({
|
||||
...item,
|
||||
project: { ...item.project, subtitle: index === 0 ? "Loja virtual" : "" },
|
||||
}));
|
||||
|
||||
const { container } = render(
|
||||
<ShowcaseProvider projects={projects}>
|
||||
<ProjectList items={withSubtitle} device="desktop" />
|
||||
</ShowcaseProvider>,
|
||||
);
|
||||
|
||||
const subs = container.querySelectorAll(".ds-reel-sub");
|
||||
expect(subs).toHaveLength(1);
|
||||
expect(subs[0]).toHaveTextContent("Loja virtual");
|
||||
});
|
||||
|
||||
it("path: pending -> the section is marked so CSS can hide it; not pending -> unmarked", () => {
|
||||
const pending = renderList({ pending: true, device: null });
|
||||
expect(pending.container.querySelector("section")).toHaveAttribute(
|
||||
"data-pending",
|
||||
);
|
||||
pending.unmount();
|
||||
|
||||
const settled = renderList({ pending: false });
|
||||
expect(settled.container.querySelector("section")).not.toHaveAttribute(
|
||||
"data-pending",
|
||||
);
|
||||
});
|
||||
|
||||
it("path: pressing 'Ampliar vídeo' on an item -> opens the enlarged window on that project", () => {
|
||||
renderList({ device: "desktop" });
|
||||
const second = screen
|
||||
.getByRole("heading", { level: 3, name: items[1].project.title })
|
||||
.closest("li") as HTMLElement;
|
||||
|
||||
fireEvent.click(
|
||||
within(second).getByRole("button", { name: /Ampliar vídeo/ }),
|
||||
);
|
||||
|
||||
expect(screen.getByRole("dialog", { name: "ampliado" })).toHaveTextContent(
|
||||
"aberto:1",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user