feat(home): seção Sobre com prova por ponto, contato por oferta e hero com a Lavação 047 na tela larga
This commit is contained in:
1 parent
d356c364e0
commit
9fd1570426
33 files changed
+1128
-369
No files matched your search
@@ -1,11 +1,13 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { serviceOffers } from "@/lib/site-content";
|
||||
import { primaryButton } from "./button-styles";
|
||||
import { WorkModalities } from "./WorkModalities";
|
||||
|
||||
/**
|
||||
* Testes de caminhos-base de WorkModalities e OfferCard: a entrada das ofertas (Reveal, CSS), o toque e o passe do
|
||||
* mouse (classes cv-press e cv-lift, sem foco nos cartões), as três ofertas (miniatura do case, botão que abre o
|
||||
* formulário com o tipo escolhido), o estilo único de cartão, o texto de pelo menos 15 px e a lista compacta de etapas.
|
||||
* mouse (classes cv-press e cv-lift, sem foco nos cartões), as três ofertas (nome e "O que você recebe" vindos de
|
||||
* `serviceOffers`, foto do case ou ilustração, botão que abre o formulário com o tipo escolhido, "Primeiro passo" uma
|
||||
* vez só depois dos cartões), o estilo único de cartão, o texto de pelo menos 15 px e a lista compacta de etapas.
|
||||
* Nada de animação em JavaScript: o movimento reduzido é tratado pelo CSS (globals.css e conversion.css).
|
||||
*/
|
||||
|
||||
@@ -86,7 +88,7 @@ describe("WorkModalities: movimento", () => {
|
||||
});
|
||||
|
||||
describe("WorkModalities: as três ofertas", () => {
|
||||
it("path: the section title is 'Escolha por onde começar' and the offers follow the headline, in order", () => {
|
||||
it("path: the section title is 'Escolha por onde começar' and the offers follow the headline, in order, with the names of serviceOffers", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
expect(
|
||||
@@ -97,19 +99,20 @@ describe("WorkModalities: as três ofertas", () => {
|
||||
).toBeInTheDocument();
|
||||
const articles = Array.from(container.querySelectorAll("article"));
|
||||
expect(articles).toHaveLength(3);
|
||||
expect(
|
||||
articles.map(
|
||||
(article) =>
|
||||
within(article).getByRole("heading", { level: 3 }).textContent,
|
||||
),
|
||||
).toEqual([
|
||||
const titles = articles.map(
|
||||
(article) =>
|
||||
within(article).getByRole("heading", { level: 3 }).textContent,
|
||||
);
|
||||
expect(titles).toEqual([
|
||||
"Site que traz pedidos pelo WhatsApp",
|
||||
"Sistema para tirar a operação da planilha",
|
||||
"Revisão de sistema que já existe, inclusive feito com IA",
|
||||
"Revisão de sistema que já existe",
|
||||
]);
|
||||
// a fonte é uma só, a mesma de /servicos
|
||||
expect(titles).toEqual(serviceOffers.map(({ name }) => name));
|
||||
});
|
||||
|
||||
it("path: the site offer cites the Lavação 047 and delivers a catalog or price table, a ready WhatsApp message, a panel to edit prices and photos and the publication", () => {
|
||||
it("path: the site offer cites the Lavação 047 and its deliverables are the ones of serviceOffers, the same list of /servicos", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const [site] = Array.from(container.querySelectorAll("article"));
|
||||
@@ -118,15 +121,10 @@ describe("WorkModalities: as três ofertas", () => {
|
||||
within(site)
|
||||
.getAllByRole("listitem")
|
||||
.map((item) => item.textContent),
|
||||
).toEqual([
|
||||
"Catálogo ou tabela de preços",
|
||||
"Mensagem pronta no WhatsApp",
|
||||
"Painel para editar preços e fotos",
|
||||
"Publicação do site",
|
||||
]);
|
||||
).toEqual(serviceOffers[0].deliverables);
|
||||
});
|
||||
|
||||
it("path: only the site offer shows a photo (the Lavação 047 poster, sized for a third of the row)", () => {
|
||||
it("path: only the site offer shows a photo (the Lavação 047 poster, sized for a third of the row); the other two have an illustration of their own", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const [site, system, review] = Array.from(
|
||||
@@ -143,43 +141,91 @@ describe("WorkModalities: as três ofertas", () => {
|
||||
);
|
||||
expect(siteImage).toHaveAttribute("loading", "lazy");
|
||||
// a vitrine da SEV é uma loja, não um painel: contradiria o texto do sistema
|
||||
expect(within(system).queryByRole("img")).toBeNull();
|
||||
expect(system.querySelector("img")).toBeNull();
|
||||
expect(review.querySelector("img")).toBeNull();
|
||||
expect(container.innerHTML).not.toContain("sev-modaintima/desktop.webp");
|
||||
expect(within(review).queryByRole("img")).toBeNull();
|
||||
});
|
||||
|
||||
it("path: the system and the review offers have a card with no photo, only an icon (decorative), and the deliverables below", () => {
|
||||
it("path: the system and the review cards have no black box with an icon: each has an illustration in the same box as the site photo", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
expect(screen.queryByTestId("offer-icon")).toBeNull();
|
||||
const boxes = ["site", "sistema", "revisao"].map((id) =>
|
||||
screen.getByTestId(`offer-art-${id}`),
|
||||
);
|
||||
// as três caixas têm as mesmas classes, então o mesmo tamanho
|
||||
expect(new Set(boxes.map((box) => box.className)).size).toBe(1);
|
||||
for (const box of boxes) {
|
||||
expect(box.className).toContain("aspect-[16/10]");
|
||||
expect(box.className).toContain("md:aspect-[21/9]");
|
||||
expect(box.className).toContain("lg:aspect-[16/10]");
|
||||
expect(box.firstElementChild).not.toBeNull();
|
||||
}
|
||||
expect(container.querySelectorAll("article")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("path: the system illustration is a sheet -> arrow -> dashboard scheme in SVG, with three indicators, no text and no numbers", () => {
|
||||
render(<WorkModalities />);
|
||||
|
||||
const art = screen.getByTestId("offer-art-sistema");
|
||||
const scheme = within(art).getByRole("img");
|
||||
expect(scheme.tagName.toLowerCase()).toBe("svg");
|
||||
expect(scheme).toHaveAccessibleName(
|
||||
"Esquema ilustrativo: as linhas de uma planilha viram um painel com três indicadores e um gráfico de barras",
|
||||
);
|
||||
expect(scheme.getAttribute("viewBox")).toBe("0 0 240 150");
|
||||
expect(scheme.querySelector('[data-art="planilha"]')).not.toBeNull();
|
||||
expect(scheme.querySelector('[data-art="seta"]')).not.toBeNull();
|
||||
const panel = scheme.querySelector('[data-art="painel"]') as Element;
|
||||
expect(panel).not.toBeNull();
|
||||
// três indicadores genéricos: um quadro com um traço de rótulo e um bloco de valor cada, sem dígito algum
|
||||
expect(panel.querySelectorAll("g").length).toBe(3);
|
||||
expect(scheme.querySelector("text")).toBeNull();
|
||||
expect(art.textContent).toBe("");
|
||||
// nada de cor de destaque: o limão é só do contato
|
||||
expect(art.innerHTML).not.toMatch(/ccff00|var\(--slash\)/i);
|
||||
});
|
||||
|
||||
it("path: the review illustration is an excerpt of the report model of /ia: three findings with a severity badge and the caption 'Exemplo ilustrativo'", () => {
|
||||
render(<WorkModalities />);
|
||||
|
||||
const art = screen.getByTestId("offer-art-revisao");
|
||||
expect(within(art).getByRole("img")).toHaveAccessibleName(
|
||||
"Exemplo ilustrativo de relatório da revisão: três achados, dois de gravidade alta e um de gravidade média",
|
||||
);
|
||||
expect(within(art).getByText("Exemplo ilustrativo")).toBeInTheDocument();
|
||||
const findings = Array.from(art.querySelectorAll("li")).map(
|
||||
(item) => item.textContent,
|
||||
);
|
||||
expect(findings).toEqual([
|
||||
"Dados expostosAlta",
|
||||
"Chave expostaAlta",
|
||||
"Sem backupMédia",
|
||||
]);
|
||||
// a gravidade está no texto do selo, não só na cor
|
||||
for (const severity of ["Alta", "Média"]) {
|
||||
expect(within(art).getAllByText(severity).length).toBeGreaterThan(0);
|
||||
}
|
||||
// o texto da ilustração também tem 15 px, como o resto da seção
|
||||
expect(art.innerHTML).not.toMatch(/text-\[(?:[0-9]|1[0-4])px\]/);
|
||||
});
|
||||
|
||||
it("path: the deliverables of the system and of the review come from serviceOffers, the same lists of /servicos", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const [, system, review] = Array.from(
|
||||
container.querySelectorAll("article"),
|
||||
);
|
||||
for (const card of [system, review]) {
|
||||
const icon = within(card).getByTestId("offer-icon");
|
||||
expect(icon.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
|
||||
expect(icon.textContent).toBe("");
|
||||
}
|
||||
expect(
|
||||
within(system)
|
||||
.getAllByRole("listitem")
|
||||
.map((item) => item.textContent),
|
||||
).toEqual([
|
||||
"Cadastro de clientes e produtos",
|
||||
"Pedidos e acompanhamento",
|
||||
"Relatórios",
|
||||
"Acesso por perfil",
|
||||
]);
|
||||
).toEqual(serviceOffers[1].deliverables);
|
||||
expect(
|
||||
within(review)
|
||||
.getAllByRole("listitem")
|
||||
.map((item) => item.textContent),
|
||||
).toEqual([
|
||||
"As falhas de acesso a dados",
|
||||
"O que dá para aproveitar",
|
||||
"O que é melhor refazer",
|
||||
"Um orçamento para pôr em produção",
|
||||
]);
|
||||
).toEqual(serviceOffers[2].deliverables);
|
||||
});
|
||||
|
||||
it("path: every card says what you receive, with three or four items, once", () => {
|
||||
@@ -195,27 +241,35 @@ describe("WorkModalities: as três ofertas", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("path: every card has its first step: a first conversation and, after it, the scope and the proposal in writing", () => {
|
||||
it("path: the first step shows once, below the three cards: a first conversation and, after it, the scope and the proposal in writing", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const cards = Array.from(container.querySelectorAll("article"));
|
||||
for (const card of cards) {
|
||||
expect(within(card).getAllByText("Primeiro passo")).toHaveLength(1);
|
||||
expect(card.textContent).toMatch(
|
||||
/Primeira conversa para entender .+\. Depois, escopo e proposta por escrito\./,
|
||||
);
|
||||
for (const card of container.querySelectorAll("article")) {
|
||||
expect(within(card).queryByText("Primeiro passo")).toBeNull();
|
||||
}
|
||||
expect(screen.getAllByText("Primeiro passo")).toHaveLength(3);
|
||||
expect(screen.getAllByText("Primeiro passo")).toHaveLength(1);
|
||||
const label = screen.getByText("Primeiro passo");
|
||||
expect(label.closest("article")).toBeNull();
|
||||
// fica depois da grade de cartões
|
||||
const grid = container.querySelector(".grid") as HTMLElement;
|
||||
expect(
|
||||
grid.compareDocumentPosition(label) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
expect(label.parentElement?.textContent).toMatch(
|
||||
/^Primeiro passoPrimeira conversa para entender .+\. Depois, escopo e proposta por escrito\.$/,
|
||||
);
|
||||
// sem prazo, sem preço
|
||||
expect(label.parentElement?.textContent).not.toMatch(/R\$|dias|prazo de/i);
|
||||
});
|
||||
|
||||
it("path: each button opens the contact form with the project type already chosen, all three with the same filled style", () => {
|
||||
it("path: each button opens the contact form card with the project type already chosen, all three with the same filled style", () => {
|
||||
const { container } = render(<WorkModalities />);
|
||||
|
||||
const articles = Array.from(container.querySelectorAll("article"));
|
||||
const buttons = [
|
||||
["Começar pelo site", "/?tipo=site#contact"],
|
||||
["Começar pelo sistema", "/?tipo=sistema#contact"],
|
||||
["Começar pela revisão", "/?tipo=revisao#contact"],
|
||||
["Começar pelo site", "/?tipo=site#contact-form"],
|
||||
["Começar pelo sistema", "/?tipo=sistema#contact-form"],
|
||||
["Começar pela revisão", "/?tipo=revisao#contact-form"],
|
||||
] as const;
|
||||
buttons.forEach(([name, href], index) => {
|
||||
const button = within(articles[index]).getByRole("link", { name });
|
||||
|
||||
Reference in new issue
Block a user