import { render, screen, within } from "@testing-library/react"; 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. * Nada de animação em JavaScript: o movimento reduzido é tratado pelo CSS (globals.css e conversion.css). */ jest.mock("./Section", () => ({ Section: ({ children, id, title, description, }: { children?: React.ReactNode; id?: string; title?: string; description?: string; }) => (

{title}

{description}

{children}
), })); jest.mock("./animations/DrawLine", () => ({ DrawLine: () => null })); const original = (image: Element) => new URL(image.getAttribute("src") ?? "", "http://localhost").searchParams.get( "url", ); describe("WorkModalities: movimento", () => { it("path: offers enter through Reveal -> same rise, same duration and a growing delay, no JavaScript animation props", () => { const { container } = render(); const reveals = [...container.querySelectorAll(".ds-reveal")]; expect(reveals).toHaveLength(3); expect( reveals.map((element) => element.style.getPropertyValue("--reveal-y")), ).toEqual(["20px", "20px", "20px"]); expect( reveals.map((element) => element.style.getPropertyValue("--reveal-duration"), ), ).toEqual(["0.75s", "0.75s", "0.75s"]); expect( reveals.map((element) => Number.parseFloat(element.style.getPropertyValue("--reveal-delay")), ), ).toEqual([0.08, 0.16, 0.24]); // o estado escondido vive no CSS: sem opacidade nem transformação inline for (const element of reveals) { expect(element.style.opacity).toBe(""); expect(element.style.transform).toBe(""); } }); it("path: the cards are not focus stops and keep no outline-none (touch and hover feedback come from CSS)", () => { const { container } = render(); const cards = container.querySelectorAll("article"); expect(cards).toHaveLength(3); for (const card of cards) { expect(card).not.toHaveAttribute("tabindex"); expect(card.className).not.toContain("outline-none"); expect(card).toHaveClass("cv-press", "cv-lift"); } }); it("path: each offer sits alone in its own reveal wrapper, which can shrink in the grid", () => { const { container } = render(); for (const card of container.querySelectorAll("article")) { const wrapper = card.closest(".ds-reveal"); expect(wrapper).toHaveClass("min-w-0"); expect(wrapper?.querySelectorAll("article")).toHaveLength(1); } }); }); 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", () => { const { container } = render(); expect( screen.getByRole("heading", { level: 2, name: "Escolha por onde começar", }), ).toBeInTheDocument(); const articles = Array.from(container.querySelectorAll("article")); expect(articles).toHaveLength(3); expect( articles.map( (article) => within(article).getByRole("heading", { level: 3 }).textContent, ), ).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", ]); }); it("path: the site offer cites the Lavação 047 (catalog or price table, ready message, panel to edit alone)", () => { const { container } = render(); const [site] = Array.from(container.querySelectorAll("article")); expect(site.textContent).toMatch(/Catálogo ou tabela de preços/); expect(site.textContent).toMatch(/mensagem pronta/); expect(site.textContent).toMatch(/painel para você editar/); expect(site.textContent).toMatch(/Lavação 047/); }); it("path: the first two offers show the poster of their case, sized for a third of the row", () => { const { container } = render(); const [site, system] = Array.from(container.querySelectorAll("article")); const siteImage = within(site).getByRole("img"); expect(original(siteImage)).toBe("/videos/lavacao-047/desktop.webp"); expect(siteImage).toHaveAccessibleName( "Site Lavação 047 aberto no computador", ); expect(siteImage).toHaveAttribute( "sizes", "(min-width: 1024px) 33vw, 100vw", ); expect(siteImage).toHaveAttribute("loading", "lazy"); expect(original(within(system).getByRole("img"))).toBe( "/videos/sev-modaintima/desktop.webp", ); }); it("path: the review offer has a checklist card instead of a poster, with what the review delivers", () => { const { container } = render(); const review = Array.from(container.querySelectorAll("article"))[2]; expect(within(review).queryByRole("img")).toBeNull(); expect(within(review).getByText("O que você recebe")).toBeInTheDocument(); for (const item of [ "As falhas de acesso a dados", "O que dá para aproveitar", "O que é melhor refazer", "Um orçamento para pôr em produção", ]) { expect(within(review).getByText(item)).toBeInTheDocument(); } }); it("path: each button opens the contact form with the project type already chosen, all three with the same filled style", () => { const { container } = render(); 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"], ] as const; buttons.forEach(([name, href], index) => { const button = within(articles[index]).getByRole("link", { name }); expect(button).toHaveAttribute("href", href); expect(button.className).toContain(primaryButton); }); }); it("path: each offer links to the case or the page that backs it", () => { const { container } = render(); const articles = Array.from(container.querySelectorAll("article")); expect( within(articles[0]).getByRole("link", { name: "Ver o case Lavação 047" }), ).toHaveAttribute("href", "/cases/lavacao-047"); expect( within(articles[1]).getByRole("link", { name: "Ver o case SEV Moda Íntima", }), ).toHaveAttribute("href", "/cases/sev-modaintima"); expect( within(articles[2]).getByRole("link", { name: "Ver como funciona a revisão", }), ).toHaveAttribute("href", "/ia"); }); it("path: no duplicate service grid and no 'Reconhece algum destes cenários?' panel", () => { const { container } = render(); expect(screen.queryByText(/Reconhece algum destes cenários/)).toBeNull(); expect(screen.queryByText(/Planilhas demais/)).toBeNull(); expect( screen.queryByRole("link", { name: "Ver todos os serviços" }), ).toBeNull(); for (const link of screen.getAllByRole("link")) { expect(link.getAttribute("href")).not.toMatch(/^\/servicos/); } expect(container.querySelectorAll("article")).toHaveLength(3); }); }); describe("WorkModalities: um só estilo de cartão", () => { it("path: the three cards share the same classes and no card is highlighted (no featured border, no shadow variant)", () => { const { container } = render(); const articles = Array.from(container.querySelectorAll("article")); const classNames = articles.map((article) => article.className); expect(new Set(classNames).size).toBe(1); for (const article of articles) { expect(article).not.toHaveAttribute("style"); expect(article.className).not.toContain("border-[var(--slash)]"); expect(article.className).not.toContain("shadow"); } }); it("path: every title reserves the same minimum height, so the texts below line up", () => { const { container } = render(); for (const article of container.querySelectorAll("article")) { const title = within(article as HTMLElement).getByRole("heading", { level: 3, }); expect(title.className).toContain("lg:min-h-[3.3em]"); expect(title.className).toContain("xl:min-h-[2.2em]"); } }); it("path: no text under 15px on the desktop either (the old 12 and 13px classes are gone)", () => { const { container } = render(); expect(container.innerHTML).not.toMatch( /(?:^|[\s:!"'])(?:md:|lg:)?!?text-\[(?:[0-9]|1[0-4])px\]|text-(?:xs|sm)\b/, ); expect(container.innerHTML).not.toContain("ds-kicker"); expect(container.innerHTML).not.toContain("md:!text-[13px]"); expect(container.innerHTML).not.toContain("md:!text-[12px]"); }); }); describe("WorkModalities: como eu trabalho", () => { it("path: the process is a compact list of four steps, with the title and the description in one line each", () => { const { container } = render(); const process = container.querySelector("#process") as HTMLElement; expect( within(process).getByRole("heading", { level: 3, name: "Como eu trabalho", }), ).toBeInTheDocument(); const steps = within(process).getAllByRole("listitem"); expect(steps).toHaveLength(4); expect( steps.map((step) => step.querySelector("strong")?.textContent), ).toEqual([ "Entender a operação.", "Definir a entrega.", "Construir e validar.", "Publicar e dar continuidade.", ]); // sem a grade de quatro cartões com subtítulo por etapa expect(process.querySelector("h4")).toBeNull(); expect(process.textContent).toMatch( /Prazo e investimento dependem do escopo/, ); }); }); describe("WorkModalities: sem prometer o que o dono não disse", () => { it("path: no price, deadline or contract promise anywhere in the section", () => { const { container } = render(); expect(container.textContent).not.toMatch( /R\$|em contrato|prazo de \d|\d+ dias|grátis|gratuit/i, ); expect(screen.queryByText("Primeiro passo")).toBeNull(); }); });