Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
import { expect, test } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/** Matriz común: móvil, tableta, escritorio compacto y escritorio amplio. */
|
|
|
|
|
const ANCHOS = [390, 768, 1024, 1440];
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
const RUTAS = [
|
|
|
|
|
'/',
|
|
|
|
|
'/musica',
|
|
|
|
|
'/musica/baladas-romanticas',
|
|
|
|
|
'/albumes/cupido-sin-flechas',
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
'/canciones/dorina',
|
|
|
|
|
'/taller/el-estribillo',
|
|
|
|
|
'/generos',
|
|
|
|
|
'/generos/bachata',
|
|
|
|
|
'/generos/glosario',
|
|
|
|
|
'/generos/letristas',
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
'/blog',
|
|
|
|
|
'/interpretes',
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
'/carrito',
|
|
|
|
|
'/entrar'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
test.describe('Responsive', () => {
|
|
|
|
|
for (const ancho of ANCHOS) {
|
|
|
|
|
test(`ninguna página desborda a lo ancho a ${ancho}px`, async ({ page }) => {
|
|
|
|
|
// Es el fallo clásico: un elemento se pasa de ancho, aparece una barra
|
|
|
|
|
// horizontal y toda la página se puede arrastrar de lado.
|
|
|
|
|
await page.setViewportSize({ width: ancho, height: 800 });
|
|
|
|
|
|
|
|
|
|
for (const ruta of RUTAS) {
|
|
|
|
|
await page.goto(ruta);
|
|
|
|
|
const desborde = await page.evaluate(
|
|
|
|
|
() => document.documentElement.scrollWidth - window.innerWidth
|
|
|
|
|
);
|
|
|
|
|
expect(desborde, `${ruta} a ${ancho}px`).toBeLessThanOrEqual(1);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('los controles se pueden pulsar con el dedo', async ({ page }) => {
|
|
|
|
|
// El contrato del sitio fija 44x44 como mínimo para controles accionables.
|
|
|
|
|
// Los enlaces dentro de texto no forman parte de esta comprobación.
|
|
|
|
|
await page.setViewportSize({ width: 390, height: 800 });
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
for (const ruta of ['/', '/musica', '/carrito', '/canciones/dorina']) {
|
|
|
|
|
await page.goto(ruta);
|
|
|
|
|
const pequenos = await page.evaluate(() => {
|
|
|
|
|
const fuera: string[] = [];
|
|
|
|
|
for (const e of document.querySelectorAll('button, input, select, [role="button"]')) {
|
|
|
|
|
if (!(e instanceof HTMLElement)) continue;
|
|
|
|
|
const ancho = e.offsetWidth;
|
|
|
|
|
const alto = e.offsetHeight;
|
|
|
|
|
if (!ancho || !alto) continue;
|
|
|
|
|
if (ancho < 44 || alto < 44) {
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
fuera.push(`${e.getAttribute('aria-label') ?? e.textContent?.trim().slice(0, 20)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [...new Set(fuera)];
|
|
|
|
|
});
|
|
|
|
|
expect(pequenos, `controles pequeños en ${ruta}`).toEqual([]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('la barra de reproducción no tapa el final de la página', async ({ page }) => {
|
|
|
|
|
// La barra se apila en un móvil y crece de 97 a 145 píxeles. El hueco que
|
|
|
|
|
// se le reserva sale de medirla, no de un número escrito a mano: con uno
|
|
|
|
|
// fijo, en móvil se comía el pie.
|
|
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
|
|
|
await page.goto('/albumes/cupido-sin-flechas');
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
await page.getByRole('button', { name: 'Reproducir', exact: true }).first().click();
|
|
|
|
|
await expect(page.getByRole('region', { name: 'Reproductor' })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
|
|
|
|
|
|
|
|
const solapa = await page.evaluate(() => {
|
|
|
|
|
const barra = document.querySelector('[aria-label="Reproductor"]')!.getBoundingClientRect();
|
|
|
|
|
const pie = document.querySelector('footer')!.getBoundingClientRect();
|
|
|
|
|
return Math.round(pie.bottom - barra.top);
|
|
|
|
|
});
|
|
|
|
|
expect(solapa).toBeLessThanOrEqual(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('en móvil la navegación se despliega y sigue completa', async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
await expect(page.locator('html')).toHaveAttribute('data-hidratado', '');
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
const nav = page.getByRole('navigation', { name: 'Principal' });
|
|
|
|
|
await expect(nav).toBeHidden();
|
|
|
|
|
const antes = await page.evaluate(() => ({
|
|
|
|
|
altoCabecera: document.querySelector('header')!.getBoundingClientRect().height,
|
|
|
|
|
inicioContenido: document.querySelector('main')!.getBoundingClientRect().top
|
|
|
|
|
}));
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: 'Menú' }).click();
|
|
|
|
|
await expect(nav).toBeVisible();
|
|
|
|
|
|
|
|
|
|
const aspecto = await nav.getByRole('link', { name: 'Música' }).evaluate((enlace) => {
|
|
|
|
|
const caja = enlace.getBoundingClientRect();
|
|
|
|
|
const cabecera = enlace.closest('header');
|
|
|
|
|
if (!cabecera) throw new Error('Cabecera ausente');
|
|
|
|
|
return {
|
|
|
|
|
fontSize: getComputedStyle(enlace).fontSize,
|
|
|
|
|
centro: caja.left + caja.width / 2,
|
|
|
|
|
fondo: getComputedStyle(cabecera).backgroundColor
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
expect(Number.parseFloat(aspecto.fontSize)).toBeGreaterThanOrEqual(18);
|
|
|
|
|
expect(Math.abs(aspecto.centro - 375 / 2)).toBeLessThan(2);
|
|
|
|
|
expect(aspecto.fondo).not.toBe('rgba(0, 0, 0, 0)');
|
|
|
|
|
|
|
|
|
|
const despues = await page.evaluate(() => ({
|
|
|
|
|
altoCabecera: document.querySelector('header')!.getBoundingClientRect().height,
|
|
|
|
|
inicioContenido: document.querySelector('main')!.getBoundingClientRect().top
|
|
|
|
|
}));
|
|
|
|
|
expect(despues.altoCabecera).toBeCloseTo(antes.altoCabecera, 0);
|
|
|
|
|
expect(despues.inicioContenido).toBeCloseTo(antes.inicioContenido, 0);
|
|
|
|
|
|
|
|
|
|
// Por nombre y no por cuenta: en móvil no se recorta el menú, y así la
|
|
|
|
|
// prueba dice cuál falta en vez de que el número no cuadra.
|
|
|
|
|
for (const seccion of ['Música', 'Músicos y grupos', 'Taller de letra', 'Blog']) {
|
|
|
|
|
await expect(nav.getByRole('link', { name: seccion })).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
await expect(nav.getByRole('link', { name: 'Bio', exact: true })).toHaveCount(0);
|
|
|
|
|
await expect(nav.getByRole('link', { name: 'Géneros', exact: true })).toHaveCount(0);
|
Una sola tabla de canciones, y la cola se puede llenar
Había dos listados: una tabla ordenable para el catálogo y una lista
aparte para los discos. Cada arreglo había que hacerlo dos veces y el
resultado era que la portada estaba en una y el año en la otra. Ahora es
un solo componente, y lo que cambia son las columnas de contexto, que se
encienden por props: dentro de un disco, la columna «Álbum» diría diez
veces lo mismo.
Solo recibe canciones. El estilo, el disco y la portada los resuelve
contra el catálogo que ya baja en `page.data`, así que quien la usa no
tiene que armar filas a mano, que era lo que obligaba a repetir código.
Cada fila lleva su portada —con la ampliada al pasar el ratón—, y debajo
del título van los géneros y las etiquetas en vez del primer verso: la
letra está entera en la ficha, y aquí lo que sirve para elegir es de qué
está hecho el tema. Las etiquetas se cortan por número, con un «+2» al
final: recortarlas con un degradado dejaba «Autoengaño» en «Autoeng», que
no parece una elección sino algo roto.
La cola ya se puede llenar. Hasta ahora solo se podía sustituir —cada
play la vaciaba y la llenaba con su contexto—, así que no había forma de
armar una escucha cogiendo temas de discos distintos. `encolar()` es la
otra mitad, y con la cola vacía arranca, porque un botón que no hace nada
es peor que no tenerlo.
Los géneros de una canción entran en el modelo: `cancion_genero` existía
en la base y no llegaba a la web. Son cosa distinta del estilo —el estilo
es cómo se ordena la obra aquí y es uno solo; los géneros son de qué está
hecha la canción y pueden ser varios— y llevan al glosario.
De paso: las letras largas van a dos columnas de 37ch, que es el mínimo
que deja caber dos y a la vez el verso más largo del catálogo —importa
afinarlo, porque en una letra dónde acaba cada verso es parte del texto—;
la página baja de 2808 a 2023 píxeles. Contacto sale de la barra de menú
y se queda solo en el pie. Y el titular pasa a «Letrista y creador
musical».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
|
|
|
|
|
// Contacto no: va solo en el pie, que es donde se busca escribir.
|
|
|
|
|
await expect(nav.getByRole('link', { name: 'Contacto' })).toHaveCount(0);
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el burger separa el acceso, las utilidades y el tema', async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
await expect(page.locator('html')).toHaveAttribute('data-hidratado', '');
|
|
|
|
|
await page.getByRole('button', { name: 'Menú' }).click();
|
|
|
|
|
|
|
|
|
|
const entrar = page.locator('.acciones-movil').getByRole('link', { name: 'Entrar' });
|
|
|
|
|
const buscar = page
|
|
|
|
|
.locator('.utilidades-movil')
|
|
|
|
|
.getByRole('link', { name: 'Buscar en Senza Paura' });
|
|
|
|
|
const carrito = page.locator('.utilidades-movil').getByRole('link', { name: /^Carrito/ });
|
|
|
|
|
const tema = page.locator('.tema-movil').getByRole('button', { name: /tema/ });
|
|
|
|
|
await expect(entrar).toBeVisible();
|
|
|
|
|
await expect(buscar).toBeVisible();
|
|
|
|
|
await expect(carrito).toBeVisible();
|
|
|
|
|
await expect(tema).toBeVisible();
|
|
|
|
|
|
|
|
|
|
const posiciones = await page.evaluate(() => {
|
|
|
|
|
const caja = (selector: string) => document.querySelector(selector)!.getBoundingClientRect();
|
|
|
|
|
return {
|
|
|
|
|
utilidades: caja('.utilidades-movil'),
|
|
|
|
|
buscar: caja('.utilidades-movil a:first-child'),
|
|
|
|
|
carrito: caja('.utilidades-movil a:last-child'),
|
|
|
|
|
tema: caja('.tema-movil'),
|
|
|
|
|
botonTema: caja('.tema-movil button'),
|
|
|
|
|
fuenteNavegacion: getComputedStyle(document.querySelector('.nav a')!).fontFamily,
|
|
|
|
|
fuenteAccion: getComputedStyle(document.querySelector('.opcion-menu-movil')!).fontFamily,
|
|
|
|
|
tamanoNavegacion: getComputedStyle(document.querySelector('.nav a')!).fontSize,
|
|
|
|
|
tamanoAccion: getComputedStyle(document.querySelector('.opcion-menu-movil')!).fontSize
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
expect(posiciones.carrito.top).toBeGreaterThanOrEqual(posiciones.buscar.bottom);
|
|
|
|
|
expect(posiciones.tema.top).toBeGreaterThan(posiciones.utilidades.bottom);
|
|
|
|
|
expect(posiciones.botonTema.width).toBe(56);
|
|
|
|
|
expect(posiciones.botonTema.height).toBe(56);
|
|
|
|
|
expect(posiciones.fuenteNavegacion).toBe(posiciones.fuenteAccion);
|
|
|
|
|
expect(posiciones.tamanoNavegacion).toBe(posiciones.tamanoAccion);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('en tema claro el menú móvil cubre el contenido con el fondo del tema', async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
|
|
|
await page.goto('/favoritos');
|
|
|
|
|
await expect(page.locator('html')).toHaveAttribute('data-hidratado', '');
|
|
|
|
|
await page.getByRole('button', { name: 'Menú' }).click();
|
|
|
|
|
await page.getByRole('button', { name: 'Cambiar al tema claro' }).click();
|
|
|
|
|
|
|
|
|
|
const colores = await page.locator('#menu-cabecera').evaluate((menu) => ({
|
|
|
|
|
menu: getComputedStyle(menu).backgroundColor,
|
|
|
|
|
pagina: getComputedStyle(document.body).backgroundColor
|
|
|
|
|
}));
|
|
|
|
|
expect(colores.menu).toBe(colores.pagina);
|
|
|
|
|
await expect(page.getByRole('navigation', { name: 'Principal' })).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('los filtros de música pasan a un panel táctil sin empujar el contenido', async ({
|
|
|
|
|
page
|
|
|
|
|
}) => {
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
|
|
|
|
|
|
|
|
await page.goto('/musica/baladas-romanticas');
|
|
|
|
|
const botonFiltros = page.getByRole('button', { name: /Filtros/ });
|
|
|
|
|
await expect(botonFiltros).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole('navigation', { name: 'Estilos' })).toBeHidden();
|
|
|
|
|
|
|
|
|
|
await botonFiltros.click();
|
|
|
|
|
const dialogo = page.getByRole('dialog', { name: 'Filtrar música' });
|
|
|
|
|
await expect(dialogo).toBeVisible();
|
|
|
|
|
await expect(dialogo.getByRole('navigation', { name: 'Estilos' })).toBeVisible();
|
|
|
|
|
await expect(dialogo.getByRole('link', { name: /Baladas románticas/ })).toHaveAttribute(
|
|
|
|
|
'aria-current',
|
|
|
|
|
'page'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const tactil = await dialogo
|
|
|
|
|
.getByRole('link', { name: /Baladas románticas/ })
|
|
|
|
|
.evaluate((enlace) => enlace.getBoundingClientRect().height);
|
|
|
|
|
expect(tactil).toBeGreaterThanOrEqual(48);
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
|
|
|
|
|
// El índice de géneros aún cabe como parte de su propia plantilla.
|
Las fichas de género salen del taller y tienen su propia sección
Con lo que pediste: portada con mapa de Iberoamérica, buscador y un aside
que empieza por Inicio, Los principios, Glosario y Letristas antes de la
lista de géneros.
El mapa es interactivo de verdad: cada país se pinta según lo que tenga
—marcado si es cuna de algún género, tenue si solo lo adoptó, apagado si
no hay nada—, y al pulsarlo la lista de abajo se parte en «nació aquí» y
«también se canta ahí». Esa diferencia es lo que un mapa cuenta mejor que
una lista.
Los contornos son de `BlankMap-Americas.svg` de Wikimedia, en dominio
público, recortados a los veinte países de Iberoamérica en América. De
640 KB a 36 KB comprimidos, y solo en esta ruta: coordenadas a un decimal
y fuera los 134 islotes que a este tamaño miden menos de un píxel. Va por
script y no pegado a mano, para poder rehacerlo.
Cada país lleva además un punto de agarre calculado —el promedio de los
vértices de su costa, no el centro de su rectángulo, que en Cuba cae en
el mar—: sin él, Puerto Rico mide cuatro píxeles y una isla alargada no
hay quien la acierte.
«Letristas» es nueva: las mismas personas que salen repartidas en nueve
fichas, aquí juntas, con su papel en cada género y sus obras.
La página del capítulo pasa a `$lib/components/Capitulo.svelte` y la usan
las dos secciones: son setecientas líneas, y copiarlas habría sido dos
sitios donde arreglar cada cosa.
Las direcciones viejas redirigen con 301 —`/taller/bolero` a
`/generos/bolero`— resolviendo contra el catálogo de capítulos, no contra
una lista escrita a mano que alguien tenga que ampliar. Un enlace
compartido no puede empezar a dar 404 porque hayamos reordenado el menú.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
await page.goto('/generos/bachata');
|
|
|
|
|
await expect(page.getByRole('navigation', { name: 'Contenido de géneros' })).toBeVisible();
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('la tabla se desplaza dentro de su marco, no arrastra la página', async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
|
|
|
await page.goto('/musica');
|
|
|
|
|
await page.getByRole('button', { name: 'Tabla' }).click();
|
|
|
|
|
|
|
|
|
|
const dentro = await page.evaluate(() => {
|
|
|
|
|
const marco = document.querySelector('.marco')!;
|
|
|
|
|
return marco.scrollWidth > marco.clientWidth;
|
|
|
|
|
});
|
|
|
|
|
expect(dentro).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el encabezado de la tabla no tapa la primera canción en anchura intermedia', async ({
|
|
|
|
|
page
|
|
|
|
|
}) => {
|
|
|
|
|
await page.setViewportSize({ width: 900, height: 650 });
|
|
|
|
|
await page.goto('/albumes/cupido-sin-flechas');
|
|
|
|
|
|
|
|
|
|
const posiciones = await page.locator('.marco table').evaluate((tabla) => {
|
|
|
|
|
const cabecera = tabla.querySelector('thead');
|
|
|
|
|
const celda = tabla.querySelector('thead th');
|
|
|
|
|
const primera = tabla.querySelector('tbody tr');
|
|
|
|
|
if (!cabecera || !celda || !primera) throw new Error('Tabla incompleta');
|
|
|
|
|
return {
|
|
|
|
|
finalCabecera: Math.round(cabecera.getBoundingClientRect().bottom),
|
|
|
|
|
inicioPrimera: Math.round(primera.getBoundingClientRect().top),
|
|
|
|
|
posicionCelda: getComputedStyle(celda).position
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(posiciones.posicionCelda).toBe('static');
|
|
|
|
|
expect(posiciones.inicioPrimera).toBeGreaterThanOrEqual(posiciones.finalCabecera);
|
|
|
|
|
});
|
Cierra una redirección abierta, quita código muerto y afina el responsive
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe('Respuesta al puntero', () => {
|
|
|
|
|
// Un control que no acusa el puntero parece deshabilitado. Se comprueba en
|
|
|
|
|
// pruebas porque no se ve en una captura en reposo, que es justo como se
|
|
|
|
|
// coló el botón dorado quedándose transparente al pasar por encima.
|
|
|
|
|
const CASOS: [string, 'link' | 'button', string][] = [
|
|
|
|
|
['/', 'link', 'Ver el disco'],
|
|
|
|
|
['/musica', 'button', 'Tabla'],
|
|
|
|
|
['/blog', 'link', 'Seguirlo por RSS'],
|
|
|
|
|
['/canciones/prometiste', 'link', 'Desamor'],
|
|
|
|
|
['/albumes/cupido-sin-flechas', 'button', 'Añadir']
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const [ruta, rol, nombre] of CASOS) {
|
|
|
|
|
test(`«${nombre}» acusa el puntero`, async ({ page }) => {
|
|
|
|
|
await page.goto(ruta);
|
|
|
|
|
const el = page.getByRole(rol, { name: nombre }).first();
|
|
|
|
|
|
|
|
|
|
const estilo = () =>
|
|
|
|
|
el.evaluate((e) => {
|
|
|
|
|
const s = getComputedStyle(e);
|
|
|
|
|
return [s.backgroundColor, s.color, s.borderColor, s.textDecorationLine].join('|');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const antes = await estilo();
|
|
|
|
|
await el.hover();
|
|
|
|
|
await expect.poll(estilo).not.toBe(antes);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('el botón dorado sigue siendo dorado al pasar por encima', async ({ page }) => {
|
|
|
|
|
// Pasó: una regla anterior usaba el atajo `background`, que borra
|
|
|
|
|
// `background-image`, y el botón se quedaba en un rectángulo oscuro con el
|
|
|
|
|
// borde de oro.
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
const boton = page.getByRole('button', { name: 'Poner el disco' });
|
|
|
|
|
|
|
|
|
|
await boton.hover();
|
|
|
|
|
const fondo = await boton.evaluate((e) => getComputedStyle(e).backgroundImage);
|
|
|
|
|
expect(fondo).toContain('linear-gradient');
|
|
|
|
|
});
|
|
|
|
|
});
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
test.describe('Contraste y color de marca', () => {
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
// El dorado hace dos trabajos y no puede ser el mismo valor en los dos: de
|
|
|
|
|
// relleno tiene que ser claro para que encima se lea el marino, y escrito
|
|
|
|
|
// tiene que ser hondo para leerse sobre el fondo. Esto lo fija.
|
|
|
|
|
const MINIMO = 4.5;
|
|
|
|
|
|
|
|
|
|
async function contraste(page: import('@playwright/test').Page, selector: string) {
|
|
|
|
|
return page
|
|
|
|
|
.locator(selector)
|
|
|
|
|
.first()
|
|
|
|
|
.evaluate((e) => {
|
|
|
|
|
const aRgb = (c: string) => {
|
|
|
|
|
const x = document.createElement('canvas').getContext('2d')!;
|
|
|
|
|
x.fillStyle = c;
|
|
|
|
|
x.fillRect(0, 0, 1, 1);
|
|
|
|
|
const d = x.getImageData(0, 0, 1, 1).data;
|
|
|
|
|
return [d[0], d[1], d[2]];
|
|
|
|
|
};
|
|
|
|
|
const lum = ([r, g, b]: number[]) => {
|
|
|
|
|
const f = (v: number) => {
|
|
|
|
|
v /= 255;
|
|
|
|
|
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
|
|
|
};
|
|
|
|
|
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
|
|
|
};
|
|
|
|
|
const s = getComputedStyle(e);
|
|
|
|
|
const [a, b] = [lum(aRgb(s.color)), lum(aRgb(s.backgroundColor))].sort((m, n) => n - m);
|
|
|
|
|
return (a + 0.05) / (b + 0.05);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Se pone `data-tema` a mano en vez de emular `prefers-color-scheme`: el
|
|
|
|
|
* sitio ya no sigue al sistema —el marino es el de la marca y se aplica
|
|
|
|
|
* siempre—, así que emular el esquema claro dejaba esta prueba comprobando
|
|
|
|
|
* dos veces lo mismo sin que nadie lo notara.
|
|
|
|
|
*/
|
|
|
|
|
for (const modo of ['oscuro', 'claro'] as const) {
|
|
|
|
|
test(`el botón dorado se lee en tema ${modo}`, async ({ page }) => {
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
await page.goto('/');
|
|
|
|
|
await page.evaluate((m) => {
|
|
|
|
|
document.documentElement.dataset.tema = m;
|
|
|
|
|
}, modo);
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
expect(await contraste(page, '.btn--primary')).toBeGreaterThan(MINIMO);
|
|
|
|
|
|
|
|
|
|
// Y también con el puntero encima, que es cuando la banda de luz se
|
|
|
|
|
// mueve por debajo del texto.
|
|
|
|
|
await page.locator('.btn--primary').first().hover();
|
|
|
|
|
expect(await contraste(page, '.btn--primary')).toBeGreaterThan(MINIMO);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('sin elegir nada, el sitio sale en marino aunque el sistema pida claro', async ({
|
|
|
|
|
page
|
|
|
|
|
}) => {
|
|
|
|
|
// Es una decisión, no un descuido: el color forma parte de la marca. Si
|
|
|
|
|
// algún día vuelve a seguir al sistema, esta prueba lo dice.
|
|
|
|
|
await page.emulateMedia({ colorScheme: 'light' });
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
|
|
|
|
|
const { fondo, claridad } = await page.evaluate(() => {
|
|
|
|
|
const color = getComputedStyle(document.body).backgroundColor;
|
|
|
|
|
/*
|
|
|
|
|
* El color viene en `oklch()`, que no se puede desmontar a mano. Se
|
|
|
|
|
* pinta y se lee el píxel: es la única conversión que no se inventa
|
|
|
|
|
* nada, y en su día unas cuentas sobre la cadena dieron números que no
|
|
|
|
|
* significaban nada.
|
|
|
|
|
*/
|
|
|
|
|
const lienzo = document.createElement('canvas');
|
|
|
|
|
lienzo.width = lienzo.height = 1;
|
|
|
|
|
const d = lienzo.getContext('2d')!;
|
|
|
|
|
d.fillStyle = color;
|
|
|
|
|
d.fillRect(0, 0, 1, 1);
|
|
|
|
|
const [r, g, b] = d.getImageData(0, 0, 1, 1).data;
|
|
|
|
|
return { fondo: color, claridad: (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(claridad, `el fondo era ${fondo}`).toBeLessThan(0.2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el botón de la cabecera sigue llevando al claro', async ({ page }) => {
|
|
|
|
|
// El marino manda por defecto, pero el claro no desaparece: está a un clic
|
|
|
|
|
// y la elección se recuerda.
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: /tema|claro|oscuro/i }).click();
|
|
|
|
|
await expect(page.locator('html')).toHaveAttribute('data-tema', 'claro');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/* El identificador de cabecera es monocromo y no depende de degradados. */
|
El nombre de la cabecera, en oro de verdad y más grande
Las mismas cinco paradas y el mismo ángulo que el botón dorado, recortadas
sobre las letras, y la banda de luz recorre el nombre al pasar el puntero igual
que recorre el botón. De 24 a 34 píxeles, que es el siguiente peldaño de la
escala y no un número a ojo.
El recorte va dentro de un `@supports` y el color plano se queda fuera: sin el
recorte, `color: transparent` dejaría la marca invisible, y eso es peor que
dejarla dorada y lisa. Es el mismo cuidado que ya tenía el botón, que guarda un
color de reserva por si el degradado no llega a pintar.
Y hace falta un segundo degradado, `--oro-escrito`. Sobre marino es
literalmente el mismo que el del botón —la peor de sus paradas da 5,4 de
contraste—, pero sobre papel no vale: medido contra el fondo claro, la parada
de brillo da **1,26**, la de superficie 2,05 y la de sombra 3,79, las tres por
debajo del mínimo de 4,5. Con el degradado del botón, el centro de las letras
desaparecía. En claro la banda va al revés: el punto más claro es el propio
`--c-accent` —5,29— y los extremos se oscurecen. Sigue pareciendo metal porque
lo que hace el efecto es la banda que cruza, no que llegue a brillar.
Es la regla que el proyecto ya tenía escrita y que yo no había aplicado: el
dorado hace dos trabajos, y de relleno tiene que ser claro mientras que escrito
tiene que ser hondo.
La prueba comprueba **cada parada** del degradado contra el fondo, en los dos
temas. A ojo, en la captura, el fallo se veía como «algo más pálido»; en
números, como ilegible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
for (const modo of ['oscuro', 'claro'] as const) {
|
|
|
|
|
test(`el logo plano conserva el contraste en tema ${modo}`, async ({ page }) => {
|
El nombre de la cabecera, en oro de verdad y más grande
Las mismas cinco paradas y el mismo ángulo que el botón dorado, recortadas
sobre las letras, y la banda de luz recorre el nombre al pasar el puntero igual
que recorre el botón. De 24 a 34 píxeles, que es el siguiente peldaño de la
escala y no un número a ojo.
El recorte va dentro de un `@supports` y el color plano se queda fuera: sin el
recorte, `color: transparent` dejaría la marca invisible, y eso es peor que
dejarla dorada y lisa. Es el mismo cuidado que ya tenía el botón, que guarda un
color de reserva por si el degradado no llega a pintar.
Y hace falta un segundo degradado, `--oro-escrito`. Sobre marino es
literalmente el mismo que el del botón —la peor de sus paradas da 5,4 de
contraste—, pero sobre papel no vale: medido contra el fondo claro, la parada
de brillo da **1,26**, la de superficie 2,05 y la de sombra 3,79, las tres por
debajo del mínimo de 4,5. Con el degradado del botón, el centro de las letras
desaparecía. En claro la banda va al revés: el punto más claro es el propio
`--c-accent` —5,29— y los extremos se oscurecen. Sigue pareciendo metal porque
lo que hace el efecto es la banda que cruza, no que llegue a brillar.
Es la regla que el proyecto ya tenía escrita y que yo no había aplicado: el
dorado hace dos trabajos, y de relleno tiene que ser claro mientras que escrito
tiene que ser hondo.
La prueba comprueba **cada parada** del degradado contra el fondo, en los dos
temas. A ojo, en la captura, el fallo se veía como «algo más pálido»; en
números, como ilegible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
await page.goto('/');
|
|
|
|
|
await page.evaluate((m) => {
|
|
|
|
|
document.documentElement.dataset.tema = m;
|
|
|
|
|
}, modo);
|
|
|
|
|
|
|
|
|
|
const resultado = await page.evaluate(() => {
|
|
|
|
|
const logo = document.querySelector<SVGElement>('header [data-logo-senza-paura]')!;
|
|
|
|
|
const fondo = document.querySelector<HTMLElement>('header')!;
|
|
|
|
|
const color = getComputedStyle(logo).color;
|
|
|
|
|
const base = getComputedStyle(fondo).backgroundColor;
|
|
|
|
|
return { color, base, etiqueta: logo.tagName, relleno: getComputedStyle(logo).fill };
|
El nombre de la cabecera, en oro de verdad y más grande
Las mismas cinco paradas y el mismo ángulo que el botón dorado, recortadas
sobre las letras, y la banda de luz recorre el nombre al pasar el puntero igual
que recorre el botón. De 24 a 34 píxeles, que es el siguiente peldaño de la
escala y no un número a ojo.
El recorte va dentro de un `@supports` y el color plano se queda fuera: sin el
recorte, `color: transparent` dejaría la marca invisible, y eso es peor que
dejarla dorada y lisa. Es el mismo cuidado que ya tenía el botón, que guarda un
color de reserva por si el degradado no llega a pintar.
Y hace falta un segundo degradado, `--oro-escrito`. Sobre marino es
literalmente el mismo que el del botón —la peor de sus paradas da 5,4 de
contraste—, pero sobre papel no vale: medido contra el fondo claro, la parada
de brillo da **1,26**, la de superficie 2,05 y la de sombra 3,79, las tres por
debajo del mínimo de 4,5. Con el degradado del botón, el centro de las letras
desaparecía. En claro la banda va al revés: el punto más claro es el propio
`--c-accent` —5,29— y los extremos se oscurecen. Sigue pareciendo metal porque
lo que hace el efecto es la banda que cruza, no que llegue a brillar.
Es la regla que el proyecto ya tenía escrita y que yo no había aplicado: el
dorado hace dos trabajos, y de relleno tiene que ser claro mientras que escrito
tiene que ser hondo.
La prueba comprueba **cada parada** del degradado contra el fondo, en los dos
temas. A ojo, en la captura, el fallo se veía como «algo más pálido»; en
números, como ilegible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(resultado.etiqueta).toBe('svg');
|
|
|
|
|
expect(resultado.relleno).toBe(resultado.color);
|
|
|
|
|
expect(resultado.color).not.toBe(resultado.base);
|
El nombre de la cabecera, en oro de verdad y más grande
Las mismas cinco paradas y el mismo ángulo que el botón dorado, recortadas
sobre las letras, y la banda de luz recorre el nombre al pasar el puntero igual
que recorre el botón. De 24 a 34 píxeles, que es el siguiente peldaño de la
escala y no un número a ojo.
El recorte va dentro de un `@supports` y el color plano se queda fuera: sin el
recorte, `color: transparent` dejaría la marca invisible, y eso es peor que
dejarla dorada y lisa. Es el mismo cuidado que ya tenía el botón, que guarda un
color de reserva por si el degradado no llega a pintar.
Y hace falta un segundo degradado, `--oro-escrito`. Sobre marino es
literalmente el mismo que el del botón —la peor de sus paradas da 5,4 de
contraste—, pero sobre papel no vale: medido contra el fondo claro, la parada
de brillo da **1,26**, la de superficie 2,05 y la de sombra 3,79, las tres por
debajo del mínimo de 4,5. Con el degradado del botón, el centro de las letras
desaparecía. En claro la banda va al revés: el punto más claro es el propio
`--c-accent` —5,29— y los extremos se oscurecen. Sigue pareciendo metal porque
lo que hace el efecto es la banda que cruza, no que llegue a brillar.
Es la regla que el proyecto ya tenía escrita y que yo no había aplicado: el
dorado hace dos trabajos, y de relleno tiene que ser claro mientras que escrito
tiene que ser hondo.
La prueba comprueba **cada parada** del degradado contra el fondo, en los dos
temas. A ojo, en la captura, el fallo se veía como «algo más pálido»; en
números, como ilegible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
test('todos los botones llevan la misma letra', async ({ page }) => {
|
|
|
|
|
// Dos botones juntos con distinto cuerpo se ven como un descuido, y así
|
|
|
|
|
// estaban: el dorado a 16px y el de al lado a 14.
|
|
|
|
|
await page.goto('/');
|
|
|
|
|
|
Textos legales, fuentes propias y ajustes de botones
Cinco páginas legales en /legal, enlazadas desde el pie de todas las páginas:
información legal, condiciones de uso, privacidad, cookies y condiciones de
venta. Son borradores con los datos del titular marcados entre corchetes y un
aviso de que hace falta revisión de un abogado antes de vender: cubren lo que
pide la ley española, no son un documento validado.
La política de cookies y la de privacidad están escritas mirando el código, no
copiadas de una plantilla: dos cookies, las dos necesarias, y dos terceros
—Stripe y Resend— que reciben lo imprescindible.
Y eran tres, porque las tipografías venían de Google: cada visita mandaba la IP
del visitante a un tercero sin pedirlo y sin que aportara nada. Ahora se sirven
desde el propio dominio. Cargar la web ya no contacta con ningún dominio ajeno,
y hay una prueba que lo comprueba.
Aparte: el símbolo de marca registrada en el aviso de derechos, los botones de
reproducir de las tarjetas pasan de 48 a 36 píxeles —eran casi una quinta parte
de la portada, y cambiaban de proporción según la tarjeta— y el de ver el
carrito solo aparece cuando hay algo dentro.
De paso se arregla un fallo de tipos que estaba latente: `resolve()` tiene una
sobrecarga por ruta y no acepta una unión, así que la navegación dejaba de
compilar en cuanto se añadían rutas. Ahora se resuelve en `site.ts`, una a una.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
// Los botones de página. El de la esquina de una tarjeta queda fuera a
|
|
|
|
|
// propósito: es un adorno accionable sobre una portada y tiene su propio
|
|
|
|
|
// tamaño, más pequeño y fijo.
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
const tamanos = await page
|
|
|
|
|
.locator('a.btn, button.solido')
|
Textos legales, fuentes propias y ajustes de botones
Cinco páginas legales en /legal, enlazadas desde el pie de todas las páginas:
información legal, condiciones de uso, privacidad, cookies y condiciones de
venta. Son borradores con los datos del titular marcados entre corchetes y un
aviso de que hace falta revisión de un abogado antes de vender: cubren lo que
pide la ley española, no son un documento validado.
La política de cookies y la de privacidad están escritas mirando el código, no
copiadas de una plantilla: dos cookies, las dos necesarias, y dos terceros
—Stripe y Resend— que reciben lo imprescindible.
Y eran tres, porque las tipografías venían de Google: cada visita mandaba la IP
del visitante a un tercero sin pedirlo y sin que aportara nada. Ahora se sirven
desde el propio dominio. Cargar la web ya no contacta con ningún dominio ajeno,
y hay una prueba que lo comprueba.
Aparte: el símbolo de marca registrada en el aviso de derechos, los botones de
reproducir de las tarjetas pasan de 48 a 36 píxeles —eran casi una quinta parte
de la portada, y cambiaban de proporción según la tarjeta— y el de ver el
carrito solo aparece cuando hay algo dentro.
De paso se arregla un fallo de tipos que estaba latente: `resolve()` tiene una
sobrecarga por ruta y no acepta una unión, así que la navegación dejaba de
compilar en cuanto se añadían rutas. Ahora se resuelve en `site.ts`, una a una.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
.evaluateAll((es) =>
|
|
|
|
|
es
|
|
|
|
|
.filter((e) => !e.closest('.tarjeta'))
|
|
|
|
|
.map((e) => `${getComputedStyle(e).fontSize} ${getComputedStyle(e).padding}`)
|
|
|
|
|
);
|
|
|
|
|
expect([...new Set(tamanos)]).toHaveLength(1);
|
Arregla el contraste del botón dorado y unifica la letra de los botones
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
});
|