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:
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',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user