- cards com moldura de navegador (endereco real) e brilho na cor do site no card que toca - um video por vez: o do card sob o mouse/foco, ou o mais perto do centro da tela; nada baixa antes de tocar, e um botao pausa os videos que se mexem sozinhos - janela ampliada em <dialog> nativo: troca computador/celular mantendo o ponto do site, capitulos clicaveis, controles proprios, setas entre projetos e foco devolvido ao fechar - dados e capitulos de cada projeto em lib/project-showcase.ts (substitui project-videos.ts) Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
178 lines
5.1 KiB
TypeScript
178 lines
5.1 KiB
TypeScript
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
import { getFeaturedProjects } from "@/lib/portfolio-data";
|
|
import { ProjectMedia } from "./ProjectMedia";
|
|
import { ShowcaseProvider } from "./ShowcaseProvider";
|
|
|
|
let reducedMotion = false;
|
|
jest.mock("@/app/hooks/useReducedMotion", () => ({
|
|
useReducedMotion: () => reducedMotion,
|
|
}));
|
|
jest.mock("./ProjectLightbox", () => ({
|
|
ProjectLightbox: () => <div role="dialog" aria-label="ampliado" />,
|
|
}));
|
|
|
|
const projects = getFeaturedProjects(null);
|
|
const nutriscan = projects.find((project) => project.id === "nutriscan");
|
|
if (!nutriscan) throw new Error("projeto ausente");
|
|
|
|
type Callback = IntersectionObserverCallback;
|
|
const callbacks: Callback[] = [];
|
|
const Original = global.IntersectionObserver;
|
|
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
reducedMotion = false;
|
|
callbacks.length = 0;
|
|
global.IntersectionObserver = class {
|
|
constructor(callback: Callback) {
|
|
callbacks.push(callback);
|
|
}
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
} as unknown as typeof IntersectionObserver;
|
|
jest.spyOn(HTMLMediaElement.prototype, "play").mockResolvedValue();
|
|
jest.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
|
|
jest.spyOn(HTMLMediaElement.prototype, "load").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
jest.restoreAllMocks();
|
|
global.IntersectionObserver = Original;
|
|
});
|
|
|
|
function renderCard() {
|
|
const view = render(
|
|
<ShowcaseProvider projects={projects}>
|
|
<article className="ds-card">
|
|
<ProjectMedia project={nutriscan as NonNullable<typeof nutriscan>} />
|
|
</article>
|
|
</ShowcaseProvider>,
|
|
);
|
|
const frame = view.container.querySelector(".ds-frame") as HTMLElement;
|
|
frame.getBoundingClientRect = () =>
|
|
({
|
|
top: 300,
|
|
height: 300,
|
|
bottom: 600,
|
|
left: 0,
|
|
right: 400,
|
|
width: 400,
|
|
x: 0,
|
|
y: 300,
|
|
}) as DOMRect;
|
|
return { ...view, frame };
|
|
}
|
|
|
|
function show(frame: HTMLElement) {
|
|
act(() => {
|
|
for (const callback of callbacks)
|
|
callback(
|
|
[
|
|
{
|
|
target: frame,
|
|
intersectionRatio: 1,
|
|
isIntersecting: true,
|
|
} as unknown as IntersectionObserverEntry,
|
|
],
|
|
{} as IntersectionObserver,
|
|
);
|
|
jest.advanceTimersByTime(40);
|
|
});
|
|
}
|
|
|
|
describe("ProjectMedia", () => {
|
|
it("frames the video with the project's address and a button that enlarges it", () => {
|
|
renderCard();
|
|
|
|
expect(screen.getByText("nutriscan.evandro.dev.br")).toBeInTheDocument();
|
|
const expand = screen.getByRole("button", {
|
|
name: "Ampliar o vídeo de NutriScan",
|
|
});
|
|
expect(expand).toHaveAttribute("aria-haspopup", "dialog");
|
|
fireEvent.click(expand);
|
|
expect(
|
|
screen.getByRole("dialog", { name: "ampliado" }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("plays only while the card is the active one", () => {
|
|
const { frame, container } = renderCard();
|
|
const video = container.querySelector("video") as HTMLVideoElement;
|
|
expect(container.querySelector(".ds-frame")).toHaveAttribute(
|
|
"data-active",
|
|
"false",
|
|
);
|
|
expect(video.querySelectorAll("source")).toHaveLength(0);
|
|
|
|
show(frame);
|
|
expect(container.querySelector(".ds-frame")).toHaveAttribute(
|
|
"data-active",
|
|
"true",
|
|
);
|
|
expect(video.querySelectorAll("source")).toHaveLength(2);
|
|
});
|
|
|
|
it("lets the visitor pause the videos that play on their own, and resume them", () => {
|
|
const { frame, container } = renderCard();
|
|
show(frame);
|
|
expect(container.querySelector(".ds-frame")).toHaveAttribute(
|
|
"data-active",
|
|
"true",
|
|
);
|
|
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "Pausar os vídeos dos projetos" }),
|
|
);
|
|
act(() => {
|
|
jest.advanceTimersByTime(40);
|
|
});
|
|
expect(container.querySelector(".ds-frame")).toHaveAttribute(
|
|
"data-active",
|
|
"false",
|
|
);
|
|
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "Retomar os vídeos dos projetos" }),
|
|
);
|
|
act(() => {
|
|
jest.advanceTimersByTime(40);
|
|
});
|
|
expect(container.querySelector(".ds-frame")).toHaveAttribute(
|
|
"data-active",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
it("has no pause button when nothing plays on its own (reduced motion)", () => {
|
|
reducedMotion = true;
|
|
renderCard();
|
|
expect(
|
|
screen.queryByRole("button", { name: /vídeos dos projetos/ }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("renders nothing for a project without a recorded video", () => {
|
|
const { container } = render(
|
|
<ShowcaseProvider projects={projects}>
|
|
<ProjectMedia project={{ id: "twins-lab", title: "Twins Lab" }} />
|
|
</ShowcaseProvider>,
|
|
);
|
|
expect(container.querySelector(".ds-frame")).toBeNull();
|
|
});
|
|
|
|
it("does nothing, without crashing, when used outside the provider", () => {
|
|
const { container } = render(
|
|
<ProjectMedia project={nutriscan as NonNullable<typeof nutriscan>} />,
|
|
);
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "Ampliar o vídeo de NutriScan" }),
|
|
);
|
|
expect(container.querySelector(".ds-frame")).not.toBeNull();
|
|
});
|
|
});
|