import { render, screen, within } from "@testing-library/react"; import type { ReactNode } from "react"; import { caseStudies, servicePages, siteUrl } from "@/lib/site-content"; import CasesPage from "../page"; import CasePage, { 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 publicCases = [ { id: "nutriscan", title: "NutriScan" }, { id: "twins-lab", title: "Twins Lab" }, { id: "reddit-explorer", title: "Reddit Posts Explorer" }, { id: "sc-plus", title: "SC Plus Aquicultura" }, { id: "gcc-installer", title: "MinGW GCC Installer" }, { id: "as-servicos", title: "AS Serviços" }, ] as const; const publicCaseIds = publicCases.map(({ id }) => id); function getCase(id: (typeof publicCases)[number]["id"]) { const caseStudy = caseStudies.find((item) => item.id === id); if (!caseStudy) throw new Error(`Missing case ${id}`); return caseStudy; } 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]; }); } function expectCaseCtas() { const destinations = screen .getAllByRole("link") .map((link) => link.getAttribute("href")); expect(destinations).toContain("/#contact"); expect(destinations).toContain("/servicos"); expect(destinations).not.toContain("/curriculo"); } it("keeps the six public params and factual case metadata", async () => { const staticSlugs = generateStaticParams().map(({ slug }) => slug); expect(staticSlugs).toHaveLength(6); expect(new Set(staticSlugs).size).toBe(6); expect([...staticSlugs].sort()).toEqual([...publicCaseIds].sort()); const caseStudy = getCase("as-servicos"); const url = `${siteUrl}/cases/${caseStudy.id}`; const metadata = await generateMetadata({ params: Promise.resolve({ slug: caseStudy.id }), }); expect(metadata).toMatchObject({ title: expect.stringContaining(caseStudy.title), description: caseStudy.summary, alternates: { canonical: url }, openGraph: { title: caseStudy.title, description: caseStudy.summary, url, }, twitter: { title: caseStudy.title, description: caseStudy.summary, }, }); expect(metadata).not.toHaveProperty("keywords"); }); it("renders the authorized detailed case in order with traceable sources", async () => { const caseStudy = getCase("as-servicos"); if (!caseStudy.detail) throw new Error("Missing detailed case content"); const ui = await CasePage({ params: Promise.resolve({ slug: caseStudy.id }), }); const { container } = render(ui); const heading = screen.getByRole("heading", { level: 1 }); const answer = screen.getByText(caseStudy.detail.answer.text); expect(heading.nextElementSibling).toBe(answer); const sectionNames = [ "Desafio verificável", "Decisões de implementação", "Limites desta evidência", "Fontes", ]; const sectionHeadings = sectionNames.map((name) => screen.getByRole("heading", { level: 2, name }), ); for (let index = 1; index < sectionHeadings.length; index += 1) { expect( sectionHeadings[index - 1].compareDocumentPosition( sectionHeadings[index], ) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); } const challengeSection = screen .getByRole("heading", { level: 2, name: "Desafio verificável" }) .closest("section"); if (!challengeSection) throw new Error("Missing challenge section"); const challengeText = within(challengeSection).getByText( caseStudy.detail.challenge.text, ); expect(challengeText).toBeVisible(); expect(screen.getAllByText(caseStudy.impact)).toEqual([challengeText]); for (const statement of caseStudy.detail.implementation) { expect(screen.getByText(statement.text)).toBeVisible(); } for (const boundary of caseStudy.detail.boundaries) { const boundaryText = screen.getByText(boundary.text); expect(boundaryText).toBeVisible(); expect(boundaryText.closest("a")).toBeNull(); } const sourceLinks = screen.getAllByRole("link", { name: /^Fonte:/ }); expect(sourceLinks).toHaveLength(caseStudy.detail.sources.length); for (const source of caseStudy.detail.sources) { const link = screen.getByRole("link", { name: `Fonte: ${source.label}` }); expect(link).toHaveAttribute("href", source.url); expect(link).toHaveAttribute("target", "_blank"); expect(link).toHaveAttribute("rel", expect.stringMatching(/noreferrer/)); expect(source.url).toMatch(/\/blob\/[0-9a-f]{40}\//); const sha = source.url.match(/\/blob\/([0-9a-f]{40})\//)?.[1]; if (!sha) throw new Error(`Missing source SHA for ${source.id}`); expect(within(link.closest("li") ?? link).getByText(sha)).toBeVisible(); } expectCaseCtas(); const graph = readJsonLdEntities(container); expect(graph.map((node) => node["@type"])).toEqual(["BreadcrumbList"]); expect(JSON.stringify(graph)).not.toMatch( /Article|FAQPage|ProfessionalService|LocalBusiness/, ); const breadcrumb = graph[0] as { itemListElement: Array<{ "@type": string; position: number; name: string; item: string; }>; }; const canonicalUrl = `${siteUrl}/cases/${caseStudy.id}`; expect(breadcrumb.itemListElement).toEqual([ { "@type": "ListItem", position: 1, name: "Início", item: siteUrl }, { "@type": "ListItem", position: 2, name: "Cases", item: `${siteUrl}/cases`, }, { "@type": "ListItem", position: 3, name: caseStudy.title, item: canonicalUrl, }, ]); const visibleBreadcrumb = screen.getByRole("navigation", { name: "Breadcrumb", }); expect( within(visibleBreadcrumb) .getAllByRole("link") .map((link) => ({ name: link.textContent, href: link.getAttribute("href"), current: link.getAttribute("aria-current"), })), ).toEqual([ { name: "Início", href: "/", current: null }, { name: "Cases", href: "/cases", current: null }, { name: caseStudy.title, href: `/cases/${caseStudy.id}`, current: "page", }, ]); }); it("keeps the fallback concise and links its declared services", async () => { const caseStudy = getCase("gcc-installer"); expect(caseStudy.detail).toBeUndefined(); const ui = await CasePage({ params: Promise.resolve({ slug: caseStudy.id }), }); const { container } = render(ui); const heading = screen.getByRole("heading", { level: 1 }); expect(heading.nextElementSibling).toHaveTextContent(caseStudy.summary); expect(screen.getByText(caseStudy.impact)).toBeVisible(); for (const technology of caseStudy.stack) { expect(screen.getByText(technology)).toBeVisible(); } expect(screen.queryByRole("link", { name: "Abrir demonstração" })).toBeNull(); expect(screen.getByRole("link", { name: "Ver código" })).toHaveAttribute( "href", caseStudy.githubUrl, ); for (const name of [ "Desafio verificável", "Decisões de implementação", "Limites desta evidência", "Fontes", ]) { expect(screen.queryByRole("heading", { name })).toBeNull(); } expect(screen.queryByRole("link", { name: /^Fonte:/ })).toBeNull(); const expectedServices = servicePages .filter(({ relatedCaseIds }) => relatedCaseIds.includes(caseStudy.id)) .map(({ slug, navLabel }) => ({ name: `Ver serviço relacionado: ${navLabel}`, href: `/servicos/${slug}`, })) .sort((a, b) => a.href.localeCompare(b.href)); const relatedLinks = screen.getAllByRole("link", { name: /^Ver serviço relacionado:/, }); expect( relatedLinks .map((link) => ({ name: link.textContent, href: link.getAttribute("href"), })) .sort((a, b) => (a.href ?? "").localeCompare(b.href ?? "")), ).toEqual(expectedServices); expectCaseCtas(); expect(readJsonLdEntities(container).map((node) => node["@type"])).toEqual([ "BreadcrumbList", ]); }); it("keeps the hub cards, schema and distinct intents aligned", () => { const { container } = render(); const heading = screen.getByRole("heading", { level: 1 }); expect(heading.nextElementSibling).toHaveTextContent(/seis projetos/i); const caseLinks = screen.getAllByRole("link", { name: "Ler case" }); const caseHrefs = caseLinks.map((link) => link.getAttribute("href")); expect(caseHrefs).toHaveLength(6); expect(new Set(caseHrefs).size).toBe(6); expect([...caseHrefs].sort()).toEqual( publicCaseIds.map((id) => `/cases/${id}`).sort(), ); expectCaseCtas(); const visibleBreadcrumb = screen.getByRole("navigation", { name: "Breadcrumb", }); expect( within(visibleBreadcrumb) .getAllByRole("link") .map((link) => [link.textContent, link.getAttribute("href")]), ).toEqual([ ["Início", "/"], ["Cases", "/cases"], ]); const graph = readJsonLdEntities(container); expect(graph.map((node) => node["@type"])).toEqual([ "CollectionPage", "ItemList", "BreadcrumbList", ]); const itemList = graph[1] as { itemListElement: Array<{ "@type": string; position: number; name: string; url: string; }>; }; const visibleItems = caseLinks.map((link, index) => { const href = link.getAttribute("href"); const expected = publicCases.find(({ id }) => href === `/cases/${id}`); if (!expected) throw new Error(`Unexpected visible case link ${href}`); const card = link.closest("article"); if (!card) throw new Error(`Missing visible card for ${href}`); expect(within(card).getByText(expected.title)).toBeVisible(); return { "@type": "ListItem", position: index + 1, name: expected.title, url: `${siteUrl}${href}`, }; }); expect(itemList.itemListElement).toEqual(visibleItems); });