import { render, screen, within } from "@testing-library/react"; import { caseStudies, getRelatedServices, siteUrl } from "@/lib/site-content"; import CasesPage from "../page"; import CasePage, { generateMetadata, generateStaticParams } from "./page"; jest.mock("next/navigation", () => ({ notFound: jest.fn() })); const clientCaseIds = [ "sev-modaintima", "lavacao-047", "sc-plus", "as-servicos", ]; const labCaseIds = [ "nutriscan", "twins-lab", "reddit-explorer", "gcc-installer", ]; const publicCaseIds = [...clientCaseIds, ...labCaseIds]; function getCase(id: string) { const caseStudy = caseStudies.find((item) => item.id === id); if (!caseStudy) throw new Error(`Missing case ${id}`); return caseStudy; } async function renderCase(id: string) { const ui = await CasePage({ params: Promise.resolve({ slug: id }) }); return render(ui); } function isBefore(first: Element, second: Element) { return Boolean( first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING, ); } 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]; }); } /** Nenhum link para /#contact nem para o GitHub; o contato é o WhatsApp com o contexto do case. */ function expectContactAndNoDeadLinks(caseStudy: { title: string }) { const hrefs = screen .getAllByRole("link") .map((link) => link.getAttribute("href") ?? ""); expect(hrefs).not.toContain("/#contact"); expect(hrefs.some((href) => /github/i.test(href))).toBe(false); const cta = screen.getByRole("link", { name: "Conversar sobre um projeto parecido", }); const destination = new URL(cta.getAttribute("href") ?? ""); expect(destination.origin).toBe("https://wa.me"); expect(destination.searchParams.get("text")).toContain( `um projeto parecido com o da ${caseStudy.title}`, ); expect(cta).toHaveAttribute("target", "_blank"); expect(cta).toHaveAttribute("rel", expect.stringContaining("noopener")); } it("exposes the eight public cases, the two new client cases among them", async () => { const staticSlugs = generateStaticParams().map(({ slug }) => slug); expect(staticSlugs).toHaveLength(8); expect(new Set(staticSlugs).size).toBe(8); expect([...staticSlugs].sort()).toEqual([...publicCaseIds].sort()); expect(staticSlugs).toContain("sev-modaintima"); expect(staticSlugs).toContain("lavacao-047"); for (const id of ["sev-modaintima", "lavacao-047", "as-servicos"]) { const caseStudy = getCase(id); const url = `${siteUrl}/cases/${caseStudy.id}`; const title = `${caseStudy.title}: ${caseStudy.subtitle}`; const metadata = await generateMetadata({ params: Promise.resolve({ slug: caseStudy.id }), }); expect(metadata).toMatchObject({ title, description: caseStudy.summary, alternates: { canonical: url }, openGraph: { title, description: caseStudy.summary, url }, twitter: { title, description: caseStudy.summary }, }); expect(metadata).not.toHaveProperty("keywords"); } }); it("renders a client case in the sales order, with media, contact and collapsed technical details", async () => { const caseStudy = getCase("sev-modaintima"); const { container } = await renderCase(caseStudy.id); const heading = screen.getByRole("heading", { level: 1 }); expect(heading).toHaveTextContent(caseStudy.headline); expect(heading.nextElementSibling).toHaveTextContent(caseStudy.summary); expect(screen.getByText(/Case de cliente/)).toBeVisible(); const sectionNames = [ "O cliente e o contexto", "O problema", "O que foi feito", "Como ficou", "Quer um projeto parecido com este?", ]; const sectionHeadings = sectionNames.map((name) => screen.getByRole("heading", { level: 2, name }), ); for (let index = 1; index < sectionHeadings.length; index += 1) { expect(isBefore(sectionHeadings[index - 1], sectionHeadings[index])).toBe( true, ); } expect(isBefore(heading, sectionHeadings[0])).toBe(true); expect(screen.getByText(caseStudy.context)).toBeVisible(); expect(screen.getByText(caseStudy.problem)).toBeVisible(); for (const delivery of caseStudy.deliveries) { expect(screen.getByText(delivery)).toBeVisible(); } // sem dado real, não há seção de resultado expect(screen.queryByRole("heading", { name: "Resultado" })).toBeNull(); // mídia: pôster e vídeo do computador e do celular const desktopVideo = screen.getByLabelText( `Vídeo do site ${caseStudy.title} no computador`, ); const mobileVideo = screen.getByLabelText( `Vídeo do site ${caseStudy.title} no celular`, ); expect(desktopVideo).toHaveAttribute( "poster", `/videos/${caseStudy.id}/desktop.webp`, ); expect(mobileVideo).toHaveAttribute( "poster", `/videos/${caseStudy.id}/mobile.webp`, ); expect( desktopVideo.querySelectorAll("source[src^='/videos/sev-modaintima/']"), ).toHaveLength(2); expect(container.querySelector(".ds-bframe")).toContainElement(desktopVideo); expect(container.querySelector(".ds-phone")).toContainElement(mobileVideo); expect( screen.getByRole("link", { name: `Abrir o site ${caseStudy.title}` }), ).toHaveAttribute("href", caseStudy.demoUrl); expectContactAndNoDeadLinks(caseStudy); // detalhes técnicos: recolhidos, no fim da página const details = container.querySelector("details"); if (!details) throw new Error("Missing technical details"); expect(details).not.toHaveAttribute("open"); expect(within(details).getByText("Detalhes técnicos")).toBeVisible(); for (const technology of caseStudy.stack) { expect(within(details).getByText(technology)).not.toBeVisible(); } for (const decision of caseStudy.decisions) { expect(within(details).getByText(decision)).not.toBeVisible(); } const contactHeading = sectionHeadings[sectionHeadings.length - 1]; expect(isBefore(contactHeading, details)).toBe(true); expect(container.querySelectorAll("details")).toHaveLength(1); const lastSection = container.querySelector("article")?.lastElementChild; expect(lastSection).toContainElement(details); // o que saiu: desafio verificável, limites da evidência, fontes e commits 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(); expect(container.textContent).not.toMatch(/Commit:|[0-9a-f]{40}/); 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("renders the two new client cases from the portfolio data", async () => { for (const id of ["sev-modaintima", "lavacao-047"]) { const caseStudy = getCase(id); const { unmount } = await renderCase(id); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( caseStudy.headline, ); expect( screen.getByRole("link", { name: `Abrir o site ${caseStudy.title}` }), ).toHaveAttribute("href", caseStudy.demoUrl); expectContactAndNoDeadLinks(caseStudy); unmount(); } }); it("labels a laboratory case, skips the media block and links its service", async () => { const caseStudy = getCase("twins-lab"); const { container } = await renderCase(caseStudy.id); expect(screen.getByText(/Projeto de laboratório/)).toBeVisible(); expect( screen.getByRole("heading", { level: 2, name: "O contexto do projeto" }), ).toBeVisible(); expect(screen.queryByRole("heading", { name: "Como ficou" })).toBeNull(); expect(container.querySelector("video")).toBeNull(); expect(screen.queryByRole("link", { name: /^Abrir o site/ })).toBeNull(); expect(screen.queryByText(/indispon/i)).toBeNull(); const relatedLinks = screen.getAllByRole("link", { name: /^Ver serviço relacionado:/, }); expect(relatedLinks.map((link) => link.getAttribute("href"))).toEqual( getRelatedServices(caseStudy.id).map(({ slug }) => `/servicos/${slug}`), ); expect(relatedLinks).toHaveLength(1); expectContactAndNoDeadLinks(caseStudy); }); it("shows the author's role and no site button for a client project without a public site", async () => { const caseStudy = getCase("as-servicos"); expect(caseStudy.demoUrl).toBeNull(); await renderCase(caseStudy.id); expect(screen.getByText("Minha participação:")).toBeVisible(); expect( screen.getByText(caseStudy.role ?? "", { exact: false }), ).toBeVisible(); expect(screen.queryByRole("link", { name: /^Abrir o site/ })).toBeNull(); expect(screen.queryByText(/indispon/i)).toBeNull(); // a capa continua nas molduras expect( screen.getByLabelText(`Vídeo do site ${caseStudy.title} no computador`), ).toBeInTheDocument(); }); it("shows the result section only when the case has real data", async () => { const caseStudy = getCase("lavacao-047"); const { unmount } = await renderCase(caseStudy.id); expect(screen.queryByRole("heading", { name: "Resultado" })).toBeNull(); unmount(); caseStudy.result = ["Dado real informado pelo cliente."]; try { await renderCase(caseStudy.id); const heading = screen.getByRole("heading", { level: 2, name: "Resultado", }); expect(screen.getByText("Dado real informado pelo cliente.")).toBeVisible(); expect( isBefore( screen.getByRole("heading", { level: 2, name: "Como ficou" }), heading, ), ).toBe(true); expect( isBefore( heading, screen.getByRole("heading", { level: 2, name: "Quer um projeto parecido com este?", }), ), ).toBe(true); } finally { delete caseStudy.result; } }); it("links every service related to a client case", async () => { const caseStudy = getCase("sc-plus"); const { container } = await renderCase(caseStudy.id); const relatedLinks = screen.getAllByRole("link", { name: /^Ver serviço relacionado:/, }); expect( relatedLinks .map((link) => link.getAttribute("href")) .sort((a, b) => (a ?? "").localeCompare(b ?? "")), ).toEqual( getRelatedServices(caseStudy.id) .map(({ slug }) => `/servicos/${slug}`) .sort(), ); expectContactAndNoDeadLinks(caseStudy); expect(readJsonLdEntities(container).map((node) => node["@type"])).toEqual([ "BreadcrumbList", ]); }); it("lists client cases with a cover first, then the laboratory, and keeps the schema aligned", () => { const { container } = render(); const heading = screen.getByRole("heading", { level: 1 }); expect(heading.nextElementSibling).toHaveTextContent( /foi feito para empresas/i, ); expect(container.textContent).not.toMatch(/Selected work/i); const clientHeading = screen.getByRole("heading", { level: 2, name: "Projetos de clientes", }); const labHeading = screen.getByRole("heading", { level: 2, name: "Laboratório", }); expect(isBefore(heading, clientHeading)).toBe(true); expect(isBefore(clientHeading, labHeading)).toBe(true); expect(screen.getByText(/Projetos pessoais e de estudo/)).toBeVisible(); // cases de cliente: capa (pôster) em moldura de navegador e botão do case em contorno const clientLinks = clientCaseIds.map((id) => { const card = container .querySelector(`a[href="/cases/${id}"]`) ?.closest("article"); if (!card) throw new Error(`Missing client card ${id}`); expect(isBefore(clientHeading, card)).toBe(true); expect(isBefore(card, labHeading)).toBe(true); expect(card.querySelector(".ds-bframe img")).toHaveAttribute( "alt", `Captura de tela do site ${getCase(id).title}`, ); return within(card).getByRole("link", { name: "Ver o case" }); }); expect(clientLinks.map((link) => link.getAttribute("href"))).toEqual( clientCaseIds.map((id) => `/cases/${id}`), ); // laboratório: sem capa const labLinks = screen.getAllByRole("link", { name: "Ver o projeto" }); expect(labLinks.map((link) => link.getAttribute("href"))).toEqual( labCaseIds.map((id) => `/cases/${id}`), ); for (const link of labLinks) { const card = link.closest("article"); expect(isBefore(labHeading, link)).toBe(true); expect(card?.querySelector(".ds-bframe")).toBeNull(); } // contato pelo WhatsApp, sem /#contact nem GitHub const hrefs = screen .getAllByRole("link") .map((link) => link.getAttribute("href") ?? ""); expect(hrefs).not.toContain("/#contact"); expect(hrefs.some((href) => /github/i.test(href))).toBe(false); expect(hrefs).toContain("/servicos"); const cta = screen.getByRole("link", { name: "Conversar sobre meu projeto" }); expect(new URL(cta.getAttribute("href") ?? "").origin).toBe("https://wa.me"); 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 = [...clientLinks, ...labLinks].map((link, index) => { const href = link.getAttribute("href"); const card = link.closest("article"); if (!card) throw new Error(`Missing visible card for ${href}`); const expected = getCase((href ?? "").replace("/cases/", "")); expect(card.querySelector(".ds-kicker")).toHaveTextContent(expected.title); return { "@type": "ListItem", position: index + 1, name: expected.title, url: `${siteUrl}${href}`, }; }); expect(itemList.itemListElement).toEqual(visibleItems); });