import { render, screen, within } from "@testing-library/react"; import { useReducedMotion } from "@/app/hooks/useReducedMotion"; import { primaryButton, secondaryButtonLight } from "./button-styles"; import { WorkModalities } from "./WorkModalities"; /** * Testes de caminhos-base de WorkModalities e TierCard: movimento normal x reduzido, plano em destaque * (botão e borda) x demais planos, e serviços do hub ausentes x presentes. * O `motion/react` é trocado por elementos simples que registram as props de animação. */ const mockSeen = jest.fn(); jest.mock("motion/react", () => { const React = require("react"); const cache = new Map(); const motion = new Proxy( {}, { get: (_target, tag: string) => { if (!cache.has(tag)) { cache.set( tag, ({ children, variants, whileHover, whileTap, transition, initial: _initial, animate: _animate, whileInView: _whileInView, viewport: _viewport, ...props }: Record & { children?: unknown }) => { mockSeen(tag, { variants, whileHover, whileTap, transition }); return React.createElement(tag, props, children); }, ); } return cache.get(tag); }, }, ); return { motion }; }); jest.mock("@/app/hooks/useReducedMotion", () => ({ useReducedMotion: jest.fn(), })); jest.mock("./Section", () => ({ Section: ({ children, id }: { children?: React.ReactNode; id?: string }) => (
{children}
), })); jest.mock("./animations/DrawLine", () => ({ DrawLine: () => null })); type Seen = { variants?: { hidden: Record; visible: { transition: Record }; }; whileHover?: unknown; whileTap?: unknown; }; const serviceLinks = [ { href: "/servicos/criacao-de-sites", label: "Sites profissionais", description: "Presença digital para a empresa.", }, { href: "/servicos/tauri-rust", label: "Aplicativos de desktop", description: "Programas rápidos para o computador.", }, ]; function seen(tag: string): Seen[] { return mockSeen.mock.calls .filter(([name]) => name === tag) .map(([, props]) => props as Seen); } beforeEach(() => { mockSeen.mockClear(); }); describe("WorkModalities: movimento", () => { it("path: normal motion -> tiers rise in, with hover lift and press feedback", () => { jest.mocked(useReducedMotion).mockReturnValue(false); render(); const items = seen("div").filter( (props) => props.variants?.hidden?.opacity === 0, ); expect(items).toHaveLength(3); for (const item of items) { expect(item.variants?.hidden).toEqual({ opacity: 0, y: 20, clipPath: "inset(100% 0 0 0)", }); expect(item.variants?.visible.transition).toEqual({ duration: 0.75, ease: [0.16, 1, 0.3, 1], }); } const cards = seen("article"); expect(cards).toHaveLength(3); for (const card of cards) { expect(card.whileHover).toEqual({ y: -4, boxShadow: "0 24px 50px rgba(0, 0, 0, 0.08)", }); expect(card.whileTap).toEqual({ scale: 0.99 }); } }); it("path: reduced motion -> tiers only fade, with no hover lift and no press feedback", () => { jest.mocked(useReducedMotion).mockReturnValue(true); render(); const items = seen("div").filter( (props) => props.variants?.hidden?.opacity === 0, ); expect(items).toHaveLength(3); for (const item of items) { expect(item.variants?.hidden).toEqual({ opacity: 0 }); expect(item.variants?.visible.transition).toEqual({ duration: 0.25 }); } for (const card of seen("article")) { expect(card.whileHover).toBeUndefined(); expect(card.whileTap).toBeUndefined(); } }); }); describe("WorkModalities: plano em destaque", () => { it("path: the featured tier has the filled button and the thick border; the others have outline buttons", () => { jest.mocked(useReducedMotion).mockReturnValue(false); const { container } = render(); const featured = screen.getByRole("link", { name: "Conversar sobre meu sistema", }); expect(featured.className).toContain(primaryButton); expect(featured.closest("article")).toHaveStyle({ borderWidth: "2px" }); expect(featured.closest("article")).toHaveClass("border-[var(--slash)]"); for (const name of ["Avaliar meu projeto", "Planejar a próxima etapa"]) { const other = screen.getByRole("link", { name }); expect(other.className).toContain(secondaryButtonLight); expect(other.className).not.toContain(primaryButton); expect(other.closest("article")).toHaveStyle({ borderWidth: "1px" }); expect(other.closest("article")).not.toHaveClass("border-[var(--slash)]"); } expect(container.querySelectorAll("article")).toHaveLength(3); }); }); describe("WorkModalities: serviços do hub", () => { it("path: no serviceLinks prop -> no service cards, the 'all services' link stays", () => { jest.mocked(useReducedMotion).mockReturnValue(false); render(); expect( screen.getByRole("link", { name: "Conhecer todos os serviços" }), ).toHaveAttribute("href", "/servicos"); expect( screen.queryByRole("link", { name: /Sites profissionais/ }), ).toBeNull(); }); it("path: serviceLinks given -> one card per service, in order, linking to its page", () => { jest.mocked(useReducedMotion).mockReturnValue(false); const { container } = render( , ); const grid = container.querySelector( "[data-reel-stagger='left']", ) as HTMLElement; const links = within(grid).getAllByRole("link"); expect(links.map((link) => link.getAttribute("href"))).toEqual( serviceLinks.map(({ href }) => href), ); expect( within(links[0]).getByText("Sites profissionais"), ).toBeInTheDocument(); expect( within(links[1]).getByText("Programas rápidos para o computador."), ).toBeInTheDocument(); }); });