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(); expect( container.querySelector('[class*="border-[var(--slash)]"]'), ).toBeNull(); }); it("keeps the full AI content, with the cited incidents summarized only here", () => { render(); 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(); 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(); 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(); 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(); 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(); 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(); 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
, closed by default", () => { const { container } = render(); 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(); 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"); });