E2E: added provider switching and MCP testing (#2029)

This commit is contained in:
Zane
2025-04-07 14:54:10 -07:00
committed by GitHub
parent e0baf577fc
commit 4ea14d0766
5 changed files with 927 additions and 141 deletions
+415 -140
View File
@@ -1,20 +1,131 @@
import { test, expect } from '@playwright/test';
import { test as base, 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';
import { showTestName, clearTestName } from './test-overlay';
const { runningQuotes } = require('./basic-mcp');
const execAsync = promisify(exec);
// Define provider interface
type Provider = {
name: string;
testPath: string;
};
// Create test fixture type
type TestFixtures = {
provider: Provider;
};
// Define available providers
const providers: Provider[] = [
{ name: 'Databricks', testPath: 'div:has(h3:text("Databricks"))[class*="relative bg-bgApp rounded-lg"]' },
{ name: 'Google', testPath: 'div:has(h3:text("Google"))[class*="relative bg-bgApp rounded-lg"]' }
];
// Create test with fixtures
const test = base.extend<TestFixtures>({
provider: [providers[0], { option: true }], // Default to first provider (Databricks)
});
// Store mainWindow reference
let mainWindow;
// Add hooks for test name overlay
// eslint-disable-next-line no-empty-pattern
test.beforeEach(async ({ }, testInfo) => {
if (mainWindow) {
// Get a clean test name without the full hierarchy
const testName = testInfo.titlePath[testInfo.titlePath.length - 1];
// Get provider name if we're in a provider suite
const providerSuite = testInfo.titlePath.find(t => t.startsWith('Provider:'));
const providerName = providerSuite ? providerSuite.split(': ')[1] : undefined;
console.log(`Setting overlay for test: "${testName}"${providerName ? ` (Provider: ${providerName})` : ''}`);
await showTestName(mainWindow, testName, providerName);
}
});
test.afterEach(async () => {
if (mainWindow) {
await clearTestName(mainWindow);
}
});
// Helper function to select a provider
async function selectProvider(mainWindow: any, provider: Provider) {
console.log(`Selecting provider: ${provider.name}`);
// Click the menu button (3 dots) if we're in chat
try {
// Wait for header and menu button to be visible
await mainWindow.waitForSelector('div[class*="bg-bgSubtle border-b border-borderSubtle"]', { timeout: 5000 });
await mainWindow.waitForTimeout(1000); // Give UI time to stabilize
const menuButton = await mainWindow.waitForSelector('button:has(svg)', {
timeout: 5000,
state: 'visible'
});
await menuButton.click();
// Wait for menu to be visible
await mainWindow.waitForTimeout(1000);
// Click "Reset provider and model"
const resetProviderButton = await mainWindow.waitForSelector('button:has-text("Reset provider and model")', { timeout: 5000 });
await resetProviderButton.click();
// Wait for page to start refreshing
await mainWindow.waitForTimeout(1000);
// Wait for page to finish reloading
await mainWindow.reload();
await mainWindow.waitForLoadState('networkidle');
} catch (e) {
console.log('Already on provider selection screen or error:', e);
}
// Wait for 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 provider card container
console.log(`Looking for ${provider.name} card...`);
const providerContainer = await mainWindow.waitForSelector(provider.testPath);
expect(await providerContainer.isVisible()).toBe(true);
// Find the Launch button within the provider container
console.log(`Looking for Launch button in ${provider.name} card...`);
const launchButton = await providerContainer.waitForSelector('button:has-text("Launch")');
expect(await launchButton.isVisible()).toBe(true);
// Take screenshot before clicking
await mainWindow.screenshot({ path: `test-results/before-${provider.name.toLowerCase()}-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);
// Take screenshot of chat interface
await mainWindow.screenshot({ path: `test-results/chat-interface-${provider.name.toLowerCase()}.png` });
}
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, '../..'),
@@ -48,28 +159,21 @@ test.describe('Goose App', () => {
...process.env,
ELECTRON_IS_DEV: '1',
NODE_ENV: 'development'
},
recordVideo: {
dir: 'test-results/videos/',
size: { width: 620, height: 680 }
}
});
// 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);
@@ -113,141 +217,312 @@ test.describe('Goose App', () => {
}
});
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' });
test.describe('General UI', () => {
test('dark mode toggle', async () => {
console.log('Testing dark mode toggle...');
// Click the three dots menu button in the top right
await mainWindow.waitForSelector('div[class*="bg-bgSubtle border-b border-borderSubtle"]');
const menuButton = await mainWindow.waitForSelector('button:has(svg)', { timeout: 10000 });
await menuButton.click();
// Find and click the dark mode toggle button
const darkModeButton = await mainWindow.waitForSelector('button:has-text("Dark Mode"), button:has-text("Light Mode")');
// Get initial state
const isDarkMode = await mainWindow.evaluate(() => document.documentElement.classList.contains('dark'));
console.log('Initial dark mode state:', isDarkMode);
// Click to toggle
await darkModeButton.click();
// Verify the change
const newDarkMode = await mainWindow.evaluate(() => document.documentElement.classList.contains('dark'));
expect(newDarkMode).toBe(!isDarkMode);
// Take screenshot to verify and pause to show the change
await mainWindow.screenshot({ path: 'test-results/dark-mode-toggle.png' });
await mainWindow.waitForTimeout(2000); // Pause in dark/light mode
// Toggle back to original state
await darkModeButton.click();
// 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' });
// Pause to show return to original state
await mainWindow.waitForTimeout(2000);
// Close menu with ESC key
await mainWindow.keyboard.press('Escape');
});
});
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' });
});
for (const provider of providers) {
test.describe(`Provider: ${provider.name}`, () => {
test.beforeAll(async () => {
// Select the provider once before all tests for this provider
await selectProvider(mainWindow, provider);
});
test('verify chat history', async () => {
console.log('Testing chat history...');
test.describe('Chat', () => {
test('chat interaction', async () => {
console.log(`Testing chat interaction with ${provider.name}...`);
// Find the chat input again
const chatInput = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]');
// Find the chat input
const chatInput = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]');
expect(await chatInput.isVisible()).toBe(true);
// Test message sending with a specific question
await chatInput.fill('What is 2+2?');
// Type a message
await chatInput.fill('Hello, can you help me with a simple task?');
// Get initial message count
const initialMessages = await mainWindow.locator('.prose').count();
// Take screenshot before sending
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-send.png` });
// Send message
await chatInput.press('Enter');
// Get initial message count
const initialMessages = await mainWindow.locator('.prose').count();
// 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 });
// Send message
await chatInput.press('Enter');
// Wait for new message
await mainWindow.waitForFunction((count) => {
const messages = document.querySelectorAll('.prose');
return messages.length > count;
}, initialMessages, { timeout: 30000 });
// Wait for loading indicator to appear
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);
// Get the latest response
const response = await mainWindow.locator('.prose').last();
const responseText = await response.textContent();
expect(responseText).toBeTruthy();
// Take screenshot of loading state
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-loading-state.png` });
// Check for message history
const messages = await mainWindow.locator('.prose').all();
expect(messages.length).toBeGreaterThanOrEqual(2);
// 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 });
// Take screenshot of chat history
await mainWindow.screenshot({ path: 'test-results/chat-history.png' });
// Wait for new message to appear
await mainWindow.waitForFunction((count) => {
const messages = document.querySelectorAll('.prose');
return messages.length > count;
}, initialMessages, { timeout: 30000 });
// Test command history (up arrow)
await chatInput.press('Control+ArrowUp');
const inputValue = await chatInput.inputValue();
expect(inputValue).toBe('What is 2+2?');
});
});
// 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/${provider.name.toLowerCase()}-chat-response.png` });
});
test('verify chat history', async () => {
console.log(`Testing chat history with ${provider.name}...`);
// 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
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/${provider.name.toLowerCase()}-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?');
});
});
test.describe('MCP Integration', () => {
test('running quotes MCP server integration', async () => {
console.log(`Testing Running Quotes MCP server integration with ${provider.name}...`);
// Clean up any existing running-quotes extensions from localStorage
await mainWindow.evaluate(() => {
const USER_SETTINGS_KEY = 'user_settings';
const settings = JSON.parse(localStorage.getItem(USER_SETTINGS_KEY) || '{"extensions":[]}');
// Remove any running-quotes extensions
settings.extensions = settings.extensions.filter(ext => ext.id !== 'running-quotes');
// Save back to localStorage
localStorage.setItem(USER_SETTINGS_KEY, JSON.stringify(settings));
// Log the cleanup
console.log('Cleaned up existing running-quotes extensions');
});
// Reload the page to ensure settings are fresh
await mainWindow.reload();
await mainWindow.waitForLoadState('networkidle');
// Click the menu button (3 dots)
const menuButton = await mainWindow.waitForSelector('button:has(svg)', { timeout: 10000 });
await menuButton.click();
// Click Advanced Settings
const advancedSettingsButton = await mainWindow.waitForSelector('button:has-text("Advanced Settings")');
await advancedSettingsButton.click();
// Wait for settings page and take screenshot
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-mcp-settings-page.png` });
// Click Add Custom Extension button and wait for modal
const addExtensionButton = await mainWindow.waitForSelector('button:has-text("Add Custom Extension")');
await addExtensionButton.click();
// Wait for modal and form to be fully rendered
await mainWindow.waitForSelector('form', { state: 'visible', timeout: 10000 });
console.log('Form found, waiting for modal animation...');
await mainWindow.waitForTimeout(1000); // Wait for modal animation
try {
// Fill ID (find by label text)
console.log('Filling ID field...');
await mainWindow.locator('label:has-text("ID *") + input[type="text"]').fill('running-quotes');
// Fill Name (find by label text)
console.log('Filling Name field...');
await mainWindow.locator('label:has-text("Name *") + input[type="text"]').fill('Running Quotes');
// Fill Description (find by label text)
console.log('Filling Description field...');
await mainWindow.locator('label:has-text("Description *") + input[type="text"]').fill('Inspirational running quotes MCP server');
// Fill Command (find by label text and placeholder)
console.log('Filling Command field...');
const mcpScriptPath = join(__dirname, 'basic-mcp.ts');
await mainWindow.locator('label:has-text("Command *") + input[placeholder="e.g. goosed mcp example"]')
.fill(`node ${mcpScriptPath}`);
// Take screenshot of filled form
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-mcp-form-filled.png` });
// Add a delay to inspect the form
console.log('Waiting 5 seconds to inspect form...');
await mainWindow.waitForTimeout(5000);
// Click Add button (it's a submit button)
console.log('Clicking Add button...');
await mainWindow.locator('button[type="submit"]').click();
// Wait for success toast and take screenshot
await mainWindow.waitForSelector('.Toastify__toast-body div div:has-text("Successfully enabled extension")',
{ state: 'visible', timeout: 10000 });
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-mcp-extension-added.png` });
console.log('Extension added successfully');
// Click Exit button to return to chat
const exitButton = await mainWindow.waitForSelector('button:has-text("Exit")', { timeout: 5000 });
await exitButton.click();
} catch (error) {
// Take error screenshot
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-mcp-form-error.png` });
console.error('Error during form filling:', error);
throw error;
}
});
test('test running quotes functionality', async () => {
console.log(`Testing running quotes functionality with ${provider.name}...`);
// 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 requesting a running quote
await chatInput.fill('Can you give me an inspirational running quote using the runningQuote tool?');
// Take screenshot before sending
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-quote-request.png` });
// Get initial message count
const initialMessages = await mainWindow.locator('.prose').count();
// Send message
await chatInput.press('Enter');
// Wait for loading indicator
const loadingIndicator = await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"',
{ timeout: 10000 });
expect(await loadingIndicator.isVisible()).toBe(true);
// Take screenshot of loading state
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-loading.png` });
// Wait for loading indicator to disappear
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);
// Click the Output dropdown to reveal the actual quote
const outputButton = await mainWindow.waitForSelector('button:has-text("Output")', { timeout: 5000 });
await outputButton.click();
// Wait a bit and dump HTML to see structure
await mainWindow.waitForTimeout(1000);
const html = await mainWindow.evaluate(() => document.documentElement.outerHTML);
console.log('Full page HTML after clicking Output:', html);
// Also dump just the response area HTML
const responseHtml = await response.evaluate(el => el.outerHTML);
console.log('Response area HTML:', responseHtml);
// Take screenshot before trying to find content
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response-debug.png` });
// Now try to get the output content
const outputContent = await mainWindow.waitForSelector('.whitespace-pre-wrap', { timeout: 5000 });
const outputText = await outputContent.textContent();
console.log('Output text:', outputText);
// Take screenshot of expanded response
await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response.png` });
// Check if the output contains one of our known quotes
const containsKnownQuote = runningQuotes.some(({ quote, author }) =>
outputText.includes(`"${quote}" - ${author}`)
);
expect(containsKnownQuote).toBe(true);
});
});
});
}
});
+71
View File
@@ -0,0 +1,71 @@
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
// Collection of running-related inspirational quotes
const runningQuotes = [
{
quote: "The miracle isn't that I finished. The miracle is that I had the courage to start.",
author: "John Bingham"
},
{
quote: "Running is the greatest metaphor for life, because you get out of it what you put into it.",
author: "Oprah Winfrey"
},
{
quote: "Pain is temporary. Quitting lasts forever.",
author: "Lance Armstrong"
},
{
quote: "Run when you can, walk if you have to, crawl if you must; just never give up.",
author: "Dean Karnazes"
},
{
quote: "The only bad workout is the one that didn't happen.",
author: "Unknown"
},
{
quote: "Whether you think you can or think you can't, you're right.",
author: "Henry Ford"
},
{
quote: "If you want to run, run a mile. If you want to experience a different life, run a marathon.",
author: "Emil Zatopek"
},
{
quote: "The voice inside your head that says you can't do this is a liar.",
author: "Unknown"
}
];
async function startServer() {
const server = new McpServer({
name: "Running Quotes",
version: "1.0.0"
});
server.tool("runningQuote",
"Generates an inspirational running quote",
async () => {
const randomQuote = runningQuotes[Math.floor(Math.random() * runningQuotes.length)];
return {
content: [{
type: "text",
text: `"${randomQuote.quote}" - ${randomQuote.author}`
}]
};
}
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Only start the server if this is the main module
if (require.main === module) {
startServer().catch(console.error);
}
module.exports = {
runningQuotes
};
+50
View File
@@ -0,0 +1,50 @@
// Helper function to show test name overlay
async function showTestName(mainWindow: any, testName: string, providerName?: string) {
await mainWindow.evaluate(({ name, provider }: { name: string, provider?: string }) => {
// Remove any existing overlay
const existing = document.getElementById('test-overlay');
if (existing) existing.remove();
// Create new overlay
const overlay = document.createElement('div');
overlay.id = 'test-overlay';
overlay.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 16px;
border-radius: 6px;
font-family: monospace;
font-size: 14px;
z-index: 2147483647; // maximum z-index integer value
pointer-events: none;
text-align: center;
max-width: 80%;
white-space: pre-wrap;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
`;
const testText = `Running: ${name}`;
const providerText = provider ? `\nProvider: ${provider}` : '';
overlay.textContent = testText + providerText;
// Insert at the beginning of <html> to ensure it's above everything
document.documentElement.insertBefore(overlay, document.documentElement.firstChild);
// Force a repaint to ensure the overlay is visible
overlay.getBoundingClientRect();
}, { name: testName, provider: providerName });
}
// Helper function to clear test name overlay
async function clearTestName(mainWindow: any) {
await mainWindow.evaluate(() => {
const overlay = document.getElementById('test-overlay');
if (overlay) overlay.remove();
});
}
export { showTestName, clearTestName };