import { expect, test } from '@playwright/test'; test.describe('Blog', () => { test('está en la navegación principal', async ({ page }) => { await page.goto('/'); await page .getByRole('navigation', { name: 'Principal' }) .getByRole('link', { name: 'Blog' }) .click(); await expect(page).toHaveURL('/blog'); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Blog'); }); test('las entradas van de la más reciente a la más antigua', async ({ page }) => { await page.goto('/blog'); const fechas = await page .locator('time') .evaluateAll((ts) => ts.map((t) => t.getAttribute('datetime') ?? '')); expect(fechas.length).toBeGreaterThan(1); expect(fechas).toEqual([...fechas].sort().reverse()); }); test('una entrada se lee entera y encadena con la anterior', async ({ page }) => { await page.goto('/blog'); await page.getByRole('link', { name: 'Escribir en un estilo que no es el tuyo' }).click(); await expect(page).toHaveURL('/blog/escribir-en-un-estilo-ajeno'); await expect( page.getByRole('heading', { level: 2, name: 'No es el ritmo, es el registro' }) ).toBeVisible(); await expect(page.locator('.documento .bloque--texto')).toHaveCount(1); await page.getByRole('link', { name: /Anterior/ }).click(); await expect(page.getByRole('heading', { level: 1 })).toHaveText( 'Lo que no cabe en una canción' ); }); test('una entrada no conserva enlaces a canciones eliminadas', async ({ page }) => { await page.goto('/blog/lo-que-no-cabe-en-una-cancion'); await expect(page.getByRole('heading', { name: 'De lo que habla' })).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Como si nada' })).toHaveCount(0); }); test('el detalle distingue una portada real de la reserva del listado', async ({ page }) => { await page.setViewportSize({ width: 1000, height: 900 }); await page.goto('/blog/lo-que-no-cabe-en-una-cancion'); const titulo = page.getByRole('heading', { level: 1 }); await expect(titulo).toHaveText('Lo que no cabe en una canción'); await expect(page.locator('article img.portada-entrada')).toHaveCount(0); await expect(page.locator('article')).not.toContainText('Sin portada'); await expect(page.getByLabel('Datos del artículo')).toContainText('Publicado'); await expect(page.getByLabel('Datos del artículo')).toContainText('Lectura'); const indice = page.getByRole('complementary', { name: 'Más en el cuaderno' }); await expect(indice).toBeVisible(); await expect( indice.getByRole('link', { name: /Lo que no cabe en una canción/ }) ).toHaveAttribute('aria-current', 'page'); expect(await indice.getByRole('link').count()).toBeGreaterThan(1); const estructura = await page.evaluate(() => { const cabecera = document.querySelector('.intro')?.getBoundingClientRect(); const lateral = document.querySelector('.indice-blog')?.getBoundingClientRect(); const interior = document.querySelector('.indice-interior'); return { diferenciaVertical: Math.abs((cabecera?.top ?? 0) - (lateral?.top ?? 0)), posicion: interior ? getComputedStyle(interior).position : '' }; }); expect(estructura.diferenciaVertical).toBeLessThan(1); expect(estructura.posicion).toBe('sticky'); const medidas = await titulo.evaluate((elemento) => { const caja = elemento.getBoundingClientRect(); return { ancho: caja.width, alto: caja.height, tamano: parseFloat(getComputedStyle(elemento).fontSize) }; }); expect(medidas.ancho).toBeGreaterThan(540); expect(medidas.ancho).toBeLessThan(600); expect(medidas.alto).toBeGreaterThan(100); expect(medidas.alto).toBeLessThan(125); expect(medidas.tamano).toBeGreaterThanOrEqual(56); const cuerpo = page.locator('.contenido .prosa').first(); await expect(cuerpo).toBeVisible(); const lectura = await cuerpo.evaluate((elemento) => { const caja = elemento.getBoundingClientRect(); return { ancho: caja.width, tamano: parseFloat(getComputedStyle(elemento).fontSize) }; }); expect(lectura.ancho).toBeGreaterThanOrEqual(570); expect(lectura.ancho).toBeLessThanOrEqual(680); expect(lectura.tamano).toBeGreaterThanOrEqual(18); const primerParrafo = cuerpo.locator('p').first(); const capitular = await primerParrafo.evaluate((elemento) => { const estilo = getComputedStyle(elemento, '::first-letter'); return { tamano: parseFloat(estilo.fontSize), flotante: estilo.float }; }); expect(capitular.tamano).toBeGreaterThan(70); expect(capitular.flotante).toBe('left'); }); test('las migas conservan el eje izquierdo de la cabecera en móvil', async ({ page }) => { await page.setViewportSize({ width: 336, height: 800 }); await page.goto('/blog/lo-que-no-cabe-en-una-cancion'); const alineacion = await page.evaluate(() => { const marca = document.querySelector('header .marca')!.getBoundingClientRect(); const ruta = document .querySelector("nav[aria-label='Ruta']")! .getBoundingClientRect(); return { diferenciaIzquierda: Math.abs(marca.left - ruta.left), espacioDerecho: innerWidth - ruta.right, ancho: ruta.width }; }); expect(alineacion.diferenciaIzquierda).toBeLessThanOrEqual(1); expect(alineacion.espacioDerecho).toBeGreaterThanOrEqual(16); expect(alineacion.ancho).toBeGreaterThanOrEqual(300); }); test('las etiquetas de una entrada filtran la música relacionada', async ({ page }) => { await page.goto('/blog/lo-que-no-cabe-en-una-cancion'); await page.getByRole('link', { name: 'Desamor', exact: true }).click(); await expect(page).toHaveURL('/musica?etiqueta=desamor'); }); test('una entrada que no existe da 404', async ({ page }) => { const respuesta = await page.goto('/blog/no-existe'); expect(respuesta?.status()).toBe(404); }); }); test.describe('Valoraciones', () => { test('quien no ha entrado ve solo las estrellas y la puntuación', async ({ page }) => { await page.goto('/canciones/prometiste'); const bloque = page.getByRole('region', { name: 'Valoración de la canción' }); await expect(bloque).toBeVisible(); await expect( bloque.getByRole('link', { name: 'Inicia sesión para valorar esta canción' }) ).toBeVisible(); await expect(bloque.locator('.estrellas span')).toHaveCount(5); await expect(bloque.locator('.media')).toBeVisible(); await expect(bloque).not.toContainText(/valoración|de 5|votos?|quitar mi voto/i); // Sin sesión no se puede votar: no hay estrellas que pulsar. await expect(bloque.getByRole('radio')).toHaveCount(0); }); test('las estrellas vuelven al tema tras entrar', async ({ page }) => { await page.goto('/canciones/prometiste'); await expect( page .getByRole('region', { name: 'Valoración de la canción' }) .getByRole('link', { name: 'Inicia sesión para valorar esta canción' }) ).toHaveAttribute('href', /volverA=%2Fcanciones%2Fprometiste/); }); test('un POST sin origen lo para el CSRF, antes de llegar a la acción', async ({ request }) => { const respuesta = await request.post('/canciones/prometiste?/valorar', { form: { puntuacion: '5' } }); expect(respuesta.status()).toBe(403); }); test('sin sesión, la acción de valorar se rechaza', async ({ request, baseURL }) => { // Con el origen correcto la petición llega a la acción, así que lo que se // comprueba aquí es su guardia y no la del framework. La interfaz no // enseña estrellas sin sesión, pero el formulario se puede enviar a mano. // // La respuesta es el resultado de la acción en JSON, no la página: el // código de `fail` viaja dentro, y el de la respuesta HTTP es 200. const respuesta = await request.post('/canciones/prometiste?/valorar', { headers: { origin: baseURL ?? '' }, form: { puntuacion: '5' } }); const resultado = JSON.parse(await respuesta.text()); expect(resultado.type).toBe('failure'); expect(resultado.status).toBe(401); }); test('una puntuación fuera de rango tampoco pasa', async ({ request, baseURL }) => { // Sin sesión gana la guardia de la cuenta; que el rango se comprueba está // cubierto aparte, en las pruebas de `aPuntuacion`. const respuesta = await request.post('/canciones/prometiste?/valorar', { headers: { origin: baseURL ?? '' }, form: { puntuacion: '9' } }); expect(JSON.parse(await respuesta.text()).type).toBe('failure'); }); }); test.describe('Favoritos', () => { test('quien no ha entrado ve el corazón y abre el acceso en la ficha', async ({ page }) => { await page.goto('/canciones/prometiste'); // No se esconde: la interfaz no cambia de forma según quién mira. const enlace = page.locator('header.ficha').getByRole('link', { name: 'Entra con tu cuenta para añadir a favoritos' }); await expect(enlace).toBeVisible(); await expect(enlace).toHaveAttribute('href', /volverA=%2Fcanciones%2Fprometiste/); await enlace.click(); await expect(page.getByRole('dialog', { name: 'Entrar o crear cuenta' })).toBeVisible(); await expect(page).toHaveURL(/\/canciones\/prometiste$/); }); test('la lista invita a entrar si no hay sesión', async ({ page }) => { await page.goto('/favoritos'); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Favoritos'); await expect(page.getByRole('link', { name: 'Entra con tu cuenta' })).toBeVisible(); }); test('sin sesión, marcar un favorito se rechaza', async ({ request, baseURL }) => { const respuesta = await request.post('/favoritos?/marcar', { headers: { origin: baseURL ?? '' }, form: { cancion: 'prometiste', volverA: '/favoritos' } }); const resultado = JSON.parse(await respuesta.text()); expect(resultado.type).toBe('failure'); expect(resultado.status).toBe(401); }); test('un tema inventado no se puede guardar', async ({ request, baseURL }) => { const respuesta = await request.post('/favoritos?/marcar', { headers: { origin: baseURL ?? '' }, form: { cancion: 'no-existe', volverA: '/favoritos' } }); expect(JSON.parse(await respuesta.text()).type).toBe('failure'); }); });