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:
1 parent
e63f3c2ebd
commit
fc44a69fb3
41 files changed
+2486
-550
No files matched your search
@@ -1,18 +1,24 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { SEO_IDS } from "@/lib/seo";
|
||||
import {
|
||||
buttonLabels,
|
||||
caseStudies,
|
||||
clientCases,
|
||||
clientCasesForListing,
|
||||
getRelatedCases,
|
||||
getServicePage,
|
||||
isClientCase,
|
||||
serviceOffers,
|
||||
servicePages,
|
||||
siteUrl,
|
||||
} from "@/lib/site-content";
|
||||
import ServicesIndexPage from "../page";
|
||||
import ServicePage, { generateMetadata, generateStaticParams } from "./page";
|
||||
|
||||
jest.mock("next/navigation", () => ({ notFound: jest.fn() }));
|
||||
// usePathname: o HoverPrefetchLink (trilha, cartões) lê o caminho para saber se está na home
|
||||
jest.mock("next/navigation", () => ({
|
||||
notFound: jest.fn(),
|
||||
usePathname: () => "/servicos",
|
||||
}));
|
||||
|
||||
async function renderService(slug: string) {
|
||||
const ui = await ServicePage({ params: Promise.resolve({ slug }) });
|
||||
@@ -25,6 +31,13 @@ function isBefore(first: Element, second: Element) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Links "Ver case" dos cartões de "Onde já apliquei isso"; a capa do topo, que também abre o case, fica de fora. */
|
||||
function caseCardLinks(container: HTMLElement) {
|
||||
const block = container.querySelector<HTMLElement>("#casos-relacionados");
|
||||
if (!block) throw new Error("Missing related cases block");
|
||||
return within(block).getAllByRole("link", { name: /^Ver case / });
|
||||
}
|
||||
|
||||
const service = getServicePage("consultoria-nextjs");
|
||||
if (!service) throw new Error("Missing representative service");
|
||||
|
||||
@@ -97,7 +110,10 @@ it("renders answer-first consulting, reciprocal case links and minimal schema",
|
||||
expect(screen.getByText(detail)).not.toBeVisible();
|
||||
}
|
||||
|
||||
const primaryCtas = screen.getAllByRole("link", { name: /WhatsApp/i });
|
||||
const primaryCtas = screen.getAllByRole("link", {
|
||||
name: buttonLabels.contact,
|
||||
});
|
||||
expect(primaryCtas).toHaveLength(2);
|
||||
for (const cta of primaryCtas) {
|
||||
const destination = new URL(cta.getAttribute("href") ?? "");
|
||||
expect(destination.origin).toBe("https://wa.me");
|
||||
@@ -106,19 +122,24 @@ it("renders answer-first consulting, reciprocal case links and minimal schema",
|
||||
expect(cta).toHaveAttribute("rel", expect.stringContaining("noopener"));
|
||||
}
|
||||
|
||||
// o case da capa (SC Plus) é citado pela legenda dela, e não ganha cartão: os cartões são os demais cases relacionados
|
||||
const heroId = service.heroCase?.id;
|
||||
expect(heroId).toBe("sc-plus");
|
||||
expect(
|
||||
container.querySelector("section figcaption a[href='/cases/sc-plus']"),
|
||||
).not.toBeNull();
|
||||
const expectedCaseLinks = service.relatedCaseIds
|
||||
.filter((id) => id !== heroId)
|
||||
.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}`,
|
||||
name: `Ver case ${caseStudy.title}`,
|
||||
href: `/cases/${id}`,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.href.localeCompare(b.href));
|
||||
const internalCaseLinks = screen.getAllByRole("link", {
|
||||
name: /^Ler case:/,
|
||||
});
|
||||
const internalCaseLinks = caseCardLinks(container);
|
||||
expect(
|
||||
internalCaseLinks
|
||||
.map((link) => ({
|
||||
@@ -208,7 +229,7 @@ it("renders answer-first consulting, reciprocal case links and minimal schema",
|
||||
]);
|
||||
});
|
||||
|
||||
it("fills the hub hero with a mosaic of three client posters and states the location in one line", () => {
|
||||
it("fills the hub hero with a mosaic of three client posters, never opening with the SEV, and states the location in one line", () => {
|
||||
const { container } = render(<ServicesIndexPage />);
|
||||
|
||||
const hero = container.querySelector("section");
|
||||
@@ -217,10 +238,18 @@ it("fills the hub hero with a mosaic of three client posters and states the loca
|
||||
expect(posters).toHaveLength(3);
|
||||
for (const [index, poster] of posters.entries()) {
|
||||
expect(decodeURIComponent(poster.getAttribute("src") ?? "")).toContain(
|
||||
`/videos/${clientCases[index].id}/desktop.webp`,
|
||||
`/videos/${clientCasesForListing[index].id}/desktop.webp`,
|
||||
);
|
||||
expect(poster).toHaveAttribute("alt", "");
|
||||
}
|
||||
const posterSources = posters.map((poster) =>
|
||||
decodeURIComponent(poster.getAttribute("src") ?? ""),
|
||||
);
|
||||
expect(posterSources[0]).toContain("/videos/lavacao-047/");
|
||||
expect(posterSources.some((src) => src.includes("sev-modaintima"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(new Set(posterSources).size).toBe(3);
|
||||
// só o primeiro, o maior, carrega na hora e com prioridade alta
|
||||
expect(posters[0]).toHaveAttribute("fetchpriority", "high");
|
||||
expect(posters[1]).toHaveAttribute("loading", "lazy");
|
||||
@@ -229,10 +258,9 @@ it("fills the hub hero with a mosaic of three client posters and states the loca
|
||||
);
|
||||
expect(container.textContent).not.toMatch(/perto de Garuva/);
|
||||
|
||||
// as capas dos projetos, mais abaixo, têm a largura da coluna e carregam preguiçosas
|
||||
const cover = container.querySelector("#projetos .ds-bframe img");
|
||||
expect(cover?.getAttribute("sizes")).toMatch(/calc\(100vw - 40px\)$/);
|
||||
expect(cover).toHaveAttribute("loading", "lazy");
|
||||
// nenhum cliente do mosaico se repete mais abaixo: as ofertas citam os cases só por link, sem capa
|
||||
expect(container.querySelectorAll(".ds-bframe")).toHaveLength(0);
|
||||
expect(container.querySelectorAll("img")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("keeps hub cards and structured items aligned", () => {
|
||||
@@ -256,10 +284,25 @@ it("keeps hub cards and structured items aligned", () => {
|
||||
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"));
|
||||
// as duas ofertas que são serviços abrem pelo cartão da oferta; os outros quatro estão em "Outros serviços"
|
||||
const offerLinks = serviceOffers
|
||||
.filter(({ href }) => publicServiceHrefs.includes(href))
|
||||
.map(({ name, href }) => ({
|
||||
href,
|
||||
link: screen.getByRole("link", { name: `Ver o serviço ${name}` }),
|
||||
}));
|
||||
expect(offerLinks).toHaveLength(2);
|
||||
for (const { href, link } of offerLinks) {
|
||||
expect(link).toHaveAttribute("href", href);
|
||||
}
|
||||
const otherLinks = itemList.itemListElement
|
||||
.filter(({ url }) => !offerLinks.some(({ href }) => url.endsWith(href)))
|
||||
.map(({ name }) => screen.getByRole("link", { name }));
|
||||
expect(otherLinks).toHaveLength(4);
|
||||
const visibleHrefs = [
|
||||
...offerLinks.map(({ link }) => link.getAttribute("href")),
|
||||
...otherLinks.map((link) => link.getAttribute("href")),
|
||||
];
|
||||
expect(visibleHrefs).toHaveLength(6);
|
||||
expect(new Set(visibleHrefs).size).toBe(6);
|
||||
expect([...visibleHrefs].sort()).toEqual([...publicServiceHrefs].sort());
|
||||
@@ -272,20 +315,68 @@ it("keeps hub cards and structured items aligned", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the client projects with covers on the hub, without disclaimers", () => {
|
||||
it("puts the three offers right after the hero, each with its name, deliverables, proof and the right page, and 'Outros serviços' below", () => {
|
||||
const { container } = render(<ServicesIndexPage />);
|
||||
|
||||
for (const id of [
|
||||
"sev-modaintima",
|
||||
"lavacao-047",
|
||||
"sc-plus",
|
||||
"as-servicos",
|
||||
]) {
|
||||
const link = container.querySelector(`a[href="/cases/${id}"]`);
|
||||
const offersHeading = screen.getByRole("heading", {
|
||||
level: 2,
|
||||
name: "Três jeitos de começar.",
|
||||
});
|
||||
const othersHeading = screen.getByRole("heading", {
|
||||
level: 2,
|
||||
name: "Outros serviços",
|
||||
});
|
||||
expect(
|
||||
isBefore(screen.getByRole("heading", { level: 1 }), offersHeading),
|
||||
).toBe(true);
|
||||
expect(isBefore(offersHeading, othersHeading)).toBe(true);
|
||||
|
||||
const cards = Array.from(
|
||||
container.querySelectorAll<HTMLElement>("section article"),
|
||||
).slice(0, 3);
|
||||
expect(cards).toHaveLength(3);
|
||||
for (const [index, offer] of serviceOffers.entries()) {
|
||||
const card = cards[index];
|
||||
expect(
|
||||
link?.closest("article")?.querySelector(".ds-bframe img"),
|
||||
).not.toBeNull();
|
||||
within(card).getByRole("heading", { level: 3, name: offer.name }),
|
||||
).toBeVisible();
|
||||
for (const item of offer.deliverables) {
|
||||
expect(within(card).getByText(item)).toBeVisible();
|
||||
}
|
||||
expect(
|
||||
within(card).getByRole("link", { name: `Ver o serviço ${offer.name}` }),
|
||||
).toHaveAttribute("href", offer.href);
|
||||
expect(within(card).getByText(offer.proof.label)).toBeVisible();
|
||||
}
|
||||
// a prova: Lavação 047 e SEV na oferta do site, a SEV e a Apolo na do sistema, a página /ia na da revisão
|
||||
expect(
|
||||
within(cards[0]).getByRole("link", { name: "Ver case Lavação 047" }),
|
||||
).toHaveAttribute("href", "/cases/lavacao-047");
|
||||
expect(
|
||||
within(cards[0]).getByRole("link", { name: "Ver case SEV Moda Íntima" }),
|
||||
).toHaveAttribute("href", "/cases/sev-modaintima");
|
||||
expect(
|
||||
within(cards[1]).getByRole("link", { name: "Ver case SEV Moda Íntima" }),
|
||||
).toHaveAttribute("href", "/cases/sev-modaintima");
|
||||
expect(within(cards[1]).getByText(/Apolo Energia/)).toBeVisible();
|
||||
expect(within(cards[2]).queryAllByRole("link")).toHaveLength(1);
|
||||
expect(
|
||||
within(cards[2]).getByRole("link", { name: /^Ver o serviço/ }),
|
||||
).toHaveAttribute("href", "/ia");
|
||||
});
|
||||
|
||||
it("keeps the hub free of case covers, with the SEV only as a proof link in two offers, and without disclaimers", () => {
|
||||
const { container } = render(<ServicesIndexPage />);
|
||||
|
||||
// a SEV aparece só como link de prova nas duas ofertas, nunca como capa ou no mosaico
|
||||
const sevLinks = container.querySelectorAll(
|
||||
`a[href="/cases/sev-modaintima"]`,
|
||||
);
|
||||
expect(sevLinks).toHaveLength(2);
|
||||
for (const link of sevLinks) {
|
||||
expect(link.querySelector("img")).toBeNull();
|
||||
}
|
||||
expect(container.querySelectorAll(".ds-bframe")).toHaveLength(0);
|
||||
expect(container.textContent).not.toMatch(
|
||||
/sem promessas|Demonstração publicada|limites|indispon/i,
|
||||
);
|
||||
@@ -315,72 +406,198 @@ it("shows the client cases first, right after the hero, with the first cover in
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const caseLinks = screen.getAllByRole("link", { name: /^Ler case:/ });
|
||||
// o case da capa (Lavação 047) é citado pela legenda dela; os cartões trazem os demais, com os de cliente à frente
|
||||
const caseLinks = caseCardLinks(container);
|
||||
expect(caseLinks.map((link) => link.getAttribute("href"))).toEqual(
|
||||
getRelatedCases(siteService.relatedCaseIds).map(({ id }) => `/cases/${id}`),
|
||||
getRelatedCases(siteService.relatedCaseIds)
|
||||
.filter(({ id }) => id !== siteService.heroCase?.id)
|
||||
.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
|
||||
// pôster do case de destaque (Lavação 047) numa moldura no topo, no lugar do fundo verde, e não a SEV
|
||||
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",
|
||||
"Captura de tela do site Lavação 047",
|
||||
);
|
||||
expect(hero?.querySelector("a[href='/cases/sev-modaintima']")).not.toBeNull();
|
||||
expect(hero?.querySelector("a[href='/cases/lavacao-047']")).not.toBeNull();
|
||||
expect(container.textContent).not.toMatch(/SEV Moda Íntima/);
|
||||
// a legenda da capa cita o case com a frase de relação e o botão "Ver case"
|
||||
const caption = hero?.querySelector("figcaption");
|
||||
expect(caption).toHaveTextContent("Lavação 047");
|
||||
expect(caption).toHaveTextContent(siteService.caseRelevance["lavacao-047"]);
|
||||
expect(
|
||||
within(caption as HTMLElement).getByRole("link", {
|
||||
name: "Ver case Lavação 047",
|
||||
}),
|
||||
).toHaveAttribute("href", "/cases/lavacao-047");
|
||||
// a mesma capa não aparece duas vezes: o case de destaque não ganha cartão, e os outros mantêm a capa deles
|
||||
expect(
|
||||
container.querySelectorAll(
|
||||
"img[alt='Captura de tela do site Lavação 047']",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
container.querySelectorAll(
|
||||
"img[alt='Captura de tela do site SC Plus Aquicultura']",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("shows no case, no frame and no 'similar projects' button on the diagnosis service, only the Apolo note", async () => {
|
||||
it("opens the two offer services with the offer name as the h1, and leaves the search title alone", async () => {
|
||||
for (const [slug, offer] of [
|
||||
["criacao-de-sites", serviceOffers[0]],
|
||||
["engenharia-de-software", serviceOffers[1]],
|
||||
] as const) {
|
||||
const { unmount } = await renderService(slug);
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: offer.name }),
|
||||
).toBeVisible();
|
||||
const metadata = await generateMetadata({
|
||||
params: Promise.resolve({ slug }),
|
||||
});
|
||||
expect(metadata.title).toBe(getServicePage(slug)?.metaTitle);
|
||||
expect(metadata.title).not.toBe(offer.name);
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("covers the systems service with the SEV on the phone, in a phone frame, once", async () => {
|
||||
const { container } = await renderService("engenharia-de-software");
|
||||
|
||||
const hero = container.querySelector("section");
|
||||
expect(hero?.querySelector(".ds-section-shell")).toHaveClass("grid");
|
||||
expect(hero?.querySelector(".ds-phone")).not.toBeNull();
|
||||
expect(hero?.querySelector(".ds-bframe")).toBeNull();
|
||||
expect(
|
||||
hero
|
||||
?.querySelector("a[href='/cases/sev-modaintima']")
|
||||
?.querySelector(".ds-phone-screen"),
|
||||
).not.toBeNull();
|
||||
// a SEV não vira imagem de navegador nem ganha cartão: a legenda do celular a cita, com a frase de relação e "Ver case"
|
||||
expect(container.querySelector("img[alt*='SEV']")).toBeNull();
|
||||
const caption = hero?.querySelector("figcaption");
|
||||
expect(caption).toHaveTextContent("SEV Moda Íntima");
|
||||
expect(caption).toHaveTextContent(
|
||||
getServicePage("engenharia-de-software")?.caseRelevance["sev-modaintima"] ??
|
||||
"",
|
||||
);
|
||||
expect(
|
||||
within(caption as HTMLElement).getByRole("link", {
|
||||
name: "Ver case SEV Moda Íntima",
|
||||
}),
|
||||
).toHaveAttribute("href", "/cases/sev-modaintima");
|
||||
// só o cartão da Lavação 047 traz capa de navegador
|
||||
expect(
|
||||
Array.from(container.querySelectorAll(".ds-bframe img")).map((image) =>
|
||||
image.getAttribute("alt"),
|
||||
),
|
||||
).toEqual(["Captura de tela do site Lavação 047"]);
|
||||
const links = caseCardLinks(container).map((link) =>
|
||||
link.getAttribute("href"),
|
||||
);
|
||||
expect(links).toEqual(["/cases/lavacao-047"]);
|
||||
});
|
||||
|
||||
it("covers the Next.js service with SC Plus, not the SEV", async () => {
|
||||
const { container } = await renderService("consultoria-nextjs");
|
||||
|
||||
const heroFrame = container.querySelector("section .ds-bframe");
|
||||
expect(heroFrame?.querySelector("img")).toHaveAttribute(
|
||||
"alt",
|
||||
"Captura de tela do site SC Plus Aquicultura",
|
||||
);
|
||||
expect(container.textContent).not.toMatch(/SEV Moda Íntima/);
|
||||
});
|
||||
|
||||
it("shows no case, no frame and no 'similar projects' button on the diagnosis service, only the Apolo note and a flow diagram", async () => {
|
||||
const { container } = await renderService("arquitetura-de-software");
|
||||
|
||||
// nenhum case de cliente é um diagnóstico: só a experiência na Apolo, em texto
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Onde já apliquei isso" }),
|
||||
screen.getByRole("heading", { level: 2, name: "Como eu trabalho nisso" }),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "Onde já apliquei isso" }),
|
||||
).toBeNull();
|
||||
expect(screen.getByText(/Apolo Energia/)).toBeVisible();
|
||||
expect(screen.queryByRole("link", { name: /^Ler case:/ })).toBeNull();
|
||||
expect(screen.queryByRole("link", { name: /^Ver case / })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "Ver projetos parecidos" }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByText(/SEV Moda Íntima/)).toBeNull();
|
||||
expect(container.querySelector(".ds-bframe")).toBeNull();
|
||||
// no lugar da metade vazia do topo: o esquema de fluxo, como imagem com texto alternativo
|
||||
const diagram = screen.getByRole("img", { name: /interface chama a API/ });
|
||||
expect(diagram.closest("section")).toBe(container.querySelector("section"));
|
||||
});
|
||||
|
||||
it("lists the Windows app as a 'Projeto próprio' on the desktop service, saying it is made in Python", async () => {
|
||||
it("lists the Windows app as a 'Projeto próprio' on the desktop service, as a personal Windows app project", async () => {
|
||||
const { container } = await renderService("tauri-rust");
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Ler case: MinGW GCC Installer" }),
|
||||
screen.getByRole("link", { name: "Ver case MinGW GCC Installer" }),
|
||||
).toHaveAttribute("href", "/cases/gcc-installer");
|
||||
expect(
|
||||
screen.getByText("MinGW GCC Installer · Projeto próprio"),
|
||||
).toBeVisible();
|
||||
expect(screen.getByText(/em Python e não em Tauri/)).toBeVisible();
|
||||
expect(
|
||||
screen.getByText(/projeto próprio de aplicativo para Windows/),
|
||||
).toBeVisible();
|
||||
expect(container.textContent).not.toMatch(/em Python e não em Tauri/);
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Ver projetos parecidos" }),
|
||||
).toHaveAttribute("href", "#casos-relacionados");
|
||||
// sem captura gravada, não há moldura nem imagem
|
||||
// sem captura gravada, não há moldura nem imagem: o topo mostra o esquema do aplicativo no computador
|
||||
expect(container.querySelector(".ds-bframe")).toBeNull();
|
||||
expect(
|
||||
screen.getByRole("img", {
|
||||
name: /aplicativo no computador troca informações/,
|
||||
}),
|
||||
).toBeVisible();
|
||||
// só projeto próprio, sem cliente: o título não fala de "onde já apliquei"
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 2, name: "Como eu trabalho nisso" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("keeps the hero in one lower column when there is no media, and in two columns when there is", async () => {
|
||||
for (const slug of [
|
||||
"tauri-rust",
|
||||
"integracao-hardware-software-iot",
|
||||
"arquitetura-de-software",
|
||||
]) {
|
||||
it("keeps the hero in two columns with a cover or a diagram, and in one lower column only when it has neither", async () => {
|
||||
const diagrams = {
|
||||
"tauri-rust": /aplicativo no computador/,
|
||||
"integracao-hardware-software-iot": /dispositivo envia as leituras/,
|
||||
"arquitetura-de-software": /interface chama a API/,
|
||||
} as const;
|
||||
for (const [slug, label] of Object.entries(diagrams)) {
|
||||
const { container, unmount } = await renderService(slug);
|
||||
const hero = container.querySelector("section");
|
||||
expect(hero?.className).not.toContain("md:!pb-sp-12");
|
||||
expect(hero?.querySelector(".ds-section-shell")).toHaveClass("grid");
|
||||
expect(
|
||||
within(hero as HTMLElement).getByRole("img", { name: label }),
|
||||
).toBeVisible();
|
||||
unmount();
|
||||
}
|
||||
|
||||
// sem capa e sem esquema, o topo volta a ser uma coluna só e mais baixa
|
||||
const bare = getServicePage("arquitetura-de-software");
|
||||
if (!bare?.diagram) throw new Error("Missing diagram");
|
||||
const originalDiagram = bare.diagram;
|
||||
delete bare.diagram;
|
||||
try {
|
||||
const { container, unmount } = await renderService(bare.slug);
|
||||
const hero = container.querySelector("section");
|
||||
expect(hero?.className).toContain("md:!pb-sp-12");
|
||||
expect(hero?.querySelector(".ds-section-shell")).not.toHaveClass("grid");
|
||||
expect(screen.queryByRole("img")).toBeNull();
|
||||
unmount();
|
||||
} finally {
|
||||
bare.diagram = originalDiagram;
|
||||
}
|
||||
|
||||
const withMedia = await renderService("criacao-de-sites");
|
||||
@@ -411,7 +628,7 @@ it("puts the location in one line, 'Garuva e Joinville (SC)', and spaces the lis
|
||||
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:/ });
|
||||
const caseLinks = caseCardLinks(container);
|
||||
expect(caseLinks.map((link) => link.getAttribute("href"))).toEqual([
|
||||
"/cases/twins-lab",
|
||||
]);
|
||||
@@ -433,19 +650,33 @@ it("labels only the laboratory projects as 'Projeto próprio'", async () => {
|
||||
});
|
||||
|
||||
it("says in one sentence, per case, why it matters to the service", async () => {
|
||||
for (const { slug, relatedCaseIds, caseRelevance } of servicePages) {
|
||||
for (const {
|
||||
slug,
|
||||
relatedCaseIds,
|
||||
caseRelevance,
|
||||
heroCase,
|
||||
} of servicePages) {
|
||||
if (relatedCaseIds.length === 0) continue;
|
||||
const { unmount } = await renderService(slug);
|
||||
const { container, unmount } = await renderService(slug);
|
||||
|
||||
for (const id of relatedCaseIds) {
|
||||
const sentence = caseRelevance[id];
|
||||
expect(sentence).toBeTruthy();
|
||||
const card = screen.getByText(sentence).closest("article");
|
||||
expect(card).toContainElement(
|
||||
screen.getByRole("link", {
|
||||
name: `Ler case: ${caseStudies.find((c) => c.id === id)?.title}`,
|
||||
}),
|
||||
);
|
||||
const link = screen.getByRole("link", {
|
||||
name: `Ver case ${caseStudies.find((c) => c.id === id)?.title}`,
|
||||
});
|
||||
if (id === heroCase?.id) {
|
||||
// o case da capa: a frase está na legenda dela, junto do botão, e não num cartão
|
||||
const caption = link.closest("figcaption");
|
||||
expect(caption).not.toBeNull();
|
||||
expect(caption).toHaveTextContent(sentence);
|
||||
expect(
|
||||
container.querySelectorAll(`article a[href="/cases/${id}"]`),
|
||||
).toHaveLength(0);
|
||||
} else {
|
||||
const card = screen.getByText(sentence).closest("article");
|
||||
expect(card).toContainElement(link);
|
||||
}
|
||||
}
|
||||
unmount();
|
||||
}
|
||||
@@ -465,9 +696,9 @@ it("cites the Apolo Energia experience in text on the IoT and architecture pages
|
||||
expect(paragraph.textContent).toMatch(/Desde 2022/);
|
||||
expect(paragraph.closest("a")).toBeNull();
|
||||
expect(paragraph.querySelector("a")).toBeNull();
|
||||
// dentro do bloco "Onde já apliquei isso", antes dos cards
|
||||
// dentro do bloco de prova ("Como eu trabalho nisso": sem case de cliente), antes dos cards
|
||||
const block = screen.getByRole("heading", {
|
||||
name: "Onde já apliquei isso",
|
||||
name: "Como eu trabalho nisso",
|
||||
});
|
||||
expect(isBefore(block, paragraph)).toBe(true);
|
||||
expect(
|
||||
@@ -497,8 +728,8 @@ it("does not mention Apolo Energia on the other service pages", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("puts the FAQ before the next step and ends every service page with a contact button", async () => {
|
||||
for (const { slug, faq, ctaLabel } of servicePages) {
|
||||
it("puts the FAQ before the next step and ends every service page with the same contact button", async () => {
|
||||
for (const { slug, faq } of servicePages) {
|
||||
const { container, unmount } = await renderService(slug);
|
||||
|
||||
const nextStep = screen.getByRole("heading", {
|
||||
@@ -517,16 +748,22 @@ it("puts the FAQ before the next step and ends every service page with a contact
|
||||
const last = container.querySelector("main")?.lastElementChild;
|
||||
expect(last).toContainElement(nextStep);
|
||||
const cta = within(last as HTMLElement).getByRole("link", {
|
||||
name: `${ctaLabel} no WhatsApp`,
|
||||
name: buttonLabels.contact,
|
||||
});
|
||||
expect(new URL(cta.getAttribute("href") ?? "").origin).toBe(
|
||||
"https://wa.me",
|
||||
);
|
||||
// as perguntas são as linhas do acordeão (o mesmo desenho da home), todas antes do botão
|
||||
for (const question of faq ?? []) {
|
||||
expect(
|
||||
isBefore(screen.getByRole("heading", { name: question.question }), cta),
|
||||
).toBe(true);
|
||||
expect(isBefore(screen.getByText(question.question), cta)).toBe(true);
|
||||
}
|
||||
// um rótulo só para o contato: sem "no WhatsApp" colado nem texto por serviço
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole("link", { name: buttonLabels.contact })
|
||||
.every((link) => link.textContent === buttonLabels.contact),
|
||||
).toBe(true);
|
||||
expect(container.textContent).not.toMatch(/diretamente/i);
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user