Added Playwright E2E testing setup (#1893)

This commit is contained in:
Zane
2025-03-28 08:20:14 -07:00
committed by GitHub
parent 5fcb3eb984
commit 594fa7ef6b
5 changed files with 350 additions and 1 deletions

View File

@@ -3,3 +3,5 @@ node_modules
out
src/bin/goosed
/src/bin/goosed.exe
/playwright-report/
/test-results/

View File

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

View File

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

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

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