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.
207 lines
7.2 KiB
207 lines
7.2 KiB
import { expect, test } from '@playwright/test';
|
|
|
|
test.describe('Feed del blog', () => {
|
|
test('se sirve como RSS y lo anuncia toda la web', async ({ page, request }) => {
|
|
const respuesta = await request.get('/blog/rss.xml');
|
|
expect(respuesta.status()).toBe(200);
|
|
expect(respuesta.headers()['content-type']).toContain('application/rss+xml');
|
|
|
|
// Sin el `<link rel="alternate">` un lector no lo encuentra solo.
|
|
await page.goto('/');
|
|
await expect(page.locator('link[type="application/rss+xml"]')).toHaveAttribute(
|
|
'href',
|
|
'/blog/rss.xml'
|
|
);
|
|
});
|
|
|
|
test('lleva todas las entradas, de la más reciente primero', async ({ request }) => {
|
|
const xml = await (await request.get('/blog/rss.xml')).text();
|
|
|
|
const fechas = [...xml.matchAll(/<pubDate>(.*?)<\/pubDate>/g)].map((m) =>
|
|
new Date(m[1]).getTime()
|
|
);
|
|
expect(fechas.length).toBeGreaterThan(1);
|
|
expect(fechas).toEqual([...fechas].sort((a, b) => b - a));
|
|
});
|
|
|
|
test('las URLs son absolutas', async ({ request }) => {
|
|
// Un feed se lee fuera del sitio: una ruta relativa no lleva a ninguna
|
|
// parte.
|
|
const xml = await (await request.get('/blog/rss.xml')).text();
|
|
const enlaces = [...xml.matchAll(/<link>(.*?)<\/link>/g)].map((m) => m[1]);
|
|
|
|
expect(enlaces.length).toBeGreaterThan(0);
|
|
expect(enlaces.every((e) => e.startsWith('https://'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('Mapa del sitio', () => {
|
|
test('se sirve como XML y lo anuncia robots.txt', async ({ request }) => {
|
|
const respuesta = await request.get('/sitemap.xml');
|
|
expect(respuesta.status()).toBe(200);
|
|
expect(respuesta.headers()['content-type']).toContain('xml');
|
|
|
|
const robots = await (await request.get('/robots.txt')).text();
|
|
expect(robots).toContain('Sitemap: https://senzapaura.es/sitemap.xml');
|
|
// Y lo que no es contenido, fuera del rastreo.
|
|
for (const privada of ['/cuenta', '/carrito', '/descargas', '/acceso']) {
|
|
expect(robots).toContain(`Disallow: ${privada}`);
|
|
}
|
|
});
|
|
|
|
test('incluye el catálogo entero', async ({ request }) => {
|
|
const xml = await (await request.get('/sitemap.xml')).text();
|
|
|
|
// Una muestra de cada tipo: si algo se cayera del mapa, sería por tipo.
|
|
expect(xml).toContain('/canciones/dorina');
|
|
expect(xml).toContain('/musica/baladas-romanticas');
|
|
expect(xml).toContain('/albumes/singles-baladas-romanticas');
|
|
expect(xml).toContain('/generos/bachata');
|
|
expect(xml).toContain('/blog/lo-que-no-cabe-en-una-cancion');
|
|
expect(xml).toContain('/etiquetas/desamor');
|
|
});
|
|
|
|
test('no publica lo que depende de quién mira', async ({ request }) => {
|
|
const xml = await (await request.get('/sitemap.xml')).text();
|
|
|
|
for (const privada of [
|
|
'/cuenta',
|
|
'/carrito',
|
|
'/favoritos',
|
|
'/entrar',
|
|
'/descargas',
|
|
'/admin'
|
|
]) {
|
|
expect(xml).not.toContain(`<loc>https://senzapaura.es${privada}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Imagen al compartir', () => {
|
|
test('una página sin portada propia usa la de reserva', async ({ page }) => {
|
|
await page.goto('/taller');
|
|
|
|
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute(
|
|
'content',
|
|
'https://senzapaura.es/og.jpg'
|
|
);
|
|
});
|
|
|
|
test('un tema comparte su propia portada', async ({ page }) => {
|
|
await page.goto('/canciones/como-si-nada');
|
|
|
|
const contenido = await page.locator('meta[property="og:image"]').getAttribute('content');
|
|
expect(contenido).not.toContain('/og.jpg');
|
|
expect(contenido).toContain('/images/');
|
|
});
|
|
|
|
test('la imagen de reserva existe y tiene la proporción de una tarjeta', async ({ request }) => {
|
|
const respuesta = await request.get('/og.jpg');
|
|
|
|
expect(respuesta.status()).toBe(200);
|
|
expect(respuesta.headers()['content-type']).toContain('image/jpeg');
|
|
});
|
|
});
|
|
|
|
test.describe('Legal', () => {
|
|
test('las cinco páginas están y se enlazan desde el pie', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
const pie = page.getByRole('navigation', { name: 'Legal' });
|
|
for (const titulo of [
|
|
'Información legal',
|
|
'Condiciones de uso',
|
|
'Política de privacidad',
|
|
'Política de cookies',
|
|
'Condiciones de venta'
|
|
]) {
|
|
await expect(pie.getByRole('link', { name: titulo })).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('cada texto legal dice cuándo se revisó', async ({ page }) => {
|
|
// Un aviso legal sin fecha no sirve: quien lo lee necesita saber a qué
|
|
// versión se está acogiendo.
|
|
await page.goto('/legal/privacidad');
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Política de privacidad');
|
|
await expect(page.getByText(/Última revisión/)).toBeVisible();
|
|
});
|
|
|
|
test('desde un texto legal se llega a los demás', async ({ page }) => {
|
|
await page.goto('/legal/cookies');
|
|
|
|
const lateral = page.getByRole('navigation', { name: 'Textos legales' });
|
|
await expect(lateral.getByRole('link', { name: 'Política de cookies' })).toHaveAttribute(
|
|
'aria-current',
|
|
'page'
|
|
);
|
|
await lateral.getByRole('link', { name: 'Condiciones de venta' }).click();
|
|
await expect(page).toHaveURL('/legal/condiciones-de-venta');
|
|
});
|
|
|
|
/*
|
|
* El índice de apartados sale del Markdown en bruto y las anclas las pone un
|
|
* plugin al compilarlo: son dos caminos distintos que tienen que dar la misma
|
|
* cadena. Si se separan, los enlaces siguen pintándose y no llevan a ningún
|
|
* sitio, que es la clase de avería que no da error y no ve nadie.
|
|
*/
|
|
for (const slug of [
|
|
'aviso-legal',
|
|
'condiciones-de-uso',
|
|
'privacidad',
|
|
'cookies',
|
|
'condiciones-de-venta'
|
|
]) {
|
|
test(`el índice de /legal/${slug} apunta a apartados que existen`, async ({ page }) => {
|
|
await page.goto(`/legal/${slug}`);
|
|
|
|
const indice = page.getByRole('navigation', { name: 'Apartados de esta página' });
|
|
const destinos = await indice
|
|
.getByRole('link')
|
|
.evaluateAll((enlaces) => enlaces.map((e) => e.getAttribute('href') ?? ''));
|
|
|
|
expect(destinos.length).toBeGreaterThan(2);
|
|
for (const destino of destinos) {
|
|
await expect(page.locator(`h2${destino}`)).toHaveCount(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('pinchar un apartado lo deja visible bajo la cabecera', async ({ page }) => {
|
|
// La cabecera es pegajosa: sin `scroll-margin-top` el salto deja el título
|
|
// justo debajo de ella y parece que no ha pasado nada.
|
|
await page.goto('/legal/condiciones-de-venta');
|
|
|
|
const indice = page.getByRole('navigation', { name: 'Apartados de esta página' });
|
|
await indice.getByRole('link', { name: 'Derecho de desistimiento' }).click();
|
|
|
|
const apartado = page.locator('h2#derecho-de-desistimiento');
|
|
await expect(apartado).toBeInViewport();
|
|
|
|
const debajo = await apartado.evaluate((h) => {
|
|
const cabecera = document.querySelector('header')!.getBoundingClientRect().bottom;
|
|
return h.getBoundingClientRect().top >= cabecera;
|
|
});
|
|
expect(debajo).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('Sin terceros', () => {
|
|
test('cargar la web no contacta con ningún dominio ajeno', async ({ page }) => {
|
|
// Las tipografías se sirven desde aquí. Cargarlas de Google mandaba la IP
|
|
// de cada visitante a un tercero en cada página, sin pedirlo.
|
|
const ajenos = new Set<string>();
|
|
page.on('request', (r) => {
|
|
const host = new URL(r.url()).host;
|
|
if (!host.startsWith('localhost') && !host.startsWith('127.0.0.1')) ajenos.add(host);
|
|
});
|
|
|
|
for (const ruta of ['/', '/musica', '/canciones/dorina', '/entrar']) {
|
|
await page.goto(ruta);
|
|
}
|
|
|
|
expect([...ajenos]).toEqual([]);
|
|
});
|
|
});
|