Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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();
|
El dominio es senzapaura.es
Cambia las referencias al dominio de trabajo: la URL canónica, los correos
de los textos legales, las direcciones de retorno de Google y Facebook, y
lo que espera la suite.
El `robots.txt` pasa de archivo estático a ruta generada. Llevaba el
dominio escrito a mano y se quedó anunciando el anterior: un mapa del
sitio mal anunciado no da ningún error, simplemente no lo encuentra
nadie. Ahora sale de `site.url`, igual que el mapa y las etiquetas de
cada página. De paso deja fuera del rastreo lo que depende de quién mira
—cuenta, carrito, favoritos, entrar, descargas—, que no es contenido.
Y el Nginx queda listo para el dominio, con los tres pasos que hay que
dar fuera del servidor: los registros A a 89.46.247.16, la redirección de
los puertos 80 y 443 a 192.168.18.171, y Certbot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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}`);
|
|
|
|
|
}
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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');
|
Las fichas de género salen del taller y tienen su propia sección
Con lo que pediste: portada con mapa de Iberoamérica, buscador y un aside
que empieza por Inicio, Los principios, Glosario y Letristas antes de la
lista de géneros.
El mapa es interactivo de verdad: cada país se pinta según lo que tenga
—marcado si es cuna de algún género, tenue si solo lo adoptó, apagado si
no hay nada—, y al pulsarlo la lista de abajo se parte en «nació aquí» y
«también se canta ahí». Esa diferencia es lo que un mapa cuenta mejor que
una lista.
Los contornos son de `BlankMap-Americas.svg` de Wikimedia, en dominio
público, recortados a los veinte países de Iberoamérica en América. De
640 KB a 36 KB comprimidos, y solo en esta ruta: coordenadas a un decimal
y fuera los 134 islotes que a este tamaño miden menos de un píxel. Va por
script y no pegado a mano, para poder rehacerlo.
Cada país lleva además un punto de agarre calculado —el promedio de los
vértices de su costa, no el centro de su rectángulo, que en Cuba cae en
el mar—: sin él, Puerto Rico mide cuatro píxeles y una isla alargada no
hay quien la acierte.
«Letristas» es nueva: las mismas personas que salen repartidas en nueve
fichas, aquí juntas, con su papel en cada género y sus obras.
La página del capítulo pasa a `$lib/components/Capitulo.svelte` y la usan
las dos secciones: son setecientas líneas, y copiarlas habría sido dos
sitios donde arreglar cada cosa.
Las direcciones viejas redirigen con 301 —`/taller/bolero` a
`/generos/bolero`— resolviendo contra el catálogo de capítulos, no contra
una lista escrita a mano que alguien tenga que ampliar. Un enlace
compartido no puede empezar a dar 404 porque hayamos reordenado el menú.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
expect(xml).toContain('/generos/bachata');
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
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'
|
|
|
|
|
]) {
|
El dominio es senzapaura.es
Cambia las referencias al dominio de trabajo: la URL canónica, los correos
de los textos legales, las direcciones de retorno de Google y Facebook, y
lo que espera la suite.
El `robots.txt` pasa de archivo estático a ruta generada. Llevaba el
dominio escrito a mano y se quedó anunciando el anterior: un mapa del
sitio mal anunciado no da ningún error, simplemente no lo encuentra
nadie. Ahora sale de `site.url`, igual que el mapa y las etiquetas de
cada página. De paso deja fuera del rastreo lo que depende de quién mira
—cuenta, carrito, favoritos, entrar, descargas—, que no es contenido.
Y el Nginx queda listo para el dominio, con los tres pasos que hay que
dar fuera del servidor: los registros A a 89.46.247.16, la redirección de
los puertos 80 y 443 a 192.168.18.171, y Certbot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
expect(xml).not.toContain(`<loc>https://senzapaura.es${privada}`);
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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',
|
El dominio es senzapaura.es
Cambia las referencias al dominio de trabajo: la URL canónica, los correos
de los textos legales, las direcciones de retorno de Google y Facebook, y
lo que espera la suite.
El `robots.txt` pasa de archivo estático a ruta generada. Llevaba el
dominio escrito a mano y se quedó anunciando el anterior: un mapa del
sitio mal anunciado no da ningún error, simplemente no lo encuentra
nadie. Ahora sale de `site.url`, igual que el mapa y las etiquetas de
cada página. De paso deja fuera del rastreo lo que depende de quién mira
—cuenta, carrito, favoritos, entrar, descargas—, que no es contenido.
Y el Nginx queda listo para el dominio, con los tres pasos que hay que
dar fuera del servidor: los registros A a 89.46.247.16, la redirección de
los puertos 80 y 443 a 192.168.18.171, y Certbot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
'https://senzapaura.es/og.jpg'
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|
Textos legales, fuentes propias y ajustes de botones
Cinco páginas legales en /legal, enlazadas desde el pie de todas las páginas:
información legal, condiciones de uso, privacidad, cookies y condiciones de
venta. Son borradores con los datos del titular marcados entre corchetes y un
aviso de que hace falta revisión de un abogado antes de vender: cubren lo que
pide la ley española, no son un documento validado.
La política de cookies y la de privacidad están escritas mirando el código, no
copiadas de una plantilla: dos cookies, las dos necesarias, y dos terceros
—Stripe y Resend— que reciben lo imprescindible.
Y eran tres, porque las tipografías venían de Google: cada visita mandaba la IP
del visitante a un tercero sin pedirlo y sin que aportara nada. Ahora se sirven
desde el propio dominio. Cargar la web ya no contacta con ningún dominio ajeno,
y hay una prueba que lo comprueba.
Aparte: el símbolo de marca registrada en el aviso de derechos, los botones de
reproducir de las tarjetas pasan de 48 a 36 píxeles —eran casi una quinta parte
de la portada, y cambiaban de proporción según la tarjeta— y el de ver el
carrito solo aparece cuando hay algo dentro.
De paso se arregla un fallo de tipos que estaba latente: `resolve()` tiene una
sobrecarga por ruta y no acepta una unión, así que la navegación dejaba de
compilar en cuanto se añadían rutas. Ahora se resuelve en `site.ts`, una a una.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
Textos legales, fuentes propias y ajustes de botones
Cinco páginas legales en /legal, enlazadas desde el pie de todas las páginas:
información legal, condiciones de uso, privacidad, cookies y condiciones de
venta. Son borradores con los datos del titular marcados entre corchetes y un
aviso de que hace falta revisión de un abogado antes de vender: cubren lo que
pide la ley española, no son un documento validado.
La política de cookies y la de privacidad están escritas mirando el código, no
copiadas de una plantilla: dos cookies, las dos necesarias, y dos terceros
—Stripe y Resend— que reciben lo imprescindible.
Y eran tres, porque las tipografías venían de Google: cada visita mandaba la IP
del visitante a un tercero sin pedirlo y sin que aportara nada. Ahora se sirven
desde el propio dominio. Cargar la web ya no contacta con ningún dominio ajeno,
y hay una prueba que lo comprueba.
Aparte: el símbolo de marca registrada en el aviso de derechos, los botones de
reproducir de las tarjetas pasan de 48 a 36 píxeles —eran casi una quinta parte
de la portada, y cambiaban de proporción según la tarjeta— y el de ver el
carrito solo aparece cuando hay algo dentro.
De paso se arregla un fallo de tipos que estaba latente: `resolve()` tiene una
sobrecarga por ruta y no acepta una unión, así que la navegación dejaba de
compilar en cuanto se añadían rutas. Ahora se resuelve en `site.ts`, una a una.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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([]);
|
|
|
|
|
});
|
|
|
|
|
});
|