Added v2 playwright e2e tests and workflow action (#2379)

This commit is contained in:
Zane
2025-04-29 07:02:43 -07:00
committed by GitHub
parent 8d6c5ef6af
commit c8f63e3f91
15 changed files with 673 additions and 39 deletions

78
.github/workflows/ui-v2.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: UI v2 CI
on:
push:
paths:
- 'ui-v2/**'
branches:
- main
pull_request:
paths:
- 'ui-v2/**'
branches:
- main
workflow_dispatch:
jobs:
ui-v2-checks:
name: Lint and Test UI v2
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
cache-dependency-path: ui-v2/package-lock.json
- name: Install Dependencies
working-directory: ui-v2
run: npm ci
- name: Run All Checks
working-directory: ui-v2
run: npm run check-all
- name: Run Unit Tests
working-directory: ui-v2
run: npm test
- name: Install Playwright Browsers
working-directory: ui-v2
run: npx playwright install --with-deps chromium
# - name: Configure Electron Sandbox
# working-directory: ui-v2
# run: |
# sudo chown root:root node_modules/electron/dist/chrome-sandbox
# sudo chmod 4755 node_modules/electron/dist/chrome-sandbox
- name: Run E2E Tests
working-directory: ui-v2
env:
HEADLESS: true
NODE_OPTIONS: "--loader ts-node/esm --max-old-space-size=4096"
run: |
# Increase system limits
sudo sysctl -w vm.max_map_count=262144
sudo sysctl -w fs.file-max=65535
ulimit -n 65535
# Run tests with xvfb
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:web:headless
# todo: fix electron tests not running in GH workflow
# xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:electron:headless
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
ui-v2/playwright-report/
ui-v2/test-results/
retention-days: 30

5
ui-v2/.gitignore vendored
View File

@@ -25,3 +25,8 @@ out/
# Generated JavaScript files in source
electron/**/*.js
!electron/**/*.config.js
# Playwright
test-results/
playwright-report/
playwright/.cache/

View File

@@ -1,20 +1,12 @@
{
"extends": [
"stylelint-config-standard"
],
"extends": ["stylelint-config-standard"],
"rules": {
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"variants",
"responsive",
"screen"
]
"ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"]
}
],
"no-descending-specificity": null
}
}
}

View File

@@ -1,4 +1,5 @@
# codename goose ui v2
Your on-machine AI agent, automating tasks seamlessly.
## Development
@@ -50,6 +51,18 @@ npm run test:ui
# Generate test coverage
npm run test:coverage
# End-to-End Testing
npm run test:e2e # Run all e2e tests headlessly
npm run test:e2e:ui # Run e2e tests with UI mode for both web and electron
npm run test:e2e:web # Run web e2e tests with browser visible
npm run test:e2e:web:headless # Run web e2e tests headlessly
npm run test:e2e:web:ui # Run web e2e tests with Playwright UI mode
npm run test:e2e:electron # Run electron e2e tests with window visible
npm run test:e2e:electron:headless # Run electron e2e tests headlessly
npm run test:e2e:electron:ui # Run electron e2e tests with Playwright UI mode
# Type checking
npm run typecheck # Check all TypeScript files
npm run tsc:web # Check web TypeScript files
@@ -85,6 +98,11 @@ npm run check-all
│ │ ├── IPlatformService.ts
│ │ └── index.ts
│ ├── test/ # Test setup and configurations
│ │ ├── e2e/ # End-to-end test files
│ │ │ ├── electron/ # Electron-specific e2e tests
│ │ │ │ └── electron.spec.ts
│ │ │ └── web/ # Web-specific e2e tests
│ │ │ └── web.spec.ts
│ │ ├── setup.ts
│ │ └── types.d.ts
│ ├── App.tsx
@@ -92,6 +110,7 @@ npm run check-all
│ └── web.tsx # Web entry
├── electron.html # Electron HTML template
├── index.html # Web HTML template
├── playwright.config.ts # Playwright e2e test configuration
├── vite.config.ts # Vite config for web
├── vite.main.config.ts # Vite config for electron main
├── vite.preload.config.ts # Vite config for preload script
@@ -118,6 +137,7 @@ export interface IPlatformService {
```
This is implemented through two concrete classes:
- `WebPlatformService`: Implements functionality for web browsers using Web APIs
- `ElectronPlatformService`: Implements functionality for Electron using IPC
@@ -130,6 +150,7 @@ The application uses a dependency injection pattern for platform services:
3. **Unified Access**: Components access platform features through a single `platformService` instance
Example usage in components:
```typescript
import { platformService } from '@platform';
@@ -142,6 +163,7 @@ await platformService.copyToClipboard(text);
For Electron-specific functionality, the architecture includes:
1. **Preload Script**: Safely exposes Electron APIs to the renderer process
```typescript
// Type definitions for Electron APIs
declare global {
@@ -154,6 +176,7 @@ declare global {
```
2. **IPC Communication**: Typed handlers for main process communication
```typescript
// Electron implementation
export class ElectronPlatformService implements IPlatformService {
@@ -168,7 +191,7 @@ export class ElectronPlatformService implements IPlatformService {
The project uses a sophisticated build system with multiple configurations:
1. **Web Build**: Vite-based build for web deployment
2. **Electron Build**:
2. **Electron Build**:
- Main Process: Separate Vite config for Electron main process
- Renderer Process: Specialized config for Electron renderer
- Preload Scripts: Dedicated build configuration for preload scripts

View File

@@ -1,14 +1,17 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
<link href="/src/index.css" rel="stylesheet">
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
/>
<link href="/src/index.css" rel="stylesheet" />
<title>Goose v2</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/electron.tsx"></script>
</body>
</html>
</html>

View File

@@ -9,27 +9,46 @@ import type {
} from 'electron';
const isDevelopment = !app.isPackaged;
const isHeadless = process.env.HEADLESS === 'true';
// Enable sandbox before app is ready
app.enableSandbox();
if (isHeadless) {
app.disableHardwareAcceleration();
}
async function createWindow() {
// Handle different preload paths for dev and prod
const preloadPath = isDevelopment
? path.join(app.getAppPath(), '.vite/build/preload/preload.js')
: path.join(__dirname, 'preload.js');
// Create the browser window.
// Create the browser window with headless options when needed
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
preload: preloadPath,
sandbox: true,
},
...(isHeadless
? {
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
preload: preloadPath,
sandbox: true,
offscreen: true,
},
}
: {
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
preload: preloadPath,
sandbox: true,
},
}),
});
// Set up CSP
@@ -108,7 +127,9 @@ async function createWindow() {
// Load the app
if (isDevelopment) {
mainWindow.loadURL('http://localhost:3001/');
mainWindow.webContents.openDevTools();
if (!isHeadless) {
mainWindow.webContents.openDevTools();
}
} else {
// In production, load from the asar archive
mainWindow.loadFile(path.join(__dirname, 'renderer/index.html'));

View File

@@ -8,13 +8,7 @@ const reactHooks = require('eslint-plugin-react-hooks');
module.exports = [
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/out/**',
'**/coverage/**',
'**/.vite/**',
],
ignores: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/coverage/**', '**/.vite/**'],
},
// Configuration for Node.js files
{

View File

@@ -13,6 +13,7 @@ const config: ForgeConfig = {
},
{
name: '@electron-forge/maker-zip',
config: {},
platforms: ['darwin'],
},
{
@@ -40,7 +41,6 @@ const config: ForgeConfig = {
{
name: 'main_window',
config: 'vite.renderer.config.ts',
entry: ['electron.html'],
},
],
}),

View File

@@ -1,10 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" />
<link href="/src/index.css" rel="stylesheet">
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
/>
<link href="/src/index.css" rel="stylesheet" />
<title>Goose v2</title>
</head>
<body>

222
ui-v2/package-lock.json generated
View File

@@ -19,6 +19,7 @@
"@electron-forge/maker-zip": "^7.8.0",
"@electron-forge/plugin-vite": "^7.8.0",
"@electron-forge/shared-types": "^7.8.0",
"@playwright/test": "^1.42.1",
"@tailwindcss/postcss": "^4.1.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -48,6 +49,7 @@
"stylelint": "^16.19.1",
"stylelint-config-standard": "^38.0.0",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"vite": "^6.3.3",
"vitest": "^3.1.2"
@@ -423,6 +425,30 @@
"node": ">=18"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
@@ -2223,6 +2249,22 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@playwright/test": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
"integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.52.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -2926,6 +2968,34 @@
"node": ">= 10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -3564,6 +3634,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
@@ -3660,6 +3743,13 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4805,6 +4895,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-dirname": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
@@ -5163,6 +5260,16 @@
"license": "MIT",
"optional": true
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dir-compare": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
@@ -9756,6 +9863,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/make-fetch-happen": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
@@ -11046,6 +11160,53 @@
"node": ">=0.10.0"
}
},
"node_modules/playwright": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.52.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/plist": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
@@ -13656,6 +13817,50 @@
"typescript": ">=4.8.4"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -13965,6 +14170,13 @@
"node": ">= 0.4.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -14721,6 +14933,16 @@
"fd-slicer": "~1.1.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -16,14 +16,22 @@
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui --project=web --project=electron --workers=1",
"test:e2e:web": "playwright test --project=web --headed",
"test:e2e:web:headless": "playwright test --project=web",
"test:e2e:web:ui": "playwright test --ui --project=web",
"test:e2e:electron": "playwright test --project=electron --headed",
"test:e2e:electron:headless": "HEADLESS=true playwright test --project=electron",
"test:e2e:electron:ui": "playwright test --ui --project=electron",
"tsc": "tsc --noEmit",
"tsc:web": "tsc --project tsconfig.json --noEmit",
"tsc:electron": "tsc --project tsconfig.electron.json --noEmit",
"typecheck": "npm run tsc:web && npm run tsc:electron",
"lint": "eslint . && npm run lint:style",
"lint:fix": "eslint . --fix && npm run lint:style:fix",
"lint:style": "stylelint \"src/**/*.{css}\"",
"lint:style:fix": "stylelint \"src/**/*.{css}\" --fix",
"lint:style": "stylelint src/**/*.css",
"lint:style:fix": "stylelint src/**/*.css --fix",
"prettier": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css}\" \"electron/**/*.{ts,tsx,js,jsx,css}\"",
"prettier:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css}\" \"electron/**/*.{ts,tsx,js,jsx,css}\"",
"format": "npm run prettier:fix && npm run lint:style:fix",
@@ -42,6 +50,7 @@
"@electron-forge/maker-zip": "^7.8.0",
"@electron-forge/plugin-vite": "^7.8.0",
"@electron-forge/shared-types": "^7.8.0",
"@playwright/test": "^1.42.1",
"@tailwindcss/postcss": "^4.1.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -71,6 +80,7 @@
"stylelint": "^16.19.1",
"stylelint-config-standard": "^38.0.0",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"vite": "^6.3.3",
"vitest": "^3.1.2"
@@ -79,7 +89,7 @@
"src/**/*.{ts,tsx}": [
"eslint --fix --max-warnings 0 --no-warn-ignored",
"prettier --write",
"tsc --noemit"
"bash -c 'tsc --pretty --noEmit --project tsconfig.json'"
],
"src/**/*.{css,json}": [
"prettier --write",

View File

@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './src/test/e2e',
workers: 1,
use: {
trace: 'on-first-retry',
// Use headless mode in CI, non-headless locally unless specified
headless: process.env.CI === 'true' || process.env.HEADLESS === 'true',
// Add longer timeouts for CI
navigationTimeout: 30000,
actionTimeout: 15000,
},
projects: [
{
name: 'web',
testMatch: ['**/web/*.spec.ts'],
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'electron',
testMatch: ['**/electron/*.spec.ts'],
use: {
...devices['Desktop Chrome'],
},
},
],
timeout: 60000, // Increase overall timeout
expect: {
timeout: 15000, // Increase expect timeout
},
reporter: [['html'], ['list']],
});

View File

@@ -0,0 +1,108 @@
import { spawn } from 'child_process';
import { Buffer } from 'node:buffer';
import { test, expect } from '@playwright/test';
let electronProcess: ReturnType<typeof spawn> | undefined;
// Helper function to safely kill a process and its children
async function killProcess(pid: number): Promise<void> {
try {
// Try to kill the process group first
try {
process.kill(-pid, 'SIGTERM');
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Failed to kill process group:', message);
}
// Wait a bit and then try to kill the process directly
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
process.kill(pid, 'SIGTERM');
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Failed to kill process:', message);
}
// Final cleanup with SIGKILL if needed
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
process.kill(pid, 'SIGKILL');
} catch (error: unknown) {
// Process is probably already dead
const message = error instanceof Error ? error.message : String(error);
console.log('Process already terminated:', message);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Error during process cleanup:', message);
}
}
test.describe('electron app', () => {
test.beforeAll(async () => {
console.log('Starting Electron app...');
console.log('Environment:', process.env.NODE_ENV);
console.log('HEADLESS:', process.env.HEADLESS);
// Start electron with minimal memory settings
electronProcess = spawn('npm', ['run', 'start:electron'], {
stdio: 'pipe',
shell: true,
env: {
...process.env,
ELECTRON_IS_DEV: '1',
NODE_ENV: 'development',
HEADLESS: process.env.HEADLESS || 'false',
ELECTRON_START_URL: 'http://localhost:3001',
// Add memory limits for Electron
ELECTRON_EXTRA_LAUNCH_ARGS: '--js-flags="--max-old-space-size=512" --disable-gpu',
},
// Set detached to false and create a new process group
detached: false,
});
// Store the PID for cleanup
const pid = electronProcess.pid;
console.log('Started Electron app with PID:', pid);
// Capture stdout and stderr for debugging
electronProcess.stdout?.on('data', (data: Buffer) => {
console.log(`Electron stdout: ${data.toString()}`);
});
electronProcess.stderr?.on('data', (data: Buffer) => {
console.log(`Electron stderr: ${data.toString()}`);
});
// Wait for the app to be ready
await new Promise((resolve) => setTimeout(resolve, 2000));
});
test.afterAll(async () => {
console.log('Stopping Electron app...');
if (electronProcess?.pid) {
console.log('Killing Electron process:', electronProcess.pid);
await killProcess(electronProcess.pid);
}
// Give processes time to fully terminate
await new Promise((resolve) => setTimeout(resolve, 2000));
});
test('shows correct runtime', async ({ page }) => {
console.log('Navigating to http://localhost:3001');
const response = await page.goto('http://localhost:3001');
console.log('Navigation status:', response?.status());
// Wait for and check the text with more detailed logging
console.log('Looking for runtime text...');
const runtimeText = page.locator('text=Running in: Electron');
// Wait for the text to be visible
await expect(runtimeText).toBeVisible({ timeout: 10000 });
});
});

View File

@@ -0,0 +1,139 @@
import { spawn } from 'child_process';
import http from 'http';
import { Buffer } from 'node:buffer';
import { test, expect } from '@playwright/test';
let webProcess: ReturnType<typeof spawn> | undefined;
// Helper function to check if server is ready
async function waitForServer(url: string, timeout: number): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
await new Promise<void>((resolve, reject) => {
const req = http.get(url, (res) => {
if (res.statusCode === 200) {
resolve();
} else {
reject(new Error(`Status code: ${res.statusCode}`));
}
res.resume(); // Consume response data to free up memory
});
req.on('error', reject);
req.setTimeout(1000, () => reject(new Error('Request timeout')));
});
console.log('Server is ready');
return true;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Waiting for server...', message);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log('Server failed to respond in time');
return false;
}
// Helper function to safely kill a process and its children
async function killProcess(pid: number): Promise<void> {
try {
// Try to kill the process group first
try {
process.kill(-pid, 'SIGTERM');
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Failed to kill process group:', message);
}
// Wait a bit and then try to kill the process directly
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
process.kill(pid, 'SIGTERM');
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Failed to kill process:', message);
}
// Final cleanup with SIGKILL if needed
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
process.kill(pid, 'SIGKILL');
} catch (error: unknown) {
// Process is probably already dead
const message = error instanceof Error ? error.message : String(error);
console.log('Process already terminated:', message);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.log('Error during process cleanup:', message);
}
}
test.describe('web app', () => {
test.beforeAll(async () => {
console.log('Starting web app...');
// Start the vite dev server
webProcess = spawn('npm', ['run', 'start:web'], {
stdio: 'pipe',
shell: true,
env: {
...process.env,
NODE_ENV: 'development',
},
// Set detached to false and create a new process group
detached: false,
});
// Store the PID for cleanup
const pid = webProcess.pid;
console.log('Started Vite server with PID:', pid);
// Capture stdout and stderr for debugging
webProcess.stdout?.on('data', (data: Buffer) => {
console.log(`Vite stdout: ${data.toString()}`);
});
webProcess.stderr?.on('data', (data: Buffer) => {
console.log(`Vite stderr: ${data.toString()}`);
});
// Wait for server to be ready
console.log('Waiting for server to be ready...');
const serverReady = await waitForServer('http://localhost:3000', 30000);
if (!serverReady) {
throw new Error('Server failed to start');
}
});
test.afterAll(async () => {
console.log('Cleaning up processes...');
if (webProcess?.pid) {
console.log('Killing Vite server process:', webProcess.pid);
await killProcess(webProcess.pid);
}
// Give processes time to fully terminate
await new Promise((resolve) => setTimeout(resolve, 2000));
});
test('shows correct runtime', async ({ page }) => {
console.log('Navigating to http://localhost:3000');
const response = await page.goto('http://localhost:3000');
console.log('Navigation status:', response?.status());
// Wait for and check the text with more detailed logging
console.log('Looking for runtime text...');
const runtimeText = page.locator('text=Running in: Web Browser');
// Wait for the text to be visible
await expect(runtimeText).toBeVisible({ timeout: 10000 });
});
});

View File

@@ -11,6 +11,7 @@ export default defineConfig({
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
css: true,
exclude: ['**/test/e2e/**', '**/node_modules/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],