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('buscar y cambiar de vista comparten el mismo control', async ({ page }) => { await page.goto('/blog'); await page.locator('html[data-hidratado]').waitFor(); await page.getByRole('searchbox', { name: 'Buscar una entrada' }).fill('no cabe'); await expect(page.getByRole('status')).toContainText('1 entrada'); await expect(page.getByRole('link', { name: 'Lo que no cabe en una canción' })).toBeVisible(); const tabla = page.getByRole('button', { name: 'Tabla' }); await tabla.click(); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await expect(page.getByRole('table')).toBeVisible(); }); test('las etiquetas no tienen una opción ficticia y admiten selección múltiple', async ({ page }) => { await page.goto('/blog'); await page.locator('html[data-hidratado]').waitFor(); const grupo = page.getByRole('group', { name: 'Filtrar por etiqueta' }); await expect(grupo.getByRole('button', { name: 'Todas', exact: true })).toHaveCount(0); const colores = await grupo .getByRole('button') .evaluateAll((botones) => botones.map((boton) => getComputedStyle(boton).color)); expect(new Set(colores).size).toBeGreaterThan(1); const primera = grupo.getByRole('button').nth(0); const segunda = grupo.getByRole('button').nth(1); await primera.click(); await segunda.click(); await expect(primera).toHaveAttribute('aria-pressed', 'true'); await expect(segunda).toHaveAttribute('aria-pressed', 'true'); await primera.click(); await expect(primera).toHaveAttribute('aria-pressed', 'false'); await expect(segunda).toHaveAttribute('aria-pressed', 'true'); }); test('Atrás y Adelante restauran la presentación y las etiquetas', async ({ page }) => { await page.goto('/blog'); await page.locator('html[data-hidratado]').waitFor(); const tabla = page.getByRole('button', { name: 'Tabla' }); const tarjetas = page.getByRole('button', { name: 'Tarjetas' }); const primeraEtiqueta = page .getByRole('group', { name: 'Filtrar por etiqueta' }) .getByRole('button') .first(); await tabla.click(); await expect(page).toHaveURL('/blog?presentacion=tabla'); await expect(page.getByRole('table')).toBeVisible(); await primeraEtiqueta.click(); await expect(page).toHaveURL(/\/blog\?presentacion=tabla&etiqueta=.+/); await expect(primeraEtiqueta).toHaveAttribute('aria-pressed', 'true'); await page.goBack(); await expect(page).toHaveURL('/blog?presentacion=tabla'); await expect(primeraEtiqueta).toHaveAttribute('aria-pressed', 'false'); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await page.goBack(); await expect(page).toHaveURL('/blog'); await expect(tarjetas).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('ul.entradas')).toBeVisible(); await page.goForward(); await expect(page).toHaveURL('/blog?presentacion=tabla'); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await page.goForward(); await expect(page).toHaveURL(/\/blog\?presentacion=tabla&etiqueta=.+/); await expect(primeraEtiqueta).toHaveAttribute('aria-pressed', 'true'); }); test('Atrás recupera la búsqueda después de abrir una entrada', async ({ page }) => { await page.goto('/blog'); const buscador = page.getByRole('searchbox', { name: 'Buscar una entrada' }); await buscador.fill('no cabe'); await expect(page.getByRole('status')).toContainText('1 entrada'); await page.getByRole('link', { name: 'Lo que no cabe en una canción' }).click(); await expect(page).toHaveURL('/blog/lo-que-no-cabe-en-una-cancion'); await page.goBack(); await expect(page).toHaveURL('/blog'); await expect(buscador).toHaveValue('no cabe'); await expect(page.getByRole('status')).toContainText('1 entrada'); }); 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('Me gusta', () => { test('muestra el pulgar y el contador, sin estrellas', async ({ page }) => { await page.goto('/canciones/prometiste'); const bloque = page.locator('header.ficha .me-gusta'); await expect( bloque.getByRole('link', { name: /Me gusta: \d+ likes?\. Inicia sesión/ }) ).toBeVisible(); await expect(bloque.locator('.contador')).toHaveText(/^\d+$/); await expect(page.locator('.estrellas')).toHaveCount(0); }); test('conserva la canción como destino al entrar', async ({ page }) => { await page.goto('/canciones/prometiste'); await expect(page.locator('header.ficha .me-gusta a')).toHaveAttribute( 'href', /volverA=%2Fcanciones%2Fprometiste/ ); }); test('rechaza un POST sin origen', async ({ request }) => { const r = await request.post('/canciones/prometiste?/meGusta', { form: { activo: '1' } }); expect(r.status()).toBe(403); }); test('rechaza likes sin sesión', async ({ request, baseURL }) => { const r = await request.post('/canciones/prometiste?/meGusta', { headers: { origin: baseURL ?? '', accept: 'application/json', 'x-sveltekit-action': 'true' }, form: { activo: '1' } }); expect(JSON.parse(await r.text())).toMatchObject({ type: 'failure', status: 401 }); }); }); 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'); await expect(page.locator('html')).toHaveAttribute('data-hidratado', ''); // 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'); }); });