test(web): Playwright e2e harness with auth-gate and dashboard specs#76
Conversation
Closes #74 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a Playwright-based e2e test harness for @gsd/web (tier-1 stubs-only) that runs against the production Vite build/preview, plus CI wiring and docs describing how to run the suite.
Changes:
- Add root and workspace scripts to run Playwright e2e tests (
npm run app:test:e2e→@gsd/web). - Introduce Playwright config, fixtures/stubs, and initial specs (auth gate + dashboard + sidebar navigation).
- Add CI workflow to run e2e on PRs and upload Playwright HTML report artifacts on failure; document e2e usage and the two-tier testing strategy.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds root app:test:e2e script targeting the web workspace. |
| package-lock.json | Adds Playwright dependencies and lockfile updates to support the new harness. |
| docs/docs/components/management-app.md | Documents how to run the Playwright e2e suite locally/CI. |
| app/packages/web/playwright.config.ts | Playwright runner configuration (testDir, webServer via Vite build/preview, reporters). |
| app/packages/web/package.json | Adds test:e2e script and Playwright dev dependency. |
| app/packages/web/e2e/specs/dashboard.spec.ts | Dashboard/Start button/sidebar navigation e2e specs. |
| app/packages/web/e2e/specs/auth-gate.spec.ts | Auth-gate e2e specs covering 401 → modal and token persistence. |
| app/packages/web/e2e/fixtures/index.ts | authedPage fixture + stubApis() network stubbing helper. |
| app/packages/web/e2e/fixtures/game-data.ts | Shared fixture payloads for env/status/costs. |
| CLAUDE.md | Documents the two-tier browser testing strategy and Playwright conventions. |
| .gitignore | Ignores Playwright reports/results/cache directories. |
| .github/workflows/e2e.yml | New CI job to run e2e tests and upload Playwright reports on failure. |
| test('should navigate to the Settings page via sidebar', async ({ authedPage: page }) => { | ||
| await stubApis(page, { statuses: [] }); | ||
| await page.goto('/'); | ||
|
|
||
| await page.getByRole('link', { name: 'Settings' }).click(); | ||
| await expect(page).toHaveURL('/settings'); | ||
| }); |
There was a problem hiding this comment.
Fixed in 1cd9f87 — added an /api/config stub (handling both GET and POST) directly in stubApis() so the /settings route loads cleanly. Also added a WATCHDOG_CONFIG fixture and a config?: WatchdogConfig override on StubOptions for parity with the other stubs.
Generated by Claude Code
- Reverse stubApis() route registration: register catch-all 404 first, specific stubs after. Playwright matches handlers in reverse registration order, so the previous order made the catch-all intercept every /api/* request and the dashboard never received data — the cause of the failing 'should render a game card' specs. Apply the same fix to the inline route handlers in auth-gate.spec. - Add /api/config stub to stubApis() so navigating to /settings does not trigger an unhandled rejection in WatchdogPanel (Copilot review). - Add permissions: contents: read to .github/workflows/e2e.yml (CodeQL / GHAS finding). - Exclude packages/web/e2e/** from React/react-hooks ESLint rules so the Playwright fixture pattern (await use(page)) doesn't trip rules-of-hooks; remove the unused 'expect' import and add the missing JSDoc for StubOptions.
|
@copilot review this again please |
Reviewed — everything looks clean now. The fixes in
All CI jobs pass (Lint, Test, E2E, Docs). No remaining issues. |
The Playwright harness from #76 hard-coded "Online"/"Offline" badges and both Start+Stop buttons being present on every card — both assumptions broke when #60 swapped to RUNNING/STOPPED labels and a single-state primary action. - ACTUAL_COSTS fixture + /api/costs/actual stub so the new KPI strip's sparkline fetch resolves without falling through the catch-all 404. - Dashboard specs updated to look up cards by their <h3> game-name heading (avoids cross-matching the sparkline / sidebar text), assert STOPPED/RUNNING badges, and handle the single-CTA pattern by checking Start has count 0 while a game is running. - New specs: search-filter narrows the grid, empty-state on no match, and KPI strip renders the four ops tiles with the live X/Y count. - GameCard: drop the redundant body connect-string row — hostname now lives only in the header next to the copy button so getByText() finds a single match per card.
Closes #60 ## Summary - New `KpiStrip` at the top of `/` — four ops tiles (Servers running, Spend today, Forecast MTD, Active alerts) each with a top color accent rule and a 7-bar sparkline driven by `/api/status` + `/api/costs/estimate` + `/api/costs/actual`. - `GameCard` redesigned: gradient top-accent rule keyed to state, Outfit-bold game name above DM Mono hostname (with copy button), status badge with icon + pulsing/animated dot covering all 5 states (RUNNING / STARTING / STOPPED / NOT_DEPLOYED / ERROR), 2×2 stats grid (Last run / Players / $/hr / Task short-id), Start-or-Stop gradient primary + Files/Logs secondary actions. - `DashboardPage` adds a client-side search input (filters by game name or hostname) and switches to a responsive 3 → 2 → 1 column grid. - e2e specs from #76 realigned with the new design — STOPPED/RUNNING badges, single-CTA pattern (`Start.toHaveCount(0)` while running) — plus new specs covering search filter, empty-state on no matches, and KPI tile rendering. - `CLAUDE.md` documents the `issue-flow` plugin as the canonical issue → PR driver, complementing the existing `/pr` command. ## Verification — acceptance criteria mapped to landing | AC | Where it lives | |---|---| | KPI strip renders 4 tiles with live data + sparkline | `app/packages/web/src/components/KpiStrip.tsx` | | GameCard uses shadcn Card / Button / Badge from #58 | `GameCard.tsx` (Card root, Button variants `start`/`stop`/`secondary`/`ghost`, state Badge) | | Status badge shows icon + text on all 5 states | `STATE_LABELS` + `StateIcon` in `GameCard.tsx` | | Search filter narrows grid in real time | `DashboardPage.tsx` `useMemo` filter on game name + hostname | | Start, Stop, Files, Logs reachable from new card | `GameCard.tsx` action row; Logs links to `/logs` (the route placeholder lands fully in #63) | | `npm run app:test` passes; existing tests updated, not deleted | 258 tests pass; no prior GameCard tests existed | ## Implementation notes - The redesigned card swaps the primary CTA based on state instead of disabling the inactive one — cleaner UX, but it required updating the e2e spec from `Start.toBeDisabled()` to `Start.toHaveCount(0)` for running games. - KPI sparkline data is currently a single shared `/api/costs/actual` series across all four tiles (cost itself for the cost tiles; same series as a coarse activity proxy for Servers running; flat zeros for Active alerts when count is 0). Per-tile historical series would need new endpoints. - Forecast MTD uses average-daily-spend × calendar-days-in-month — simple extrapolation, no Cost Explorer Forecast API. - The `Logs` per-card button is `<Link to="/logs">` (asChild). The `/logs` page is a placeholder until #63 lands — the link exists so the action is "reachable" per the AC. ## Test plan - [ ] Local: `npm run app:dev`, browse to `/`, verify the four KPI tiles render with live values and a 7-bar sparkline beneath each. - [ ] Type a partial game name or hostname into the search input — grid narrows in real time; clear it — grid restores. - [ ] Start a stopped game from a card — primary CTA swaps to Stop after the 3s refresh; click Files — modal opens; click Logs — navigates to `/logs`. - [ ] CI: `npm run app:test` (vitest, 258 tests) and `npm run app:test:e2e` (Playwright via `.github/workflows/e2e.yml`) both green. --- _Generated by [Claude Code](https://claude.ai/code/session_01TiPfpb2vvkRMMXqc9oX5d2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Closes #74
Summary
Adds a Playwright e2e harness for
@gsd/webthat runs against the production build (vite build+vite preview) with every/api/*call stubbed at the network layer. The Nest server is never started — this is the stubs-only tier (#74); the full-stack tier with a real Nest server lands separately in #75.npm run app:test:e2e(root) →playwright testin@gsd/web..github/workflows/e2e.yml) installs the Chromium browser with system deps, runs the suite on every PR, and uploadsplaywright-report/(traces + videos) as an artifact on failure.app/packages/web/e2e/fixtures/; specs undere2e/specs/.authedPagePlaywright fixture pre-seedslocalStorage.apiTokenso most specs start authenticated; auth-gate specs use the basetestto exercise the 401-trigger path.page.route('**/api/**')returns 404 for any endpoint not explicitly stubbed so missing stubs surface as fast test failures rather than hangs.Spec coverage delivered with this PR
Auth gate (
auth-gate.spec.ts):should show token modal when API returns 401should load dashboard when valid token is already storedshould save token and show dashboard after reloadDashboard (
dashboard.spec.ts):should render a game card for a stopped game/for a running game with IPshould render multiple game cardsshould show empty-state message when no games are configuredshould fire POST /api/start/:game when Start is clickedshould disable Start button for a running game/logs,/discord,/settingsPer-surface specs (Costs panel, Stop confirmation, polling indicator, etc.) follow with their respective UI issues (#60–#68) — the harness is now in place to plug them in.
Acceptance criteria from #74
npm run app:test:e2eruns the suite locally and in CI against the production build.docs/docs/components/management-app.mdgains a "Running e2e tests" section.npm run app:testcontinues to pass; e2e suite runs as a separate command.CLAUDE.md's "Code & Test Conventions" section now describes the two-tier strategy (test(web): integration / e2e test harness for the dashboard #74 stubs vs test: full-stack integration test suite (real Nest server + mocked AWS SDK) #75 real Nest) and when to add specs to each tier.Implementation notes
npm run build && npm run preview; Playwright polls untilhttp://localhost:4173responds.reuseExistingServeris true locally so re-runs are fast ifvite previewis already running.app/packages/web/test-results/,playwright-report/, andplaywright/.cache/to.gitignoreso local artifacts don't get committed.Test plan
sudo npx playwright install-deps chromiumonce, thennpm run app:test:e2efrom repo root passes.npm run app:teststill passes (the unit-test suite is untouched).app/packages/web/playwright-report/index.htmlafter a local run to inspect traces/videos.