new changes
This commit is contained in:
+26
-1
@@ -37,7 +37,32 @@ exactly those screenshots and API calls, and the report must show them.
|
||||
|
||||
## 2. Automate it with Claude Code
|
||||
|
||||
From the repo root, use the prompt in
|
||||
**The quick way — the `qa` tool.** It assembles a *grounded* generation prompt
|
||||
(your card + the app model + the conventions) so Claude never guesses selectors,
|
||||
routes, or endpoints:
|
||||
|
||||
```bash
|
||||
npm run qa:index # build the app-model grounding index (do once / after UI changes)
|
||||
node scripts/qa-tool.mjs list # list scenario cards
|
||||
node scripts/qa-tool.mjs prompt BOOK-012 # print the grounded prompt — paste into Claude Code
|
||||
node scripts/qa-tool.mjs run BOOK-012 # run the card's tests + open the evidence report
|
||||
```
|
||||
|
||||
`npm run qa:index` reads the codebase and writes
|
||||
[`app-model.md`](app-model.md) / `app-model.json`: the routes, page objects,
|
||||
API endpoints, areas and accounts that actually exist. `prompt` embeds that plus
|
||||
the card and [`e2e/CLAUDE.md`](../e2e/CLAUDE.md) into one ready-to-paste ask, so
|
||||
the generated test is correct by construction. `run` wraps `run-tests.ps1` for
|
||||
the card's layer and opens the evidence.
|
||||
|
||||
Generation stays human-in-the-loop: the tool prepares the grounded ask and runs
|
||||
the result; you (with Claude Code) write and review the test.
|
||||
|
||||
**Prefer a screen?** `npm run qa:ui` opens a small local web UI (localhost:9500)
|
||||
over the same commands: pick a card, copy its grounded prompt, hit **Run tests**,
|
||||
and see the evidence report inline. Same engine, nicer front door.
|
||||
|
||||
**Or by hand:** use the template in
|
||||
[`generate-test.prompt.md`](generate-test.prompt.md) with your card id. Claude
|
||||
reads the card and the conventions in [`e2e/CLAUDE.md`](../e2e/CLAUDE.md) and
|
||||
generates the spec using our page objects and fixtures.
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"generatedAt": "2026-10-10T09:15:03.203Z",
|
||||
"baseUrl": "http://localhost:4200",
|
||||
"auth": {
|
||||
"type": "basic",
|
||||
"tenantHeader": "X-Tenant",
|
||||
"note": "Credentials verified within the named tenant; take accounts from e2e/fixtures/accounts.ts, never hardcode.",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "SKYLINE_AGENT",
|
||||
"email": "agent@skyline.test",
|
||||
"tenant": "skyline",
|
||||
"role": "agent"
|
||||
},
|
||||
{
|
||||
"name": "SKYLINE_ADMIN",
|
||||
"email": "admin@skyline.test",
|
||||
"tenant": "skyline",
|
||||
"role": "admin"
|
||||
},
|
||||
{
|
||||
"name": "GLOBETROTTER_AGENT",
|
||||
"email": "agent@globetrotter.test",
|
||||
"tenant": "globetrotter",
|
||||
"role": "agent"
|
||||
},
|
||||
{
|
||||
"name": "GLOBETROTTER_ADMIN",
|
||||
"email": "admin@globetrotter.test",
|
||||
"tenant": "globetrotter",
|
||||
"role": "admin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
"/login",
|
||||
"/dashboard",
|
||||
"/search",
|
||||
"/book/:flightId",
|
||||
"/bookings",
|
||||
"/bookings/:bookingId"
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"spec": "auth.spec.ts",
|
||||
"title": "Signing in and sessions"
|
||||
},
|
||||
{
|
||||
"spec": "search.spec.ts",
|
||||
"title": "Searching for flights"
|
||||
},
|
||||
{
|
||||
"spec": "booking.spec.ts",
|
||||
"title": "Booking and cancelling"
|
||||
},
|
||||
{
|
||||
"spec": "tenant-isolation.spec.ts",
|
||||
"title": "Keeping agencies separate"
|
||||
},
|
||||
{
|
||||
"spec": "dashboard.spec.ts",
|
||||
"title": "Agency dashboard"
|
||||
},
|
||||
{
|
||||
"spec": "simulation.spec.ts",
|
||||
"title": "Behaviour when things go wrong"
|
||||
},
|
||||
{
|
||||
"spec": "responsive.spec.ts",
|
||||
"title": "Phones and small screens"
|
||||
}
|
||||
],
|
||||
"api": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/session",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/flights",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/flights/{id}",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/airports",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/airlines",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/bookings",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/bookings",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/bookings/{id}",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/bookings/{id}/cancel",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/dashboard",
|
||||
"auth": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/health",
|
||||
"auth": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/tenants",
|
||||
"auth": false
|
||||
}
|
||||
],
|
||||
"pageObjects": [
|
||||
{
|
||||
"file": "e2e/pages/bookings.page.ts",
|
||||
"classes": [
|
||||
"BookingsPage",
|
||||
"BookingDetailPage"
|
||||
],
|
||||
"locators": [
|
||||
"rows",
|
||||
"emptyState",
|
||||
"filterAll",
|
||||
"filterConfirmed",
|
||||
"filterCancelled",
|
||||
"confirmationBanner",
|
||||
"reference",
|
||||
"statusBadge",
|
||||
"travellerRows",
|
||||
"totalPaid",
|
||||
"cancelButton",
|
||||
"confirmCancelButton",
|
||||
"keepButton",
|
||||
"cancelledNotice"
|
||||
],
|
||||
"methods": [
|
||||
"goto()",
|
||||
"settled()",
|
||||
"rowFor(reference: string)",
|
||||
"open(reference: string)",
|
||||
"filterBy(status: 'all' | 'confirmed' | 'cancelled')",
|
||||
"bookingReference()",
|
||||
"cancel(expectedReference: string)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "e2e/pages/checkout.page.ts",
|
||||
"classes": [
|
||||
"CheckoutPage"
|
||||
],
|
||||
"locators": [
|
||||
"travellerFieldsets",
|
||||
"addTraveller",
|
||||
"contactEmail",
|
||||
"contactPhone",
|
||||
"confirm",
|
||||
"total",
|
||||
"error",
|
||||
"seatsLeft"
|
||||
],
|
||||
"methods": [
|
||||
"waitUntilReady()",
|
||||
"totalAmount()",
|
||||
"fillTraveller(index: number, traveller: Traveller)",
|
||||
"fillTravellers(travellers: Traveller[])",
|
||||
"removeTraveller(index: number)",
|
||||
"confirmBooking()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "e2e/pages/dashboard.page.ts",
|
||||
"classes": [
|
||||
"DashboardPage"
|
||||
],
|
||||
"locators": [
|
||||
"greeting",
|
||||
"tiles",
|
||||
"recentRows",
|
||||
"topRoutes"
|
||||
],
|
||||
"methods": [
|
||||
"goto()",
|
||||
"settled()",
|
||||
"tileValue(label: string)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "e2e/pages/login.page.ts",
|
||||
"classes": [
|
||||
"LoginPage"
|
||||
],
|
||||
"locators": [
|
||||
"tenantSelect",
|
||||
"email",
|
||||
"password",
|
||||
"submit",
|
||||
"error",
|
||||
"demoChips"
|
||||
],
|
||||
"methods": [
|
||||
"goto()",
|
||||
"useDemoChip(account: Account)",
|
||||
"fill(account: Account, password = account.password)",
|
||||
"signIn(account: Account)",
|
||||
"signInExpectingFailure(account: Account, password: string)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "e2e/pages/search.page.ts",
|
||||
"classes": [
|
||||
"FlightCard",
|
||||
"SearchPage"
|
||||
],
|
||||
"locators": [
|
||||
"root",
|
||||
"origin",
|
||||
"destination",
|
||||
"date",
|
||||
"cabin",
|
||||
"travellers",
|
||||
"searchButton",
|
||||
"resetButton",
|
||||
"sort",
|
||||
"resultSummary",
|
||||
"results",
|
||||
"emptyState",
|
||||
"nextPage",
|
||||
"prevPage"
|
||||
],
|
||||
"methods": [
|
||||
"fare()",
|
||||
"seatsLeft()",
|
||||
"flightNumber()",
|
||||
"durationMinutes()",
|
||||
"goto()",
|
||||
"card(index = 0)",
|
||||
"settled()",
|
||||
"searchRoute(from: string, to: string)",
|
||||
"setTravellers(n: number)",
|
||||
"sortBy(value: 'price' | 'duration' | 'departure')",
|
||||
"totalResults()",
|
||||
"cardForFlight(flightNumber: string)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "e2e/pages/shell.page.ts",
|
||||
"classes": [
|
||||
"ShellPage"
|
||||
],
|
||||
"locators": [
|
||||
"agencyName",
|
||||
"agencySlug",
|
||||
"accountName",
|
||||
"accountMeta",
|
||||
"signOut"
|
||||
],
|
||||
"methods": [
|
||||
"goToDashboard()",
|
||||
"goToSearch()",
|
||||
"goToBookings()",
|
||||
"signOutAndWait()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"conventions": "e2e/CLAUDE.md"
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# App model — grounding index for test generation
|
||||
|
||||
> Generated by `npm run qa:index` from the codebase. Do not edit by hand.
|
||||
|
||||
Base URL: `http://localhost:4200` · Auth: basic (header `X-Tenant`)
|
||||
|
||||
## Accounts
|
||||
|
||||
| const | email | tenant | role |
|
||||
| --- | --- | --- | --- |
|
||||
| `SKYLINE_AGENT` | agent@skyline.test | skyline | agent |
|
||||
| `SKYLINE_ADMIN` | admin@skyline.test | skyline | admin |
|
||||
| `GLOBETROTTER_AGENT` | agent@globetrotter.test | globetrotter | agent |
|
||||
| `GLOBETROTTER_ADMIN` | admin@globetrotter.test | globetrotter | admin |
|
||||
|
||||
_Password: see `DEMO_PASSWORD` in accounts.ts. Credentials verified within the named tenant; take accounts from e2e/fixtures/accounts.ts, never hardcode._
|
||||
|
||||
## Routes
|
||||
|
||||
- `/login`
|
||||
- `/dashboard`
|
||||
- `/search`
|
||||
- `/book/:flightId`
|
||||
- `/bookings`
|
||||
- `/bookings/:bookingId`
|
||||
|
||||
## Areas (spec file → report area)
|
||||
|
||||
- `auth.spec.ts` — Signing in and sessions
|
||||
- `search.spec.ts` — Searching for flights
|
||||
- `booking.spec.ts` — Booking and cancelling
|
||||
- `tenant-isolation.spec.ts` — Keeping agencies separate
|
||||
- `dashboard.spec.ts` — Agency dashboard
|
||||
- `simulation.spec.ts` — Behaviour when things go wrong
|
||||
- `responsive.spec.ts` — Phones and small screens
|
||||
|
||||
## API endpoints
|
||||
|
||||
| method | path | auth |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/session` | required |
|
||||
| GET | `/api/flights` | required |
|
||||
| GET | `/api/flights/{id}` | required |
|
||||
| GET | `/api/airports` | required |
|
||||
| GET | `/api/airlines` | required |
|
||||
| GET | `/api/bookings` | required |
|
||||
| POST | `/api/bookings` | required |
|
||||
| GET | `/api/bookings/{id}` | required |
|
||||
| POST | `/api/bookings/{id}/cancel` | required |
|
||||
| GET | `/api/dashboard` | required |
|
||||
| GET | `/api/health` | public |
|
||||
| GET | `/api/tenants` | public |
|
||||
|
||||
## Page objects (use these — no raw selectors in specs)
|
||||
|
||||
### `e2e/pages/bookings.page.ts` — BookingsPage, BookingDetailPage
|
||||
- locators: `rows`, `emptyState`, `filterAll`, `filterConfirmed`, `filterCancelled`, `confirmationBanner`, `reference`, `statusBadge`, `travellerRows`, `totalPaid`, `cancelButton`, `confirmCancelButton`, `keepButton`, `cancelledNotice`
|
||||
- methods: `goto()`, `settled()`, `rowFor(reference: string)`, `open(reference: string)`, `filterBy(status: 'all' | 'confirmed' | 'cancelled')`, `bookingReference()`, `cancel(expectedReference: string)`
|
||||
|
||||
### `e2e/pages/checkout.page.ts` — CheckoutPage
|
||||
- locators: `travellerFieldsets`, `addTraveller`, `contactEmail`, `contactPhone`, `confirm`, `total`, `error`, `seatsLeft`
|
||||
- methods: `waitUntilReady()`, `totalAmount()`, `fillTraveller(index: number, traveller: Traveller)`, `fillTravellers(travellers: Traveller[])`, `removeTraveller(index: number)`, `confirmBooking()`
|
||||
|
||||
### `e2e/pages/dashboard.page.ts` — DashboardPage
|
||||
- locators: `greeting`, `tiles`, `recentRows`, `topRoutes`
|
||||
- methods: `goto()`, `settled()`, `tileValue(label: string)`
|
||||
|
||||
### `e2e/pages/login.page.ts` — LoginPage
|
||||
- locators: `tenantSelect`, `email`, `password`, `submit`, `error`, `demoChips`
|
||||
- methods: `goto()`, `useDemoChip(account: Account)`, `fill(account: Account, password = account.password)`, `signIn(account: Account)`, `signInExpectingFailure(account: Account, password: string)`
|
||||
|
||||
### `e2e/pages/search.page.ts` — FlightCard, SearchPage
|
||||
- locators: `root`, `origin`, `destination`, `date`, `cabin`, `travellers`, `searchButton`, `resetButton`, `sort`, `resultSummary`, `results`, `emptyState`, `nextPage`, `prevPage`
|
||||
- methods: `fare()`, `seatsLeft()`, `flightNumber()`, `durationMinutes()`, `goto()`, `card(index = 0)`, `settled()`, `searchRoute(from: string, to: string)`, `setTravellers(n: number)`, `sortBy(value: 'price' | 'duration' | 'departure')`, `totalResults()`, `cardForFlight(flightNumber: string)`
|
||||
|
||||
### `e2e/pages/shell.page.ts` — ShellPage
|
||||
- locators: `agencyName`, `agencySlug`, `accountName`, `accountMeta`, `signOut`
|
||||
- methods: `goToDashboard()`, `goToSearch()`, `goToBookings()`, `signOutAndWait()`
|
||||
Reference in New Issue
Block a user