Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,38 @@ jobs:

- name: Run build
run: npm run build

# Playwright end-to-end test of the core loop (NEXT.md step 0.1, #79).
# The chat route is stubbed in the browser, so no provider key is needed.
e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NUXT_TELEMETRY_DISABLED: 1
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium

- name: Run e2e tests
run: npm run test:e2e

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
retention-days: 7
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"generate": "nuxt generate",
"preview": "nuxt preview",
"typecheck": "nuxt typecheck",
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.superpowers\" \"#docs/superpowers\"",
"test:e2e": "tsc -p tests/e2e && playwright test",
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.superpowers\" \"#docs/superpowers\" \"#tests/e2e/test-results\" \"#tests/e2e/playwright-report\"",
"prepare": "husky"
},
"engines": {
Expand All @@ -20,6 +21,7 @@
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.11.2",
"@playwright/test": "^1.63.0",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^22.20.2",
"@vueuse/core": "^13.9.0",
Expand Down
48 changes: 48 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { defineConfig, devices } from '@playwright/test'

// End-to-end tests run against a production build (`nuxt build`, then the
// Nitro server), not `nuxt dev`: no dependency pre-bundling reloads, and the
// same output that gets deployed. See tests/e2e/README.md.
const PORT = Number(process.env.E2E_PORT ?? 3179)
const baseURL = `http://127.0.0.1:${PORT}`
const CI = !!process.env.CI

export default defineConfig({
testDir: './tests/e2e',
outputDir: './tests/e2e/test-results',
fullyParallel: true,
forbidOnly: CI,
retries: CI ? 1 : 0,
timeout: 60_000,
expect: { timeout: 10_000 },
reporter: [
[CI ? 'github' : 'list'],
['html', { open: 'never', outputFolder: './tests/e2e/playwright-report' }],
],
use: {
baseURL,
actionTimeout: 10_000,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: 'npm run build && node .output/server/index.mjs',
url: baseURL,
reuseExistingServer: !CI,
timeout: 180_000,
env: {
NITRO_HOST: '127.0.0.1',
NITRO_PORT: String(PORT),
NUXT_TELEMETRY_DISABLED: '1',
// The tests answer /api/chat in the browser. Blank provider keys mean a
// request that ever reaches this server fails with 401 instead of
// calling a real model with a key from the shell.
OPENAI_API_KEY: '',
ANTHROPIC_API_KEY: '',
GEMINI_API_KEY: '',
GOOGLE_GENERATIVE_AI_API_KEY: '',
DEEPSEEK_API_KEY: '',
},
},
})
2 changes: 2 additions & 0 deletions tests/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test-results/
playwright-report/
92 changes: 92 additions & 0 deletions tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
type: playbook
title: End-to-end tests
description: How to run the Playwright test of the core loop, and how its stubbed agent works.
tags: [tests, e2e, playwright, ci]
generated: { by: claude-code/claude-opus-5.5, at: 2026-10-03T00:34:00Z }
---

# End-to-end tests

A Playwright test of the core loop from [NEXT.md](../../NEXT.md) step
0.1: install a fixture app, reload, update it, break it, and ask the
agent to fix it. NEXT.md makes this suite the definition of "still
works", and CI runs it on pull requests and on pushes to `main`.

## Run it

```bash
npm ci
npx playwright install chromium # once per Playwright version
npm run test:e2e
```

`test:e2e` type-checks `tests/e2e` and then runs Playwright, which builds
the app (`nuxt build`) and serves the production output on
`http://127.0.0.1:3179`. Set `E2E_PORT` to use another port. Outside CI,
a server that is already listening on that port is reused instead of
rebuilt, so stop any server you started by hand after changing app code.

Other ways to run it:

```bash
npm run test:e2e -- --headed # watch it in a browser window
npm run test:e2e -- --ui # Playwright UI mode
npx playwright show-report tests/e2e/playwright-report
```

Failure artifacts (traces, screenshots, error context) go to
`tests/e2e/test-results/` and the HTML report to
`tests/e2e/playwright-report/`. Both are gitignored. In CI they are
uploaded as the `playwright-report` artifact when the job fails.

## What it covers

| Step | How | Asserts |
| --- | --- | --- |
| Install | `window.__dominic.installFixture('pomodoro-timer')` | the timer renders |
| Persist | reload, then the taskbar's **Open Pomodoro Timer** | the app comes back from the virtual filesystem |
| Update | `window.__dominic.updateApp(...)` | the new source renders in the same window |
| Break | `window.__dominic.injectBrokenApp('compile')` | the error card and **Ask Agent to Fix** appear |
| Recover | click **Ask Agent to Fix** | the request carries the app's id and source; the `update_app` reply mounts a working app in place |

A second test runs Break and Recover with `injectBrokenApp('runtime')`,
which goes through the runner's `onErrorCaptured` boundary instead of
the compile path.

## The stubbed agent

No model provider or API key is involved, locally or in CI.
`support/agent-stub.ts` answers `POST /api/chat` inside the browser with
`page.route`, in the AI SDK v4 data-stream format that the server's
`toDataStreamResponse()` sends:

```text
f:{"messageId":"msg-e2e"}
0:"Found the bug; sending a fixed version."
9:{"toolCallId":"call-e2e","toolName":"update_app","args":{...}}
e:{"finishReason":"tool-calls","usage":{...},"isContinued":false}
d:{"finishReason":"tool-calls","usage":{...}}
```

Every line ends in a newline, because the client parser in
`useAgentChat.ts` ignores a last line that has none. The lines are
built with `formatDataStreamPart` from the `ai` package, so they follow
the SDK version the server uses. If the chat route changes its stream
format, change the stub in the same pull request.

Two guards keep the suite self-contained:

- Every request that leaves the app's origin is aborted. Today that is
only the Tailwind Play CDN script; the shell's own styles are built in.
- The test server starts with blank provider keys, so a request that
ever got past the stub fails with 401 instead of calling a real model.

## Adding a test

- Start with `openDesktop(page)` and drive the desktop through
`dominicHooks(page)` and visible controls.
- Find windows with `appWindow(page, title)`.
- Use web-first assertions (`await expect(locator)...`), not sleeps.
- Each test gets a fresh browser context, so the virtual filesystem
(`localStorage`) starts empty.
Loading
Loading