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/
|
out/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# playwright e2e artifacts
|
||||||
|
.artifacts/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
.last-run.json
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -25,6 +25,20 @@ yarn test # unit tests (vitest)
|
|||||||
yarn typecheck
|
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
|
## Conventions
|
||||||
|
|
||||||
- Yarn 4 workspaces; ESM everywhere; `.js` extensions on relative imports
|
- 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",
|
"dev": "electron-vite dev 2>&1 | tee desktop.log",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"start": "electron-vite preview",
|
"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": {
|
"dependencies": {
|
||||||
"@commitea/core": "workspace:*",
|
"@commitea/core": "workspace:*",
|
||||||
@@ -15,6 +18,7 @@
|
|||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.1",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@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"
|
resolution: "@commitea/desktop@workspace:apps/desktop"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@commitea/core": "workspace:*"
|
"@commitea/core": "workspace:*"
|
||||||
|
"@playwright/test": "npm:^1.61.1"
|
||||||
"@types/node": "npm:^22.13.1"
|
"@types/node": "npm:^22.13.1"
|
||||||
"@types/react": "npm:^18.3.18"
|
"@types/react": "npm:^18.3.18"
|
||||||
"@types/react-dom": "npm:^18.3.5"
|
"@types/react-dom": "npm:^18.3.5"
|
||||||
@@ -720,6 +721,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@rolldown/pluginutils@npm:1.0.0-beta.27":
|
||||||
version: 1.0.0-beta.27
|
version: 1.0.0-beta.27
|
||||||
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27"
|
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27"
|
||||||
@@ -1890,6 +1902,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@npm:2.3.3"
|
resolution: "fsevents@npm:2.3.3"
|
||||||
@@ -1900,6 +1922,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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>":
|
"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
|
version: 2.3.3
|
||||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"postcss-import@npm:^15.1.0":
|
||||||
version: 15.1.0
|
version: 15.1.0
|
||||||
resolution: "postcss-import@npm:15.1.0"
|
resolution: "postcss-import@npm:15.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user