import { render, screen } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { ParDialogo } from '../ParDialogo'; const par = { n: 1, name: 'Crecer', situacion: 'El siguiente salto no tiene mapa.', respuesta: 'Me sumerjo hasta entender tu empresa desde dentro.', }; const montar = (extra: Partial[0]> = {}) => render( , ); describe('', () => { it('renders both voices of the exchange', () => { montar(); expect(screen.getByText(/El siguiente salto no tiene mapa/)).toBeInTheDocument(); expect(screen.getByText(/Me sumerjo hasta entender tu empresa desde dentro/)).toBeInTheDocument(); }); it('attributes the answer to creeer, not to the reader', () => { montar(); // Without a speaker label the answer reads as more description of the // problem. The wordmark is that label — and is the only thing // allowed to draw cre≡er, never hand-typed in the JSX. expect(screen.getByLabelText('creeer')).toBeInTheDocument(); }); it('labels the situation with its inflection point', () => { montar(); expect(screen.getByText(/01 · Crecer/)).toBeInTheDocument(); }); it('tells the two voices apart by more than colour', () => { const { container } = montar(); // A reader who cannot separate gold from navy still has the speaker label, // the column, and a rule down the side of each block. Colour alone would // fail the spec's accessibility criterion. expect(container.querySelector('.border-l-2')).toBeInTheDocument(); expect(container.querySelector('span[aria-hidden="true"]')?.className).toMatch(/bg-dorado/); }); it('is a list item, so the section can mount six of them as a list', () => { montar(); expect(screen.getAllByRole('listitem')).toHaveLength(1); }); it('keeps the answer in the DOM before the reader reaches it', () => { montar({ activo: false }); // The answer waits its turn by going transparent, never by leaving the // document. `hidden` or `display: none` would take it away from screen // readers and from the static export the crawler sees — half the section's // message, gone, to buy an animation. const respuesta = screen.getByText(/Me sumerjo hasta entender tu empresa desde dentro/); expect(respuesta).toBeInTheDocument(); expect(respuesta.closest('[hidden]')).toBeNull(); expect(screen.getByLabelText(/Respuesta de creeer a: Crecer/).className).toMatch(/opacity-0\b/); }); it('keeps the reveal off any element Framer writes inline styles onto', () => { montar({ activo: false }); // Framer resolves `fadeInUp` into the inline style — `opacity: 1` — and an // inline style beats a Tailwind class. When the reveal lived on the same // element as `variants`, the markup read correctly and the browser ignored // it: every answer was legible from the moment the section scrolled in, // and only the gold rule animated. The reveal must sit on a plain child // that Framer never writes to. const bloque = screen.getByLabelText(/Respuesta de creeer a: Crecer/); expect(bloque.getAttribute('style') ?? '').not.toMatch(/opacity/); }); it('arrives already answered under reduced motion, whatever the scroll did', () => { montar({ activo: false, reducido: true }); // These readers get no scroll reveal at all. If the resolved state depended // on the animation having run, the six answers would sit at opacity zero // for them — the exact failure this project has already shipped twice. const bloque = screen.getByLabelText(/Respuesta de creeer a: Crecer/); expect(bloque.className).toMatch(/opacity-100/); expect(bloque.querySelector('span[aria-hidden="true"]')?.className).toMatch(/scale-y-100/); }); it('lets the reader drive the conversation, not only the scroll', () => { const activar = vi.fn(); const { container } = montar({ onActivar: activar }); // Hover and keyboard focus reach the same state scroll does, so the pair // is not the exclusive property of whoever scrolls with a mouse wheel. const item = container.querySelector('li')!; item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); expect(activar).toHaveBeenCalledWith(1); }); it('does not observe the viewport at all under reduced motion', () => { const observar = vi.fn(); const original = window.IntersectionObserver; window.IntersectionObserver = vi.fn(() => ({ observe: observar, unobserve: () => {}, disconnect: () => {}, })) as unknown as typeof window.IntersectionObserver; montar({ reducido: true }); expect(observar).not.toHaveBeenCalled(); window.IntersectionObserver = original; }); });