import { expect, test } from '@playwright/test'; test.describe('Página de música', () => { test('los estilos están siempre a la vista, con su recuento', async ({ page }) => { await page.goto('/musica'); const lateral = page.getByRole('navigation', { name: 'Estilos' }); await expect(lateral).toBeVisible(); await expect(lateral.getByRole('link', { name: /Baladas románticas/ })).toBeVisible(); await expect(lateral.getByRole('link', { name: /Clásica/ })).toBeVisible(); }); test('las etiquetas se despliegan bajo el buscador con su propio toggle', async ({ page }) => { await page.setViewportSize({ width: 1000, height: 800 }); await page.goto('/musica?vista=canciones&idioma=es'); await page.waitForSelector('html[data-hidratado]'); const lateral = page.locator('aside.lateral'); const nube = page.getByRole('navigation', { name: 'Filtrar por etiqueta' }); const boton = page.getByRole('button', { name: 'Etiquetas', exact: true }); await expect(lateral.getByRole('navigation', { name: 'Filtrar por etiqueta' })).toHaveCount(0); await expect(nube).toBeHidden(); await expect(boton).toHaveAttribute('aria-pressed', 'false'); await expect(boton).toHaveAttribute('aria-expanded', 'false'); await boton.click(); await expect(nube).toBeVisible(); await expect(boton).toHaveAttribute('aria-pressed', 'true'); await expect(boton).toHaveAttribute('aria-expanded', 'true'); await expect(boton).toHaveClass(/oro/); const medidas = await page.evaluate(() => { const aside = document.querySelector('aside.lateral')!; const barra = document.querySelector('.barra')!; const etiquetas = document.querySelector('.filtros-etiquetas')!; const resultado = document.querySelector('.resultado')!; return { desbordaAside: aside.scrollHeight > aside.clientHeight, debajoDeBarra: Boolean( barra.compareDocumentPosition(etiquetas) & Node.DOCUMENT_POSITION_FOLLOWING ), antesDelResultado: Boolean( etiquetas.compareDocumentPosition(resultado) & Node.DOCUMENT_POSITION_FOLLOWING ), tieneRotuloVisible: etiquetas.querySelector('p') !== null }; }); expect(medidas.desbordaAside).toBe(false); expect(medidas.debajoDeBarra).toBe(true); expect(medidas.antesDelResultado).toBe(true); expect(medidas.tieneRotuloVisible).toBe(false); await boton.click(); await expect(nube).toBeHidden(); await expect(boton).toHaveAttribute('aria-pressed', 'false'); }); test('ofrece vistas explícitas para todos los álbumes y todas las canciones', async ({ page }) => { await page.goto('/musica'); const catalogo = page.getByRole('navigation', { name: 'Ver catálogo' }); await expect(catalogo.getByRole('link', { name: 'Todo el catálogo' })).toBeVisible(); await expect(catalogo.getByRole('link', { name: 'Todos los álbumes' })).toBeVisible(); const todas = catalogo.getByRole('link', { name: 'Todas las canciones' }); await todas.click(); await expect(page).toHaveURL('/musica?vista=canciones'); await expect(todas).toHaveAttribute('aria-current', 'page'); await expect(page.getByRole('heading', { level: 2, name: 'Canciones' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Álbumes' })).toHaveCount(0); await expect(page.getByRole('status')).toContainText('11 canciones'); await page .getByRole('navigation', { name: 'Estilos' }) .getByRole('link', { name: /Baladas románticas/ }) .click(); await expect(page).toHaveURL('/musica?vista=canciones&estilo=baladas-romanticas'); await expect(page.getByRole('heading', { name: 'Álbumes' })).toHaveCount(0); }); test('Atrás y Adelante restauran la presentación y los filtros del catálogo', async ({ page }) => { await page.goto('/musica?vista=albumes'); await page.locator('html[data-hidratado]').waitFor(); const tabla = page.getByRole('button', { name: 'Tabla' }); const tarjetas = page.getByRole('button', { name: 'Tarjetas' }); await tabla.click(); await expect(page).toHaveURL('/musica?vista=albumes&presentacion=tabla'); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('section.albumes table')).toBeVisible(); await page .getByRole('navigation', { name: 'Estilos' }) .getByRole('link', { name: /Baladas románticas/ }) .click(); await expect(page).toHaveURL( '/musica?vista=albumes&presentacion=tabla&estilo=baladas-romanticas' ); await expect(page.getByRole('status')).toContainText('estilo «Baladas románticas»'); await page.goBack(); await expect(page).toHaveURL('/musica?vista=albumes&presentacion=tabla'); await expect(page.getByRole('status')).not.toContainText('estilo «Baladas románticas»'); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await page.goBack(); await expect(page).toHaveURL('/musica?vista=albumes'); await expect(tarjetas).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('section.albumes .tarjeta')).not.toHaveCount(0); await page.goForward(); await expect(page).toHaveURL('/musica?vista=albumes&presentacion=tabla'); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await page.goForward(); await expect(page).toHaveURL( '/musica?vista=albumes&presentacion=tabla&estilo=baladas-romanticas' ); await expect(page.getByRole('status')).toContainText('estilo «Baladas románticas»'); }); test('Atrás recupera la búsqueda después de abrir una canción', async ({ page }) => { await page.goto('/musica'); const buscador = page.getByRole('searchbox', { name: 'Buscar en el catálogo' }); await buscador.fill('desamor'); await expect(page.getByRole('status')).toContainText('búsqueda «desamor»'); await page.getByRole('heading', { name: 'Prometiste' }).getByRole('link').click(); await expect(page).toHaveURL('/canciones/prometiste'); await page.goBack(); await expect(page).toHaveURL('/musica'); await expect(buscador).toHaveValue('desamor'); await expect(page.getByRole('status')).toContainText('búsqueda «desamor»'); }); test('las vistas de todos los álbumes y canciones limpian los filtros activos', async ({ page }) => { await page.goto('/musica?vista=albumes&estilo=clasica'); const catalogo = page.getByRole('navigation', { name: 'Ver catálogo' }); await catalogo.getByRole('link', { name: 'Todos los álbumes' }).click(); await expect(page).toHaveURL('/musica?vista=albumes'); expect(await page.locator('section.albumes .tarjeta').count()).toBeGreaterThan(0); await page.goto('/musica?vista=canciones&idioma=ga&etiqueta=rabia'); await catalogo.getByRole('link', { name: 'Todas las canciones' }).click(); await expect(page).toHaveURL('/musica?vista=canciones'); expect(await page.locator('section.canciones .tarjeta').count()).toBeGreaterThan(0); await page.setViewportSize({ width: 800, height: 900 }); await page.goto('/musica?vista=albumes&estilo=clasica'); await page.locator('html[data-hidratado]').waitFor(); await page.getByRole('button', { name: /Filtros/ }).click(); const dialogo = page.getByRole('dialog', { name: 'Filtrar música' }); await expect(dialogo).toBeVisible(); await dialogo.getByRole('link', { name: 'Todos los álbumes' }).click(); await expect(page).toHaveURL('/musica?vista=albumes'); await expect(dialogo).not.toBeVisible(); await expect(page.getByRole('button', { name: 'Filtros', exact: true })).toBeVisible(); await expect(page.locator('.contador-filtros')).toHaveCount(0); expect(await page.locator('section.albumes .tarjeta').count()).toBeGreaterThan(0); }); test('buscar y elegir vista son una sola pieza', async ({ page }) => { /* * Eran dos cajas con un hueco entre ellas y parecían dos controles sin * relación. El borde es de la barra: dentro, el campo y los botones no * tienen canto propio y llegan al mismo alto. */ await page.setViewportSize({ width: 1100, height: 800 }); await page.goto('/musica'); const medidas = await page.evaluate(() => { const campo = document.querySelector('input[type="search"]')!; const activo = document.querySelector('.vista.oro')!; const barra = document.querySelector('.barra')!.getBoundingClientRect(); return { bordeDelCampo: getComputedStyle(campo).borderTopWidth, altoCampo: Math.round(campo.getBoundingClientRect().height), altoBoton: Math.round(activo.getBoundingClientRect().height), altoBarra: Math.round(barra.height), dentro: activo.getBoundingClientRect().right <= barra.right + 1 }; }); expect(medidas.bordeDelCampo).toBe('0px'); expect(medidas.altoCampo).toBe(medidas.altoBoton); // Sin hueco: la barra mide lo que miden sus piezas, más su propio canto. expect(medidas.altoBarra).toBeLessThanOrEqual(medidas.altoCampo + 2); expect(medidas.dentro).toBe(true); }); test('la vista activa lleva el oro de los botones, no un color plano', async ({ page }) => { await page.goto('/musica'); await page.waitForSelector('html[data-hidratado]'); const oro = page.locator('.vista.oro'); await expect(oro).toHaveText('Tarjetas'); // El degradado es lo que se pedía: al lado hay un botón dorado de verdad // y se notaba que el conmutador no era el mismo oro. const pintura = await oro.evaluate((e) => { const s = getComputedStyle(e); return { imagen: s.backgroundImage, filo: s.boxShadow, color: s.color }; }); expect(pintura.imagen).toContain('linear-gradient'); expect(pintura.filo).toContain('inset'); // Y la letra es la del botón dorado, no el gris del inactivo: el `.vista` // de la página le ganaba por especificidad al `.oro` general. const inactivo = await page .getByRole('button', { name: 'Tabla' }) .evaluate((e) => getComputedStyle(e).color); expect(pintura.color).not.toBe(inactivo); // Y cambia con la vista. const tabla = page.getByRole('button', { name: 'Tabla' }); await tabla.click(); await expect(tabla).toHaveAttribute('aria-pressed', 'true'); await expect(tabla).toHaveClass(/oro/); }); test('en un móvil la barra se apila sin estirar el campo', async ({ page }) => { // En columna, `flex-basis` mide el alto: las doce rem que dan ancho al // campo lo convertían en un cajón de doscientos píxeles. await page.setViewportSize({ width: 360, height: 800 }); await page.goto('/musica'); const alto = await page .locator('input[type="search"]') .evaluate((e) => Math.round(e.getBoundingClientRect().height)); expect(alto).toBeLessThan(60); }); test('la tabla ordena por la columna que se pulsa', async ({ page }) => { await page.goto('/musica'); await page.waitForSelector('html[data-hidratado]'); await page.getByRole('button', { name: 'Tabla' }).click(); const tabla = page.locator('section.canciones table'); const columna = tabla.getByRole('columnheader', { name: 'Duración' }); await expect(columna).toHaveAttribute('aria-sort', 'none'); // Las cifras empiezan de mayor a menor: lo más largo arriba. await columna.getByRole('button').click(); await expect(columna).toHaveAttribute('aria-sort', 'descending'); // Por su clase y no por su posición: la tabla lleva portada, y el número // de columna cambia con lo que se enseñe en cada página. const duraciones = await tabla.locator('tbody td.duracion').allInnerTexts(); const enSegundos = duraciones.map((d) => { const [m, s] = d.trim().split(':').map(Number); return m * 60 + s; }); expect(enSegundos).toEqual([...enSegundos].sort((a, b) => b - a)); // Y volver a pulsarla le da la vuelta. await columna.getByRole('button').click(); await expect(columna).toHaveAttribute('aria-sort', 'ascending'); }); test('Atrás conserva el orden elegido en la tabla', async ({ page }) => { await page.goto('/musica?vista=canciones&presentacion=tabla'); await page.locator('html[data-hidratado]').waitFor(); const columna = page.getByRole('columnheader', { name: 'Duración' }); await columna.getByRole('button').click(); await expect(columna).toHaveAttribute('aria-sort', 'descending'); await page.locator('section.canciones tbody th a').first().click(); await expect(page).toHaveURL(/\/canciones\//); await page.goBack(); await expect(page).toHaveURL('/musica?vista=canciones&presentacion=tabla'); await expect(page.getByRole('columnheader', { name: 'Duración' })).toHaveAttribute( 'aria-sort', 'descending' ); }); test('la búsqueda encuentra por etiqueta, no solo por título', async ({ page }) => { await page.goto('/musica'); await page.getByLabel('Buscar en el catálogo').fill('desamor'); // Ningún tema se llama «desamor»: si aparecen, es por sus etiquetas. const estado = page.getByRole('status'); await expect(estado).toContainText('búsqueda «desamor»'); await expect(page.getByRole('heading', { name: 'Prometiste' })).toBeVisible(); }); test('una búsqueda sin resultados lo dice', async ({ page }) => { await page.goto('/musica'); await page.getByLabel('Buscar en el catálogo').fill('zzzzz'); await expect(page.getByRole('status')).toContainText('0 canciones'); await expect(page.getByText('No hay canciones que coincidan.')).toBeVisible(); }); test('en la página de un estilo se ven todos sus temas', async ({ page }) => { // No solo los destacados: si no, un tema recién añadido no se ve. await page.goto('/musica/baladas-romanticas'); await expect(page.getByRole('heading', { name: 'Canciones', exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Dorina' })).toBeVisible(); }); test('los álbumes tienen la misma anchura que las canciones y la portada ocupa la tarjeta', async ({ page }) => { await page.setViewportSize({ width: 1000, height: 1000 }); await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('/musica'); const albumes = page.locator('section.albumes'); await expect(albumes.getByRole('heading', { level: 2, name: 'Álbumes' })).toBeVisible(); await expect(albumes.getByText('Álbum', { exact: true })).toBeVisible(); await expect(albumes.getByText('Colección', { exact: true })).toBeVisible(); const medidas = await page.evaluate(() => { const rejilla = document.querySelector('section.albumes .rejilla-albumes')!; const album = rejilla.querySelector('.tarjeta')!; const portada = album.querySelector('.tarjeta__medio')!; const titulo = album.querySelector('.titulo')!; const meta = album.querySelector('.meta')!; const cancion = document.querySelector('section.canciones .tarjeta')!; const tipo = album.querySelector('.tipo')!; const reproducirAlbum = album.querySelector('.tarjeta__reproduccion button')!; return { columnas: getComputedStyle(rejilla).gridTemplateColumns.split(' ').length, album: album.getBoundingClientRect(), portada: portada.getBoundingClientRect(), cancion: cancion.getBoundingClientRect(), titulo: Number.parseFloat(getComputedStyle(titulo).fontSize), meta: Number.parseFloat(getComputedStyle(meta).fontSize), playAlbumEnPortada: Boolean(portada.querySelector('button')), playAlbumEnCuerpo: Boolean( album.querySelector('.tarjeta__cuerpo .tarjeta__reproduccion button') ), playCancionEnPortada: Boolean(cancion.querySelector('.tarjeta__medio button')), playCancionEnCuerpo: Boolean( cancion.querySelector('.tarjeta__cuerpo .tarjeta__reproduccion button') ), alineacionPlay: Math.abs( tipo.getBoundingClientRect().top + tipo.getBoundingClientRect().height / 2 - (reproducirAlbum.getBoundingClientRect().top + reproducirAlbum.getBoundingClientRect().height / 2) ) < 2 }; }); expect(medidas.columnas).toBe(2); expect(Math.abs(medidas.album.width - medidas.cancion.width)).toBeLessThan(2); expect(medidas.portada.width).toBeGreaterThan(medidas.album.width * 0.98); expect(Math.abs(medidas.portada.width - medidas.portada.height)).toBeLessThan(2); expect(medidas.titulo).toBeGreaterThanOrEqual(24); expect(medidas.meta).toBeGreaterThanOrEqual(14); expect(medidas.playAlbumEnPortada).toBe(false); expect(medidas.playAlbumEnCuerpo).toBe(true); expect(medidas.playCancionEnPortada).toBe(false); expect(medidas.playCancionEnCuerpo).toBe(true); expect(medidas.alineacionPlay).toBe(true); }); test('la tarjeta responde al puntero y la portada permanece fija', async ({ page }) => { await page.goto('/musica'); const tarjeta = page.locator('section.albumes .tarjeta').first(); await expect(tarjeta).toBeVisible(); await tarjeta.scrollIntoViewIfNeeded(); // Deja terminar la entrada escalonada antes de medir la posición real. await page.waitForTimeout(1100); const imagen = tarjeta.locator('.tarjeta__medio img'); const posicionInicial = await imagen.boundingBox(); expect(posicionInicial).not.toBeNull(); await tarjeta.hover({ position: { x: 24, y: 24 } }); await page.waitForTimeout(500); const posicionHoverInicial = await imagen.boundingBox(); const cajaTarjeta = await tarjeta.boundingBox(); expect(cajaTarjeta).not.toBeNull(); await page.mouse.move( cajaTarjeta!.x + cajaTarjeta!.width * 0.75, cajaTarjeta!.y + cajaTarjeta!.height * 0.72 ); await page.waitForTimeout(100); const posicionHoverFinal = await imagen.boundingBox(); const sombra = await tarjeta.evaluate((nodo) => getComputedStyle(nodo).boxShadow); const destello = await tarjeta.evaluate( (nodo) => getComputedStyle(nodo, '::after').animationName ); const portada = await imagen.evaluate((nodo) => ({ transformacion: getComputedStyle(nodo).transform, filtro: getComputedStyle(nodo).filter, transicion: getComputedStyle(nodo).transitionDuration })); expect(sombra).not.toBe('none'); expect(destello).toBe('tarjeta-destello'); for (const posicion of [posicionHoverInicial, posicionHoverFinal]) { expect(posicion).not.toBeNull(); expect(posicion!.x).toBeCloseTo(posicionInicial!.x, 1); expect(posicion!.y).toBeCloseTo(posicionInicial!.y, 1); expect(posicion!.width).toBeCloseTo(posicionInicial!.width, 1); expect(posicion!.height).toBeCloseTo(posicionInicial!.height, 1); } expect(portada).toEqual({ transformacion: 'none', filtro: 'none', transicion: '0s' }); }); test('los rótulos de sección respiran antes de sus títulos', async ({ page }) => { await page.goto('/musica'); for (const seccion of ['.albumes', '.canciones']) { const distancia = await page.locator(seccion).evaluate((elemento) => { const rotulo = elemento.querySelector('.eyebrow')!.getBoundingClientRect(); const titulo = elemento.querySelector('h2')!.getBoundingClientRect(); return titulo.top - rotulo.bottom; }); expect(distancia).toBeGreaterThanOrEqual(8); } }); test('el catálogo general muestra todos los álbumes publicados antes de filtrar', async ({ page, request }) => { const sitemap = await (await request.get('/sitemap.xml')).text(); const esperados = [ ...new Set([...sitemap.matchAll(/[^<]*\/albumes\/([^<]+)<\/loc>/g)].map((m) => m[1])) ].sort(); await page.goto('/musica'); const catalogo = page.locator('section.albumes'); await expect(catalogo.getByRole('heading', { level: 2, name: 'Álbumes' })).toBeVisible(); const visibles = ( await catalogo .locator('a.tarjeta__enlace') .evaluateAll((enlaces) => enlaces.map((enlace) => new URL((enlace as HTMLAnchorElement).href).pathname.split('/').pop()! ) ) ).sort(); expect(visibles).toEqual(esperados); }); test('el aside filtra a la vez los álbumes y las canciones por estilo', async ({ page }) => { await page.goto('/musica'); const estilos = page.getByRole('navigation', { name: 'Estilos' }); await estilos.getByRole('link', { name: /Baladas románticas/ }).click(); await expect(page).toHaveURL('/musica?estilo=baladas-romanticas'); await expect(estilos.getByRole('link', { name: /Baladas románticas/ })).toHaveAttribute( 'aria-current', 'page' ); await expect(page.getByRole('status')).toContainText('estilo «Baladas románticas»'); await expect(page.locator('section.albumes').getByText('Cupido sin flechas')).toBeVisible(); await expect(page.locator('section.canciones').getByText('Dorina')).toBeVisible(); await estilos.getByRole('link', { name: /Baladas románticas/ }).click(); await expect(page).toHaveURL('/musica'); await expect(estilos.locator('[aria-current]')).toHaveCount(0); await expect(page.getByRole('status')).not.toContainText('estilo «Baladas románticas»'); }); test('el aside ofrece los idiomas del catálogo y filtra canciones y lanzamientos', async ({ page }) => { await page.goto('/musica'); const idiomas = page.getByRole('navigation', { name: 'Idiomas' }); await expect(idiomas.getByRole('link', { name: 'Todos los idiomas' })).toHaveCount(0); await expect(idiomas.locator('[aria-current]')).toHaveCount(0); for (const idioma of [ 'Español', 'Portugués', 'Inglés', 'Francés', 'Italiano', 'Armenio', 'Búlgaro', 'Latín', 'Irlandés' ]) await expect(idiomas.getByText(idioma, { exact: true })).toBeVisible(); const frances = idiomas.getByRole('link', { name: /Francés/ }); await frances.click(); await expect(page).toHaveURL('/musica?idioma=fr'); await expect(frances).toHaveAttribute('aria-current', 'page'); await page.evaluate(() => { document.documentElement.dataset.tema = 'claro'; }); expect( await frances.evaluate((enlace) => ({ bordeIzquierdo: getComputedStyle(enlace).borderLeftWidth, peso: getComputedStyle(enlace).fontWeight })) ).toEqual({ bordeIzquierdo: '0px', peso: '600' }); await expect(page.getByRole('status')).toContainText('idioma «Francés»'); await expect(page.getByRole('heading', { name: /Ton silence/ })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Dorina' })).toHaveCount(0); await expect(page.locator('section.albumes').getByText('Cupido sin flechas')).toBeVisible(); await frances.click(); await expect(page).toHaveURL('/musica'); await expect(idiomas.locator('[aria-current]')).toHaveCount(0); await frances.click(); await expect(page).toHaveURL('/musica?idioma=fr'); await page .getByRole('navigation', { name: 'Estilos' }) .getByRole('link', { name: /Baladas románticas/ }) .click(); await expect(page).toHaveURL('/musica?estilo=baladas-romanticas&idioma=fr'); }); test('la ficha publica el idioma real de la letra', async ({ page }) => { await page.goto('/canciones/ton-silence-me-libere'); const ficha = page.getByRole('heading', { name: 'Ficha' }).locator('..'); await expect(ficha.getByText('Idioma', { exact: true })).toBeVisible(); await expect(ficha.getByText(/francés/i)).toBeVisible(); await expect(ficha.getByText('Voces', { exact: true })).toBeVisible(); await expect(ficha.getByText('Dueto mixto', { exact: true })).toBeVisible(); await expect(ficha.getByText('Acceso de escucha', { exact: true })).toBeVisible(); await expect(ficha.getByText('Público', { exact: true })).toBeVisible(); await expect(page.locator('.texto[lang="fr"]')).toBeVisible(); }); test('los likes y las acciones personales están junto al título', async ({ page }) => { await page.goto('/canciones/ton-silence-me-libere'); const cabecera = page.locator('header.ficha'); const likes = cabecera.locator('.me-gusta'); await expect(likes.getByRole('link', { name: /Me gusta:/ })).toBeVisible(); await expect(likes.locator('.contador')).toHaveText(/^\d+$/); await expect(cabecera.locator('.estrellas')).toHaveCount(0); const familia = await likes .locator('.contador') .evaluate((e) => getComputedStyle(e).fontFamily); expect(familia).toContain('Archivo'); const favorito = cabecera.getByRole('link', { name: /añadir a favoritos/i }); const lista = cabecera.getByRole('link', { name: /añadir a una lista de reproducción/i }); await expect(favorito).toBeVisible(); await expect(lista).toBeVisible(); await expect(favorito.locator('svg')).toHaveAttribute('width', '28'); await expect(lista.locator('svg')).toHaveAttribute('width', '28'); }); test('la letra y las tarjetas de la ficha comparten retícula y tipografía de interfaz', async ({ page }) => { await page.goto('/canciones/ton-silence-me-libere'); const filaPestanas = page.getByRole('tablist', { name: 'Contenido del tema' }); const ficha = page.locator('aside.lateral'); const tituloFicha = ficha.getByRole('heading', { name: 'Ficha' }); const cajaPestanas = await filaPestanas.boundingBox(); const cajaFicha = await tituloFicha.boundingBox(); expect(cajaPestanas).not.toBeNull(); expect(cajaFicha).not.toBeNull(); expect( Math.abs( (cajaPestanas?.y ?? 0) + (cajaPestanas?.height ?? 0) - ((cajaFicha?.y ?? 0) + (cajaFicha?.height ?? 0)) ) ).toBeLessThan(2); const familiaPestana = await filaPestanas .getByRole('tab', { name: 'Letra' }) .evaluate((elemento) => getComputedStyle(elemento).fontFamily); const familiaFicha = await tituloFicha.evaluate( (elemento) => getComputedStyle(elemento).fontFamily ); const familiaLetra = await page .locator('.texto.versos') .evaluate((elemento) => getComputedStyle(elemento).fontFamily); expect(familiaFicha).toBe(familiaPestana); expect(familiaLetra).not.toBe(familiaFicha); await expect(filaPestanas.getByRole('tab', { name: 'Letra' })).toHaveCSS('font-size', '15px'); await expect(tituloFicha).toHaveCSS('font-size', '15px'); await expect(ficha.locator('dl').first()).toHaveCSS('font-size', '15px'); const tarjetas = ficha.locator('section.bloque'); expect(await tarjetas.count()).toBeGreaterThanOrEqual(2); for (const tarjeta of await tarjetas.all()) { await expect(tarjeta).toHaveCSS('border-top-style', 'solid'); await expect(tarjeta).toHaveCSS('overflow', 'hidden'); } }); }); test.describe('Etiquetas', () => { test('la ficha de un tema lleva su etiqueta al filtro de música', async ({ page }) => { await page.goto('/canciones/ton-silence-me-libere'); await page.getByRole('link', { name: 'Rabia', exact: true }).click(); await expect(page).toHaveURL('/musica?etiqueta=rabia'); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Música'); }); test('las etiquetas del desplegable filtran temas de discos distintos', async ({ page }) => { await page.goto('/musica'); await page.waitForSelector('html[data-hidratado]'); const nube = page.getByRole('navigation', { name: 'Filtrar por etiqueta' }); await page.getByRole('button', { name: 'Etiquetas', exact: true }).click(); await expect(nube).toBeVisible(); await expect(page.locator('aside.lateral').getByRole('link', { name: 'Rabia' })).toHaveCount(0); await nube.getByRole('link', { name: /Rabia, pulsar para añadir/ }).click(); await expect(page).toHaveURL('/musica?etiqueta=rabia'); await expect(nube.getByRole('link', { name: /Rabia, seleccionada/ })).toHaveClass(/activa/); await expect(nube.getByRole('link', { name: 'Todas', exact: true })).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Ton silence me libère' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Tus malas maneras' })).toBeVisible(); await nube.getByRole('link', { name: /Rabia, seleccionada/ }).click(); await expect(page).toHaveURL('/musica'); await expect(nube).toBeVisible(); await expect(nube.getByRole('link', { name: /Rabia, pulsar para añadir/ })).not.toHaveClass( /activa/ ); }); test('las etiquetas admiten selección múltiple y se eliminan de forma independiente', async ({ page }) => { await page.goto('/musica'); await page.waitForSelector('html[data-hidratado]'); await page.getByRole('button', { name: 'Etiquetas', exact: true }).click(); const nube = page.getByRole('navigation', { name: 'Filtrar por etiqueta' }); await nube.getByRole('link', { name: /Rabia, pulsar para añadir/ }).click(); await nube.getByRole('link', { name: /Desamor, pulsar para añadir/ }).click(); await expect(page).toHaveURL('/musica?etiqueta=rabia&etiqueta=desamor'); await expect(page.getByRole('button', { name: 'Etiquetas (2)' })).toHaveAttribute( 'aria-pressed', 'true' ); await expect(nube.getByRole('link', { name: /Rabia, seleccionada/ })).toHaveClass(/activa/); await expect(nube.getByRole('link', { name: /Desamor, seleccionada/ })).toHaveClass(/activa/); await expect(page.getByRole('status')).toContainText('etiquetas «Rabia» o «Desamor»'); await expect(page.getByRole('heading', { name: 'Ton silence me libère' })).toBeVisible(); await nube.getByRole('link', { name: /Rabia, seleccionada/ }).click(); await expect(page).toHaveURL('/musica?etiqueta=desamor'); await expect(nube.getByRole('link', { name: /Rabia, pulsar para añadir/ })).not.toHaveClass( /activa/ ); await expect(nube.getByRole('link', { name: /Desamor, seleccionada/ })).toHaveClass(/activa/); }); test('las etiquetas del desplegable se ordenan de más usada a menos', async ({ page }) => { await page.goto('/musica'); await page.getByRole('button', { name: 'Etiquetas', exact: true }).click(); const titulos = await page .getByRole('navigation', { name: 'Filtrar por etiqueta' }) .locator('a') .evaluateAll((enlaces) => enlaces.map((enlace) => enlace.getAttribute('title') ?? '')); const cuentas = titulos.map((titulo) => Number(titulo.split(' ')[0])); expect(cuentas).toEqual([...cuentas].sort((a, b) => b - a)); }); test('ya no existe una sección pública independiente de etiquetas', async ({ page }) => { const respuesta = await page.goto('/etiquetas'); expect(respuesta?.status()).toBe(404); }); });