La puerta del panel, y una portada que dice qué falta
Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
import { expect, test, type Locator, type Page } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Escribe en un campo y comprueba que se ha quedado escrito.
|
|
|
|
|
*
|
|
|
|
|
* El bucle no es paranoia. `page.goto` vuelve cuando la página ha cargado, no
|
|
|
|
|
* cuando SvelteKit la ha hidratado, y al hidratar, Svelte reescribe el valor de
|
|
|
|
|
* cada `input` con el del componente, que está vacío. Lo que se teclee antes de
|
|
|
|
|
* ese instante desaparece.
|
|
|
|
|
*
|
|
|
|
|
* Solo le pasaba al primer campo —los demás se rellenan milisegundos después,
|
|
|
|
|
* ya hidratada— y solo con la máquina cargada, así que fallaba en la tanda
|
|
|
|
|
* completa y pasaba al repetir la prueba a solas. Es la peor clase de fallo:
|
|
|
|
|
* el que parece un fantasma.
|
|
|
|
|
*/
|
|
|
|
|
async function escribir(campo: Locator, valor: string) {
|
|
|
|
|
await expect(async () => {
|
|
|
|
|
await campo.fill(valor);
|
|
|
|
|
await expect(campo).toHaveValue(valor, { timeout: 500 });
|
|
|
|
|
}).toPass({ timeout: 10_000 });
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
/** Rellena el formulario con datos válidos, salvo lo que se sobrescriba. */
|
|
|
|
|
async function rellenar(
|
La puerta del panel, y una portada que dice qué falta
Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
page: Page,
|
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
|
|
|
campos: Partial<{ nombre: string; email: string; motivo: string; mensaje: string }> = {}
|
|
|
|
|
) {
|
La puerta del panel, y una portada que dice qué falta
Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
await escribir(page.getByLabel('Nombre'), campos.nombre ?? 'Ana Ruiz');
|
|
|
|
|
await escribir(page.getByLabel('Correo electrónico'), campos.email ?? 'ana@example.com');
|
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
|
|
|
await page.getByLabel('Motivo').selectOption(campos.motivo ?? 'colaboracion');
|
La puerta del panel, y una portada que dice qué falta
Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 weeks ago
|
|
|
await escribir(
|
|
|
|
|
page.getByLabel('Mensaje'),
|
|
|
|
|
campos.mensaje ?? 'Me gustaría hablar contigo sobre una colaboración para un disco.'
|
|
|
|
|
);
|
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('Formulario de contacto', () => {
|
|
|
|
|
test('la validación del servidor devuelve los errores por campo', async ({ page }) => {
|
|
|
|
|
await page.goto('/contacto');
|
|
|
|
|
// "A" y "Hola" pasan la validación del navegador y fallan en el servidor,
|
|
|
|
|
// que es justo lo que interesa comprobar aquí.
|
|
|
|
|
await rellenar(page, { nombre: 'A', mensaje: 'Hola' });
|
|
|
|
|
await page.getByRole('button', { name: 'Enviar mensaje' }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText('Indica tu nombre.')).toBeVisible();
|
|
|
|
|
await expect(page.getByText(/Cuenta un poco más/)).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('lo escrito no se pierde cuando la validación falla', async ({ page }) => {
|
|
|
|
|
await page.goto('/contacto');
|
|
|
|
|
await rellenar(page, { nombre: 'A' });
|
|
|
|
|
await page.getByRole('button', { name: 'Enviar mensaje' }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText('Indica tu nombre.')).toBeVisible();
|
|
|
|
|
await expect(page.getByLabel('Correo electrónico')).toHaveValue('ana@example.com');
|
|
|
|
|
await expect(page.getByLabel('Motivo')).toHaveValue('colaboracion');
|
|
|
|
|
await expect(page.getByLabel('Mensaje')).not.toBeEmpty();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('el campo trampa descarta los envíos automáticos', async ({ page }) => {
|
|
|
|
|
await page.goto('/contacto');
|
|
|
|
|
await rellenar(page);
|
|
|
|
|
|
|
|
|
|
// Solo un cliente automático rellena este campo: está fuera de la vista
|
|
|
|
|
// y fuera del recorrido de tabulación.
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
const trampa = document.querySelector('#companiaWeb');
|
|
|
|
|
if (trampa instanceof HTMLInputElement) trampa.value = 'https://spam.example';
|
|
|
|
|
});
|
|
|
|
|
await page.getByRole('button', { name: 'Enviar mensaje' }).click();
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText('No se ha podido enviar el mensaje.')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('un envío válido no muestra errores de campo', async ({ page }) => {
|
|
|
|
|
await page.goto('/contacto');
|
|
|
|
|
await rellenar(page);
|
|
|
|
|
await page.getByRole('button', { name: 'Enviar mensaje' }).click();
|
|
|
|
|
|
|
|
|
|
// El resultado final depende de si hay proveedor de correo configurado
|
|
|
|
|
// (ver src/lib/server/correo.ts), así que solo se comprueba que la
|
|
|
|
|
// validación ha quedado atrás.
|
|
|
|
|
await expect(page.getByText('Indica tu nombre.')).toHaveCount(0);
|
|
|
|
|
await expect(page.getByText(/Cuenta un poco más/)).toHaveCount(0);
|
|
|
|
|
await expect(page.getByText('Ese correo no parece válido.')).toHaveCount(0);
|
|
|
|
|
});
|
|
|
|
|
});
|