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 `` 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>/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>/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(`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(); 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([]); }); });