import { expect, test } from '@playwright/test'; import { stringify } from 'devalue'; test('la música y los letristas no enlazan fichas de géneros sin publicar', async ({ page }) => { await page.goto('/musica'); await page.locator('html[data-hidratado]').waitFor(); await page.getByRole('button', { name: 'Tabla', exact: true }).click(); await expect(page.getByRole('table')).toBeVisible(); await expect(page.locator('a[href="/generos/bolero-son"]')).toHaveCount(0); await page.goto('/generos/letristas'); await expect(page.getByText('Bolero-son', { exact: true }).first()).toBeVisible(); await expect(page.locator('a[href="/generos/bolero-son"]')).toHaveCount(0); }); test('el directorio de intérpretes usa una galería contenida con retratos dominantes', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto('/interpretes'); const tarjeta = page.locator('.artistas .tarjeta').first(); const retrato = tarjeta.locator('.marco-retrato'); await expect(tarjeta).toBeVisible(); const medidas = await page.evaluate(() => { const contenedor = document.querySelector('.pagina')!; const ficha = document.querySelector('.artistas .tarjeta')!; const imagen = ficha.querySelector('.marco-retrato')!; return { izquierda: Math.round(contenedor.getBoundingClientRect().left), anchoTarjeta: Math.round(ficha.getBoundingClientRect().width), anchoRetrato: Math.round(imagen.getBoundingClientRect().width), altoRetrato: Math.round(imagen.getBoundingClientRect().height) }; }); expect(medidas.izquierda).toBeGreaterThan(100); expect(medidas.anchoTarjeta).toBeLessThan(450); expect(medidas.anchoRetrato).toBe(medidas.anchoTarjeta - 2); expect(medidas.altoRetrato).toBeGreaterThan(medidas.anchoRetrato); await expect(retrato).toContainText('SP'); }); test('contacto conserva el mensaje ante un fallo de red y permite reintentar', async ({ page }) => { await page.goto('/contacto'); await page.locator('html[data-hidratado]').waitFor(); await page.route('**/contacto', (ruta) => ruta.request().method() === 'POST' ? ruta.abort() : ruta.continue() ); await page.getByLabel('Nombre', { exact: true }).fill('Prueba local'); await page.getByLabel('Correo electrónico').fill('prueba@example.test'); await page.getByLabel('Motivo').selectOption('colaboracion'); await page .getByLabel('Mensaje', { exact: true }) .fill('Este mensaje se conserva y no se envía fuera de la prueba.'); await page.getByRole('button', { name: 'Enviar mensaje' }).click(); await expect(page.getByRole('alert')).toContainText('Conservamos tu mensaje'); await expect(page.getByLabel('Mensaje', { exact: true })).toHaveValue( 'Este mensaje se conserva y no se envía fuera de la prueba.' ); await expect(page.getByLabel('Correo electrónico')).toHaveValue('prueba@example.test'); await expect(page.getByRole('button', { name: 'Enviar mensaje' })).toBeEnabled(); await expect(page).toHaveURL('/contacto'); }); test('el acceso conserva el correo si falla la conexión', async ({ page }) => { await page.goto('/entrar'); await page.locator('html[data-hidratado]').waitFor(); await page.route('**/entrar?*/solicitar', (ruta) => ruta.abort()); await page.getByLabel('Correo electrónico').fill('prueba@example.test'); await page.getByRole('button', { name: 'Enviarme el código' }).click(); await expect(page.getByRole('alert')).toContainText('Conservamos los datos'); await expect(page.getByLabel('Correo electrónico')).toHaveValue('prueba@example.test'); await expect(page.getByRole('button', { name: 'Enviarme el código' })).toBeEnabled(); }); test('el código pegado con espacios se acepta y bloquea envíos simultáneos', async ({ page }) => { await page.goto('/entrar?paso=codigo&email=prueba%40example.test'); await page.locator('html[data-hidratado]').waitFor(); const campo = page.getByLabel('Código', { exact: true }); await campo.evaluate((nodo) => { const datos = new DataTransfer(); datos.setData('text/plain', '123 456'); nodo.dispatchEvent( new ClipboardEvent('paste', { clipboardData: datos, bubbles: true, cancelable: true }) ); }); await expect(campo).toHaveValue('123456'); let liberar!: () => void; const espera = new Promise((resolver) => { liberar = resolver; }); await page.route('**/entrar?*/verificar', async (ruta) => { await espera; await ruta.fulfill({ contentType: 'application/json', body: JSON.stringify({ type: 'failure', status: 400, data: stringify({ paso: 'codigo', email: 'prueba@example.test', error: 'Código de prueba rechazado.' }) }) }); }); await page.getByRole('button', { name: 'Entrar', exact: true }).click(); await expect(page.getByRole('button', { name: 'Enviar otro código' })).toBeDisabled(); await expect(page.getByRole('button', { name: 'Usar otro correo' })).toBeDisabled(); liberar(); await expect(page.getByRole('alert')).toContainText('Código de prueba rechazado'); await expect(campo).toHaveValue('123456'); }); test('el modal y el formulario de contacto tienen etiquetas y focos independientes', async ({ page }) => { await page.goto('/contacto'); await page.locator('html[data-hidratado]').waitFor(); await page.getByRole('banner').getByRole('link', { name: 'Entrar', exact: true }).click(); const dialogo = page.getByRole('dialog'); await expect(dialogo.getByLabel('Correo electrónico')).toBeFocused(); await dialogo.locator('label').filter({ hasText: 'Correo electrónico' }).click(); await expect(dialogo.getByLabel('Correo electrónico')).toBeFocused(); const duplicados = await page.locator('[id]').evaluateAll((nodos) => { const ids = nodos.map((n) => n.id); return ids.filter((id, i) => ids.indexOf(id) !== i); }); expect(duplicados).toEqual([]); }); test('la barra espaciadora acciona el botón enfocado y no cambia la reproducción', async ({ page }) => { await page.goto('/albumes/cupido-sin-flechas'); await page.locator('html[data-hidratado]').waitFor(); await page.getByRole('button', { name: 'Reproducir', exact: true }).first().click(); const barra = page.getByRole('region', { name: 'Reproductor' }); await expect(barra.getByRole('button', { name: 'Pausar', exact: true })).toBeVisible(); const tema = page.getByRole('button', { name: /Cambiar al tema/ }).first(); const nombre = await tema.getAttribute('aria-label'); await tema.focus(); await page.keyboard.press('Space'); await expect(tema).not.toHaveAttribute('aria-label', nombre!); await expect(barra.getByRole('button', { name: 'Pausar', exact: true })).toBeVisible(); await barra.getByRole('button', { name: 'Pausar', exact: true }).focus(); await page.keyboard.press('Space'); await expect(barra.getByRole('button', { name: 'Reproducir', exact: true })).toBeVisible(); }); test('cada canción muestra sus versiones y recomendaciones en una fila horizontal', async ({ page }) => { await page.goto('/canciones/cuando-tus-manos-me-tocan'); await page.locator('html[data-hidratado]').waitFor(); await page.getByRole('tab', { name: 'Versiones', exact: true }).click(); const panel = page.getByRole('tabpanel', { name: 'Versiones', exact: true }); await expect(panel).toBeVisible(); await expect(panel.getByText('Versión principal', { exact: true })).toBeVisible(); const relacionadas = page.getByRole('region', { name: 'Canciones relacionadas' }); await expect(relacionadas).toBeVisible(); expect(await relacionadas.locator('.carril > li').count()).toBeGreaterThan(1); const medidas = await relacionadas .locator('.carril') .evaluate((carril) => ({ ancho: carril.clientWidth, contenido: carril.scrollWidth })); expect(medidas.contenido).toBeGreaterThan(medidas.ancho); await expect(relacionadas.getByText(/^Por |Mismo género|Con |Del mismo/).first()).toBeVisible(); const motivos = relacionadas.locator('.motivo .etiqueta'); await expect(motivos.first()).toBeVisible(); const estilos = await motivos.evaluateAll((elementos) => elementos.map((elemento) => { const estilo = getComputedStyle(elemento); return { color: estilo.color, fondo: estilo.backgroundColor, tamano: estilo.fontSize }; }) ); expect(estilos.every((estilo) => estilo.tamano === '11px')).toBe(true); expect(estilos.every((estilo) => estilo.fondo === 'rgba(0, 0, 0, 0)')).toBe(true); expect(new Set(estilos.map((estilo) => estilo.color)).size).toBeGreaterThan(1); const tarjetas = relacionadas.locator('.carril > li article'); const alineacion = await tarjetas.evaluateAll((elementos) => elementos.slice(0, 4).map((tarjeta) => { const nota = tarjeta.querySelector('.nota'); const pie = tarjeta.querySelector('.pie'); const titulo = tarjeta.querySelector('.titulo'); const personales = pie?.querySelector('.acciones-personales'); const iconos = personales?.querySelectorAll('svg'); const compra = pie?.querySelector('.compra'); if (!nota || !titulo || !iconos || iconos.length !== 2 || !compra) throw new Error('Tarjeta incompleta'); return { nota: Math.round(nota.getBoundingClientRect().top), titulo: Math.round(titulo.getBoundingClientRect().left), favorito: Math.round(iconos[0].getBoundingClientRect().left), lista: Math.round(iconos[1].getBoundingClientRect().left), compra: Math.round(compra.getBoundingClientRect().left) }; }) ); const posicionesDeNota = alineacion.map((tarjeta) => tarjeta.nota); expect(Math.max(...posicionesDeNota) - Math.min(...posicionesDeNota)).toBeLessThanOrEqual(1); expect(alineacion.every((tarjeta) => Math.abs(tarjeta.favorito - tarjeta.titulo) <= 1)).toBe( true ); expect( alineacion.every( (tarjeta) => tarjeta.favorito < tarjeta.lista && tarjeta.lista < tarjeta.compra ) ).toBe(true); }); test('favoritos, listas y likes abren el acceso como popup sin abandonar la canción', async ({ page }) => { const ruta = '/canciones/cuando-tus-manos-me-tocan'; await page.goto(ruta); await page.locator('html[data-hidratado]').waitFor(); const ficha = page.locator('header.ficha'); for (const { nombre, motivo } of [ { nombre: /añadir a favoritos/i, motivo: 'Necesitas entrar para guardar esta canción en Favoritos y recuperarla desde cualquier dispositivo.' }, { nombre: /añadir a una lista de reproducción/i, motivo: 'Necesitas entrar para añadir esta canción a una lista de reproducción o crear una nueva.' }, { nombre: /Me gusta: .*Inicia sesión/i, motivo: 'Necesitas entrar para dar Me gusta a esta canción. Cada cuenta puede dar un like y quitarlo cuando quiera.' } ]) { await ficha.getByRole('link', { name: nombre }).click(); const dialogo = page.getByRole('dialog'); await expect(dialogo).toBeVisible(); await expect(dialogo.getByText(motivo, { exact: true })).toBeVisible(); await expect(page).toHaveURL(ruta); await page.keyboard.press('Escape'); await expect(dialogo).toBeHidden(); } });