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
280 lines
10 KiB
TypeScript
280 lines
10 KiB
TypeScript
import { browser, expect } from '@wdio/globals';
|
||
import { exec } from '../helpers/execute.ts';
|
||
|
||
// Phase C — UI Interaction Tests (C1–C4)
|
||
// 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);
|
||
});
|
||
});
|