home
  • Español
  • English
  • Português
  • Como trabalho
  • star Projetos Destacados
  • Experiência
  • mail Contato
arrow_back Voltar ao projeto Início

Testing do projeto

Testing · Marlow

5 ficheiros markdown (visão, unitários, integração, e2e, GitHub Actions).

Nesta página Testing · visión 0% expand_more
Testing · visión Comandos Convenciones Testing · unitarios Recorridos (shop/fulfillment.spec.ts) Direcciones (shop/address-rules.spec.ts) Sesión firmada (shop/session-seal.spec.ts) Build antiguo (stale-build.spec.ts) Testing · integración Transferencia (shop/bank-transfer.spec.ts) Cancelar (shop/order-cancel.spec.ts) Webhook de Neon Auth (mail/neon-auth-webhook.spec.ts) Email (mail/shop-mail.spec.ts) Migraciones (shop/shop-migration.spec.ts) Testing · flujos completos Compra como invitado Permisos por rol Otros recorridos del fichero Lo que no cubren Testing · CI Qué para un deploy hoy Qué haría un workflow

12-testing.md

Testing · visión

Marlow tiene 264 tests en 36 ficheros, todos con Vitest sobre jsdom a través del builder @angular/build:unit-test. La suite entera tarda unos 3 segundos y no necesita red ni base de datos: sin DATABASE_URL, el libro de la tienda vive en memoria y cada test lo reinicia (resetShopServer()).

CapaFicherosTestsQué cubre
Unitarios16114Reglas puras: direcciones, recorridos, sesión firmada, stock, dinero, idioma, plantillas de email
Integración1169La API de la tienda llamada directamente, la cuenta, el webhook de Neon Auth, el envío de email y las migraciones SQL
Flujos completos981La app entera renderizada con sus rutas reales y la API interceptada hacia el servidor de verdad
CI——No hay GitHub Actions; el build de la plataforma es la única puerta automática

Comandos

npm test -- --watch=false   # una pasada: los 264 tests
npm test                    # en una terminal, se queda observando cambios
npm run test:scripts        # node --test sobre scripts/ (los 28 tests de la plantilla)

Convenciones

  • El nombre del test es una frase del comportamiento, escrita como lo diría la tienda: “lets a guest reach confirmation without signing in”, “takes no money for a cancelled order”.
  • Sin mocks del dominio. Los tests de rutas no simulan la API: un HttpInterceptorFn llama a handleShopHttp, el mismo código que corre en el servidor.
  • Lo externo se sustituye en el borde: un mailer falso que recoge los emails, un directorio de cuentas falso en lugar de Neon, claves Ed25519 generadas en el test para el webhook.
  • El tiempo se controla. SessionSeal y el webhook reciben un reloj, y el carrusel corre con temporizadores falsos de Vitest, así que caducidades y turnos se prueban sin esperar.
  • Personas fijas: Avery (cliente), Jordan (personal) y Sloan (administración), con sesiones abiertas por openSession, como hace el servidor tras Neon Auth.

13-testing-unit.md

Testing · unitarios

Funciones y clases sin TestBed, sin HTTP y sin base de datos. 16 ficheros, 114 tests.

FicheroTestsCubre
shop/address-rules.spec.ts15Nombre, email, teléfono, calle y ciudad; formato postal y región por país
shop/shop-cloth.spec.ts11Tejido y cuidados, fotos por color (y sus dos ficheros en disco), prendas relacionadas, filas de Neon a prendas y pedidos
shop/shop-floor.spec.ts9Estados de stock, tallas por color, agotado, envío gratis, dólares a céntimos, bolsa guardada
shop/fulfillment.spec.ts7Recorridos de envío y recogida, la frase de cada paso, qué se puede cancelar
shop/order-list.spec.ts7Buscar y ordenar los pedidos del cliente desde la URL
shop/session-seal.spec.ts6Firmar, leer, caducar y rechazar tokens
i18n/i18n.spec.ts6Idioma del navegador frente a la elección guardada, frases sin traducción y sus marcadores
mail/shop-emails.spec.ts19Cada plantilla de email, en inglés y en español
ui/slide-show.spec.ts, ui/slide-track.spec.ts13Turnos del carrusel, pausa, movimiento reducido, seguimiento del deslizamiento
stale-build.spec.ts5Recargar una vez cuando falta el script de un build anterior
layout/site-header/header-tuck.spec.ts5La cabecera que se esconde al bajar y vuelve al subir
Otros11Cola del panel de tienda, cliente de Neon Auth, mailer, ficheros estáticos

Recorridos (shop/fulfillment.spec.ts)

Un envío y una recogida comparten los dos primeros pasos y terminan distinto. Un pedido cancelado no tiene recorrido, solo su nombre.

it('follows a shipped order from the shop to the door', () => {
  expect(labels(shipped('placed'))).toEqual([
    'Order placed',
    'Being prepared',
    'On its way',
    'Delivered',
  ]);
  expect(statuses(shipped('shipped'))).toEqual(['done', 'done', 'current', 'upcoming']);
});

it('has no track for a cancelled order, only its name', () => {
  expect(fulfillmentTrack(shipped('cancelled'))).toEqual([]);
  expect(fulfillmentName('cancelled')).toBe('Cancelled');
});

Direcciones (shop/address-rules.spec.ts)

Una tabla de casos por país. Las mismas funciones validan el formulario y el pedido en el servidor.

const cases: [string, string, boolean][] = [
  ['United States', '02903', true],
  ['United States', '2903', false],
  ['Spain', '28013', true],
  ['Spain', '53001', false],
  ['Canada', 'K1A 0B1', true],
  ['Canada', 'D1A 0B1', false],
  ['United Kingdom', 'SW1A 1AA', true],
  ['Portugal', '1100-148', true],
  // …
];

Sesión firmada (shop/session-seal.spec.ts)

Un token firmado por un proceso lo lee otro con la misma clave, como tras un reinicio. Otra clave, un payload cambiado a mano o un token de las cuentas compartidas retiradas no valen.

it('is read by any server that shares the key, as after a restart', () => {
  const token = sealAt(() => NOW).issue(ADA);
  expect(sealAt(() => NOW).read(token)).toEqual(ADA);
});

it('is refused by a server with another key', () => {
  const token = sealAt(() => NOW).issue(ADA);
  expect(sealAt(() => NOW, 'postgres://another-secret').read(token)).toBeNull();
});

Build antiguo (stale-build.spec.ts)

it('loads the page in full when its script belongs to a build the server replaced', async () => {
  const router = openShop(() => Promise.reject(CHROME_MISSING_SCRIPT));
  await goTo(router, '/checkout?from=bag');
  expect(fullLoads).toEqual(['/checkout?from=bag']);
});

it('does not reload in a loop when the full load meets the same failure', async () => {
  const router = openShop(() => Promise.reject(CHROME_MISSING_SCRIPT));
  await goTo(router, '/checkout');
  await goTo(router, '/checkout');
  expect(fullLoads).toEqual(['/checkout']);
});

El mismo fichero comprueba que reconoce el error de script ausente en Chrome, Firefox y Safari, que tienen mensajes distintos.

14-testing-integration.md

Testing · integración

Varios módulos juntos, con lo externo sustituido en el borde. Casi todos llaman a handleShopHttp(method, path, body, authorization), la función que el servidor Express usa para cada /api/*, y leen el resultado como lo leería el navegador. 11 ficheros, 69 tests.

FicheroTestsCubre
shop/account-server.spec.ts11Sesión guardada, cuentas retiradas, cambiar nombre, borrar la cuenta con un JWT reciente
mail/neon-auth-webhook.spec.ts10Firma Ed25519, reintentos, enlaces de contraseña en el idioma recordado
shop/pickup.spec.ts8Pedido para recoger: sin dirección, sin envío, pago en tienda solo ahí
mail/shop-mail.spec.ts8Qué email sale en cada momento, y que un fallo no tumba el pedido
shop/bank-transfer.spec.ts7Pago pendiente, enlace firmado, pagar una vez, cancelado no cobra
shop/shop-domain.spec.ts7Catálogo, bolsa, checkout, permisos del personal y entrada del servidor
shop/order-cancel.spec.ts6Quién cancela, hasta cuándo, y que el stock vuelve
shop/shop-presentation.spec.ts4Tailwind 4 sobre tokens, SSR, sin Ionic, Archivo autoalojada, movimiento reducido
shop/neon-accounts.spec.ts3Renombrar y borrar en una transacción, con un cliente SQL falso
shop/shop-migration.spec.ts3Las migraciones leídas como texto: RLS, permisos, sin tarjeta
shop/neon-auth-relay.spec.ts2El relay devuelve la cookie como propia y rechaza rutas ajenas sin llamar a Neon

Transferencia (shop/bank-transfer.spec.ts)

El enlace del email paga el pedido una sola vez; un código que no es el del pedido da 404 sin decir si el pedido existe.

it('marks the order paid once the transfer is sent, and only once', () => {
  const order = placed('bank-transfer', `Bearer ${openSession(ADA).token}`);
  const pay = apiPathOf(linkOf(order));

  const sent = handleShopHttp('POST', pay, {}, null);
  const again = handleShopHttp('POST', pay, {}, null);

  expect((sent.body as TransferDetails).state).toBe('paid');
  expect((again.body as TransferDetails).state).toBe('paid');
});

it('refuses a link whose code is not the shop own, without saying whether the order exists', () => {
  const order = placed();
  const forged = `/api/transfers/${order.reference}/not-the-code-for-this-order`;
  const refusal = { status: 404, body: { message: 'That payment link is not valid.' } };

  expect(handleShopHttp('GET', forged, null, null)).toEqual(refusal);
  expect(handleShopHttp('POST', forged, {}, null)).toEqual(refusal);
});

Cancelar (shop/order-cancel.spec.ts)

it('lets a client cancel their own order and puts its pieces back on the floor', () => {
  const before = shirtsOnTheFloor();
  const order = shirtOrder(avery);
  expect(shirtsOnTheFloor()).toBe(before - 2);

  const cancelled = cancelOrder(avery, order.reference);

  expect(cancelled.fulfillment).toBe('cancelled');
  expect(shirtsOnTheFloor()).toBe(before);
});

Otros casos del fichero: no se cancela un pedido que ya salió, ni uno de otro cliente, ni uno de invitado, y sin sesión la API responde 403.

Webhook de Neon Auth (mail/neon-auth-webhook.spec.ts)

El test genera su propio par de claves Ed25519 y firma los cuerpos como lo haría Neon. Un cuerpo cambiado tras firmar, una clave que Neon no publica o una entrega de hace más de 5 minutos se rechazan.

it('refuses a replay older than five minutes', async () => {
  const old = String(NOW - 6 * 60_000);
  await expect(verifyNeonWebhook(RESET, headers(RESET, old), keys, NOW)).rejects.toThrow();
});

Email (mail/shop-mail.spec.ts)

Un mailer falso recoge cada mensaje. Si falla, el pedido sigue y el error queda en el log.

it('never fails the order when the email cannot go out, and says why in the log', async () => {
  mailer.failing = true;
  await expect(mail.orderPlaced(ORDER, null)).resolves.toBeUndefined();
  expect(reported).toHaveLength(1);
});

Migraciones (shop/shop-migration.spec.ts)

Lee db/migrations/*.sql como texto. Si un cambio diera escritura a anonymous, moviera el rol a una columna editable o añadiera un campo de tarjeta, el test falla antes de que la migración llegue a una rama de Neon.

expect(sql).not.toMatch(/grant\s+(insert|update|delete|all)[^;]*\bto\s+anonymous/i);
expect(sql).toContain('client_id = (select auth.uid())');
expect(sql).not.toContain('card_number');

15-testing-e2e.md

Testing · flujos completos

Marlow no tiene Playwright. Los recorridos de punta a punta se prueban con la app entera en TestBed: las rutas reales de app.routes.ts, el shell, los guards y las páginas, sobre jsdom. Las llamadas a /api/* no se simulan: un interceptor las pasa a handleShopHttp, el mismo código del servidor, con el libro de la tienda en memoria. 9 ficheros, 81 tests.

const shopApiInterceptor: HttpInterceptorFn = (req, next) => {
  const url = new URL(req.url, 'http://marlow.test');
  if (!url.pathname.startsWith('/api/')) {
    return next(req);
  }
  const result = handleShopHttp(
    req.method,
    url.pathname,
    req.body,
    req.headers.get('Authorization'),
  );
  // ≥ 400 → HttpErrorResponse; si no, HttpResponse con result.body
};
FicheroTestsCubre
shop/shop-routes.spec.ts33Compra, checkout, color y talla, foto a pantalla completa, cancelación, transferencia, panel de tienda, permisos
pages/account/account-pages.spec.ts16Entrar con Neon Auth, cambiar nombre y contraseña, borrar la cuenta, recuperar la contraseña, retomar o renovar la sesión
layout/shell.spec.ts11Landmarks, enlace de salto, foco tras navegar, la bolsa que se abre y se cierra, el aviso de página lenta
app.spec.ts5La portada con las 8 prendas y sus precios, qué fotos se cargan con la página, una categoría con su nota de corte, una URL desconocida vuelve a /
shop/bag.spec.ts4El panel se abre en la línea añadida; la bolsa se guarda y se restaura
Otros12Carrusel de entrada, lupa de foto, revelado al hacer scroll, botones de la tienda

Compra como invitado

it('lets a guest reach confirmation without signing in', async () => {
  const { fixture } = await render('/product/oxford-shirt');
  await choose(fixture, 'input[name="size"][value="S"]');
  await clickButton(fixture, 'Add to bag');
  expect(document.body.textContent).toContain('Added Oxford Cloth Shirt, White S.');

  await go(fixture, '/checkout');
  await typeInto(fixture, 'contact-name', 'Ada Lovelace');
  await typeInto(fixture, 'contact-email', 'ada@example.com');
  await typeInto(fixture, 'contact-phone', '5550148');
  await typeInto(fixture, 'ship-line1', '12 Loom Street');
  await typeInto(fixture, 'ship-city', 'Providence');
  await pick(fixture, 'ship-region', 'Rhode Island');
  await typeInto(fixture, 'ship-postal', '02903');
  await choose(fixture, 'input[value="bank-transfer"]');
  await clickButton(fixture, 'Place order');

  expect(TestBed.inject(Router).url).toBe('/order/MR-1001');
  expect(document.body.textContent).toContain('Your order is placed.');
  expect(document.body.textContent).toContain('Providence, Rhode Island 02903');
});

Permisos por rol

El mismo test pasa por un visitante, un cliente, el personal y la administración, y comprueba qué abre cada uno.

it('refuses clients at both staff areas and workers at the backoffice', async () => {
  const { fixture } = await render('/owner');
  expect(document.body.textContent).toContain('closed to you');

  adopt(fixture, AVERY); // cliente
  await go(fixture, '/desk');
  expect(document.body.textContent).toContain('closed to you');

  adopt(fixture, JORDAN); // personal
  await go(fixture, '/owner/pieces');
  expect(document.body.textContent).toContain('closed to you');
  await go(fixture, '/desk/stock');
  expect(document.getElementById('piece-price')).toBeNull();

  adopt(fixture, SLOAN); // administración
  await go(fixture, '/owner/pieces');
  expect(document.getElementById('piece-price')).not.toBeNull();
});

Otros recorridos del fichero

  • Recoger en tienda, gratis y pagando allí; cambiar entre envío y recogida sin perder la dirección escrita.
  • Un campo muestra su error al salir de él; el checkout incompleto conserva lo escrito y señala lo que falta.
  • La dirección se adapta al país: estado en Estados Unidos, formato español en España.
  • Elegir color en la tarjeta cambia la foto y abre la ficha en ese color.
  • La ficha pide una talla en lugar de adivinarla.
  • El cliente cancela su pedido tras confirmar; al invitado se le dice que lo pida en la tienda.
  • La transferencia aparece pendiente, se paga en el banco simulado y pasa a Pagado.
  • Un pedido para recoger avanza por sus propios pasos en el panel de tienda.
  • Back vuelve por el historial si se vino desde la tienda, y a un sitio sensato si la página se abrió desde fuera.

Lo que no cubren

jsdom no pinta: no hay layout, ni showModal() (ui/dialog.ts usa el atributo open como reserva), ni View Transitions. El aspecto, el móvil y la accesibilidad se revisaron con Chrome headless y axe-core durante el trabajo (escritorio y móvil, claro y oscuro), con scripts que no están en el repo.

16-testing-ci.md

Testing · CI

No hay .github/workflows en el repo. Lo que corre solo en cada push a main es el build de la plataforma de hosting, y ese build no lanza los tests.

Qué para un deploy hoy

PuertaDóndeQué para
npm ciPlataforma de hostingUn package-lock.json desfasado
ng buildPlataforma de hostingUn error de TypeScript o de plantilla, o un bundle inicial de más de 1 MB
npm testA mano, antes de cada commitUn cambio que rompe un comportamiento probado

PROJECT.md define cuándo un cambio está terminado: el build pasa, los tests pasan e incluyen el comportamiento cambiado, las rutas SSR responden con HTML renderizado, npm run test:scripts pasa si cambió scripts/, y no hay secretos en ficheros versionados. Hoy esa regla la cumple quien hace el commit, no una máquina.

Qué haría un workflow

La suite no necesita servicios: sin DATABASE_URL, la tienda vive en memoria. Un job bastaría:

- uses: actions/setup-node@v4
  with:
    node-version: 24
    cache: npm
- run: npm ci
- run: npm test -- --watch=false
- run: npm run test:scripts
- run: npm run build

Ningún secreto haría falta en GitHub. Tampoco Chromium, porque los flujos completos corren en jsdom.

Nesta página

Testing · visión Comandos Convenciones Testing · unitarios Recorridos (shop/fulfillment.spec.ts) Direcciones (shop/address-rules.spec.ts) Sesión firmada (shop/session-seal.spec.ts) Build antiguo (stale-build.spec.ts) Testing · integración Transferencia (shop/bank-transfer.spec.ts) Cancelar (shop/order-cancel.spec.ts) Webhook de Neon Auth (mail/neon-auth-webhook.spec.ts) Email (mail/shop-mail.spec.ts) Migraciones (shop/shop-migration.spec.ts) Testing · flujos completos Compra como invitado Permisos por rol Otros recorridos del fichero Lo que no cubren Testing · CI Qué para un deploy hoy Qué haría un workflow

0% lido

folder_zip Descarregar todos (ZIP)
Pergunte sobre o Cristian Assistente com IA, só dados públicos

É um assistente automático e pode errar. Para algo importante, escreva ao Cristian.