agent-orchestrator/tests/e2e/specs/phase-c-ui.test.ts
Hibryda 6a8181f33a fix(e2e): cross-protocol browser.execute() — works with both WebDriver + CDP
Root cause: WebDriverIO devtools protocol wraps functions in a polyfill
that puts `return` inside eval() (not a function body) → "Illegal return".

Fix: exec() wrapper in helpers/execute.ts converts function args to IIFE
strings before passing to browser.execute(). Works identically on both
WebDriver (Tauri) and CDP/devtools (Electrobun CEF).

- 35 spec files updated (browser.execute → exec)
- 4 config files updated (string-form expressions)
- helpers/actions.ts + assertions.ts updated
- 560 vitest + 116 cargo passing
2026-03-22 06:33:55 +01:00

280 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { browser, expect } from '@wdio/globals';
import { exec } from '../helpers/execute.ts';
// Phase C — UI Interaction Tests (C1C4)
// Command palette, search overlay, notification center, keyboard navigation.
// ─── Helpers ──────────────────────────────────────────────────────────
/** Open command palette via Ctrl+K. */
async function openPalette(): Promise<void> {
await exec(() => document.body.focus());
await browser.pause(100);
await browser.keys(['Control', 'k']);
const palette = await browser.$('[data-testid="command-palette"]');
await palette.waitForDisplayed({ timeout: 3000 });
}
/** Close command palette via Escape. */
async function closePalette(): Promise<void> {
await browser.keys('Escape');
await browser.pause(300);
}
/** Type into palette input and get filtered results. */
async function paletteSearch(query: string): Promise<string[]> {
const input = await browser.$('[data-testid="palette-input"]');
await input.setValue(query);
await browser.pause(300);
return exec(() => {
const items = document.querySelectorAll('.palette-item .cmd-label');
return Array.from(items).map(el => el.textContent?.trim() ?? '');
});
}
// ─── Scenario C1: Command Palette — Hardening Commands ────────────────
describe('Scenario C1 — Command Palette Hardening Commands', () => {
afterEach(async () => {
// Ensure palette is closed after each test
try {
const isVisible = await exec(() => {
const el = document.querySelector('[data-testid="command-palette"]');
return el !== null && window.getComputedStyle(el).display !== 'none';
});
if (isVisible) {
await closePalette();
}
} catch {
// Ignore if palette doesn't exist
}
});
it('should find settings command in palette', async () => {
await openPalette();
const results = await paletteSearch('settings');
expect(results.length).toBeGreaterThanOrEqual(1);
const hasSettings = results.some(r => r.toLowerCase().includes('settings'));
expect(hasSettings).toBe(true);
});
it('should find terminal command in palette', async () => {
await openPalette();
const results = await paletteSearch('terminal');
expect(results.length).toBeGreaterThanOrEqual(1);
const hasTerminal = results.some(r => r.toLowerCase().includes('terminal'));
expect(hasTerminal).toBe(true);
});
it('should find keyboard shortcuts command in palette', async () => {
await openPalette();
const results = await paletteSearch('keyboard');
expect(results.length).toBeGreaterThanOrEqual(1);
const hasShortcuts = results.some(r => r.toLowerCase().includes('keyboard'));
expect(hasShortcuts).toBe(true);
});
it('should list all commands grouped by category when input is empty', async () => {
await openPalette();
const input = await browser.$('[data-testid="palette-input"]');
await input.clearValue();
await browser.pause(200);
const itemCount = await exec(() =>
document.querySelectorAll('.palette-item').length,
);
// v3 has 18+ commands
expect(itemCount).toBeGreaterThanOrEqual(10);
// Commands should be organized in groups (categories)
const groups = await exec(() => {
const headers = document.querySelectorAll('.palette-category');
return Array.from(headers).map(h => h.textContent?.trim() ?? '');
});
// Should have at least 2 command groups
expect(groups.length).toBeGreaterThanOrEqual(2);
});
});
// ─── Scenario C2: Search Overlay (Ctrl+Shift+F) ──────────────────────
describe('Scenario C2 — Search Overlay (FTS5)', () => {
it('should open search overlay with Ctrl+Shift+F', async () => {
await exec(() => document.body.focus());
await browser.pause(100);
await browser.keys(['Control', 'Shift', 'f']);
await browser.pause(500);
const overlay = await exec(() => {
// SearchOverlay uses .search-overlay class
const el = document.querySelector('.search-overlay, [data-testid="search-overlay"]');
return el !== null;
});
expect(overlay).toBe(true);
});
it('should have search input focused', async () => {
const isFocused = await exec(() => {
const input = document.querySelector('.search-overlay input, [data-testid="search-input"]') as HTMLInputElement | null;
if (!input) return false;
input.focus();
return input === document.activeElement;
});
expect(isFocused).toBe(true);
});
it('should show no results for nonsense query', async () => {
await exec(() => {
const input = document.querySelector('.search-overlay input, [data-testid="search-input"]') as HTMLInputElement | null;
if (input) {
input.value = 'zzz_nonexistent_xyz_999';
input.dispatchEvent(new Event('input', { bubbles: true }));
}
});
await browser.pause(500); // 300ms debounce + render time
const resultCount = await exec(() => {
const results = document.querySelectorAll('.search-result, .search-result-item');
return results.length;
});
expect(resultCount).toBe(0);
});
it('should close search overlay with Escape', async () => {
await browser.keys('Escape');
await browser.pause(300);
const overlay = await exec(() => {
const el = document.querySelector('.search-overlay, [data-testid="search-overlay"]');
if (!el) return false;
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
});
expect(overlay).toBe(false);
});
});
// ─── Scenario C3: Notification Center ─────────────────────────────────
describe('Scenario C3 — Notification Center', () => {
it('should render notification bell in status bar', async () => {
const hasBell = await exec(() => {
const bar = document.querySelector('[data-testid="status-bar"]');
// NotificationCenter is in status bar with bell icon
const bell = bar?.querySelector('.notification-bell, .bell-icon, [data-testid="notification-bell"]');
return bell !== null;
});
expect(hasBell).toBe(true);
});
it('should open notification panel on bell click', async () => {
await exec(() => {
const bell = document.querySelector('.notification-bell, .bell-icon, [data-testid="notification-bell"]');
if (bell) (bell as HTMLElement).click();
});
await browser.pause(300);
const panelOpen = await exec(() => {
const panel = document.querySelector('.notification-panel, .notification-dropdown, [data-testid="notification-panel"]');
if (!panel) return false;
const style = window.getComputedStyle(panel);
return style.display !== 'none';
});
expect(panelOpen).toBe(true);
});
it('should show empty state or notification history', async () => {
const content = await exec(() => {
const panel = document.querySelector('.notification-panel, .notification-dropdown, [data-testid="notification-panel"]');
return panel?.textContent ?? '';
});
// Panel should have some text content (either "No notifications" or actual notifications)
expect(content.length).toBeGreaterThan(0);
});
it('should close notification panel on outside click', async () => {
// Click the backdrop overlay to close the panel
await exec(() => {
const backdrop = document.querySelector('.notification-center .backdrop');
if (backdrop) (backdrop as HTMLElement).click();
});
await browser.pause(300);
const panelOpen = await exec(() => {
const panel = document.querySelector('.notification-panel, .notification-dropdown, [data-testid="notification-panel"]');
if (!panel) return false;
const style = window.getComputedStyle(panel);
return style.display !== 'none';
});
expect(panelOpen).toBe(false);
});
});
// ─── Scenario C4: Keyboard Navigation ────────────────────────────────
describe('Scenario C4 — Keyboard-First Navigation', () => {
it('should toggle settings with Ctrl+Comma', async () => {
await exec(() => document.body.focus());
await browser.pause(100);
await browser.keys(['Control', ',']);
await browser.pause(500);
const settingsVisible = await exec(() => {
const panel = document.querySelector('.sidebar-panel');
if (!panel) return false;
const style = window.getComputedStyle(panel);
return style.display !== 'none';
});
expect(settingsVisible).toBe(true);
// Close it
await browser.keys('Escape');
await browser.pause(300);
});
it('should toggle sidebar with Ctrl+B', async () => {
await exec(() => document.body.focus());
await browser.pause(100);
// First open settings to have sidebar content
await browser.keys(['Control', ',']);
await browser.pause(300);
const initialState = await exec(() => {
const panel = document.querySelector('.sidebar-panel');
return panel !== null && window.getComputedStyle(panel).display !== 'none';
});
// Toggle sidebar
await browser.keys(['Control', 'b']);
await browser.pause(300);
const afterToggle = await exec(() => {
const panel = document.querySelector('.sidebar-panel');
if (!panel) return false;
return window.getComputedStyle(panel).display !== 'none';
});
// State should have changed
if (initialState) {
expect(afterToggle).toBe(false);
}
// Clean up — close sidebar if still open
await browser.keys('Escape');
await browser.pause(200);
});
it('should focus project with Alt+1', async () => {
await exec(() => document.body.focus());
await browser.pause(100);
await browser.keys(['Alt', '1']);
await browser.pause(300);
const hasActive = await exec(() => {
const active = document.querySelector('.project-box.active');
return active !== null;
});
expect(hasActive).toBe(true);
});
});