fix(hilo): la banda recupera su grosor y remata en el ≡
El grosor nunca se decidió: la primera versión ocupaba toda la columna, así que el viewBox se estiraba por 11.5 y un strokeWidth de 1.5 se pintaba a ~17px reales. Al mudar el hilo a una banda de 56px el mismo valor quedó en 0.84px, y con eso dejó de leerse. - Grosor fijado en 14px reales con non-scaling-stroke, ya no heredado del escalado del viewBox. - Banda de vuelta a toda la columna: ±184px de recorrido lateral. - Cuatro ondulaciones largas en vez de una sola (que se aplanaba) o diez cortas (que se leían inquietas). - El trazo termina al 85% del scroll: colgado del scroll de página sólo llegaba al 100% pasado el footer, así que el final no se veía nunca. - Remata en el ≡ del wordmark, con la geometría de lib/simbolo.ts. Es lo que pedía el spec y era lo que faltaba. Test nuevo que exige stroke-width >= 10: el grosor ya se había perdido dos veces por accidente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
367e769f5e
commit
3c2185e200
2 files changed
+151
-22
No files matched your search
@@ -1,6 +1,26 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { HiloConductor } from '../HiloConductor';
|
||||
import { BARRAS_SIMBOLO, VIEWBOX_SIMBOLO } from '@/lib/simbolo';
|
||||
|
||||
const preferirMenosMovimiento = () => {
|
||||
window.matchMedia = (query: string) =>
|
||||
({
|
||||
matches: true,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}) as unknown as MediaQueryList;
|
||||
};
|
||||
|
||||
const simboloFinal = (container: HTMLElement) =>
|
||||
Array.from(container.querySelectorAll('svg')).find(
|
||||
(svg) => svg.getAttribute('viewBox') === VIEWBOX_SIMBOLO,
|
||||
);
|
||||
|
||||
describe('<HiloConductor />', () => {
|
||||
const matchMediaOriginal = window.matchMedia;
|
||||
@@ -65,18 +85,61 @@ describe('<HiloConductor />', () => {
|
||||
expect(wrapperClass).not.toContain('2xl:block');
|
||||
});
|
||||
|
||||
it('keeps its declared weight on screen despite the non-uniform viewBox scale', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
// The band is stretched with preserveAspectRatio="none": y over the whole
|
||||
// document, x over a couple hundred pixels. That squeezes the stroke of a
|
||||
// near-vertical line by the x factor alone, so a declared 1.5 rendered as
|
||||
// well under 1 real pixel — a ghost hairline. non-scaling-stroke pins the
|
||||
// width to the value we actually chose.
|
||||
expect(container.querySelector('path')).toHaveAttribute('vector-effect', 'non-scaling-stroke');
|
||||
});
|
||||
|
||||
it('reads as a band, not as a hairline', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
const grosor = Number(container.querySelector('path')?.getAttribute('stroke-width'));
|
||||
// Twice regressed into a thread nobody could see. The band is the page's
|
||||
// spine and has to carry weight; the text stays legible because it sits a
|
||||
// stacking level above, not because the band is faint.
|
||||
expect(grosor).toBeGreaterThanOrEqual(10);
|
||||
});
|
||||
|
||||
it('sweeps wide enough across the column to read as a route', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
const d = container.querySelector('path')?.getAttribute('d') ?? '';
|
||||
// Coordinates come in x/y pairs, so the even ones are the horizontal
|
||||
// excursion, in a 0–100 viewBox stretched across the content column.
|
||||
// Too little travel and the swell collapses into a straight rule.
|
||||
const numeros = (d.match(/-?\d+(?:\.\d+)?/g) ?? []).map(Number);
|
||||
const xs = numeros.filter((_, i) => i % 2 === 0);
|
||||
expect(Math.max(...xs) - Math.min(...xs)).toBeGreaterThanOrEqual(30);
|
||||
expect((d.match(/C/g) ?? []).length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('resolves into the ≡ of the wordmark instead of stopping in mid-air', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
const simbolo = simboloFinal(container);
|
||||
// Per the design: the thread "resuelve en el ≡ del wordmark en el cierre".
|
||||
// It reuses the brand geometry rather than drawing a lookalike.
|
||||
expect(simbolo).toBeDefined();
|
||||
expect(simbolo?.querySelectorAll('rect')).toHaveLength(BARRAS_SIMBOLO.length);
|
||||
});
|
||||
|
||||
it('shows the resolved ≡ from the start under reduced motion', () => {
|
||||
preferirMenosMovimiento();
|
||||
|
||||
const { container } = render(<HiloConductor />);
|
||||
const barras = Array.from(simboloFinal(container)?.querySelectorAll('rect') ?? []);
|
||||
// These readers never get the scroll draw that reveals the mark, so the
|
||||
// ending has to be there already — otherwise the thread ends in nothing.
|
||||
expect(barras).toHaveLength(BARRAS_SIMBOLO.length);
|
||||
barras.forEach((barra) => {
|
||||
expect(barra.getAttribute('style') ?? '').not.toContain('opacity: 0');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a genuine plain path under reduced motion, not a neutralised motion.path', () => {
|
||||
window.matchMedia = (query: string) =>
|
||||
({
|
||||
matches: true,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}) as unknown as MediaQueryList;
|
||||
preferirMenosMovimiento();
|
||||
|
||||
const { container } = render(<HiloConductor />);
|
||||
const path = container.querySelector('path');
|
||||
|
||||
Reference in new issue
Block a user