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/jukebox-programacion.e2e.ts

750 lines
35 KiB

import { expect, test } from '@playwright/test';
import pg from 'pg';
import { randomUUID } from 'node:crypto';
// Solo contra desarrollo con el código de prueba habilitado y una cuenta efímera.
test.describe('Programación autenticada del Juke-box', () => {
test.skip(process.env.E2E_JUKEBOX !== '1', 'Requiere la base de desarrollo y ACCESO_PRUEBAS.');
test.describe.configure({ mode: 'serial' });
const usuarioId = randomUUID();
const otroId = randomUUID();
const email = `jukebox-${usuarioId}@example.test`;
const listaId = randomUUID();
const listaAjena = randomUUID();
let canciones: { id: string; slug: string; titulo: string }[];
let db: pg.Client;
let conectada = false;
test.beforeAll(async () => {
db = new pg.Client({ connectionString: process.env.DATABASE_URL });
await db.connect();
conectada = true;
canciones = (
await db.query(
"select id, slug, titulo from cancion where estado='publicado' and slug in ('prometiste','cuando-tus-manos-me-tocan') order by slug"
)
).rows;
await db.query('insert into usuario (id,email) values ($1,$2), ($3,$4)', [
usuarioId,
email,
otroId,
`otro-${usuarioId}@example.test`
]);
await db.query(
"insert into suscripcion (id,usuario_id,plan_id,estado) select $1,$2,id,'activa' from plan_suscripcion where codigo='friend' and acceso_jukebox=true",
[randomUUID(), usuarioId]
);
await db.query(
"insert into lista_reproduccion (id,usuario_id,nombre) values ($1,$2,'Mi selección de mañana'),($3,$4,'Lista ajena')",
[listaId, usuarioId, listaAjena, otroId]
);
for (const [orden, c] of canciones.entries())
await db.query(
'insert into lista_reproduccion_cancion (lista_id,cancion_id,orden) values ($1,$2,$3)',
[listaId, c.id, orden]
);
});
test.afterAll(async () => {
if (db && conectada) {
await db.query('delete from usuario where id = any($1::text[])', [[usuarioId, otroId]]);
await db.end();
}
});
test.beforeEach(async ({ page }) => {
await page.goto(`/entrar?paso=codigo&email=${encodeURIComponent(email)}&volverA=%2Fjuke-box`);
await page.locator('html[data-hidratado]').waitFor();
await page.getByLabel('Código', { exact: true }).fill('123456');
await page.getByRole('button', { name: 'Entrar', exact: true }).click();
await expect(page).toHaveURL('/juke-box');
});
let primero = '';
let segundo = '';
test('separa reproducción y edición, crea dos jukebox y conserva catálogo, listas y modos', async ({
page
}) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await expect(page.locator('.programador')).toHaveCount(0);
await page.getByRole('link', { name: 'Configura tu Jukebox', exact: true }).click();
await expect(page).toHaveURL('/juke-box/configurar');
await page.getByRole('link', { name: 'Crear jukebox', exact: true }).click();
await expect(page).toHaveURL('/juke-box/nuevo');
await page.getByLabel('Nombre del jukebox', { exact: true }).fill('Mañanas');
await page.getByLabel('Descripción', { exact: true }).fill('Música para empezar el día.');
await expect(page.getByLabel('Nombre de la franja')).toHaveCount(0);
await expect(page.getByLabel('Zona horaria')).toHaveCount(0);
await page.getByRole('button', { name: 'Crear primera franja' }).click();
await page.getByRole('button', { name: 'Listas y canciones', exact: true }).click();
await page.getByRole('button', { name: '+ Añadir contenido', exact: true }).click();
await page
.getByRole('button', { name: 'Añadir lista Mi selección de mañana', exact: true })
.click();
await page.getByRole('button', { name: 'Canciones', exact: true }).click();
await page
.getByRole('button', { name: 'Añadir canción ' + canciones[1].titulo, exact: true })
.click();
await page.getByRole('button', { name: 'Listo', exact: false }).click();
await page.getByRole('button', { name: 'Subir ' + canciones[1].titulo, exact: true }).click();
await expect(page.locator('.contenidos > .contenido').first()).toContainText(
canciones[1].titulo
);
await page.getByRole('button', { name: 'Bajar ' + canciones[1].titulo, exact: true }).click();
await page.getByRole('button', { name: 'Todo el catálogo', exact: true }).click();
await page.getByRole('button', { name: 'Listas y canciones', exact: true }).click();
await expect(page.locator('.contenidos > .contenido')).toHaveCount(2);
await page.getByRole('button', { name: 'Guardar jukebox', exact: true }).click();
await expect(page).toHaveURL(/juke-box\/configurar\?jukebox=.+/);
primero = new URL(page.url()).searchParams.get('jukebox')!;
await expect(page.locator('.programador')).toHaveCount(0);
await page.getByRole('link', { name: '+ Crear jukebox', exact: true }).click();
await page.getByLabel('Nombre del jukebox', { exact: true }).fill('Tardes');
await page
.getByLabel('Descripción', { exact: true })
.fill('Todo el catálogo al caer la tarde.');
await page.getByRole('button', { name: 'Crear primera franja' }).click();
await page.getByRole('button', { name: 'Aleatoria', exact: true }).click();
await expect(
page.getByRole('button', { name: 'Todo el catálogo', exact: true })
).toHaveAttribute('aria-pressed', 'true');
await page.getByRole('button', { name: 'Guardar jukebox', exact: true }).click();
await expect(page).toHaveURL(/juke-box\/configurar\?jukebox=.+/);
segundo = new URL(page.url()).searchParams.get('jukebox')!;
expect(segundo).not.toBe(primero);
await page.reload();
await page.locator('html[data-hidratado]').waitFor();
await expect(page.locator('.coleccion .jukebox')).toHaveCount(2);
await expect(page.locator('.coleccion')).toContainText('Música para empezar el día.');
await expect(page.locator('.coleccion')).toContainText('Todo el catálogo al caer la tarde.');
await expect(page.locator('.horarios')).toContainText('Todo el catálogo');
await expect(page.locator('.horarios')).toContainText('Aleatoria');
await page.getByRole('link', { name: 'Abrir reproductor de Mañanas', exact: true }).click();
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await page.getByRole('button', { name: 'Reproducir Mañanas', exact: true }).click();
await expect(page.locator('.estado-jukebox')).toContainText('Mañanas');
const alturasAcciones = await page.locator('.acciones-heroe').evaluate((fila) =>
Array.from(fila.querySelectorAll('button'))
.filter((b) => b.offsetHeight)
.map((b) => {
const r = b.getBoundingClientRect();
return r.y + r.height / 2;
})
);
expect(Math.max(...alturasAcciones) - Math.min(...alturasAcciones)).toBeLessThanOrEqual(1);
await expect(page.getByRole('region', { name: 'Reproductor', exact: true })).toContainText(
canciones[0].titulo
);
await page.getByRole('link', { name: 'Configura tu Jukebox', exact: true }).click();
await page.getByRole('link', { name: 'Editar Tardes', exact: true }).click();
await expect(page).toHaveURL('/juke-box/' + segundo + '/editar');
await page.getByRole('button', { name: 'Secuencial', exact: true }).click();
await page.getByRole('button', { name: 'Guardar jukebox', exact: true }).click();
await expect(page).toHaveURL(/juke-box\/configurar\?jukebox=.+/);
await expect(page.locator('.control-jukebox')).toContainText('Mañanas');
await expect(page.getByRole('region', { name: 'Reproductor', exact: true })).toContainText(
canciones[0].titulo
);
await page.getByRole('link', { name: 'Abrir reproductor de Tardes', exact: true }).click();
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await page.getByRole('button', { name: 'Reproducir Tardes', exact: true }).click();
await expect(page.locator('.estado-jukebox')).toContainText('Tardes');
await page
.locator('.estado-jukebox')
.getByRole('button', { name: 'Detener Jukebox', exact: true })
.click();
await expect(page.locator('.estado-jukebox')).toContainText('Tardes');
await expect(page.locator('.estado-jukebox')).toContainText('Detenido');
expect((await page.request.get('/juke-box/' + segundo + '/programacion')).status()).toBe(200);
await page.goto('/juke-box');
await page.locator('html[data-hidratado]').waitFor();
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await expect(page.getByRole('list', { name: 'Tus jukebox' })).toBeVisible();
await page.getByRole('button', { name: 'Reproducir Mañanas', exact: true }).click();
await expect(page.getByRole('list', { name: 'Tus jukebox' })).not.toBeVisible();
await expect(page.locator('.estado-jukebox')).toContainText('Mañanas');
await page
.locator('.estado-jukebox')
.getByRole('button', { name: 'Detener Jukebox', exact: true })
.click();
});
test('mantiene borradores con el historial y permite cancelar un jukebox nuevo sin crearlo', async ({
page
}) => {
await page.getByRole('link', { name: 'Configura tu Jukebox', exact: true }).click();
await page.getByRole('link', { name: 'Editar Mañanas', exact: true }).click();
await page.getByRole('button', { name: 'Aleatoria', exact: true }).click();
await page.getByRole('banner').getByRole('link', { name: 'Música', exact: true }).click();
await expect(page).toHaveURL('/musica');
await page.goBack();
await expect(page.getByRole('button', { name: 'Aleatoria', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await page.getByRole('button', { name: 'Deshacer cambios', exact: true }).click();
await expect(page.getByRole('button', { name: 'Secuencial', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await page.getByRole('link', { name: 'Cancelar', exact: true }).click();
await page.getByRole('link', { name: '+ Crear jukebox', exact: true }).click();
await page.getByRole('button', { name: 'Crear primera franja' }).click();
await page.getByRole('link', { name: 'Cancelar', exact: true }).click();
await expect(page.locator('.coleccion .jukebox')).toHaveCount(2);
});
test('comprueba propietario, listas ajenas, revisión y la independencia de guardados', async ({
page,
request
}) => {
const endpoint = '/juke-box/' + primero + '/programacion';
expect((await request.get(endpoint)).status()).toBe(401);
const actual = await (await page.request.get(endpoint)).json();
const headers = { origin: new URL(page.url()).origin };
const ajena = structuredClone(actual);
ajena.programacion.franjas[0].contenidos.push({ tipo: 'lista', id: listaAjena });
expect((await page.request.put(endpoint, { headers, data: ajena })).status()).toBe(400);
const otroJukebox = randomUUID();
await db.query(
'insert into jukebox(id,usuario_id,numero,zona_horaria,franjas) values($1,$2,1,$3,$4)',
[otroJukebox, otroId, 'UTC', JSON.stringify(actual.programacion.franjas)]
);
expect((await page.request.get('/juke-box/' + otroJukebox + '/programacion')).status()).toBe(
404
);
expect(
(
await page.request.put('/juke-box/' + otroJukebox + '/programacion', {
headers,
data: actual
})
).status()
).toBe(409);
expect(
(
await page.request.delete('/juke-box/' + otroJukebox + '/programacion', {
headers,
data: { revision: 1 }
})
).status()
).toBe(409);
const revSegundo = await (
await page.request.get('/juke-box/' + segundo + '/programacion')
).json();
actual.programacion.usuarioId = otroId;
actual.programacion.franjas[0].nombre = 'Campo obsoleto';
expect((await page.request.put(endpoint, { headers, data: actual })).status()).toBe(200);
expect((await page.request.put(endpoint, { headers, data: actual })).status()).toBe(409);
const recargado = await (await page.request.get(endpoint)).json();
expect(recargado.programacion.usuarioId).toBeUndefined();
expect(recargado.programacion.franjas[0].nombre).toBeUndefined();
expect(
(await (await page.request.get('/juke-box/' + segundo + '/programacion')).json()).revision
).toBe(revSegundo.revision);
expect(
(
await page.request.put(endpoint, {
headers: { origin: 'https://ajena.example' },
data: recargado
})
).status()
).toBe(403);
});
test('el hero ocupa 100vh, conserva el play dorado y coloca las dos acciones debajo del título', async ({
page
}) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.emulateMedia({ reducedMotion: 'reduce' });
await expect(page.getByRole('heading', { name: 'Mis jukebox', exact: true })).toHaveCount(0);
await expect(page.getByRole('contentinfo')).toHaveCount(1);
expect(
await page.getByRole('contentinfo').evaluate((pie) => pie.getBoundingClientRect().top)
).toBe(1000);
await page.locator('.heroe-jukebox img').evaluate((img: HTMLImageElement) => img.decode());
const geometria = await page.locator('.heroe-jukebox').evaluate((h) => {
const titulo = h.querySelector('h1')!.getBoundingClientRect();
const botones = h.querySelector('.acciones-heroe')!.getBoundingClientRect();
return {
alto: h.getBoundingClientRect().height,
ventana: innerHeight,
izquierda: botones.x - titulo.x,
debajo: botones.y >= titulo.bottom
};
});
expect(geometria.alto).toBe(geometria.ventana);
expect(geometria.izquierda).toBe(0);
expect(geometria.debajo).toBe(true);
await expect
.poll(() =>
page
.locator('.titulo-heroe')
.evaluate((h) =>
Math.abs(
h.querySelector('h1')!.getBoundingClientRect().width -
h.querySelector('.entradilla span')!.getBoundingClientRect().width
)
)
)
.toBeLessThan(1);
expect(
await page
.locator('.entradilla span')
.evaluate((s) => parseFloat(getComputedStyle(s).fontSize))
).toBeGreaterThan(24);
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await expect(page.getByRole('list', { name: 'Tus jukebox' })).toContainText(
'Música para empezar el día.'
);
await page.screenshot({ path: 'test-results-publica/jukebox-desplegable-oscuro.png' });
await page.keyboard.press('Escape');
await expect(page.getByText('Piloto automático', { exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Play JukeBox', exact: true })).toHaveClass(
/btn--primary/
);
await expect(
page.getByRole('link', { name: 'Configura tu Jukebox', exact: true })
).not.toHaveClass(/btn--primary/);
await page.screenshot({ path: 'test-results-publica/jukebox-hero-100vh-oscuro.png' });
await page.getByRole('button', { name: 'Cambiar al tema claro' }).click();
await expect(page.locator('html')).toHaveAttribute('data-tema', 'claro');
await page.evaluate(
() =>
new Promise<void>((done) =>
requestAnimationFrame(() => requestAnimationFrame(() => done()))
)
);
await page.screenshot({ path: 'test-results-publica/jukebox-hero-100vh-claro.png' });
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await page.screenshot({ path: 'test-results-publica/jukebox-desplegable-claro.png' });
await page.keyboard.press('Escape');
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(
true
);
expect(
await page.locator('.heroe-jukebox').evaluate((h) => h.getBoundingClientRect().height)
).toBe(844);
await page.screenshot({ path: 'test-results-publica/jukebox-hero-100vh-movil.png' });
expect(
await page.getByRole('contentinfo').evaluate((pie) => pie.getBoundingClientRect().top)
).toBe(844);
await page.getByRole('link', { name: 'Configura tu Jukebox', exact: true }).click();
await page.getByRole('link', { name: 'Editar Mañanas', exact: true }).click();
await page.getByRole('button', { name: '+ Añadir contenido', exact: true }).click();
await page.getByRole('button', { name: 'Canciones', exact: true }).click();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(
true
);
const alturas = await page
.locator('.selector .etiqueta')
.evaluateAll((ns) => ns.map((n) => n.getBoundingClientRect().height));
expect(Math.max(...alturas)).toBeLessThanOrEqual(26);
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
await page.screenshot({
path: 'test-results-publica/jukebox-editor-movil.png',
fullPage: true
});
const guardarFranja = page.getByRole('button', { name: 'Guardar franja', exact: true });
const claro = await guardarFranja.evaluate((b) => getComputedStyle(b).backgroundColor);
await expect(guardarFranja).toHaveCSS('background-image', 'none');
await expect(page.locator('.toggle-rail')).toHaveCount(4);
await page.setViewportSize({ width: 1440, height: 1000 });
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
await page.screenshot({
path: 'test-results-publica/jukebox-formulario-claro.png',
fullPage: true
});
await page.getByRole('button', { name: 'Cambiar al tema oscuro' }).click();
await expect
.poll(() => guardarFranja.evaluate((b) => getComputedStyle(b).backgroundColor))
.not.toBe(claro);
await page.screenshot({
path: 'test-results-publica/jukebox-formulario-oscuro.png',
fullPage: true
});
});
test('cambia de franja al navegar, aplica sus modos y reproduce audio real', async ({ page }) => {
const endpoint = '/juke-box/' + primero + '/programacion';
const actual = await (await page.request.get(endpoint)).json();
actual.programacion = {
zonaHoraria: 'UTC',
franjas: [
{
id: 'primera',
inicio: '09:00',
fin: '10:00',
orden: 'secuencial',
catalogo: false,
contenidos: [{ tipo: 'lista', id: listaId }]
},
{
id: 'segunda',
inicio: '10:00',
fin: '11:00',
orden: 'aleatoria',
catalogo: false,
contenidos: [{ tipo: 'cancion', id: canciones[1].slug }]
}
]
};
expect(
(
await page.request.put(endpoint, {
headers: { origin: new URL(page.url()).origin },
data: actual
})
).status()
).toBe(200);
await page.addInitScript(() => {
window.Audio = new Proxy(window.Audio, {
construct(Original, args) {
const audio = Reflect.construct(Original, args) as HTMLAudioElement;
audio.dataset.pruebaJukebox = '';
document.body.append(audio);
return audio;
}
});
});
await page.clock.setFixedTime(new Date('2026-09-14T08:59:50Z'));
await page.goto('/juke-box?jukebox=' + primero);
await page.locator('html[data-hidratado]').waitFor();
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await page.getByRole('button', { name: 'Reproducir Mañanas', exact: true }).click();
await expect(page.locator('.estado-jukebox')).toContainText('Esperando próxima franja');
const respuesta = page.waitForResponse(
(r) => r.url().includes('/escucha/' + canciones[0].slug) && [200, 206].includes(r.status())
);
await page.clock.setFixedTime(new Date('2026-09-14T09:00:01Z'));
await respuesta;
const audio = page.locator('audio[data-prueba-jukebox]');
await expect
.poll(() =>
audio.evaluate(
(a: HTMLAudioElement) => !a.paused && a.readyState >= 2 && a.currentTime > 0.05
)
)
.toBe(true);
await page.getByRole('banner').getByRole('link', { name: 'Música', exact: true }).click();
await expect(page).toHaveURL('/musica');
await page.clock.setFixedTime(new Date('2026-09-14T10:00:01Z'));
await expect(page.locator('.control-jukebox')).toContainText('Franja 2');
await expect(page.getByRole('region', { name: 'Reproductor', exact: true })).toContainText(
canciones[1].titulo
);
await page.clock.setFixedTime(new Date('2026-09-14T11:00:01Z'));
await expect(page.locator('.control-jukebox')).toContainText('Esperando próxima franja');
await expect(page.getByRole('region', { name: 'Reproductor', exact: true })).toBeVisible();
await expect(page.getByRole('group', { name: 'Sonando ahora', exact: true })).toHaveCount(0);
await page.getByRole('button', { name: 'Desactivar Jukebox', exact: true }).click();
const control = page.getByRole('group', { name: 'Jukebox', exact: true });
const barra = page.getByRole('region', { name: 'Reproductor', exact: true });
await expect(control).toContainText('Mañanas');
await expect(control).toContainText('Detenido');
await expect(control.getByRole('button', { name: 'Reproducir', exact: true })).toBeVisible();
const guardado = await (await page.request.get(endpoint)).json();
expect(guardado.nombre).toBe(actual.nombre);
expect(guardado.programacion).toEqual(actual.programacion);
await page.clock.setFixedTime(new Date('2026-09-15T09:15:00Z'));
await page.waitForTimeout(1200);
await expect(control).toContainText('Detenido');
expect(await audio.evaluate((a: HTMLAudioElement) => a.paused)).toBe(true);
await control.getByRole('button', { name: 'Reproducir', exact: true }).click();
await expect(control).toContainText('Franja 1');
await expect
.poll(() => audio.evaluate((a: HTMLAudioElement) => !a.paused && a.currentTime > 0.05))
.toBe(true);
await control.getByRole('button', { name: 'Detener Jukebox', exact: true }).click();
await expect(control).toContainText('Detenido');
await expect(barra.getByRole('group', { name: 'Sonando ahora', exact: true })).toHaveCount(0);
expect(await audio.evaluate((a: HTMLAudioElement) => a.paused)).toBe(true);
await barra.screenshot({ path: 'test-results-publica/jukebox-detenido-oscuro.png' });
await page.getByRole('button', { name: 'Cambiar al tema claro', exact: true }).click();
await barra.screenshot({ path: 'test-results-publica/jukebox-detenido-claro.png' });
await page.setViewportSize({ width: 390, height: 844 });
await barra.screenshot({ path: 'test-results-publica/jukebox-detenido-movil.png' });
expect(
await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)
).toBeLessThanOrEqual(0);
});
test('interrumpe y recupera audio real, agrupa los controles y permite volver desde la configuración', async ({
page
}) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.clock.setFixedTime(new Date('2026-09-14T09:15:00Z'));
await page.addInitScript(() => {
window.Audio = new Proxy(window.Audio, {
construct(Original, args) {
const audio = Reflect.construct(Original, args) as HTMLAudioElement;
audio.dataset.pruebaJukebox = '';
document.body.append(audio);
return audio;
}
});
});
await page.goto('/juke-box?jukebox=' + primero);
await page.locator('html[data-hidratado]').waitFor();
await page.getByRole('button', { name: 'Elegir Jukebox', exact: true }).click();
await page.getByRole('button', { name: 'Reproducir Mañanas', exact: true }).click();
const barra = page.getByRole('region', { name: 'Reproductor', exact: true });
const control = barra.getByRole('group', { name: 'Jukebox', exact: true });
const audio = page.locator('audio[data-prueba-jukebox]');
const suena = async () => {
await expect
.poll(() =>
audio.evaluate(
(a: HTMLAudioElement) => !a.paused && a.readyState >= 2 && a.currentTime > 0
)
)
.toBe(true);
};
await suena();
await page.getByRole('banner').getByRole('link', { name: 'Música', exact: true }).click();
await page.locator(`main a[href="/canciones/${canciones[1].slug}"]`).first().click();
await expect(page).toHaveURL('/canciones/' + canciones[1].slug);
await audio.evaluate((a: HTMLAudioElement) => {
a.currentTime = 37;
});
await page.getByRole('button', { name: 'Escuchar completa', exact: true }).click();
await expect(control).toContainText('Vuelve al terminar');
await expect(barra.getByRole('group', { name: 'Sonando ahora', exact: true })).toContainText(
canciones[1].titulo
);
await suena();
// El texto y sus acciones permanecen juntos, incluso con mucho ancho disponible.
for (const [tema, width] of [
['oscuro', 1440],
['claro', 1440],
['movil', 390]
] as const) {
if (tema === 'claro')
await page.getByRole('button', { name: 'Cambiar al tema claro', exact: true }).click();
await page.setViewportSize({ width, height: tema === 'movil' ? 844 : 1000 });
await expect(
control.getByRole('button', { name: 'Continuar', exact: true })
).toBeInViewport();
const medidas = await control.evaluate((grupo) => {
const datos = grupo.querySelector('.datos')!.getBoundingClientRect();
const botones = Array.from(grupo.querySelectorAll('button')).map((b) =>
b.getBoundingClientRect()
);
return {
separacion: botones[0].left - datos.right,
entreBotones: botones[1].left - botones[0].right,
desborde: document.documentElement.scrollWidth - innerWidth
};
});
expect(medidas.separacion).toBeLessThanOrEqual(12);
expect(medidas.separacion).toBeGreaterThanOrEqual(0);
expect(medidas.entreBotones).toBeLessThanOrEqual(12);
expect(medidas.desborde).toBeLessThanOrEqual(0);
await barra.screenshot({ path: `test-results-publica/jukebox-retorno-${tema}.png` });
await expect(barra.getByText('Calidad de escucha', { exact: true })).toHaveCount(0);
const tipo = await control.evaluate((grupo) => ({
nombre: getComputedStyle(grupo.querySelector('a')!).fontSize,
boton: getComputedStyle(grupo.querySelector('button')!).fontSize,
alto: grupo.querySelector('button')!.getBoundingClientRect().height
}));
expect(tipo).toEqual({ nombre: '16px', boton: '13px', alto: 36 });
const estiloContinuar = await control
.getByRole('button', { name: 'Continuar', exact: true })
.evaluate((b) => ({
fondo: getComputedStyle(b).backgroundImage,
peso: getComputedStyle(b).fontWeight
}));
expect(estiloContinuar.fondo).toContain('linear-gradient');
expect(estiloContinuar.peso).toBe('400');
if (tema !== 'movil') {
const continuar = control.getByRole('button', { name: 'Continuar', exact: true });
const colorDorado = await continuar.evaluate((b) =>
getComputedStyle(b).getPropertyValue('--c-accent-text').trim()
);
await expect(continuar).toHaveCSS('color', colorDorado);
await continuar.hover();
await continuar.evaluate(async (b) => {
await Promise.all(b.getAnimations().map((a) => a.finished.catch(() => {})));
});
await expect(continuar).toHaveCSS('color', colorDorado);
await barra.screenshot({
path: `test-results-publica/jukebox-continuar-hover-${tema}.png`
});
await page.mouse.move(0, 0);
await continuar.focus();
await page.keyboard.press('Tab');
await page.keyboard.press('Shift+Tab');
await expect(continuar).toBeFocused();
await expect(continuar).toHaveCSS('color', colorDorado);
await barra.screenshot({ path: `test-results-publica/jukebox-continuar-foco-${tema}.png` });
}
if (tema === 'movil') {
expect((await barra.boundingBox())!.height).toBeLessThanOrEqual(240);
await expect(
barra.getByRole('button', { name: 'Tema anterior', exact: true })
).toBeInViewport();
await expect(
barra.getByRole('button', { name: 'Tema siguiente', exact: true })
).toBeInViewport();
}
}
await page.setViewportSize({ width: 1440, height: 1000 });
await control.getByRole('button', { name: 'Continuar', exact: true }).click();
await suena();
await expect(barra.getByRole('group', { name: 'Sonando ahora', exact: true })).toContainText(
canciones[0].titulo
);
await expect
.poll(() => audio.evaluate((a: HTMLAudioElement) => a.currentTime))
.toBeGreaterThanOrEqual(37);
expect(await audio.evaluate((a: HTMLAudioElement) => a.currentTime)).toBeLessThan(45);
await expect(control.getByRole('button', { name: 'Continuar', exact: true })).toHaveCount(0);
// La pausa desde la ficha y desde la barra devuelve inmediatamente el jukebox.
for (const origen of ['ficha', 'barra'] as const) {
await page.getByRole('button', { name: 'Escuchar completa', exact: true }).click();
await suena();
await expect(control).toContainText('Vuelve al terminar');
await (origen === 'ficha' ? page.locator('main') : barra)
.getByRole('button', { name: 'Pausar', exact: true })
.click();
await expect(control).toContainText('Franja 1');
await suena();
await expect(barra.getByRole('group', { name: 'Sonando ahora', exact: true })).toContainText(
canciones[0].titulo
);
await expect
.poll(() => audio.evaluate((a: HTMLAudioElement) => a.currentTime))
.toBeGreaterThanOrEqual(37);
await expect(control.getByRole('button', { name: 'Continuar', exact: true })).toHaveCount(0);
}
// Si el jukebox ya estaba pausado, una canción manual no levanta esa pausa.
await barra.getByRole('button', { name: 'Pausar', exact: true }).click();
await expect(control).toContainText('Franja 1 · En pausa');
await page.getByRole('button', { name: 'Escuchar completa', exact: true }).click();
await suena();
await expect(control).toContainText('Jukebox en pausa');
await page.locator('main').getByRole('button', { name: 'Pausar', exact: true }).click();
await expect(control).toContainText('Franja 1 · En pausa');
expect(await audio.evaluate((a: HTMLAudioElement) => a.paused)).toBe(true);
await expect(barra.getByRole('group', { name: 'Sonando ahora', exact: true })).toContainText(
canciones[0].titulo
);
await barra.getByRole('button', { name: 'Reproducir', exact: true }).click();
await suena();
// Un final real del audio elegido vuelve automáticamente al mismo punto.
await page.getByRole('button', { name: 'Escuchar completa', exact: true }).click();
await suena();
await audio.evaluate((a: HTMLAudioElement) => {
a.currentTime = a.duration - 0.15;
});
await expect(control).toContainText('Franja 1');
await suena();
await expect
.poll(() => audio.evaluate((a: HTMLAudioElement) => a.currentTime))
.toBeGreaterThanOrEqual(37);
// El reloj cambia de franja durante la elección manual sin cortarla.
await page.getByRole('button', { name: 'Escuchar completa', exact: true }).click();
await suena();
await page.clock.setFixedTime(new Date('2026-09-14T10:00:01Z'));
await expect(control).toContainText('Vuelve al terminar');
await audio.evaluate((a: HTMLAudioElement) => {
a.currentTime = a.duration - 0.15;
});
await expect(control).toContainText('Franja 2');
await suena();
expect(await audio.evaluate((a: HTMLAudioElement) => a.currentTime)).toBeLessThan(10);
await barra.getByRole('button', { name: 'Pausar', exact: true }).click();
await page.clock.setFixedTime(new Date('2026-09-14T11:00:01Z'));
// Dejamos pasar al menos una consulta real del reloj de programación.
await page.waitForTimeout(1200);
await expect(control).toContainText('Franja 2 · En pausa');
expect(await audio.evaluate((a: HTMLAudioElement) => a.paused)).toBe(true);
await barra.screenshot({ path: 'test-results-publica/jukebox-pausa-persistente.png' });
await barra.getByRole('button', { name: 'Reproducir', exact: true }).click();
await expect(control).toContainText('Esperando próxima franja');
await expect(barra.getByRole('group', { name: 'Sonando ahora', exact: true })).toHaveCount(0);
await barra.screenshot({ path: 'test-results-publica/jukebox-espera-compacta.png' });
await control.getByRole('link', { name: 'Mañanas', exact: true }).click();
await expect(page).toHaveURL('/juke-box?jukebox=' + primero);
await page.getByRole('link', { name: 'Configura tu Jukebox', exact: true }).click();
await expect(page).toHaveURL('/juke-box/configurar');
await expect(page.getByRole('link', { name: 'Volver al Juke-box', exact: true })).toBeVisible();
await page.screenshot({ path: 'test-results-publica/jukebox-coleccion-claro.png' });
await page.getByRole('link', { name: 'Editar Mañanas', exact: true }).click();
await expect(page).toHaveURL('/juke-box/' + primero + '/editar');
await expect(
page.getByRole('heading', { name: 'Editar Jukebox 1', exact: true })
).toBeVisible();
await page.evaluate(() => window.scrollTo(0, 0));
const separacion = await page.evaluate(() => {
const cabecera = document.querySelector('header')!.getBoundingClientRect();
const migas = document.querySelector('.migas')!.getBoundingClientRect();
return migas.top - cabecera.bottom;
});
expect(separacion).toBeGreaterThanOrEqual(0);
expect(separacion).toBeLessThanOrEqual(32);
expect((await page.locator('.agenda').boundingBox())!.y).toBeLessThanOrEqual(460);
await page.screenshot({ path: 'test-results-publica/jukebox-editor-cabecera.png' });
await page.getByRole('button', { name: 'Cambiar al tema oscuro', exact: true }).click();
await page.screenshot({ path: 'test-results-publica/jukebox-editor-oscuro.png' });
await page.setViewportSize({ width: 390, height: 844 });
await page.locator('.editor').scrollIntoViewIfNeeded();
await page.screenshot({ path: 'test-results-publica/jukebox-editor-movil.png' });
expect(
await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)
).toBeLessThanOrEqual(0);
await page.setViewportSize({ width: 1440, height: 1000 });
await page.getByRole('link', { name: 'Volver al Juke-box', exact: true }).click();
await expect(page).toHaveURL('/juke-box?jukebox=' + primero);
await control.getByRole('button', { name: 'Desactivar Jukebox', exact: true }).click();
await expect(barra).toBeVisible();
await expect(control).toContainText('Detenido');
});
test('guardar una franja persiste sus cambios sin guardar el borrador de otra', async ({
page
}) => {
await page.goto('/juke-box/' + primero + '/editar');
await page.locator('html[data-hidratado]').waitFor();
await expect(page.getByLabel('Nombre del jukebox')).toHaveValue('Mañanas');
await page.getByLabel('Descripción', { exact: true }).fill('Descripción editada y guardada.');
await page.getByRole('button', { name: 'Aleatoria', exact: true }).click();
await page.locator('.franjas .franja').nth(1).click();
await page.getByLabel('Hasta', { exact: true }).fill('12:00');
await page.getByRole('button', { name: 'Guardar franja', exact: true }).click();
await expect(page.getByRole('status')).toContainText('Franja 2 guardada');
const guardado = await (
await page.request.get('/juke-box/' + primero + '/programacion')
).json();
expect(guardado.nombre).toBe('Mañanas');
expect(guardado.descripcion).toBe('Descripción editada y guardada.');
expect(guardado.programacion.franjas[0].orden).toBe('secuencial');
expect(guardado.programacion.franjas[1].fin).toBe('12:00');
await expect(page.getByText('Cambios sin guardar', { exact: true })).toBeVisible();
// Otra pestaña comprueba lo persistido sin restaurar el borrador del historial.
const recargada = await page.context().newPage();
await recargada.goto('/juke-box/' + primero + '/editar');
await recargada.locator('html[data-hidratado]').waitFor();
await expect(recargada.getByLabel('Descripción', { exact: true })).toHaveValue(
'Descripción editada y guardada.'
);
await expect(
recargada.getByRole('button', { name: 'Secuencial', exact: true })
).toHaveAttribute('aria-pressed', 'true');
await recargada.close();
});
test('detecta solapamientos y elimina solo el jukebox elegido', async ({ page }) => {
await page.getByRole('link', { name: 'Configura tu Jukebox', exact: true }).click();
await page.getByRole('link', { name: 'Editar Mañanas', exact: true }).click();
await page.getByLabel('Hasta', { exact: true }).fill('10:30');
await expect(page.getByRole('alert')).toContainText('solapa');
await expect(page.getByRole('button', { name: 'Guardar jukebox' })).toBeDisabled();
await page.getByRole('button', { name: 'Deshacer cambios', exact: true }).click();
await page.getByRole('link', { name: 'Cancelar', exact: true }).click();
await page.getByRole('link', { name: 'Editar Tardes', exact: true }).click();
await page.getByRole('button', { name: 'Eliminar Jukebox', exact: true }).click();
await page.getByRole('button', { name: 'Sí, eliminar Jukebox', exact: true }).click();
await expect(page).toHaveURL('/juke-box/configurar');
await expect(page.locator('.coleccion .jukebox')).toHaveCount(1);
await expect(page.locator('.coleccion .jukebox')).toContainText('Mañanas');
});
});

Powered by TurnKey Linux.