Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import { getShowcaseAssets } from "@/lib/project-showcase";
|
|
import { caseStudies } from "@/lib/site-content";
|
|
import { CaseCover, coverSizes } from "./CaseCover";
|
|
import { getCaseMedia } from "./case-media";
|
|
import { coverTransitionProps } from "./cover-transition";
|
|
|
|
/**
|
|
* Testes de caminhos-base de CaseCover: sem captura (nada), com captura (moldura + pôster),
|
|
* os valores padrão de `sizes` e `priority` (prioridade = carga imediata e `fetchpriority="high"`)
|
|
* e o pôster como elemento compartilhado da troca de página.
|
|
*/
|
|
|
|
jest.mock("./case-media", () => ({ getCaseMedia: jest.fn() }));
|
|
|
|
// o dublê do <ViewTransition> põe as props num <div> para o teste ler
|
|
jest.mock("react", () =>
|
|
jest.requireActual("@/__mocks__/view-transition-probe").reactWithProbe(),
|
|
);
|
|
|
|
const mockedGetCaseMedia = jest.mocked(getCaseMedia);
|
|
const props = {
|
|
id: "sev-modaintima",
|
|
title: "SEV Moda Íntima",
|
|
demoUrl: "https://sevmodaintima.com.br",
|
|
};
|
|
|
|
afterEach(() => {
|
|
mockedGetCaseMedia.mockReset();
|
|
});
|
|
|
|
describe("CaseCover", () => {
|
|
it("path: capture found -> the poster is the shared element of the page change (case-<id>, only the pair animates)", () => {
|
|
mockedGetCaseMedia.mockImplementation(getShowcaseAssets);
|
|
render(<CaseCover {...props} />);
|
|
|
|
const image = screen.getByAltText(`Captura de tela do site ${props.title}`);
|
|
const transition = image.parentElement;
|
|
expect(transition).toHaveAttribute("data-vt-name", `case-${props.id}`);
|
|
expect(transition).toHaveAttribute("data-vt-share", "case-cover");
|
|
expect(transition).toHaveAttribute("data-vt-default", "none");
|
|
});
|
|
|
|
it("coverTransitionProps: every case gets its own name, valid as a CSS identifier", () => {
|
|
const names = caseStudies.map(({ id }) => coverTransitionProps(id).name);
|
|
for (const name of names) expect(name).toMatch(/^[a-z][a-z0-9-]*$/);
|
|
expect(new Set(names).size).toBe(names.length);
|
|
});
|
|
|
|
it("path: no desktop capture -> renders nothing", () => {
|
|
mockedGetCaseMedia.mockReturnValue(undefined);
|
|
const { container } = render(<CaseCover {...props} />);
|
|
|
|
expect(container).toBeEmptyDOMElement();
|
|
expect(mockedGetCaseMedia).toHaveBeenCalledWith(props.id, "desktop");
|
|
});
|
|
|
|
it("path: capture found -> poster inside the browser frame, with the real domain in the bar", () => {
|
|
mockedGetCaseMedia.mockImplementation(getShowcaseAssets);
|
|
const { container } = render(<CaseCover {...props} />);
|
|
|
|
const image = screen.getByAltText(`Captura de tela do site ${props.title}`);
|
|
expect(container.querySelector(".ds-bframe")).toContainElement(image);
|
|
expect(image.getAttribute("src")).toContain(
|
|
encodeURIComponent(`/videos/${props.id}/desktop.webp`),
|
|
);
|
|
expect(container.querySelector(".ds-bframe-url")).toHaveTextContent(
|
|
"sevmodaintima.com.br",
|
|
);
|
|
});
|
|
|
|
it("path: sizes and priority omitted -> default sizes and lazy loading", () => {
|
|
mockedGetCaseMedia.mockImplementation(getShowcaseAssets);
|
|
render(<CaseCover {...props} />);
|
|
|
|
const image = screen.getByAltText(`Captura de tela do site ${props.title}`);
|
|
expect(image).toHaveAttribute("sizes", coverSizes.card);
|
|
expect(image).toHaveAttribute("loading", "lazy");
|
|
expect(image).not.toHaveAttribute("fetchpriority");
|
|
});
|
|
|
|
it("path: sizes and priority given -> uses them, with eager loading and high fetch priority", () => {
|
|
mockedGetCaseMedia.mockImplementation(getShowcaseAssets);
|
|
render(<CaseCover {...props} sizes="320px" priority />);
|
|
|
|
const image = screen.getByAltText(`Captura de tela do site ${props.title}`);
|
|
expect(image).toHaveAttribute("sizes", "320px");
|
|
expect(image).toHaveAttribute("loading", "eager");
|
|
expect(image).toHaveAttribute("fetchpriority", "high");
|
|
});
|
|
|
|
it("path: no 100vw sizes -> every preset describes the width the cover really has", () => {
|
|
// 100vw baixava a imagem de 750 px para uma capa de 300 px no celular
|
|
for (const sizes of Object.values(coverSizes)) {
|
|
expect(sizes).not.toMatch(/(^|, )100vw$/);
|
|
expect(sizes).toMatch(/calc\(100vw - \d+px\)$/);
|
|
}
|
|
});
|
|
});
|