mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-09 08:34:22 +01:00
Added Playwright E2E testing setup (#1893)
This commit is contained in:
2
ui/desktop/.gitignore
vendored
2
ui/desktop/.gitignore
vendored
@@ -3,3 +3,5 @@ node_modules
|
||||
out
|
||||
src/bin/goosed
|
||||
/src/bin/goosed.exe
|
||||
/playwright-report/
|
||||
/test-results/
|
||||
|
||||
64
ui/desktop/package-lock.json
generated
64
ui/desktop/package-lock.json
generated
@@ -62,6 +62,7 @@
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@eslint/js": "^8.56.0",
|
||||
"@hey-api/openapi-ts": "^0.64.4",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
@@ -2504,6 +2505,22 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.51.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz",
|
||||
"integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.51.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/colors": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz",
|
||||
@@ -13430,6 +13447,53 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.51.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz",
|
||||
"integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.51.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.51.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz",
|
||||
"integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==",
|
||||
"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",
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
"bundle:windows": "node scripts/build-main.js && npm run make -- --platform=win32 --arch=x64 && node scripts/copy-windows-dlls.js",
|
||||
"bundle:intel": "npm run make -- --arch=x64 && cd out/Goose-darwin-x64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose_intel_mac.zip",
|
||||
"debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app",
|
||||
"test-e2e": "electron-forge start > /tmp/out.txt & ELECTRON_PID=$! && sleep 12 && if grep -q 'renderer: ChatWindow loaded' /tmp/out.txt; then echo 'process is running'; pkill -f electron; else echo 'not starting correctly'; cat /tmp/out.txt; pkill -f electron; exit 1; fi",
|
||||
"test-e2e": "npm run generate-api && playwright test",
|
||||
"test-e2e:ui": "npm run generate-api && playwright test --ui",
|
||||
"test-e2e:debug": "npm run generate-api && playwright test --debug",
|
||||
"test-e2e:report": "playwright show-report",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
|
||||
"lint:check": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
|
||||
@@ -35,6 +38,7 @@
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@eslint/js": "^8.56.0",
|
||||
"@hey-api/openapi-ts": "^0.64.4",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
|
||||
26
ui/desktop/playwright.config.ts
Normal file
26
ui/desktop/playwright.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests/e2e',
|
||||
timeout: 60000,
|
||||
expect: {
|
||||
timeout: 30000
|
||||
},
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
['html'],
|
||||
['list']
|
||||
],
|
||||
use: {
|
||||
actionTimeout: 30000,
|
||||
navigationTimeout: 30000,
|
||||
trace: 'on-first-retry',
|
||||
video: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure'
|
||||
},
|
||||
outputDir: 'test-results',
|
||||
preserveOutput: 'failures-only'
|
||||
};
|
||||
|
||||
export default config;
|
||||
253
ui/desktop/tests/e2e/app.spec.ts
Normal file
253
ui/desktop/tests/e2e/app.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { _electron as electron } from '@playwright/test';
|
||||
import { join } from 'path';
|
||||
import { spawn, exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
test.describe('Goose App', () => {
|
||||
let electronApp;
|
||||
let appProcess;
|
||||
let mainWindow;
|
||||
let isProviderSelected = false;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
console.log('Starting Electron app...');
|
||||
|
||||
// Start the electron-forge process
|
||||
appProcess = spawn('npm', ['run', 'start-gui'], {
|
||||
cwd: join(__dirname, '../..'),
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_IS_DEV: '1',
|
||||
NODE_ENV: 'development'
|
||||
}
|
||||
});
|
||||
|
||||
// Log process output
|
||||
appProcess.stdout.on('data', (data) => {
|
||||
console.log('App stdout:', data.toString());
|
||||
});
|
||||
|
||||
appProcess.stderr.on('data', (data) => {
|
||||
console.log('App stderr:', data.toString());
|
||||
});
|
||||
|
||||
// Wait a bit for the app to start
|
||||
console.log('Waiting for app to start...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Launch Electron for testing
|
||||
electronApp = await electron.launch({
|
||||
args: ['.vite/build/main.js'],
|
||||
cwd: join(__dirname, '../..'),
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_IS_DEV: '1',
|
||||
NODE_ENV: 'development'
|
||||
}
|
||||
});
|
||||
|
||||
// Get the main window once for all tests
|
||||
mainWindow = await electronApp.firstWindow();
|
||||
await mainWindow.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Check if we're already on the chat screen
|
||||
try {
|
||||
const chatInput = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]',
|
||||
{ timeout: 5000 });
|
||||
isProviderSelected = await chatInput.isVisible();
|
||||
console.log('Provider already selected, chat interface visible');
|
||||
} catch (e) {
|
||||
console.log('On provider selection screen');
|
||||
isProviderSelected = false;
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
console.log('Final cleanup...');
|
||||
|
||||
// Close the test instance
|
||||
if (electronApp) {
|
||||
await electronApp.close().catch(console.error);
|
||||
}
|
||||
|
||||
// Kill any remaining electron processes
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
await execAsync('taskkill /F /IM electron.exe');
|
||||
} else {
|
||||
await execAsync('pkill -f electron || true');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!error.message?.includes('no process found')) {
|
||||
console.error('Error killing electron processes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Kill any remaining npm processes from start-gui
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
await execAsync('taskkill /F /IM node.exe');
|
||||
} else {
|
||||
await execAsync('pkill -f "start-gui" || true');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!error.message?.includes('no process found')) {
|
||||
console.error('Error killing npm processes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Kill the specific npm process if it's still running
|
||||
try {
|
||||
if (appProcess && appProcess.pid) {
|
||||
process.kill(appProcess.pid);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 'ESRCH') {
|
||||
console.error('Error killing npm process:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('verify initial screen and select provider if needed', async () => {
|
||||
console.log('Checking initial screen state...');
|
||||
|
||||
if (!isProviderSelected) {
|
||||
// Take screenshot of provider selection screen
|
||||
await mainWindow.screenshot({ path: 'test-results/provider-selection.png' });
|
||||
|
||||
// Verify provider selection screen
|
||||
const heading = await mainWindow.waitForSelector('h2:has-text("Choose a Provider")', { timeout: 10000 });
|
||||
const headingText = await heading.textContent();
|
||||
expect(headingText).toBe('Choose a Provider');
|
||||
|
||||
// Find and verify the Databricks card container
|
||||
console.log('Looking for Databricks card...');
|
||||
const databricksContainer = await mainWindow.waitForSelector(
|
||||
'div:has(h3:text("Databricks"))[class*="relative bg-bgApp rounded-lg"]'
|
||||
);
|
||||
expect(await databricksContainer.isVisible()).toBe(true);
|
||||
|
||||
// Find the Launch button within the Databricks container
|
||||
console.log('Looking for Launch button in Databricks card...');
|
||||
const launchButton = await databricksContainer.waitForSelector('button:has-text("Launch")');
|
||||
expect(await launchButton.isVisible()).toBe(true);
|
||||
|
||||
// Take screenshot before clicking
|
||||
await mainWindow.screenshot({ path: 'test-results/before-databricks-click.png' });
|
||||
|
||||
// Click the Launch button
|
||||
await launchButton.click();
|
||||
|
||||
// Wait for chat interface to appear
|
||||
const chatTextarea = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]',
|
||||
{ timeout: 15000 });
|
||||
expect(await chatTextarea.isVisible()).toBe(true);
|
||||
} else {
|
||||
console.log('Provider already selected, skipping provider selection test');
|
||||
}
|
||||
|
||||
// Take screenshot of current state
|
||||
await mainWindow.screenshot({ path: 'test-results/chat-interface.png' });
|
||||
});
|
||||
|
||||
test('chat interaction', async () => {
|
||||
console.log('Testing chat interaction...');
|
||||
|
||||
// Find the chat input
|
||||
const chatInput = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]');
|
||||
expect(await chatInput.isVisible()).toBe(true);
|
||||
|
||||
// Type a message
|
||||
await chatInput.fill('Hello, can you help me with a simple task?');
|
||||
|
||||
// Take screenshot before sending
|
||||
await mainWindow.screenshot({ path: 'test-results/before-send.png' });
|
||||
|
||||
// Get initial message count
|
||||
const initialMessages = await mainWindow.locator('.prose').count();
|
||||
|
||||
// Send message
|
||||
await chatInput.press('Enter');
|
||||
|
||||
// Wait for loading indicator to appear (using the specific class and text)
|
||||
console.log('Waiting for loading indicator...');
|
||||
const loadingGoose = await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"',
|
||||
{ timeout: 10000 });
|
||||
expect(await loadingGoose.isVisible()).toBe(true);
|
||||
|
||||
// Take screenshot of loading state
|
||||
await mainWindow.screenshot({ path: 'test-results/loading-state.png' });
|
||||
|
||||
// Wait for loading indicator to disappear
|
||||
console.log('Waiting for response...');
|
||||
await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"',
|
||||
{ state: 'hidden', timeout: 30000 });
|
||||
|
||||
// Wait for new message to appear
|
||||
await mainWindow.waitForFunction((count) => {
|
||||
const messages = document.querySelectorAll('.prose');
|
||||
return messages.length > count;
|
||||
}, initialMessages, { timeout: 30000 });
|
||||
|
||||
// Get the latest response
|
||||
const response = await mainWindow.locator('.prose').last();
|
||||
expect(await response.isVisible()).toBe(true);
|
||||
|
||||
// Verify response has content
|
||||
const responseText = await response.textContent();
|
||||
expect(responseText).toBeTruthy();
|
||||
expect(responseText.length).toBeGreaterThan(0);
|
||||
|
||||
// Take screenshot of response
|
||||
await mainWindow.screenshot({ path: 'test-results/chat-response.png' });
|
||||
});
|
||||
|
||||
test('verify chat history', async () => {
|
||||
console.log('Testing chat history...');
|
||||
|
||||
// Find the chat input again
|
||||
const chatInput = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]');
|
||||
|
||||
// Test message sending with a specific question
|
||||
await chatInput.fill('What is 2+2?');
|
||||
|
||||
// Get initial message count
|
||||
const initialMessages = await mainWindow.locator('.prose').count();
|
||||
|
||||
// Send message
|
||||
await chatInput.press('Enter');
|
||||
|
||||
// Wait for loading indicator and response using the correct selector
|
||||
await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"', { timeout: 10000 });
|
||||
await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"',
|
||||
{ state: 'hidden', timeout: 30000 });
|
||||
|
||||
// Wait for new message
|
||||
await mainWindow.waitForFunction((count) => {
|
||||
const messages = document.querySelectorAll('.prose');
|
||||
return messages.length > count;
|
||||
}, initialMessages, { timeout: 30000 });
|
||||
|
||||
// Get the latest response
|
||||
const response = await mainWindow.locator('.prose').last();
|
||||
const responseText = await response.textContent();
|
||||
expect(responseText).toBeTruthy();
|
||||
|
||||
// Check for message history
|
||||
const messages = await mainWindow.locator('.prose').all();
|
||||
expect(messages.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Take screenshot of chat history
|
||||
await mainWindow.screenshot({ path: 'test-results/chat-history.png' });
|
||||
|
||||
// Test command history (up arrow)
|
||||
await chatInput.press('Control+ArrowUp');
|
||||
const inputValue = await chatInput.inputValue();
|
||||
expect(inputValue).toBe('What is 2+2?');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user