mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-15 11:34:27 +01:00
Added v2 playwright e2e tests and workflow action (#2379)
This commit is contained in:
78
.github/workflows/ui-v2.yml
vendored
Normal file
78
.github/workflows/ui-v2.yml
vendored
Normal 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
5
ui-v2/.gitignore
vendored
@@ -25,3 +25,8 @@ out/
|
||||
# Generated JavaScript files in source
|
||||
electron/**/*.js
|
||||
!electron/**/*.config.js
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -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
222
ui-v2/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
35
ui-v2/playwright.config.ts
Normal file
35
ui-v2/playwright.config.ts
Normal 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']],
|
||||
});
|
||||
108
ui-v2/src/test/e2e/electron/electron.spec.ts
Normal file
108
ui-v2/src/test/e2e/electron/electron.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
139
ui-v2/src/test/e2e/web/web.spec.ts
Normal file
139
ui-v2/src/test/e2e/web/web.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user