O botão das ofertas rola até o contato com o tipo marcado e não quebra mais as âncoras; ofertas com o que você recebe e primeiro passo, logo depois da vitrine; bloco de IA fora da home; cartão largo da SEV no hero do celular; separador da faixa de clientes só entre itens; favicon de 13 para 1,8 KB; pré-carregamento só no hover e nunca de /#... fora da home; X do menu e cabeçalho opaco; WhatsApp com "o case"; /ia com 3 passos e modelo de relatório ilustrativo; Sobre alinhado ao currículo; CSP Report-Only com os domínios do Ads.
342 lines
13 KiB
TypeScript
342 lines
13 KiB
TypeScript
import { render, screen, within } from "@testing-library/react";
|
|
import { primaryButton } from "./button-styles";
|
|
import { WorkModalities } from "./WorkModalities";
|
|
|
|
/**
|
|
* Testes de caminhos-base de WorkModalities e OfferCard: a entrada das ofertas (Reveal, CSS), o toque e o passe do
|
|
* mouse (classes cv-press e cv-lift, sem foco nos cartões), as três ofertas (miniatura do case, botão que abre o
|
|
* formulário com o tipo escolhido), o estilo único de cartão, o texto de pelo menos 15 px e a lista compacta de etapas.
|
|
* Nada de animação em JavaScript: o movimento reduzido é tratado pelo CSS (globals.css e conversion.css).
|
|
*/
|
|
|
|
jest.mock("./Section", () => ({
|
|
Section: ({
|
|
children,
|
|
id,
|
|
title,
|
|
description,
|
|
}: {
|
|
children?: React.ReactNode;
|
|
id?: string;
|
|
title?: string;
|
|
description?: string;
|
|
}) => (
|
|
<section id={id}>
|
|
<h2>{title}</h2>
|
|
<p>{description}</p>
|
|
{children}
|
|
</section>
|
|
),
|
|
}));
|
|
|
|
jest.mock("./animations/DrawLine", () => ({ DrawLine: () => null }));
|
|
|
|
const original = (image: Element) =>
|
|
new URL(image.getAttribute("src") ?? "", "http://localhost").searchParams.get(
|
|
"url",
|
|
);
|
|
|
|
describe("WorkModalities: movimento", () => {
|
|
it("path: offers enter through Reveal -> same rise, same duration and a growing delay, no JavaScript animation props", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
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: the cards are not focus stops and keep no outline-none (touch and hover feedback come from CSS)", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
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");
|
|
}
|
|
});
|
|
|
|
it("path: each offer 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: as três ofertas", () => {
|
|
it("path: the section title is 'Escolha por onde começar' and the offers follow the headline, in order", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
expect(
|
|
screen.getByRole("heading", {
|
|
level: 2,
|
|
name: "Escolha por onde começar",
|
|
}),
|
|
).toBeInTheDocument();
|
|
const articles = Array.from(container.querySelectorAll("article"));
|
|
expect(articles).toHaveLength(3);
|
|
expect(
|
|
articles.map(
|
|
(article) =>
|
|
within(article).getByRole("heading", { level: 3 }).textContent,
|
|
),
|
|
).toEqual([
|
|
"Site que traz pedidos pelo WhatsApp",
|
|
"Sistema para tirar a operação da planilha",
|
|
"Revisão de sistema que já existe, inclusive feito com IA",
|
|
]);
|
|
});
|
|
|
|
it("path: the site offer cites the Lavação 047 and delivers a catalog or price table, a ready WhatsApp message, a panel to edit prices and photos and the publication", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const [site] = Array.from(container.querySelectorAll("article"));
|
|
expect(site.textContent).toMatch(/Lavação 047/);
|
|
expect(
|
|
within(site)
|
|
.getAllByRole("listitem")
|
|
.map((item) => item.textContent),
|
|
).toEqual([
|
|
"Catálogo ou tabela de preços",
|
|
"Mensagem pronta no WhatsApp",
|
|
"Painel para editar preços e fotos",
|
|
"Publicação do site",
|
|
]);
|
|
});
|
|
|
|
it("path: only the site offer shows a photo (the Lavação 047 poster, sized for a third of the row)", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const [site, system, review] = Array.from(
|
|
container.querySelectorAll("article"),
|
|
);
|
|
const siteImage = within(site).getByRole("img");
|
|
expect(original(siteImage)).toBe("/videos/lavacao-047/desktop.webp");
|
|
expect(siteImage).toHaveAccessibleName(
|
|
"Site Lavação 047 aberto no computador",
|
|
);
|
|
expect(siteImage).toHaveAttribute(
|
|
"sizes",
|
|
"(min-width: 1024px) 33vw, 100vw",
|
|
);
|
|
expect(siteImage).toHaveAttribute("loading", "lazy");
|
|
// a vitrine da SEV é uma loja, não um painel: contradiria o texto do sistema
|
|
expect(within(system).queryByRole("img")).toBeNull();
|
|
expect(system.querySelector("img")).toBeNull();
|
|
expect(container.innerHTML).not.toContain("sev-modaintima/desktop.webp");
|
|
expect(within(review).queryByRole("img")).toBeNull();
|
|
});
|
|
|
|
it("path: the system and the review offers have a card with no photo, only an icon (decorative), and the deliverables below", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const [, system, review] = Array.from(
|
|
container.querySelectorAll("article"),
|
|
);
|
|
for (const card of [system, review]) {
|
|
const icon = within(card).getByTestId("offer-icon");
|
|
expect(icon.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
|
|
expect(icon.textContent).toBe("");
|
|
}
|
|
expect(
|
|
within(system)
|
|
.getAllByRole("listitem")
|
|
.map((item) => item.textContent),
|
|
).toEqual([
|
|
"Cadastro de clientes e produtos",
|
|
"Pedidos e acompanhamento",
|
|
"Relatórios",
|
|
"Acesso por perfil",
|
|
]);
|
|
expect(
|
|
within(review)
|
|
.getAllByRole("listitem")
|
|
.map((item) => item.textContent),
|
|
).toEqual([
|
|
"As falhas de acesso a dados",
|
|
"O que dá para aproveitar",
|
|
"O que é melhor refazer",
|
|
"Um orçamento para pôr em produção",
|
|
]);
|
|
});
|
|
|
|
it("path: every card says what you receive, with three or four items, once", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
for (const card of container.querySelectorAll("article")) {
|
|
expect(
|
|
within(card as HTMLElement).getAllByText("O que você recebe"),
|
|
).toHaveLength(1);
|
|
const items = within(card as HTMLElement).getAllByRole("listitem");
|
|
expect(items.length).toBeGreaterThanOrEqual(3);
|
|
expect(items.length).toBeLessThanOrEqual(4);
|
|
}
|
|
});
|
|
|
|
it("path: every card has its first step: a first conversation and, after it, the scope and the proposal in writing", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const cards = Array.from(container.querySelectorAll("article"));
|
|
for (const card of cards) {
|
|
expect(within(card).getAllByText("Primeiro passo")).toHaveLength(1);
|
|
expect(card.textContent).toMatch(
|
|
/Primeira conversa para entender .+\. Depois, escopo e proposta por escrito\./,
|
|
);
|
|
}
|
|
expect(screen.getAllByText("Primeiro passo")).toHaveLength(3);
|
|
});
|
|
|
|
it("path: each button opens the contact form with the project type already chosen, all three with the same filled style", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const articles = Array.from(container.querySelectorAll("article"));
|
|
const buttons = [
|
|
["Começar pelo site", "/?tipo=site#contact"],
|
|
["Começar pelo sistema", "/?tipo=sistema#contact"],
|
|
["Começar pela revisão", "/?tipo=revisao#contact"],
|
|
] as const;
|
|
buttons.forEach(([name, href], index) => {
|
|
const button = within(articles[index]).getByRole("link", { name });
|
|
expect(button).toHaveAttribute("href", href);
|
|
expect(button.className).toContain(primaryButton);
|
|
});
|
|
});
|
|
|
|
it("path: each offer links to the case or the page that backs it", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const articles = Array.from(container.querySelectorAll("article"));
|
|
expect(
|
|
within(articles[0]).getByRole("link", { name: "Ver case Lavação 047" }),
|
|
).toHaveAttribute("href", "/cases/lavacao-047");
|
|
expect(
|
|
within(articles[1]).getByRole("link", {
|
|
name: "Ver case SEV Moda Íntima",
|
|
}),
|
|
).toHaveAttribute("href", "/cases/sev-modaintima");
|
|
expect(
|
|
within(articles[2]).getByRole("link", {
|
|
name: "Ver como funciona a revisão",
|
|
}),
|
|
).toHaveAttribute("href", "/ia");
|
|
});
|
|
|
|
it("path: no duplicate service grid and no 'Reconhece algum destes cenários?' panel, only one link to /servicos after the cards", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
expect(screen.queryByText(/Reconhece algum destes cenários/)).toBeNull();
|
|
expect(screen.queryByText(/Planilhas demais/)).toBeNull();
|
|
const services = screen.getByRole("link", {
|
|
name: "Ver todos os serviços",
|
|
});
|
|
expect(services).toHaveAttribute("href", "/servicos");
|
|
expect(container.querySelectorAll("article")).toHaveLength(3);
|
|
// o link fica fora dos cartões, depois da grade
|
|
expect(services.closest("article")).toBeNull();
|
|
expect(
|
|
screen
|
|
.getAllByRole("link")
|
|
.filter((link) => /^\/servicos/.test(link.getAttribute("href") ?? "")),
|
|
).toEqual([services]);
|
|
});
|
|
});
|
|
|
|
describe("WorkModalities: um só estilo de cartão", () => {
|
|
it("path: the three cards share the same classes and no card is highlighted (no featured border, no shadow variant)", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const articles = Array.from(container.querySelectorAll("article"));
|
|
const classNames = articles.map((article) => article.className);
|
|
expect(new Set(classNames).size).toBe(1);
|
|
for (const article of articles) {
|
|
expect(article).not.toHaveAttribute("style");
|
|
expect(article.className).not.toContain("border-[var(--slash)]");
|
|
expect(article.className).not.toContain("shadow");
|
|
}
|
|
});
|
|
|
|
it("path: every title reserves the same minimum height, so the texts below line up", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
for (const article of container.querySelectorAll("article")) {
|
|
const title = within(article as HTMLElement).getByRole("heading", {
|
|
level: 3,
|
|
});
|
|
expect(title.className).toContain("lg:min-h-[3.3em]");
|
|
expect(title.className).toContain("xl:min-h-[2.2em]");
|
|
}
|
|
});
|
|
|
|
it("path: no text under 15px on the desktop either (the old 12 and 13px classes are gone)", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
expect(container.innerHTML).not.toMatch(
|
|
/(?:^|[\s:!"'])(?:md:|lg:)?!?text-\[(?:[0-9]|1[0-4])px\]|text-(?:xs|sm)\b/,
|
|
);
|
|
expect(container.innerHTML).not.toContain("ds-kicker");
|
|
expect(container.innerHTML).not.toContain("md:!text-[13px]");
|
|
expect(container.innerHTML).not.toContain("md:!text-[12px]");
|
|
});
|
|
});
|
|
|
|
describe("WorkModalities: como eu trabalho", () => {
|
|
it("path: the process is a compact list of four steps, with the title and the description in one line each", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
const process = container.querySelector("#process") as HTMLElement;
|
|
expect(
|
|
within(process).getByRole("heading", {
|
|
level: 3,
|
|
name: "Como eu trabalho",
|
|
}),
|
|
).toBeInTheDocument();
|
|
const steps = within(process).getAllByRole("listitem");
|
|
expect(steps).toHaveLength(4);
|
|
expect(
|
|
steps.map((step) => step.querySelector("strong")?.textContent),
|
|
).toEqual([
|
|
"Entender a operação.",
|
|
"Definir a entrega.",
|
|
"Construir e validar.",
|
|
"Publicar e dar continuidade.",
|
|
]);
|
|
// sem a grade de quatro cartões com subtítulo por etapa
|
|
expect(process.querySelector("h4")).toBeNull();
|
|
expect(process.textContent).toMatch(
|
|
/Prazo e investimento dependem do escopo/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("WorkModalities: sem prometer o que o dono não disse", () => {
|
|
it("path: no price, deadline or contract promise anywhere in the section", () => {
|
|
const { container } = render(<WorkModalities />);
|
|
|
|
expect(container.textContent).not.toMatch(
|
|
/R\$|em contrato|prazo de \d|\d+ dias|grátis|gratuit/i,
|
|
);
|
|
});
|
|
});
|