diff --git a/.gitignore b/.gitignore index 81a65b3..2affdfb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,11 @@ coverage/ .vite/ *.tsbuildinfo +# Playwright e2e artifacts +test-results/ +playwright-report/ +.playwright/ + # Local docs scratch space .docs/tmp/ diff --git a/CLAUDE.md b/CLAUDE.md index e6e0bd6..b497dbd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,11 +12,12 @@ Non-trivial work is tracked as a change folder under `.docs/`: `proposal.md` (wh - `npm run check` — the gate: `typecheck && lint && test && build`. Run before any PR. - `npm run dev` / `npm run build` — Vite dev server / production build into `dist/`. -- `npm run test` — Vitest (`vitest run`). Single file: `npx vitest run tests/capture.test.ts`. Single test: `npx vitest run -t "name substring"`. Watch: `npx vitest`. +- `npm run test` — Vitest unit tests (`vitest run`, scoped to `tests/**/*.test.ts` by `vitest.config.ts`). Single file: `npx vitest run tests/capture.test.ts`. Single test: `npx vitest run -t "name substring"`. Watch: `npx vitest`. +- `npm run test:e2e` — Playwright end-to-end smoke test (`tests/e2e/extension.spec.ts`, config `playwright.config.ts`). Builds `dist/`, then launches **real Chromium in new-headless with the unpacked extension loaded** (`--load-extension`) and asserts: the extension/service worker loads (no popup, `downloads` permission), the on-page capture notice shows → hides cleanly → is removed (driven through the real content script via `SB_CAPTURE_*` messages), and the editor page renders. Requires a one-time `npx playwright install chromium`. **Deliberately excluded from `npm run check`/CI** (needs downloaded browsers + new-headless) — run it locally on demand. - `npm run typecheck`, `npm run lint` (`lint:fix`), `npm run format` (`format:check`). - `npm run gen:icons` — regenerate `public/icons/*` from `scripts/generate-icons.mjs`. -To run the extension: `npm run build`, then load the `dist/` folder via `chrome://extensions` (Developer mode → Load unpacked). There is no test runner for the live extension — UI flows are verified manually (capture, annotate/comment, timeline select/remove, viewer, cloud-LLM fallback). +To run the extension: `npm run build`, then load the `dist/` folder via `chrome://extensions` (Developer mode → Load unpacked). `npm run test:e2e` automates the page-side flows; the parts it can't (clicking the toolbar icon, real `captureVisibleTab`) are still verified manually (capture, annotate/comment, timeline select/remove, viewer, cloud-LLM fallback). ## Architecture diff --git a/package-lock.json b/package-lock.json index 44f011a..e90b45b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.0", + "@playwright/test": "^1.61.1", "@types/chrome": "^0.2.0", "@types/node": "^22.10.0", "@types/react": "^18.3.10", @@ -676,6 +677,22 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@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/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -3285,6 +3302,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/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-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/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/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/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.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", diff --git a/package.json b/package.json index cccd7c5..98effb4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run", + "test:e2e": "npm run build && playwright test", "typecheck": "tsc --noEmit", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -31,6 +32,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.0", + "@playwright/test": "^1.61.1", "@types/chrome": "^0.2.0", "@types/node": "^22.10.0", "@types/react": "^18.3.10", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..5be7c45 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "@playwright/test"; + +// E2E smoke test for the built extension. It launches real Chromium (new +// headless) with the unpacked `dist/` loaded, so it is intentionally kept out +// of the `npm run check` gate and CI — run it locally with `npm run test:e2e` +// (which builds `dist/` first). Browsers must be installed once via +// `npx playwright install chromium`. +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: false, + workers: 1, + reporter: "list", + timeout: 60_000, + expect: { timeout: 10_000 } +}); diff --git a/tests/e2e/extension.spec.ts b/tests/e2e/extension.spec.ts new file mode 100644 index 0000000..30422a0 --- /dev/null +++ b/tests/e2e/extension.spec.ts @@ -0,0 +1,133 @@ +import { existsSync } from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + chromium, + expect, + test, + type BrowserContext, + type Worker as ServiceWorker +} from "@playwright/test"; + +// Loads the unpacked `dist/` extension in real Chromium and verifies the parts +// of one-click capture that are automatable without the browser's toolbar UI: +// the extension loads, the on-page capture notice shows/hides/removes cleanly +// (driven through the real content script), and the editor page renders. + +const dir = path.dirname(fileURLToPath(import.meta.url)); +const EXT = path.resolve(dir, "..", "..", "dist"); + +const PAGE_HTML = `