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.
750 lines
35 KiB
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');
|
|
});
|
|
});
|