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,54 +1,14 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
|
||||
import { primaryButton, secondaryButtonLight } from "./button-styles";
|
||||
import { WorkModalities } from "./WorkModalities";
|
||||
|
||||
/**
|
||||
* Testes de caminhos-base de WorkModalities e TierCard: movimento normal x reduzido, plano em destaque
|
||||
* (botão e borda) x demais planos, e serviços do hub ausentes x presentes.
|
||||
* O `motion/react` é trocado por elementos simples que registram as props de animação.
|
||||
* Testes de caminhos-base de WorkModalities e TierCard: a entrada dos planos (Reveal, CSS), o toque e o passe do
|
||||
* mouse (classes cv-press e cv-lift, sem foco nos cartões), plano em destaque (botão e borda) x demais planos,
|
||||
* primeiro passo de cada plano, e serviços do hub (só os três principais).
|
||||
* Nada de animação em JavaScript: o movimento reduzido é tratado pelo CSS (globals.css e conversion.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("./Section", () => ({
|
||||
Section: ({ children, id }: { children?: React.ReactNode; id?: string }) => (
|
||||
<section id={id}>{children}</section>
|
||||
@@ -57,94 +17,91 @@ jest.mock("./Section", () => ({
|
||||
|
||||
jest.mock("./animations/DrawLine", () => ({ DrawLine: () => null }));
|
||||
|
||||
type Seen = {
|
||||
variants?: {
|
||||
hidden: Record<string, unknown>;
|
||||
visible: { transition: Record<string, unknown> };
|
||||
};
|
||||
whileHover?: unknown;
|
||||
whileTap?: unknown;
|
||||
};
|
||||
|
||||
/** O hub inteiro, fora da ordem da home: a home mostra só sites, sistemas web e integrações, nesta ordem. */
|
||||
const serviceLinks = [
|
||||
{
|
||||
href: "/servicos/tauri-rust",
|
||||
label: "Aplicativos de desktop",
|
||||
description: "Programas rápidos para o computador.",
|
||||
},
|
||||
{
|
||||
href: "/servicos/integracao-hardware-software-iot",
|
||||
label: "Integração de equipamentos e sistemas",
|
||||
description: "Máquinas e sistemas trocando dados.",
|
||||
},
|
||||
{
|
||||
href: "/servicos/consultoria-nextjs",
|
||||
label: "Melhorias em sites e sistemas",
|
||||
description: "Evolução do que já existe.",
|
||||
},
|
||||
{
|
||||
href: "/servicos/criacao-de-sites",
|
||||
label: "Sites profissionais",
|
||||
description: "Presença digital para a empresa.",
|
||||
},
|
||||
{
|
||||
href: "/servicos/tauri-rust",
|
||||
label: "Aplicativos de desktop",
|
||||
description: "Programas rápidos para o computador.",
|
||||
href: "/servicos/engenharia-de-software",
|
||||
label: "Sistemas web e aplicativos mobile",
|
||||
description: "Sistemas sob medida.",
|
||||
},
|
||||
{
|
||||
href: "/servicos/arquitetura-de-software",
|
||||
label: "Diagnóstico e arquitetura",
|
||||
description: "Avaliação técnica.",
|
||||
},
|
||||
];
|
||||
|
||||
function seen(tag: string): Seen[] {
|
||||
return mockSeen.mock.calls
|
||||
.filter(([name]) => name === tag)
|
||||
.map(([, props]) => props as Seen);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockSeen.mockClear();
|
||||
});
|
||||
|
||||
describe("WorkModalities: movimento", () => {
|
||||
it("path: normal motion -> tiers rise in, with hover lift and press feedback", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(false);
|
||||
it("path: tiers enter through Reveal -> same rise, same duration and a growing delay, no JavaScript animation props", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
render(<WorkModalities />);
|
||||
|
||||
const items = seen("div").filter(
|
||||
(props) => props.variants?.hidden?.opacity === 0,
|
||||
);
|
||||
expect(items).toHaveLength(3);
|
||||
for (const item of items) {
|
||||
expect(item.variants?.hidden).toEqual({
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
clipPath: "inset(100% 0 0 0)",
|
||||
});
|
||||
expect(item.variants?.visible.transition).toEqual({
|
||||
duration: 0.75,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
});
|
||||
}
|
||||
const cards = seen("article");
|
||||
expect(cards).toHaveLength(3);
|
||||
for (const card of cards) {
|
||||
expect(card.whileHover).toEqual({
|
||||
y: -4,
|
||||
boxShadow: "0 24px 50px rgba(0, 0, 0, 0.08)",
|
||||
});
|
||||
expect(card.whileTap).toEqual({ scale: 0.99 });
|
||||
const reveals = [...container.querySelectorAll<HTMLElement>(".ds-reveal")];
|
||||
expect(reveals).toHaveLength(3);
|
||||
expect(
|
||||
reveals.map((element) => element.style.getPropertyValue("--reveal-y")),
|
||||
).toEqual(["20px", "20px", "20px"]);
|
||||
expect(
|
||||
reveals.map((element) =>
|
||||
element.style.getPropertyValue("--reveal-duration"),
|
||||
),
|
||||
).toEqual(["0.75s", "0.75s", "0.75s"]);
|
||||
expect(
|
||||
reveals.map((element) =>
|
||||
Number.parseFloat(element.style.getPropertyValue("--reveal-delay")),
|
||||
),
|
||||
).toEqual([0.08, 0.16, 0.24]);
|
||||
// o estado escondido vive no CSS: sem opacidade nem transformação inline
|
||||
for (const element of reveals) {
|
||||
expect(element.style.opacity).toBe("");
|
||||
expect(element.style.transform).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
it("path: reduced motion -> tiers only fade, with no hover lift and no press feedback", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(true);
|
||||
it("path: the cards are not focus stops and keep no outline-none (touch and hover feedback come from CSS)", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
render(<WorkModalities />);
|
||||
|
||||
const items = seen("div").filter(
|
||||
(props) => props.variants?.hidden?.opacity === 0,
|
||||
);
|
||||
expect(items).toHaveLength(3);
|
||||
for (const item of items) {
|
||||
expect(item.variants?.hidden).toEqual({ opacity: 0 });
|
||||
expect(item.variants?.visible.transition).toEqual({ duration: 0.25 });
|
||||
const cards = container.querySelectorAll("article");
|
||||
expect(cards).toHaveLength(3);
|
||||
for (const card of cards) {
|
||||
expect(card).not.toHaveAttribute("tabindex");
|
||||
expect(card.className).not.toContain("outline-none");
|
||||
expect(card).toHaveClass("cv-press", "cv-lift");
|
||||
}
|
||||
for (const card of seen("article")) {
|
||||
expect(card.whileHover).toBeUndefined();
|
||||
expect(card.whileTap).toBeUndefined();
|
||||
});
|
||||
|
||||
it("path: each tier sits alone in its own reveal wrapper, which can shrink in the grid", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
for (const card of container.querySelectorAll("article")) {
|
||||
const wrapper = card.closest(".ds-reveal");
|
||||
expect(wrapper).toHaveClass("min-w-0");
|
||||
expect(wrapper?.querySelectorAll("article")).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("WorkModalities: plano em destaque", () => {
|
||||
it("path: the featured tier has the filled button and the thick border; the others have outline buttons", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(false);
|
||||
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const featured = screen.getByRole("link", {
|
||||
@@ -167,21 +124,17 @@ describe("WorkModalities: plano em destaque", () => {
|
||||
|
||||
describe("WorkModalities: serviços do hub", () => {
|
||||
it("path: no serviceLinks prop -> no service cards, the 'all services' link stays", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(false);
|
||||
|
||||
render(<WorkModalities />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Conhecer todos os serviços" }),
|
||||
screen.getByRole("link", { name: "Ver todos os serviços" }),
|
||||
).toHaveAttribute("href", "/servicos");
|
||||
expect(
|
||||
screen.queryByRole("link", { name: /Sites profissionais/ }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("path: serviceLinks given -> one card per service, in order, linking to its page", () => {
|
||||
jest.mocked(useReducedMotion).mockReturnValue(false);
|
||||
|
||||
it("path: serviceLinks given -> only the three main services (sites, sistemas web, integrações), in that order", () => {
|
||||
const { container } = render(
|
||||
<WorkModalities serviceLinks={serviceLinks} />,
|
||||
);
|
||||
@@ -190,14 +143,69 @@ describe("WorkModalities: serviços do hub", () => {
|
||||
"[data-reel-stagger='left']",
|
||||
) as HTMLElement;
|
||||
const links = within(grid).getAllByRole("link");
|
||||
expect(links.map((link) => link.getAttribute("href"))).toEqual(
|
||||
serviceLinks.map(({ href }) => href),
|
||||
);
|
||||
expect(links.map((link) => link.getAttribute("href"))).toEqual([
|
||||
"/servicos/criacao-de-sites",
|
||||
"/servicos/engenharia-de-software",
|
||||
"/servicos/integracao-hardware-software-iot",
|
||||
]);
|
||||
expect(
|
||||
within(links[0]).getByText("Sites profissionais"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(links[1]).getByText("Programas rápidos para o computador."),
|
||||
within(links[1]).getByText("Sistemas sob medida."),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Ver todos os serviços" }),
|
||||
).toHaveAttribute("href", "/servicos");
|
||||
});
|
||||
|
||||
it("path: a main service missing from the hub -> it is skipped, the others stay", () => {
|
||||
const { container } = render(
|
||||
<WorkModalities
|
||||
serviceLinks={serviceLinks.filter(
|
||||
({ href }) => !href.endsWith("/engenharia-de-software"),
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
|
||||
const grid = container.querySelector(
|
||||
"[data-reel-stagger='left']",
|
||||
) as HTMLElement;
|
||||
expect(
|
||||
within(grid)
|
||||
.getAllByRole("link")
|
||||
.map((link) => link.getAttribute("href")),
|
||||
).toEqual([
|
||||
"/servicos/criacao-de-sites",
|
||||
"/servicos/integracao-hardware-software-iot",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WorkModalities: primeiro passo", () => {
|
||||
it("path: each tier states a concrete first step, with no price, deadline or contract promise", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const articles = Array.from(container.querySelectorAll("article"));
|
||||
expect(articles).toHaveLength(3);
|
||||
for (const article of articles) {
|
||||
expect(within(article).getByText("Primeiro passo")).toBeInTheDocument();
|
||||
}
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
/Primeira conversa para entender o problema\. Depois, escopo e proposta por escrito\./,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
screen.getByText(/Você me mostra o que já existe\./),
|
||||
).toBeInTheDocument();
|
||||
for (const gone of [
|
||||
"Proposta por projeto",
|
||||
"Escopo de análise definido",
|
||||
"Etapas acordadas",
|
||||
]) {
|
||||
expect(screen.queryByText(gone)).toBeNull();
|
||||
}
|
||||
expect(container.textContent).not.toMatch(/R\$|em contrato|prazo de \d/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user