|
|
import { expect, test } from '@playwright/test';
|
|
|
|
|
|
test.describe('Taller de letras', () => {
|
|
|
test('el índice separa los principios de las fichas', async ({ page }) => {
|
|
|
await page.goto('/taller');
|
|
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Taller de letras');
|
|
|
await expect(page.getByRole('heading', { level: 2, name: 'Los principios' })).toBeVisible();
|
|
|
await expect(page.getByRole('heading', { level: 2, name: 'Fichas de creación' })).toBeVisible();
|
|
|
// El índice lateral rotula sus bloques, pero no como encabezados: si no,
|
|
|
// el esquema de la página diría dos veces lo mismo.
|
|
|
await expect(page.getByRole('heading', { level: 2 })).toHaveCount(2);
|
|
|
});
|
|
|
|
|
|
test('se llega a un principio y se encadena con el siguiente', async ({ page }) => {
|
|
|
await page.goto('/taller');
|
|
|
// El listado de la página, no el índice lateral, que repite los enlaces.
|
|
|
await page
|
|
|
.locator('section.bloque')
|
|
|
.getByRole('link', { name: /Lo que hace que una letra funcione/ })
|
|
|
.click();
|
|
|
|
|
|
await expect(page).toHaveURL('/taller/lo-que-hace-que-una-letra-funcione');
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
|
|
|
'Lo que hace que una letra funcione'
|
|
|
);
|
|
|
|
|
|
// Los capítulos están pensados para seguirse en orden.
|
|
|
await page.getByRole('link', { name: /Siguiente/ }).click();
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText('La imagen concreta');
|
|
|
});
|
|
|
|
|
|
test('una ficha de género trae sus datos de creación', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Bolero');
|
|
|
await expect(page.getByText('Santiago de Cuba, 1883')).toBeVisible();
|
|
|
|
|
|
/*
|
|
|
* Los datos comparables van en una franja a lo ancho, no en una barra
|
|
|
* lateral: el compás y la métrica de un género se leen juntos o no se leen.
|
|
|
* Antes era una lista de definiciones en una columna estrecha a la derecha,
|
|
|
* con los datos en letra pequeña y el texto escorado.
|
|
|
*/
|
|
|
const franja = page.locator('.datos .dato');
|
|
|
await expect(franja).toHaveCount(4);
|
|
|
await expect(franja.filter({ hasText: 'Compás' })).toContainText('2/4');
|
|
|
await expect(franja.filter({ hasText: 'Metro' })).toContainText('Octosílabo');
|
|
|
|
|
|
await expect(page.locator('aside.lateral')).toHaveCount(0);
|
|
|
|
|
|
await expect(page.getByRole('heading', { level: 2, name: 'De qué se habla' })).toBeVisible();
|
|
|
await expect(
|
|
|
page.getByRole('heading', { level: 2, name: 'Rasgos de escritura' })
|
|
|
).toBeVisible();
|
|
|
await expect(
|
|
|
page.getByRole('heading', { level: 2, name: 'Estructura de las canciones' })
|
|
|
).toBeVisible();
|
|
|
});
|
|
|
|
|
|
test('la franja de datos ocupa el ancho, y el texto su medida de lectura', async ({ page }) => {
|
|
|
// Es la diferencia con la barra lateral de antes, y lo que hace que los
|
|
|
// datos se puedan comparar de un vistazo.
|
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const datos = (await page.locator('.datos').boundingBox())!;
|
|
|
const texto = (await page.locator('.lectura .texto').boundingBox())!;
|
|
|
|
|
|
expect(datos.width).toBeGreaterThan(texto.width * 1.4);
|
|
|
expect(Math.abs(datos.x - texto.x)).toBeLessThan(2);
|
|
|
});
|
|
|
|
|
|
test('nada se queda corto por la derecha', async ({ page }) => {
|
|
|
/*
|
|
|
* La queja recurrente, convertida en prueba: la franja de arriba, el índice
|
|
|
* del capítulo y los bloques de abajo tienen que terminar en el mismo
|
|
|
* borde. Un texto no puede llenar novecientos píxeles —a menos de letra de
|
|
|
* veinticuatro puntos salen ciento veinte caracteres por línea—, así que lo
|
|
|
* que cubre ese ancho es lo que va a su lado, no el texto estirado.
|
|
|
*/
|
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const derecha = async (selector: string) => {
|
|
|
const caja = (await page.locator(selector).first().boundingBox())!;
|
|
|
return caja.x + caja.width;
|
|
|
};
|
|
|
|
|
|
const franja = await derecha('.datos');
|
|
|
const indice = await derecha('.apartados');
|
|
|
const bloque = await derecha('.bloque');
|
|
|
|
|
|
expect(Math.abs(franja - indice)).toBeLessThan(2);
|
|
|
expect(Math.abs(franja - bloque)).toBeLessThan(2);
|
|
|
});
|
|
|
|
|
|
test('la portada del taller también lleva el índice al lado', async ({ page }) => {
|
|
|
// El índice está en el layout de la sección, así que acompaña a todas sus
|
|
|
// páginas, no solo a los capítulos.
|
|
|
await page.goto('/taller');
|
|
|
|
|
|
const indice = page.getByRole('navigation', { name: 'Contenido del taller' });
|
|
|
await expect(indice).toBeVisible();
|
|
|
await expect(indice.getByRole('link', { name: 'Bolero' })).toBeVisible();
|
|
|
// En la portada no hay capítulo abierto que marcar.
|
|
|
await expect(indice.locator('a[aria-current="page"]')).toHaveCount(0);
|
|
|
});
|
|
|
|
|
|
test('cada capítulo lleva el índice del taller al lado', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const indice = page.getByRole('navigation', { name: 'Contenido del taller' });
|
|
|
await expect(indice).toBeVisible();
|
|
|
// Marca dónde estás dentro de la estructura.
|
|
|
await expect(indice.getByRole('link', { name: 'Bolero' })).toHaveAttribute(
|
|
|
'aria-current',
|
|
|
'page'
|
|
|
);
|
|
|
// Y enseña los dos bloques enteros, no solo el actual.
|
|
|
await expect(indice.getByRole('link', { name: 'El estribillo' })).toBeVisible();
|
|
|
});
|
|
|
|
|
|
test('un principio no enseña ficha de creación', async ({ page }) => {
|
|
|
// La ficha es de los estilos; en un principio serían bloques vacíos.
|
|
|
await page.goto('/taller/el-estribillo');
|
|
|
|
|
|
await expect(page.locator('.datos')).toHaveCount(0);
|
|
|
await expect(page.locator('.instrumentos')).toHaveCount(0);
|
|
|
await expect(page.getByRole('heading', { level: 2, name: 'Linaje' })).toHaveCount(0);
|
|
|
});
|
|
|
|
|
|
/*
|
|
|
* Lo que sigue solo existe desde que el taller lee de la base: en un archivo
|
|
|
* de Markdown no cabía la instrumentación con el peso de cada instrumento,
|
|
|
* ni los letristas con sus fechas, ni de qué géneros viene este.
|
|
|
*/
|
|
|
|
|
|
test('la ficha dice qué país es la cuna y cuáles lo adoptaron', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const paises = page.locator('.paises li');
|
|
|
await expect(paises).toHaveCount(6);
|
|
|
// La cuna va primero aunque alfabéticamente no le toque.
|
|
|
await expect(paises.first()).toContainText('Cuba');
|
|
|
await expect(paises.first()).toContainText('cuna');
|
|
|
await expect(paises.filter({ hasText: 'México' })).not.toContainText('cuna');
|
|
|
});
|
|
|
|
|
|
test('la instrumentación distingue lo esencial de lo ocasional', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const filas = page.locator('.instrumentos li');
|
|
|
await expect(filas.filter({ hasText: 'Guitarra' })).toContainText('esencial');
|
|
|
await expect(filas.filter({ hasText: 'Piano' })).toContainText('ocasional');
|
|
|
// El papel es lo que hace útil el dato: no basta con enumerar instrumentos.
|
|
|
await expect(filas.filter({ hasText: 'Requinto' })).toContainText('adornos');
|
|
|
});
|
|
|
|
|
|
test('los letristas traen país, años y papel', async ({ page }) => {
|
|
|
await page.goto('/taller/tango');
|
|
|
|
|
|
const gardel = page.locator('.personas li').filter({ hasText: 'Carlos Gardel' });
|
|
|
await expect(gardel).toContainText('Argentina · 1890–1935');
|
|
|
// «interprete» se guarda sin tilde, pero se lee con ella.
|
|
|
await expect(gardel).toContainText('intérprete');
|
|
|
});
|
|
|
|
|
|
test('las obras de referencia se fechan y se acreditan', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const primera = page.locator('.obras li').first();
|
|
|
await expect(primera).toContainText('1883');
|
|
|
await expect(primera).toContainText('Tristezas');
|
|
|
await expect(primera).toContainText('José «Pepe» Sánchez');
|
|
|
|
|
|
// Un fragmento de letra ajena solo se sostiene si se dice por qué está.
|
|
|
await expect(page.locator('.obras')).toHaveCount(1);
|
|
|
await expect(page.getByText('derecho de cita')).toBeVisible();
|
|
|
});
|
|
|
|
|
|
test('el linaje no enlaza a un género sin capítulo', async ({ page }) => {
|
|
|
/*
|
|
|
* El bolero-son existe en la base porque el bolero y el son confluyen en
|
|
|
* él, pero nadie ha escrito su ficha. Enlazarlo mandaba a un 404.
|
|
|
*/
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const linaje = page.locator('.rama');
|
|
|
await expect(linaje.getByRole('link', { name: 'Bachata' })).toBeVisible();
|
|
|
await expect(linaje.getByText('Bolero-son')).toBeVisible();
|
|
|
await expect(linaje.getByRole('link', { name: 'Bolero-son' })).toHaveCount(0);
|
|
|
});
|
|
|
|
|
|
test('la ficha enseña versos, no solo los describe', async ({ page }) => {
|
|
|
/*
|
|
|
* Se podía leer que el bolero va en octosílabo y endecasílabo alternados sin
|
|
|
* ver nunca uno. Un taller que describe la métrica y no la enseña está
|
|
|
* pidiendo que se la imaginen.
|
|
|
*/
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const ejemplos = page.locator('.ejemplo');
|
|
|
await expect(ejemplos).toHaveCount(2);
|
|
|
|
|
|
const primero = ejemplos.first();
|
|
|
await expect(primero).toContainText('Dejaste la taza a medias');
|
|
|
await expect(primero).toContainText('métrica');
|
|
|
// Cuatro versos, cada uno con su cuenta.
|
|
|
await expect(primero.locator('.estrofa li')).toHaveCount(4);
|
|
|
await expect(primero.locator('.cuenta').first()).toHaveText('8');
|
|
|
});
|
|
|
|
|
|
test('la escansión marca la sinalefa y el acento', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
// Dos palabras en una casilla, unidas por el arco: al cantarlas se dice
|
|
|
// una sola sílaba. Es lo que en un texto corrido no se ve.
|
|
|
const casillas = page.locator('.ejemplo').first().locator('.escansion').first();
|
|
|
await expect(casillas.locator('.silaba')).toHaveCount(8);
|
|
|
await expect(casillas).toContainText('za⁀a');
|
|
|
|
|
|
// Y el octosílabo lleva siempre acento en la séptima.
|
|
|
await expect(casillas.locator('.silaba.tonica').last()).toHaveText('me');
|
|
|
});
|
|
|
|
|
|
test('el octosílabo agudo mide ocho con siete casillas', async ({ page }) => {
|
|
|
// La ley del acento final, que es la primera regla que hay que tener
|
|
|
// automatizada para escribir sobre clave.
|
|
|
await page.goto('/taller/son-cubano');
|
|
|
|
|
|
const segundo = page.locator('.estrofa li').nth(1);
|
|
|
await expect(segundo.locator('.verso')).toHaveText('yo respondo con el son;');
|
|
|
await expect(segundo.locator('.silaba')).toHaveCount(7);
|
|
|
await expect(segundo.locator('.cuenta')).toHaveText('8');
|
|
|
});
|
|
|
|
|
|
test('los ejemplos van antes que la documentación del género', async ({ page }) => {
|
|
|
// Es un taller: quien abre esta página viene a escribir una letra, no a
|
|
|
// documentarse. Primero lo que se usa para escribir.
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const titulos = await page.getByRole('heading', { level: 2 }).allTextContents();
|
|
|
expect(titulos.indexOf('Ejemplos de versos')).toBeLessThan(titulos.indexOf('Instrumentación'));
|
|
|
expect(titulos.indexOf('Rasgos de escritura')).toBeLessThan(
|
|
|
titulos.indexOf('Obras de referencia')
|
|
|
);
|
|
|
});
|
|
|
|
|
|
test('cada obra de referencia dice dónde escucharla', async ({ page }) => {
|
|
|
await page.goto('/taller/tango');
|
|
|
|
|
|
const cambalache = page.locator('.obras li').filter({ hasText: 'Cambalache' });
|
|
|
const spotify = cambalache.getByRole('link', { name: 'Spotify' });
|
|
|
|
|
|
await expect(spotify).toBeVisible();
|
|
|
await expect(cambalache.getByRole('link', { name: 'YouTube' })).toBeVisible();
|
|
|
// En otra pestaña: irse del taller a mitad de una ficha es perder el sitio.
|
|
|
await expect(spotify).toHaveAttribute('target', '_blank');
|
|
|
await expect(spotify).toHaveAttribute('rel', /noopener/);
|
|
|
});
|
|
|
|
|
|
test('se avisa de lo que aún no ha revisado nadie', async ({ page }) => {
|
|
|
// La musicología la reunió un modelo y la ficha se publica con el nombre
|
|
|
// del titular. Callarlo sería lo cómodo.
|
|
|
await page.goto('/taller/bolero');
|
|
|
|
|
|
const aviso = page.locator('.sin-revisar');
|
|
|
await expect(aviso).toContainText('pendiente de revisión');
|
|
|
// Y enumera todo lo que sale de ahí, ejemplos de versos incluidos.
|
|
|
await expect(aviso).toContainText('versos');
|
|
|
});
|
|
|
|
|
|
/* --- El glosario ------------------------------------------------------- */
|
|
|
|
|
|
test('los términos del glosario salen marcados en el texto', async ({ page }) => {
|
|
|
// Lo que se pidió: «cada vez que aparezca un término del glosario en el
|
|
|
// texto, estará marcado para visualizarse con un popup».
|
|
|
await page.goto('/taller/son-cubano');
|
|
|
|
|
|
const marcados = page.locator('.texto .termino');
|
|
|
await expect(marcados.first()).toBeVisible();
|
|
|
|
|
|
// Cada término una sola vez por apartado, no en cada aparición: un texto
|
|
|
// con nueve «compás» subrayados no hay quien lo lea.
|
|
|
const slugs = await marcados.evaluateAll((bs) =>
|
|
|
bs.map((b) => (b as HTMLElement).dataset.termino)
|
|
|
);
|
|
|
expect(new Set(slugs).size).toBeGreaterThan(1);
|
|
|
});
|
|
|
|
|
|
test('al pulsar un término se abre su definición', async ({ page }) => {
|
|
|
await page.goto('/taller/son-cubano');
|
|
|
|
|
|
const montuno = page.locator('.texto .termino[data-termino="montuno"]').first();
|
|
|
await montuno.click();
|
|
|
|
|
|
const globo = page.getByRole('dialog', { name: /Definición de montuno/ });
|
|
|
await expect(globo).toBeVisible();
|
|
|
await expect(globo).toContainText('llamada y respuesta');
|
|
|
|
|
|
// Y se cierra con Escape, devolviendo el foco a donde estaba.
|
|
|
await page.keyboard.press('Escape');
|
|
|
await expect(globo).toBeHidden();
|
|
|
await expect(montuno).toBeFocused();
|
|
|
});
|
|
|
|
|
|
test('el globo no tapa el término que se acaba de pulsar', async ({ page }) => {
|
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
|
await page.goto('/taller/son-cubano');
|
|
|
|
|
|
const termino = page.locator('.texto .termino').first();
|
|
|
await termino.click();
|
|
|
|
|
|
const suyo = (await termino.boundingBox())!;
|
|
|
const globo = (await page.locator('.globo').boundingBox())!;
|
|
|
|
|
|
const seSolapan = globo.y < suyo.y + suyo.height && suyo.y < globo.y + globo.height;
|
|
|
expect(seSolapan).toBe(false);
|
|
|
});
|
|
|
|
|
|
test('no se marca dentro de un encabezado', async ({ page }) => {
|
|
|
// Ensuciaría el índice del capítulo, que sale de los encabezados.
|
|
|
await page.goto('/taller/son-cubano');
|
|
|
await expect(page.locator('.texto h2 .termino')).toHaveCount(0);
|
|
|
});
|
|
|
|
|
|
test('el glosario tiene su propia página, con las acepciones numeradas', async ({ page }) => {
|
|
|
await page.goto('/taller/glosario');
|
|
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Glosario');
|
|
|
await expect(page.getByRole('heading', { level: 2, name: 'sinalefa' })).toBeVisible();
|
|
|
|
|
|
/*
|
|
|
* «Clave» tiene dos: patrón rítmico e instrumento. Con una sola
|
|
|
* definición por palabra, una de las dos sería falsa, y por eso las
|
|
|
* acepciones van numeradas.
|
|
|
*/
|
|
|
const clave = page.locator('#clave');
|
|
|
await expect(clave.locator('.acepciones > li')).toHaveCount(2);
|
|
|
await expect(clave).toContainText('patrón rítmico');
|
|
|
await expect(clave).toContainText('percusión');
|
|
|
});
|
|
|
|
|
|
test('se llega al glosario desde el índice del taller', async ({ page }) => {
|
|
|
await page.goto('/taller/bolero');
|
|
|
await page
|
|
|
.getByRole('navigation', { name: 'Contenido del taller' })
|
|
|
.getByRole('link', { name: 'Glosario' })
|
|
|
.click();
|
|
|
|
|
|
await expect(page).toHaveURL('/taller/glosario');
|
|
|
});
|
|
|
|
|
|
test('el taller está en la navegación principal', async ({ page }) => {
|
|
|
await page.goto('/');
|
|
|
await page
|
|
|
.getByRole('navigation', { name: 'Principal' })
|
|
|
.getByRole('link', { name: 'Taller' })
|
|
|
.click();
|
|
|
|
|
|
await expect(page).toHaveURL('/taller');
|
|
|
});
|
|
|
});
|
|
|
|
|
|
test.describe('Tema claro y oscuro', () => {
|
|
|
test('sin elegir nada no hay atributo, y manda el marino', async ({ browser }) => {
|
|
|
// Aunque el sistema pida el claro. El color es de la marca y no lo decide
|
|
|
// una preferencia que se configuró pensando en otra cosa.
|
|
|
const claro = await browser.newContext({ colorScheme: 'light' });
|
|
|
const pagina = await claro.newPage();
|
|
|
await pagina.goto('/taller');
|
|
|
|
|
|
await expect(pagina.locator('html')).not.toHaveAttribute('data-tema', /.+/);
|
|
|
await expect(pagina.getByRole('button', { name: 'Cambiar al tema claro' })).toBeVisible();
|
|
|
|
|
|
await claro.close();
|
|
|
});
|
|
|
|
|
|
test('la elección manual manda y se recuerda', async ({ browser }) => {
|
|
|
const claro = await browser.newContext({ colorScheme: 'light' });
|
|
|
const pagina = await claro.newPage();
|
|
|
await pagina.goto('/taller');
|
|
|
|
|
|
await pagina.getByRole('button', { name: 'Cambiar al tema claro' }).click();
|
|
|
await expect(pagina.locator('html')).toHaveAttribute('data-tema', 'claro');
|
|
|
|
|
|
// Sobrevive a la recarga y, sobre todo, se aplica antes de pintar: lo hace
|
|
|
// un script en línea, no la hidratación.
|
|
|
await pagina.reload();
|
|
|
await expect(pagina.locator('html')).toHaveAttribute('data-tema', 'claro');
|
|
|
|
|
|
// Y se puede volver.
|
|
|
await pagina.getByRole('button', { name: 'Cambiar al tema oscuro' }).click();
|
|
|
await expect(pagina.locator('html')).toHaveAttribute('data-tema', 'oscuro');
|
|
|
|
|
|
await claro.close();
|
|
|
});
|
|
|
});
|