Files
portifolio-evandrodevbr/app/components/cinematic/hero-dive.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

111 lines
3.7 KiB
TypeScript

import { act, render } from "@testing-library/react";
import type { CSSProperties } from "react";
import HeroField, { DIVE_QUERY } from "./HeroField";
/*
* A altura do mergulho do hero vive num lugar só: `--hero-tail` (globals.css), que o CSS usa na altura do hero e na
* margem de Projetos, e que o HeroField lê para saber onde o progresso (--hero-p) acaba: quando Projetos começa a
* subir sobre o palco, `--hero-tail` telas antes do fim do hero. Estes testes conferem o comportamento (o progresso
* segue a variável), não o texto do CSS.
*/
jest.mock("./BlocosScene", () => ({
__esModule: true,
default: () => null,
prepareBlocos: () => Promise.resolve(),
}));
const win = window as unknown as Record<string, unknown>;
const original = {
matchMedia: window.matchMedia,
webgl2: win.WebGL2RenderingContext,
getContext: HTMLCanvasElement.prototype.getContext,
innerHeight: Object.getOwnPropertyDescriptor(window, "innerHeight"),
};
const SCREEN = 800;
beforeEach(() => {
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
matches: query === DIVE_QUERY,
media: query,
addEventListener() {},
removeEventListener() {},
}));
Object.defineProperty(navigator, "hardwareConcurrency", {
configurable: true,
value: 8,
});
win.WebGL2RenderingContext = function WebGL2RenderingContext() {};
HTMLCanvasElement.prototype.getContext = (() => ({
getExtension: () => null,
})) as unknown as typeof HTMLCanvasElement.prototype.getContext;
Object.defineProperty(window, "innerHeight", {
configurable: true,
value: SCREEN,
});
});
afterEach(() => {
window.matchMedia = original.matchMedia;
win.WebGL2RenderingContext = original.webgl2;
HTMLCanvasElement.prototype.getContext = original.getContext;
if (original.innerHeight)
Object.defineProperty(window, "innerHeight", original.innerHeight);
});
/** O hero com a altura (px) que o CSS lhe dá, rolado `scrolled` px; devolve o progresso do mergulho. */
function progressAt({
tail,
height,
scrolled,
}: {
tail?: number;
height: number;
scrolled: number;
}) {
const style =
tail === undefined ? undefined : ({ "--hero-tail": tail } as CSSProperties);
const { container, unmount } = render(
<section className="hero" style={style}>
<HeroField />
</section>,
);
const hero = container.querySelector(".hero") as HTMLElement;
hero.getBoundingClientRect = () =>
({ top: -scrolled, height, bottom: height - scrolled }) as DOMRect;
act(() => {
window.dispatchEvent(new Event("resize"));
});
// o rAF do jsdom corre em ~16 ms
return new Promise<number>((resolve) => {
setTimeout(() => {
const progress = Number(hero.style.getPropertyValue("--hero-p"));
unmount();
resolve(progress);
}, 60);
});
}
describe("hero dive: --hero-tail is the one source of its height", () => {
it("ends the dive where Projects starts to rise: --hero-tail screens before the end of the hero", async () => {
// hero de 2800 px com 2 telas de cauda: o mergulho tem 2800 - 2 * 800 = 1200 px
expect(
await progressAt({ tail: 2, height: 2800, scrolled: 600 }),
).toBeCloseTo(0.5);
expect(await progressAt({ tail: 2, height: 2800, scrolled: 1200 })).toBe(1);
});
it("follows the variable: the same hero with a shorter tail has a longer dive", async () => {
// 2800 - 1,5 * 800 = 1600 px de mergulho
expect(
await progressAt({ tail: 1.5, height: 2800, scrolled: 800 }),
).toBeCloseTo(0.5);
});
it("treats a hero without --hero-tail as stage only (the dive is the rest of the height)", async () => {
// 2000 - 1 * 800 = 1200 px de mergulho
expect(await progressAt({ height: 2000, scrolled: 600 })).toBeCloseTo(0.5);
});
});