CommiTea foundation + P3 UI: gitea client, e2e harness, all product screens #35

Merged
christian merged 8 commits from p1/gitea-read-client into main 2026-07-08 20:42:13 +00:00
18 changed files with 882 additions and 10 deletions
Showing only changes of commit 96c2b8b1c2 - Show all commits

6
.gitignore vendored
View File

@@ -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

View File

@@ -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

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": ["."]
}

View File

@@ -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",

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

View File

@@ -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"