Files
portifolio-evandrodevbr/app/components/Reveal.test.tsx
T
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
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.
2026-11-10 03:48:25 -03:00

250 lines
8.0 KiB
TypeScript

import { act, render, screen } from "@testing-library/react";
import { DrawLine } from "./animations/DrawLine";
import { LazyRevealImage } from "./animations/LazyRevealImage";
import { Reveal } from "./Reveal";
import { Section } from "./Section";
/**
* Entradas por rolagem sem biblioteca de animação: o componente só marca data-shown (ou data-loaded) quando o
* IntersectionObserver avisa e o CSS (globals.css) faz a transição. Aqui vale o contrato com o CSS: as classes,
* as variáveis e o momento em que as marcas aparecem.
*/
type Observer = {
callback: IntersectionObserverCallback;
observed: Element[];
};
let observers: Observer[] = [];
const original = global.IntersectionObserver;
beforeEach(() => {
observers = [];
Object.defineProperty(window, "innerHeight", {
configurable: true,
value: 800,
});
global.IntersectionObserver = class {
private readonly record: Observer;
constructor(callback: IntersectionObserverCallback) {
this.record = { callback, observed: [] };
observers.push(this.record);
}
observe(target: Element) {
this.record.observed.push(target);
}
unobserve() {}
disconnect() {}
takeRecords() {
return [];
}
} as unknown as typeof IntersectionObserver;
});
afterEach(() => {
global.IntersectionObserver = original;
});
/** Avisa que `target` entrou inteiro na janela. */
function reveal(target: Element) {
act(() => {
for (const observer of observers)
if (observer.observed.includes(target))
observer.callback(
[
{
isIntersecting: true,
intersectionRatio: 1,
intersectionRect: { height: 400 },
target,
} as unknown as IntersectionObserverEntry,
],
{} as IntersectionObserver,
);
});
}
describe("Reveal", () => {
it("renders its children and waits for the viewport to mark them as shown", () => {
const { container } = render(
<Reveal>
<p>Conteúdo</p>
</Reveal>,
);
const wrapper = container.firstElementChild as HTMLElement;
expect(screen.getByText("Conteúdo")).toBeInTheDocument();
expect(wrapper).toHaveClass("ds-reveal");
expect(wrapper).not.toHaveAttribute("data-shown");
reveal(wrapper);
expect(wrapper).toHaveAttribute("data-shown");
});
it("hands the CSS its offset, duration and delay as custom properties", () => {
const { container } = render(
<Reveal y={40} duration={0.9} delay={0.12} className="extra">
x
</Reveal>,
);
const wrapper = container.firstElementChild as HTMLElement;
expect(wrapper.style.getPropertyValue("--reveal-y")).toBe("40px");
expect(wrapper.style.getPropertyValue("--reveal-duration")).toBe("0.9s");
expect(wrapper.style.getPropertyValue("--reveal-delay")).toBe("0.12s");
expect(wrapper).toHaveClass("ds-reveal", "extra");
});
it("uses no inline opacity or transform: the hidden state lives in the CSS, so no script means visible content", () => {
const { container } = render(<Reveal>x</Reveal>);
const wrapper = container.firstElementChild as HTMLElement;
expect(wrapper.style.opacity).toBe("");
expect(wrapper.style.transform).toBe("");
expect(wrapper.style.clipPath).toBe("");
});
});
describe("DrawLine", () => {
it("draws once the line is mostly on screen, and takes its delay from the prop", () => {
const { container } = render(<DrawLine delay={0.16} className="x" />);
const line = container.firstElementChild as HTMLElement;
expect(line).toHaveClass("ds-draw-line", "x");
expect(line.style.getPropertyValue("--reveal-delay")).toBe("0.16s");
expect(line).not.toHaveAttribute("data-shown");
reveal(line);
expect(line).toHaveAttribute("data-shown");
});
});
describe("LazyRevealImage", () => {
it("shows the picture (data-loaded) only after it has loaded", async () => {
const { container } = render(
<LazyRevealImage src="/foto.webp" alt="Foto" className="h-40" />,
);
const wrapper = container.firstElementChild as HTMLElement;
const image = screen.getByAltText("Foto");
expect(wrapper).toHaveClass("ds-lazy-reveal", "h-40");
expect(wrapper).not.toHaveAttribute("data-loaded");
// o next/image só repassa o onLoad depois de decodificar a imagem (uma microtarefa)
await act(async () => {
image.dispatchEvent(new Event("load"));
});
expect(wrapper).toHaveAttribute("data-loaded");
});
it("does not wait forever for an image that was already complete before the page hydrated", async () => {
const complete = jest
.spyOn(HTMLImageElement.prototype, "complete", "get")
.mockReturnValue(true);
const width = jest
.spyOn(HTMLImageElement.prototype, "naturalWidth", "get")
.mockReturnValue(640);
let view: ReturnType<typeof render> | undefined;
await act(async () => {
view = render(<LazyRevealImage src="/foto.webp" alt="Foto" />);
});
expect(view?.container.firstElementChild).toHaveAttribute("data-loaded");
complete.mockRestore();
width.mockRestore();
});
});
describe("Section", () => {
it("renders the heading, the description and the content, and marks only the header as it enters", () => {
const { container } = render(
<Section
id="sobre"
eyebrow="Sobre"
title="Quem faz"
description="Texto de apoio"
aside={<span>lateral</span>}
>
<p>Corpo</p>
</Section>,
);
const header = container.querySelector(".ds-section-header") as HTMLElement;
expect(container.querySelector("section#sobre")).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "Quem faz" }),
).toBeInTheDocument();
expect(screen.getByText("Texto de apoio")).toHaveClass(
"ds-section-description",
"ds-reveal",
);
expect(screen.getByText("Sobre")).toHaveClass("ds-eyebrow", "ds-type");
// um <div>, não um <aside>: dentro de <main> o <aside> seria um landmark "complementary" fora do topo
const side = screen.getByText("lateral").parentElement;
expect(side?.tagName).toBe("DIV");
expect(side).toHaveClass("ds-reveal");
expect(container.querySelector("aside")).toBeNull();
expect(screen.queryByRole("complementary")).toBeNull();
expect(screen.getByText("Corpo")).toBeInTheDocument();
expect(header).not.toHaveAttribute("data-shown");
reveal(header);
expect(header).toHaveAttribute("data-shown");
});
it("staggers the header items with growing delays", () => {
render(
<Section
eyebrow="Sobre"
title="Quem faz"
description="Texto"
aside={<span>lateral</span>}
>
x
</Section>,
);
const delay = (element: HTMLElement) =>
Number.parseFloat(element.style.getPropertyValue("--reveal-delay"));
const eyebrow = delay(screen.getByText("Sobre"));
const description = delay(screen.getByText("Texto"));
// o conteúdo lateral é um <div class="ds-reveal">, não um <aside> (ver Section.tsx)
const aside = delay(
screen.getByText("lateral").closest(".ds-reveal") as HTMLElement,
);
expect(eyebrow).toBeLessThan(description);
expect(description).toBeLessThan(aside);
});
it("wraps the body in Reveal, unless the section animates its own children", () => {
const { container, rerender } = render(
<Section title="Titulo">
<p>Corpo</p>
</Section>,
);
expect(screen.getByText("Corpo").closest(".ds-reveal")).toBeInTheDocument();
rerender(
<Section title="Titulo" disableChildrenAnimation>
<p>Corpo</p>
</Section>,
);
expect(screen.getByText("Corpo").closest(".ds-reveal")).toBeNull();
expect(container.querySelector(".ds-section-shell")).toBeInTheDocument();
});
it("uses the dark skin when asked", () => {
const { container } = render(
<Section title="Titulo" dark>
x
</Section>,
);
expect(container.querySelector("section")).toHaveClass(
"ds-section",
"ds-section-dark",
);
});
});