CI / verify (push) Failing after 5m9s
Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
257 lines
7.7 KiB
TypeScript
257 lines
7.7 KiB
TypeScript
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<string, unknown>) => {
|
|
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<string, unknown>[] {
|
|
const scripts = container.querySelectorAll<HTMLScriptElement>(
|
|
'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<string, unknown>;
|
|
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: service.ctaLabel,
|
|
});
|
|
expect(primaryCtas).toHaveLength(2);
|
|
for (const cta of primaryCtas) {
|
|
expect(cta).toHaveAttribute("href", "/#contact");
|
|
}
|
|
|
|
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(<ServicesIndexPage />);
|
|
|
|
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"],
|
|
]);
|
|
|
|
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` },
|
|
],
|
|
});
|
|
});
|