You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/e2e/musica.e2e.ts

687 lines
29 KiB

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<HTMLElement>('aside.lateral')!;
const barra = document.querySelector<HTMLElement>('.barra')!;
const etiquetas = document.querySelector<HTMLElement>('.filtros-etiquetas')!;
const resultado = document.querySelector<HTMLElement>('.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');
Una sola tabla de canciones, y la cola se puede llenar Había dos listados: una tabla ordenable para el catálogo y una lista aparte para los discos. Cada arreglo había que hacerlo dos veces y el resultado era que la portada estaba en una y el año en la otra. Ahora es un solo componente, y lo que cambia son las columnas de contexto, que se encienden por props: dentro de un disco, la columna «Álbum» diría diez veces lo mismo. Solo recibe canciones. El estilo, el disco y la portada los resuelve contra el catálogo que ya baja en `page.data`, así que quien la usa no tiene que armar filas a mano, que era lo que obligaba a repetir código. Cada fila lleva su portada —con la ampliada al pasar el ratón—, y debajo del título van los géneros y las etiquetas en vez del primer verso: la letra está entera en la ficha, y aquí lo que sirve para elegir es de qué está hecho el tema. Las etiquetas se cortan por número, con un «+2» al final: recortarlas con un degradado dejaba «Autoengaño» en «Autoeng», que no parece una elección sino algo roto. La cola ya se puede llenar. Hasta ahora solo se podía sustituir —cada play la vaciaba y la llenaba con su contexto—, así que no había forma de armar una escucha cogiendo temas de discos distintos. `encolar()` es la otra mitad, y con la cola vacía arranca, porque un botón que no hace nada es peor que no tenerlo. Los géneros de una canción entran en el modelo: `cancion_genero` existía en la base y no llegaba a la web. Son cosa distinta del estilo —el estilo es cómo se ordena la obra aquí y es uno solo; los géneros son de qué está hecha la canción y pueden ser varios— y llevan al glosario. De paso: las letras largas van a dos columnas de 37ch, que es el mínimo que deja caber dos y a la vez el verso más largo del catálogo —importa afinarlo, porque en una letra dónde acaba cada verso es parte del texto—; la página baja de 2808 a 2023 píxeles. Contacto sale de la barra de menú y se queda solo en el pie. Y el titular pasa a «Letrista y creador musical». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
// 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<HTMLElement>('section.albumes .rejilla-albumes')!;
const album = rejilla.querySelector<HTMLElement>('.tarjeta')!;
const portada = album.querySelector<HTMLElement>('.tarjeta__medio')!;
const titulo = album.querySelector<HTMLElement>('.titulo')!;
const meta = album.querySelector<HTMLElement>('.meta')!;
const cancion = document.querySelector<HTMLElement>('section.canciones .tarjeta')!;
const tipo = album.querySelector<HTMLElement>('.tipo')!;
const reproducirAlbum = album.querySelector<HTMLElement>('.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<HTMLElement>('.eyebrow')!.getBoundingClientRect();
const titulo = elemento.querySelector<HTMLElement>('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(/<loc>[^<]*\/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);
});
});

Powered by TurnKey Linux.