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:
Croissant Le Doux
2026-07-08 11:38:59 -04:00
parent d6f531eb6d
commit 96c2b8b1c2
9 changed files with 223 additions and 1 deletions

View 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 }

View 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
}
}

View 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')
})

View File

@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node"],
"moduleResolution": "Bundler",
"noEmit": true
},
"include": ["."]
}