feat(home): posicionamento com prova, menu sem recarregar e WhatsApp em todo o site
H1 'Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.'; HeroWork maior com a SEV; menu Cases, Serviços, Como trabalho, Sobre e IA com next/link e pré-carregamento no hover; menu do celular em tela cheia; WhatsApp flutuante e no rodapé com a mensagem da rota; rodapé em todas as páginas; FAQ da home; /ia como oferta de revisão de app feito com IA; formulário com noscript, validação do mailto e plano B do pop-up; cartões sem foco indevido; sem a fonte Monument.
This commit is contained in:
1 parent
e80f73d075
commit
10f170abf4
56 files changed
+3099
-906
No files matched your search
@@ -1,53 +1,12 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
|
||||
import About from "./About";
|
||||
|
||||
/**
|
||||
* Testes de caminhos-base de About: todas as decisões dependem de `shouldReduceMotion`
|
||||
* (variantes dos cards, transição, hover e toque), então há dois caminhos: movimento normal e reduzido.
|
||||
* O `motion/react` é trocado por elementos simples que registram as props de animação.
|
||||
* Testes de caminhos-base de About: a entrada dos quatro cartões (Reveal, CSS) com atrasos crescentes, o
|
||||
* feedback de toque e de mouse por classes (cv-press, cv-lift; cartões sem foco), o conteúdo e a coluna de
|
||||
* apoio. Nada de animação em JavaScript: o movimento reduzido é tratado pelo CSS.
|
||||
*/
|
||||
|
||||
const mockSeen = jest.fn();
|
||||
|
||||
jest.mock("motion/react", () => {
|
||||
const React = require("react");
|
||||
const cache = new Map<string, unknown>();
|
||||
const motion = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, tag: string) => {
|
||||
if (!cache.has(tag)) {
|
||||
cache.set(
|
||||
tag,
|
||||
({
|
||||
children,
|
||||
variants,
|
||||
whileHover,
|
||||
whileTap,
|
||||
transition,
|
||||
initial: _initial,
|
||||
animate: _animate,
|
||||
whileInView: _whileInView,
|
||||
viewport: _viewport,
|
||||
...props
|
||||
}: Record<string, unknown> & { children?: unknown }) => {
|
||||
mockSeen(tag, { variants, whileHover, whileTap, transition });
|
||||
return React.createElement(tag, props, children);
|
||||
},
|
||||
);
|
||||
}
|
||||
return cache.get(tag);
|
||||
},
|
||||
},
|
||||
);
|
||||
return { motion };
|
||||
});
|
||||
|
||||
jest.mock("@/app/hooks/useReducedMotion", () => ({
|
||||
useReducedMotion: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/public/edev.jpg", () => ({
|
||||
__esModule: true,
|
||||
default: { src: "/edev.jpg", width: 10, height: 10 },
|
||||
@@ -58,13 +17,16 @@ jest.mock("./Section", () => ({
|
||||
children,
|
||||
id,
|
||||
title,
|
||||
aside,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
id?: string;
|
||||
title?: string;
|
||||
aside?: React.ReactNode;
|
||||
}) => (
|
||||
<section id={id}>
|
||||
<h2>{title}</h2>
|
||||
{aside}
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
@@ -77,82 +39,93 @@ jest.mock("./animations/LazyRevealImage", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
type Seen = {
|
||||
variants?: {
|
||||
hidden: Record<string, unknown>;
|
||||
visible: { transition: Record<string, unknown> };
|
||||
};
|
||||
whileHover?: unknown;
|
||||
whileTap?: unknown;
|
||||
};
|
||||
|
||||
function renderedCards(): Seen[] {
|
||||
return mockSeen.mock.calls
|
||||
.filter(([tag]) => tag === "article")
|
||||
.map(([, seen]) => seen as Seen);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockSeen.mockClear();
|
||||
});
|
||||
|
||||
describe("About", () => {
|
||||
it("path: normal motion -> cards rise and clip in, with hover lift and press feedback", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(false);
|
||||
it("path: the four cards enter through Reveal -> same rise and duration, delays growing in steps", () => {
|
||||
const { container } = render(<About />);
|
||||
|
||||
render(<About />);
|
||||
|
||||
const cards = renderedCards();
|
||||
expect(cards).toHaveLength(4); // 3 etapas da história + o card do perfil
|
||||
for (const card of cards) {
|
||||
expect(card.variants?.hidden).toEqual({
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
clipPath: "inset(100% 0 0 0)",
|
||||
});
|
||||
expect(card.variants?.visible.transition).toEqual({
|
||||
duration: 0.8,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
});
|
||||
expect(card.whileHover).toEqual({
|
||||
y: -4,
|
||||
boxShadow: "0 18px 40px rgba(10,10,10,0.16)",
|
||||
});
|
||||
expect(card.whileTap).toEqual({ scale: 0.99 });
|
||||
const reveals = [...container.querySelectorAll<HTMLElement>(".ds-reveal")];
|
||||
// 3 etapas da história + o card do perfil (o cabeçalho da seção é um dublê, sem Reveal)
|
||||
expect(reveals).toHaveLength(4);
|
||||
expect(
|
||||
reveals.map((element) => element.style.getPropertyValue("--reveal-y")),
|
||||
).toEqual(["20px", "20px", "20px", "20px"]);
|
||||
expect(
|
||||
reveals.map((element) =>
|
||||
element.style.getPropertyValue("--reveal-duration"),
|
||||
),
|
||||
).toEqual(["0.8s", "0.8s", "0.8s", "0.8s"]);
|
||||
const delays = reveals.map((element) =>
|
||||
Number.parseFloat(element.style.getPropertyValue("--reveal-delay")),
|
||||
);
|
||||
expect(delays).toEqual([0.08, 0.16, 0.24, 0.32]);
|
||||
for (const element of reveals) {
|
||||
expect(element.style.opacity).toBe("");
|
||||
expect(element.style.transform).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
it("path: reduced motion -> cards only fade, with no hover lift and no press feedback", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(true);
|
||||
it("path: the four cards are not focus stops and use the CSS press feedback", () => {
|
||||
const { container } = render(<About />);
|
||||
|
||||
render(<About />);
|
||||
|
||||
const cards = renderedCards();
|
||||
const cards = container.querySelectorAll("article");
|
||||
expect(cards).toHaveLength(4);
|
||||
for (const card of cards) {
|
||||
expect(card.variants?.hidden).toEqual({ opacity: 0 });
|
||||
expect(card.variants?.visible.transition).toEqual({ duration: 0.25 });
|
||||
expect(card.whileHover).toBeUndefined();
|
||||
expect(card.whileTap).toBeUndefined();
|
||||
expect(card).not.toHaveAttribute("tabindex");
|
||||
expect(card).toHaveClass("cv-press");
|
||||
}
|
||||
});
|
||||
|
||||
it("path: content does not depend on motion -> same story, profile points and résumé link", () => {
|
||||
for (const reduced of [false, true]) {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(reduced);
|
||||
const { unmount } = render(<About />);
|
||||
it("path: the three story cards lift on hover and press with CSS; the profile card presses only", () => {
|
||||
const { container } = render(<About />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
level: 3,
|
||||
name: "Do sinal físico ao produto digital",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Arquitetura pragmática")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Ver trajetória completa" }),
|
||||
).toHaveAttribute("href", "/curriculo");
|
||||
unmount();
|
||||
const cards = [...container.querySelectorAll("article")];
|
||||
expect(cards).toHaveLength(4);
|
||||
for (const card of cards.slice(0, 3)) {
|
||||
expect(card).toHaveClass("cv-press", "cv-lift");
|
||||
}
|
||||
expect(cards[3]).toHaveClass("cv-press");
|
||||
});
|
||||
|
||||
it("path: the story, the profile points and the résumé link are all there", () => {
|
||||
render(<About />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
level: 3,
|
||||
name: "Você fala direto com quem desenvolve",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Solução do tamanho do seu problema"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Ver trajetória completa" }),
|
||||
).toHaveAttribute("href", "/curriculo");
|
||||
});
|
||||
|
||||
it("path: speaks in client benefits, with no technical jargon, and cites the Apolo Energia work", () => {
|
||||
const { container } = render(<About />);
|
||||
|
||||
expect(container.textContent).not.toMatch(
|
||||
/protocolos de baixo nível|operabilidade|camada física|sinal físico/i,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Desde 2022 desenvolvo sistemas internos, aplicativos e integrações IoT na Apolo Energia.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("path: the location label stays on one line and the columns stretch to the same height", () => {
|
||||
const { container } = render(<About />);
|
||||
|
||||
expect(screen.getByText(/Garuva, SC · Atendimento remoto/)).toHaveClass(
|
||||
"whitespace-nowrap",
|
||||
);
|
||||
expect(container.querySelector(".ds-grid-2")).toHaveClass("items-stretch");
|
||||
const story = container.querySelectorAll("article");
|
||||
for (const card of Array.from(story).slice(0, 3)) {
|
||||
expect(card).toHaveClass("flex-1");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user