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:
1 parent
3756905a1b
commit
6e1022b58c
44 files changed
+2691
-1424
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user