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.
193 lines
8.5 KiB
193 lines
8.5 KiB
|
4 weeks ago
|
import { test, expect } from '@playwright/test';
|
||
|
|
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
||
|
|
import { readFileSync, mkdirSync } from 'node:fs';
|
||
|
|
import { parseEnv } from 'node:util';
|
||
|
|
import pg from 'pg';
|
||
|
|
const env = parseEnv(readFileSync('.env.estudio.local', 'utf8'));
|
||
|
|
const url = new URL(env.DATABASE_URL);
|
||
|
|
if (
|
||
|
|
!/^estudio_pruebas_/.test(env.ESTUDIO_ESQUEMA) ||
|
||
|
|
!url.searchParams.get('options')?.includes(`search_path=${env.ESTUDIO_ESQUEMA},`)
|
||
|
|
)
|
||
|
|
throw Error('Estas pruebas requieren un esquema aislado.');
|
||
|
|
const db = new pg.Pool({ connectionString: env.DATABASE_URL, max: 2 });
|
||
|
|
const token = randomBytes(32).toString('hex');
|
||
|
|
const admin = 'estudio-e2e-admin';
|
||
|
|
test.beforeAll(async () => {
|
||
|
|
expect((await db.query('SELECT current_schema() AS nombre')).rows[0].nombre).toBe(
|
||
|
|
env.ESTUDIO_ESQUEMA
|
||
|
|
);
|
||
|
|
await db.query(
|
||
|
|
"INSERT INTO usuario(id,email,nombre,rol) VALUES($1,'estudio@example.test','Cuenta de pruebas','admin') ON CONFLICT(id) DO NOTHING",
|
||
|
|
[admin]
|
||
|
|
);
|
||
|
|
await db.query(
|
||
|
|
"INSERT INTO sesion(id,usuario_id,expira_en) VALUES($1,$2,now()+interval '1 day')",
|
||
|
|
[createHash('sha256').update(token).digest('hex'), admin]
|
||
|
|
);
|
||
|
|
mkdirSync('docs/assets/estudio', { recursive: true });
|
||
|
|
});
|
||
|
|
test.afterAll(async () => {
|
||
|
|
await db.query('DELETE FROM sesion WHERE id=$1', [
|
||
|
|
createHash('sha256').update(token).digest('hex')
|
||
|
|
]);
|
||
|
|
await db.end();
|
||
|
|
});
|
||
|
|
test.beforeEach(async ({ context }) => {
|
||
|
|
await context.addCookies([
|
||
|
|
{
|
||
|
|
name: 'sesion',
|
||
|
|
value: token,
|
||
|
|
domain: '127.0.0.1',
|
||
|
|
path: '/',
|
||
|
|
httpOnly: true,
|
||
|
|
sameSite: 'Lax'
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
test('portada y navegación nueva', async ({ page }) => {
|
||
|
|
await page.goto('/');
|
||
|
|
await expect(page.getByRole('heading', { name: 'Administración', exact: true })).toBeVisible();
|
||
|
|
await expect(page.getByRole('navigation', { name: 'Administración' })).toBeVisible();
|
||
|
|
await page.screenshot({ path: 'docs/assets/estudio/inicio.png', fullPage: true });
|
||
|
|
for (const path of [
|
||
|
|
'/canciones',
|
||
|
|
'lanzamientos',
|
||
|
|
'personas',
|
||
|
|
'estilos',
|
||
|
|
'blog',
|
||
|
|
'paginas',
|
||
|
|
'legales',
|
||
|
|
'capitulos',
|
||
|
|
'generos',
|
||
|
|
'glosario',
|
||
|
|
'prompts',
|
||
|
|
'modelos',
|
||
|
|
'vocabularios',
|
||
|
|
'redirecciones',
|
||
|
|
'medios',
|
||
|
|
'actividad',
|
||
|
|
'operacion/pedidos',
|
||
|
|
'operacion/cuentas',
|
||
|
|
'operacion/suscripciones'
|
||
|
|
]) {
|
||
|
|
const response = await page.goto(path.startsWith('/') ? path : `/${path}`);
|
||
|
|
expect(response?.status(), path).toBe(200);
|
||
|
|
await expect(page.locator('h1')).toBeVisible();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
test('crear canción, conservar el borrador y añadir artista sin salir', async ({ page }) => {
|
||
|
|
const titulo = `Canción de prueba ${randomUUID().slice(0, 8)}`;
|
||
|
|
await page.goto('/canciones/nuevo');
|
||
|
|
await page.getByLabel('Nombre o título').fill(titulo);
|
||
|
|
await page.getByRole('button', { name: 'Crear borrador' }).click();
|
||
|
|
await expect(page.getByRole('heading', { name: titulo, exact: true, level: 1 })).toBeVisible();
|
||
|
|
const cancionUrl = page.url();
|
||
|
|
await page.getByRole('link', { name: 'Datos y contenido', exact: true }).click();
|
||
|
|
const inspiracion = page.getByRole('group', { name: 'Inspiración de la canción' });
|
||
|
|
await expect(inspiracion).toBeVisible();
|
||
|
|
await inspiracion.getByRole('button', { name: 'Texto', exact: false }).click();
|
||
|
|
await inspiracion.getByLabel(/^Texto/).fill('Una nota que debe conservarse.');
|
||
|
|
await page.getByRole('button', { name: 'Guardar borrador', exact: true }).click();
|
||
|
|
await expect(page.getByRole('status')).toContainText('guardado');
|
||
|
|
await page.getByRole('link', { name: 'Grabaciones', exact: true }).click();
|
||
|
|
await page.getByRole('link', { name: 'Abrir →', exact: true }).click();
|
||
|
|
await expect(page).toHaveURL(/\/grabaciones\/[^/?]+/);
|
||
|
|
await page.getByRole('link', { name: 'Créditos', exact: true }).click();
|
||
|
|
const bloque = page
|
||
|
|
.locator('section.bloque')
|
||
|
|
.filter({ has: page.getByRole('heading', { name: 'Intérpretes principales', exact: true }) });
|
||
|
|
await bloque.getByRole('button', { name: 'Crear', exact: false }).click();
|
||
|
|
const dialogo = page.getByRole('dialog');
|
||
|
|
await expect(dialogo).toBeVisible();
|
||
|
|
const artista = `Intérprete ${randomUUID().slice(0, 8)}`;
|
||
|
|
await dialogo.getByLabel('Nombre', { exact: true }).fill(artista);
|
||
|
|
await dialogo.getByRole('button', { name: 'Crear y seleccionar' }).click();
|
||
|
|
await expect(dialogo).not.toBeVisible();
|
||
|
|
await expect(bloque.getByRole('link', { name: artista })).toBeVisible();
|
||
|
|
await page.getByRole('button', { name: 'Guardar borrador', exact: true }).click();
|
||
|
|
await expect(page.getByRole('status')).toContainText('guardado');
|
||
|
|
await page.goto(cancionUrl);
|
||
|
|
await page.getByRole('link', { name: 'Datos y contenido', exact: true }).click();
|
||
|
|
await expect(
|
||
|
|
page.getByRole('group', { name: 'Inspiración de la canción' }).getByLabel(/^Texto/)
|
||
|
|
).toHaveValue('Una nota que debe conservarse.');
|
||
|
|
await page.screenshot({ path: 'docs/assets/estudio/ficha.png', fullPage: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('el blog permite componer y reordenar cinco tipos de sección', async ({ page }) => {
|
||
|
|
const titulo = `Entrada por bloques ${randomUUID().slice(0, 8)}`;
|
||
|
|
await page.goto('/blog/nuevo');
|
||
|
|
await page.getByLabel('Nombre o título').fill(titulo);
|
||
|
|
await page.getByRole('button', { name: 'Crear borrador' }).click();
|
||
|
|
|
||
|
|
const editor = page.getByRole('group', { name: 'Secciones' });
|
||
|
|
for (const tipo of ['Texto', 'Código', 'Vídeo', 'Imagen', 'Sonido'])
|
||
|
|
await editor.getByRole('button', { name: tipo, exact: false }).click();
|
||
|
|
|
||
|
|
await expect(editor.locator('li.seccion')).toHaveCount(5);
|
||
|
|
await editor.getByLabel(/^Texto/).fill('Primer bloque del artículo.');
|
||
|
|
await editor.getByLabel('Código', { exact: true }).fill('const cancion = true;');
|
||
|
|
await editor.getByLabel('Lenguaje', { exact: false }).fill('typescript');
|
||
|
|
await editor.getByLabel('Dirección del vídeo').fill('https://youtu.be/dQw4w9WgXcQ');
|
||
|
|
await editor.getByLabel('Dirección de la imagen').fill('/imagenes/ejemplo.jpg');
|
||
|
|
await editor.getByLabel('Descripción accesible').fill('Imagen de ejemplo');
|
||
|
|
await editor.getByLabel('Dirección del sonido').fill('https://media.example/maqueta.mp3');
|
||
|
|
|
||
|
|
await editor.getByRole('button', { name: 'Subir sección 2' }).click();
|
||
|
|
await expect(editor.locator('li.seccion').first().getByLabel('Tipo de sección')).toHaveValue(
|
||
|
|
'codigo'
|
||
|
|
);
|
||
|
|
await page.screenshot({ path: 'docs/assets/estudio/editor-secciones-blog.png', fullPage: true });
|
||
|
|
await page.getByRole('button', { name: 'Guardar borrador', exact: true }).click();
|
||
|
|
await expect(page.getByRole('status')).toContainText('guardado');
|
||
|
|
await page.reload();
|
||
|
|
await expect(editor.locator('li.seccion')).toHaveCount(5);
|
||
|
|
await expect(editor.getByLabel('Código', { exact: true })).toHaveValue('const cancion = true;');
|
||
|
|
});
|
||
|
|
test('ficha publicada: guardar un borrador no cambia lo visible', async ({ page }) => {
|
||
|
|
const {
|
||
|
|
rows: [d]
|
||
|
|
} = await db.query(
|
||
|
|
"SELECT * FROM estudio_documento WHERE tipo='paginas' AND estado='publicado' LIMIT 1"
|
||
|
|
);
|
||
|
|
expect(d).toBeTruthy();
|
||
|
|
const antes = JSON.stringify(d.publicado);
|
||
|
|
await page.goto(`/paginas/${d.id}?tab=datos`);
|
||
|
|
await page.getByLabel('Descripción breve').fill(`Borrador de prueba ${Date.now()}`);
|
||
|
|
await page.getByRole('button', { name: 'Guardar borrador', exact: true }).first().click();
|
||
|
|
await expect(page.getByRole('status')).toBeVisible();
|
||
|
|
const {
|
||
|
|
rows: [despues]
|
||
|
|
} = await db.query('SELECT publicado,borrador FROM estudio_documento WHERE id=$1', [d.id]);
|
||
|
|
expect(JSON.stringify(despues.publicado)).toBe(antes);
|
||
|
|
expect(despues.borrador).not.toEqual(despues.publicado);
|
||
|
|
});
|
||
|
|
test('la navegación móvil y los formularios no desbordan', async ({ page }) => {
|
||
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
||
|
|
await page.goto('/');
|
||
|
|
await page.getByRole('button', { name: 'Abrir navegación' }).click();
|
||
|
|
await page
|
||
|
|
.getByRole('navigation', { name: 'Administración' })
|
||
|
|
.getByRole('link', { name: 'Lanzamientos', exact: true })
|
||
|
|
.click();
|
||
|
|
await expect(page.getByRole('heading', { name: 'Lanzamientos', exact: true })).toBeVisible();
|
||
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(
|
||
|
|
true
|
||
|
|
);
|
||
|
|
await page.screenshot({ path: 'docs/assets/estudio/movil.png', fullPage: true });
|
||
|
|
});
|
||
|
|
test('el panel exige sesión para lectura, POST y vista previa', async ({ request }) => {
|
||
|
|
const r = await request.get('/', { maxRedirects: 0 });
|
||
|
|
expect(r.status()).toBe(303);
|
||
|
|
expect(r.headers().location).toMatch(/^\/entrar/);
|
||
|
|
const post = await request.post('/artistas/nuevo', {
|
||
|
|
form: { titulo: 'No autorizado' },
|
||
|
|
maxRedirects: 0,
|
||
|
|
headers: { origin: 'http://127.0.0.1:5176', accept: 'text/html' }
|
||
|
|
});
|
||
|
|
expect(post.status()).toBe(303);
|
||
|
|
const preview = await request.get('/canciones/no-existe/vista-previa', { maxRedirects: 0 });
|
||
|
|
expect(preview.status()).toBe(303);
|
||
|
|
});
|