You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/e2e/navegacion.e2e.ts

360 lines
13 KiB

import { expect, test } from '@playwright/test';
test.describe('Navegación', () => {
test('la portada presenta al artista y lleva a la música', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Senza Paura');
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
await expect(page.getByText('Letrista y creador musical').first()).toBeVisible();
// La cabecera destaca un tema y ofrece el catálogo entero.
await page.getByRole('link', { name: 'Toda la música' }).click();
await expect(page).toHaveURL('/musica');
});
test('el fondo de la cabecera es propio, no un incrustado', async ({ page }) => {
// Lo que se comprueba es la decisión, no la técnica con que esté hecha:
// un iframe de terceros en la primera pantalla pesaría más que el resto de
// la página y dejaría cookies antes de que nadie pida ver nada.
await page.goto('/');
await expect(page.locator('iframe')).toHaveCount(0);
// Y el fondo existe: hoy es un shader propio sobre una imagen propia.
await expect(page.locator('.fondo')).not.toHaveCount(0);
});
test('la cabecera acompaña al desplazamiento', async ({ page }) => {
await page.goto('/');
await page.evaluate(() => window.scrollBy(0, 1400));
const cabecera = page.getByRole('banner');
await expect(cabecera).toBeVisible();
const caja = await cabecera.boundingBox();
expect(caja?.y).toBeLessThan(1);
});
test('el botón de tema cierra el menú', async ({ page }) => {
await page.goto('/');
const ultimo = page.locator('header .cuenta > *').last();
await expect(ultimo).toHaveRole('button');
await expect(ultimo).toHaveAttribute('aria-label', /tema/);
});
Portada por estilos, tarjetas con otro movimiento, y fuera la palabra «segmento» La palabra se va del proyecto entero, interfaz e identificadores. Venía del encargo inicial pero no es la del oficio y se colaba en la interfaz: lo que hay son estilos, discos y temas. Lo que agrupa un estilo con su obra pasa a ser `EstiloConObra`, y el componente que lo pinta, `BloqueDeEstilo`. La portada cuenta el catálogo en tres pasos: el estilo principal a fila completa, sus discos debajo, y el resto en una rejilla donde cada tarjeta dice cuánto hay y de qué se habla. Las etiquetas de un estilo no se declaran: salen de sus temas, así que no hay dos sitios que mantener. La página de un estilo estrena su propio `h1` —antes heredaba un `h2` del listado, y se quedaba sin encabezado de nivel uno— y pierde el botón de reproducir suelto en la cabecera, que no decía qué iba a sonar. Los demás estilos la acompañan en una barra lateral que ahora vive en el layout de la sección, así que la llevan también la portada de música y cualquier página que se añada. Las tarjetas se mueven de otra manera: reflejo que nace bajo el puntero, borde dorado, portada que recupera el color, subrayado que crece y respuesta al pulsar. Nada usa la curva por defecto. Dos rarezas del minificador de CSS, ambas comprobadas en el build y ambas dejaban efectos muertos: junta las funciones de un filtro sin el espacio que las separa, y borra `backdrop-filter` sin prefijo si se declara también la versión `-webkit-`. Y la canción real estrena su vídeo de YouTube. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
test('la música se presenta por estilos', async ({ page }) => {
await page.goto('/musica');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Música');
Portada por estilos, tarjetas con otro movimiento, y fuera la palabra «segmento» La palabra se va del proyecto entero, interfaz e identificadores. Venía del encargo inicial pero no es la del oficio y se colaba en la interfaz: lo que hay son estilos, discos y temas. Lo que agrupa un estilo con su obra pasa a ser `EstiloConObra`, y el componente que lo pinta, `BloqueDeEstilo`. La portada cuenta el catálogo en tres pasos: el estilo principal a fila completa, sus discos debajo, y el resto en una rejilla donde cada tarjeta dice cuánto hay y de qué se habla. Las etiquetas de un estilo no se declaran: salen de sus temas, así que no hay dos sitios que mantener. La página de un estilo estrena su propio `h1` —antes heredaba un `h2` del listado, y se quedaba sin encabezado de nivel uno— y pierde el botón de reproducir suelto en la cabecera, que no decía qué iba a sonar. Los demás estilos la acompañan en una barra lateral que ahora vive en el layout de la sección, así que la llevan también la portada de música y cualquier página que se añada. Las tarjetas se mueven de otra manera: reflejo que nace bajo el puntero, borde dorado, portada que recupera el color, subrayado que crece y respuesta al pulsar. Nada usa la curva por defecto. Dos rarezas del minificador de CSS, ambas comprobadas en el build y ambas dejaban efectos muertos: junta las funciones de un filtro sin el espacio que las separa, y borra `backdrop-filter` sin prefijo si se declara también la versión `-webkit-`. Y la canción real estrena su vídeo de YouTube. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
// El estilo principal va primero.
const estilos = page.getByRole('heading', { level: 2 });
await expect(estilos.first()).toHaveText('Baladas románticas');
for (const estilo of ['Clásica', 'Rock', 'Pop melódico']) {
await expect(page.getByRole('heading', { level: 2, name: estilo })).toBeVisible();
}
});
Portada por estilos, tarjetas con otro movimiento, y fuera la palabra «segmento» La palabra se va del proyecto entero, interfaz e identificadores. Venía del encargo inicial pero no es la del oficio y se colaba en la interfaz: lo que hay son estilos, discos y temas. Lo que agrupa un estilo con su obra pasa a ser `EstiloConObra`, y el componente que lo pinta, `BloqueDeEstilo`. La portada cuenta el catálogo en tres pasos: el estilo principal a fila completa, sus discos debajo, y el resto en una rejilla donde cada tarjeta dice cuánto hay y de qué se habla. Las etiquetas de un estilo no se declaran: salen de sus temas, así que no hay dos sitios que mantener. La página de un estilo estrena su propio `h1` —antes heredaba un `h2` del listado, y se quedaba sin encabezado de nivel uno— y pierde el botón de reproducir suelto en la cabecera, que no decía qué iba a sonar. Los demás estilos la acompañan en una barra lateral que ahora vive en el layout de la sección, así que la llevan también la portada de música y cualquier página que se añada. Las tarjetas se mueven de otra manera: reflejo que nace bajo el puntero, borde dorado, portada que recupera el color, subrayado que crece y respuesta al pulsar. Nada usa la curva por defecto. Dos rarezas del minificador de CSS, ambas comprobadas en el build y ambas dejaban efectos muertos: junta las funciones de un filtro sin el espacio que las separa, y borra `backdrop-filter` sin prefijo si se declara también la versión `-webkit-`. Y la canción real estrena su vídeo de YouTube. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
test('cada estilo tiene su propia página, con su propio título', async ({ page }) => {
await page.goto('/musica');
await page.getByRole('heading', { level: 2, name: 'Clásica' }).getByRole('link').click();
await expect(page).toHaveURL('/musica/clasica');
Portada por estilos, tarjetas con otro movimiento, y fuera la palabra «segmento» La palabra se va del proyecto entero, interfaz e identificadores. Venía del encargo inicial pero no es la del oficio y se colaba en la interfaz: lo que hay son estilos, discos y temas. Lo que agrupa un estilo con su obra pasa a ser `EstiloConObra`, y el componente que lo pinta, `BloqueDeEstilo`. La portada cuenta el catálogo en tres pasos: el estilo principal a fila completa, sus discos debajo, y el resto en una rejilla donde cada tarjeta dice cuánto hay y de qué se habla. Las etiquetas de un estilo no se declaran: salen de sus temas, así que no hay dos sitios que mantener. La página de un estilo estrena su propio `h1` —antes heredaba un `h2` del listado, y se quedaba sin encabezado de nivel uno— y pierde el botón de reproducir suelto en la cabecera, que no decía qué iba a sonar. Los demás estilos la acompañan en una barra lateral que ahora vive en el layout de la sección, así que la llevan también la portada de música y cualquier página que se añada. Las tarjetas se mueven de otra manera: reflejo que nace bajo el puntero, borde dorado, portada que recupera el color, subrayado que crece y respuesta al pulsar. Nada usa la curva por defecto. Dos rarezas del minificador de CSS, ambas comprobadas en el build y ambas dejaban efectos muertos: junta las funciones de un filtro sin el espacio que las separa, y borra `backdrop-filter` sin prefijo si se declara también la versión `-webkit-`. Y la canción real estrena su vídeo de YouTube. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
// En su página el estilo es el h1, no un h2 heredado del listado: es de lo
// que va la página.
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Clásica');
await expect(page.getByRole('link', { name: 'Preludios para piano' }).first()).toBeVisible();
});
Portada por estilos, tarjetas con otro movimiento, y fuera la palabra «segmento» La palabra se va del proyecto entero, interfaz e identificadores. Venía del encargo inicial pero no es la del oficio y se colaba en la interfaz: lo que hay son estilos, discos y temas. Lo que agrupa un estilo con su obra pasa a ser `EstiloConObra`, y el componente que lo pinta, `BloqueDeEstilo`. La portada cuenta el catálogo en tres pasos: el estilo principal a fila completa, sus discos debajo, y el resto en una rejilla donde cada tarjeta dice cuánto hay y de qué se habla. Las etiquetas de un estilo no se declaran: salen de sus temas, así que no hay dos sitios que mantener. La página de un estilo estrena su propio `h1` —antes heredaba un `h2` del listado, y se quedaba sin encabezado de nivel uno— y pierde el botón de reproducir suelto en la cabecera, que no decía qué iba a sonar. Los demás estilos la acompañan en una barra lateral que ahora vive en el layout de la sección, así que la llevan también la portada de música y cualquier página que se añada. Las tarjetas se mueven de otra manera: reflejo que nace bajo el puntero, borde dorado, portada que recupera el color, subrayado que crece y respuesta al pulsar. Nada usa la curva por defecto. Dos rarezas del minificador de CSS, ambas comprobadas en el build y ambas dejaban efectos muertos: junta las funciones de un filtro sin el espacio que las separa, y borra `backdrop-filter` sin prefijo si se declara también la versión `-webkit-`. Y la canción real estrena su vídeo de YouTube. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
test('la página de un estilo lleva al lado todos los demás', async ({ page }) => {
await page.goto('/musica/clasica');
const lateral = page.getByRole('navigation', { name: 'Estilos' });
await expect(lateral).toBeVisible();
await expect(lateral.getByRole('link', { name: /Rock/ })).toBeVisible();
// Y marca en cuál estás.
await expect(lateral.getByRole('link', { name: /Clásica/ })).toHaveAttribute(
'aria-current',
'page'
);
});
test('desde un estilo se llega al álbum y del álbum al tema', async ({ page }) => {
await page.goto('/musica/baladas-romanticas');
await page.getByRole('link', { name: 'Como si nada' }).first().click();
await expect(page).toHaveURL('/albumes/como-si-nada');
await expect(page.getByRole('heading', { level: 2, name: 'Temas' })).toBeVisible();
await page.getByRole('link', { name: 'Media verdad' }).click();
await expect(page).toHaveURL('/canciones/media-verdad');
});
test('una dirección inexistente muestra la página de error', async ({ page }) => {
const respuesta = await page.goto('/canciones/no-existe');
expect(respuesta?.status()).toBe(404);
Las páginas de error, con su lámina y su tema Cada una lleva la suya: la del 404 y la del 500, recortadas del original —traían una banda plana pegada a la derecha, de generarlas— y pasadas a webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva dibujado: dice lo mismo con otras palabras, para que no se lea dos veces seguidas y para que la página siga teniendo titular si la imagen no carga. La música suena con un botón, no sola. Los navegadores cortan el audio que empieza sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría, es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice. Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera —SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al 500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra para esto. Un 403 no es ninguna de las dos cosas: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración, con su mensaje, en vez de con una lámina que miente. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni pintar su propia página de error. Va desnudo a propósito: sin hoja de estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que pidiera fuera podría estar caído también. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
// El titular no repite lo que la lámina ya lleva dibujado.
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Esta dirección no lleva a ninguna parte'
);
// El mensaje que lanzó el `load`, que dice más que la frase de reserva.
await expect(page.getByText('Ese tema no existe.')).toBeVisible();
await expect(page).toHaveTitle(/Página no encontrada/);
});
});
test.describe('La página de error', () => {
test('lleva su lámina y sale del paso', async ({ page }) => {
Las páginas de error, a pantalla completa y desnudas Las láminas están dibujadas como carteles y venían armonizadas: el dibujo ocupa el 62,6 % de la izquierda y el resto es una banda lisa, que es el sitio del texto. Las recorté creyendo que sobraba y no: ahora va la lámina entera de fondo, pegada a la izquierda con el alto exacto de la pantalla, y lo que sobre a la derecha se pinta del color de la propia banda, así que en un monitor ancho la banda se alarga y no se ve dónde acaba la imagen. La columna del texto empieza a 0,88 × el alto, que es donde empieza la banda sea cual sea el ancho. Sin cabecera, sin pie y sin barra de reproducción. Un cartel a pantalla completa con el menú del sitio encima deja de ser un cartel, y de un error se sale por la puerta que pone la propia página. En vertical no vale ese reparto: al alto de un móvil el cartel se quedaría en una franja del centro con el número cortado por los lados. Ahí se apila, y con la otra versión del archivo —solo el dibujo, sin la banda—, con el panel del color del borde del dibujo, que es exactamente el color con el que la imagen termina. Un 403 no es ninguno de los dos casos: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, desnudo a propósito, para cuando la aplicación no puede ni pintar su propia página de error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
const respuestas: Record<string, number> = {};
page.on('response', (r) => {
if (r.url().includes('/errores/')) respuestas[r.url().split('/').pop()!] = r.status();
});
Las páginas de error, con su lámina y su tema Cada una lleva la suya: la del 404 y la del 500, recortadas del original —traían una banda plana pegada a la derecha, de generarlas— y pasadas a webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva dibujado: dice lo mismo con otras palabras, para que no se lea dos veces seguidas y para que la página siga teniendo titular si la imagen no carga. La música suena con un botón, no sola. Los navegadores cortan el audio que empieza sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría, es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice. Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera —SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al 500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra para esto. Un 403 no es ninguna de las dos cosas: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración, con su mensaje, en vez de con una lámina que miente. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni pintar su propia página de error. Va desnudo a propósito: sin hoja de estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que pidiera fuera podría estar caído también. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
await page.goto('/no-existe-esto');
Las páginas de error, a pantalla completa y desnudas Las láminas están dibujadas como carteles y venían armonizadas: el dibujo ocupa el 62,6 % de la izquierda y el resto es una banda lisa, que es el sitio del texto. Las recorté creyendo que sobraba y no: ahora va la lámina entera de fondo, pegada a la izquierda con el alto exacto de la pantalla, y lo que sobre a la derecha se pinta del color de la propia banda, así que en un monitor ancho la banda se alarga y no se ve dónde acaba la imagen. La columna del texto empieza a 0,88 × el alto, que es donde empieza la banda sea cual sea el ancho. Sin cabecera, sin pie y sin barra de reproducción. Un cartel a pantalla completa con el menú del sitio encima deja de ser un cartel, y de un error se sale por la puerta que pone la propia página. En vertical no vale ese reparto: al alto de un móvil el cartel se quedaría en una franja del centro con el número cortado por los lados. Ahí se apila, y con la otra versión del archivo —solo el dibujo, sin la banda—, con el panel del color del borde del dibujo, que es exactamente el color con el que la imagen termina. Un 403 no es ninguno de los dos casos: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, desnudo a propósito, para cuando la aplicación no puede ni pintar su propia página de error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
// La lámina es el fondo de la pantalla, no una imagen dentro del contenido.
const pantalla = page.locator('.pantalla');
await expect(pantalla).toHaveCSS('background-image', /errores\/404\.webp/);
// Y el archivo existe de verdad, no solo la regla que lo nombra.
expect(respuestas['404.webp']).toBe(200);
Las páginas de error, con su lámina y su tema Cada una lleva la suya: la del 404 y la del 500, recortadas del original —traían una banda plana pegada a la derecha, de generarlas— y pasadas a webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva dibujado: dice lo mismo con otras palabras, para que no se lea dos veces seguidas y para que la página siga teniendo titular si la imagen no carga. La música suena con un botón, no sola. Los navegadores cortan el audio que empieza sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría, es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice. Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera —SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al 500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra para esto. Un 403 no es ninguna de las dos cosas: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración, con su mensaje, en vez de con una lámina que miente. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni pintar su propia página de error. Va desnudo a propósito: sin hoja de estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que pidiera fuera podría estar caído también. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
await page.getByRole('link', { name: 'Volver al inicio' }).click();
await expect(page).toHaveURL('/');
});
Las páginas de error, a pantalla completa y desnudas Las láminas están dibujadas como carteles y venían armonizadas: el dibujo ocupa el 62,6 % de la izquierda y el resto es una banda lisa, que es el sitio del texto. Las recorté creyendo que sobraba y no: ahora va la lámina entera de fondo, pegada a la izquierda con el alto exacto de la pantalla, y lo que sobre a la derecha se pinta del color de la propia banda, así que en un monitor ancho la banda se alarga y no se ve dónde acaba la imagen. La columna del texto empieza a 0,88 × el alto, que es donde empieza la banda sea cual sea el ancho. Sin cabecera, sin pie y sin barra de reproducción. Un cartel a pantalla completa con el menú del sitio encima deja de ser un cartel, y de un error se sale por la puerta que pone la propia página. En vertical no vale ese reparto: al alto de un móvil el cartel se quedaría en una franja del centro con el número cortado por los lados. Ahí se apila, y con la otra versión del archivo —solo el dibujo, sin la banda—, con el panel del color del borde del dibujo, que es exactamente el color con el que la imagen termina. Un 403 no es ninguno de los dos casos: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, desnudo a propósito, para cuando la aplicación no puede ni pintar su propia página de error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
/*
* Va desnuda: un cartel a pantalla completa con la barra del sitio encima
* deja de ser un cartel. De un error se sale por la puerta que pone la
* propia página.
*/
test('no lleva cabecera, ni pie, ni reproductor', async ({ page }) => {
await page.goto('/no-existe-esto');
await expect(page.locator('header')).toHaveCount(0);
await expect(page.locator('footer')).toHaveCount(0);
await expect(page.getByRole('region', { name: 'Reproductor' })).toHaveCount(0);
await expect(page.getByRole('link', { name: 'Volver al inicio' })).toBeVisible();
});
Las páginas de error, con su lámina y su tema Cada una lleva la suya: la del 404 y la del 500, recortadas del original —traían una banda plana pegada a la derecha, de generarlas— y pasadas a webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva dibujado: dice lo mismo con otras palabras, para que no se lea dos veces seguidas y para que la página siga teniendo titular si la imagen no carga. La música suena con un botón, no sola. Los navegadores cortan el audio que empieza sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría, es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice. Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera —SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al 500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra para esto. Un 403 no es ninguna de las dos cosas: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración, con su mensaje, en vez de con una lámina que miente. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni pintar su propia página de error. Va desnudo a propósito: sin hoja de estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que pidiera fuera podría estar caído también. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
/*
* Sin esto, todo el que aterrice en un 404 se traería 686 KB de mp3 que a lo
* mejor no va a escuchar. Es la razón de ser del `preload="none"`.
*/
test('no se trae el mp3 hasta que se pide', async ({ page }) => {
const pedidos: string[] = [];
page.on('request', (p) => {
if (p.url().includes('/errores/')) pedidos.push(p.url());
});
await page.goto('/no-existe-esto');
Las páginas de error, a pantalla completa y desnudas Las láminas están dibujadas como carteles y venían armonizadas: el dibujo ocupa el 62,6 % de la izquierda y el resto es una banda lisa, que es el sitio del texto. Las recorté creyendo que sobraba y no: ahora va la lámina entera de fondo, pegada a la izquierda con el alto exacto de la pantalla, y lo que sobre a la derecha se pinta del color de la propia banda, así que en un monitor ancho la banda se alarga y no se ve dónde acaba la imagen. La columna del texto empieza a 0,88 × el alto, que es donde empieza la banda sea cual sea el ancho. Sin cabecera, sin pie y sin barra de reproducción. Un cartel a pantalla completa con el menú del sitio encima deja de ser un cartel, y de un error se sale por la puerta que pone la propia página. En vertical no vale ese reparto: al alto de un móvil el cartel se quedaría en una franja del centro con el número cortado por los lados. Ahí se apila, y con la otra versión del archivo —solo el dibujo, sin la banda—, con el panel del color del borde del dibujo, que es exactamente el color con el que la imagen termina. Un 403 no es ninguno de los dos casos: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, desnudo a propósito, para cuando la aplicación no puede ni pintar su propia página de error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
await expect(page.getByRole('button', { name: 'Poner música' })).toBeVisible();
Las páginas de error, con su lámina y su tema Cada una lleva la suya: la del 404 y la del 500, recortadas del original —traían una banda plana pegada a la derecha, de generarlas— y pasadas a webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva dibujado: dice lo mismo con otras palabras, para que no se lea dos veces seguidas y para que la página siga teniendo titular si la imagen no carga. La música suena con un botón, no sola. Los navegadores cortan el audio que empieza sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría, es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice. Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera —SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al 500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra para esto. Un 403 no es ninguna de las dos cosas: la página existe y el sitio funciona, es que no te toca. Esos salen sin ilustración, con su mensaje, en vez de con una lámina que miente. Y se descartan los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en español es peor que no poner nada. Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni pintar su propia página de error. Va desnudo a propósito: sin hoja de estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que pidiera fuera podría estar caído también. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
expect(pedidos.some((u) => u.endsWith('.mp3'))).toBe(false);
await page.getByRole('button', { name: 'Poner música' }).click();
await expect(page.getByRole('button', { name: 'Parar' })).toBeVisible();
expect(pedidos.some((u) => u.endsWith('/errores/404.mp3'))).toBe(true);
expect(await page.locator('.musica audio').evaluate((a: HTMLAudioElement) => a.paused)).toBe(
false
);
});
test('el botón vuelve a su sitio al parar', async ({ page }) => {
await page.goto('/no-existe-esto');
await page.getByRole('button', { name: 'Poner música' }).click();
await page.getByRole('button', { name: 'Parar' }).click();
await expect(page.getByRole('button', { name: 'Poner música' })).toBeVisible();
expect(await page.locator('.musica audio').evaluate((a: HTMLAudioElement) => a.paused)).toBe(
true
);
});
});
test.describe('Carrusel de la portada', () => {
test('tiene tres láminas y solo se ve una', async ({ page }) => {
await page.goto('/');
await expect(page.locator('.lamina')).toHaveCount(3);
await expect(page.locator('.lamina.visible')).toHaveCount(1);
});
test('los puntos llevan a cada lámina', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Ir a: Aprender a escribir letras' }).click();
await expect(
page.getByRole('heading', { name: /Quieres escribir tus propias letras/ })
).toBeVisible();
await page.getByRole('button', { name: 'Ir a: Comprar canciones' }).click();
await expect(page.getByRole('heading', { name: /canciones por un café/ })).toBeVisible();
});
test('el teclado no entra en las láminas que no se ven', async ({ page }) => {
// Van con `inert`: sin eso, tabular desde la cabecera cae en botones
// invisibles y el foco se pierde.
await page.goto('/');
const ocultas = page.locator('.lamina:not(.visible)');
await expect(ocultas).toHaveCount(2);
for (const lamina of await ocultas.all()) {
await expect(lamina).toHaveJSProperty('inert', true);
}
});
test('cada lámina se lleva su propio fondo', async ({ page }) => {
/*
* El fondo a sangre de cada lámina va con `z-index: -1`. Si la lámina no
* crea su propio contexto de apilamiento, ese fondo se escapa hacia atrás
* y se pinta detrás del carrusel entero: al pasar de una lámina a otra
* asomaba la foto de la de al lado, y el fondo de la que entraba daba un
* salto al terminar el fundido.
*
* `opacity: 1` no crea contexto —por eso fallaba justo en la lámina
* visible—, así que lo que se comprueba es que haya algo que sí lo cree.
*/
await page.goto('/');
const laminas = await page.locator('.lamina').all();
expect(laminas.length).toBeGreaterThan(1);
for (const lamina of laminas) {
const propio = await lamina.evaluate((el) => {
const cs = getComputedStyle(el);
return (
cs.isolation === 'isolate' ||
Number(cs.opacity) < 1 ||
cs.transform !== 'none' ||
cs.filter !== 'none' ||
(cs.position !== 'static' && cs.zIndex !== 'auto')
);
});
expect(propio).toBe(true);
}
});
test('al pasar de lámina no asoma la foto de la otra', async ({ page }) => {
await page.goto('/');
// A mitad del fundido, que es cuando se veían las dos superpuestas.
await page.getByRole('button', { name: 'Ir a: Aprender a escribir letras' }).click();
await page.waitForTimeout(300);
// El fondo de la lámina que se va tiene que estar dentro de ella, no
// suelto por detrás: se comprueba que su opacidad efectiva la acompañe.
const asoma = await page.evaluate(() => {
const laminas = [...document.querySelectorAll('.lamina')];
return laminas.some((l) => {
const fondo = l.querySelector('.fondo');
if (!fondo) return false;
const cs = getComputedStyle(l);
// Una lámina que se está yendo no puede pintar su fondo a plena vista.
return Number(cs.opacity) < 0.5 && getComputedStyle(fondo).opacity === '1' && !esSuyo(l);
});
function esSuyo(l: Element) {
const cs = getComputedStyle(l);
return (
cs.isolation === 'isolate' ||
Number(cs.opacity) < 1 ||
cs.transform !== 'none' ||
(cs.position !== 'static' && cs.zIndex !== 'auto')
);
}
});
expect(asoma).toBe(false);
});
test('se puede parar el paso automático', async ({ page }) => {
await page.goto('/');
const parar = page.getByRole('button', { name: 'Parar el paso automático' });
await expect(parar).toBeVisible();
await parar.click();
await expect(page.getByRole('button', { name: 'Reanudar el paso automático' })).toBeVisible();
});
});
test.describe('Fondo animado de la portada', () => {
test('se pinta y arranca', async ({ page }) => {
await page.goto('/');
// La clase solo se pone cuando el programa ha compilado y está dibujando:
// que exista el canvas no significa que haya salido nada.
await expect(page.locator('canvas.activo')).toHaveCount(1);
});
test('con movimiento reducido no se monta', async ({ page }) => {
// Quien pide menos movimiento no quiere una primera pantalla que ondula.
// En tema oscuro, porque es donde hay imagen debajo que comprobar: en
// claro el fondo lo pinta el propio shader.
await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' });
await page.goto('/');
await expect(page.locator('canvas.activo')).toHaveCount(0);
// Y debajo sigue el fondo de siempre, no un hueco.
await expect(page.locator('.fondo img').first()).toBeAttached();
});
test('deja de dibujar cuando ya no se ve', async ({ page }) => {
// Un shader a pantalla completa detrás de una página por la que ya has
// bajado es batería tirada.
await page.goto('/');
await expect(page.locator('canvas.activo')).toHaveCount(1);
await page.evaluate(() => {
(window as unknown as { cuadros: number }).cuadros = 0;
const original = window.requestAnimationFrame;
window.requestAnimationFrame = (cb) =>
original(() => {
(window as unknown as { cuadros: number }).cuadros++;
cb(performance.now());
});
});
await page.evaluate(() => window.scrollTo(0, 5000));
await page.waitForTimeout(600);
const antes = await page.evaluate(() => (window as unknown as { cuadros: number }).cuadros);
await page.waitForTimeout(800);
const despues = await page.evaluate(() => (window as unknown as { cuadros: number }).cuadros);
expect(despues - antes).toBe(0);
});
test('solo lo lleva la cabecera, no las tres láminas', async ({ page }) => {
await page.goto('/');
// Tres shaders a pantalla completa pintando lo mismo a la vez no aportan
// nada y cuestan el triple.
await expect(page.locator('canvas')).toHaveCount(1);
});
});
El panel se va a su propio dominio y su propio proceso Estaba mal hecho y el síntoma era el `if` sobre `/admin` que acabó metido en el layout de la web pública: la web sabiendo que existe el panel, al revés de como debe ser. Ahora son dos aplicaciones del mismo repositorio. Lo que las separa son tres líneas de `vite.config.ts`: con `APP=panel`, las rutas salen de `src/panel/rutas`, los hooks de `src/panel/hooks.server.ts` y el resultado va a `build-panel/`. El servidor público se compila sin las rutas del panel, así que en `senzapaura.es` no es que `/admin` esté protegido: no existe, y hay una prueba que lo comprueba pidiéndolo. Comparten `src/lib` —esquema, consultas, acceso—, que es lo que evita mantener las migraciones por duplicado. Para que eso funcione, dos cosas que resolvían rutas del sitio salen de en medio: la navegación, a `$lib/navegacion`, y los tipos ambientales, que ahora son dos porque el `PageData` del sitio lleva carrito y favoritos y el del panel no. El precio de otro dominio es que el panel necesita su propia página de entrar: la cookie se emite para el host y no para el dominio padre, que es justo lo que hace que la sesión del panel no viaje en las peticiones de la web. Usa el mismo código de `$lib/server/auth`, con una diferencia: solo manda código a una cuenta que ya administre, y contesta lo mismo exista o no. Si dijera «esa cuenta no administra», el formulario sería una forma cómoda de averiguar cuál sí. Y con dominio propio, el 404 de ayer pasa a ser un 403 con el motivo escrito: esconder el panel de quien ya está en `admin.senzapaura.es` no engaña a nadie, y a esa rama solo llega alguien que administraba y ha dejado de administrar. Dos servidores en las pruebas, uno por proyecto, con su `baseURL`: una prueba del panel no puede pedir por accidente una página del sitio. Aparte, la prueba de contacto otra vez. El arreglo de ayer no valía —comprobar que el texto está escrito no impide que lo borren un instante después—, así que el layout marca con `data-hidratado` el momento en que Svelte toma el control y la prueba espera a eso. Quince pasadas con cuatro trabajadores, sin un fallo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
test.describe('El panel no está en la web pública', () => {
/*
* Vive en otro dominio y en otro proceso. El servidor público se compila sin
* sus rutas, así que esto no comprueba que estén escondidas: comprueba que
* aquí no existen. Es la diferencia entre una puerta cerrada y una pared.
*/
test('«/admin» no es una dirección de este sitio', async ({ request }) => {
const respuesta = await request.get('/admin', { maxRedirects: 0 });
expect(respuesta.status()).toBe(404);
});
test('ni robots.txt lo nombra', async ({ request }) => {
// Anunciarlo en robots sería repartir la dirección del panel a quien pase.
const robots = await request.get('/robots.txt');
expect(await robots.text()).not.toContain('/admin');
});
test('ni hay un solo enlace a él', async ({ page }) => {
for (const ruta of ['/', '/musica', '/generos', '/blog', '/cuenta']) {
await page.goto(ruta);
await expect(page.locator('a[href*="/admin"]')).toHaveCount(0);
}
});
});

Powered by TurnKey Linux.