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, type Page } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/** El tamaño del pack, tal como lo dice `$lib/tienda`. */
|
|
|
|
|
const CANCIONES_POR_PACK = 10;
|
|
|
|
|
|
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
|
|
|
/** Añade un tema desde el listado de un álbum, por su título. */
|
|
|
|
|
async function anadir(page: Page, titulo: string) {
|
Deja de repetir el fondo de la portada, las migas y las etiquetas
Tres piezas estaban copiadas en varios sitios y ahora se escriben una vez: el
fondo a sangre de la portada, las migas de pan y la pastilla de una etiqueta.
El fondo ya había divergido entre la cabecera y las láminas de llamada: el
degradado era distinto por descuido y no por decisión, que es lo que pasa con el
marcado duplicado.
Las migas pasan a ser una lista ordenada, que es su marcado correcto: un lector
de pantalla anuncia cuántos escalones hay y en cuál se va, y el último deja de
ser un enlace a donde ya estás.
Tres cosas salieron de hacerlo, ninguna cosmética:
- Con el texto como clave del bucle, un disco y un tema que se llaman igual
—«Como si nada» es las dos cosas— daban claves duplicadas y tumbaban la ficha
al hidratar. El SSR salía bien, así que solo se veía en el navegador.
- Al compartir el fondo, las tres láminas montaban el clip de vídeo. En tema
claro eso eran tres copias del mismo vídeo en la portada. Ahora lo pide solo
la cabecera.
- La lista de temas de un disco no tenía nombre, así que no se distinguía de
ninguna otra lista de la página. Ahora se llama «Temas».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
// Dentro de la lista de temas, no en cualquier lista: las migas de pan
|
|
|
|
|
// también son elementos de lista, y el título del disco se repite en ellas.
|
|
|
|
|
const fila = page
|
|
|
|
|
.getByRole('list', { name: 'Temas' })
|
|
|
|
|
.getByRole('listitem')
|
|
|
|
|
.filter({ hasText: titulo })
|
|
|
|
|
.first();
|
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 fila.getByRole('button', { name: 'Añadir' }).click();
|
|
|
|
|
await expect(fila.getByRole('button', { name: 'Quitar' })).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deja el carrito con un pack completo.
|
|
|
|
|
*
|
|
|
|
|
* Recorre discos añadiendo lo que haya a la venta hasta llegar al número, en
|
|
|
|
|
* vez de nombrar los temas uno a uno: así el tamaño del pack puede cambiar
|
|
|
|
|
* —pasó de cinco a diez— sin que haya que reescribir la lista, y añadir o
|
|
|
|
|
* quitar una canción del catálogo no rompe la prueba.
|
|
|
|
|
*/
|
|
|
|
|
async function llenarUnPack(page: Page, cuantas = CANCIONES_POR_PACK) {
|
|
|
|
|
let puestas = 0;
|
|
|
|
|
|
|
|
|
|
for (const album of ['como-si-nada', 'dos-orillas', 'ruido-blanco', 'preludios-para-piano']) {
|
|
|
|
|
if (puestas >= cuantas) break;
|
|
|
|
|
await page.goto(`/albumes/${album}`);
|
|
|
|
|
|
|
|
|
|
const botones = page
|
|
|
|
|
.getByRole('list', { name: 'Temas' })
|
|
|
|
|
.getByRole('button', { name: 'Añadir' });
|
|
|
|
|
while (puestas < cuantas && (await botones.count()) > 0) {
|
|
|
|
|
await botones.first().click();
|
|
|
|
|
// El botón cambia a «Quitar», así que la cuenta de «Añadir» baja sola.
|
|
|
|
|
await expect(botones).toHaveCount((await botones.count()) - 1 + 1 - 1);
|
|
|
|
|
puestas++;
|
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(puestas, 'no hay bastantes temas a la venta para llenar un pack').toBe(cuantas);
|
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.describe('Carrito', () => {
|
|
|
|
|
test('se puede llenar sin haber entrado', async ({ page }) => {
|
|
|
|
|
await page.goto('/albumes/como-si-nada');
|
|
|
|
|
|
|
|
|
|
const carrito = page.getByRole('link', { name: /^Carrito/ });
|
|
|
|
|
await anadir(page, 'Media verdad');
|
|
|
|
|
|
|
|
|
|
await expect(carrito).toContainText('1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el pago se bloquea mientras el pack está a medias', async ({ page }) => {
|
|
|
|
|
await page.goto('/albumes/como-si-nada');
|
|
|
|
|
await anadir(page, 'Media verdad');
|
|
|
|
|
await page.goto('/carrito');
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
page.getByText(`Faltan ${CANCIONES_POR_PACK - 1} canciones para completar el pack.`).first()
|
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
|
|
|
).toBeVisible();
|
|
|
|
|
// Sin sesión el botón de pagar ni siquiera aparece: primero hay que entrar.
|
|
|
|
|
await expect(page.getByRole('link', { name: 'Entrar para comprar' })).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('propone canciones para cerrar el pack', async ({ page }) => {
|
|
|
|
|
await page.goto('/albumes/como-si-nada');
|
|
|
|
|
await anadir(page, 'Media verdad');
|
|
|
|
|
await page.goto('/carrito');
|
|
|
|
|
|
|
|
|
|
const sugerencias = page.getByRole('heading', { level: 2, name: 'Completa el pack' });
|
|
|
|
|
await expect(sugerencias).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('un pack completo cuenta y se puede pagar', 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 llenarUnPack(page);
|
|
|
|
|
await page.goto('/carrito');
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText(`1 pack de ${CANCIONES_POR_PACK} canciones.`)).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
|
|
|
await expect(page.getByText('1,50 €').first()).toBeVisible();
|
|
|
|
|
// Al estar completo, ya no hay nada que sugerir.
|
|
|
|
|
await expect(page.getByRole('heading', { level: 2, name: 'Completa el pack' })).toHaveCount(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('se pueden quitar canciones', async ({ page }) => {
|
|
|
|
|
await page.goto('/albumes/como-si-nada');
|
|
|
|
|
await anadir(page, 'Media verdad');
|
|
|
|
|
await page.goto('/carrito');
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: 'Quitar' }).first().click();
|
|
|
|
|
await expect(page.getByText('No has añadido ninguna canción todavía.')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe('Modal de acceso', () => {
|
|
|
|
|
test('«Entrar» abre el diálogo sin salir de la página', async ({ page }) => {
|
|
|
|
|
await page.goto('/musica');
|
|
|
|
|
|
|
|
|
|
await page.locator('header').getByRole('link', { name: 'Entrar', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
const dialogo = page.locator('dialog.acceso');
|
|
|
|
|
await expect(dialogo).toBeVisible();
|
|
|
|
|
// Sigue siendo /musica: entrar no debería sacarte de donde estabas.
|
|
|
|
|
await expect(page).toHaveURL(/\/musica$/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('tiene las dos mitades: imagen a un lado y formulario al otro', async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
|
|
|
await page.goto('/musica');
|
|
|
|
|
await page.locator('header').getByRole('link', { name: 'Entrar', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
const imagen = page.locator('dialog.acceso .ilustracion');
|
|
|
|
|
const formulario = page.locator('dialog.acceso .panel');
|
|
|
|
|
await expect(imagen).toBeVisible();
|
|
|
|
|
await expect(formulario).toBeVisible();
|
|
|
|
|
|
|
|
|
|
const izquierda = (await imagen.boundingBox())!;
|
|
|
|
|
const derecha = (await formulario.boundingBox())!;
|
|
|
|
|
// La imagen entera queda a la izquierda del formulario, sin solaparse.
|
|
|
|
|
expect(izquierda.x + izquierda.width).toBeLessThanOrEqual(derecha.x + 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el foco entra en el campo del correo, no en el campo trampa', async ({ page }) => {
|
|
|
|
|
// `showModal()` enfoca lo primero que encuentra alcanzable, y lo primero
|
|
|
|
|
// del formulario es la trampa contra envíos automáticos: está fuera de
|
|
|
|
|
// pantalla pero se puede enfocar.
|
|
|
|
|
await page.goto('/musica');
|
|
|
|
|
await page.locator('header').getByRole('link', { name: 'Entrar', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.locator('dialog.acceso #email')).toBeFocused();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('se cierra con Escape y el foco vuelve a «Entrar»', async ({ page }) => {
|
|
|
|
|
await page.goto('/musica');
|
|
|
|
|
const abrir = page.locator('header').getByRole('link', { name: 'Entrar', exact: true });
|
|
|
|
|
await abrir.click();
|
|
|
|
|
await expect(page.locator('dialog.acceso')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
|
|
|
|
|
await expect(page.locator('dialog.acceso')).toBeHidden();
|
|
|
|
|
await expect(abrir).toBeFocused();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('cerrado no ocupa sitio en la página', async ({ page }) => {
|
|
|
|
|
/*
|
|
|
|
|
* El navegador oculta un `<dialog>` cerrado, pero cualquier regla nuestra
|
|
|
|
|
* de `display` le gana por venir de la hoja del autor. Con una suelta, el
|
|
|
|
|
* diálogo cerrado añadía una pantalla entera de vacío al final de todas
|
|
|
|
|
* las páginas del sitio.
|
|
|
|
|
*/
|
|
|
|
|
await page.goto('/taller');
|
|
|
|
|
|
|
|
|
|
const oculto = await page
|
|
|
|
|
.locator('dialog.acceso')
|
|
|
|
|
.evaluate((d) => getComputedStyle(d).display === 'none');
|
|
|
|
|
expect(oculto).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('desde el modal, la vuelta apunta a donde se estaba', async ({ page }) => {
|
|
|
|
|
await page.goto('/musica/baladas-romanticas');
|
|
|
|
|
await page.locator('header').getByRole('link', { name: 'Entrar', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.locator('dialog.acceso input[name="volverA"]')).toHaveValue(
|
|
|
|
|
'/musica/baladas-romanticas'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('los enlaces legales abren aparte y no se llevan el formulario', async ({
|
|
|
|
|
page,
|
|
|
|
|
context
|
|
|
|
|
}) => {
|
|
|
|
|
/*
|
|
|
|
|
* Desde un modal, un enlace normal navega por debajo: la página cambia sin
|
|
|
|
|
* que se vea y, al cerrar el diálogo, has aterrizado en otro sitio con lo
|
|
|
|
|
* escrito perdido.
|
|
|
|
|
*/
|
|
|
|
|
await page.goto('/musica');
|
|
|
|
|
await page.locator('header').getByRole('link', { name: 'Entrar', exact: true }).click();
|
|
|
|
|
await page.fill('dialog.acceso #email', 'sigue.aqui@example.com');
|
|
|
|
|
|
|
|
|
|
const [aparte] = await Promise.all([
|
|
|
|
|
context.waitForEvent('page'),
|
|
|
|
|
page.locator('dialog.acceso').getByRole('link', { name: 'condiciones de uso' }).click()
|
|
|
|
|
]);
|
|
|
|
|
await aparte.waitForLoadState();
|
|
|
|
|
|
|
|
|
|
expect(aparte.url()).toContain('/legal/condiciones-de-uso');
|
|
|
|
|
await expect(page).toHaveURL(/\/musica$/);
|
|
|
|
|
await expect(page.locator('dialog.acceso')).toBeVisible();
|
|
|
|
|
await expect(page.locator('dialog.acceso #email')).toHaveValue('sigue.aqui@example.com');
|
|
|
|
|
await aparte.close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('sin JavaScript, «Entrar» sigue llevando a la página', async ({ browser }) => {
|
|
|
|
|
const contexto = await browser.newContext({ javaScriptEnabled: false });
|
|
|
|
|
const pagina = await contexto.newPage();
|
|
|
|
|
await pagina.goto('/musica');
|
|
|
|
|
|
|
|
|
|
await pagina.locator('header').getByRole('link', { name: 'Entrar', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
await expect(pagina).toHaveURL(/\/entrar/);
|
|
|
|
|
await expect(pagina.getByLabel('Correo electrónico')).toBeVisible();
|
|
|
|
|
await contexto.close();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
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.describe('Cuenta', () => {
|
|
|
|
|
test('la cuenta exige entrar', async ({ page }) => {
|
|
|
|
|
await page.goto('/cuenta');
|
|
|
|
|
|
|
|
|
|
await expect(page).toHaveURL(/\/entrar/);
|
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Entrar o crear cuenta');
|
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('pedir un código deja el alta en marcha', async ({ page }) => {
|
|
|
|
|
/*
|
|
|
|
|
* Correo distinto en cada ejecución: los límites del formulario de acceso
|
|
|
|
|
* viven en la base de datos y sobreviven entre pasadas, así que repetir la
|
|
|
|
|
* suite con una dirección fija acabaría chocando con el tope por correo.
|
|
|
|
|
*/
|
|
|
|
|
const correo = `prueba.e2e.${Date.now()}@example.com`;
|
|
|
|
|
|
|
|
|
|
await page.goto('/entrar');
|
|
|
|
|
await page.getByLabel('Correo electrónico').fill(correo);
|
|
|
|
|
await page.getByRole('button', { name: 'Enviarme el código' }).click();
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Lo que se comprueba es que la acción se ejecuta y responde algo
|
|
|
|
|
* coherente: o pasa al paso del código, o dice por qué no. Cuál de las dos
|
|
|
|
|
* toca depende del entorno —si hay proveedor de correo, si se han pedido
|
|
|
|
|
* muchos códigos desde esta IP—, así que se acepta cualquiera en vez de
|
|
|
|
|
* enumerar mensajes, que es lo que hacía frágil este test.
|
|
|
|
|
*/
|
|
|
|
|
await expect(page.getByLabel('Código').or(page.getByRole('alert'))).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el paso del código se puede retomar sin pedir otro', async ({ page }) => {
|
|
|
|
|
// Recargar la página, o volver a ella desde el correo, no debería obligar
|
|
|
|
|
// a gastar un código nuevo.
|
|
|
|
|
await page.goto('/entrar?paso=codigo&email=prueba.e2e%40example.com');
|
|
|
|
|
|
|
|
|
|
await expect(page.getByLabel('Código')).toBeVisible();
|
|
|
|
|
await expect(page.getByText('prueba.e2e@example.com')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el navegador no deja enviar un código incompleto', async ({ page }) => {
|
|
|
|
|
await page.goto('/entrar?paso=codigo&email=prueba.e2e%40example.com');
|
|
|
|
|
|
|
|
|
|
const campo = page.getByLabel('Código');
|
|
|
|
|
await campo.fill('12');
|
|
|
|
|
await page.getByRole('button', { name: 'Entrar' }).click();
|
|
|
|
|
|
|
|
|
|
// El `pattern` del campo corta el envío sin salir del navegador, así que
|
|
|
|
|
// no llega a verse el mensaje del servidor: seguimos en el mismo paso.
|
|
|
|
|
await expect(campo).toBeVisible();
|
|
|
|
|
expect(await campo.evaluate((el) => (el as HTMLInputElement).validity.valid)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('rechaza un código que no existe', async ({ page }) => {
|
|
|
|
|
await page.goto('/entrar?paso=codigo&email=nadie.e2e%40example.com');
|
|
|
|
|
|
|
|
|
|
await page.getByLabel('Código').fill('000000');
|
|
|
|
|
await page.getByRole('button', { name: 'Entrar' }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText(/El código ha caducado o ya se ha usado/)).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('rechaza un correo mal formado', async ({ page }) => {
|
|
|
|
|
await page.goto('/entrar');
|
|
|
|
|
// `type="email"` ya filtra lo obvio; esto pasa el filtro del navegador y
|
|
|
|
|
// lo tiene que rechazar el servidor.
|
|
|
|
|
await page.getByLabel('Correo electrónico').fill('alguien@example');
|
|
|
|
|
await page.getByRole('button', { name: 'Enviarme el código' }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText('Ese correo no parece válido.')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe('Descargas', () => {
|
|
|
|
|
test('el audio completo no se sirve sin sesión', async ({ request }) => {
|
|
|
|
|
const respuesta = await request.get('/descargas/como-si-nada');
|
|
|
|
|
expect(respuesta.status()).toBe(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el fragmento de muestra sí es público', async ({ request }) => {
|
|
|
|
|
const respuesta = await request.get('/muestras/como-si-nada/01-como-si-nada.wav');
|
|
|
|
|
|
|
|
|
|
expect(respuesta.ok()).toBe(true);
|
|
|
|
|
expect(respuesta.headers()['content-type']).toContain('audio');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('no hay ruta pública al audio completo', async ({ request }) => {
|
|
|
|
|
// La carpeta /audio existía antes de que el catálogo se pusiera a la
|
|
|
|
|
// venta; que siga muerta es parte de lo que protege los masters.
|
|
|
|
|
const respuesta = await request.get('/audio/como-si-nada/01-como-si-nada.wav');
|
|
|
|
|
expect(respuesta.status()).toBe(404);
|
|
|
|
|
});
|
|
|
|
|
});
|