feat(servicos): ofertas no topo de /servicos, capa por assunto e diagramas

/servicos abre com as 3 ofertas e depois outros serviços; H1 de criação de sites e de engenharia com o nome da oferta; capa de cada serviço pelo assunto (SEV fora do próprio case cai de ~11 para 2 imagens); diagrama de fluxo em SVG nos serviços sem case; cases com o que o cliente passou a poder fazer e contato logo depois de Como ficou; FAQ num componente só; lastmod por rota; ProfessionalService com areaServed.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 19:14:46 -03:00
1 parent e63f3c2ebd
commit fc44a69fb3
41 files changed
+2486 -550

No files matched your search

+73 -38
View File
@@ -1,5 +1,9 @@
import { render, screen, within } from "@testing-library/react";
import { clientCases } from "@/lib/site-content";
import {
buttonLabels,
clientCasesForListing,
labCases,
} from "@/lib/site-content";
import { getCaseMedia } from "./_components/case-media";
import CasesPage from "./page";
@@ -32,7 +36,7 @@ function clientCard(container: HTMLElement, id: string) {
describe("CasesPage: cases de cliente", () => {
it("path: client case without a desktop capture -> card keeps text and links but no cover", () => {
const [withoutCapture, withCapture] = clientCases;
const [withoutCapture, withCapture] = clientCasesForListing;
mockedGetCaseMedia.mockImplementation((id, device) =>
id === withoutCapture.id ? undefined : actualGetCaseMedia(id, device),
);
@@ -42,14 +46,12 @@ describe("CasesPage: cases de cliente", () => {
const bare = clientCard(container, withoutCapture.id);
expect(bare.querySelector(".ds-bframe")).toBeNull();
expect(bare.querySelector("img")).toBeNull();
expect(
within(bare).queryByRole("link", {
name: `Ver o case ${withoutCapture.title}`,
}),
).toBeNull();
expect(bare.querySelector("a[aria-hidden='true']")).toBeNull();
expect(within(bare).getByText(withoutCapture.headline)).toBeVisible();
expect(
within(bare).getByRole("link", { name: "Ver o case" }),
within(bare).getByRole("link", {
name: `${buttonLabels.case} ${withoutCapture.title}`,
}),
).toHaveAttribute("href", `/cases/${withoutCapture.id}`);
// os outros cases de cliente continuam com a capa
@@ -58,55 +60,88 @@ describe("CasesPage: cases de cliente", () => {
"alt",
`Captura de tela do site ${withCapture.title}`,
);
expect(screen.getAllByRole("link", { name: "Ver o case" })).toHaveLength(
clientCases.length,
});
it("path: every case -> one 'Ver case' button, named after its project, and no 'Ver o case' or 'Ler case'", () => {
render(<CasesPage />);
const buttons = screen.getAllByRole("link", { name: /^Ver case / });
expect(buttons).toHaveLength(
clientCasesForListing.length + labCases.length,
);
for (const link of buttons) {
expect(link.textContent?.startsWith(buttonLabels.case)).toBe(true);
}
expect(
screen.queryByRole("link", { name: /Ver o case|Ler case|Ver o projeto/ }),
).toBeNull();
});
it("path: contact -> 'Falar sobre meu projeto', and the list of clients opens with Lavação 047 and ends with the SEV", () => {
const { container } = render(<CasesPage />);
expect(
screen.getByRole("link", { name: buttonLabels.contact }),
).toHaveAttribute("href", expect.stringContaining("https://wa.me/"));
const clientTitles = Array.from(
container.querySelectorAll("#cases-clientes ~ div article .ds-kicker"),
).map((kicker) => kicker.textContent);
expect(clientTitles).toEqual(
clientCasesForListing.map(({ title }) => title),
);
expect(clientTitles[0]).toBe("Lavação 047");
expect(clientTitles[clientTitles.length - 1]).toBe("SEV Moda Íntima");
});
});
describe("CasesPage: topo com mosaico de pôsteres", () => {
it("path: captures found -> two columns with three posters of the other clients, not the first card's", () => {
const { container } = render(<CasesPage />);
const hero = container.querySelector("section");
expect(hero?.querySelector(".ds-section-shell")).toHaveClass("grid");
expect(hero?.className).not.toContain("md:!pb-sp-12");
const posters = Array.from(hero?.querySelectorAll("img") ?? []).map(
(image) => decodeURIComponent(image.getAttribute("src") ?? ""),
);
expect(posters).toHaveLength(3);
expect(posters.join(" ")).not.toContain(clientCases[0].id);
for (const [index, { id }] of clientCases.slice(1).entries()) {
expect(posters[index]).toContain(`/videos/${id}/desktop.webp`);
}
});
it("path: no capture for the other clients -> no mosaic, a lower one-column strip, and the first card cover takes the priority", () => {
mockedGetCaseMedia.mockImplementation((id, device) =>
id === clientCases[0].id ? actualGetCaseMedia(id, device) : undefined,
);
describe("CasesPage: topo sem mosaico", () => {
it("path: captures found -> no poster mosaic (the cards below already carry the covers), a lower one-column strip, no 'Veja o que já' twice", () => {
const { container } = render(<CasesPage />);
const hero = container.querySelector("section");
expect(hero?.querySelector("img")).toBeNull();
expect(hero?.querySelector(".ds-section-shell")).not.toHaveClass("grid");
expect(hero?.className).toContain("md:!pb-sp-12");
const firstCover = clientCard(container, clientCases[0].id).querySelector(
".ds-bframe img",
// o título e o subtítulo não repetem a mesma frase
const heading = within(hero as HTMLElement).getByRole("heading", {
level: 1,
});
expect(heading).toHaveTextContent("Veja o que já fiz para empresas.");
expect(hero?.textContent?.match(/Veja o que já/g)).toHaveLength(1);
expect(hero?.textContent).not.toMatch(/\bdireto\b/);
});
it("path: no capture for the other clients -> still no mosaic, and the first card cover takes the priority", () => {
mockedGetCaseMedia.mockImplementation((id, device) =>
id === clientCasesForListing[0].id
? actualGetCaseMedia(id, device)
: undefined,
);
const { container } = render(<CasesPage />);
const hero = container.querySelector("section");
expect(hero?.querySelector("img")).toBeNull();
const firstCover = clientCard(
container,
clientCasesForListing[0].id,
).querySelector(".ds-bframe img");
expect(firstCover).toHaveAttribute("loading", "eager");
expect(firstCover).toHaveAttribute("fetchpriority", "high");
});
});
describe("CasesPage: prioridade da capa do primeiro cartão", () => {
it("path: mosaic at the top -> the first card cover stays lazy, the priority is the mosaic's first poster", () => {
it("path: no mosaic -> the first card cover is the page's biggest image, so it loads at once with high priority; the others stay lazy", () => {
const { container } = render(<CasesPage />);
const hero = container.querySelector("section");
expect(hero?.querySelector("img")).toHaveAttribute("loading", "eager");
for (const { id } of clientCases) {
const [first, ...others] = clientCasesForListing;
const firstCover = clientCard(container, first.id).querySelector(
".ds-bframe img",
);
expect(firstCover).toHaveAttribute("loading", "eager");
expect(firstCover).toHaveAttribute("fetchpriority", "high");
for (const { id } of others) {
const cover = clientCard(container, id).querySelector(".ds-bframe img");
expect(cover).toHaveAttribute("loading", "lazy");
expect(cover).not.toHaveAttribute("fetchpriority");