feat(cases): cases de clientes, serviços ligados à prova e SEO por página

Cases da SEV Moda Íntima e da Lavação 047; case no formato cliente, problema, entrega, mídia e contato, com detalhes técnicos recolhidos; sem GitHub, ressalvas e fontes de commit. /cases separa clientes e Laboratório. Serviços mostram o case de cliente certo e o bloco de investimento e prazo só aparece preenchido. Metadata social própria por página, not-found com noindex, sitemap com /ia.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 03:31:57 -03:00
1 parent 3756905a1b
commit 6e1022b58c
44 files changed
+2691 -1424

No files matched your search

+148 -26
View File
@@ -1,33 +1,26 @@
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 {
caseStudies,
getRelatedCases,
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,
}));
async function renderService(slug: string) {
const ui = await ServicePage({ params: Promise.resolve({ slug }) });
return render(ui);
}
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);
},
}));
function isBefore(first: Element, second: Element) {
return Boolean(
first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING,
);
}
const service = getServicePage("consultoria-nextjs");
if (!service) throw new Error("Missing representative service");
@@ -86,10 +79,7 @@ it("keeps static params and route metadata canonical", async () => {
});
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 { container } = await renderService(service.slug);
const answer = screen.getByText(service.answer);
expect(answer).toBeVisible();
@@ -153,7 +143,15 @@ it("renders answer-first consulting, reciprocal case links and minimal schema",
"rel",
expect.stringMatching(/noreferrer/),
);
expect(externalLink.getAttribute("href")).not.toMatch(/github/i);
}
const hrefs = screen
.getAllByRole("link")
.map((link) => link.getAttribute("href") ?? "");
expect(hrefs).not.toContain("/#contact");
expect(container.textContent).not.toMatch(
/Exemplos ilustrativos|não projetos já entregues|Sem promessas|indispon|Ver código/i,
);
const graph = readJsonLdEntities(container);
expect(graph.map((node) => node["@type"])).toEqual([
@@ -243,3 +241,127 @@ it("keeps hub cards and structured items aligned", () => {
],
});
});
it("shows the client projects with covers on the hub, without disclaimers", () => {
const { container } = render(<ServicesIndexPage />);
for (const id of [
"sev-modaintima",
"lavacao-047",
"sc-plus",
"as-servicos",
]) {
const link = container.querySelector(`a[href="/cases/${id}"]`);
expect(
link?.closest("article")?.querySelector(".ds-bframe img"),
).not.toBeNull();
}
expect(container.textContent).not.toMatch(
/sem promessas|Demonstração publicada|limites|indispon/i,
);
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);
});
it("shows the client cases first, right after the hero, with the first cover in a frame", async () => {
const { container } = await renderService("criacao-de-sites");
const siteService = getServicePage("criacao-de-sites");
if (!siteService) throw new Error("Missing service");
const casesHeading = screen.getByRole("heading", {
level: 2,
name: "Veja projetos parecidos, já prontos",
});
expect(
isBefore(screen.getByRole("heading", { level: 1 }), casesHeading),
).toBe(true);
expect(
isBefore(
casesHeading,
screen.getByRole("heading", { level: 2, name: "Para quem faz sentido" }),
),
).toBe(true);
const caseLinks = screen.getAllByRole("link", { name: /^Ler case:/ });
expect(caseLinks.map((link) => link.getAttribute("href"))).toEqual(
getRelatedCases(siteService.relatedCaseIds).map(({ id }) => `/cases/${id}`),
);
expect(caseLinks.map((link) => link.getAttribute("href"))).toEqual([
"/cases/sev-modaintima",
"/cases/lavacao-047",
"/cases/sc-plus",
"/cases/as-servicos",
]);
// pôster do primeiro case numa moldura no topo, no lugar do fundo verde
const hero = container.querySelector("section");
const heroFrame = hero?.querySelector(".ds-bframe");
expect(heroFrame).not.toBeNull();
expect(heroFrame?.querySelector("img")).toHaveAttribute(
"alt",
"Captura de tela do site SEV Moda Íntima",
);
expect(hero?.querySelector("a[href='/cases/sev-modaintima']")).not.toBeNull();
});
it("hides the related block and the hero frame when a service has no case", async () => {
const { container } = await renderService("tauri-rust");
expect(
screen.queryByRole("heading", {
name: "Veja projetos parecidos, já prontos",
}),
).toBeNull();
expect(screen.queryByRole("link", { name: /^Ler case:/ })).toBeNull();
expect(
screen.queryByRole("link", { name: "Ver projetos parecidos" }),
).toBeNull();
expect(container.querySelector(".ds-bframe")).toBeNull();
// GCC Installer (Python e Tkinter) não pertence a um serviço de Tauri e Rust
expect(container.textContent).not.toMatch(/GCC|Tkinter/);
});
it("labels the Twins Lab prototype as a laboratory project on the IoT service", async () => {
const { container } = await renderService("integracao-hardware-software-iot");
const caseLinks = screen.getAllByRole("link", { name: /^Ler case:/ });
expect(caseLinks.map((link) => link.getAttribute("href"))).toEqual([
"/cases/twins-lab",
]);
expect(screen.getByText(/Protótipo de laboratório/)).toBeVisible();
// sem captura gravada, não há moldura nem imagem
expect(container.querySelector(".ds-bframe")).toBeNull();
});
it("renders the offer block only when some field is filled in", async () => {
const empty = await renderService("criacao-de-sites");
expect(
screen.queryByRole("heading", { name: "Investimento e prazo" }),
).toBeNull();
empty.unmount();
const offerService = getServicePage("criacao-de-sites");
if (!offerService) throw new Error("Missing service");
offerService.typicalTimeline = "Texto de prazo de teste";
try {
await renderService("criacao-de-sites");
expect(
screen.getByRole("heading", { level: 2, name: "Investimento e prazo" }),
).toBeVisible();
expect(screen.getByText("Prazo típico")).toBeVisible();
expect(screen.getByText("Texto de prazo de teste")).toBeVisible();
expect(screen.queryByText("A partir de")).toBeNull();
expect(screen.queryByText("Garantia")).toBeNull();
} finally {
delete offerService.typicalTimeline;
}
});
it("keeps related case ids resolving to real cases", () => {
for (const id of service.relatedCaseIds) {
expect(caseStudies.some((item) => item.id === id)).toBe(true);
}
});