test(desktop): Playwright electron e2e harness
Launches the built app (out/main/index.js) via Playwright's _electron API — no browser project, no chromium download. Adds a launch fixture (electronApp/window/app), an AppPage page object with a screenshot helper for autonomous visual review, and a boot smoke suite (shell renders, @commitea/core label-parse runs in the renderer, preload API exposed). Scripts: e2e (build+run), e2e:only, e2e:report. Artifacts gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,6 +3,12 @@ dist/
|
||||
out/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# playwright e2e artifacts
|
||||
.artifacts/
|
||||
test-results/
|
||||
playwright-report/
|
||||
.last-run.json
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
14
README.md
14
README.md
@@ -25,6 +25,20 @@ yarn test # unit tests (vitest)
|
||||
yarn typecheck
|
||||
```
|
||||
|
||||
### End-to-end (Electron + Playwright)
|
||||
|
||||
```sh
|
||||
yarn workspace @commitea/desktop e2e # build, then drive the built app
|
||||
yarn workspace @commitea/desktop e2e:only # reuse existing out/ build (tight loop)
|
||||
yarn workspace @commitea/desktop e2e:report # open the last HTML report
|
||||
```
|
||||
|
||||
Tests launch the built app (`out/main/index.js`) through Playwright's
|
||||
`_electron` API — no browser project, no chromium download. Fixtures and page
|
||||
objects live in `apps/desktop/e2e/`; screenshots land in
|
||||
`e2e/.artifacts/screens/` for visual review. Page objects use user-facing
|
||||
locators (`getByRole`/`getByText`), never CSS/DOM structure.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Yarn 4 workspaces; ESM everywhere; `.js` extensions on relative imports
|
||||
|
||||
48
apps/desktop/e2e/fixtures.ts
Normal file
48
apps/desktop/e2e/fixtures.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import {
|
||||
_electron as electron,
|
||||
expect,
|
||||
test as base,
|
||||
type ElectronApplication,
|
||||
type Page,
|
||||
} from '@playwright/test'
|
||||
|
||||
import { AppPage } from './pages/app.page.js'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
/** The built main-process entry Playwright launches. `yarn e2e` builds it first. */
|
||||
const MAIN_ENTRY = join(here, '..', 'out', 'main', 'index.js')
|
||||
|
||||
interface CommiteaFixtures {
|
||||
/** The launched Electron application. */
|
||||
electronApp: ElectronApplication
|
||||
/** The app's first (and only) BrowserWindow, as a Playwright Page. */
|
||||
window: Page
|
||||
/** Page Object over the app shell. */
|
||||
app: AppPage
|
||||
}
|
||||
|
||||
export const test = base.extend<CommiteaFixtures>({
|
||||
electronApp: async ({}, use) => {
|
||||
const electronApp = await electron.launch({
|
||||
args: [MAIN_ENTRY],
|
||||
env: { ...process.env, NODE_ENV: 'test', COMMITEA_E2E: '1' },
|
||||
})
|
||||
await use(electronApp)
|
||||
await electronApp.close()
|
||||
},
|
||||
|
||||
window: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow()
|
||||
await window.waitForLoadState('domcontentloaded')
|
||||
await use(window)
|
||||
},
|
||||
|
||||
app: async ({ window }, use) => {
|
||||
await use(new AppPage(window))
|
||||
},
|
||||
})
|
||||
|
||||
export { expect }
|
||||
40
apps/desktop/e2e/pages/app.page.ts
Normal file
40
apps/desktop/e2e/pages/app.page.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { expect, type Locator, type Page } from '@playwright/test'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
/** Where `screenshot()` drops PNGs — gitignored, readable for visual review. */
|
||||
const SCREENS_DIR = join(here, '..', '.artifacts', 'screens')
|
||||
|
||||
/**
|
||||
* Page Object over the CommiTea app shell. As real screens land (P3), add
|
||||
* per-screen page objects and accessor methods here; keep locators
|
||||
* user-facing (getByRole/getByText), never CSS/DOM structure.
|
||||
*/
|
||||
export class AppPage {
|
||||
constructor(readonly page: Page) {}
|
||||
|
||||
get heading(): Locator {
|
||||
return this.page.getByRole('heading', { name: 'CommiTea' })
|
||||
}
|
||||
|
||||
/** Assert the shell has rendered. */
|
||||
async expectLoaded(): Promise<void> {
|
||||
await expect(this.heading).toBeVisible()
|
||||
}
|
||||
|
||||
/** Read the `commitea` preload API surface from the renderer. */
|
||||
async preloadApi(): Promise<Record<string, unknown> | undefined> {
|
||||
return this.page.evaluate(
|
||||
() => (globalThis as unknown as { commitea?: Record<string, unknown> }).commitea,
|
||||
)
|
||||
}
|
||||
|
||||
/** Capture a full-page screenshot for visual review; returns the path. */
|
||||
async screenshot(name: string): Promise<string> {
|
||||
const path = join(SCREENS_DIR, `${name}.png`)
|
||||
await this.page.screenshot({ path, fullPage: true })
|
||||
return path
|
||||
}
|
||||
}
|
||||
19
apps/desktop/e2e/smoke.spec.ts
Normal file
19
apps/desktop/e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
test('boots and renders the CommiTea shell', async ({ app }) => {
|
||||
await app.expectLoaded()
|
||||
await app.screenshot('boot')
|
||||
})
|
||||
|
||||
test('renders the label-parse check from @commitea/core', async ({ window }) => {
|
||||
// Proves the core package is bundled + running in the renderer, not just
|
||||
// that the window opened. Updates as the scaffold gives way to real screens.
|
||||
await expect(window.getByText(/label parse check/i)).toBeVisible()
|
||||
await expect(window.getByText(/est\/3d · p\/2 · hard true/)).toBeVisible()
|
||||
})
|
||||
|
||||
test('exposes the commitea preload API', async ({ app }) => {
|
||||
const api = await app.preloadApi()
|
||||
expect(api).toBeDefined()
|
||||
expect(api).toHaveProperty('platform')
|
||||
})
|
||||
10
apps/desktop/e2e/tsconfig.json
Normal file
10
apps/desktop/e2e/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["node"],
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
"dev": "electron-vite dev 2>&1 | tee desktop.log",
|
||||
"build": "electron-vite build",
|
||||
"start": "electron-vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"e2e": "electron-vite build && playwright test",
|
||||
"e2e:only": "playwright test",
|
||||
"e2e:report": "playwright show-report e2e/.artifacts/report"
|
||||
},
|
||||
"dependencies": {
|
||||
"@commitea/core": "workspace:*",
|
||||
@@ -15,6 +18,7 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
|
||||
26
apps/desktop/playwright.config.ts
Normal file
26
apps/desktop/playwright.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* E2E harness for the Electron app. Tests launch the *built* app
|
||||
* (`out/main/index.js`) through Playwright's `_electron` API — see
|
||||
* `e2e/fixtures.ts`. There is no browser project and no chromium download:
|
||||
* `_electron` drives the app's own bundled electron.
|
||||
*
|
||||
* Run `yarn e2e` (builds first) for a fresh run, or `yarn e2e:only` to reuse
|
||||
* the existing `out/` build during a tight iteration loop.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
outputDir: './e2e/.artifacts/test-results',
|
||||
fullyParallel: false,
|
||||
workers: 1, // one electron instance at a time — deterministic, avoids window races
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 5_000 },
|
||||
reporter: [['list'], ['html', { outputFolder: './e2e/.artifacts/report', open: 'never' }]],
|
||||
use: {
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
})
|
||||
55
yarn.lock
55
yarn.lock
@@ -241,6 +241,7 @@ __metadata:
|
||||
resolution: "@commitea/desktop@workspace:apps/desktop"
|
||||
dependencies:
|
||||
"@commitea/core": "workspace:*"
|
||||
"@playwright/test": "npm:^1.61.1"
|
||||
"@types/node": "npm:^22.13.1"
|
||||
"@types/react": "npm:^18.3.18"
|
||||
"@types/react-dom": "npm:^18.3.5"
|
||||
@@ -720,6 +721,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@playwright/test@npm:^1.61.1":
|
||||
version: 1.61.1
|
||||
resolution: "@playwright/test@npm:1.61.1"
|
||||
dependencies:
|
||||
playwright: "npm:1.61.1"
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 10c0/45004139eaa7c7ec8129e4ed77a9d734aa09ed40b69162b871e4123f1d70217b7b73651431a9343ed5090d5fed11c6df1bff2a56ff4b87dc7b0371b5da87cf9c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rolldown/pluginutils@npm:1.0.0-beta.27":
|
||||
version: 1.0.0-beta.27
|
||||
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27"
|
||||
@@ -1890,6 +1902,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:2.3.2":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@npm:2.3.2"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@npm:2.3.3"
|
||||
@@ -1900,6 +1922,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||
@@ -2458,6 +2489,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright-core@npm:1.61.1":
|
||||
version: 1.61.1
|
||||
resolution: "playwright-core@npm:1.61.1"
|
||||
bin:
|
||||
playwright-core: cli.js
|
||||
checksum: 10c0/c28896ba82a602182e240ed4f9c467fb0dd9cb57d510f8aba9f25183fe1cde3a0105725a29baffa4157fe885bbb11e228032a2aad0424111584fde45303f07bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright@npm:1.61.1":
|
||||
version: 1.61.1
|
||||
resolution: "playwright@npm:1.61.1"
|
||||
dependencies:
|
||||
fsevents: "npm:2.3.2"
|
||||
playwright-core: "npm:1.61.1"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 10c0/cea1bc4d2a64ec3ef683891606774029da7b8a8036ed98332565cec2f8e89549ca1fab9a89fbfba4e08e27e0d7e7d148031e965af66d5ff97bb3c34058cfcad0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-import@npm:^15.1.0":
|
||||
version: 15.1.0
|
||||
resolution: "postcss-import@npm:15.1.0"
|
||||
|
||||
Reference in New Issue
Block a user