diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6631fb8..db3736e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -57,7 +57,7 @@ infra/ → Azure infrastructure (Bicep) 4. **Gherkin step definitions** — BDD behavioral tests, run against Aspire environment 5. **Smoke tests** — post-deployment verification (per increment) -> **All Cucumber and Playwright tests run against the Aspire environment** (`aspire run`), which orchestrates API and Web services identically to production. +> **All Cucumber and Playwright tests run against the Aspire environment** (`aspire start` + `aspire wait`), which orchestrates API and Web services identically to production. > **Iterative delivery:** Each increment goes through the full test → implement → deploy cycle. After each increment, ALL tests pass and the app is deployed. ## Human Gates diff --git a/.github/skills/aspire/SKILL.md b/.github/skills/aspire/SKILL.md new file mode 100644 index 0000000..79e41d6 --- /dev/null +++ b/.github/skills/aspire/SKILL.md @@ -0,0 +1,93 @@ +--- +name: aspire +description: "Orchestrates Aspire distributed applications using the Aspire CLI for running, debugging, and managing distributed apps. USE FOR: aspire start, aspire stop, start aspire app, aspire describe, list aspire integrations, debug aspire issues, view aspire logs, add aspire resource, aspire dashboard, update aspire apphost. DO NOT USE FOR: non-Aspire .NET apps (use dotnet CLI), container-only deployments (use docker/podman), Azure deployment after local testing (use azure-deploy skill). INVOKES: Aspire CLI commands (aspire start, aspire describe, aspire otel logs, aspire docs search, aspire add), bash. FOR SINGLE OPERATIONS: Use Aspire CLI commands directly for quick resource status or doc lookups." +--- + +# Aspire Skill + +This repository uses Aspire to orchestrate its distributed application. Resources are defined in the AppHost project (`apphost.cs` or `apphost.ts`). + +## CLI command reference + +| Task | Command | +|---|---| +| Start the app | `aspire start` | +| Start isolated (worktrees) | `aspire start --isolated` | +| Restart the app | `aspire start` (stops previous automatically) | +| Wait for resource healthy | `aspire wait ` | +| Stop the app | `aspire stop` | +| List resources | `aspire describe` or `aspire resources` | +| Run resource command | `aspire resource ` | +| Start/stop/restart resource | `aspire resource start|stop|restart` | +| View console logs | `aspire logs [resource]` | +| View structured logs | `aspire otel logs [resource]` | +| View traces | `aspire otel traces [resource]` | +| Logs for a trace | `aspire otel logs --trace-id ` | +| Add an integration | `aspire add` | +| List running AppHosts | `aspire ps` | +| Update AppHost packages | `aspire update` | +| Search docs | `aspire docs search ` | +| Get doc page | `aspire docs get ` | +| List doc pages | `aspire docs list` | +| Environment diagnostics | `aspire doctor` | +| List resource MCP tools | `aspire mcp tools` | +| Call resource MCP tool | `aspire mcp call --input ` | + +Most commands support `--format Json` for machine-readable output. Use `--apphost ` to target a specific AppHost. + +## Key workflows + +### Running in agent environments + +Use `aspire start` to run the AppHost in the background. When working in a git worktree, use `--isolated` to avoid port conflicts and to prevent sharing user secrets or other local state with other running instances: + +```bash +aspire start --isolated +``` + +Use `aspire wait ` to block until a resource is healthy before interacting with it: + +```bash +aspire start --isolated +aspire wait myapi +``` + +Relaunching is safe — `aspire start` automatically stops any previous instance. Re-run `aspire start` whenever changes are made to the AppHost project. + +### Debugging issues + +Before making code changes, inspect the app state: + +1. `aspire describe` — check resource status +2. `aspire otel logs ` — view structured logs +3. `aspire logs ` — view console output +4. `aspire otel traces ` — view distributed traces + +### Adding integrations + +Use `aspire docs search` to find integration documentation, then `aspire docs get` to read the full guide. Use `aspire add` to add the integration package to the AppHost. + +After adding an integration, restart the app with `aspire start` for the new resource to take effect. + +### Using resource MCP tools + +Some resources expose MCP tools (e.g. `WithPostgresMcp()` adds SQL query tools). Discover and call them via CLI: + +```bash +aspire mcp tools # list available tools +aspire mcp tools --format Json # includes input schemas +aspire mcp call --input '{"key":"value"}' # invoke a tool +``` + +## Important rules + +- **Always start the app first** (`aspire start`) before making changes to verify the starting state. +- **To restart, just run `aspire start` again** — it automatically stops the previous instance. NEVER use `aspire stop` then `aspire run`. NEVER use `aspire run` at all. +- Use `--isolated` when working in a worktree. +- **Avoid persistent containers** early in development to prevent state management issues. +- **Never install the Aspire workload** — it is obsolete. +- Prefer `aspire.dev` and `learn.microsoft.com/dotnet/aspire` for official documentation. + +## Playwright CLI + +If configured, use Playwright CLI for functional testing of resources. Get endpoints via `aspire describe`. Run `playwright-cli --help` for available commands. \ No newline at end of file diff --git a/.github/skills/e2e-generation/SKILL.md b/.github/skills/e2e-generation/SKILL.md index 65be796..2c878ef 100644 --- a/.github/skills/e2e-generation/SKILL.md +++ b/.github/skills/e2e-generation/SKILL.md @@ -148,7 +148,7 @@ Apply tags consistently: All e2e tests run against the **Aspire-managed environment**. The Playwright config already handles this: -- `webServer` in `e2e/playwright.config.ts` starts `aspire run --project ../apphost.cs` +- `webServer` in `e2e/playwright.config.ts` starts `aspire start` + `aspire wait web` - Base URL defaults to `http://localhost:3001` (Aspire web port) - API is available at `http://localhost:5001` (Aspire API port) - Running locally via Aspire is **non-negotiable** — never start API/Web individually diff --git a/.github/skills/gherkin-generation/SKILL.md b/.github/skills/gherkin-generation/SKILL.md index 34e51a0..4a5d187 100644 --- a/.github/skills/gherkin-generation/SKILL.md +++ b/.github/skills/gherkin-generation/SKILL.md @@ -56,7 +56,7 @@ When operating in `capture-existing` mode, follow these steps instead of the sta 1. **Read the FRD's "Current Implementation" section** — this is the primary input. Understand what the feature does today, including its endpoints, UI flows, data handling, and known limitations. 2. **Read extracted API contracts** (`specs/contracts/api/*.yaml`) — use the extracted OpenAPI specs to understand exact endpoint behavior: request/response shapes, status codes, and error responses. -3. **Observe actual behavior (if app is running)** — if the application is available (e.g., via `aspire run`), make requests or walk through flows to verify your understanding. Resolve any discrepancies between docs and actual behavior in favor of actual behavior. +3. **Observe actual behavior (if app is running)** — if the application is available (e.g., via `aspire start`), make requests or walk through flows to verify your understanding. Resolve any discrepancies between docs and actual behavior in favor of actual behavior. 4. **Generate happy-path scenarios** — write scenarios for the primary success paths of each feature area as it works today. 5. **Generate known edge-case scenarios** — document edge cases that the current implementation handles (or mishandles). Only include edge cases you can confirm exist. 6. **Generate error-handling scenarios** — document how the app currently responds to invalid input, unauthorized access, missing resources, etc. diff --git a/.github/skills/implementation/SKILL.md b/.github/skills/implementation/SKILL.md index 36a83e0..6b67ffe 100644 --- a/.github/skills/implementation/SKILL.md +++ b/.github/skills/implementation/SKILL.md @@ -142,7 +142,7 @@ See `references/web-slice.md` for the full procedure. ## Integration Slice — Overview Wire API + Web together via the Aspire environment. Replace all mocks with real -API calls. All tests run against `aspire run`. +API calls. All tests run against `aspire start`. **Key rules:** - **Mock removal is mandatory.** No mock API calls may remain after this slice. diff --git a/.github/skills/implementation/references/integration-slice.md b/.github/skills/implementation/references/integration-slice.md index 637f15a..78dd228 100644 --- a/.github/skills/implementation/references/integration-slice.md +++ b/.github/skills/implementation/references/integration-slice.md @@ -5,7 +5,7 @@ After BOTH the API and Web slices are complete (all their tests green), wire them together and verify end-to-end behavior. The key task is **replacing any mock API calls in the Web slice with real fetch calls to the running API**. All -integration tests run against the **Aspire environment** (`aspire run`), which +integration tests run against the **Aspire environment** (`aspire start`), which orchestrates API and Web services identically to production. ## Procedure @@ -33,7 +33,9 @@ orchestrates API and Web services identically to production. and the real API responses — the contract types are the source of truth. 3. Start the Aspire environment (if not already running): - aspire run + aspire start + aspire wait api --status healthy + aspire wait web --status healthy 4. Verify Playwright browsers are installed: npx playwright install --with-deps @@ -107,7 +109,9 @@ Before marking the integration slice as done, verify: ```bash # Start Aspire environment -aspire run +aspire start +aspire wait api --status healthy +aspire wait web --status healthy # Cucumber for a specific feature npx cucumber-js --tags "@{feature}" diff --git a/.github/skills/implementation/references/stack-patterns.md b/.github/skills/implementation/references/stack-patterns.md index ae8b05f..6cb5b58 100644 --- a/.github/skills/implementation/references/stack-patterns.md +++ b/.github/skills/implementation/references/stack-patterns.md @@ -93,7 +93,9 @@ cd src/web && npx vitest run ```bash # Start Aspire environment -aspire run +aspire start +aspire wait api --status healthy +aspire wait web --status healthy # Cucumber for a specific feature npx cucumber-js --tags "@{feature}" diff --git a/.github/skills/playwright-cli/SKILL.md b/.github/skills/playwright-cli/SKILL.md new file mode 100644 index 0000000..11bad2b --- /dev/null +++ b/.github/skills/playwright-cli/SKILL.md @@ -0,0 +1,278 @@ +--- +name: playwright-cli +description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages. +allowed-tools: Bash(playwright-cli:*) +--- + +# Browser Automation with playwright-cli + +## Quick start + +```bash +# open new browser +playwright-cli open +# navigate to a page +playwright-cli goto https://playwright.dev +# interact with the page using refs from the snapshot +playwright-cli click e15 +playwright-cli type "page.click" +playwright-cli press Enter +# take a screenshot (rarely used, as snapshot is more common) +playwright-cli screenshot +# close the browser +playwright-cli close +``` + +## Commands + +### Core + +```bash +playwright-cli open +# open and navigate right away +playwright-cli open https://example.com/ +playwright-cli goto https://playwright.dev +playwright-cli type "search query" +playwright-cli click e3 +playwright-cli dblclick e7 +playwright-cli fill e5 "user@example.com" +playwright-cli drag e2 e8 +playwright-cli hover e4 +playwright-cli select e9 "option-value" +playwright-cli upload ./document.pdf +playwright-cli check e12 +playwright-cli uncheck e12 +playwright-cli snapshot +playwright-cli snapshot --filename=after-click.yaml +playwright-cli eval "document.title" +playwright-cli eval "el => el.textContent" e5 +playwright-cli dialog-accept +playwright-cli dialog-accept "confirmation text" +playwright-cli dialog-dismiss +playwright-cli resize 1920 1080 +playwright-cli close +``` + +### Navigation + +```bash +playwright-cli go-back +playwright-cli go-forward +playwright-cli reload +``` + +### Keyboard + +```bash +playwright-cli press Enter +playwright-cli press ArrowDown +playwright-cli keydown Shift +playwright-cli keyup Shift +``` + +### Mouse + +```bash +playwright-cli mousemove 150 300 +playwright-cli mousedown +playwright-cli mousedown right +playwright-cli mouseup +playwright-cli mouseup right +playwright-cli mousewheel 0 100 +``` + +### Save as + +```bash +playwright-cli screenshot +playwright-cli screenshot e5 +playwright-cli screenshot --filename=page.png +playwright-cli pdf --filename=page.pdf +``` + +### Tabs + +```bash +playwright-cli tab-list +playwright-cli tab-new +playwright-cli tab-new https://example.com/page +playwright-cli tab-close +playwright-cli tab-close 2 +playwright-cli tab-select 0 +``` + +### Storage + +```bash +playwright-cli state-save +playwright-cli state-save auth.json +playwright-cli state-load auth.json + +# Cookies +playwright-cli cookie-list +playwright-cli cookie-list --domain=example.com +playwright-cli cookie-get session_id +playwright-cli cookie-set session_id abc123 +playwright-cli cookie-set session_id abc123 --domain=example.com --httpOnly --secure +playwright-cli cookie-delete session_id +playwright-cli cookie-clear + +# LocalStorage +playwright-cli localstorage-list +playwright-cli localstorage-get theme +playwright-cli localstorage-set theme dark +playwright-cli localstorage-delete theme +playwright-cli localstorage-clear + +# SessionStorage +playwright-cli sessionstorage-list +playwright-cli sessionstorage-get step +playwright-cli sessionstorage-set step 3 +playwright-cli sessionstorage-delete step +playwright-cli sessionstorage-clear +``` + +### Network + +```bash +playwright-cli route "**/*.jpg" --status=404 +playwright-cli route "https://api.example.com/**" --body='{"mock": true}' +playwright-cli route-list +playwright-cli unroute "**/*.jpg" +playwright-cli unroute +``` + +### DevTools + +```bash +playwright-cli console +playwright-cli console warning +playwright-cli network +playwright-cli run-code "async page => await page.context().grantPermissions(['geolocation'])" +playwright-cli tracing-start +playwright-cli tracing-stop +playwright-cli video-start +playwright-cli video-stop video.webm +``` + +## Open parameters +```bash +# Use specific browser when creating session +playwright-cli open --browser=chrome +playwright-cli open --browser=firefox +playwright-cli open --browser=webkit +playwright-cli open --browser=msedge +# Connect to browser via extension +playwright-cli open --extension + +# Use persistent profile (by default profile is in-memory) +playwright-cli open --persistent +# Use persistent profile with custom directory +playwright-cli open --profile=/path/to/profile + +# Start with config file +playwright-cli open --config=my-config.json + +# Close the browser +playwright-cli close +# Delete user data for the default session +playwright-cli delete-data +``` + +## Snapshots + +After each command, playwright-cli provides a snapshot of the current browser state. + +```bash +> playwright-cli goto https://example.com +### Page +- Page URL: https://example.com/ +- Page Title: Example Domain +### Snapshot +[Snapshot](.playwright-cli/page-2026-02-14T19-22-42-679Z.yml) +``` + +You can also take a snapshot on demand using `playwright-cli snapshot` command. + +If `--filename` is not provided, a new snapshot file is created with a timestamp. Default to automatic file naming, use `--filename=` when artifact is a part of the workflow result. + +## Browser Sessions + +```bash +# create new browser session named "mysession" with persistent profile +playwright-cli -s=mysession open example.com --persistent +# same with manually specified profile directory (use when requested explicitly) +playwright-cli -s=mysession open example.com --profile=/path/to/profile +playwright-cli -s=mysession click e6 +playwright-cli -s=mysession close # stop a named browser +playwright-cli -s=mysession delete-data # delete user data for persistent session + +playwright-cli list +# Close all browsers +playwright-cli close-all +# Forcefully kill all browser processes +playwright-cli kill-all +``` + +## Local installation + +In some cases user might want to install playwright-cli locally. If running globally available `playwright-cli` binary fails, use `npx playwright-cli` to run the commands. For example: + +```bash +npx playwright-cli open https://example.com +npx playwright-cli click e1 +``` + +## Example: Form submission + +```bash +playwright-cli open https://example.com/form +playwright-cli snapshot + +playwright-cli fill e1 "user@example.com" +playwright-cli fill e2 "password123" +playwright-cli click e3 +playwright-cli snapshot +playwright-cli close +``` + +## Example: Multi-tab workflow + +```bash +playwright-cli open https://example.com +playwright-cli tab-new https://example.com/other +playwright-cli tab-list +playwright-cli tab-select 0 +playwright-cli snapshot +playwright-cli close +``` + +## Example: Debugging with DevTools + +```bash +playwright-cli open https://example.com +playwright-cli click e4 +playwright-cli fill e7 "test" +playwright-cli console +playwright-cli network +playwright-cli close +``` + +```bash +playwright-cli open https://example.com +playwright-cli tracing-start +playwright-cli click e4 +playwright-cli fill e7 "test" +playwright-cli tracing-stop +playwright-cli close +``` + +## Specific tasks + +* **Request mocking** [references/request-mocking.md](references/request-mocking.md) +* **Running Playwright code** [references/running-code.md](references/running-code.md) +* **Browser session management** [references/session-management.md](references/session-management.md) +* **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md) +* **Test generation** [references/test-generation.md](references/test-generation.md) +* **Tracing** [references/tracing.md](references/tracing.md) +* **Video recording** [references/video-recording.md](references/video-recording.md) diff --git a/.github/skills/playwright-cli/references/request-mocking.md b/.github/skills/playwright-cli/references/request-mocking.md new file mode 100644 index 0000000..9005fda --- /dev/null +++ b/.github/skills/playwright-cli/references/request-mocking.md @@ -0,0 +1,87 @@ +# Request Mocking + +Intercept, mock, modify, and block network requests. + +## CLI Route Commands + +```bash +# Mock with custom status +playwright-cli route "**/*.jpg" --status=404 + +# Mock with JSON body +playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json + +# Mock with custom headers +playwright-cli route "**/api/data" --body='{"ok":true}' --header="X-Custom: value" + +# Remove headers from requests +playwright-cli route "**/*" --remove-header=cookie,authorization + +# List active routes +playwright-cli route-list + +# Remove a route or all routes +playwright-cli unroute "**/*.jpg" +playwright-cli unroute +``` + +## URL Patterns + +``` +**/api/users - Exact path match +**/api/*/details - Wildcard in path +**/*.{png,jpg,jpeg} - Match file extensions +**/search?q=* - Match query parameters +``` + +## Advanced Mocking with run-code + +For conditional responses, request body inspection, response modification, or delays: + +### Conditional Response Based on Request + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/login', route => { + const body = route.request().postDataJSON(); + if (body.username === 'admin') { + route.fulfill({ body: JSON.stringify({ token: 'mock-token' }) }); + } else { + route.fulfill({ status: 401, body: JSON.stringify({ error: 'Invalid' }) }); + } + }); +}" +``` + +### Modify Real Response + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/user', async route => { + const response = await route.fetch(); + const json = await response.json(); + json.isPremium = true; + await route.fulfill({ response, json }); + }); +}" +``` + +### Simulate Network Failures + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/offline', route => route.abort('internetdisconnected')); +}" +# Options: connectionrefused, timedout, connectionreset, internetdisconnected +``` + +### Delayed Response + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/slow', async route => { + await new Promise(r => setTimeout(r, 3000)); + route.fulfill({ body: JSON.stringify({ data: 'loaded' }) }); + }); +}" +``` diff --git a/.github/skills/playwright-cli/references/running-code.md b/.github/skills/playwright-cli/references/running-code.md new file mode 100644 index 0000000..7d6d22f --- /dev/null +++ b/.github/skills/playwright-cli/references/running-code.md @@ -0,0 +1,232 @@ +# Running Custom Playwright Code + +Use `run-code` to execute arbitrary Playwright code for advanced scenarios not covered by CLI commands. + +## Syntax + +```bash +playwright-cli run-code "async page => { + // Your Playwright code here + // Access page.context() for browser context operations +}" +``` + +## Geolocation + +```bash +# Grant geolocation permission and set location +playwright-cli run-code "async page => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); +}" + +# Set location to London +playwright-cli run-code "async page => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 }); +}" + +# Clear geolocation override +playwright-cli run-code "async page => { + await page.context().clearPermissions(); +}" +``` + +## Permissions + +```bash +# Grant multiple permissions +playwright-cli run-code "async page => { + await page.context().grantPermissions([ + 'geolocation', + 'notifications', + 'camera', + 'microphone' + ]); +}" + +# Grant permissions for specific origin +playwright-cli run-code "async page => { + await page.context().grantPermissions(['clipboard-read'], { + origin: 'https://example.com' + }); +}" +``` + +## Media Emulation + +```bash +# Emulate dark color scheme +playwright-cli run-code "async page => { + await page.emulateMedia({ colorScheme: 'dark' }); +}" + +# Emulate light color scheme +playwright-cli run-code "async page => { + await page.emulateMedia({ colorScheme: 'light' }); +}" + +# Emulate reduced motion +playwright-cli run-code "async page => { + await page.emulateMedia({ reducedMotion: 'reduce' }); +}" + +# Emulate print media +playwright-cli run-code "async page => { + await page.emulateMedia({ media: 'print' }); +}" +``` + +## Wait Strategies + +```bash +# Wait for network idle +playwright-cli run-code "async page => { + await page.waitForLoadState('networkidle'); +}" + +# Wait for specific element +playwright-cli run-code "async page => { + await page.waitForSelector('.loading', { state: 'hidden' }); +}" + +# Wait for function to return true +playwright-cli run-code "async page => { + await page.waitForFunction(() => window.appReady === true); +}" + +# Wait with timeout +playwright-cli run-code "async page => { + await page.waitForSelector('.result', { timeout: 10000 }); +}" +``` + +## Frames and Iframes + +```bash +# Work with iframe +playwright-cli run-code "async page => { + const frame = page.locator('iframe#my-iframe').contentFrame(); + await frame.locator('button').click(); +}" + +# Get all frames +playwright-cli run-code "async page => { + const frames = page.frames(); + return frames.map(f => f.url()); +}" +``` + +## File Downloads + +```bash +# Handle file download +playwright-cli run-code "async page => { + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.click('a.download-link') + ]); + await download.saveAs('./downloaded-file.pdf'); + return download.suggestedFilename(); +}" +``` + +## Clipboard + +```bash +# Read clipboard (requires permission) +playwright-cli run-code "async page => { + await page.context().grantPermissions(['clipboard-read']); + return await page.evaluate(() => navigator.clipboard.readText()); +}" + +# Write to clipboard +playwright-cli run-code "async page => { + await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!'); +}" +``` + +## Page Information + +```bash +# Get page title +playwright-cli run-code "async page => { + return await page.title(); +}" + +# Get current URL +playwright-cli run-code "async page => { + return page.url(); +}" + +# Get page content +playwright-cli run-code "async page => { + return await page.content(); +}" + +# Get viewport size +playwright-cli run-code "async page => { + return page.viewportSize(); +}" +``` + +## JavaScript Execution + +```bash +# Execute JavaScript and return result +playwright-cli run-code "async page => { + return await page.evaluate(() => { + return { + userAgent: navigator.userAgent, + language: navigator.language, + cookiesEnabled: navigator.cookieEnabled + }; + }); +}" + +# Pass arguments to evaluate +playwright-cli run-code "async page => { + const multiplier = 5; + return await page.evaluate(m => document.querySelectorAll('li').length * m, multiplier); +}" +``` + +## Error Handling + +```bash +# Try-catch in run-code +playwright-cli run-code "async page => { + try { + await page.click('.maybe-missing', { timeout: 1000 }); + return 'clicked'; + } catch (e) { + return 'element not found'; + } +}" +``` + +## Complex Workflows + +```bash +# Login and save state +playwright-cli run-code "async page => { + await page.goto('https://example.com/login'); + await page.fill('input[name=email]', 'user@example.com'); + await page.fill('input[name=password]', 'secret'); + await page.click('button[type=submit]'); + await page.waitForURL('**/dashboard'); + await page.context().storageState({ path: 'auth.json' }); + return 'Login successful'; +}" + +# Scrape data from multiple pages +playwright-cli run-code "async page => { + const results = []; + for (let i = 1; i <= 3; i++) { + await page.goto(\`https://example.com/page/\${i}\`); + const items = await page.locator('.item').allTextContents(); + results.push(...items); + } + return results; +}" +``` diff --git a/.github/skills/playwright-cli/references/session-management.md b/.github/skills/playwright-cli/references/session-management.md new file mode 100644 index 0000000..fac9606 --- /dev/null +++ b/.github/skills/playwright-cli/references/session-management.md @@ -0,0 +1,169 @@ +# Browser Session Management + +Run multiple isolated browser sessions concurrently with state persistence. + +## Named Browser Sessions + +Use `-s` flag to isolate browser contexts: + +```bash +# Browser 1: Authentication flow +playwright-cli -s=auth open https://app.example.com/login + +# Browser 2: Public browsing (separate cookies, storage) +playwright-cli -s=public open https://example.com + +# Commands are isolated by browser session +playwright-cli -s=auth fill e1 "user@example.com" +playwright-cli -s=public snapshot +``` + +## Browser Session Isolation Properties + +Each browser session has independent: +- Cookies +- LocalStorage / SessionStorage +- IndexedDB +- Cache +- Browsing history +- Open tabs + +## Browser Session Commands + +```bash +# List all browser sessions +playwright-cli list + +# Stop a browser session (close the browser) +playwright-cli close # stop the default browser +playwright-cli -s=mysession close # stop a named browser + +# Stop all browser sessions +playwright-cli close-all + +# Forcefully kill all daemon processes (for stale/zombie processes) +playwright-cli kill-all + +# Delete browser session user data (profile directory) +playwright-cli delete-data # delete default browser data +playwright-cli -s=mysession delete-data # delete named browser data +``` + +## Environment Variable + +Set a default browser session name via environment variable: + +```bash +export PLAYWRIGHT_CLI_SESSION="mysession" +playwright-cli open example.com # Uses "mysession" automatically +``` + +## Common Patterns + +### Concurrent Scraping + +```bash +#!/bin/bash +# Scrape multiple sites concurrently + +# Start all browsers +playwright-cli -s=site1 open https://site1.com & +playwright-cli -s=site2 open https://site2.com & +playwright-cli -s=site3 open https://site3.com & +wait + +# Take snapshots from each +playwright-cli -s=site1 snapshot +playwright-cli -s=site2 snapshot +playwright-cli -s=site3 snapshot + +# Cleanup +playwright-cli close-all +``` + +### A/B Testing Sessions + +```bash +# Test different user experiences +playwright-cli -s=variant-a open "https://app.com?variant=a" +playwright-cli -s=variant-b open "https://app.com?variant=b" + +# Compare +playwright-cli -s=variant-a screenshot +playwright-cli -s=variant-b screenshot +``` + +### Persistent Profile + +By default, browser profile is kept in memory only. Use `--persistent` flag on `open` to persist the browser profile to disk: + +```bash +# Use persistent profile (auto-generated location) +playwright-cli open https://example.com --persistent + +# Use persistent profile with custom directory +playwright-cli open https://example.com --profile=/path/to/profile +``` + +## Default Browser Session + +When `-s` is omitted, commands use the default browser session: + +```bash +# These use the same default browser session +playwright-cli open https://example.com +playwright-cli snapshot +playwright-cli close # Stops default browser +``` + +## Browser Session Configuration + +Configure a browser session with specific settings when opening: + +```bash +# Open with config file +playwright-cli open https://example.com --config=.playwright/my-cli.json + +# Open with specific browser +playwright-cli open https://example.com --browser=firefox + +# Open in headed mode +playwright-cli open https://example.com --headed + +# Open with persistent profile +playwright-cli open https://example.com --persistent +``` + +## Best Practices + +### 1. Name Browser Sessions Semantically + +```bash +# GOOD: Clear purpose +playwright-cli -s=github-auth open https://github.com +playwright-cli -s=docs-scrape open https://docs.example.com + +# AVOID: Generic names +playwright-cli -s=s1 open https://github.com +``` + +### 2. Always Clean Up + +```bash +# Stop browsers when done +playwright-cli -s=auth close +playwright-cli -s=scrape close + +# Or stop all at once +playwright-cli close-all + +# If browsers become unresponsive or zombie processes remain +playwright-cli kill-all +``` + +### 3. Delete Stale Browser Data + +```bash +# Remove old browser data to free disk space +playwright-cli -s=oldsession delete-data +``` diff --git a/.github/skills/playwright-cli/references/storage-state.md b/.github/skills/playwright-cli/references/storage-state.md new file mode 100644 index 0000000..c856db5 --- /dev/null +++ b/.github/skills/playwright-cli/references/storage-state.md @@ -0,0 +1,275 @@ +# Storage Management + +Manage cookies, localStorage, sessionStorage, and browser storage state. + +## Storage State + +Save and restore complete browser state including cookies and storage. + +### Save Storage State + +```bash +# Save to auto-generated filename (storage-state-{timestamp}.json) +playwright-cli state-save + +# Save to specific filename +playwright-cli state-save my-auth-state.json +``` + +### Restore Storage State + +```bash +# Load storage state from file +playwright-cli state-load my-auth-state.json + +# Reload page to apply cookies +playwright-cli open https://example.com +``` + +### Storage State File Format + +The saved file contains: + +```json +{ + "cookies": [ + { + "name": "session_id", + "value": "abc123", + "domain": "example.com", + "path": "/", + "expires": 1735689600, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "https://example.com", + "localStorage": [ + { "name": "theme", "value": "dark" }, + { "name": "user_id", "value": "12345" } + ] + } + ] +} +``` + +## Cookies + +### List All Cookies + +```bash +playwright-cli cookie-list +``` + +### Filter Cookies by Domain + +```bash +playwright-cli cookie-list --domain=example.com +``` + +### Filter Cookies by Path + +```bash +playwright-cli cookie-list --path=/api +``` + +### Get Specific Cookie + +```bash +playwright-cli cookie-get session_id +``` + +### Set a Cookie + +```bash +# Basic cookie +playwright-cli cookie-set session abc123 + +# Cookie with options +playwright-cli cookie-set session abc123 --domain=example.com --path=/ --httpOnly --secure --sameSite=Lax + +# Cookie with expiration (Unix timestamp) +playwright-cli cookie-set remember_me token123 --expires=1735689600 +``` + +### Delete a Cookie + +```bash +playwright-cli cookie-delete session_id +``` + +### Clear All Cookies + +```bash +playwright-cli cookie-clear +``` + +### Advanced: Multiple Cookies or Custom Options + +For complex scenarios like adding multiple cookies at once, use `run-code`: + +```bash +playwright-cli run-code "async page => { + await page.context().addCookies([ + { name: 'session_id', value: 'sess_abc123', domain: 'example.com', path: '/', httpOnly: true }, + { name: 'preferences', value: JSON.stringify({ theme: 'dark' }), domain: 'example.com', path: '/' } + ]); +}" +``` + +## Local Storage + +### List All localStorage Items + +```bash +playwright-cli localstorage-list +``` + +### Get Single Value + +```bash +playwright-cli localstorage-get token +``` + +### Set Value + +```bash +playwright-cli localstorage-set theme dark +``` + +### Set JSON Value + +```bash +playwright-cli localstorage-set user_settings '{"theme":"dark","language":"en"}' +``` + +### Delete Single Item + +```bash +playwright-cli localstorage-delete token +``` + +### Clear All localStorage + +```bash +playwright-cli localstorage-clear +``` + +### Advanced: Multiple Operations + +For complex scenarios like setting multiple values at once, use `run-code`: + +```bash +playwright-cli run-code "async page => { + await page.evaluate(() => { + localStorage.setItem('token', 'jwt_abc123'); + localStorage.setItem('user_id', '12345'); + localStorage.setItem('expires_at', Date.now() + 3600000); + }); +}" +``` + +## Session Storage + +### List All sessionStorage Items + +```bash +playwright-cli sessionstorage-list +``` + +### Get Single Value + +```bash +playwright-cli sessionstorage-get form_data +``` + +### Set Value + +```bash +playwright-cli sessionstorage-set step 3 +``` + +### Delete Single Item + +```bash +playwright-cli sessionstorage-delete step +``` + +### Clear sessionStorage + +```bash +playwright-cli sessionstorage-clear +``` + +## IndexedDB + +### List Databases + +```bash +playwright-cli run-code "async page => { + return await page.evaluate(async () => { + const databases = await indexedDB.databases(); + return databases; + }); +}" +``` + +### Delete Database + +```bash +playwright-cli run-code "async page => { + await page.evaluate(() => { + indexedDB.deleteDatabase('myDatabase'); + }); +}" +``` + +## Common Patterns + +### Authentication State Reuse + +```bash +# Step 1: Login and save state +playwright-cli open https://app.example.com/login +playwright-cli snapshot +playwright-cli fill e1 "user@example.com" +playwright-cli fill e2 "password123" +playwright-cli click e3 + +# Save the authenticated state +playwright-cli state-save auth.json + +# Step 2: Later, restore state and skip login +playwright-cli state-load auth.json +playwright-cli open https://app.example.com/dashboard +# Already logged in! +``` + +### Save and Restore Roundtrip + +```bash +# Set up authentication state +playwright-cli open https://example.com +playwright-cli eval "() => { document.cookie = 'session=abc123'; localStorage.setItem('user', 'john'); }" + +# Save state to file +playwright-cli state-save my-session.json + +# ... later, in a new session ... + +# Restore state +playwright-cli state-load my-session.json +playwright-cli open https://example.com +# Cookies and localStorage are restored! +``` + +## Security Notes + +- Never commit storage state files containing auth tokens +- Add `*.auth-state.json` to `.gitignore` +- Delete state files after automation completes +- Use environment variables for sensitive data +- By default, sessions run in-memory mode which is safer for sensitive operations diff --git a/.github/skills/playwright-cli/references/test-generation.md b/.github/skills/playwright-cli/references/test-generation.md new file mode 100644 index 0000000..7a09df3 --- /dev/null +++ b/.github/skills/playwright-cli/references/test-generation.md @@ -0,0 +1,88 @@ +# Test Generation + +Generate Playwright test code automatically as you interact with the browser. + +## How It Works + +Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code. +This code appears in the output and can be copied directly into your test files. + +## Example Workflow + +```bash +# Start a session +playwright-cli open https://example.com/login + +# Take a snapshot to see elements +playwright-cli snapshot +# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"] + +# Fill form fields - generates code automatically +playwright-cli fill e1 "user@example.com" +# Ran Playwright code: +# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); + +playwright-cli fill e2 "password123" +# Ran Playwright code: +# await page.getByRole('textbox', { name: 'Password' }).fill('password123'); + +playwright-cli click e3 +# Ran Playwright code: +# await page.getByRole('button', { name: 'Sign In' }).click(); +``` + +## Building a Test File + +Collect the generated code into a Playwright test: + +```typescript +import { test, expect } from '@playwright/test'; + +test('login flow', async ({ page }) => { + // Generated code from playwright-cli session: + await page.goto('https://example.com/login'); + await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); + await page.getByRole('textbox', { name: 'Password' }).fill('password123'); + await page.getByRole('button', { name: 'Sign In' }).click(); + + // Add assertions + await expect(page).toHaveURL(/.*dashboard/); +}); +``` + +## Best Practices + +### 1. Use Semantic Locators + +The generated code uses role-based locators when possible, which are more resilient: + +```typescript +// Generated (good - semantic) +await page.getByRole('button', { name: 'Submit' }).click(); + +// Avoid (fragile - CSS selectors) +await page.locator('#submit-btn').click(); +``` + +### 2. Explore Before Recording + +Take snapshots to understand the page structure before recording actions: + +```bash +playwright-cli open https://example.com +playwright-cli snapshot +# Review the element structure +playwright-cli click e5 +``` + +### 3. Add Assertions Manually + +Generated code captures actions but not assertions. Add expectations in your test: + +```typescript +// Generated action +await page.getByRole('button', { name: 'Submit' }).click(); + +// Manual assertion +await expect(page.getByText('Success')).toBeVisible(); +``` diff --git a/.github/skills/playwright-cli/references/tracing.md b/.github/skills/playwright-cli/references/tracing.md new file mode 100644 index 0000000..7ce7bab --- /dev/null +++ b/.github/skills/playwright-cli/references/tracing.md @@ -0,0 +1,139 @@ +# Tracing + +Capture detailed execution traces for debugging and analysis. Traces include DOM snapshots, screenshots, network activity, and console logs. + +## Basic Usage + +```bash +# Start trace recording +playwright-cli tracing-start + +# Perform actions +playwright-cli open https://example.com +playwright-cli click e1 +playwright-cli fill e2 "test" + +# Stop trace recording +playwright-cli tracing-stop +``` + +## Trace Output Files + +When you start tracing, Playwright creates a `traces/` directory with several files: + +### `trace-{timestamp}.trace` + +**Action log** - The main trace file containing: +- Every action performed (clicks, fills, navigations) +- DOM snapshots before and after each action +- Screenshots at each step +- Timing information +- Console messages +- Source locations + +### `trace-{timestamp}.network` + +**Network log** - Complete network activity: +- All HTTP requests and responses +- Request headers and bodies +- Response headers and bodies +- Timing (DNS, connect, TLS, TTFB, download) +- Resource sizes +- Failed requests and errors + +### `resources/` + +**Resources directory** - Cached resources: +- Images, fonts, stylesheets, scripts +- Response bodies for replay +- Assets needed to reconstruct page state + +## What Traces Capture + +| Category | Details | +|----------|---------| +| **Actions** | Clicks, fills, hovers, keyboard input, navigations | +| **DOM** | Full DOM snapshot before/after each action | +| **Screenshots** | Visual state at each step | +| **Network** | All requests, responses, headers, bodies, timing | +| **Console** | All console.log, warn, error messages | +| **Timing** | Precise timing for each operation | + +## Use Cases + +### Debugging Failed Actions + +```bash +playwright-cli tracing-start +playwright-cli open https://app.example.com + +# This click fails - why? +playwright-cli click e5 + +playwright-cli tracing-stop +# Open trace to see DOM state when click was attempted +``` + +### Analyzing Performance + +```bash +playwright-cli tracing-start +playwright-cli open https://slow-site.com +playwright-cli tracing-stop + +# View network waterfall to identify slow resources +``` + +### Capturing Evidence + +```bash +# Record a complete user flow for documentation +playwright-cli tracing-start + +playwright-cli open https://app.example.com/checkout +playwright-cli fill e1 "4111111111111111" +playwright-cli fill e2 "12/25" +playwright-cli fill e3 "123" +playwright-cli click e4 + +playwright-cli tracing-stop +# Trace shows exact sequence of events +``` + +## Trace vs Video vs Screenshot + +| Feature | Trace | Video | Screenshot | +|---------|-------|-------|------------| +| **Format** | .trace file | .webm video | .png/.jpeg image | +| **DOM inspection** | Yes | No | No | +| **Network details** | Yes | No | No | +| **Step-by-step replay** | Yes | Continuous | Single frame | +| **File size** | Medium | Large | Small | +| **Best for** | Debugging | Demos | Quick capture | + +## Best Practices + +### 1. Start Tracing Before the Problem + +```bash +# Trace the entire flow, not just the failing step +playwright-cli tracing-start +playwright-cli open https://example.com +# ... all steps leading to the issue ... +playwright-cli tracing-stop +``` + +### 2. Clean Up Old Traces + +Traces can consume significant disk space: + +```bash +# Remove traces older than 7 days +find .playwright-cli/traces -mtime +7 -delete +``` + +## Limitations + +- Traces add overhead to automation +- Large traces can consume significant disk space +- Some dynamic content may not replay perfectly diff --git a/.github/skills/playwright-cli/references/video-recording.md b/.github/skills/playwright-cli/references/video-recording.md new file mode 100644 index 0000000..38391b3 --- /dev/null +++ b/.github/skills/playwright-cli/references/video-recording.md @@ -0,0 +1,43 @@ +# Video Recording + +Capture browser automation sessions as video for debugging, documentation, or verification. Produces WebM (VP8/VP9 codec). + +## Basic Recording + +```bash +# Start recording +playwright-cli video-start + +# Perform actions +playwright-cli open https://example.com +playwright-cli snapshot +playwright-cli click e1 +playwright-cli fill e2 "test input" + +# Stop and save +playwright-cli video-stop demo.webm +``` + +## Best Practices + +### 1. Use Descriptive Filenames + +```bash +# Include context in filename +playwright-cli video-stop recordings/login-flow-2024-01-15.webm +playwright-cli video-stop recordings/checkout-test-run-42.webm +``` + +## Tracing vs Video + +| Feature | Video | Tracing | +|---------|-------|---------| +| Output | WebM file | Trace file (viewable in Trace Viewer) | +| Shows | Visual recording | DOM snapshots, network, console, actions | +| Use case | Demos, documentation | Debugging, analysis | +| Size | Larger | Smaller | + +## Limitations + +- Recording adds slight overhead to automation +- Large recordings can consume significant disk space diff --git a/.github/skills/test-generation/references/templates.md b/.github/skills/test-generation/references/templates.md index 94bd882..bfcbde5 100644 --- a/.github/skills/test-generation/references/templates.md +++ b/.github/skills/test-generation/references/templates.md @@ -97,7 +97,7 @@ The hooks capture a screenshot after **every** Gherkin step. These screenshots a - Config: `e2e/playwright.config.ts` — baseURL defaults to `http://localhost:3001` (Aspire web port) - Page Objects: `e2e/pages/{page-name}.page.ts` — one class per page, derived from UI prototypes -- Web server: Playwright config auto-starts `aspire run` for local runs +- Web server: Playwright config auto-starts `aspire start` + `aspire wait` for local runs - Do NOT create or modify e2e specs or POMs — they are Phase 3 artifacts --- diff --git a/.github/skills/test-runner/SKILL.md b/.github/skills/test-runner/SKILL.md index 66a2e4b..614f92f 100644 --- a/.github/skills/test-runner/SKILL.md +++ b/.github/skills/test-runner/SKILL.md @@ -19,11 +19,20 @@ Execute tests and return structured results for the orchestrator. ## Steps -1. **Determine test type** — Select the test suite based on current phase and task -2. **Run tests** — Execute the command, capture stdout and stderr -3. **Parse results** — Extract pass/fail counts, failure details, and test names -4. **Detect flaky tests** — If a test failed, re-run it once; if it passes on retry, flag as flaky -5. **Structure output** — Format results for the orchestrator +1. **Verify Aspire environment** (for Gherkin/e2e/smoke/all) — Before running integration tests, ensure the Aspire environment is running: + - Use `aspire describe --format Json` (or the Aspire MCP `list_resources` tool) to check resource status + - If resources are not healthy, run `aspire start` + `aspire wait api --status healthy` + `aspire wait web --status healthy` + - If resources show errors, use `aspire logs ` (or `list_console_logs`) to diagnose +2. **Determine test type** — Select the test suite based on current phase and task +3. **Run tests** — Execute the command, capture stdout and stderr +4. **Parse results** — Extract pass/fail counts, failure details, and test names +5. **Detect flaky tests** — If a test failed, re-run it once; if it passes on retry, flag as flaky +6. **Diagnose failures with Aspire MCP** — On test failure, use Aspire observability: + - `list_console_logs` for resource stdout/stderr around failure time + - `list_structured_logs` for OpenTelemetry log entries + - `list_traces` to find the failing request's distributed trace + - `list_trace_structured_logs` with the trace ID for full request lifecycle +7. **Structure output** — Format results for the orchestrator ## Output Format @@ -38,8 +47,21 @@ Failed tests: - : ``` +## Aspire MCP Debugging Workflow + +When tests fail against the Aspire environment: + +``` +1. list_resources → Are all resources Running + Healthy? +2. list_console_logs(resource) → Any errors in stdout/stderr? +3. list_traces(resource) → Find the trace for the failing request +4. list_trace_structured_logs → Full trace lifecycle with all spans +5. execute_resource_command → Restart a resource if stuck +``` + ## Edge Cases - Test runner itself fails (not assertions) → report as infrastructure failure +- Aspire resources unhealthy → restart via `aspire start` (auto-stops previous) - Tests exceed 5 minutes → check for hung processes - Always capture both stdout and stderr diff --git a/.mcp.json b/.mcp.json index db9dc17..361a817 100644 --- a/.mcp.json +++ b/.mcp.json @@ -30,14 +30,8 @@ "type": "local", "command": "aspire", "args": [ - "mcp", - "start" - ], - "env": { - "DOTNET_ROOT": "${DOTNET_ROOT}" - }, - "tools": [ - "*" + "agent", + "mcp" ] } } diff --git a/.playwright/cli.config.json b/.playwright/cli.config.json new file mode 100644 index 0000000..b93a2ca --- /dev/null +++ b/.playwright/cli.config.json @@ -0,0 +1,8 @@ +{ + "browser": { + "browserName": "chromium", + "launchOptions": { + "channel": "msedge" + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 95f4fd8..8218d67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -536,8 +536,8 @@ infra/ # Azure Bicep templates |---|---|---| | Unit tests (API) | `cd src/api && npm test` | Vitest + Supertest, runs all API tests | | Unit tests (API, watch) | `cd src/api && npm run test:watch` | Re-runs on file changes | -| Cucumber/Gherkin | `npx cucumber-js` | Runs against Aspire environment (auto-started by hooks) | -| Playwright e2e | `npx playwright test --config=e2e/playwright.config.ts` | Runs against Aspire environment (auto-started by webServer config) | +| Cucumber/Gherkin | `npx cucumber-js` | Runs against Aspire environment (auto-started by hooks via `aspire start` + `aspire wait`) | +| Playwright e2e | `npx playwright test --config=e2e/playwright.config.ts` | Runs against Aspire environment (auto-started by webServer config via `aspire start` + `aspire wait`) | | Playwright specific | `npx playwright test e2e/{feature}.spec.ts` | Single feature e2e against Aspire | | Playwright smoke | `npx playwright test --grep @smoke` | Smoke tests only | | Playwright UI mode | `npx playwright test --ui` | Interactive debugging | @@ -547,10 +547,16 @@ infra/ # Azure Bicep templates | Service | Command | URL | |---|---|---| -| **Aspire (all services)** | `aspire run` | Web: http://localhost:3001, API: http://localhost:5001 | +| **Aspire (all services)** | `aspire start` | Web: http://localhost:3001, API: http://localhost:5001 | +| Wait for healthy | `aspire wait api --status healthy` | Blocks until API is ready | +| Describe resources | `aspire describe` | Show resource status, health, endpoints | +| View logs | `aspire logs ` | Stream console logs for a resource | +| Stop Aspire | `aspire stop` | Clean shutdown of all resources | | Frontend (standalone) | `cd src/web && npm run dev` | http://localhost:3000 | | Backend (standalone) | `cd src/api && npm run dev` | http://localhost:5001 (dev) / 8080 (container) | +> **Always use `aspire start`** (background, non-blocking) — never `aspire run` (interactive, blocking). Use `aspire wait --status healthy` to block until a resource is ready. Use `aspire describe` to inspect resource status. The Aspire MCP server provides `list_resources`, `list_console_logs`, `list_traces` for runtime observability. + > **Prefer Aspire** for all integration, Cucumber, and e2e testing. Standalone dev servers are only for isolated slice work (API-only or Web-only development). ### Build Commands diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index b4e2cfc..14fc03d 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ }, ], webServer: process.env.PLAYWRIGHT_BASE_URL ? undefined : { - command: 'aspire run --project ../apphost.cs', + command: 'aspire start --nologo && aspire wait web --status healthy --timeout 90 --nologo', url: 'http://localhost:3001', reuseExistingServer: !process.env.CI, timeout: 120000, diff --git a/package.json b/package.json index 16f3212..27e5e4c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dev:api": "cd src/api && npm run dev", "dev:docs": "mkdocs serve --dev-addr 0.0.0.0:8000", "dev:all": "npx concurrently \"npm run dev:api\" \"npm run dev\" \"npm run dev:docs\"", - "dev:aspire": "aspire run", + "dev:aspire": "aspire start", "build": "cd src/web && npm run build", "build:api": "cd src/api && npm run build", "build:all": "npm run build:api && npm run build", diff --git a/tests/features/support/hooks.ts b/tests/features/support/hooks.ts index a22746d..9aca00b 100644 --- a/tests/features/support/hooks.ts +++ b/tests/features/support/hooks.ts @@ -2,17 +2,16 @@ import { Before, After, AfterStep, BeforeAll, AfterAll, setDefaultTimeout } from import { CustomWorld } from './world'; import * as fs from 'fs'; import * as path from 'path'; -import { spawn, ChildProcess } from 'child_process'; +import { execSync, ChildProcess } from 'child_process'; setDefaultTimeout(30_000); const SCREENSHOT_BASE_DIR = path.resolve(process.cwd(), 'docs', 'screenshots'); const GENERATE_SCREENSHOTS = process.env.GENERATE_SCREENSHOTS === 'true'; -const WEB_URL = process.env.WEB_URL || 'http://localhost:3000'; +const WEB_URL = process.env.WEB_URL || 'http://localhost:3001'; const API_URL = process.env.API_URL || 'http://localhost:5001'; -let apiProcess: ChildProcess | null = null; -let webProcess: ChildProcess | null = null; +let aspireStarted = false; async function isServerRunning(url: string): Promise { try { @@ -23,40 +22,46 @@ async function isServerRunning(url: string): Promise { } } -async function waitForServer(url: string, timeout: number): Promise { - const start = Date.now(); - while (Date.now() - start < timeout) { - if (await isServerRunning(url)) return; - await new Promise(r => setTimeout(r, 500)); +function isAspireRunning(): boolean { + try { + const output = execSync('aspire ps --format Json --nologo 2>/dev/null', { encoding: 'utf-8' }); + const parsed = JSON.parse(output); + return Array.isArray(parsed) && parsed.length > 0; + } catch { + return false; } - throw new Error(`Server at ${url} did not start within ${timeout}ms`); } BeforeAll(async function () { fs.mkdirSync(SCREENSHOT_BASE_DIR, { recursive: true }); - // Start API server if not already running + // Use Aspire to orchestrate all services (API + Web) if (!(await isServerRunning(`${API_URL}/health`))) { - apiProcess = spawn('npm', ['run', 'dev'], { - cwd: path.resolve(process.cwd(), 'src/api'), - stdio: 'pipe', - detached: true, - shell: true, - env: { ...process.env, JWT_SECRET: process.env.JWT_SECRET || 'test-jwt-secret-for-cucumber' }, - }); - await waitForServer(`${API_URL}/health`, 30000); - } + if (!isAspireRunning()) { + console.log('Starting Aspire AppHost...'); + execSync('aspire start --nologo', { + cwd: process.cwd(), + stdio: 'inherit', + timeout: 60000, + }); + aspireStarted = true; + } - // Start web server if generating screenshots and not already running - if (GENERATE_SCREENSHOTS && !(await isServerRunning(WEB_URL))) { - webProcess = spawn('npm', ['run', 'dev'], { - cwd: path.resolve(process.cwd(), 'src/web'), - stdio: 'pipe', - detached: true, - shell: true, - env: { ...process.env, PORT: '3000' }, + // Wait for API to be healthy + console.log('Waiting for API to be healthy...'); + execSync('aspire wait api --status healthy --timeout 60 --nologo', { + stdio: 'inherit', + timeout: 70000, }); - await waitForServer(WEB_URL, 60000); + + // Wait for Web to be healthy when generating screenshots or running @ui tests + if (GENERATE_SCREENSHOTS) { + console.log('Waiting for Web to be healthy...'); + execSync('aspire wait web --status healthy --timeout 60 --nologo', { + stdio: 'inherit', + timeout: 70000, + }); + } } }); @@ -132,12 +137,9 @@ After(async function (this: CustomWorld, { result }) { }); AfterAll(async function () { - if (apiProcess && apiProcess.pid) { - try { process.kill(-apiProcess.pid, 'SIGTERM'); } catch { /* already stopped */ } - apiProcess = null; - } - if (webProcess && webProcess.pid) { - try { process.kill(-webProcess.pid, 'SIGTERM'); } catch { /* already stopped */ } - webProcess = null; + // Stop Aspire if we started it + if (aspireStarted) { + try { execSync('aspire stop --nologo', { stdio: 'inherit', timeout: 15000 }); } catch { /* already stopped */ } + aspireStarted = false; } });