Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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();
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
// 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.
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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);
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 }) => {
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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');
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
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 }) => {
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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');
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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 }) => {
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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
|
|
|
|
|
);
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
});
|
Carrusel en la portada y letras a dos columnas
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
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();
|
Carrusel en la portada y letras a dos columnas
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
Carrusel en la portada y letras a dos columnas
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|