Prometí en la vista previa que entrarían «de una en una al bajar» y lo que monté las traía a las cuatro a la vez: un stagger de contenedor se dispara con la sección al 30% en pantalla, y la sección mide 1.200px, así que el lector veía aparecer todas las frases mucho antes de llegar a ellas. Ahora cada cita usa TextoDesenfoque, que lleva su propio observador: cada una espera su turno y entonces se resuelve desde un desenfoque, palabra a palabra. Es el dolor entrando en foco, y es el mismo recurso que ya usan el hero y la frase ancla — vocabulario del proyecto, no un invento nuevo. Verificado midiendo la opacidad de la palabra animada de cada cita: al asomar la sección, las dos primeras están resueltas y las dos últimas a cero; a media sección, las cuatro. Antes de corregir el selector medía el span envoltorio y daba unos en todas, que no significaba nada. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
58 lines
2.7 KiB
TypeScript
58 lines
2.7 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import { describe, it, expect } from 'vitest';
|
|
import { Dolores } from '../Dolores';
|
|
import { dolores } from '@/lib/content';
|
|
|
|
describe('<Dolores />', () => {
|
|
it('renders the four pains verbatim, in the client\'s first person', () => {
|
|
const { container } = render(<Dolores />);
|
|
// The quotes resolve word by word, so each word is its own span and no
|
|
// single element holds the whole sentence as its own text — getByText
|
|
// would never find it. The blockquote's textContent is what the reader
|
|
// ends up with, and that is what has to be verbatim.
|
|
const citas = Array.from(container.querySelectorAll('blockquote')).map((b) =>
|
|
(b.textContent ?? '').replace(/\s+/g, ' '),
|
|
);
|
|
for (const dolor of dolores.quotes) {
|
|
expect(citas.some((c) => c.includes(dolor))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('says each pain exactly once', () => {
|
|
const { container } = render(<Dolores />);
|
|
// A marquee needed the four sentences painted four times over — twice per
|
|
// row for a seamless loop, twice again for the second row — and three of
|
|
// those runs had to be hidden from assistive tech or a screen reader would
|
|
// recite the whole list twice. Stacked, there is nothing to hide: one
|
|
// sentence, once, said once.
|
|
for (const dolor of dolores.quotes) {
|
|
const apariciones = Array.from(container.querySelectorAll('blockquote')).filter(
|
|
(el) => el.textContent?.includes(dolor),
|
|
);
|
|
expect(apariciones).toHaveLength(1);
|
|
expect(apariciones[0].closest('[aria-hidden="true"]')).toBeNull();
|
|
}
|
|
expect(container.querySelectorAll('li')).toHaveLength(dolores.quotes.length);
|
|
});
|
|
it('closes with the resolution, not with more effort', () => {
|
|
const { container } = render(<Dolores />);
|
|
expect(container.textContent).toContain('Esto no se resuelve con más esfuerzo.');
|
|
expect(container.textContent).toContain('Se resuelve con una estrategia clara.');
|
|
});
|
|
it('centres the knot so the conductor thread runs through it', () => {
|
|
const { container } = render(<Dolores />);
|
|
// The knot is the centred decorative svg of the section; the conductor
|
|
// thread runs down the centre, so only a centred knot gets traversed by
|
|
// it — stitching the pain to the resolution below.
|
|
const centrados = Array.from(container.querySelectorAll('svg')).filter((s) => {
|
|
const cls = s.getAttribute('class') ?? '';
|
|
return cls.includes('left-1/2') && cls.includes('-translate-x-1/2');
|
|
});
|
|
expect(centrados.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
it('does NOT introduce scarcity', () => {
|
|
const { container } = render(<Dolores />);
|
|
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
|
|
});
|
|
});
|