feat(showcase): moldura de navegador, um video por vez e video ampliado nos cards
- 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]>
This commit is contained in:
1 parent
60dc2f1878
commit
a1a1e91135
18 files changed
+3564
-155
No files matched your search
@@ -0,0 +1,177 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user