255 lines
9.7 KiB
TypeScript
255 lines
9.7 KiB
TypeScript
import { render, screen, within } from "@testing-library/react";
|
|
import { offerLabels } from "@/lib/offer-labels";
|
|
import AiPage, { metadata } from "./page";
|
|
|
|
it("uses a short search title: up to 44 characters, and 60 with the layout suffix", () => {
|
|
// o template de app/layout.tsx acrescenta " | Evandro.dev.br"
|
|
const suffix = " | Evandro.dev.br";
|
|
expect(metadata.title).toBe("Revisão de sistema e de app feito com IA");
|
|
expect(String(metadata.title).length).toBeLessThanOrEqual(44);
|
|
expect(`${metadata.title}${suffix}`.length).toBeLessThanOrEqual(60);
|
|
expect(metadata).toMatchObject({
|
|
description: expect.any(String),
|
|
alternates: { canonical: "https://evandro.dev.br/ia" },
|
|
});
|
|
// como nas outras páginas, o título da prévia de compartilhamento é o mesmo da busca, sem o sufixo
|
|
expect(metadata.openGraph?.title).toBe(
|
|
"Revisão de sistema e de app feito com IA",
|
|
);
|
|
expect(metadata.twitter?.title).toBe(
|
|
"Revisão de sistema e de app feito com IA",
|
|
);
|
|
});
|
|
|
|
it("uses no lime outside the contact actions (the quote on the paper background is ink)", () => {
|
|
const { container } = render(<AiPage />);
|
|
|
|
expect(
|
|
container.querySelector('[class*="border-[var(--slash)]"]'),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("keeps the full AI content, with the cited incidents summarized only here", () => {
|
|
render(<AiPage />);
|
|
|
|
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
"Seu app feito com IA está pronto para receber clientes? Eu testo antes deles.",
|
|
);
|
|
for (const heading of [
|
|
"Se a IA cria um sistema, por que contratar um desenvolvedor?",
|
|
"O que casos documentados ensinam",
|
|
"E a LGPD?",
|
|
]) {
|
|
expect(screen.getByRole("heading", { name: heading })).toBeInTheDocument();
|
|
}
|
|
for (const incident of [/Lovable:/, /Replit:/, /Base44:/]) {
|
|
expect(screen.getByRole("heading", { name: incident })).toBeInTheDocument();
|
|
}
|
|
expect(
|
|
screen.getByRole("link", { name: "Reportagem da Semafor (em inglês)" }),
|
|
).toHaveAttribute("rel", "noopener noreferrer");
|
|
});
|
|
|
|
it("offers WhatsApp conversations about reviewing an AI-built system", () => {
|
|
render(<AiPage />);
|
|
|
|
const links = screen
|
|
.getAllByRole("link")
|
|
.filter((link) => link.getAttribute("href")?.startsWith("https://wa.me/"));
|
|
expect(links.map((link) => link.textContent)).toEqual([
|
|
"Pedir a revisão do meu app",
|
|
"Avaliar meu projeto",
|
|
]);
|
|
for (const link of links) {
|
|
const text = new URL(link.getAttribute("href") ?? "").searchParams.get(
|
|
"text",
|
|
);
|
|
// a mensagem tem de formar uma frase: "Quero conversar sobre a revisão de um aplicativo feito com IA."
|
|
expect(text).toContain(
|
|
"Quero conversar sobre a revisão de um aplicativo feito com IA.",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("has breadcrumb navigation back to the home", () => {
|
|
render(<AiPage />);
|
|
|
|
const breadcrumb = screen.getByRole("navigation", {
|
|
name: "Trilha de navegação",
|
|
});
|
|
expect(
|
|
within(breadcrumb).getByRole("link", { name: "Início" }),
|
|
).toHaveAttribute("href", "/");
|
|
expect(
|
|
within(breadcrumb).getByText(offerLabels.revisao.name),
|
|
).toHaveAttribute("aria-current", "page");
|
|
});
|
|
|
|
it("presents the review as an offer: what the person receives, with no price or deadline", () => {
|
|
const { container } = render(<AiPage />);
|
|
|
|
const offer = screen.getByRole("heading", {
|
|
name: "O que você recebe na revisão",
|
|
});
|
|
const section = offer.closest("section") as HTMLElement;
|
|
const items = within(section)
|
|
.getAllByRole("listitem")
|
|
.map((item) => item.querySelector("h3")?.textContent);
|
|
expect(items).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",
|
|
]);
|
|
expect(container.textContent).not.toMatch(/R\$|\d+ dias|em até \d+/);
|
|
});
|
|
|
|
it("shows 'Como funciona em 3 passos' in a card on the right half of the hero, next to the headline, instead of repeating the deliverables", () => {
|
|
render(<AiPage />);
|
|
|
|
const card = screen.getByTestId("ia-steps-card");
|
|
expect(
|
|
within(card).getByText("Como funciona em 3 passos"),
|
|
).toBeInTheDocument();
|
|
const steps = within(card).getAllByRole("listitem");
|
|
expect(steps).toHaveLength(3);
|
|
expect(
|
|
steps.map((item) => item.querySelector("strong")?.textContent),
|
|
).toEqual([
|
|
"Conversa pelo WhatsApp.",
|
|
"Teste e avaliação.",
|
|
"Relatório e orçamento.",
|
|
]);
|
|
// a conversa vem primeiro e repete o aviso da página: nada de senhas nem acessos por mensagem
|
|
expect(steps[0]).toHaveTextContent(
|
|
"Não envie senhas nem acessos por mensagem.",
|
|
);
|
|
// o cartão divide a grade do hero com o título (metade direita no desktop) e não cria uma região de página
|
|
const hero = screen.getByRole("heading", { level: 1 }).closest("section");
|
|
expect(hero).toContainElement(card);
|
|
expect(card.parentElement).toHaveClass(
|
|
"lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]",
|
|
);
|
|
expect(screen.queryByRole("complementary")).toBeNull();
|
|
// os quatro entregáveis ficam só na seção "O que você recebe na revisão": o hero não os repete
|
|
expect(within(card).queryByText("O que você recebe")).toBeNull();
|
|
for (const item of [
|
|
"As falhas de acesso a dados",
|
|
"O que dá para aproveitar",
|
|
"O que é melhor refazer",
|
|
"Um orçamento para pôr em produção",
|
|
]) {
|
|
expect(within(card).queryByText(item)).toBeNull();
|
|
}
|
|
expect(
|
|
screen.getAllByRole("heading", { name: /O que você recebe/ }),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps the in-page secondary link and brings back a secondary link to the cases in the hero, both as text links", () => {
|
|
render(<AiPage />);
|
|
|
|
const hero = screen.getByRole("heading", { level: 1 }).closest("section");
|
|
const secondary = within(hero as HTMLElement).getByRole("link", {
|
|
name: "Ver como a revisão funciona",
|
|
});
|
|
expect(secondary).toHaveAttribute("href", "#ia-receber");
|
|
expect(document.querySelector(`section[id="ia-receber"]`)).not.toBeNull();
|
|
const cases = within(hero as HTMLElement).getByRole("link", {
|
|
name: "Ver projetos entregues",
|
|
});
|
|
expect(cases).toHaveAttribute("href", "/cases");
|
|
// os dois são links de texto; o único botão cheio do hero é o do WhatsApp
|
|
expect(cases.className).toBe(secondary.className);
|
|
const hrefs = within(hero as HTMLElement)
|
|
.getAllByRole("link")
|
|
.map((link) => link.getAttribute("href"));
|
|
expect(hrefs.filter((href) => href?.startsWith("/cases"))).toEqual([
|
|
"/cases",
|
|
]);
|
|
});
|
|
|
|
it("shows the report model with three illustrative findings (flaw, severity, suggested fix), labelled as examples and with no client name", () => {
|
|
const { container } = render(<AiPage />);
|
|
|
|
const heading = screen.getByRole("heading", {
|
|
level: 2,
|
|
name: "Modelo do relatório",
|
|
});
|
|
const section = heading.closest("section") as HTMLElement;
|
|
expect(section).toHaveAttribute("id", "ia-modelo");
|
|
expect(section).toHaveTextContent("exemplos ilustrativos");
|
|
expect(section).toHaveTextContent("não são de nenhum cliente");
|
|
|
|
const findings = within(section).getAllByRole("article");
|
|
expect(findings).toHaveLength(3);
|
|
for (const [index, finding] of findings.entries()) {
|
|
expect(finding).toHaveTextContent(`Exemplo ilustrativo 0${index + 1}`);
|
|
expect(
|
|
Array.from(finding.querySelectorAll("dt")).map(
|
|
(term) => term.textContent,
|
|
),
|
|
).toEqual(["Falha", "Gravidade", "Correção sugerida"]);
|
|
for (const definition of finding.querySelectorAll("dd")) {
|
|
expect(definition.textContent?.trim()).not.toBe("");
|
|
}
|
|
}
|
|
// a gravidade é uma palavra só, de uma escala simples
|
|
expect(
|
|
findings.map((finding) => finding.querySelectorAll("dd")[1].textContent),
|
|
).toEqual(["Alta", "Alta", "Média"]);
|
|
// sem nome de cliente do portfólio, sem preço nem prazo
|
|
expect(section.textContent).not.toMatch(
|
|
/SEV|Lavação|SC Plus|AS Serviços|Apolo|R\$|\d+ dias|em até \d+/,
|
|
);
|
|
// o modelo vem depois do que se recebe e antes de "por que contratar"
|
|
const receive = container.querySelector("#ia-receber") as HTMLElement;
|
|
expect(
|
|
receive.compareDocumentPosition(section) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
).toBeTruthy();
|
|
const hire = screen
|
|
.getByRole("heading", {
|
|
name: "Se a IA cria um sistema, por que contratar um desenvolvedor?",
|
|
})
|
|
.closest("section") as HTMLElement;
|
|
expect(
|
|
section.compareDocumentPosition(hire) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it("keeps the incident news collapsed in a <details>, closed by default", () => {
|
|
const { container } = render(<AiPage />);
|
|
|
|
const heading = screen.getByRole("heading", {
|
|
name: "O que casos documentados ensinam",
|
|
});
|
|
const details = heading.closest("details") as HTMLDetailsElement;
|
|
expect(details).not.toBeNull();
|
|
expect(details.open).toBe(false);
|
|
expect(heading.closest("summary")).not.toBeNull();
|
|
for (const incident of [/Lovable:/, /Replit:/, /Base44:/]) {
|
|
expect(within(details).getByText(incident)).toBeInTheDocument();
|
|
}
|
|
// só os incidentes ficam recolhidos: a oferta e a LGPD ficam à vista
|
|
expect(container.querySelectorAll("details")).toHaveLength(1);
|
|
expect(
|
|
screen
|
|
.getByRole("heading", { name: "O que você recebe na revisão" })
|
|
.closest("details"),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("names the review like the offer on the home: the trail and the structured data use the offer name, not the search title", () => {
|
|
const { container } = render(<AiPage />);
|
|
|
|
const script = container.querySelector('script[type="application/ld+json"]');
|
|
const data = JSON.parse(script?.textContent ?? "{}");
|
|
const breadcrumb = data["@graph"].find(
|
|
(node: { "@type": string }) => node["@type"] === "BreadcrumbList",
|
|
);
|
|
expect(
|
|
breadcrumb.itemListElement.map(({ name }: { name: string }) => name),
|
|
).toEqual(["Início", offerLabels.revisao.name]);
|
|
expect(offerLabels.revisao.name).toBe("Revisão de sistema que já existe");
|
|
});
|