From 29b176d2db6c62d1798f95350c00e06066abe800 Mon Sep 17 00:00:00 2001 From: Huxley Date: Wed, 15 Jul 2026 20:17:24 +0800 Subject: [PATCH] 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. --- .codex/config.toml | 22 +++++++ .gitignore | 4 ++ docs/web/decisions.md | 26 ++++++++ docs/web/roadmap.md | 2 + mise.toml | 4 ++ web/README.md | 38 ++++++++++- web/e2e/login-page.spec.ts | 11 ++++ web/package-lock.json | 129 +++++++++++++++++++++++++++++++++++++ web/package.json | 6 ++ web/playwright.config.ts | 44 +++++++++++++ 10 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 .codex/config.toml create mode 100644 web/e2e/login-page.spec.ts create mode 100644 web/playwright.config.ts diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..3b80c80 --- /dev/null +++ b/.codex/config.toml @@ -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 diff --git a/.gitignore b/.gitignore index ab2258a..778c025 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,10 @@ go.work.sum # Mise mise.local.toml +# Project-local development caches and browser artifacts +/.cache/ +/.artifacts/ + # MyGO configuration # Tracking config.example.yaml in version control config.yaml diff --git a/docs/web/decisions.md b/docs/web/decisions.md index 9be6f2e..c5876af 100644 --- a/docs/web/decisions.md +++ b/docs/web/decisions.md @@ -43,3 +43,29 @@ account, admin, or large-transfer designs. transfer policy in MyGO code. - Handwritten TypeScript wire types remain temporary until an OpenAPI contract 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. diff --git a/docs/web/roadmap.md b/docs/web/roadmap.md index 21dd887..5ff252c 100644 --- a/docs/web/roadmap.md +++ b/docs/web/roadmap.md @@ -36,6 +36,8 @@ The project foundation contains: - Ant Design icons for application and file actions - Oxlint from the Vite template - 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. diff --git a/mise.toml b/mise.toml index 38b040d..c387613 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,7 @@ [tools] go = "1.26.2" node = "24" + +[env] +NPM_CONFIG_CACHE = "{{config_root}}/.cache/npm" +PLAYWRIGHT_BROWSERS_PATH = "{{config_root}}/.cache/ms-playwright" diff --git a/web/README.md b/web/README.md index 9992bd3..c208d01 100644 --- a/web/README.md +++ b/web/README.md @@ -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 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 ```bash 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 planned dependencies. diff --git a/web/e2e/login-page.spec.ts b/web/e2e/login-page.spec.ts new file mode 100644 index 0000000..8e9136c --- /dev/null +++ b/web/e2e/login-page.spec.ts @@ -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() +}) diff --git a/web/package-lock.json b/web/package-lock.json index 0d549d1..5e00927 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,6 +16,8 @@ "react-router": "^8.2.0" }, "devDependencies": { + "@playwright/mcp": "0.0.78", + "@playwright/test": "1.61.1", "@tailwindcss/vite": "^4.3.2", "@types/node": "^24.13.2", "@types/react": "^19.2.17", @@ -581,6 +583,86 @@ "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": { "version": "6.0.0", "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" } }, + "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": { "version": "8.5.19", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz", diff --git a/web/package.json b/web/package.json index 6f07ed2..029399f 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,11 @@ "dev": "vite", "build": "tsc -b && vite build", "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:e2e": "playwright test", "typecheck": "tsc -b", "check": "npm run lint && npm run test && npm run build", "preview": "vite preview" @@ -25,6 +29,8 @@ "react-router": "^8.2.0" }, "devDependencies": { + "@playwright/mcp": "0.0.78", + "@playwright/test": "1.61.1", "@tailwindcss/vite": "^4.3.2", "@types/node": "^24.13.2", "@types/react": "^19.2.17", diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 0000000..8d5c3a6 --- /dev/null +++ b/web/playwright.config.ts @@ -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, + }, +})