/** * Example end-to-end tests. * * Deliberately spec-agnostic: reads /api/spec at runtime to discover * the first model, then exercises the full CRUD path through the UI. * Works unchanged when you edit specs/main.specly to change models. * * What's verified: * 1. Runtime mounts and renders a view. * 2. Frontend ↔ backend plumbing: API base URL, routes, pluralisation * (e.g. Category → /api/categories, not /api/categorys), entity * shape, list rendering. * * Prerequisites: * 1. `npm run setup` (one-time: realize, install, db push) * 2. `npm run dev:backend` (terminal 1) * 3. `npm run dev:frontend` (terminal 2) * 4. `npm run test:e2e` (this test) */ import { test } from '@playwright/test'; import { bootRuntime, expectViewRenders, navigateToModel, createEntity, expectEntityInList, } from '@specverse/runtime/test-harness'; test('runtime mounts and renders a view', async ({ browser }) => { const { page } = await bootRuntime(browser); await expectViewRenders(page); }); test('frontend creates an entity and shows it in the list', async ({ browser }) => { const { page } = await bootRuntime(browser); // Skip gracefully if no backend is reachable — some templates ship // frontend-only and have no API server to exercise CRUD against. const backendBase = process.env.SPECVERSE_BACKEND_URL || 'http://localhost:3000'; const specResponse = await page.request.get(`${backendBase}/api/spec`).catch(() => null); if (!specResponse || !specResponse.ok()) { test.skip(true, `No backend reachable at ${backendBase} — skipping CRUD flow`); return; } const spec = await specResponse.json(); const modelsField = spec?.models || spec?.component?.models; const firstModel = Array.isArray(modelsField) ? modelsField.find((m: any) => m?.name)?.name : Object.keys(modelsField || {})[0]; if (!firstModel) { test.skip(true, 'Spec has no models — nothing to CRUD-test'); return; } // Synthesise a minimal body from required non-auto business fields. const modelDef = Array.isArray(modelsField) ? modelsField.find((m: any) => m.name === firstModel) : modelsField[firstModel]; const attrs = Array.isArray(modelDef?.attributes) ? modelDef.attributes : Object.entries(modelDef?.attributes || {}).map(([name, def]: [string, any]) => ({ name, ...def })); const body: Record = {}; for (const attr of attrs) { if (!attr?.required) continue; if (attr.category === 'metadata' || attr.auto) continue; const type = String(attr.type || 'String').toLowerCase(); if (/int|number|float|double/.test(type)) body[attr.name] = 1; else if (/bool/.test(type)) body[attr.name] = false; else if (/date/.test(type)) body[attr.name] = '2026-01-01T00:00:00.000Z'; else body[attr.name] = `test-${attr.name}`; } // Give the display-name field a recognisable, unique value so we can // assert it in the list later. const displayField = body['name'] !== undefined ? 'name' : body['title'] !== undefined ? 'title' : body['label'] !== undefined ? 'label' : Object.keys(body)[0]; if (displayField) { body[displayField] = `${body[displayField]}-${Date.now()}`; } const created = await createEntity(page, firstModel, body); await navigateToModel(page, firstModel); await expectEntityInList(page, firstModel, String(body[displayField] ?? created.id)); });