import { fireEvent, render, screen, within } from "@testing-library/react"; import { setNavigation } from "@/__mocks__/next/navigation"; import { buttonLabels, caseStudies, clientCasesForListing, getRelatedServices, siteUrl, } from "@/lib/site-content"; import { similarProjectSubject } from "@/lib/whatsapp"; import CasesPage from "../page"; import CasePage, { generateMetadata, generateStaticParams } from "./page"; beforeEach(() => { setNavigation({ pathname: "/cases" }); }); 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, com o mesmo rótulo * ("Falar sobre meu projeto") no case de cliente e no de laboratório. A mensagem é a mesma frase nos dois, a do cabeçalho: * `similarProjectSubject(título)`. Um botão só por página (o do fecho): sem dois "Falar sobre meu projeto" seguidos. */ function expectContactAndNoDeadLinks(caseStudy: { id: string; 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 ctas = screen.getAllByRole("link", { name: buttonLabels.contact }); expect(ctas).toHaveLength(1); for (const cta of ctas) { const destination = new URL(cta.getAttribute("href") ?? ""); expect(destination.origin).toBe("https://wa.me"); expect(destination.searchParams.get("text")).toContain( similarProjectSubject(caseStudy.title), ); expect(cta).toHaveAttribute("target", "_blank"); expect(cta).toHaveAttribute("rel", expect.stringContaining("noopener")); // limão preenchido só no contato: o botão de play e os demais não o usam expect(cta.className).toContain("bg-[var(--slash)]"); } } 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 { metaTitle: title, metaDescription: description } = caseStudy; const metadata = await generateMetadata({ params: Promise.resolve({ slug: caseStudy.id }), }); expect(metadata).toMatchObject({ title, description, alternates: { canonical: url }, openGraph: { title, description, url }, twitter: { title, description }, }); 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(isBefore(heading, screen.getByText(caseStudy.summary))).toBe(true); expect(screen.getByText(/Case de cliente/)).toBeVisible(); const sectionNames = [ "O cliente e o contexto", "O problema", "O que foi feito", "Como ficou", "O que o cliente passou a poder fazer", "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: o pôster do computador e do celular com um botão de play; nenhum vídeo parado com controles expect(container.querySelector("video")).toBeNull(); const desktopPlay = screen.getByRole("button", { name: `Reproduzir o vídeo do site ${caseStudy.title} no computador`, }); const mobilePlay = screen.getByRole("button", { name: `Reproduzir o vídeo do site ${caseStudy.title} no celular`, }); const mediaSection = screen .getByRole("heading", { level: 2, name: "Como ficou" }) .closest("section"); expect(mediaSection?.querySelector(".ds-bframe")).toContainElement( desktopPlay, ); expect(mediaSection?.querySelector(".ds-phone")).toContainElement(mobilePlay); expect( screen.getByRole("link", { name: `Abrir o site ${caseStudy.title}` }), ).toHaveAttribute("href", caseStudy.demoUrl); // um botão de contato só, no fecho: as capacidades (logo depois de "Como ficou") não repetem o botão expectContactAndNoDeadLinks(caseStudy); const capabilitiesSection = sectionHeadings[4].closest("section"); expect( within(capabilitiesSection as HTMLElement).queryByRole("link", { name: buttonLabels.contact, }), ).toBeNull(); const closingSection = sectionHeadings[5].closest("section"); expect(closingSection).toContainElement( within(closingSection as HTMLElement).getByRole("link", { name: buttonLabels.contact, }), ); for (const capability of caseStudy.capabilities ?? []) { expect( within(capabilitiesSection as HTMLElement).getByText(capability), ).toBeVisible(); } // detalhes técnicos: recolhidos, junto do conteúdo (logo depois do que foi feito), não numa faixa à parte 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 doneSection = sectionHeadings[2].closest("section"); expect(doneSection).toContainElement(details); expect(isBefore(screen.getByText(caseStudy.deliveries[0]), details)).toBe( true, ); expect(isBefore(details, sectionHeadings[3])).toBe(true); expect(container.querySelectorAll("details")).toHaveLength(1); expect( container.querySelector("section[aria-label='Detalhes técnicos']"), ).toBeNull(); // 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([ "CreativeWork", "BreadcrumbList", ]); expect(JSON.stringify(graph)).not.toMatch( /Article|FAQPage|ProfessionalService|LocalBusiness/, ); const breadcrumb = graph[1] 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: "Trilha de navegação", }); 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); // um botão de contato só, no fecho, mesmo com "Como ficou" na página expectContactAndNoDeadLinks(caseStudy); unmount(); } }); it("labels a laboratory case, skips the media block and shows its service as a card", 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(container.querySelector(".ds-bframe")).toBeNull(); expect(screen.queryByRole("link", { name: /^Abrir o site/ })).toBeNull(); expect(screen.queryByText(/indispon/i)).toBeNull(); const relatedLinks = screen.getAllByRole("link", { name: /^Ver o serviço:/, }); expect(relatedLinks.map((link) => link.getAttribute("href"))).toEqual( getRelatedServices(caseStudy.id).map(({ slug }) => `/servicos/${slug}`), ); expect(relatedLinks).toHaveLength(1); expectContactAndNoDeadLinks(caseStudy); }); it("turns the contact of a laboratory case into the 'Fale sobre o seu caso' invitation, with the same button label as a client case", async () => { for (const id of labCaseIds) { const { unmount } = await renderCase(id); expect(screen.queryByText("Quer um projeto parecido com este?")).toBeNull(); expect( screen.queryByRole("link", { name: /Conversar sobre um projeto parecido|Tem um problema parecido/, }), ).toBeNull(); expect( screen.getByRole("heading", { level: 2, name: "Fale sobre o seu caso" }), ).toBeVisible(); // um botão só, com ou sem "Como ficou" (NutriScan tem) expectContactAndNoDeadLinks(getCase(id)); // o laboratório não tem "o que o cliente passou a poder fazer" expect( screen.queryByRole("heading", { name: "O que o cliente passou a poder fazer", }), ).toBeNull(); unmount(); } }); it("writes what the client can do now right after 'Como ficou', with no contact button of its own, for every client case", async () => { for (const id of clientCaseIds) { const caseStudy = getCase(id); const { unmount } = await renderCase(id); const media = screen.getByRole("heading", { level: 2, name: "Como ficou" }); const capabilities = screen.getByRole("heading", { level: 2, name: "O que o cliente passou a poder fazer", }); // a seção vem logo depois da mídia, sem outra seção no meio const mediaSection = media.closest("section"); const capabilitiesSection = capabilities.closest("section"); expect(mediaSection?.nextElementSibling).toBe(capabilitiesSection); const items = within(capabilitiesSection as HTMLElement) .getAllByRole("listitem") .map((item) => item.textContent); expect(items).toEqual(caseStudy.capabilities); expect(items.length).toBeGreaterThanOrEqual(2); expect(items.length).toBeLessThanOrEqual(3); // sem número inventado expect(items.join(" ")).not.toMatch(/\d/); // o único botão de contato é o do fecho, depois da lista: não há dois "Falar sobre meu projeto" seguidos expect( within(capabilitiesSection as HTMLElement).queryByRole("link", { name: buttonLabels.contact, }), ).toBeNull(); const closingSection = screen .getByRole("heading", { level: 2, name: "Quer um projeto parecido com este?", }) .closest("section"); expect(capabilitiesSection?.nextElementSibling).toBe(closingSection); expect( within(closingSection as HTMLElement).getAllByRole("link", { name: buttonLabels.contact, }), ).toHaveLength(1); unmount(); } // a frase do dono da Lavação 047 vem dos dados do projeto await renderCase("lavacao-047"); expect( screen.getByText( "O dono muda preços, textos, fotos e o mapa sem pedir a ninguém, e a mudança aparece no site sem nova publicação.", ), ).toBeVisible(); }); 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.getByRole("button", { name: `Reproduzir o 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("shows at most two related services, as cards under the contact button", async () => { // lavacao-047 tem três serviços ligados; só os dois primeiros viram cards const sev = getCase("lavacao-047"); const all = getRelatedServices(sev.id); expect(all.length).toBeGreaterThan(2); const { unmount } = await renderCase(sev.id); const cards = screen.getAllByRole("link", { name: /^Ver o serviço:/ }); expect(cards).toHaveLength(2); expect(cards.map((link) => link.getAttribute("href"))).toEqual( all.slice(0, 2).map(({ slug }) => `/servicos/${slug}`), ); for (const link of cards) { const card = link.closest("article"); expect(card).toHaveClass("ds-card"); expect( within(card as HTMLElement).getByRole("heading", { level: 3 }), ).toBeVisible(); } expect( screen.queryByRole("link", { name: /^Ver serviço relacionado:/ }), ).toBeNull(); // o botão do fecho (o único) vem antes dos cartões const contact = screen.getByRole("link", { name: buttonLabels.contact }); expect(isBefore(contact, cards[0])).toBe(true); unmount(); // sc-plus e a SEV têm dois (a SEV liga à oferta de site e à de sistema); a AS Serviços, um for (const [id, expected] of [ ["sc-plus", 2], ["as-servicos", 1], ["sev-modaintima", 2], ] as const) { const { unmount: unmountCase } = await renderCase(id); const links = screen.getAllByRole("link", { name: /^Ver o serviço:/ }); expect(links).toHaveLength(expected); expect(links.map((link) => link.getAttribute("href"))).toEqual( getRelatedServices(id) .slice(0, 2) .map(({ slug }) => `/servicos/${slug}`), ); expectContactAndNoDeadLinks(getCase(id)); unmountCase(); } }); it("links the SEV case to the site offer and to the system offer, each by the name of the offer", async () => { await renderCase("sev-modaintima"); const links = screen.getAllByRole("link", { name: /^Ver o serviço:/ }); expect(links.map((link) => link.getAttribute("href"))).toEqual([ "/servicos/criacao-de-sites", "/servicos/engenharia-de-software", ]); expect(links.map((link) => link.textContent)).toEqual([ "Ver o serviço: Site que traz pedidos pelo WhatsApp", "Ver o serviço: Sistema para tirar a operação da planilha", ]); }); it("writes the CreativeWork of a client case with creator, client and keywords", async () => { const caseStudy = getCase("lavacao-047"); const { container } = await renderCase(caseStudy.id); const work = readJsonLdEntities(container).find( (node) => node["@type"] === "CreativeWork", ); expect(work).toEqual({ "@type": "CreativeWork", "@id": `${siteUrl}/cases/lavacao-047#creative-work`, name: "Lavação 047: Site e painel para lavação automotiva", description: caseStudy.metaDescription, url: `${siteUrl}/cases/lavacao-047`, image: `${siteUrl}/cases/lavacao-047/opengraph-image/lavacao-047`, inLanguage: "pt-BR", creator: { "@id": `${siteUrl}/#person` }, sourceOrganization: { "@type": "Organization", name: "Lavação 047", url: "https://landing-page-lavacao047.vercel.app", }, keywords: caseStudy.stack.join(", "), }); }); it("writes the CreativeWork of a laboratory case without a client", async () => { const caseStudy = getCase("twins-lab"); const { container } = await renderCase(caseStudy.id); const work = readJsonLdEntities(container).find( (node) => node["@type"] === "CreativeWork", ); expect(work).toMatchObject({ name: "Twins Lab: Painel ao vivo de um reator simulado", url: `${siteUrl}/cases/twins-lab`, creator: { "@id": `${siteUrl}/#person` }, keywords: "Next.js, TypeScript, NextAuth, MongoDB, SSE", }); expect(work).not.toHaveProperty("sourceOrganization"); }); it("puts the project cover next to the title, and below it on a phone, in a browser frame", async () => { const caseStudy = getCase("sev-modaintima"); const { container } = await renderCase(caseStudy.id); const hero = container.querySelector("section"); if (!hero) throw new Error("Missing hero"); const frame = hero.querySelector(".ds-bframe"); if (!frame) throw new Error("Missing hero frame"); expect(frame.querySelector("img")).toHaveAttribute( "alt", `Captura de tela do site ${caseStudy.title}`, ); expect(frame.querySelector(".ds-bframe-url")).toHaveTextContent( "sevmodaintima.com.br", ); // a capa abre a página: carrega antes das demais imagens expect(frame.querySelector("img")).not.toHaveAttribute("loading", "lazy"); // ordem do documento (a do celular): título, mídia, resumo const heading = screen.getByRole("heading", { level: 1 }); const summary = screen.getByText(caseStudy.summary); expect(hero).toContainElement(summary); expect(isBefore(heading, frame)).toBe(true); expect(isBefore(frame, summary)).toBe(true); // no computador: duas colunas, texto à esquerda e mídia à direita const grid = frame.closest(".grid"); expect(grid?.className).toContain("lg:grid-cols-["); expect(frame.parentElement?.className).toContain("lg:col-start-2"); expect(heading.parentElement?.className).toContain("lg:col-start-1"); }); it("keeps the hero in one column when the case has no capture", async () => { const { container } = await renderCase("twins-lab"); const hero = container.querySelector("section"); expect(hero?.querySelector(".ds-bframe")).toBeNull(); expect(hero?.querySelector(".grid")).toBeNull(); }); it("shows the optional facts only when they are filled in", async () => { const caseStudy = getCase("lavacao-047"); const empty = await renderCase(caseStudy.id); expect(screen.queryByRole("list", { name: "Fatos do projeto" })).toBeNull(); empty.unmount(); caseStudy.facts = ["Fato de teste um", "Fato de teste dois"]; try { await renderCase(caseStudy.id); const list = screen.getByRole("list", { name: "Fatos do projeto" }); expect(within(list).getAllByRole("listitem")).toHaveLength(2); expect(within(list).getByText("Fato de teste um")).toBeVisible(); expect(isBefore(screen.getByText(caseStudy.summary), list)).toBe(true); } finally { delete caseStudy.facts; } }); it("starts the video only when the visitor presses play, and moves focus to it", async () => { const caseStudy = getCase("sev-modaintima"); const { container } = await renderCase(caseStudy.id); const play = screen.getByRole("button", { name: `Reproduzir o vídeo do site ${caseStudy.title} no computador`, }); expect(play).toBeVisible(); expect(play.className).not.toContain("--slash"); expect(container.querySelector("video")).toBeNull(); fireEvent.click(play); const video = screen.getByLabelText( `Vídeo do site ${caseStudy.title} no computador`, ); expect(video.tagName).toBe("VIDEO"); expect(video).toHaveAttribute( "poster", `/videos/${caseStudy.id}/desktop.webp`, ); expect(video).toHaveAttribute("controls"); expect(video).toHaveFocus(); expect( video.querySelectorAll("source[src^='/videos/sev-modaintima/']"), ).toHaveLength(2); expect(video.closest(".ds-bframe")).not.toBeNull(); expect( screen.queryByRole("button", { name: `Reproduzir o vídeo do site ${caseStudy.title} no computador`, }), ).toBeNull(); // o celular continua com o pôster até a pessoa tocar nele expect( screen.getByRole("button", { name: `Reproduzir o vídeo do site ${caseStudy.title} no celular`, }), ).toBeVisible(); }); it("gives the case page the same shell as the home, with no narrow max-width", async () => { const { container } = await renderCase("sev-modaintima"); for (const shell of container.querySelectorAll(".ds-section-shell")) { expect(shell.className).not.toMatch(/max-w-/); } }); 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(/Abra um case/); // o título e o subtítulo não repetem "Veja o que já" expect(heading.nextElementSibling).not.toHaveTextContent(/Veja o que já/); 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, na ordem da vitrine (SEV primeiro): capa (pôster) em moldura de navegador e botão "Ver case" em contorno const listedIds = clientCasesForListing.map(({ id }) => id); expect([...listedIds].sort()).toEqual([...clientCaseIds].sort()); expect(listedIds).toEqual([ "sev-modaintima", "lavacao-047", "sc-plus", "as-servicos", ]); const clientLinks = listedIds.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: `${buttonLabels.case} ${getCase(id).title}`, }); }); expect(clientLinks.map((link) => link.getAttribute("href"))).toEqual( listedIds.map((id) => `/cases/${id}`), ); // laboratório: lista compacta, uma linha por projeto (nome, o que é, "Projeto próprio" e o link), sem capa nem cartão const labLinks = within( labHeading.closest("section") as HTMLElement, ).getAllByRole("link", { name: /^Ver case / }); expect(labLinks.map((link) => link.getAttribute("href"))).toEqual( labCaseIds.map((id) => `/cases/${id}`), ); const labList = labLinks[0].closest("ul"); expect(labList?.querySelectorAll(":scope > li")).toHaveLength( labCaseIds.length, ); expect(labList?.querySelector("article, .ds-card, .ds-bframe")).toBeNull(); labLinks.forEach((link, index) => { const row = link.closest("li"); const caseStudy = getCase(labCaseIds[index]); expect(isBefore(labHeading, link)).toBe(true); expect(link).toHaveAccessibleName(`Ver case ${caseStudy.title}`); expect( within(row as HTMLElement).getByRole("heading", { level: 3, name: caseStudy.title, }), ).toBeVisible(); expect( within(row as HTMLElement).getByText(caseStudy.subtitle), ).toBeVisible(); expect( within(row as HTMLElement).getByText("Projeto próprio"), ).toBeVisible(); // sem o resumo longo do cartão antigo expect(row).not.toHaveTextContent(caseStudy.summary); }); // 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: buttonLabels.contact }); expect(new URL(cta.getAttribute("href") ?? "").origin).toBe("https://wa.me"); const visibleBreadcrumb = screen.getByRole("navigation", { name: "Trilha de navegação", }); 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"); // cartão do cliente ou linha da lista do laboratório const card = link.closest("article, li"); if (!card) throw new Error(`Missing visible card for ${href}`); const expected = getCase((href ?? "").replace("/cases/", "")); expect(card).toHaveTextContent(expected.title); return { "@type": "ListItem", position: index + 1, name: expected.title, url: `${siteUrl}${href}`, }; }); expect(itemList.itemListElement).toEqual(visibleItems); });