build(web): add project-local Playwright tooling

- build: pin Playwright Test and MCP with repository-local npm, browser, and artifact paths plus sandboxed headless Chromium configuration.
- test: add a browser smoke test for the login page while keeping Playwright isolated from Vitest.
- docs: document Debian dependency setup, dual Chromium revisions, E2E commands, and project-scoped Codex MCP usage.
This commit is contained in:
2026-07-15 20:17:24 +08:00
parent 04eb8727eb
commit 29b176d2db
10 changed files with 285 additions and 1 deletions
+22
View File
@@ -0,0 +1,22 @@
[mcp_servers.playwright]
command = "mise"
args = [
"exec",
"--",
"node",
"node_modules/@playwright/mcp/cli.js",
"--headless",
"--browser",
"chromium",
"--sandbox",
"--isolated",
"--output-dir",
"../.artifacts/playwright-mcp",
"--output-max-size",
"104857600",
]
cwd = "web"
enabled = true
required = false
startup_timeout_sec = 30
tool_timeout_sec = 120
+4
View File
@@ -34,6 +34,10 @@ go.work.sum
# Mise # Mise
mise.local.toml mise.local.toml
# Project-local development caches and browser artifacts
/.cache/
/.artifacts/
# MyGO configuration # MyGO configuration
# Tracking config.example.yaml in version control # Tracking config.example.yaml in version control
config.yaml config.yaml
+26
View File
@@ -43,3 +43,29 @@ account, admin, or large-transfer designs.
transfer policy in MyGO code. transfer policy in MyGO code.
- Handwritten TypeScript wire types remain temporary until an OpenAPI contract - Handwritten TypeScript wire types remain temporary until an OpenAPI contract
is available. is available.
## 2026-07-14: Project-local Headless Browser Tooling
**Context**: Browser behavior needs deterministic E2E coverage and interactive
agent debugging inside a long-lived headless Debian Incus container. Tooling
should remain reproducible without placing browser binaries or generated
artifacts in a developer's home directory.
**Decisions**:
| Area | Choice | Guidance |
|------|--------|----------|
| Regression tests | Playwright Test with bundled Chromium | Keep browser E2E tests separate from `npm run check` because browser installation is an explicit environment setup step. |
| Agent debugging | Official Playwright MCP over project-scoped STDIO | Codex starts the locked local package from `.codex/config.toml`; do not use a global install, an `@latest` npx invocation, or a listening MCP service. |
| Browser state | Headless, isolated, and sandboxed | Use bundled Chromium, discard the MCP profile after each session, and retain the Chromium sandbox supported by the Incus environment. |
| Local resources | Repository `.cache/` and `.artifacts/` directories | `mise.toml` defines the portable npm and browser cache paths. Generated resources remain ignored by Git. |
| Browser versions | Install both locked Playwright revisions | The stable test runner and current MCP package require different Chromium revisions; do not force either package onto an unsupported executable. |
**Consequences**:
- Debian browser libraries are installed once in the Incus container root
filesystem; npm packages, browsers, traces, screenshots, and MCP output stay
project-scoped.
- A fresh environment runs `npm ci`, the system dependency installer, and both
browser install scripts before browser tests or MCP debugging.
- Browser failures can retain traces, screenshots, and video under
`.artifacts/playwright/` without adding generated files to source control.
+2
View File
@@ -36,6 +36,8 @@ The project foundation contains:
- Ant Design icons for application and file actions - Ant Design icons for application and file actions
- Oxlint from the Vite template - Oxlint from the Vite template
- Vitest for framework-independent client and session tests - Vitest for framework-independent client and session tests
- Playwright Test with a headless Chromium smoke test
- Project-scoped Playwright MCP for interactive browser debugging
Ant Design owns reusable UI components and theme tokens. Tailwind CSS is initially limited to application layout, spacing, and responsive utilities. Ant Design owns reusable UI components and theme tokens. Tailwind CSS is initially limited to application layout, spacing, and responsive utilities.
+4
View File
@@ -1,3 +1,7 @@
[tools] [tools]
go = "1.26.2" go = "1.26.2"
node = "24" node = "24"
[env]
NPM_CONFIG_CACHE = "{{config_root}}/.cache/npm"
PLAYWRIGHT_BROWSERS_PATH = "{{config_root}}/.cache/ms-playwright"
+37 -1
View File
@@ -36,12 +36,48 @@ Vite proxies `/api` to `http://127.0.0.1:10086`. Production deployments must
serve the static build and `/api/v1` from the same origin, normally through a serve the static build and `/api/v1` from the same origin, normally through a
reverse proxy. reverse proxy.
## Browser Testing and Debugging
The repository uses Playwright Test for repeatable browser tests and the
official Playwright MCP server for interactive browser debugging. Mise keeps
the npm cache and Playwright browser binaries under the repository-level
`.cache/` directory.
Install the Debian browser dependencies once per development container:
```bash
mise exec -- npm run playwright:install:deps
```
Install the Chromium revisions used by Playwright Test and Playwright MCP:
```bash
mise exec -- npm run playwright:install
mise exec -- npm run playwright:install:mcp
```
The two commands are intentionally separate because the locked stable test
runner and MCP package currently depend on different Playwright revisions.
Both revisions are stored in `.cache/ms-playwright/` and are ignored by Git.
Run the headless browser tests with Chromium sandboxing enabled:
```bash
mise exec -- npm run test:e2e
```
Codex loads the project-scoped Playwright MCP server from
`.codex/config.toml` after the repository is trusted and Codex is restarted.
The MCP browser uses bundled Chromium in headless, isolated, sandboxed mode;
its generated files are kept under `.artifacts/playwright-mcp/`.
## Checks ## Checks
```bash ```bash
mise exec -- npm run check mise exec -- npm run check
``` ```
The production build is emitted to `dist/` as static assets. See The production build is emitted to `dist/` as static assets. Browser E2E tests
are run separately because they require the browser setup above. See
`docs/web/roadmap.md` at the repository root for architecture boundaries and `docs/web/roadmap.md` at the repository root for architecture boundaries and
planned dependencies. planned dependencies.
+11
View File
@@ -0,0 +1,11 @@
import { expect, test } from '@playwright/test'
test('renders the login page in a real browser', async ({ page }) => {
const response = await page.goto('/login')
expect(response?.ok()).toBe(true)
await expect(page.getByRole('heading', { name: 'Sign in to MyGO' })).toBeVisible()
await expect(page.getByLabel('Email')).toBeVisible()
await expect(page.getByLabel('Password')).toBeVisible()
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible()
})
+129
View File
@@ -16,6 +16,8 @@
"react-router": "^8.2.0" "react-router": "^8.2.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/mcp": "0.0.78",
"@playwright/test": "1.61.1",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.2", "@types/node": "^24.13.2",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
@@ -581,6 +583,86 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
} }
}, },
"node_modules/@playwright/mcp": {
"version": "0.0.78",
"resolved": "https://registry.npmmirror.com/@playwright/mcp/-/mcp-0.0.78.tgz",
"integrity": "sha512-XLTUeA6mEN9sQ+hJ4dfG8EIkDbxS0K3Trc2RBkUJuf02TgE2FQRNTMtq/aJfhyRMINsRl/Ybc4sxcWLtFn4/TQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.62.0-alpha-1783623505000",
"playwright-core": "1.62.0-alpha-1783623505000"
},
"bin": {
"playwright-mcp": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@playwright/test": {
"version": "1.61.1",
"resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.61.1.tgz",
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@playwright/test/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/@playwright/test/node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@playwright/test/node_modules/playwright-core": {
"version": "1.61.1",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rc-component/async-validator": { "node_modules/@rc-component/async-validator": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-6.0.0.tgz", "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-6.0.0.tgz",
@@ -2677,6 +2759,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-6KV9h4PP3hqu4NaGdxxcijWfYh9LJcFI/R2sP4TTC4I5cFo3oRawN0ETlW5MkE3cQEgKhhoj0KUNz4sfpCT0Tg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.0-alpha-1783623505000"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-CPJZdsA/KGT2QQlekiV6Wt+QlQrZHVSZ6oiNtOI/bYYOIVLM8jfKGWTM4zQiyd4UN+40Cq4cA6lxmZHZbtPvJQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.19", "version": "8.5.19",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz",
+6
View File
@@ -11,7 +11,11 @@
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "oxlint", "lint": "oxlint",
"playwright:install": "playwright install chromium",
"playwright:install:deps": "playwright install-deps chromium",
"playwright:install:mcp": "node node_modules/playwright/cli.js install chromium",
"test": "vitest run", "test": "vitest run",
"test:e2e": "playwright test",
"typecheck": "tsc -b", "typecheck": "tsc -b",
"check": "npm run lint && npm run test && npm run build", "check": "npm run lint && npm run test && npm run build",
"preview": "vite preview" "preview": "vite preview"
@@ -25,6 +29,8 @@
"react-router": "^8.2.0" "react-router": "^8.2.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/mcp": "0.0.78",
"@playwright/test": "1.61.1",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.2", "@types/node": "^24.13.2",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
+44
View File
@@ -0,0 +1,44 @@
import { defineConfig, devices } from '@playwright/test'
import { fileURLToPath } from 'node:url'
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
export default defineConfig({
testDir: './e2e',
outputDir: `${repositoryRoot}/.artifacts/playwright/test-results`,
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['list'],
[
'html',
{
open: 'never',
outputFolder: `${repositoryRoot}/.artifacts/playwright/report`,
},
],
],
use: {
baseURL: 'http://127.0.0.1:5173',
launchOptions: {
chromiumSandbox: true,
},
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev -- --host 127.0.0.1',
url: 'http://127.0.0.1:5173/login',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})