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'); 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/); }); test('la música se presenta por estilos', async ({ page }) => { await page.goto('/musica'); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Música'); // 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(); } }); 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'); // 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(); }); 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); // 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 }) => { const respuestas: Record = {}; page.on('response', (r) => { if (r.url().includes('/errores/')) respuestas[r.url().split('/').pop()!] = r.status(); }); await page.goto('/no-existe-esto'); // 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); await page.getByRole('link', { name: 'Volver al inicio' }).click(); await expect(page).toHaveURL('/'); }); /* * 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(); }); /* * 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'); await expect(page.getByRole('button', { name: 'Poner música' })).toBeVisible(); 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); }); }); 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); } }); });