import { render, screen, within } from "@testing-library/react"; import type { ReactNode } from "react"; import { SEO_IDS } from "@/lib/seo"; import { caseStudies, getServicePage, siteUrl } from "@/lib/site-content"; import ServicesIndexPage from "../page"; import ServicePage, { generateMetadata, generateStaticParams } from "./page"; jest.mock("next/navigation", () => ({ notFound: jest.fn() })); jest.mock("@/app/components/cinematic/HeroBackground", () => ({ __esModule: true, default: () => null, })); jest.mock("@/app/components/cinematic/MagneticButton", () => ({ __esModule: true, default: ({ as = "button", strength: _strength, children, ...props }: { as?: string; strength?: number; children?: ReactNode; } & Record) => { const React = require("react"); return React.createElement(as, props, children); }, })); const service = getServicePage("consultoria-nextjs"); if (!service) throw new Error("Missing representative service"); const publicServiceSlugs = [ "criacao-de-sites", "engenharia-de-software", "consultoria-nextjs", "tauri-rust", "integracao-hardware-software-iot", "arquitetura-de-software", ] as const; const publicServiceHrefs = publicServiceSlugs.map( (slug) => `/servicos/${slug}`, ); function readJsonLdEntities(container: HTMLElement): Record[] { const scripts = container.querySelectorAll( 'script[type="application/ld+json"]', ); if (scripts.length === 0) throw new Error("Missing JSON-LD"); return Array.from(scripts).flatMap((script) => { if (!script.textContent) throw new Error("Empty JSON-LD"); const value = JSON.parse(script.textContent) as Record; return Array.isArray(value["@graph"]) ? value["@graph"] : [value]; }); } it("keeps static params and route metadata canonical", async () => { const staticSlugs = generateStaticParams().map(({ slug }) => slug); expect(staticSlugs).toHaveLength(6); expect(new Set(staticSlugs).size).toBe(6); expect([...staticSlugs].sort()).toEqual([...publicServiceSlugs].sort()); const metadata = await generateMetadata({ params: Promise.resolve({ slug: service.slug }), }); const url = `${siteUrl}/servicos/${service.slug}`; expect(metadata).toMatchObject({ title: service.seoTitle, description: service.metaDescription, alternates: { canonical: url }, openGraph: { title: service.seoTitle, description: service.metaDescription, url, }, twitter: { title: service.seoTitle, description: service.metaDescription, }, }); expect(metadata).not.toHaveProperty("keywords"); }); it("renders answer-first consulting, reciprocal case links and minimal schema", async () => { const ui = await ServicePage({ params: Promise.resolve({ slug: service.slug }), }); const { container } = render(ui); const answer = screen.getByText(service.answer); expect(answer).toBeVisible(); for (const item of [ ...service.outcomes, ...service.examples, ...service.process, ]) { expect(screen.getByText(item)).toBeVisible(); } for (const detail of service.technicalDetails) { expect(screen.getByText(detail)).not.toBeVisible(); } const primaryCtas = screen.getAllByRole("link", { name: /WhatsApp/i }); for (const cta of primaryCtas) { const destination = new URL(cta.getAttribute("href") ?? ""); expect(destination.origin).toBe("https://wa.me"); expect(destination.searchParams.get("text")).toContain(service.navLabel); expect(cta).toHaveAttribute("target", "_blank"); expect(cta).toHaveAttribute("rel", expect.stringContaining("noopener")); } const expectedCaseLinks = service.relatedCaseIds .map((id) => { const caseStudy = caseStudies.find((item) => item.id === id); if (!caseStudy) throw new Error(`Missing related case ${id}`); return { name: `Ler case: ${caseStudy.title}`, href: `/cases/${id}`, }; }) .sort((a, b) => a.href.localeCompare(b.href)); const internalCaseLinks = screen.getAllByRole("link", { name: /^Ler case:/, }); expect( internalCaseLinks .map((link) => ({ name: link.textContent, href: link.getAttribute("href"), })) .sort((a, b) => (a.href ?? "").localeCompare(b.href ?? "")), ).toEqual(expectedCaseLinks); for (const internalLink of internalCaseLinks) { const firstExternalLink = internalLink .closest("article") ?.querySelector('a[target="_blank"]'); if (firstExternalLink) { expect( internalLink.compareDocumentPosition(firstExternalLink) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); } } const externalLinks = container.querySelectorAll('a[target="_blank"]'); expect(externalLinks.length).toBeGreaterThan(0); for (const externalLink of externalLinks) { expect(externalLink).toHaveAttribute( "rel", expect.stringMatching(/noreferrer/), ); } const graph = readJsonLdEntities(container); expect(graph.map((node) => node["@type"])).toEqual([ "Service", "BreadcrumbList", ]); expect(JSON.stringify(graph)).not.toMatch( /Article|FAQPage|ProfessionalService/, ); const serviceSchema = graph.find((node) => node["@type"] === "Service"); expect(serviceSchema).toMatchObject({ provider: { "@id": SEO_IDS.person }, }); const breadcrumb = graph.find( (node) => node["@type"] === "BreadcrumbList", ) as { itemListElement: Array<{ "@type": string; position: number; name: string; item: string; }>; }; expect(breadcrumb.itemListElement).toEqual([ { "@type": "ListItem", position: 1, name: "Início", item: siteUrl }, { "@type": "ListItem", position: 2, name: "Serviços", item: `${siteUrl}/servicos`, }, { "@type": "ListItem", position: 3, name: service.navLabel, item: `${siteUrl}/servicos/${service.slug}`, }, ]); const visibleBreadcrumb = screen.getByRole("navigation", { name: "Breadcrumb", }); expect( within(visibleBreadcrumb) .getAllByRole("link") .map((link) => [link.textContent, link.getAttribute("href")]), ).toEqual([ ["Início", "/"], ["Serviços", "/servicos"], [service.navLabel, `/servicos/${service.slug}`], ]); }); it("keeps hub cards and structured items aligned", () => { const { container } = render(); const graph = readJsonLdEntities(container); expect(graph.map((node) => node["@type"])).toEqual([ "CollectionPage", "ItemList", "BreadcrumbList", ]); const itemList = graph[1] as { itemListElement: Array<{ position: number; url: string; name: string }>; }; expect(itemList.itemListElement.map(({ position }) => position)).toEqual([ 1, 2, 3, 4, 5, 6, ]); const structuredUrls = itemList.itemListElement.map(({ url }) => url); expect(structuredUrls).toHaveLength(6); expect(new Set(structuredUrls).size).toBe(6); expect([...structuredUrls].sort()).toEqual( publicServiceHrefs.map((href) => `${siteUrl}${href}`).sort(), ); const visibleCards = itemList.itemListElement.map(({ name }) => screen.getByRole("link", { name }), ); const visibleHrefs = visibleCards.map((link) => link.getAttribute("href")); expect(visibleHrefs).toHaveLength(6); expect(new Set(visibleHrefs).size).toBe(6); expect([...visibleHrefs].sort()).toEqual([...publicServiceHrefs].sort()); expect(graph[2]).toMatchObject({ "@type": "BreadcrumbList", itemListElement: [ { position: 1, name: "Início", item: siteUrl }, { position: 2, name: "Serviços", item: `${siteUrl}/servicos` }, ], }); });