Files
portifolio-evandrodevbr/app/components/WorkModalities.paths.test.tsx

396 lines
16 KiB
TypeScript

import { render, screen, within } from "@testing-library/react";
import { serviceOffers } from "@/lib/site-content";
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 (nome e "O que você recebe" vindos de
* `serviceOffers`, foto do case ou ilustração, botão que abre o formulário com o tipo escolhido, "Primeiro passo" uma
* vez só depois dos cartões), 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, with the names of serviceOffers", () => {
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);
const titles = articles.map(
(article) =>
within(article).getByRole("heading", { level: 3 }).textContent,
);
expect(titles).toEqual([
"Site que traz pedidos pelo WhatsApp",
"Sistema para tirar a operação da planilha",
"Revisão de sistema que já existe",
]);
// a fonte é uma só, a mesma de /servicos
expect(titles).toEqual(serviceOffers.map(({ name }) => name));
});
it("path: the site offer cites the Lavação 047 and its deliverables are the ones of serviceOffers, the same list of /servicos", () => {
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(serviceOffers[0].deliverables);
});
it("path: only the site offer shows a photo (the Lavação 047 poster, sized for a third of the row); the other two have an illustration of their own", () => {
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(system.querySelector("img")).toBeNull();
expect(review.querySelector("img")).toBeNull();
expect(container.innerHTML).not.toContain("sev-modaintima/desktop.webp");
});
it("path: the system and the review cards have no black box with an icon: each has an illustration in the same box as the site photo", () => {
const { container } = render(<WorkModalities />);
expect(screen.queryByTestId("offer-icon")).toBeNull();
const boxes = ["site", "sistema", "revisao"].map((id) =>
screen.getByTestId(`offer-art-${id}`),
);
// as três caixas têm as mesmas classes, então o mesmo tamanho
expect(new Set(boxes.map((box) => box.className)).size).toBe(1);
for (const box of boxes) {
expect(box.className).toContain("aspect-[16/10]");
expect(box.className).toContain("md:aspect-[21/9]");
expect(box.className).toContain("lg:aspect-[16/10]");
expect(box.firstElementChild).not.toBeNull();
}
expect(container.querySelectorAll("article")).toHaveLength(3);
});
it("path: the system illustration is a sheet -> arrow -> dashboard scheme in SVG, with three indicators, no text and no numbers", () => {
render(<WorkModalities />);
const art = screen.getByTestId("offer-art-sistema");
const scheme = within(art).getByRole("img");
expect(scheme.tagName.toLowerCase()).toBe("svg");
expect(scheme).toHaveAccessibleName(
"Esquema ilustrativo: as linhas de uma planilha viram um painel com três indicadores e um gráfico de barras",
);
expect(scheme.getAttribute("viewBox")).toBe("0 0 240 150");
expect(scheme.querySelector('[data-art="planilha"]')).not.toBeNull();
expect(scheme.querySelector('[data-art="seta"]')).not.toBeNull();
const panel = scheme.querySelector('[data-art="painel"]') as Element;
expect(panel).not.toBeNull();
// três indicadores genéricos: um quadro com um traço de rótulo e um bloco de valor cada, sem dígito algum
expect(panel.querySelectorAll("g").length).toBe(3);
expect(scheme.querySelector("text")).toBeNull();
expect(art.textContent).toBe("");
// nada de cor de destaque: o limão é só do contato
expect(art.innerHTML).not.toMatch(/ccff00|var\(--slash\)/i);
});
it("path: the review illustration is an excerpt of the report model of /ia: three findings with a severity badge and the caption 'Exemplo ilustrativo'", () => {
render(<WorkModalities />);
const art = screen.getByTestId("offer-art-revisao");
expect(within(art).getByRole("img")).toHaveAccessibleName(
"Exemplo ilustrativo de relatório da revisão: três achados, dois de gravidade alta e um de gravidade média",
);
expect(within(art).getByText("Exemplo ilustrativo")).toBeInTheDocument();
const findings = Array.from(art.querySelectorAll("li")).map(
(item) => item.textContent,
);
expect(findings).toEqual([
"Dados expostosAlta",
"Chave expostaAlta",
"Sem backupMédia",
]);
// a gravidade está no texto do selo, não só na cor
for (const severity of ["Alta", "Média"]) {
expect(within(art).getAllByText(severity).length).toBeGreaterThan(0);
}
// o texto da ilustração também tem 15 px, como o resto da seção
expect(art.innerHTML).not.toMatch(/text-\[(?:[0-9]|1[0-4])px\]/);
});
it("path: the deliverables of the system and of the review come from serviceOffers, the same lists of /servicos", () => {
const { container } = render(<WorkModalities />);
const [, system, review] = Array.from(
container.querySelectorAll("article"),
);
expect(
within(system)
.getAllByRole("listitem")
.map((item) => item.textContent),
).toEqual(serviceOffers[1].deliverables);
expect(
within(review)
.getAllByRole("listitem")
.map((item) => item.textContent),
).toEqual(serviceOffers[2].deliverables);
});
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: the first step shows once, below the three cards: a first conversation and, after it, the scope and the proposal in writing", () => {
const { container } = render(<WorkModalities />);
for (const card of container.querySelectorAll("article")) {
expect(within(card).queryByText("Primeiro passo")).toBeNull();
}
expect(screen.getAllByText("Primeiro passo")).toHaveLength(1);
const label = screen.getByText("Primeiro passo");
expect(label.closest("article")).toBeNull();
// fica depois da grade de cartões
const grid = container.querySelector(".grid") as HTMLElement;
expect(
grid.compareDocumentPosition(label) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(label.parentElement?.textContent).toMatch(
/^Primeiro passoPrimeira conversa para entender .+\. Depois, escopo e proposta por escrito\.$/,
);
// sem prazo, sem preço
expect(label.parentElement?.textContent).not.toMatch(/R\$|dias|prazo de/i);
});
it("path: each button opens the contact form card 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-form"],
["Começar pelo sistema", "/?tipo=sistema#contact-form"],
["Começar pela revisão", "/?tipo=revisao#contact-form"],
] 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,
);
});
});