diff --git a/apps/desktop/src-tauri/src/commands/oauth.rs b/apps/desktop/src-tauri/src/commands/oauth.rs index f7141187..258afdda 100644 --- a/apps/desktop/src-tauri/src/commands/oauth.rs +++ b/apps/desktop/src-tauri/src/commands/oauth.rs @@ -656,6 +656,28 @@ pub async fn get_oauth_clients( Ok(client_infos) } +/// Approve a registered OAuth client by ID (for E2E testing). +/// In production, clients are approved via the consent flow. +#[tauri::command] +pub async fn approve_oauth_client( + client_id: String, + gateway_state: State<'_, Arc>>, +) -> Result<(), String> { + let app_state = gateway_state.read().await; + let Some(ref gw_state) = app_state.gateway_state else { + return Err("Gateway not running".to_string()); + }; + let state = gw_state.read().await; + let Some(repo) = state.inbound_client_repository() else { + return Err("Database not available".to_string()); + }; + repo.approve_client(&client_id) + .await + .map_err(|e| format!("Failed to approve client: {}", e))?; + info!("[OAuth] Approved client via test command: {}", client_id); + Ok(()) +} + /// Information about a connected OAuth client #[derive(Debug, Serialize, Deserialize)] pub struct OAuthClientInfo { diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 49ffafc9..3e498f1e 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -776,6 +776,7 @@ pub fn run() { commands::approve_oauth_consent, commands::get_pending_consent, commands::get_oauth_clients, + commands::approve_oauth_client, commands::update_oauth_client, commands::delete_oauth_client, commands::get_oauth_client_grants, diff --git a/apps/desktop/src/assets/client-icons/claude.svg b/apps/desktop/src/assets/client-icons/claude.svg new file mode 100644 index 00000000..62dc0db1 --- /dev/null +++ b/apps/desktop/src/assets/client-icons/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/apps/desktop/src/assets/client-icons/cursor.svg b/apps/desktop/src/assets/client-icons/cursor.svg new file mode 100644 index 00000000..6fe47e89 --- /dev/null +++ b/apps/desktop/src/assets/client-icons/cursor.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/desktop/src/assets/client-icons/vscode.png b/apps/desktop/src/assets/client-icons/vscode.png new file mode 100644 index 00000000..37a08476 Binary files /dev/null and b/apps/desktop/src/assets/client-icons/vscode.png differ diff --git a/apps/desktop/src/assets/client-icons/windsurf.svg b/apps/desktop/src/assets/client-icons/windsurf.svg new file mode 100644 index 00000000..3563d8fc --- /dev/null +++ b/apps/desktop/src/assets/client-icons/windsurf.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/desktop/src/features/clients/ClientsPage.tsx b/apps/desktop/src/features/clients/ClientsPage.tsx index 330ff893..33eaf143 100644 --- a/apps/desktop/src/features/clients/ClientsPage.tsx +++ b/apps/desktop/src/features/clients/ClientsPage.tsx @@ -1,5 +1,9 @@ import { useState, useEffect } from 'react'; import { listen } from '@tauri-apps/api/event'; +import cursorIcon from '@/assets/client-icons/cursor.svg'; +import vscodeIcon from '@/assets/client-icons/vscode.png'; +import claudeIcon from '@/assets/client-icons/claude.svg'; +import windsurfIcon from '@/assets/client-icons/windsurf.svg'; import { Laptop, Loader2, @@ -75,17 +79,35 @@ const CONNECTION_MODES = [ }, ]; -// Client icon component +// Bundled icons for well-known AI clients (matched by client_name) +const KNOWN_CLIENT_ICONS: Record = { + cursor: cursorIcon, + 'vs code': vscodeIcon, + vscode: vscodeIcon, + 'visual studio code': vscodeIcon, + 'claude desktop': claudeIcon, + claude: claudeIcon, + windsurf: windsurfIcon, + codeium: windsurfIcon, +}; + +// Client icon component โ€” uses bundled icon for known clients, falls back to logo_uri, then emoji function ClientIcon({ logo_uri, client_name }: { logo_uri?: string | null; client_name: string }) { - if (logo_uri) { - return {client_name}; + const iconUrl = KNOWN_CLIENT_ICONS[client_name.toLowerCase()] || logo_uri; + if (iconUrl) { + return ( + {client_name} { + e.currentTarget.style.display = 'none'; + e.currentTarget.parentElement!.append(document.createTextNode('๐Ÿค–')); + }} + /> + ); } - - // Default icons based on client name - if (client_name.toLowerCase().includes('cursor')) return 'โšก'; - if (client_name.toLowerCase().includes('vscode')) return '๐Ÿ’ป'; - if (client_name.toLowerCase().includes('code')) return '๐Ÿ“'; - return '๐Ÿค–'; + return ๐Ÿค–; } export default function ClientsPage() { diff --git a/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx b/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx index 44d0c817..92587e39 100644 --- a/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx +++ b/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx @@ -172,20 +172,28 @@ export function FeatureSetsPage() { loadData(viewSpace?.id); // Refresh list to get updated member counts etc. }; - // Filter feature sets (backend already filters server-all for disabled servers) - const filteredSets = featureSets.filter(fs => { - // Hide implicit custom sets - if (fs.name.endsWith(' - Custom')) return false; - - // Apply search filter - if (!searchQuery) return true; - const query = searchQuery.toLowerCase(); - return ( - fs.name.toLowerCase().includes(query) || - fs.description?.toLowerCase().includes(query) || - fs.feature_set_type.toLowerCase().includes(query) - ); - }); + // Filter and sort feature sets (backend already filters server-all for disabled servers) + const filteredSets = featureSets + .filter(fs => { + // Hide implicit custom sets + if (fs.name.endsWith(' - Custom')) return false; + + // Apply search filter + if (!searchQuery) return true; + const query = searchQuery.toLowerCase(); + return ( + fs.name.toLowerCase().includes(query) || + fs.description?.toLowerCase().includes(query) || + fs.feature_set_type.toLowerCase().includes(query) + ); + }) + .sort((a, b) => { + // Sort order: all โ†’ default โ†’ custom โ†’ server-all + const order: Record = { all: 0, default: 1, custom: 2, 'server-all': 3 }; + const aOrder = order[a.feature_set_type] ?? 2; + const bOrder = order[b.feature_set_type] ?? 2; + return aOrder - bOrder; + }); return ( <> diff --git a/apps/desktop/src/features/registry/ServerCard.tsx b/apps/desktop/src/features/registry/ServerCard.tsx index 9a0a0447..58a219be 100644 --- a/apps/desktop/src/features/registry/ServerCard.tsx +++ b/apps/desktop/src/features/registry/ServerCard.tsx @@ -102,7 +102,13 @@ export function ServerCard({ > {/* Header */}
-
{server.icon || '๐Ÿ“ฆ'}
+
+ {server.icon?.startsWith('http') ? ( + { e.currentTarget.style.display = 'none'; e.currentTarget.parentElement!.append(document.createTextNode('๐Ÿ“ฆ')); }} /> + ) : ( + server.icon || '๐Ÿ“ฆ' + )} +

diff --git a/apps/desktop/src/features/servers/ServerCard.tsx b/apps/desktop/src/features/servers/ServerCard.tsx index ec43b332..5aa6bf33 100644 --- a/apps/desktop/src/features/servers/ServerCard.tsx +++ b/apps/desktop/src/features/servers/ServerCard.tsx @@ -218,7 +218,11 @@ export function ServerCard({
{/* Icon */}
- {server.icon || "๐Ÿ”Œ"} + {server.icon?.startsWith('http') ? ( + { e.currentTarget.style.display = 'none'; e.currentTarget.parentElement!.append(document.createTextNode('๐Ÿ“ฆ')); }} /> + ) : ( + server.icon || "๐Ÿ”Œ" + )}
{/* Name & Status */} diff --git a/apps/desktop/src/features/servers/ServersPage.tsx b/apps/desktop/src/features/servers/ServersPage.tsx index 73e00648..52daf44c 100644 --- a/apps/desktop/src/features/servers/ServersPage.tsx +++ b/apps/desktop/src/features/servers/ServersPage.tsx @@ -899,6 +899,7 @@ export function ServersPage() { )} -
{server.icon || '๐Ÿ“ฆ'}
+
+ {server.icon?.startsWith('http') ? ( + { e.currentTarget.style.display = 'none'; e.currentTarget.parentElement!.append(document.createTextNode('๐Ÿ“ฆ')); }} /> + ) : ( + server.icon || '๐Ÿ“ฆ' + )} +
{server.name}
diff --git a/apps/desktop/src/features/spaces/SpacesPage.tsx b/apps/desktop/src/features/spaces/SpacesPage.tsx index 1c62ddc4..7e9ca240 100644 --- a/apps/desktop/src/features/spaces/SpacesPage.tsx +++ b/apps/desktop/src/features/spaces/SpacesPage.tsx @@ -200,10 +200,10 @@ export function SpacesPage() { }`} data-testid={`space-card-${space.id}`} > - + {/* Header */} -
-
+
+
{space.icon || '๐ŸŒ'}
diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index deb43343..95f8e522 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -211,29 +211,29 @@ /* Responsive auto-fill grid for cards */ .auto-fill-cards { display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); gap: 1.5rem; } /* Larger cards on wider screens */ @media (min-width: 1280px) { .auto-fill-cards { - grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); - gap: 2rem; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.75rem; } } @media (min-width: 1536px) { .auto-fill-cards { - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 2rem; } } @media (min-width: 1920px) { .auto-fill-cards { - grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); - gap: 2.5rem; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 2rem; } } } diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md new file mode 100644 index 00000000..2868791c --- /dev/null +++ b/docs/screenshots/README.md @@ -0,0 +1,61 @@ +# Screenshot Generation + +Automated screenshots of the McpMux desktop app for docs and marketing. + +## Quick Start + +```bash +# 1. Build the desktop app (required โ€” E2E runs against the release binary) +pnpm build + +# 2. Run the screenshot capture spec +pnpm exec wdio run tests/e2e/wdio.conf.ts --spec tests/e2e/specs/capture-screenshots.manual.ts +``` + +## What Gets Captured + +| Screenshot | Page | Content | +|---|---|---| +| `dashboard.png` | Dashboard | Gateway status, stats cards, client config snippet | +| `servers.png` | My Servers | Installed servers with connection states | +| `discover.png` | Discover | Server registry (mock bundle with 12 servers) | +| `spaces.png` | Spaces | Workspace cards (default + 3 additional) | +| `featuresets.png` | FeatureSets | Permission bundles | +| `clients.png` | Clients | Connected AI clients (Cursor, VS Code, Claude Desktop) | +| `settings.png` | Settings | App settings page | + +## Output Locations + +Screenshots are saved to two locations: + +- **`mcp-mux/docs/screenshots/`** โ€” Used by the project README +- **`mcpmux.discover.ui/public/screenshots/`** โ€” Used by the discover website + +## Editing Mock Data + +All screenshot mock data lives in a single file: + +``` +tests/e2e/mocks/screenshot-preseed.ts +``` + +This controls: +- **Spaces** created (name, icon) +- **Servers** installed and enabled (by ID from `fixtures.ts`) +- **FeatureSets** created (name, description) +- **OAuth Clients** shown on the Clients page (name, version, connection mode) + +After editing, re-run the capture command above. + +## How It Works + +1. The E2E framework launches the Tauri app with a mock bundle API (port 8787) +2. The `before()` hook seeds data via `window.__TAURI_TEST_API__` (spaces, servers, feature sets) +3. OAuth clients are mocked by intercepting `window.__TAURI_INTERNALS__.invoke` +4. Each `it()` block navigates to a page and saves a screenshot + +## Notes + +- This spec uses `.manual.ts` (not `.wdio.ts`) so it is **excluded from `pnpm test:e2e`** โ€” it only runs when invoked explicitly +- Server definitions come from `tests/e2e/mocks/mock-bundle-api/fixtures.ts` +- The app must be built before running (`pnpm build`) โ€” the signing error is non-blocking diff --git a/docs/screenshots/clients.png b/docs/screenshots/clients.png index 755bd59c..b14863d7 100644 Binary files a/docs/screenshots/clients.png and b/docs/screenshots/clients.png differ diff --git a/docs/screenshots/dashboard.png b/docs/screenshots/dashboard.png index 7869e308..8a42460e 100644 Binary files a/docs/screenshots/dashboard.png and b/docs/screenshots/dashboard.png differ diff --git a/docs/screenshots/discover.png b/docs/screenshots/discover.png index 0847b1fa..784ab39f 100644 Binary files a/docs/screenshots/discover.png and b/docs/screenshots/discover.png differ diff --git a/docs/screenshots/featuresets.png b/docs/screenshots/featuresets.png index b62a1b5e..5d426d98 100644 Binary files a/docs/screenshots/featuresets.png and b/docs/screenshots/featuresets.png differ diff --git a/docs/screenshots/servers.png b/docs/screenshots/servers.png index 71dbec6b..4bbea31d 100644 Binary files a/docs/screenshots/servers.png and b/docs/screenshots/servers.png differ diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png index f06057d0..b97121ba 100644 Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ diff --git a/docs/screenshots/spaces.png b/docs/screenshots/spaces.png index 5e1d3a03..44cfca01 100644 Binary files a/docs/screenshots/spaces.png and b/docs/screenshots/spaces.png differ diff --git a/tests/e2e/helpers/selectors.ts b/tests/e2e/helpers/selectors.ts index 7bc2886b..b6220335 100644 --- a/tests/e2e/helpers/selectors.ts +++ b/tests/e2e/helpers/selectors.ts @@ -21,21 +21,29 @@ export const byTestId = (testId: string) => $(`[data-testid="${testId}"]`); */ export async function waitForModalClose(timeout = TIMEOUT.short): Promise { try { - const overlay = await $('.fixed.inset-0.bg-black\\/20'); - const exists = await overlay.isExisting().catch(() => false); - - if (!exists) { - return; // No modal, nothing to wait for - } - - // Try to wait for it to close naturally - const closed = await overlay.waitForDisplayed({ timeout, reverse: true }).then(() => true).catch(() => false); - - if (!closed) { - // Modal still open - try to dismiss it with Escape key - console.log('[waitForModalClose] Modal still displayed, trying Escape key'); - await browser.keys('Escape'); - await browser.pause(500); + // Match any fixed fullscreen overlay (bg-black/20, bg-black/50, bg-black/60) + const overlays = await $$('.fixed.inset-0'); + + for (const overlay of overlays) { + const isDisplayed = await overlay.isDisplayed().catch(() => false); + if (!isDisplayed) continue; + + // Check if it looks like a modal backdrop (has bg-black in its classes) + const cls = await overlay.getAttribute('class').catch(() => '') ?? ''; + if (!cls.includes('bg-black')) continue; + + // Try to wait for it to close naturally + const closed = await overlay + .waitForDisplayed({ timeout, reverse: true }) + .then(() => true) + .catch(() => false); + + if (!closed) { + // Modal still open - try to dismiss it with Escape key + console.log('[waitForModalClose] Modal still displayed, trying Escape key'); + await browser.keys('Escape'); + await browser.pause(500); + } } } catch { // Silently continue - modal handling shouldn't fail tests diff --git a/tests/e2e/helpers/tauri-api.ts b/tests/e2e/helpers/tauri-api.ts index 8b21bf6c..14de4fcc 100644 --- a/tests/e2e/helpers/tauri-api.ts +++ b/tests/e2e/helpers/tauri-api.ts @@ -115,6 +115,7 @@ export async function createFeatureSet(input: { name: string; space_id: string; description?: string; + icon?: string; }): Promise { return invoke('create_feature_set', { input }); } @@ -130,7 +131,7 @@ export async function deleteFeatureSet(id: string): Promise { export interface InstalledServer { id: string; space_id: string; - server_id: string; // Definition ID (e.g. "echo-server") + server_id: string; // Definition ID (e.g. "github-server") is_enabled?: boolean; enabled?: boolean; input_values: Record; @@ -164,6 +165,24 @@ export async function disableServerV2(spaceId: string, serverId: string): Promis return invoke('disable_server_v2', { spaceId, serverId }); } +// ============================================================================ +// Registry API +// ============================================================================ + +/** Force-refresh the server registry bundle (bypasses cache). */ +export async function refreshRegistry(): Promise { + return invoke('refresh_registry'); +} + +// ============================================================================ +// OAuth API +// ============================================================================ + +/** Approve a DCR-registered OAuth client by ID (for E2E testing). */ +export async function approveOAuthClient(clientId: string): Promise { + return invoke('approve_oauth_client', { clientId }); +} + // ============================================================================ // Gateway API // ============================================================================ diff --git a/tests/e2e/mocks/mock-bundle-api/fixtures.ts b/tests/e2e/mocks/mock-bundle-api/fixtures.ts index 8f3c8ee5..85ea980e 100644 --- a/tests/e2e/mocks/mock-bundle-api/fixtures.ts +++ b/tests/e2e/mocks/mock-bundle-api/fixtures.ts @@ -1,8 +1,11 @@ /** * Bundle Fixtures for E2E Testing * - * These fixtures define test servers that point to our stub MCP servers, + * These fixtures define realistic MCP servers that point to our stub MCP servers, * covering all transport types, auth modes, and input configurations. + * + * For screenshots, the names/icons/descriptions are realistic (GitHub, Slack, etc.) + * but the actual transport commands/urls point to our local stub servers. */ // Stub server ports @@ -90,18 +93,18 @@ export interface RegistryBundle { }; } -// Test servers pointing to our stub MCP servers +// Realistic MCP servers pointing to our stub servers for E2E const TEST_SERVERS: ServerDefinition[] = [ - // 1. Stdio server with npx (no inputs, no auth) - simplest case + // 1. GitHub โ€” stdio, no auth, official { - id: 'echo-server', - name: 'Echo Server', - alias: 'echo', - description: 'Simple echo server for testing - returns what you send', - icon: '๐Ÿ”Š', + id: 'github-server', + name: 'GitHub', + alias: 'github', + description: 'Repository management, issues, pull requests, and code search via the GitHub API', + icon: 'https://cdn.simpleicons.org/github', schema_version: '2.0', categories: ['developer-tools'], - tags: ['test', 'echo', 'simple'], + tags: ['github', 'git', 'repository', 'issues'], transport: { type: 'stdio', command: 'node', @@ -115,10 +118,14 @@ const TEST_SERVERS: ServerDefinition[] = [ type: 'none', }, publisher: { - name: 'McpMux Test', + name: 'Model Context Protocol', + domain: 'modelcontextprotocol.io', verified: true, - domain_verified: false, - official: false, + domain_verified: true, + official: true, + }, + links: { + repository: 'https://github.com/modelcontextprotocol/servers', }, platforms: ['all'], capabilities: { @@ -128,50 +135,164 @@ const TEST_SERVERS: ServerDefinition[] = [ }, }, - // 2. Stdio server with API key input + // 2. Filesystem โ€” stdio, no auth, official { - id: 'api-key-server', - name: 'API Key Server', - alias: 'apikey', - description: 'Server requiring an API key input for testing input handling', - icon: '๐Ÿ”‘', + id: 'filesystem-server', + name: 'Filesystem', + alias: 'fs', + description: 'Secure file operations with configurable access controls and sandboxing', + icon: 'https://cdn.simpleicons.org/files/4285F4', schema_version: '2.0', - categories: ['developer-tools'], - tags: ['test', 'api-key', 'auth'], + categories: ['file-system'], + tags: ['filesystem', 'files', 'directory', 'local'], + transport: { + type: 'stdio', + command: 'node', + args: ['--experimental-strip-types', 'tests/e2e/mocks/stub-mcp-server/stdio-server.ts', '${input:DIRECTORY}'], + env: {}, + metadata: { + inputs: [ + { + id: 'DIRECTORY', + label: 'Allowed Directory', + description: 'Directory the server can access', + type: 'text', + required: true, + secret: false, + placeholder: 'C:\\Users\\Projects', + }, + ], + }, + }, + auth: { + type: 'none', + }, + publisher: { + name: 'Model Context Protocol', + domain: 'modelcontextprotocol.io', + verified: true, + domain_verified: true, + official: true, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: true, + prompts: false, + }, + }, + + // 3. PostgreSQL โ€” stdio, api_key, official + { + id: 'postgres-server', + name: 'PostgreSQL', + alias: 'postgres', + description: 'Query databases, manage schemas, inspect tables, and run migrations', + icon: 'https://cdn.simpleicons.org/postgresql', + schema_version: '2.0', + categories: ['database'], + tags: ['postgres', 'database', 'sql', 'schema'], transport: { type: 'stdio', command: 'node', args: ['--experimental-strip-types', 'tests/e2e/mocks/stub-mcp-server/stdio-server.ts'], env: { - TEST_API_KEY: '${input:API_KEY}', + DATABASE_URL: '${input:DATABASE_URL}', }, metadata: { inputs: [ { - id: 'API_KEY', - label: 'Test API Key', - description: 'API key for testing (any value works)', + id: 'DATABASE_URL', + label: 'Connection String', + description: 'PostgreSQL connection URL', type: 'password', required: true, secret: true, - placeholder: 'test_key_xxx', - obtain: { - url: 'https://example.com/get-key', - instructions: 'For testing, enter any value', - button_label: 'Get Test Key', - }, + placeholder: 'postgresql://user:pass@localhost:5432/db', }, ], }, }, auth: { type: 'api_key', - instructions: 'Enter any value for testing', + instructions: 'Provide your PostgreSQL connection string', }, publisher: { - name: 'McpMux Test', + name: 'Model Context Protocol', + domain: 'modelcontextprotocol.io', verified: true, - domain_verified: false, + domain_verified: true, + official: true, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: true, + prompts: false, + }, + }, + + // 4. Slack โ€” http, oauth + { + id: 'slack-server', + name: 'Slack', + alias: 'slack', + description: 'Send messages, manage channels, and search conversations across workspaces', + icon: 'https://github.com/slackapi.png?size=128', + schema_version: '2.0', + categories: ['productivity'], + tags: ['slack', 'messaging', 'chat', 'team'], + transport: { + type: 'http', + url: `http://localhost:${STUB_OAUTH_PORT}/mcp`, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'oauth', + }, + publisher: { + name: 'Slack', + domain: 'slack.com', + verified: true, + domain_verified: true, + official: false, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: false, + prompts: true, + }, + }, + + // 5. Brave Search โ€” http, api_key + { + id: 'brave-search', + name: 'Brave Search', + alias: 'brave', + description: 'Web search with privacy-focused results and AI-ready summaries', + icon: 'https://cdn.simpleicons.org/brave', + schema_version: '2.0', + categories: ['search'], + tags: ['search', 'web', 'brave', 'privacy'], + transport: { + type: 'http', + url: `http://localhost:${STUB_HTTP_PORT}/mcp`, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'api_key', + instructions: 'Get your API key from search.brave.com', + }, + publisher: { + name: 'Brave', + domain: 'brave.com', + verified: true, + domain_verified: true, official: false, }, platforms: ['all'], @@ -182,40 +303,31 @@ const TEST_SERVERS: ServerDefinition[] = [ }, }, - // 3. Stdio server with directory input (like Filesystem) + // 6. Docker โ€” stdio, no auth { - id: 'directory-server', - name: 'Directory Server', - alias: 'dir', - description: 'Server with directory path input for testing path inputs', - icon: '๐Ÿ“‚', + id: 'docker-server', + name: 'Docker', + alias: 'docker', + description: 'Manage containers, images, networks, and volumes from your AI client', + icon: 'https://cdn.simpleicons.org/docker', schema_version: '2.0', - categories: ['file-system'], - tags: ['test', 'directory', 'path'], + categories: ['developer-tools'], + tags: ['docker', 'containers', 'devops', 'infrastructure'], transport: { type: 'stdio', command: 'node', - args: ['--experimental-strip-types', 'tests/e2e/mocks/stub-mcp-server/stdio-server.ts', '${input:DIRECTORY}'], + args: ['--experimental-strip-types', 'tests/e2e/mocks/stub-mcp-server/stdio-server.ts'], env: {}, metadata: { - inputs: [ - { - id: 'DIRECTORY', - label: 'Target Directory', - description: 'Directory path to operate on', - type: 'text', - required: true, - secret: false, - placeholder: 'C:\\Users\\test', - }, - ], + inputs: [], }, }, auth: { type: 'none', }, publisher: { - name: 'McpMux Test', + name: 'Docker', + domain: 'docker.com', verified: true, domain_verified: false, official: false, @@ -228,16 +340,51 @@ const TEST_SERVERS: ServerDefinition[] = [ }, }, - // 4. HTTP server with no auth (like Cloudflare Docs) + // 7. Notion โ€” http, oauth { - id: 'http-noauth-server', - name: 'HTTP Server (No Auth)', - alias: 'httptest', - description: 'HTTP server without authentication for testing remote connections', - icon: '๐ŸŒ', + id: 'notion-server', + name: 'Notion', + alias: 'notion', + description: 'Create pages, query databases, and manage workspaces programmatically', + icon: 'https://cdn.simpleicons.org/notion', + schema_version: '2.0', + categories: ['productivity'], + tags: ['notion', 'notes', 'wiki', 'database'], + transport: { + type: 'http', + url: `http://localhost:${STUB_OAUTH_PORT}/mcp`, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'oauth', + }, + publisher: { + name: 'Notion', + domain: 'notion.so', + verified: true, + domain_verified: true, + official: false, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: true, + prompts: false, + }, + }, + + // 8. AWS โ€” http, api_key + { + id: 'aws-server', + name: 'AWS', + alias: 'aws', + description: 'Interact with S3, Lambda, DynamoDB, and other AWS services', + icon: 'https://github.com/aws.png?size=128', schema_version: '2.0', categories: ['cloud'], - tags: ['test', 'http', 'remote'], + tags: ['aws', 'cloud', 's3', 'lambda'], transport: { type: 'http', url: `http://localhost:${STUB_HTTP_PORT}/mcp`, @@ -246,12 +393,14 @@ const TEST_SERVERS: ServerDefinition[] = [ }, }, auth: { - type: 'none', + type: 'api_key', + instructions: 'Provide your AWS access key and secret', }, publisher: { - name: 'McpMux Test', + name: 'Amazon Web Services', + domain: 'aws.amazon.com', verified: true, - domain_verified: false, + domain_verified: true, official: false, }, platforms: ['all'], @@ -262,16 +411,89 @@ const TEST_SERVERS: ServerDefinition[] = [ }, }, - // 5. HTTP server with OAuth (like Atlassian) + // 9. SQLite โ€” stdio, no auth { - id: 'http-oauth-server', - name: 'HTTP Server (OAuth)', - alias: 'oauthtest', - description: 'HTTP server with OAuth authentication for testing auth flows', - icon: '๐Ÿ”', + id: 'sqlite-server', + name: 'SQLite', + alias: 'sqlite', + description: 'Local database queries with read/write access control and schema inspection', + icon: 'https://cdn.simpleicons.org/sqlite', + schema_version: '2.0', + categories: ['database'], + tags: ['sqlite', 'database', 'local', 'sql'], + transport: { + type: 'stdio', + command: 'node', + args: ['--experimental-strip-types', 'tests/e2e/mocks/stub-mcp-server/stdio-server.ts'], + env: {}, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'none', + }, + publisher: { + name: 'Model Context Protocol', + domain: 'modelcontextprotocol.io', + verified: true, + domain_verified: true, + official: true, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: true, + prompts: false, + }, + }, + + // 10. Sentry โ€” http, api_key + { + id: 'sentry-server', + name: 'Sentry', + alias: 'sentry', + description: 'Error tracking, performance monitoring, and release management', + icon: 'https://cdn.simpleicons.org/sentry', + schema_version: '2.0', + categories: ['developer-tools'], + tags: ['sentry', 'errors', 'monitoring', 'performance'], + transport: { + type: 'http', + url: `http://localhost:${STUB_HTTP_PORT}/mcp`, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'api_key', + instructions: 'Get your auth token from sentry.io', + }, + publisher: { + name: 'Sentry', + domain: 'sentry.io', + verified: true, + domain_verified: true, + official: false, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: false, + prompts: false, + }, + }, + + // 11. Linear โ€” http, oauth + { + id: 'linear-server', + name: 'Linear', + alias: 'linear', + description: 'Issue tracking, project management, and sprint workflows', + icon: 'https://cdn.simpleicons.org/linear', schema_version: '2.0', categories: ['productivity'], - tags: ['test', 'http', 'oauth'], + tags: ['linear', 'issues', 'project-management', 'sprints'], transport: { type: 'http', url: `http://localhost:${STUB_OAUTH_PORT}/mcp`, @@ -283,9 +505,10 @@ const TEST_SERVERS: ServerDefinition[] = [ type: 'oauth', }, publisher: { - name: 'McpMux Test', + name: 'Linear', + domain: 'linear.app', verified: true, - domain_verified: false, + domain_verified: true, official: false, }, platforms: ['all'], @@ -296,16 +519,51 @@ const TEST_SERVERS: ServerDefinition[] = [ }, }, - // 6. HTTP server with API key (header-based) + // 12. Cloudflare โ€” http, no auth { - id: 'http-apikey-server', - name: 'HTTP Server (API Key)', - alias: 'httpkey', - description: 'HTTP server requiring API key in header', - icon: '๐Ÿ”’', + id: 'cloudflare-server', + name: 'Cloudflare', + alias: 'cf', + description: 'Search and browse Cloudflare documentation and API references', + icon: 'https://cdn.simpleicons.org/cloudflare', schema_version: '2.0', - categories: ['search'], - tags: ['test', 'http', 'api-key'], + categories: ['cloud'], + tags: ['cloudflare', 'docs', 'cdn', 'workers'], + transport: { + type: 'http', + url: `http://localhost:${STUB_HTTP_PORT}/mcp`, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'none', + }, + publisher: { + name: 'Cloudflare', + domain: 'cloudflare.com', + verified: true, + domain_verified: true, + official: false, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: true, + prompts: false, + }, + }, + + // 13. Cloudflare Workers โ€” http, api_key + { + id: 'cloudflare-workers-server', + name: 'Cloudflare Workers', + alias: 'cf-workers', + description: 'Deploy, manage, and monitor Cloudflare Workers and KV namespaces', + icon: 'https://cdn.simpleicons.org/cloudflareworkers', + schema_version: '2.0', + categories: ['cloud'], + tags: ['cloudflare', 'workers', 'serverless', 'edge'], transport: { type: 'http', url: `http://localhost:${STUB_HTTP_PORT}/mcp`, @@ -315,18 +573,55 @@ const TEST_SERVERS: ServerDefinition[] = [ }, auth: { type: 'api_key', - instructions: 'Enter any value for testing', + instructions: 'Provide your Cloudflare API token', }, publisher: { - name: 'McpMux Test', + name: 'Cloudflare', + domain: 'cloudflare.com', verified: true, - domain_verified: false, + domain_verified: true, official: false, }, platforms: ['all'], capabilities: { tools: true, - resources: false, + resources: true, + prompts: false, + }, + }, + + // 14. Azure โ€” http, api_key + { + id: 'azure-server', + name: 'Azure', + alias: 'azure', + description: 'Manage Azure resources, deploy services, and monitor infrastructure', + icon: 'https://github.com/azure.png?size=128', + schema_version: '2.0', + categories: ['cloud'], + tags: ['azure', 'cloud', 'microsoft', 'infrastructure'], + transport: { + type: 'http', + url: `http://localhost:${STUB_HTTP_PORT}/mcp`, + metadata: { + inputs: [], + }, + }, + auth: { + type: 'api_key', + instructions: 'Provide your Azure subscription credentials', + }, + publisher: { + name: 'Microsoft', + domain: 'azure.microsoft.com', + verified: true, + domain_verified: true, + official: false, + }, + platforms: ['all'], + capabilities: { + tools: true, + resources: true, prompts: false, }, }, @@ -335,6 +630,7 @@ const TEST_SERVERS: ServerDefinition[] = [ const TEST_CATEGORIES: Category[] = [ { id: 'developer-tools', name: 'Developer Tools', icon: '๐Ÿ’ป' }, { id: 'file-system', name: 'File System', icon: '๐Ÿ“‚' }, + { id: 'database', name: 'Database', icon: '๐Ÿ—„๏ธ' }, { id: 'cloud', name: 'Cloud', icon: 'โ˜๏ธ' }, { id: 'productivity', name: 'Productivity', icon: 'โšก' }, { id: 'search', name: 'Search', icon: '๐Ÿ”' }, @@ -355,7 +651,10 @@ export const BUNDLE_DATA: RegistryBundle = { { id: 'all', label: 'All Categories' }, { id: 'developer-tools', label: 'Developer Tools', icon: '๐Ÿ’ป', match: { field: 'categories', operator: 'contains', value: 'developer-tools' } }, { id: 'file-system', label: 'File System', icon: '๐Ÿ“‚', match: { field: 'categories', operator: 'contains', value: 'file-system' } }, + { id: 'database', label: 'Database', icon: '๐Ÿ—„๏ธ', match: { field: 'categories', operator: 'contains', value: 'database' } }, { id: 'cloud', label: 'Cloud', icon: 'โ˜๏ธ', match: { field: 'categories', operator: 'contains', value: 'cloud' } }, + { id: 'productivity', label: 'Productivity', icon: 'โšก', match: { field: 'categories', operator: 'contains', value: 'productivity' } }, + { id: 'search', label: 'Search', icon: '๐Ÿ”', match: { field: 'categories', operator: 'contains', value: 'search' } }, ], }, { @@ -391,6 +690,6 @@ export const BUNDLE_DATA: RegistryBundle = { items_per_page: 24, }, home: { - featured_server_ids: ['echo-server', 'http-noauth-server'], + featured_server_ids: ['github-server', 'postgres-server', 'slack-server'], }, }; diff --git a/tests/e2e/mocks/screenshot-preseed.ts b/tests/e2e/mocks/screenshot-preseed.ts new file mode 100644 index 00000000..49d9d8ec --- /dev/null +++ b/tests/e2e/mocks/screenshot-preseed.ts @@ -0,0 +1,44 @@ +/** + * Screenshot Preseed Data + * + * Single source of truth for all mock data used in screenshot capture. + * Edit this file to change what appears in screenshots, then re-run: + * pnpm exec wdio run tests/e2e/wdio.conf.ts --spec tests/e2e/specs/capture-screenshots.manual.ts + */ + +export const PRESEED: { + spaces: { name: string; icon: string }[]; + serversToInstall: string[]; + featureSets: { name: string; description: string; icon?: string }[]; +} = { + /** Additional spaces to create (default space is created automatically) */ + spaces: [ + { name: 'Work Projects', icon: '๐Ÿ’ผ' }, + { name: 'Personal', icon: '๐Ÿ ' }, + { name: 'Experiments', icon: '๐Ÿงช' }, + { name: 'Production', icon: '๐Ÿš€' }, + { name: 'Staging', icon: '๐Ÿ”ง' }, + ], + + /** Server IDs to install in the default space (must match IDs in fixtures.ts). + * Order matters โ€” GitHub first for screenshot prominence. */ + serversToInstall: [ + 'github-server', + 'filesystem-server', + 'postgres-server', + 'slack-server', + 'brave-search', + 'docker-server', + 'notion-server', + 'aws-server', + 'cloudflare-workers-server', + 'azure-server', + ], + + /** Custom feature sets to create in the default space */ + featureSets: [ + { name: 'Read Only', description: 'Only read operations โ€” no writes or deletes', icon: '๐Ÿ”’' }, + { name: 'Dev Tools', description: 'GitHub + PostgreSQL + Filesystem access', icon: '๐Ÿ› ๏ธ' }, + { name: 'Full Access', description: 'All servers and capabilities enabled', icon: '๐Ÿš€' }, + ], +}; diff --git a/tests/e2e/specs/capture-screenshots.manual.ts b/tests/e2e/specs/capture-screenshots.manual.ts new file mode 100644 index 00000000..2336e750 --- /dev/null +++ b/tests/e2e/specs/capture-screenshots.manual.ts @@ -0,0 +1,356 @@ +/** + * Screenshot Capture for Docs & Marketing + * + * Seeds realistic data, navigates to each page, and saves screenshots + * to docs/screenshots/ for the README and discover UI. + * + * Strategy: + * - Real Tauri API calls for spaces, servers, feature sets, gateway, OAuth clients + * - Servers are enabled via enable_server_v2 (sets enabled=true in DB) + * - Server connection status overridden to "connected" via Tauri event emission + * (window.__TAURI_INTERNALS__.invoke is non-writable/non-configurable in Tauri v2, + * so we cannot mock invoke; instead we emit events that useServerManager listens to) + * - OAuth clients registered via HTTP POST to gateway's DCR endpoint, then approved + * + * This file uses .manual.ts (not .wdio.ts) so it is excluded from `pnpm test:e2e`. + * It must be invoked explicitly: + * + * pnpm exec wdio run tests/e2e/wdio.conf.ts --spec tests/e2e/specs/capture-screenshots.manual.ts + * + * Prerequisites: + * - Desktop app built: `pnpm build` (needs target/release/mcpmux.exe) + * - Mock bundle API fixtures updated: tests/e2e/mocks/mock-bundle-api/fixtures.ts + * + * To change what appears in screenshots, edit: + * tests/e2e/mocks/screenshot-preseed.ts (spaces, servers to install, feature sets) + * This file's OAUTH_CLIENTS constant (DCR client registrations) + * + * Output: + * - docs/screenshots/*.png (for README) + * - ../mcpmux.discover.ui/public/screenshots/*.png (for discover site) + */ + +import path from 'path'; +import fs from 'fs'; +import { byTestId, safeClick } from '../helpers/selectors'; +import { + createSpace, + setActiveSpace, + createFeatureSet, + installServer, + getActiveSpace, + refreshRegistry, + enableServerV2, + emitEvent, + approveOAuthClient, +} from '../helpers/tauri-api'; +import { PRESEED } from '../mocks/screenshot-preseed'; + +// Output paths +const DOCS_DIR = path.resolve('./docs/screenshots'); +const DISCOVER_DIR = path.resolve('../mcpmux.discover.ui/public/screenshots'); + +function ensureDir(dir: string) { + fs.mkdirSync(dir, { recursive: true }); +} + +async function saveScreenshot(name: string) { + ensureDir(DOCS_DIR); + const docsPath = path.join(DOCS_DIR, `${name}.png`); + await browser.saveScreenshot(docsPath); + console.log(`[screenshot] Saved: ${docsPath}`); + + // Copy to discover UI public folder + try { + ensureDir(DISCOVER_DIR); + const discoverPath = path.join(DISCOVER_DIR, `${name}.png`); + fs.copyFileSync(docsPath, discoverPath); + console.log(`[screenshot] Copied to: ${discoverPath}`); + } catch (err) { + console.warn(`[screenshot] Could not copy to discover UI: ${err}`); + } +} + +// โ”€โ”€ OAuth Client Definitions for DCR Registration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const OAUTH_CLIENTS = [ + { + client_name: 'Cursor', + redirect_uris: ['http://127.0.0.1:6274/callback'], + logo_uri: 'https://github.com/getcursor.png?size=128', + software_id: 'com.cursor.app', + software_version: '0.48.2', + }, + { + client_name: 'VS Code', + redirect_uris: ['http://127.0.0.1:6275/callback'], + logo_uri: 'https://github.com/microsoft.png?size=128', + software_id: 'com.microsoft.vscode', + software_version: '1.96.4', + }, + { + client_name: 'Claude Desktop', + redirect_uris: ['http://127.0.0.1:6276/callback'], + logo_uri: 'https://github.com/anthropics.png?size=128', + software_id: 'com.anthropic.claude-desktop', + software_version: '1.2.0', + }, + { + client_name: 'Windsurf', + redirect_uris: ['http://127.0.0.1:6277/callback'], + logo_uri: 'https://github.com/codeium.png?size=128', + software_id: 'com.codeium.windsurf', + software_version: '1.6.0', + }, +]; + +/** + * Ensure the gateway is running and return its base URL. + * First checks if already running (may auto-start when servers are enabled), + * then starts it if needed. + */ +async function ensureGatewayRunning(): Promise { + // Check if gateway is already running + const status = await browser.executeAsync( + (done: (result: { running: boolean; url: string | null }) => void) => { + (window as any).__TAURI_TEST_API__ + .invoke('get_gateway_status', {}) + .then((s: { running: boolean; url: string | null }) => done(s)) + .catch(() => done({ running: false, url: null })); + } + ) as { running: boolean; url: string | null }; + + if (status.running && status.url) { + console.log(`[setup] Gateway already running at: ${status.url}`); + return status.url; + } + + // Start the gateway + const url = await browser.executeAsync( + (done: (result: string) => void) => { + (window as any).__TAURI_TEST_API__ + .invoke('start_gateway', {}) + .then((result: string) => done(result)) + .catch((e: unknown) => done('ERROR:' + String(e))); + } + ); + + if (typeof url === 'string' && !url.startsWith('ERROR:')) { + console.log(`[setup] Gateway started at: ${url}`); + return url; + } + + // Fallback: check status again (might have started despite error) + console.warn(`[setup] start_gateway returned: ${url}, checking status...`); + const retry = await browser.executeAsync( + (done: (result: { running: boolean; url: string | null }) => void) => { + (window as any).__TAURI_TEST_API__ + .invoke('get_gateway_status', {}) + .then((s: { running: boolean; url: string | null }) => done(s)) + .catch(() => done({ running: false, url: null })); + } + ) as { running: boolean; url: string | null }; + + if (retry.running && retry.url) { + console.log(`[setup] Gateway running (fallback) at: ${retry.url}`); + return retry.url; + } + + throw new Error('Gateway failed to start'); +} + +/** + * Register OAuth clients via DCR POST and approve them. + * Returns the registered client IDs. + */ +async function registerOAuthClients(gatewayUrl: string): Promise { + const clientIds: string[] = []; + for (const client of OAUTH_CLIENTS) { + try { + const response = await fetch(`${gatewayUrl}/oauth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(client), + }); + if (!response.ok) { + console.warn(`[setup] DCR failed for ${client.client_name}: ${response.status}`); + continue; + } + const data = (await response.json()) as { client_id: string }; + clientIds.push(data.client_id); + console.log(`[setup] Registered OAuth client: ${client.client_name} (${data.client_id})`); + + // Approve the client (bypasses consent flow for E2E) + await approveOAuthClient(data.client_id); + console.log(`[setup] Approved: ${client.client_name}`); + } catch (e) { + console.warn(`[setup] Failed to register ${client.client_name}:`, e); + } + } + return clientIds; +} + +/** + * Emit server-status-changed events to override the connection status. + * Uses a high flow_id (9999) to ensure events are accepted by useServerManager + * even if the real backend already emitted events with lower flow_ids. + */ +async function emitConnectedStatus(serverIds: string[], spaceId: string): Promise { + for (const serverId of serverIds) { + await emitEvent('server-status-changed', { + space_id: spaceId, + server_id: serverId, + status: 'connected', + flow_id: 9999, + has_connected_before: true, + message: null, + }); + } + console.log(`[mock] Emitted 'connected' status for ${serverIds.length} servers`); +} + +describe('Screenshot Capture', function () { + this.timeout(120000); + + let defaultSpaceId: string; + let gatewayUrl: string; + + before(async () => { + // Set a fixed window size for readable, consistent screenshots. + // 1280x800 is optimal: text is legible, full UI visible, standard 16:10 ratio. + await browser.setWindowSize(1280, 800); + + // ---- Seed data from preseed config ---- + + // Get default space + const activeSpace = await getActiveSpace(); + defaultSpaceId = activeSpace?.id || ''; + console.log('[setup] Default space:', defaultSpaceId); + + // Create additional spaces + for (const spaceDef of PRESEED.spaces) { + try { + const space = await createSpace(spaceDef.name, spaceDef.icon); + console.log(`[setup] Created space: ${spaceDef.name} (${space.id})`); + } catch (e) { + console.warn(`[setup] Failed to create space ${spaceDef.name}:`, e); + } + } + + // Force-refresh the registry so server definitions are available + await refreshRegistry(); + await browser.pause(2000); + + // Install servers into default space + for (const serverId of PRESEED.serversToInstall) { + try { + await installServer(serverId, defaultSpaceId); + console.log(`[setup] Installed ${serverId}`); + } catch (e) { + console.warn(`[setup] Failed to install ${serverId}:`, e); + } + } + + // Enable all servers (sets enabled=true in DB, triggers background connection attempts) + for (const serverId of PRESEED.serversToInstall) { + try { + await enableServerV2(defaultSpaceId, serverId); + console.log(`[setup] Enabled ${serverId}`); + } catch (e) { + console.warn(`[setup] Failed to enable ${serverId}:`, e); + } + } + + // Create feature sets + for (const fsDef of PRESEED.featureSets) { + try { + await createFeatureSet({ + name: fsDef.name, + space_id: defaultSpaceId, + description: fsDef.description, + icon: fsDef.icon, + }); + console.log(`[setup] Created feature set: ${fsDef.name}`); + } catch (e) { + console.warn(`[setup] Failed to create feature set ${fsDef.name}:`, e); + } + } + + // Ensure gateway is running (may have auto-started when servers were enabled) + try { + gatewayUrl = await ensureGatewayRunning(); + await browser.pause(1000); // Let gateway fully initialize + + // Register and approve OAuth clients via DCR + await registerOAuthClients(gatewayUrl); + } catch (e) { + console.warn('[setup] Gateway/OAuth setup failed:', e); + gatewayUrl = ''; + } + + // Set active space back to default + await setActiveSpace(defaultSpaceId); + + // Reload the page so the frontend store picks up all seeded data + // (spaces, feature sets, etc. created via Tauri invoke aren't in the Zustand store yet) + await browser.refresh(); + await browser.pause(3000); + }); + + it('captures My Servers', async () => { + const nav = await byTestId('nav-my-servers'); + await safeClick(nav); + await browser.pause(2000); + + // Override server statuses to show "Connected" via Tauri events. + // useServerManager listens for these events and updates React state directly. + await emitConnectedStatus(PRESEED.serversToInstall, defaultSpaceId); + await browser.pause(2000); // Wait for React to re-render + + await saveScreenshot('servers'); + }); + + it('captures Discover Registry', async () => { + const nav = await byTestId('nav-discover'); + await safeClick(nav); + await browser.pause(3000); // Registry loads from mock bundle API + await saveScreenshot('discover'); + }); + + it('captures Spaces', async () => { + const nav = await byTestId('nav-spaces'); + await safeClick(nav); + await browser.pause(2000); + await saveScreenshot('spaces'); + }); + + it('captures FeatureSets', async () => { + const nav = await byTestId('nav-featuresets'); + await safeClick(nav); + await browser.pause(2000); + await saveScreenshot('featuresets'); + }); + + it('captures Connected Clients', async () => { + const nav = await byTestId('nav-clients'); + await safeClick(nav); + await browser.pause(2000); + await saveScreenshot('clients'); + }); + + // Dashboard captured after other pages so it remounts with fresh data + // (client count, server stats reflect all setup done in before() hook) + it('captures Dashboard', async () => { + const nav = await byTestId('nav-dashboard'); + await safeClick(nav); + await browser.pause(2000); + await saveScreenshot('dashboard'); + }); + + it('captures Settings', async () => { + const nav = await byTestId('nav-settings'); + await safeClick(nav); + await browser.pause(2000); + await saveScreenshot('settings'); + }); +}); diff --git a/tests/e2e/specs/comprehensive.wdio.ts b/tests/e2e/specs/comprehensive.wdio.ts index fa432891..af80ce3c 100644 --- a/tests/e2e/specs/comprehensive.wdio.ts +++ b/tests/e2e/specs/comprehensive.wdio.ts @@ -33,7 +33,7 @@ describe('Comprehensive: Space Isolation', () => { let defaultSpaceId: string; let workSpaceId: string; let personalSpaceId: string; - const echoServerId = 'echo-server'; // From mock bundle + const githubServerId = 'github-server'; // From mock bundle before(async () => { // Get default space @@ -52,15 +52,15 @@ describe('Comprehensive: Space Isolation', () => { }); it('TC-COMP-SP-001: Install server only in Work space', async () => { - // Install Echo server in Work space only - await installServer(echoServerId, workSpaceId); + // Install GitHub server in Work space only + await installServer(githubServerId, workSpaceId); // Verify isolation const workServers = await listInstalledServers(workSpaceId); const personalServers = await listInstalledServers(personalSpaceId); - const hasInWork = workServers.some(s => s.server_id === echoServerId || s.id === echoServerId); - const notInPersonal = !personalServers.some(s => s.server_id === echoServerId || s.id === echoServerId); + const hasInWork = workServers.some(s => s.server_id === githubServerId || s.id === githubServerId); + const notInPersonal = !personalServers.some(s => s.server_id === githubServerId || s.id === githubServerId); expect(hasInWork).toBe(true); expect(notInPersonal).toBe(true); @@ -75,7 +75,7 @@ describe('Comprehensive: Space Isolation', () => { // Enable server - MCP handshake can fail on CI, so wrap in try-catch try { - await enableServerV2(workSpaceId, echoServerId); + await enableServerV2(workSpaceId, githubServerId); await browser.pause(5000); // Wait for connection (longer for CI) } catch (e) { console.log('[test] Enable server failed (may be expected on CI):', e); @@ -84,7 +84,7 @@ describe('Comprehensive: Space Isolation', () => { // Check for server-all FeatureSet (may or may not exist depending on connection success) const featureSets = await listFeatureSetsBySpace(workSpaceId); const serverAllFs = featureSets.find( - fs => fs.feature_set_type === 'server-all' && fs.server_id === echoServerId + fs => fs.feature_set_type === 'server-all' && fs.server_id === githubServerId ); console.log('[test] FeatureSets in Work space:', featureSets.map(fs => fs.name)); @@ -105,10 +105,10 @@ describe('Comprehensive: Space Isolation', () => { await browser.saveScreenshot('./tests/e2e/screenshots/comp-01-work-servers.png'); const pageSource = await browser.getPageSource(); - const hasEchoOrServer = pageSource.includes('Echo') || pageSource.includes('echo') || + const hasGithubOrServer = pageSource.includes('GitHub') || pageSource.includes('github') || pageSource.includes('Enable') || pageSource.includes('Disable') || (pageSource.includes('My Servers') && pageSource.includes('installed-server')); - expect(hasEchoOrServer).toBe(true); + expect(hasGithubOrServer).toBe(true); }); it('TC-COMP-SP-004: Switch space and verify server not visible', async () => { @@ -122,18 +122,18 @@ describe('Comprehensive: Space Isolation', () => { await browser.saveScreenshot('./tests/e2e/screenshots/comp-02-personal-servers.png'); - // Personal space should not have Echo server + // Personal space should not have GitHub server const servers = await listInstalledServers(personalSpaceId); - expect(servers.some(s => s.server_id === echoServerId || s.id === echoServerId)).toBe(false); + expect(servers.some(s => s.server_id === githubServerId || s.id === githubServerId)).toBe(false); }); after(async () => { // Cleanup try { - await disableServerV2(workSpaceId, echoServerId); + await disableServerV2(workSpaceId, githubServerId); } catch (e) { /* ignore */ } try { - await uninstallServer(echoServerId, workSpaceId); + await uninstallServer(githubServerId, workSpaceId); } catch (e) { /* ignore */ } try { await deleteSpace(workSpaceId); @@ -218,7 +218,7 @@ describe('Comprehensive: Client Grants', () => { describe('Comprehensive: Server Lifecycle with API', () => { let defaultSpaceId: string; - const serverId = 'echo-server'; // From mock bundle + const serverId = 'github-server'; // From mock bundle before(async () => { const activeSpace = await getActiveSpace(); @@ -249,10 +249,10 @@ describe('Comprehensive: Server Lifecycle with API', () => { await browser.saveScreenshot('./tests/e2e/screenshots/comp-04-server-installed.png'); const pageSource = await browser.getPageSource(); - // Check for Echo Server or related content - const hasServer = - pageSource.includes('Echo') || - pageSource.includes('echo') || + // Check for GitHub Server or related content + const hasServer = + pageSource.includes('GitHub') || + pageSource.includes('github') || pageSource.includes('Server') || pageSource.includes('Enable'); @@ -289,7 +289,7 @@ describe('Comprehensive: Server Lifecycle with API', () => { pageSource.includes('Connected') || pageSource.includes('Disable') || pageSource.includes('tools') || - pageSource.includes('Echo') || + pageSource.includes('GitHub') || pageSource.includes('Enable') ).toBe(true); }); @@ -378,7 +378,7 @@ describe('Comprehensive: Custom FeatureSet', () => { describe('Comprehensive: Multi-Space Server Management', () => { let defaultSpaceId: string; const testSpaces: string[] = []; - const serverId = 'echo-server'; // From mock bundle + const serverId = 'github-server'; // From mock bundle before(async () => { const activeSpace = await getActiveSpace(); diff --git a/tests/e2e/specs/deeplink-install.wdio.ts b/tests/e2e/specs/deeplink-install.wdio.ts index 96fdee96..cbfe1a12 100644 --- a/tests/e2e/specs/deeplink-install.wdio.ts +++ b/tests/e2e/specs/deeplink-install.wdio.ts @@ -12,7 +12,7 @@ import { uninstallServer, } from '../helpers/tauri-api'; -const ECHO_SERVER_ID = 'echo-server'; +const GITHUB_SERVER_ID = 'github-server'; /** Simulate a deep link install event (as if mcpmux://install?server=xxx was received) */ async function simulateInstallDeepLink(serverId: string) { @@ -27,25 +27,25 @@ describe('Deep Link Install - Valid Server', () => { const space = await getActiveSpace(); activeSpaceId = space?.id || ''; - // Ensure echo-server is not installed (clean state) + // Ensure github-server is not installed (clean state) if (activeSpaceId) { const installed = await listInstalledServers(activeSpaceId); - const echoInstalled = installed.some( - (s) => s.server_id === ECHO_SERVER_ID + const githubInstalled = installed.some( + (s) => s.server_id === GITHUB_SERVER_ID ); - if (echoInstalled) { + if (githubInstalled) { try { - await uninstallServer(ECHO_SERVER_ID, activeSpaceId); + await uninstallServer(GITHUB_SERVER_ID, activeSpaceId); await browser.pause(1000); } catch (e) { - console.log('[setup] Could not uninstall echo-server:', e); + console.log('[setup] Could not uninstall github-server:', e); } } } }); it('TC-DL-001: Deep link shows install modal with server info', async () => { - await simulateInstallDeepLink(ECHO_SERVER_ID); + await simulateInstallDeepLink(GITHUB_SERVER_ID); await browser.pause(3000); // Wait for server definition lookup await browser.saveScreenshot( @@ -66,7 +66,7 @@ describe('Deep Link Install - Valid Server', () => { const nameDisplayed = await serverName.isDisplayed().catch(() => false); if (nameDisplayed) { const text = await serverName.getText(); - expect(text).toContain('Echo'); + expect(text).toContain('GitHub'); } } }); @@ -125,16 +125,16 @@ describe('Deep Link Install - Valid Server', () => { // Verify server is actually installed via API if (activeSpaceId) { const installed = await listInstalledServers(activeSpaceId); - const echoInstalled = installed.some( - (s) => s.server_id === ECHO_SERVER_ID + const githubInstalled = installed.some( + (s) => s.server_id === GITHUB_SERVER_ID ); - expect(echoInstalled).toBe(true); + expect(githubInstalled).toBe(true); } }); it('TC-DL-004: Deep link for already-installed server shows warning', async () => { - // Echo server was installed in TC-DL-003 - await simulateInstallDeepLink(ECHO_SERVER_ID); + // GitHub server was installed in TC-DL-003 + await simulateInstallDeepLink(GITHUB_SERVER_ID); await browser.pause(3000); await browser.saveScreenshot( @@ -169,14 +169,14 @@ describe('Deep Link Install - Valid Server', () => { // First uninstall so we get a clean modal if (activeSpaceId) { try { - await uninstallServer(ECHO_SERVER_ID, activeSpaceId); + await uninstallServer(GITHUB_SERVER_ID, activeSpaceId); await browser.pause(1000); } catch (e) { /* ignore */ } } - await simulateInstallDeepLink(ECHO_SERVER_ID); + await simulateInstallDeepLink(GITHUB_SERVER_ID); await browser.pause(3000); const modal = await byTestId('install-modal'); @@ -199,10 +199,10 @@ describe('Deep Link Install - Valid Server', () => { }); after(async () => { - // Cleanup: uninstall echo-server if it was installed + // Cleanup: uninstall github-server if it was installed if (activeSpaceId) { try { - await uninstallServer(ECHO_SERVER_ID, activeSpaceId); + await uninstallServer(GITHUB_SERVER_ID, activeSpaceId); } catch (e) { /* ignore */ } @@ -214,16 +214,19 @@ describe('Deep Link Install - Valid Server', () => { describe('Deep Link Install - Invalid Server', () => { it('TC-DL-006: Deep link with unknown server ID shows error', async () => { await simulateInstallDeepLink('nonexistent-server-12345'); - await browser.pause(3000); + + // Wait for the error modal to appear (loading -> error can take time) + const errorModal = await byTestId('install-modal-error'); + const appeared = await errorModal + .waitForDisplayed({ timeout: TIMEOUT.long }) + .then(() => true) + .catch(() => false); await browser.saveScreenshot( './tests/e2e/screenshots/dl-06-not-found.png' ); - const errorModal = await byTestId('install-modal-error'); - const isDisplayed = await errorModal.isDisplayed().catch(() => false); - - if (isDisplayed) { + if (appeared) { // Error message should mention the server was not found const errorMsg = await byTestId('install-modal-error-message'); const text = await errorMsg.getText().catch(() => ''); @@ -235,7 +238,7 @@ describe('Deep Link Install - Invalid Server', () => { await browser.pause(1000); await waitForModalClose(); } else { - // On slow CI, check page source + // Fallback: check page source for error text const pageSource = await browser.getPageSource(); const hasError = pageSource.includes('not found') || diff --git a/tests/e2e/specs/featureset.wdio.ts b/tests/e2e/specs/featureset.wdio.ts index 1caf7743..3b911e89 100644 --- a/tests/e2e/specs/featureset.wdio.ts +++ b/tests/e2e/specs/featureset.wdio.ts @@ -34,7 +34,7 @@ describe('FeatureSet - Builtin Sets', () => { }); describe('FeatureSet - Server-All Auto Creation', () => { - it('Setup: Install and Enable Echo Server', async () => { + it('Setup: Install and Enable GitHub Server', async () => { const discoverButton = await byTestId('nav-discover'); await safeClick(discoverButton); await browser.pause(2000); @@ -42,10 +42,10 @@ describe('FeatureSet - Server-All Auto Creation', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('Echo'); + await searchInput.setValue('GitHub'); await browser.pause(1000); - const installButton = await byTestId('install-btn-echo-server'); + const installButton = await byTestId('install-btn-github-server'); const isInstallDisplayed = await installButton.isDisplayed().catch(() => false); if (isInstallDisplayed) { @@ -59,7 +59,7 @@ describe('FeatureSet - Server-All Auto Creation', () => { await safeClick(myServersButton); await browser.pause(2000); - const enableButton = await byTestId('enable-server-echo-server'); + const enableButton = await byTestId('enable-server-github-server'); const isEnableDisplayed = await enableButton.isDisplayed().catch(() => false); if (isEnableDisplayed) { @@ -79,31 +79,31 @@ describe('FeatureSet - Server-All Auto Creation', () => { expect(isConnected).toBe(true); }); - it('TC-FS-002: Verify server-all FeatureSet is created for Echo Server', async () => { + it('TC-FS-002: Verify server-all FeatureSet is created for GitHub Server', async () => { const featureSetsButton = await byTestId('nav-featuresets'); await safeClick(featureSetsButton); await browser.pause(2000); - + await browser.saveScreenshot('./tests/e2e/screenshots/fs-03-featuresets-with-server.png'); - - // Look for Echo Server's FeatureSet + + // Look for GitHub Server's FeatureSet const pageSource = await browser.getPageSource(); - const hasEchoFeatureSet = - pageSource.includes('Echo Server') || - pageSource.includes('Echo'); - - console.log('[DEBUG] Has Echo FeatureSet:', hasEchoFeatureSet); - - // Echo Server feature set should appear when server is enabled - expect(hasEchoFeatureSet).toBe(true); + const hasGithubFeatureSet = + pageSource.includes('GitHub Server') || + pageSource.includes('GitHub'); + + console.log('[DEBUG] Has GitHub FeatureSet:', hasGithubFeatureSet); + + // GitHub Server feature set should appear when server is enabled + expect(hasGithubFeatureSet).toBe(true); }); - it('TC-FS-003: Click on Echo Server FeatureSet to see its features', async () => { + it('TC-FS-003: Click on GitHub Server FeatureSet to see its features', async () => { const cards = await $$('[data-testid^="featureset-card-"]'); let targetCard = null; for (const card of cards) { const text = await card.getText(); - if (text.includes('Echo')) { + if (text.includes('GitHub')) { targetCard = card; break; } @@ -117,10 +117,10 @@ describe('FeatureSet - Server-All Auto Creation', () => { await browser.saveScreenshot('./tests/e2e/screenshots/fs-04-featureset-details.png'); - // Check for features (tools from Echo Server) + // Check for features (tools from GitHub Server) const pageSource = await browser.getPageSource(); const hasFeatures = - pageSource.includes('echo') || + pageSource.includes('github') || pageSource.includes('add') || pageSource.includes('get_time') || pageSource.includes('Tools') || @@ -158,7 +158,7 @@ describe('FeatureSet - Server-All Auto Creation', () => { await myServersButton.click(); await browser.pause(2000); - const disableButton = await byTestId('disable-server-echo-server'); + const disableButton = await byTestId('disable-server-github-server'); const isDisableDisplayed = await disableButton.isDisplayed().catch(() => false); if (isDisableDisplayed) { @@ -173,14 +173,14 @@ describe('FeatureSet - Server-All Auto Creation', () => { await browser.saveScreenshot('./tests/e2e/screenshots/fs-05-after-disable.png'); - // Echo Server FeatureSet should be hidden (or less prominent) + // GitHub Server FeatureSet should be hidden (or less prominent) const pageSource = await browser.getPageSource(); // The test passes if page loads - actual visibility depends on UI design expect(pageSource.includes('Feature')).toBe(true); }); - it('Cleanup: Uninstall Echo Server', async () => { + it('Cleanup: Uninstall GitHub Server', async () => { // Close any open panel first const panelCloseBtn = await byTestId('featureset-panel-close'); if (await panelCloseBtn.isDisplayed().catch(() => false)) { @@ -196,10 +196,10 @@ describe('FeatureSet - Server-All Auto Creation', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('Echo'); + await searchInput.setValue('GitHub'); await browser.pause(1000); - const uninstallButton = await byTestId('uninstall-btn-echo-server'); + const uninstallButton = await byTestId('uninstall-btn-github-server'); const isDisplayed = await uninstallButton.isDisplayed().catch(() => false); if (isDisplayed) { diff --git a/tests/e2e/specs/server-config.wdio.ts b/tests/e2e/specs/server-config.wdio.ts index 38f0583f..74a31f22 100644 --- a/tests/e2e/specs/server-config.wdio.ts +++ b/tests/e2e/specs/server-config.wdio.ts @@ -5,8 +5,8 @@ import { byTestId, TIMEOUT, waitForModalClose, safeClick } from '../helpers/selectors'; -describe('Server Configuration - API Key Server', () => { - it('TC-SC-001: Install API Key Server and click Enable shows config modal', async () => { +describe('Server Configuration - PostgreSQL', () => { + it('TC-SC-001: Install PostgreSQL Server and click Enable shows config modal', async () => { const discoverButton = await byTestId('nav-discover'); await safeClick(discoverButton); await browser.pause(2000); @@ -14,12 +14,12 @@ describe('Server Configuration - API Key Server', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('API Key'); + await searchInput.setValue('PostgreSQL'); await browser.pause(1000); await browser.saveScreenshot('./tests/e2e/screenshots/sc-01-search-apikey.png'); - const installButton = await byTestId('install-btn-api-key-server'); + const installButton = await byTestId('install-btn-postgres-server'); const isInstallDisplayed = await installButton.isDisplayed().catch(() => false); if (isInstallDisplayed) { @@ -29,22 +29,22 @@ describe('Server Configuration - API Key Server', () => { await waitForModalClose(); } - const uninstallButton = await byTestId('uninstall-btn-api-key-server'); + const uninstallButton = await byTestId('uninstall-btn-postgres-server'); await expect(uninstallButton).toBeDisplayed(); await browser.saveScreenshot('./tests/e2e/screenshots/sc-02-apikey-installed.png'); }); - it('TC-SC-002: Enable shows configuration modal with API Key input', async () => { + it('TC-SC-002: Enable shows configuration modal with connection input', async () => { const myServersButton = await byTestId('nav-my-servers'); await safeClick(myServersButton); await browser.pause(2000); - // Verify API Key Server is in the list + // Verify PostgreSQL Server is in the list const pageSource = await browser.getPageSource(); - expect(pageSource.includes('API Key Server')).toBe(true); + expect(pageSource.includes('PostgreSQL')).toBe(true); - const enableButton = await byTestId('enable-server-api-key-server'); + const enableButton = await byTestId('enable-server-postgres-server'); await safeClick(enableButton); await browser.pause(1000); @@ -60,12 +60,12 @@ describe('Server Configuration - API Key Server', () => { expect(hasConfigModal).toBe(true); }); - it('TC-SC-002b: Enter API Key and save configuration', async () => { - const apiKeyInput = await byTestId('config-input-API_KEY'); - const isInputDisplayed = await apiKeyInput.isDisplayed().catch(() => false); - + it('TC-SC-002b: Enter connection string and save configuration', async () => { + const configInput = await byTestId('config-input-DATABASE_URL'); + const isInputDisplayed = await configInput.isDisplayed().catch(() => false); + if (isInputDisplayed) { - await apiKeyInput.setValue('test_api_key_12345'); + await configInput.setValue('postgresql://test:test@localhost:5432/testdb'); await browser.pause(500); await browser.saveScreenshot('./tests/e2e/screenshots/sc-04-entered-key.png'); @@ -93,7 +93,7 @@ describe('Server Configuration - API Key Server', () => { expect(modalClosed).toBe(true); }); - it('Cleanup: Uninstall API Key Server', async () => { + it('Cleanup: Uninstall PostgreSQL Server', async () => { const discoverButton = await byTestId('nav-discover'); await safeClick(discoverButton); await browser.pause(2000); @@ -101,10 +101,10 @@ describe('Server Configuration - API Key Server', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('API Key'); + await searchInput.setValue('PostgreSQL'); await browser.pause(1000); - const uninstallButton = await byTestId('uninstall-btn-api-key-server'); + const uninstallButton = await byTestId('uninstall-btn-postgres-server'); const isDisplayed = await uninstallButton.isDisplayed().catch(() => false); if (isDisplayed) { @@ -118,8 +118,8 @@ describe('Server Configuration - API Key Server', () => { }); }); -describe('Server Configuration - Directory Server', () => { - it('TC-SC-003: Install Directory Server', async () => { +describe('Server Configuration - Filesystem', () => { + it('TC-SC-003: Install Filesystem Server', async () => { const discoverButton = await byTestId('nav-discover'); await safeClick(discoverButton); await browser.pause(2000); @@ -127,12 +127,12 @@ describe('Server Configuration - Directory Server', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('Directory'); + await searchInput.setValue('Filesystem'); await browser.pause(1000); await browser.saveScreenshot('./tests/e2e/screenshots/sc-07-search-dir.png'); - const installButton = await byTestId('install-btn-directory-server'); + const installButton = await byTestId('install-btn-filesystem-server'); const isInstallDisplayed = await installButton.isDisplayed().catch(() => false); if (isInstallDisplayed) { @@ -142,7 +142,7 @@ describe('Server Configuration - Directory Server', () => { await waitForModalClose(); } - const uninstallButton = await byTestId('uninstall-btn-directory-server'); + const uninstallButton = await byTestId('uninstall-btn-filesystem-server'); await expect(uninstallButton).toBeDisplayed(); }); @@ -151,7 +151,7 @@ describe('Server Configuration - Directory Server', () => { await safeClick(myServersButton); await browser.pause(2000); - const enableButton = await byTestId('enable-server-directory-server'); + const enableButton = await byTestId('enable-server-filesystem-server'); const isEnableDisplayed = await enableButton.isDisplayed().catch(() => false); if (isEnableDisplayed) { @@ -182,7 +182,7 @@ describe('Server Configuration - Directory Server', () => { await browser.saveScreenshot('./tests/e2e/screenshots/sc-10-dir-after-config.png'); }); - it('Cleanup: Uninstall Directory Server', async () => { + it('Cleanup: Uninstall Filesystem Server', async () => { const discoverButton = await byTestId('nav-discover'); await safeClick(discoverButton); await browser.pause(2000); @@ -190,10 +190,10 @@ describe('Server Configuration - Directory Server', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('Directory'); + await searchInput.setValue('Filesystem'); await browser.pause(1000); - const uninstallButton = await byTestId('uninstall-btn-directory-server'); + const uninstallButton = await byTestId('uninstall-btn-filesystem-server'); const isDisplayed = await uninstallButton.isDisplayed().catch(() => false); if (isDisplayed) { diff --git a/tests/e2e/specs/server-lifecycle.wdio.ts b/tests/e2e/specs/server-lifecycle.wdio.ts index 612dbae7..d396d685 100644 --- a/tests/e2e/specs/server-lifecycle.wdio.ts +++ b/tests/e2e/specs/server-lifecycle.wdio.ts @@ -5,8 +5,8 @@ import { byTestId, TIMEOUT, waitForModalClose } from '../helpers/selectors'; -describe('Server Installation - Echo Server (No Inputs)', () => { - it('TC-SD-004: Install Echo Server from Discover page', async () => { +describe('Server Installation - GitHub Server (No Inputs)', () => { + it('TC-SD-004: Install GitHub Server from Discover page', async () => { const discoverButton = await byTestId('nav-discover'); await discoverButton.click(); await browser.pause(3000); // Wait for registry to fully load @@ -14,22 +14,22 @@ describe('Server Installation - Echo Server (No Inputs)', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('Echo'); + await searchInput.setValue('GitHub'); await browser.pause(3000); // Allow search results to load (longer for CI) - await browser.saveScreenshot('./tests/e2e/screenshots/sl-01-search-echo.png'); + await browser.saveScreenshot('./tests/e2e/screenshots/sl-01-search-github.png'); // Check if already installed (uninstall button visible) - can happen if previous test didn't clean up - const uninstallButton = await byTestId('uninstall-btn-echo-server'); + const uninstallButton = await byTestId('uninstall-btn-github-server'); const alreadyInstalled = await uninstallButton.isDisplayed().catch(() => false); if (alreadyInstalled) { - console.log('[TC-SD-004] Echo Server already installed, skipping install'); + console.log('[TC-SD-004] GitHub Server already installed, skipping install'); await browser.saveScreenshot('./tests/e2e/screenshots/sl-02-installed.png'); return; } - const installButton = await byTestId('install-btn-echo-server'); + const installButton = await byTestId('install-btn-github-server'); // Use longer timeout for CI where registry loading can be slow await installButton.waitForDisplayed({ timeout: TIMEOUT.long }); await installButton.waitForClickable({ timeout: TIMEOUT.medium }); @@ -42,7 +42,7 @@ describe('Server Installation - Echo Server (No Inputs)', () => { await browser.saveScreenshot('./tests/e2e/screenshots/sl-02-installed.png'); }); - it('TC-SL-001: Enable Echo Server (verify server appears in My Servers)', async () => { + it('TC-SL-001: Enable GitHub Server (verify server appears in My Servers)', async () => { await waitForModalClose(); const myServersButton = await byTestId('nav-my-servers'); await myServersButton.click(); @@ -50,11 +50,11 @@ describe('Server Installation - Echo Server (No Inputs)', () => { await browser.saveScreenshot('./tests/e2e/screenshots/sl-03-my-servers.png'); - // Verify Echo Server is in the list + // Verify GitHub Server is in the list const pageSource = await browser.getPageSource(); - expect(pageSource.includes('Echo Server')).toBe(true); + expect(pageSource.includes('GitHub')).toBe(true); - const enableButton = await byTestId('enable-server-echo-server'); + const enableButton = await byTestId('enable-server-github-server'); const isEnableDisplayed = await enableButton.isDisplayed().catch(() => false); if (isEnableDisplayed) { @@ -80,7 +80,7 @@ describe('Server Installation - Echo Server (No Inputs)', () => { pageSource.includes('Connected') || pageSource.includes('tools') || pageSource.includes('Disable') || - pageSource.includes('Echo Server') || + pageSource.includes('GitHub Server') || pageSource.includes('Enable'); expect(hasServerContent).toBe(true); @@ -88,25 +88,25 @@ describe('Server Installation - Echo Server (No Inputs)', () => { it('TC-SL-003: Disable connected server', async () => { await waitForModalClose(); - const disableButton = await byTestId('disable-server-echo-server'); + const disableButton = await byTestId('disable-server-github-server'); const isDisableDisplayed = await disableButton.isDisplayed().catch(() => false); if (isDisableDisplayed) { await disableButton.click(); await browser.pause(2000); await browser.saveScreenshot('./tests/e2e/screenshots/sl-06-disabled.png'); - const enableButton = await byTestId('enable-server-echo-server'); + const enableButton = await byTestId('enable-server-github-server'); await expect(enableButton).toBeDisplayed(); } else { // Server might not be connected (MCP handshake can fail on CI) // Just verify the server card is still present const pageSource = await browser.getPageSource(); - const hasServer = pageSource.includes('Echo Server') || pageSource.includes('Enable'); + const hasServer = pageSource.includes('GitHub Server') || pageSource.includes('Enable'); expect(hasServer).toBe(true); } }); - it('TC-SD-005: Uninstall Echo Server', async () => { + it('TC-SD-005: Uninstall GitHub Server', async () => { await waitForModalClose(); const discoverButton = await byTestId('nav-discover'); await discoverButton.click(); @@ -115,10 +115,10 @@ describe('Server Installation - Echo Server (No Inputs)', () => { const searchInput = await byTestId('search-input'); await searchInput.clearValue(); await browser.pause(300); - await searchInput.setValue('Echo'); + await searchInput.setValue('GitHub'); await browser.pause(2000); - const uninstallButton = await byTestId('uninstall-btn-echo-server'); + const uninstallButton = await byTestId('uninstall-btn-github-server'); await uninstallButton.waitForDisplayed({ timeout: TIMEOUT.medium }); await uninstallButton.waitForClickable({ timeout: TIMEOUT.medium }); await uninstallButton.click(); @@ -126,7 +126,7 @@ describe('Server Installation - Echo Server (No Inputs)', () => { await browser.saveScreenshot('./tests/e2e/screenshots/sl-07-uninstalled.png'); - const installButton = await byTestId('install-btn-echo-server'); + const installButton = await byTestId('install-btn-github-server'); await expect(installButton).toBeDisplayed(); }); }); diff --git a/tests/e2e/wdio.conf.ts b/tests/e2e/wdio.conf.ts index 13a88e9b..ff40a009 100644 --- a/tests/e2e/wdio.conf.ts +++ b/tests/e2e/wdio.conf.ts @@ -80,6 +80,32 @@ function checkTauriDriver(): boolean { } +// Clear the app's SQLite database and related files for a clean start +function clearAppData(): void { + const filesToDelete = [ + path.join(APP_DATA_DIR, 'mcpmux.db'), + path.join(APP_DATA_DIR, 'mcpmux.db-shm'), + path.join(APP_DATA_DIR, 'mcpmux.db-wal'), + path.join(APP_DATA_DIR, 'cache'), + path.join(APP_DATA_DIR, 'spaces'), + ]; + + for (const filePath of filesToDelete) { + try { + if (fs.existsSync(filePath)) { + if (fs.lstatSync(filePath).isDirectory()) { + fs.rmSync(filePath, { recursive: true, force: true }); + } else { + fs.unlinkSync(filePath); + } + console.log(`[e2e] Cleared: ${filePath}`); + } + } catch (error) { + console.warn(`[e2e] Failed to clear ${filePath}:`, error); + } + } +} + // Clear the app's registry bundle cache so it fetches fresh from our mock function clearBundleCache(): void { try { @@ -179,6 +205,26 @@ function closeTauriDriver() { stopMockServers(); } +// Kill any processes listening on our mock server ports (leftover from previous runs) +function killPortProcesses(): void { + const ports = [MOCK_BUNDLE_API_PORT, STUB_MCP_HTTP_PORT, STUB_MCP_OAUTH_PORT]; + for (const port of ports) { + try { + if (process.platform === 'win32') { + // Find PIDs listening on this port and kill them + const result = spawnSync('cmd', ['/c', `for /f "tokens=5" %a in ('netstat -ano ^| findstr ":${port} " ^| findstr LISTEN') do taskkill /F /PID %a`], { stdio: 'pipe', shell: true }); + if (result.stdout?.toString().includes('SUCCESS')) { + console.log(`[e2e] Killed process on port ${port}`); + } + } else { + spawnSync('fuser', ['-k', `${port}/tcp`], { stdio: 'ignore' }); + } + } catch { + // Ignore errors - no process may be on this port + } + } +} + // Kill any running mcpmux processes to prevent single-instance conflicts function killMcpmuxProcesses(): void { try { @@ -314,25 +360,28 @@ export const config: Options.Testrunner = { // Verify app is built checkAppBuilt(); + // Kill any leftover mcpmux processes and clear all app data BEFORE + // tauri-driver starts the app. This avoids EBUSY errors from trying + // to delete the SQLite DB while the app still holds a lock on it. + killMcpmuxProcesses(); + // Brief pause to let processes fully exit + await new Promise((resolve) => setTimeout(resolve, 2000)); + clearSingleInstanceLock(); + clearAppData(); + // Clear bundle cache so app fetches from our mock clearBundleCache(); + // Kill any leftover mock servers from previous runs (prevents EADDRINUSE) + killPortProcesses(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Start mock servers await startMockServers(); }, // Start tauri-driver before the session starts beforeSession: function () { - // Kill any leftover mcpmux processes and clear lock from previous sessions - killMcpmuxProcesses(); - clearSingleInstanceLock(); - - // Small delay to ensure processes are fully terminated - spawnSync('sleep', ['1'], { stdio: 'ignore', shell: process.platform !== 'win32' }); - if (process.platform === 'win32') { - spawnSync('timeout', ['/t', '1', '/nobreak'], { stdio: 'ignore', shell: true }); - } - const tauriDriverPath = path.resolve( os.homedir(), '.cargo',