diff --git a/tests/e2e/helpers/actions.ts b/tests/e2e/helpers/actions.ts index da006f4..a665dae 100644 --- a/tests/e2e/helpers/actions.ts +++ b/tests/e2e/helpers/actions.ts @@ -1,11 +1,12 @@ /** * Reusable test actions — common UI operations used across spec files. * - * All actions use browser.execute() for DOM queries with fallback selectors - * to support both Tauri and Electrobun UIs (WebKitGTK reliability pattern). + * All actions use exec() (cross-protocol safe wrapper) for DOM queries with + * fallback selectors to support both Tauri and Electrobun UIs. */ import { browser } from '@wdio/globals'; +import { exec } from './execute.ts'; import * as S from './selectors.ts'; /** @@ -30,8 +31,7 @@ export async function waitForPort(port: number, timeout: number): Promise /** Open settings panel via gear icon click */ export async function openSettings(): Promise { - // Try clicking settings button — may need multiple attempts on WebKitGTK - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]') ?? document.querySelector('.sidebar-icon') ?? document.querySelector('.rail-btn'); @@ -40,7 +40,7 @@ export async function openSettings(): Promise { await browser.pause(300); // Check if panel opened; if not, try keyboard shortcut (Ctrl+,) - const opened = await browser.execute(() => + const opened = await exec(() => document.querySelector('.sidebar-panel, .settings-drawer, .settings-panel') !== null, ); if (!opened) { @@ -48,19 +48,19 @@ export async function openSettings(): Promise { await browser.pause(300); } - // Wait for either settings panel class (Tauri: .sidebar-panel, Electrobun: .settings-drawer) + // Wait for either settings panel class await browser.waitUntil( async () => - browser.execute(() => + exec(() => document.querySelector('.sidebar-panel, .settings-drawer, .settings-panel') !== null, - ) as Promise, + ), { timeout: 5_000 }, ); } /** Close settings panel */ export async function closeSettings(): Promise { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.settings-close') ?? document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); @@ -70,8 +70,7 @@ export async function closeSettings(): Promise { /** Switch to a settings category by index (0-based) */ export async function switchSettingsCategory(index: number): Promise { - await browser.execute((idx: number) => { - // Tauri: .settings-sidebar .sidebar-item | Electrobun: .settings-tab or .cat-btn + await exec((idx: number) => { const tabs = document.querySelectorAll('.settings-sidebar .sidebar-item, .settings-tab, .cat-btn'); if (tabs[idx]) (tabs[idx] as HTMLElement).click(); }, index); @@ -80,7 +79,7 @@ export async function switchSettingsCategory(index: number): Promise { /** Switch active group by clicking the nth group button (0-based) */ export async function switchGroup(index: number): Promise { - await browser.execute((idx: number) => { + await exec((idx: number) => { const groups = document.querySelectorAll('.group-btn:not(.add-group-btn)'); if (groups[idx]) (groups[idx] as HTMLElement).click(); }, index); @@ -126,7 +125,7 @@ export async function closeSearch(): Promise { /** Add a new terminal tab by clicking the add button */ export async function addTerminalTab(): Promise { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.tab-add-btn'); if (btn) (btn as HTMLElement).click(); }); @@ -135,8 +134,7 @@ export async function addTerminalTab(): Promise { /** Click a project-level tab (model, docs, files, etc.) */ export async function clickProjectTab(tabName: string): Promise { - await browser.execute((name: string) => { - // Tauri: .ptab | Electrobun: .project-tab or .tab-btn + await exec((name: string) => { const tabs = document.querySelectorAll('.ptab, .project-tab, .tab-btn'); for (const tab of tabs) { if ((tab as HTMLElement).textContent?.toLowerCase().includes(name.toLowerCase())) { @@ -156,7 +154,7 @@ export async function waitForElement(selector: string, timeout = 5_000): Promise /** Check if an element exists and is displayed (safe for optional elements) */ export async function isVisible(selector: string): Promise { - return browser.execute((sel: string) => { + return exec((sel: string) => { const el = document.querySelector(sel); if (!el) return false; const style = getComputedStyle(el); @@ -166,7 +164,7 @@ export async function isVisible(selector: string): Promise { /** Get the display CSS value for an element (for display-toggle awareness) */ export async function getDisplay(selector: string): Promise { - return browser.execute((sel: string) => { + return exec((sel: string) => { const el = document.querySelector(sel); if (!el) return 'not-found'; return getComputedStyle(el).display; @@ -175,8 +173,7 @@ export async function getDisplay(selector: string): Promise { /** Open notification drawer by clicking bell */ export async function openNotifications(): Promise { - await browser.execute(() => { - // Tauri: .bell-btn | Electrobun: .notif-btn + await exec(() => { const btn = document.querySelector('.notif-btn') ?? document.querySelector('.bell-btn') ?? document.querySelector('[data-testid="notification-bell"]'); @@ -187,8 +184,7 @@ export async function openNotifications(): Promise { /** Close notification drawer */ export async function closeNotifications(): Promise { - await browser.execute(() => { - // Tauri: .notification-center .backdrop | Electrobun: .notif-backdrop + await exec(() => { const backdrop = document.querySelector('.notif-backdrop') ?? document.querySelector('.notification-center .backdrop'); if (backdrop) (backdrop as HTMLElement).click(); diff --git a/tests/e2e/helpers/assertions.ts b/tests/e2e/helpers/assertions.ts index 4bd7b49..39b18fb 100644 --- a/tests/e2e/helpers/assertions.ts +++ b/tests/e2e/helpers/assertions.ts @@ -1,17 +1,17 @@ /** * Custom E2E assertions — domain-specific checks for Agent Orchestrator. * - * Uses browser.execute() for DOM queries with dual selectors to support - * both Tauri and Electrobun UIs (WebKitGTK reliability). + * Uses exec() (cross-protocol safe wrapper) for DOM queries with dual + * selectors to support both Tauri and Electrobun UIs. */ import { browser, expect } from '@wdio/globals'; +import { exec } from './execute.ts'; import * as S from './selectors.ts'; /** Assert that a project card with the given name is visible in the grid */ export async function assertProjectVisible(name: string): Promise { - const found = await browser.execute((n: string) => { - // Tauri: .project-box | Electrobun: .project-card | Both: .project-header + const found = await exec((n: string) => { const cards = document.querySelectorAll('.project-box, .project-card, .project-header'); for (const card of cards) { if (card.textContent?.includes(n)) return true; @@ -31,7 +31,7 @@ export async function assertTerminalResponds(): Promise { /** Assert that a CSS custom property has changed after a theme switch */ export async function assertThemeApplied(varName = '--ctp-base'): Promise { - const value = await browser.execute((v: string) => { + const value = await exec((v: string) => { return getComputedStyle(document.documentElement).getPropertyValue(v).trim(); }, varName); expect(value.length).toBeGreaterThan(0); @@ -49,12 +49,12 @@ export async function assertSettingsPersist(selector: string): Promise { export async function assertStatusBarComplete(): Promise { await browser.waitUntil( async () => - browser.execute(() => { + exec(() => { const el = document.querySelector('[data-testid="status-bar"]') ?? document.querySelector('.status-bar'); if (!el) return false; return getComputedStyle(el).display !== 'none'; - }) as Promise, + }), { timeout: 10_000, timeoutMsg: 'Status bar not visible within 10s' }, ); } @@ -65,7 +65,7 @@ export async function assertElementCount( expected: number, comparison: 'eq' | 'gte' | 'lte' = 'eq', ): Promise { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, selector); @@ -78,7 +78,7 @@ export async function assertElementCount( /** Assert an element has a specific CSS class */ export async function assertHasClass(selector: string, className: string): Promise { - const hasIt = await browser.execute((sel: string, cls: string) => { + const hasIt = await exec((sel: string, cls: string) => { const el = document.querySelector(sel); return el?.classList.contains(cls) ?? false; }, selector, className); diff --git a/tests/e2e/helpers/execute.ts b/tests/e2e/helpers/execute.ts new file mode 100644 index 0000000..48b9d72 --- /dev/null +++ b/tests/e2e/helpers/execute.ts @@ -0,0 +1,52 @@ +/** + * Cross-protocol browser.execute() wrapper. + * + * WebDriverIO's devtools protocol (CDP via puppeteer-core) breaks when + * browser.execute() receives a function argument because: + * 1. WebDriverIO prepends a polyfill before `return (fn).apply(null, arguments)` + * 2. The devtools executeScript trims the script, finds no leading `return`, + * and passes it directly to eval() + * 3. eval() fails with "Illegal return statement" because `return` is outside + * a function body (the polyfill lines precede it) + * + * Fix: always pass a string expression to browser.execute(). Arguments are + * JSON-serialized and inlined into the script — no reliance on the `arguments` + * object which is protocol-dependent. + * + * Works identically with: + * - WebDriver protocol (Tauri via tauri-driver) + * - devtools/CDP protocol (Electrobun via CEF) + */ + +import { browser } from '@wdio/globals'; + +/** + * Execute a function in the browser, cross-protocol safe. + * + * Usage mirrors browser.execute(): + * exec(() => document.title) + * exec((sel) => document.querySelector(sel) !== null, '.my-class') + * exec((a, b) => a + b, 1, 2) + */ +export async function exec(fn: (...args: any[]) => R, ...args: any[]): Promise { + const fnStr = fn.toString(); + const serializedArgs = args.map(a => JSON.stringify(a)).join(', '); + // Wrap as an IIFE expression — no `return` at the top level + const script = `return (${fnStr})(${serializedArgs})`; + return browser.execute(script) as Promise; +} + +/** + * Skip a test programmatically — works with both protocols. + * + * Mocha's this.skip() requires a non-arrow `function()` context. In + * WebDriverIO hooks (beforeTest), `this` may not carry the Mocha context + * with devtools protocol. This helper uses the same mechanism but is + * callable from any context that has the Mocha `this`. + * + * Usage inside `it('...', async function () { ... })`: + * if (condition) { skipTest(this); return; } + */ +export function skipTest(ctx: Mocha.Context): void { + ctx.skip(); +} diff --git a/tests/e2e/specs/agent.test.ts b/tests/e2e/specs/agent.test.ts index d0649ef..8b73432 100644 --- a/tests/e2e/specs/agent.test.ts +++ b/tests/e2e/specs/agent.test.ts @@ -5,10 +5,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { sendPrompt } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Agent pane', () => { it('should show the prompt input area', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.CHAT_INPUT); if (exists) { @@ -39,7 +40,7 @@ describe('Agent pane', () => { }); it('should show idle status by default', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent?.toLowerCase() ?? ''; }, S.AGENT_STATUS_TEXT); @@ -50,7 +51,7 @@ describe('Agent pane', () => { it('should accept text in the prompt input', async () => { await sendPrompt('test prompt'); - const value = await browser.execute(() => { + const value = await exec(() => { const ta = document.querySelector('.chat-input textarea') as HTMLTextAreaElement; if (ta) return ta.value; const inp = document.querySelector('.chat-input input') as HTMLInputElement; @@ -62,7 +63,7 @@ describe('Agent pane', () => { }); it('should show provider indicator', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.PROVIDER_BADGE); @@ -72,7 +73,7 @@ describe('Agent pane', () => { }); it('should show cost display', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.AGENT_COST); if (exists) { @@ -82,7 +83,7 @@ describe('Agent pane', () => { }); it('should show model selector or label', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.MODEL_LABEL); if (exists) { @@ -93,7 +94,7 @@ describe('Agent pane', () => { it('should have tool call display structure', async () => { // Tool calls render inside details elements - const hasStructure = await browser.execute(() => { + const hasStructure = await exec(() => { return document.querySelector('.tool-call') ?? document.querySelector('.tool-group') ?? document.querySelector('details'); @@ -103,7 +104,7 @@ describe('Agent pane', () => { }); it('should have timeline dots container', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.timeline') ?? document.querySelector('.turn-dots') ?? document.querySelector('.agent-timeline'); @@ -115,7 +116,7 @@ describe('Agent pane', () => { const stopBtn = await browser.$(S.STOP_BTN); if (await stopBtn.isExisting()) { // Stop button should not be displayed when agent is idle - const display = await browser.execute((sel: string) => { + const display = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return 'none'; return getComputedStyle(el).display; @@ -126,7 +127,7 @@ describe('Agent pane', () => { }); it('should have context meter', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.context-meter') ?? document.querySelector('.usage-meter') ?? document.querySelector('.token-meter'); @@ -135,7 +136,7 @@ describe('Agent pane', () => { }); it('should have prompt area with proper dimensions', async () => { - const dims = await browser.execute((sel: string) => { + const dims = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return null; const rect = el.getBoundingClientRect(); @@ -149,7 +150,7 @@ describe('Agent pane', () => { it('should clear prompt after send attempt', async () => { // Clear any existing text first - await browser.execute(() => { + await exec(() => { const ta = document.querySelector('.chat-input textarea') as HTMLTextAreaElement; if (ta) { ta.value = ''; ta.dispatchEvent(new Event('input')); } }); diff --git a/tests/e2e/specs/comms.test.ts b/tests/e2e/specs/comms.test.ts index 3080ad8..c9ff33a 100644 --- a/tests/e2e/specs/comms.test.ts +++ b/tests/e2e/specs/comms.test.ts @@ -4,10 +4,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; +import { exec } from '../helpers/execute.ts'; describe('Communications tab', () => { it('should render the comms tab container', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.COMMS_TAB); if (exists) { @@ -20,7 +21,7 @@ describe('Communications tab', () => { const modeBar = await browser.$(S.COMMS_MODE_BAR); if (!(await modeBar.isExisting())) return; - const texts = await browser.execute((sel: string) => { + const texts = await exec((sel: string) => { const buttons = document.querySelectorAll(sel); return Array.from(buttons).map(b => b.textContent?.trim() ?? ''); }, S.MODE_BTN); @@ -31,7 +32,7 @@ describe('Communications tab', () => { }); it('should highlight the active mode button', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.MODE_BTN_ACTIVE); if (exists) { @@ -47,7 +48,7 @@ describe('Communications tab', () => { }); it('should show channel list with hash prefix', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.CH_HASH); @@ -71,7 +72,7 @@ describe('Communications tab', () => { }); it('should have send button disabled when input empty', async () => { - const disabled = await browser.execute((sel: string) => { + const disabled = await exec((sel: string) => { const btn = document.querySelector(sel) as HTMLButtonElement; return btn?.disabled ?? null; }, S.MSG_SEND_BTN); @@ -81,7 +82,7 @@ describe('Communications tab', () => { }); it('should switch to DMs mode on DMs button click', async () => { - const switched = await browser.execute((sel: string) => { + const switched = await exec((sel: string) => { const buttons = document.querySelectorAll(sel); if (buttons.length < 2) return false; (buttons[1] as HTMLElement).click(); @@ -91,7 +92,7 @@ describe('Communications tab', () => { if (switched) { expect(switched).toBe(true); // Switch back - await browser.execute((sel: string) => { + await exec((sel: string) => { const buttons = document.querySelectorAll(sel); if (buttons[0]) (buttons[0] as HTMLElement).click(); }, S.MODE_BTN); @@ -100,13 +101,13 @@ describe('Communications tab', () => { }); it('should show DM contact list in DMs mode', async () => { - await browser.execute((sel: string) => { + await exec((sel: string) => { const buttons = document.querySelectorAll(sel); if (buttons.length >= 2) (buttons[1] as HTMLElement).click(); }, S.MODE_BTN); await browser.pause(300); - const hasList = await browser.execute(() => { + const hasList = await exec(() => { return (document.querySelector('.dm-list') ?? document.querySelector('.contact-list') ?? document.querySelector('.comms-sidebar')) !== null; @@ -114,7 +115,7 @@ describe('Communications tab', () => { expect(typeof hasList).toBe('boolean'); // Switch back - await browser.execute((sel: string) => { + await exec((sel: string) => { const buttons = document.querySelectorAll(sel); if (buttons[0]) (buttons[0] as HTMLElement).click(); }, S.MODE_BTN); diff --git a/tests/e2e/specs/context.test.ts b/tests/e2e/specs/context.test.ts index 55169d7..c3e8f01 100644 --- a/tests/e2e/specs/context.test.ts +++ b/tests/e2e/specs/context.test.ts @@ -5,6 +5,7 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { clickProjectTab } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Context tab', () => { before(async () => { @@ -12,7 +13,7 @@ describe('Context tab', () => { }); it('should render the context tab container', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.CONTEXT_TAB); if (exists) { @@ -22,28 +23,28 @@ describe('Context tab', () => { }); it('should show token meter', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.TOKEN_METER); expect(typeof exists).toBe('boolean'); }); it('should show file references section', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.FILE_REFS); expect(typeof exists).toBe('boolean'); }); it('should show turn count', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.TURN_COUNT); expect(typeof exists).toBe('boolean'); }); it('should show stats bar', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.context-stats') ?? document.querySelector('.stats-bar') ?? document.querySelector('.context-header')) !== null; @@ -52,7 +53,7 @@ describe('Context tab', () => { }); it('should show anchor section if available', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.anchor-section') ?? document.querySelector('.anchors') ?? document.querySelector('.anchor-budget')) !== null; @@ -61,7 +62,7 @@ describe('Context tab', () => { }); it('should show segmented meter bar', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.segment-bar') ?? document.querySelector('.meter-bar') ?? document.querySelector('.progress-bar')) !== null; @@ -70,7 +71,7 @@ describe('Context tab', () => { }); it('should show turn breakdown list', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.turn-list') ?? document.querySelector('.turn-breakdown') ?? document.querySelector('.context-turns')) !== null; @@ -79,7 +80,7 @@ describe('Context tab', () => { }); it('should have proper layout dimensions', async () => { - const dims = await browser.execute(() => { + const dims = await exec(() => { const el = document.querySelector('.context-tab'); if (!el) return null; const rect = el.getBoundingClientRect(); diff --git a/tests/e2e/specs/diagnostics.test.ts b/tests/e2e/specs/diagnostics.test.ts index 1f74753..9c3b6a5 100644 --- a/tests/e2e/specs/diagnostics.test.ts +++ b/tests/e2e/specs/diagnostics.test.ts @@ -8,10 +8,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { openSettings, closeSettings, switchSettingsCategory } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; /** Navigate to the last settings tab (expected to be Diagnostics on Electrobun) */ async function navigateToLastTab(): Promise { - const tabCount = await browser.execute(() => { + const tabCount = await exec(() => { return (document.querySelectorAll('.settings-sidebar .sidebar-item').length || document.querySelectorAll('.settings-tab').length || document.querySelectorAll('.cat-btn').length); @@ -34,7 +35,7 @@ describe('Diagnostics tab', () => { }); it('should render the diagnostics container', async function () { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.DIAGNOSTICS); if (!exists) { @@ -47,12 +48,12 @@ describe('Diagnostics tab', () => { }); it('should show Transport Diagnostics heading', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.DIAG_HEADING); @@ -62,12 +63,12 @@ describe('Diagnostics tab', () => { }); it('should show PTY daemon connection status', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } - const texts = await browser.execute((sel: string) => { + const texts = await exec((sel: string) => { const keys = document.querySelectorAll(sel); return Array.from(keys).map(k => k.textContent ?? ''); }, S.DIAG_KEY); @@ -77,12 +78,12 @@ describe('Diagnostics tab', () => { }); it('should show agent fleet section', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } - const texts = await browser.execute((sel: string) => { + const texts = await exec((sel: string) => { const labels = document.querySelectorAll(sel); return Array.from(labels).map(l => l.textContent?.toLowerCase() ?? ''); }, S.DIAG_LABEL); @@ -92,12 +93,12 @@ describe('Diagnostics tab', () => { }); it('should show last refresh timestamp', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } - const footerExists = await browser.execute((sel: string) => { + const footerExists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.DIAG_FOOTER); if (footerExists) { @@ -107,7 +108,7 @@ describe('Diagnostics tab', () => { }); it('should have a refresh button', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } @@ -119,12 +120,12 @@ describe('Diagnostics tab', () => { }); it('should show connection indicator with color', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } - const hasIndicator = await browser.execute(() => { + const hasIndicator = await exec(() => { return (document.querySelector('.diag-status') ?? document.querySelector('.status-dot') ?? document.querySelector('.connection-status')) !== null; @@ -133,12 +134,12 @@ describe('Diagnostics tab', () => { }); it('should show session count', async function () { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.diagnostics') !== null; }); if (!exists) { this.skip(); return; } - const hasCount = await browser.execute(() => { + const hasCount = await exec(() => { return (document.querySelector('.session-count') ?? document.querySelector('.diag-value')) !== null; }); diff --git a/tests/e2e/specs/features.test.ts b/tests/e2e/specs/features.test.ts index b5dc216..6feb0fb 100644 --- a/tests/e2e/specs/features.test.ts +++ b/tests/e2e/specs/features.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; /** Reset UI to home state (close any open panels/overlays). */ async function resetToHomeState(): Promise { @@ -15,7 +16,7 @@ async function resetToHomeState(): Promise { async function closeSettings(): Promise { const panel = await browser.$('.sidebar-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -29,14 +30,14 @@ async function openCommandPalette(): Promise { await closeSettings(); // Check if already open - const alreadyOpen = await browser.execute(() => { + const alreadyOpen = await exec(() => { const p = document.querySelector('.palette'); return p !== null && getComputedStyle(p).display !== 'none'; }); if (alreadyOpen) return; // Dispatch Ctrl+K via JS for reliability with WebKit2GTK/tauri-driver - await browser.execute(() => { + await exec(() => { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', code: 'KeyK', ctrlKey: true, bubbles: true, cancelable: true, })); @@ -49,14 +50,14 @@ async function openCommandPalette(): Promise { /** Close command palette if open — uses backdrop click (more reliable than Escape). */ async function closeCommandPalette(): Promise { - const isOpen = await browser.execute(() => { + const isOpen = await exec(() => { const p = document.querySelector('.palette'); return p !== null && getComputedStyle(p).display !== 'none'; }); if (!isOpen) return; // Click backdrop to close (more reliable than dispatching Escape) - await browser.execute(() => { + await exec(() => { const backdrop = document.querySelector('.palette-backdrop'); if (backdrop) (backdrop as HTMLElement).click(); }); @@ -76,7 +77,7 @@ describe('BTerminal — Command Palette', () => { // Verify input accepts text (functional focus test, not activeElement check // which is unreliable in WebKit2GTK/tauri-driver) - const canType = await browser.execute(() => { + const canType = await exec(() => { const el = document.querySelector('.palette-input') as HTMLInputElement | null; if (!el) return false; el.focus(); @@ -142,7 +143,7 @@ describe('BTerminal — Command Palette', () => { const palette = await browser.$('.palette'); // Click the backdrop (outside the palette) - await browser.execute(() => { + await exec(() => { const backdrop = document.querySelector('.palette-backdrop'); if (backdrop) (backdrop as HTMLElement).click(); }); diff --git a/tests/e2e/specs/files.test.ts b/tests/e2e/specs/files.test.ts index 1342e22..dd8fe5f 100644 --- a/tests/e2e/specs/files.test.ts +++ b/tests/e2e/specs/files.test.ts @@ -5,6 +5,7 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { clickProjectTab } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('File browser', () => { before(async () => { @@ -34,7 +35,7 @@ describe('File browser', () => { }); it('should show directory rows in tree', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.FB_DIR); if (count > 0) { @@ -43,7 +44,7 @@ describe('File browser', () => { }); it('should show file rows in tree', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.FB_FILE); if (count > 0) { @@ -52,7 +53,7 @@ describe('File browser', () => { }); it('should show placeholder when no file selected', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent?.toLowerCase() ?? ''; }, S.FB_EMPTY); @@ -68,7 +69,7 @@ describe('File browser', () => { await dirs[0].click(); await browser.pause(500); - const isOpen = await browser.execute((sel: string) => { + const isOpen = await exec((sel: string) => { const chevron = document.querySelector(`${sel} .fb-chevron`); return chevron?.classList.contains('open') ?? false; }, S.FB_DIR); @@ -84,7 +85,7 @@ describe('File browser', () => { await files[0].click(); await browser.pause(500); - const hasContent = await browser.execute(() => { + const hasContent = await exec(() => { return (document.querySelector('.fb-editor-header') ?? document.querySelector('.fb-image-wrap') ?? document.querySelector('.fb-error') @@ -94,7 +95,7 @@ describe('File browser', () => { }); it('should show file type icon in tree', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.FILE_TYPE); if (count > 0) { @@ -114,14 +115,14 @@ describe('File browser', () => { }); it('should have CodeMirror editor for text files', async () => { - const hasCM = await browser.execute(() => { + const hasCM = await exec(() => { return document.querySelector('.cm-editor') !== null; }); expect(typeof hasCM).toBe('boolean'); }); it('should have save button when editing', async () => { - const hasBtn = await browser.execute(() => { + const hasBtn = await exec(() => { return (document.querySelector('.save-btn') ?? document.querySelector('.fb-save')) !== null; }); @@ -129,7 +130,7 @@ describe('File browser', () => { }); it('should show dirty indicator for modified files', async () => { - const hasDirty = await browser.execute(() => { + const hasDirty = await exec(() => { return (document.querySelector('.dirty-dot') ?? document.querySelector('.unsaved')) !== null; }); @@ -137,7 +138,7 @@ describe('File browser', () => { }); it('should handle image display', async () => { - const hasImage = await browser.execute(() => { + const hasImage = await exec(() => { return document.querySelector('.fb-image-wrap') ?? document.querySelector('.fb-image'); }); @@ -145,7 +146,7 @@ describe('File browser', () => { }); it('should have PDF viewer component', async () => { - const hasPdf = await browser.execute(() => { + const hasPdf = await exec(() => { return document.querySelector('.pdf-viewer') ?? document.querySelector('.pdf-container'); }); diff --git a/tests/e2e/specs/groups.test.ts b/tests/e2e/specs/groups.test.ts index 8335342..351386b 100644 --- a/tests/e2e/specs/groups.test.ts +++ b/tests/e2e/specs/groups.test.ts @@ -8,10 +8,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { switchGroup } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Group sidebar', () => { it('should show group buttons in sidebar', async function () { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.GROUP_BTN); if (count === 0) { @@ -23,12 +24,12 @@ describe('Group sidebar', () => { }); it('should show numbered circle for each group', async function () { - const hasGroups = await browser.execute(() => { + const hasGroups = await exec(() => { return document.querySelector('.group-circle') !== null; }); if (!hasGroups) { this.skip(); return; } - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.GROUP_CIRCLE); @@ -36,19 +37,19 @@ describe('Group sidebar', () => { }); it('should highlight the active group', async function () { - const hasGroups = await browser.execute(() => { + const hasGroups = await exec(() => { return document.querySelectorAll('.group-btn').length > 0; }); if (!hasGroups) { this.skip(); return; } - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.GROUP_BTN_ACTIVE); expect(count).toBe(1); }); it('should show add group button', async function () { - const hasGroups = await browser.execute(() => { + const hasGroups = await exec(() => { return document.querySelectorAll('.group-btn').length > 0; }); if (!hasGroups) { this.skip(); return; } @@ -57,7 +58,7 @@ describe('Group sidebar', () => { if (await addBtn.isExisting()) { await expect(addBtn).toBeDisplayed(); - const text = await browser.execute(() => { + const text = await exec(() => { const circle = document.querySelector('.add-group-btn .group-circle'); return circle?.textContent ?? ''; }); @@ -66,14 +67,14 @@ describe('Group sidebar', () => { }); it('should switch active group on click', async function () { - const groupCount = await browser.execute(() => { + const groupCount = await exec(() => { return document.querySelectorAll('.group-btn:not(.add-group-btn)').length; }); if (groupCount < 2) { this.skip(); return; } await switchGroup(1); - const isActive = await browser.execute(() => { + const isActive = await exec(() => { const groups = document.querySelectorAll('.group-btn:not(.add-group-btn)'); return groups[1]?.classList.contains('active') ?? false; }); @@ -84,7 +85,7 @@ describe('Group sidebar', () => { }); it('should show notification badge structure', async function () { - const hasGroups = await browser.execute(() => { + const hasGroups = await exec(() => { return document.querySelectorAll('.group-btn').length > 0; }); if (!hasGroups) { this.skip(); return; } @@ -104,19 +105,19 @@ describe('Group sidebar', () => { }); it('should update project grid on group switch', async function () { - const groupCount = await browser.execute(() => { + const groupCount = await exec(() => { return document.querySelectorAll('.group-btn:not(.add-group-btn)').length; }); if (groupCount < 2) { this.skip(); return; } - const cardsBefore = await browser.execute(() => { + const cardsBefore = await exec(() => { return document.querySelectorAll('.project-box, .project-card').length; }); await switchGroup(1); await browser.pause(300); - const cardsAfter = await browser.execute(() => { + const cardsAfter = await exec(() => { return document.querySelectorAll('.project-box, .project-card').length; }); @@ -136,12 +137,12 @@ describe('Group sidebar', () => { }); it('should persist active group across sessions', async function () { - const hasGroups = await browser.execute(() => { + const hasGroups = await exec(() => { return document.querySelectorAll('.group-btn:not(.add-group-btn)').length > 0; }); if (!hasGroups) { this.skip(); return; } - const activeIdx = await browser.execute(() => { + const activeIdx = await exec(() => { const groups = document.querySelectorAll('.group-btn:not(.add-group-btn)'); for (let i = 0; i < groups.length; i++) { if (groups[i].classList.contains('active')) return i; diff --git a/tests/e2e/specs/keyboard.test.ts b/tests/e2e/specs/keyboard.test.ts index ccac399..a26fb7e 100644 --- a/tests/e2e/specs/keyboard.test.ts +++ b/tests/e2e/specs/keyboard.test.ts @@ -5,12 +5,13 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { openCommandPalette, closeCommandPalette } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Command palette', () => { it('should open via Ctrl+K', async () => { await openCommandPalette(); - const visible = await browser.execute((sel: string) => { + const visible = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return false; return getComputedStyle(el).display !== 'none'; @@ -33,7 +34,7 @@ describe('Command palette', () => { }); it('should list commands (14+ expected)', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.PALETTE_ITEM); // Command count varies: 14 base + up to 5 project focus + N group switches @@ -41,12 +42,12 @@ describe('Command palette', () => { }); it('should show command labels and shortcuts', async () => { - const labelCount = await browser.execute((sel: string) => { + const labelCount = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.CMD_LABEL); expect(labelCount).toBeGreaterThan(0); - const shortcutCount = await browser.execute((sel: string) => { + const shortcutCount = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.CMD_SHORTCUT); expect(shortcutCount).toBeGreaterThan(0); @@ -59,7 +60,7 @@ describe('Command palette', () => { await input.setValue('terminal'); await browser.pause(200); - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.PALETTE_ITEM); expect(count).toBeLessThan(18); @@ -67,7 +68,7 @@ describe('Command palette', () => { }); it('should highlight first item', async () => { - const hasHighlight = await browser.execute(() => { + const hasHighlight = await exec(() => { return (document.querySelector('.palette-item.active') ?? document.querySelector('.palette-item.highlighted') ?? document.querySelector('.palette-item:first-child')) !== null; @@ -98,20 +99,20 @@ describe('Command palette', () => { await closeCommandPalette(); - const hidden = await browser.execute((sel: string) => { + const hidden = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return true; return getComputedStyle(el).display === 'none'; }, S.PALETTE_BACKDROP); if (!hidden) { // Fallback: click the backdrop to close - await browser.execute(() => { + await exec(() => { const backdrop = document.querySelector('.palette-backdrop'); if (backdrop) (backdrop as HTMLElement).click(); }); await browser.pause(300); } - const finalCheck = await browser.execute((sel: string) => { + const finalCheck = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return true; return getComputedStyle(el).display === 'none'; diff --git a/tests/e2e/specs/llm-judged.test.ts b/tests/e2e/specs/llm-judged.test.ts index ace9f0a..f7c20ea 100644 --- a/tests/e2e/specs/llm-judged.test.ts +++ b/tests/e2e/specs/llm-judged.test.ts @@ -6,6 +6,7 @@ */ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; const API_KEY = process.env.ANTHROPIC_API_KEY; const SKIP = !API_KEY; @@ -43,7 +44,7 @@ describe('LLM-judged UI quality', () => { it('should have complete settings panel', async function () { if (SKIP) return this.skip(); - const html = await browser.execute(() => { + const html = await exec(() => { const panel = document.querySelector('.settings-drawer') ?? document.querySelector('.sidebar-panel'); return panel?.innerHTML?.slice(0, 2000) ?? ''; @@ -58,7 +59,7 @@ describe('LLM-judged UI quality', () => { it('should have visually consistent theme', async function () { if (SKIP) return this.skip(); - const vars = await browser.execute(() => { + const vars = await exec(() => { const s = getComputedStyle(document.documentElement); return { base: s.getPropertyValue('--ctp-base').trim(), @@ -77,7 +78,7 @@ describe('LLM-judged UI quality', () => { it('should have proper error handling in UI', async function () { if (SKIP) return this.skip(); - const toasts = await browser.execute(() => { + const toasts = await exec(() => { return document.querySelectorAll('.toast-error, .load-error').length; }); @@ -90,7 +91,7 @@ describe('LLM-judged UI quality', () => { it('should have readable text contrast', async function () { if (SKIP) return this.skip(); - const colors = await browser.execute(() => { + const colors = await exec(() => { const body = getComputedStyle(document.body); return { bg: body.backgroundColor, @@ -109,7 +110,7 @@ describe('LLM-judged UI quality', () => { it('should have well-structured project cards', async function () { if (SKIP) return this.skip(); - const html = await browser.execute(() => { + const html = await exec(() => { const card = document.querySelector('.project-box') ?? document.querySelector('.project-card'); return card?.innerHTML?.slice(0, 1500) ?? ''; }); @@ -125,7 +126,7 @@ describe('LLM-judged UI quality', () => { it('should have consistent layout structure', async function () { if (SKIP) return this.skip(); - const layout = await browser.execute(() => { + const layout = await exec(() => { const el = document.querySelector('.app-shell') ?? document.body; const children = Array.from(el.children).map(c => ({ tag: c.tagName, @@ -145,7 +146,7 @@ describe('LLM-judged UI quality', () => { it('should have accessible interactive elements', async function () { if (SKIP) return this.skip(); - const stats = await browser.execute(() => { + const stats = await exec(() => { const buttons = document.querySelectorAll('button'); const withLabel = Array.from(buttons).filter(b => b.textContent?.trim() || b.getAttribute('aria-label') || b.getAttribute('title') @@ -163,7 +164,7 @@ describe('LLM-judged UI quality', () => { if (SKIP) return this.skip(); // Check console for errors (if available) - const errorCount = await browser.execute(() => { + const errorCount = await exec(() => { return document.querySelectorAll('.toast-error, .load-error, .error-boundary').length; }); @@ -173,7 +174,7 @@ describe('LLM-judged UI quality', () => { it('should have responsive grid layout', async function () { if (SKIP) return this.skip(); - const grid = await browser.execute(() => { + const grid = await exec(() => { const el = document.querySelector('.project-grid'); if (!el) return null; const rect = el.getBoundingClientRect(); @@ -188,7 +189,7 @@ describe('LLM-judged UI quality', () => { it('should have status bar with meaningful content', async function () { if (SKIP) return this.skip(); - const content = await browser.execute(() => { + const content = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]') ?? document.querySelector('.status-bar'); return bar?.textContent?.trim() ?? ''; diff --git a/tests/e2e/specs/notifications.test.ts b/tests/e2e/specs/notifications.test.ts index e33f973..552e819 100644 --- a/tests/e2e/specs/notifications.test.ts +++ b/tests/e2e/specs/notifications.test.ts @@ -8,10 +8,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { openNotifications, closeNotifications } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Notification system', () => { it('should show the notification bell button', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.notif-btn') ?? document.querySelector('.bell-btn') ?? document.querySelector('[data-testid="notification-bell"]')) !== null; @@ -23,7 +24,7 @@ describe('Notification system', () => { it('should open notification drawer on bell click', async () => { await openNotifications(); - const visible = await browser.execute(() => { + const visible = await exec(() => { // Tauri: .notification-center .panel | Electrobun: .notif-drawer const el = document.querySelector('.notif-drawer') ?? document.querySelector('[data-testid="notification-panel"]') @@ -37,7 +38,7 @@ describe('Notification system', () => { }); it('should show drawer header with title', async () => { - const text = await browser.execute(() => { + const text = await exec(() => { // Tauri: .panel-title | Electrobun: .drawer-title const el = document.querySelector('.drawer-title') ?? document.querySelector('.panel-title'); @@ -49,7 +50,7 @@ describe('Notification system', () => { }); it('should show clear all button', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { // Tauri: .action-btn with "Clear" | Electrobun: .clear-btn return (document.querySelector('.clear-btn') ?? document.querySelector('.action-btn')) !== null; @@ -60,7 +61,7 @@ describe('Notification system', () => { it('should show empty state or notification items', async () => { await openNotifications(); - const hasContent = await browser.execute(() => { + const hasContent = await exec(() => { // Tauri: .empty or .notification-item | Electrobun: .notif-empty or .notif-item const empty = document.querySelector('.notif-empty') ?? document.querySelector('.empty'); const items = document.querySelectorAll('.notif-item, .notification-item'); @@ -73,7 +74,7 @@ describe('Notification system', () => { it('should close drawer on backdrop click', async () => { await closeNotifications(); - const hidden = await browser.execute(() => { + const hidden = await exec(() => { const el = document.querySelector('.notif-drawer') ?? document.querySelector('[data-testid="notification-panel"]') ?? document.querySelector('.notification-center .panel'); @@ -84,7 +85,7 @@ describe('Notification system', () => { }); it('should show unread badge when notifications exist', async () => { - const hasBadge = await browser.execute(() => { + const hasBadge = await exec(() => { return (document.querySelector('.notif-badge') ?? document.querySelector('.unread-count') ?? document.querySelector('.badge')) !== null; @@ -96,7 +97,7 @@ describe('Notification system', () => { it('should reopen drawer after close', async () => { await openNotifications(); - const visible = await browser.execute(() => { + const visible = await exec(() => { const el = document.querySelector('.notif-drawer') ?? document.querySelector('[data-testid="notification-panel"]') ?? document.querySelector('.notification-center .panel'); @@ -112,7 +113,7 @@ describe('Notification system', () => { it('should show notification timestamp', async () => { await openNotifications(); - const hasTimestamp = await browser.execute(() => { + const hasTimestamp = await exec(() => { return (document.querySelector('.notif-time') ?? document.querySelector('.notif-timestamp')) !== null; }); @@ -122,7 +123,7 @@ describe('Notification system', () => { it('should show mark-read action', async () => { await openNotifications(); - const hasAction = await browser.execute(() => { + const hasAction = await exec(() => { return (document.querySelector('.mark-read') ?? document.querySelector('.notif-action') ?? document.querySelector('.action-btn')) !== null; diff --git a/tests/e2e/specs/phase-a-agent.test.ts b/tests/e2e/specs/phase-a-agent.test.ts index 17c1bf6..a6521dc 100644 --- a/tests/e2e/specs/phase-a-agent.test.ts +++ b/tests/e2e/specs/phase-a-agent.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase A — Agent: Agent pane initial state + prompt submission + NEW agent tests. // Shares a single Tauri app session with other phase-a-* spec files. @@ -8,7 +9,7 @@ import { browser, expect } from '@wdio/globals'; async function waitForAgentStatus(status: string, timeout = 30_000): Promise { await browser.waitUntil( async () => { - const attr = await browser.execute(() => { + const attr = await exec(() => { const el = document.querySelector('[data-testid="agent-pane"]'); return el?.getAttribute('data-agent-status') ?? 'idle'; }); @@ -29,18 +30,18 @@ async function sendAgentPrompt(text: string): Promise { await textarea.setValue(text); await browser.pause(200); const submitBtn = await browser.$('[data-testid="agent-submit"]'); - await browser.execute((el) => (el as HTMLElement).click(), submitBtn); + await submitBtn.click(); } async function getAgentStatus(): Promise { - return browser.execute(() => { + return exec(() => { const el = document.querySelector('[data-testid="agent-pane"]'); return el?.getAttribute('data-agent-status') ?? 'unknown'; }); } async function getMessageCount(): Promise { - return browser.execute(() => { + return exec(() => { const area = document.querySelector('[data-testid="agent-messages"]'); return area ? area.children.length : 0; }); @@ -51,7 +52,7 @@ async function getMessageCount(): Promise { describe('Scenario 3 — Agent Pane Initial State', () => { it('should display agent pane in idle status', async () => { if (!(await agentPaneExists())) { - await browser.execute(() => { + await exec(() => { const tab = document.querySelector('[data-testid="project-tabs"] .ptab'); if (tab) (tab as HTMLElement).click(); }); @@ -75,7 +76,7 @@ describe('Scenario 3 — Agent Pane Initial State', () => { it('should have empty messages area initially', async () => { const msgArea = await browser.$('[data-testid="agent-messages"]'); await expect(msgArea).toBeExisting(); - const msgCount = await browser.execute(() => { + const msgCount = await exec(() => { const area = document.querySelector('[data-testid="agent-messages"]'); return area ? area.querySelectorAll('.message').length : 0; }); @@ -83,7 +84,7 @@ describe('Scenario 3 — Agent Pane Initial State', () => { }); it('should show agent provider name or badge', async () => { - const hasContent = await browser.execute(() => { + const hasContent = await exec(() => { const session = document.querySelector('[data-testid="agent-session"]'); return session !== null && (session.textContent ?? '').length > 0; }); @@ -93,7 +94,7 @@ describe('Scenario 3 — Agent Pane Initial State', () => { it('should show cost display area (when session exists) or prompt area', async () => { // status-strip only renders when session is non-null; before first query // the agent pane shows only the prompt area — both are valid states - const state = await browser.execute(() => { + const state = await exec(() => { const pane = document.querySelector('[data-testid="agent-pane"]'); if (!pane) return 'no-pane'; if (pane.querySelector('.status-strip')) return 'has-status'; @@ -106,7 +107,7 @@ describe('Scenario 3 — Agent Pane Initial State', () => { it('should show agent pane with prompt or status area', async () => { // Context meter only visible during running state; verify pane structure instead - const hasPane = await browser.execute(() => { + const hasPane = await exec(() => { const pane = document.querySelector('[data-testid="agent-pane"]'); return pane !== null; }); @@ -114,7 +115,7 @@ describe('Scenario 3 — Agent Pane Initial State', () => { }); it('should have tool call/result collapsible sections area', async () => { - const ready = await browser.execute(() => { + const ready = await exec(() => { const area = document.querySelector('[data-testid="agent-messages"]'); return area !== null && area instanceof HTMLElement; }); @@ -138,7 +139,7 @@ describe('Scenario 7 — Agent Prompt Submission', () => { const textarea = await browser.$('[data-testid="agent-prompt"]'); await textarea.setValue('Test prompt'); await browser.pause(200); - const isDisabled = await browser.execute(() => { + const isDisabled = await exec(() => { const btn = document.querySelector('[data-testid="agent-submit"]'); return btn ? (btn as HTMLButtonElement).disabled : true; }); @@ -204,7 +205,7 @@ describe('Scenario 7 — Agent Prompt Submission', () => { try { await waitForAgentStatus('running', 15_000); } catch { this.skip(); return; } - const uiState = await browser.execute(() => { + const uiState = await exec(() => { const textarea = document.querySelector('[data-testid="agent-prompt"]') as HTMLTextAreaElement | null; const stopBtn = document.querySelector('[data-testid="agent-stop"]'); return { @@ -214,7 +215,7 @@ describe('Scenario 7 — Agent Prompt Submission', () => { }); expect(uiState.textareaDisabled || uiState.stopBtnVisible).toBe(true); try { await waitForAgentStatus('idle', 40_000); } catch { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="agent-stop"]'); if (btn) (btn as HTMLElement).click(); }); diff --git a/tests/e2e/specs/phase-a-navigation.test.ts b/tests/e2e/specs/phase-a-navigation.test.ts index e681328..b7e34d8 100644 --- a/tests/e2e/specs/phase-a-navigation.test.ts +++ b/tests/e2e/specs/phase-a-navigation.test.ts @@ -1,26 +1,27 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase A — Navigation: Terminal tabs + command palette + project focus + NEW tests. // Shares a single Tauri app session with other phase-a-* spec files. describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { before(async () => { // Ensure Model tab active (terminal section only visible in Model tab) - await browser.execute(() => { + await exec(() => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs[0]) (tabs[0] as HTMLElement).click(); // Model is first tab }); await browser.pause(300); // Expand terminal section if collapsed - const isExpanded = await browser.execute(() => + const isExpanded = await exec(() => document.querySelector('[data-testid="terminal-tabs"]') !== null ); if (!isExpanded) { - await browser.execute(() => { + await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); if (toggle) (toggle as HTMLElement).click(); }); await browser.waitUntil( - async () => browser.execute(() => + async () => exec(() => document.querySelector('[data-testid="terminal-tabs"]') !== null ) as Promise, { timeout: 5000, timeoutMsg: 'Terminal tabs did not appear after expanding' }, @@ -29,19 +30,19 @@ describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { }); it('should display terminal tabs container', async () => { - const exists = await browser.execute(() => + const exists = await exec(() => document.querySelector('[data-testid="terminal-tabs"]') !== null ); expect(exists).toBe(true); }); it('should add a shell tab via data-testid button', async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="tab-add"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); - const tabTitle = await browser.execute(() => { + const tabTitle = await exec(() => { const el = document.querySelector('.tab-bar .tab-title'); return el?.textContent ?? ''; }); @@ -50,28 +51,28 @@ describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { it('should show active tab styling after adding tab', async () => { // Ensure at least one tab exists (may need to add one) - const tabCount = await browser.execute(() => + const tabCount = await exec(() => document.querySelectorAll('[data-testid="terminal-tabs"] .tab-bar .tab').length ); if (tabCount === 0) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="tab-add"]') ?? document.querySelector('.add-first'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); } - const hasActive = await browser.execute(() => + const hasActive = await exec(() => document.querySelector('[data-testid="terminal-tabs"] .tab.active') !== null ); expect(hasActive).toBe(true); }); it('should close tab and show empty state', async () => { - await browser.execute(() => { + await exec(() => { document.querySelectorAll('[data-testid="terminal-tabs"] .tab-close').forEach(btn => (btn as HTMLElement).click()); }); await browser.pause(500); - const hasEmpty = await browser.execute(() => + const hasEmpty = await exec(() => document.querySelector('[data-testid="terminal-tabs"] .add-first') !== null || document.querySelector('[data-testid="terminal-tabs"] .empty-terminals') !== null ); @@ -79,7 +80,7 @@ describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { }); it('should show terminal toggle chevron', async () => { - const has = await browser.execute(() => { + const has = await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); return toggle?.querySelector('.toggle-chevron') !== null; }); @@ -88,12 +89,12 @@ describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { it('should show agent preview button (eye icon) if agent session active', async () => { // Preview button presence depends on active agent session — may not be present - const hasPreviewBtn = await browser.execute(() => { + const hasPreviewBtn = await exec(() => { const tabs = document.querySelector('[data-testid="terminal-tabs"]'); return tabs?.querySelector('.tab-add.tab-agent-preview') !== null; }); if (hasPreviewBtn) { - const withinTabs = await browser.execute(() => { + const withinTabs = await exec(() => { const tabs = document.querySelector('[data-testid="terminal-tabs"]'); return tabs?.querySelector('.tab-add.tab-agent-preview') !== null; }); @@ -103,38 +104,38 @@ describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { it('should maintain terminal state across project tab switches', async () => { // Add a shell tab - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="tab-add"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); - const countBefore = await browser.execute(() => + const countBefore = await exec(() => document.querySelectorAll('.tab-bar .tab').length, ); // Switch to Files tab and back - await browser.execute(() => { + await exec(() => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs.length >= 2) (tabs[1] as HTMLElement).click(); }); await browser.pause(300); - await browser.execute(() => { + await exec(() => { const tab = document.querySelector('[data-testid="project-tabs"] .ptab'); if (tab) (tab as HTMLElement).click(); }); await browser.pause(300); - const countAfter = await browser.execute(() => + const countAfter = await exec(() => document.querySelectorAll('.tab-bar .tab').length, ); expect(countAfter).toBe(countBefore); // Clean up - await browser.execute(() => { + await exec(() => { document.querySelectorAll('.tab-close').forEach(btn => (btn as HTMLElement).click()); }); await browser.pause(300); }); after(async () => { - await browser.execute(() => { + await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); if (toggle?.querySelector('.toggle-chevron.expanded')) (toggle as HTMLElement).click(); }); @@ -144,7 +145,7 @@ describe('Scenario 4 — Terminal Tab Management (data-testid)', () => { describe('Scenario 5 — Command Palette (data-testid)', () => { before(async () => { - await browser.execute(() => { + await exec(() => { const p = document.querySelector('[data-testid="command-palette"]'); if (p && (p as HTMLElement).offsetParent !== null) document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); @@ -153,7 +154,7 @@ describe('Scenario 5 — Command Palette (data-testid)', () => { }); it('should open palette and show data-testid input', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(200); await browser.keys(['Control', 'k']); const palette = await browser.$('[data-testid="command-palette"]'); @@ -163,7 +164,7 @@ describe('Scenario 5 — Command Palette (data-testid)', () => { }); it('should have focused input', async () => { - const isFocused = await browser.execute(() => { + const isFocused = await exec(() => { const el = document.querySelector('[data-testid="palette-input"]') as HTMLInputElement | null; if (!el) return false; el.focus(); @@ -187,14 +188,14 @@ describe('Scenario 5 — Command Palette (data-testid)', () => { it('should show command categories in palette', async () => { // Re-open palette if closed by previous test - const isOpen = await browser.execute(() => + const isOpen = await exec(() => document.querySelector('[data-testid="command-palette"]') !== null ); if (!isOpen) { await browser.keys(['Control', 'k']); await browser.pause(500); } - const catCount = await browser.execute(() => + const catCount = await exec(() => document.querySelectorAll('.palette-category').length, ); expect(catCount).toBeGreaterThanOrEqual(1); @@ -205,14 +206,14 @@ describe('Scenario 5 — Command Palette (data-testid)', () => { await input.clearValue(); await input.setValue('settings'); await browser.pause(300); - const executed = await browser.execute(() => { + const executed = await exec(() => { const item = document.querySelector('.palette-item'); if (item) { (item as HTMLElement).click(); return true; } return false; }); expect(executed).toBe(true); await browser.pause(500); - const paletteGone = await browser.execute(() => { + const paletteGone = await exec(() => { const p = document.querySelector('[data-testid="command-palette"]'); return p === null || (p as HTMLElement).offsetParent === null; }); @@ -223,11 +224,11 @@ describe('Scenario 5 — Command Palette (data-testid)', () => { }); it('should show keyboard shortcut hints in palette items', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(200); await browser.keys(['Control', 'k']); await browser.pause(300); - const has = await browser.execute(() => + const has = await exec(() => document.querySelectorAll('.palette-item .cmd-shortcut').length > 0, ); expect(has).toBe(true); @@ -242,7 +243,7 @@ describe('Scenario 5 — Command Palette (data-testid)', () => { describe('Scenario 6 — Project Focus & Tab Switching', () => { before(async () => { - await browser.execute(() => { + await exec(() => { const tab = document.querySelector('[data-testid="project-tabs"] .ptab'); if (tab) (tab as HTMLElement).click(); }); @@ -250,7 +251,7 @@ describe('Scenario 6 — Project Focus & Tab Switching', () => { }); it('should focus project on header click', async () => { - await browser.execute(() => { + await exec(() => { const header = document.querySelector('.project-header'); if (header) (header as HTMLElement).click(); }); @@ -260,18 +261,18 @@ describe('Scenario 6 — Project Focus & Tab Switching', () => { }); it('should switch to Files tab and back without losing agent session', async () => { - expect(await browser.execute(() => + expect(await exec(() => document.querySelector('[data-testid="agent-session"]') !== null, )).toBe(true); - await browser.execute(() => { + await exec(() => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs.length >= 2) (tabs[1] as HTMLElement).click(); }); await browser.pause(500); - expect(await browser.execute(() => + expect(await exec(() => document.querySelector('[data-testid="agent-session"]') !== null, )).toBe(true); - await browser.execute(() => { + await exec(() => { const tab = document.querySelector('[data-testid="project-tabs"] .ptab'); if (tab) (tab as HTMLElement).click(); }); @@ -281,46 +282,46 @@ describe('Scenario 6 — Project Focus & Tab Switching', () => { }); it('should preserve agent status across tab switches', async () => { - const statusBefore = await browser.execute(() => + const statusBefore = await exec(() => document.querySelector('[data-testid="agent-pane"]')?.getAttribute('data-agent-status') ?? 'unknown', ); - await browser.execute(() => { + await exec(() => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs.length >= 3) (tabs[2] as HTMLElement).click(); }); await browser.pause(300); - await browser.execute(() => { + await exec(() => { const tab = document.querySelector('[data-testid="project-tabs"] .ptab'); if (tab) (tab as HTMLElement).click(); }); await browser.pause(300); - const statusAfter = await browser.execute(() => + const statusAfter = await exec(() => document.querySelector('[data-testid="agent-pane"]')?.getAttribute('data-agent-status') ?? 'unknown', ); expect(statusAfter).toBe(statusBefore); }); it('should show tab count badge in terminal toggle', async () => { - await browser.execute(() => { + await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); if (toggle) (toggle as HTMLElement).click(); }); await browser.pause(300); - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="tab-add"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); - const text = await browser.execute(() => + const text = await exec(() => document.querySelector('[data-testid="terminal-toggle"]')?.textContent?.trim() ?? '', ); expect(text.length).toBeGreaterThan(0); // Clean up - await browser.execute(() => { + await exec(() => { document.querySelectorAll('.tab-close').forEach(btn => (btn as HTMLElement).click()); }); await browser.pause(300); - await browser.execute(() => { + await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); if (toggle?.querySelector('.toggle-chevron.expanded')) (toggle as HTMLElement).click(); }); diff --git a/tests/e2e/specs/phase-a-structure.test.ts b/tests/e2e/specs/phase-a-structure.test.ts index 2c10246..f532b0a 100644 --- a/tests/e2e/specs/phase-a-structure.test.ts +++ b/tests/e2e/specs/phase-a-structure.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase A — Structure: App structural integrity + settings panel + NEW structural tests. // Shares a single Tauri app session with other phase-a-* spec files. @@ -22,7 +23,7 @@ describe('Scenario 1 — App Structural Integrity', () => { }); it('should have data-project-id on project boxes', async () => { - const projectId = await browser.execute(() => { + const projectId = await exec(() => { const box = document.querySelector('[data-testid="project-box"]'); return box?.getAttribute('data-project-id') ?? null; }); @@ -50,7 +51,7 @@ describe('Scenario 1 — App Structural Integrity', () => { it('should have data-testid on status bar sections (agent-counts, cost, attention)', async () => { // Status bar left section contains agent state items and attention - const leftItems = await browser.execute(() => { + const leftItems = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); if (!bar) return { left: false, right: false }; const left = bar.querySelector('.left'); @@ -62,7 +63,7 @@ describe('Scenario 1 — App Structural Integrity', () => { }); it('should render project accent colors (different per slot)', async () => { - const accents = await browser.execute(() => { + const accents = await exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); const styles: string[] = []; boxes.forEach(box => { @@ -83,7 +84,7 @@ describe('Scenario 1 — App Structural Integrity', () => { }); it('should show project name in header', async () => { - const name = await browser.execute(() => { + const name = await exec(() => { const el = document.querySelector('.project-header .project-name'); return el?.textContent?.trim() ?? ''; }); @@ -91,7 +92,7 @@ describe('Scenario 1 — App Structural Integrity', () => { }); it('should show project icon in header', async () => { - const icon = await browser.execute(() => { + const icon = await exec(() => { const el = document.querySelector('.project-header .project-icon'); return el?.textContent?.trim() ?? ''; }); @@ -100,7 +101,7 @@ describe('Scenario 1 — App Structural Integrity', () => { }); it('should have correct grid layout (project boxes fill available space)', async () => { - const layout = await browser.execute(() => { + const layout = await exec(() => { const box = document.querySelector('[data-testid="project-box"]') as HTMLElement | null; if (!box) return { width: 0, height: 0 }; const rect = box.getBoundingClientRect(); @@ -117,7 +118,7 @@ describe('Scenario 1 — App Structural Integrity', () => { describe('Scenario 2 — Settings Panel (data-testid)', () => { before(async () => { // Ensure settings panel is closed before starting - await browser.execute(() => { + await exec(() => { const panel = document.querySelector('.sidebar-panel'); if (panel && (panel as HTMLElement).offsetParent !== null) { const btn = document.querySelector('.panel-close'); @@ -129,7 +130,7 @@ describe('Scenario 2 — Settings Panel (data-testid)', () => { it('should open settings via data-testid button', async () => { // Use JS click for reliability with WebKit2GTK/tauri-driver - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); @@ -139,7 +140,7 @@ describe('Scenario 2 — Settings Panel (data-testid)', () => { // Wait for settings panel content to mount await browser.waitUntil( async () => { - const has = await browser.execute(() => + const has = await exec(() => document.querySelector('.settings-panel .settings-content') !== null, ); return has as boolean; diff --git a/tests/e2e/specs/phase-b-grid.test.ts b/tests/e2e/specs/phase-b-grid.test.ts index 8bdad41..ab937dc 100644 --- a/tests/e2e/specs/phase-b-grid.test.ts +++ b/tests/e2e/specs/phase-b-grid.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase B — Grid: Multi-project grid, tab switching, status bar. // Scenarios B1-B3 + new grid/UI tests. @@ -6,14 +7,14 @@ import { browser, expect } from '@wdio/globals'; // ─── Helpers ────────────────────────────────────────────────────────── async function getProjectIds(): Promise { - return browser.execute(() => { + return exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); return Array.from(boxes).map((b) => b.getAttribute('data-project-id') ?? '').filter(Boolean); }); } async function focusProject(id: string): Promise { - await browser.execute((pid) => { + await exec((pid) => { const h = document.querySelector(`[data-project-id="${pid}"] .project-header`); if (h) (h as HTMLElement).click(); }, id); @@ -21,7 +22,7 @@ async function focusProject(id: string): Promise { } async function switchProjectTab(id: string, tabIndex: number): Promise { - await browser.execute((pid, idx) => { + await exec((pid, idx) => { const tabs = document.querySelector(`[data-project-id="${pid}"]`)?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs?.[idx]) (tabs[idx] as HTMLElement).click(); }, id, tabIndex); @@ -29,7 +30,7 @@ async function switchProjectTab(id: string, tabIndex: number): Promise { } async function getAgentStatus(id: string): Promise { - return browser.execute((pid) => { + return exec((pid) => { const p = document.querySelector(`[data-project-id="${pid}"] [data-testid="agent-pane"]`); return p?.getAttribute('data-agent-status') ?? 'not-found'; }, id); @@ -50,7 +51,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { it('should render multiple project boxes', async () => { await browser.waitUntil( async () => { - const count = await browser.execute(() => + const count = await exec(() => document.querySelectorAll('[data-testid="project-box"]').length, ); return (count as number) >= 1; @@ -65,7 +66,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { }); it('should show project headers with CWD paths', async () => { - const headers = await browser.execute(() => { + const headers = await exec(() => { const els = document.querySelectorAll('.project-header .info-cwd'); return Array.from(els).map((e) => e.textContent?.trim() ?? ''); }); @@ -87,7 +88,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { if (ids.length < 1) return; await focusProject(ids[0]); - const isActive = await browser.execute((id) => { + const isActive = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); return box?.classList.contains('active') ?? false; }, ids[0]); @@ -95,7 +96,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { }); it('should show project-specific accent colors on each box border', async () => { - const accents = await browser.execute(() => { + const accents = await exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); return Array.from(boxes).map((b) => getComputedStyle(b as HTMLElement).getPropertyValue('--accent').trim()); }); @@ -103,7 +104,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { }); it('should render project icons (emoji) in headers', async () => { - const icons = await browser.execute(() => { + const icons = await exec(() => { const els = document.querySelectorAll('.project-header .project-icon, .project-header .emoji'); return Array.from(els).map((e) => e.textContent?.trim() ?? ''); }); @@ -115,7 +116,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { it('should show project CWD tooltip on hover', async () => { const ids = await getProjectIds(); if (ids.length < 1) return; - const titleAttr = await browser.execute((id) => { + const titleAttr = await exec((id) => { const el = document.querySelector(`[data-project-id="${id}"] .project-header .info-cwd`); return el?.getAttribute('title') ?? el?.textContent?.trim() ?? ''; }, ids[0]); @@ -126,7 +127,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { const ids = await getProjectIds(); if (ids.length < 2) return; await focusProject(ids[0]); - const isActive = await browser.execute((id) => { + const isActive = await exec((id) => { return document.querySelector(`[data-project-id="${id}"]`)?.classList.contains('active') ?? false; }, ids[0]); expect(isActive).toBe(true); @@ -135,7 +136,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { it('should show all base tabs per project', async () => { const ids = await getProjectIds(); if (ids.length < 1) return; - const tabLabels = await browser.execute((id) => { + const tabLabels = await exec((id) => { const tabs = document.querySelector(`[data-project-id="${id}"]`)?.querySelectorAll('[data-testid="project-tabs"] .ptab'); return Array.from(tabs ?? []).map((t) => t.textContent?.trim() ?? ''); }, ids[0]); @@ -148,7 +149,7 @@ describe('Scenario B1 — Multi-Project Grid', () => { const ids = await getProjectIds(); if (ids.length < 1) return; await switchProjectTab(ids[0], 0); - const hasTerminal = await browser.execute((id) => { + const hasTerminal = await exec((id) => { return document.querySelector(`[data-project-id="${id}"] [data-testid="terminal-tabs"], [data-project-id="${id}"] .terminal-section`) !== null; }, ids[0]); expect(hasTerminal).toBe(true); @@ -167,7 +168,7 @@ describe('Scenario B2 — Independent Tab Switching', () => { if (ids.length < 2) { console.log('Skipping B2 — need 2+ projects'); this.skip(); return; } await switchProjectTab(ids[0], 3); // Files tab await switchProjectTab(ids[1], 0); // Model tab - const getActiveTab = (id: string) => browser.execute((pid) => { + const getActiveTab = (id: string) => exec((pid) => { return document.querySelector(`[data-project-id="${pid}"] [data-testid="project-tabs"] .ptab.active`)?.textContent?.trim() ?? ''; }, id); const firstActive = await getActiveTab(ids[0]); @@ -182,7 +183,7 @@ describe('Scenario B2 — Independent Tab Switching', () => { await focusProject(ids[0]); await focusProject(ids[1]); await focusProject(ids[0]); - const activeTab = await browser.execute((id) => { + const activeTab = await exec((id) => { return document.querySelector(`[data-project-id="${id}"] [data-testid="project-tabs"] .ptab.active`)?.textContent?.trim() ?? ''; }, ids[0]); expect(activeTab).toBe('Model'); @@ -193,7 +194,7 @@ describe('Scenario B2 — Independent Tab Switching', () => { describe('Scenario B3 — Status Bar Fleet State', () => { it('should show agent count in status bar', async () => { - const barText = await browser.execute(() => { + const barText = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); return bar?.textContent ?? ''; }); @@ -201,7 +202,7 @@ describe('Scenario B3 — Status Bar Fleet State', () => { }); it('should show no burn rate when all agents idle', async () => { - const hasBurnRate = await browser.execute(() => { + const hasBurnRate = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); const burnEl = bar?.querySelector('.burn-rate'); const costEl = bar?.querySelector('.cost'); @@ -219,7 +220,7 @@ describe('Scenario B3 — Status Bar Fleet State', () => { const ids = await getProjectIds(); if (ids.length < 2) return; await focusProject(ids[1]); - const barAfter = await browser.execute(() => { + const barAfter = await exec(() => { return document.querySelector('[data-testid="status-bar"]')?.textContent ?? ''; }); expect(barAfter.length).toBeGreaterThan(0); diff --git a/tests/e2e/specs/phase-b-llm.test.ts b/tests/e2e/specs/phase-b-llm.test.ts index 0c67c54..7a57352 100644 --- a/tests/e2e/specs/phase-b-llm.test.ts +++ b/tests/e2e/specs/phase-b-llm.test.ts @@ -1,5 +1,6 @@ import { browser, expect } from '@wdio/globals'; import { isJudgeAvailable, assertWithJudge } from '../infra/llm-judge'; +import { exec } from '../helpers/execute.ts'; // Phase B — LLM: LLM-judged agent responses, code generation, context tab. // Scenarios B4-B6 + new agent/context tests. Requires ANTHROPIC_API_KEY for LLM tests. @@ -7,29 +8,29 @@ import { isJudgeAvailable, assertWithJudge } from '../infra/llm-judge'; // ─── Helpers ────────────────────────────────────────────────────────── async function getProjectIds(): Promise { - return browser.execute(() => { + return exec(() => { return Array.from(document.querySelectorAll('[data-testid="project-box"]')) .map((b) => b.getAttribute('data-project-id') ?? '').filter(Boolean); }); } async function focusProject(id: string): Promise { - await browser.execute((pid) => { + await exec((pid) => { (document.querySelector(`[data-project-id="${pid}"] .project-header`) as HTMLElement)?.click(); }, id); await browser.pause(300); } async function getAgentStatus(id: string): Promise { - return browser.execute((pid) => + return exec((pid) => document.querySelector(`[data-project-id="${pid}"] [data-testid="agent-pane"]`)?.getAttribute('data-agent-status') ?? 'not-found', id); } async function sendPromptInProject(id: string, text: string): Promise { await focusProject(id); - await browser.execute((pid, prompt) => { + await exec((pid, prompt) => { const ta = document.querySelector(`[data-project-id="${pid}"] [data-testid="agent-prompt"]`) as HTMLTextAreaElement | null; if (ta) { ta.value = prompt; ta.dispatchEvent(new Event('input', { bubbles: true })); } }, id, text); await browser.pause(200); - await browser.execute((pid) => { + await exec((pid) => { (document.querySelector(`[data-project-id="${pid}"] [data-testid="agent-submit"]`) as HTMLElement)?.click(); }, id); } @@ -38,11 +39,11 @@ async function waitForAgentStatus(id: string, status: string, timeout = 60_000): { timeout, timeoutMsg: `Agent ${id} did not reach "${status}" in ${timeout}ms` }); } async function getAgentMessages(id: string): Promise { - return browser.execute((pid) => + return exec((pid) => document.querySelector(`[data-project-id="${pid}"] [data-testid="agent-messages"]`)?.textContent ?? '', id); } async function switchTab(id: string, idx: number): Promise { - await browser.execute((pid, i) => { + await exec((pid, i) => { const tabs = document.querySelector(`[data-project-id="${pid}"]`)?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs?.[i]) (tabs[i] as HTMLElement).click(); }, id, idx); @@ -105,7 +106,7 @@ describe('Scenario B4 — LLM-Judged Agent Response', () => { const pid = ids[0]; const status = await getAgentStatus(pid); if (status === 'idle') { - const hasCost = await browser.execute((id) => { + const hasCost = await exec((id) => { return document.querySelector(`[data-project-id="${id}"] .cost-bar, [data-project-id="${id}"] .usage-meter, [data-project-id="${id}"] [data-testid="agent-cost"]`) !== null; }, pid); expect(typeof hasCost).toBe('boolean'); @@ -116,7 +117,7 @@ describe('Scenario B4 — LLM-Judged Agent Response', () => { const ids = await getProjectIds(); if (ids.length < 1) return; const pid = ids[0]; - const modelInfo = await browser.execute((id) => { + const modelInfo = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const modelEl = box?.querySelector('.model-name, .session-model, [data-testid="agent-model"]'); const strip = box?.querySelector('.status-strip'); @@ -171,7 +172,7 @@ describe('Scenario B6 — Context Tab After Agent Activity', () => { if (ids.length < 1) return; const pid = ids[0]; await switchTab(pid, 2); - const content = await browser.execute((id) => { + const content = await exec((id) => { return document.querySelector(`[data-project-id="${id}"] .context-stats, [data-project-id="${id}"] .token-meter, [data-project-id="${id}"] .stat-value`)?.textContent ?? ''; }, pid); if (content) { expect(content.length).toBeGreaterThan(0); } @@ -183,7 +184,7 @@ describe('Scenario B6 — Context Tab After Agent Activity', () => { if (ids.length < 1) return; const pid = ids[0]; await switchTab(pid, 2); - const tokenData = await browser.execute((id) => { + const tokenData = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const meter = box?.querySelector('.token-meter, .context-meter, [data-testid="token-meter"]'); const stats = box?.querySelectorAll('.stat-value'); @@ -200,7 +201,7 @@ describe('Scenario B6 — Context Tab After Agent Activity', () => { if (ids.length < 1) return; const pid = ids[0]; await switchTab(pid, 2); - const refCount = await browser.execute((id) => { + const refCount = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const refs = box?.querySelectorAll('.file-ref, .file-reference, [data-testid="file-refs"] li'); return refs?.length ?? 0; diff --git a/tests/e2e/specs/phase-c-llm.test.ts b/tests/e2e/specs/phase-c-llm.test.ts index b922d33..05c0816 100644 --- a/tests/e2e/specs/phase-c-llm.test.ts +++ b/tests/e2e/specs/phase-c-llm.test.ts @@ -1,5 +1,6 @@ import { browser, expect } from '@wdio/globals'; import { isJudgeAvailable, assertWithJudge } from '../infra/llm-judge'; +import { exec } from '../helpers/execute.ts'; // Phase C — LLM-Judged Tests (C10-C11) // Settings completeness and status bar completeness via LLM judge. @@ -15,13 +16,13 @@ describe('Scenario C10 — LLM-Judged Settings Completeness', () => { } // Open settings - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); - const settingsContent = await browser.execute(() => { + const settingsContent = await exec(() => { const panel = document.querySelector('.sidebar-panel .settings-panel'); return panel?.textContent ?? ''; }); @@ -37,7 +38,7 @@ describe('Scenario C10 — LLM-Judged Settings Completeness', () => { console.log(`LLM Judge: ${verdict.reasoning} (confidence: ${verdict.confidence})`); } - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -55,12 +56,12 @@ describe('Scenario C11 — LLM-Judged Status Bar Completeness', () => { return; } - const statusBarContent = await browser.execute(() => { + const statusBarContent = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); return bar?.textContent ?? ''; }); - const statusBarHtml = await browser.execute(() => { + const statusBarHtml = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); return bar?.innerHTML ?? ''; }); diff --git a/tests/e2e/specs/phase-c-tabs.test.ts b/tests/e2e/specs/phase-c-tabs.test.ts index e3a3412..a506905 100644 --- a/tests/e2e/specs/phase-c-tabs.test.ts +++ b/tests/e2e/specs/phase-c-tabs.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase C — Tab-Based Feature Tests (C5-C9) // Settings panel, project health, metrics tab, context tab, files tab. @@ -7,7 +8,7 @@ import { browser, expect } from '@wdio/globals'; /** Get all project box IDs currently rendered. */ async function getProjectIds(): Promise { - return browser.execute(() => { + return exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); return Array.from(boxes).map( (b) => b.getAttribute('data-project-id') ?? '', @@ -17,7 +18,7 @@ async function getProjectIds(): Promise { /** Switch to a tab in a specific project box. */ async function switchProjectTab(projectId: string, tabIndex: number): Promise { - await browser.execute((id, idx) => { + await exec((id, idx) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (tabs && tabs[idx]) (tabs[idx] as HTMLElement).click(); @@ -32,7 +33,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { // Close sidebar panel if open const panel = await browser.$('.sidebar-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -40,7 +41,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { } // Open settings - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); @@ -48,7 +49,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { }); it('should show Appearance section with theme dropdown', async () => { - const hasTheme = await browser.execute(() => { + const hasTheme = await exec(() => { const panel = document.querySelector('.sidebar-panel .settings-panel'); if (!panel) return false; const text = panel.textContent ?? ''; @@ -58,7 +59,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { }); it('should show font settings (UI font and Terminal font)', async () => { - const hasFonts = await browser.execute(() => { + const hasFonts = await exec(() => { const panel = document.querySelector('.sidebar-panel .settings-panel'); if (!panel) return false; const text = panel.textContent ?? ''; @@ -69,7 +70,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { it('should show default shell setting in Agents category', async () => { // Switch to Agents category which contains shell settings - const hasShell = await browser.execute(() => { + const hasShell = await exec(() => { // Check across all settings categories const panel = document.querySelector('.sidebar-panel .settings-panel'); if (!panel) return false; @@ -81,7 +82,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { it('should have theme dropdown with many themes', async () => { // Click the theme dropdown - const opened = await browser.execute(() => { + const opened = await exec(() => { const btn = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (btn) { (btn as HTMLElement).click(); return true; } return false; @@ -89,13 +90,13 @@ describe('Scenario C5 — Settings Panel Sections', () => { if (opened) { await browser.pause(300); - const optionCount = await browser.execute(() => { + const optionCount = await exec(() => { return document.querySelectorAll('.dropdown-menu .dropdown-item').length; }); expect(optionCount).toBeGreaterThanOrEqual(15); // Close dropdown - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (btn) (btn as HTMLElement).click(); }); @@ -105,7 +106,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { after(async () => { // Close settings - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -117,7 +118,7 @@ describe('Scenario C5 — Settings Panel Sections', () => { describe('Scenario C6 — Project Health Indicators', () => { it('should show status dots on project headers', async () => { - const hasDots = await browser.execute(() => { + const hasDots = await exec(() => { const dots = document.querySelectorAll('.project-header .status-dot'); return dots.length; }); @@ -129,7 +130,7 @@ describe('Scenario C6 — Project Health Indicators', () => { const ids = await getProjectIds(); if (ids.length < 1) return; - const dotColor = await browser.execute((id) => { + const dotColor = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const dot = box?.querySelector('.status-dot'); if (!dot) return 'not-found'; @@ -142,7 +143,7 @@ describe('Scenario C6 — Project Health Indicators', () => { }); it('should show status bar agent counts', async () => { - const counts = await browser.execute(() => { + const counts = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); if (!bar) return ''; return bar.textContent ?? ''; @@ -159,7 +160,7 @@ describe('Scenario C7 — Metrics Tab', () => { const ids = await getProjectIds(); if (ids.length < 1) return; - const hasMetrics = await browser.execute((id) => { + const hasMetrics = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (!tabs) return false; @@ -175,7 +176,7 @@ describe('Scenario C7 — Metrics Tab', () => { const projectId = ids[0]; // Find and click Metrics tab - await browser.execute((id) => { + await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (!tabs) return; @@ -188,7 +189,7 @@ describe('Scenario C7 — Metrics Tab', () => { }, projectId); await browser.pause(500); - const hasContent = await browser.execute((id) => { + const hasContent = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const panel = box?.querySelector('.metrics-panel, .metrics-tab'); return panel !== null; @@ -212,7 +213,7 @@ describe('Scenario C8 — Context Tab Visualization', () => { // Switch to Context tab (index 2) await switchProjectTab(projectId, 2); - const hasContextUI = await browser.execute((id) => { + const hasContextUI = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const ctx = box?.querySelector('.context-tab, .context-stats, .token-meter, .stat-value'); return ctx !== null; @@ -237,7 +238,7 @@ describe('Scenario C9 — Files Tab & Code Editor', () => { await switchProjectTab(projectId, 3); await browser.pause(500); - const hasTree = await browser.execute((id) => { + const hasTree = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tree = box?.querySelector('.file-tree, .directory-tree, .files-tab'); return tree !== null; @@ -250,7 +251,7 @@ describe('Scenario C9 — Files Tab & Code Editor', () => { const ids = await getProjectIds(); if (ids.length < 1) return; - const fileNames = await browser.execute((id) => { + const fileNames = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const items = box?.querySelectorAll('.tree-name'); return Array.from(items ?? []).map(el => el.textContent?.trim() ?? ''); diff --git a/tests/e2e/specs/phase-c-ui.test.ts b/tests/e2e/specs/phase-c-ui.test.ts index ac95b64..e402838 100644 --- a/tests/e2e/specs/phase-c-ui.test.ts +++ b/tests/e2e/specs/phase-c-ui.test.ts @@ -1,4 +1,5 @@ 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. @@ -7,7 +8,7 @@ import { browser, expect } from '@wdio/globals'; /** Open command palette via Ctrl+K. */ async function openPalette(): Promise { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', 'k']); const palette = await browser.$('[data-testid="command-palette"]'); @@ -25,7 +26,7 @@ async function paletteSearch(query: string): Promise { const input = await browser.$('[data-testid="palette-input"]'); await input.setValue(query); await browser.pause(300); - return browser.execute(() => { + return exec(() => { const items = document.querySelectorAll('.palette-item .cmd-label'); return Array.from(items).map(el => el.textContent?.trim() ?? ''); }); @@ -37,7 +38,7 @@ describe('Scenario C1 — Command Palette Hardening Commands', () => { afterEach(async () => { // Ensure palette is closed after each test try { - const isVisible = await browser.execute(() => { + const isVisible = await exec(() => { const el = document.querySelector('[data-testid="command-palette"]'); return el !== null && window.getComputedStyle(el).display !== 'none'; }); @@ -79,14 +80,14 @@ describe('Scenario C1 — Command Palette Hardening Commands', () => { await input.clearValue(); await browser.pause(200); - const itemCount = await browser.execute(() => + 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 browser.execute(() => { + const groups = await exec(() => { const headers = document.querySelectorAll('.palette-category'); return Array.from(headers).map(h => h.textContent?.trim() ?? ''); }); @@ -99,12 +100,12 @@ describe('Scenario C1 — Command Palette Hardening Commands', () => { describe('Scenario C2 — Search Overlay (FTS5)', () => { it('should open search overlay with Ctrl+Shift+F', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', 'Shift', 'f']); await browser.pause(500); - const overlay = await browser.execute(() => { + const overlay = await exec(() => { // SearchOverlay uses .search-overlay class const el = document.querySelector('.search-overlay, [data-testid="search-overlay"]'); return el !== null; @@ -113,7 +114,7 @@ describe('Scenario C2 — Search Overlay (FTS5)', () => { }); it('should have search input focused', async () => { - const isFocused = await browser.execute(() => { + const isFocused = await exec(() => { const input = document.querySelector('.search-overlay input, [data-testid="search-input"]') as HTMLInputElement | null; if (!input) return false; input.focus(); @@ -123,7 +124,7 @@ describe('Scenario C2 — Search Overlay (FTS5)', () => { }); it('should show no results for nonsense query', async () => { - await browser.execute(() => { + await exec(() => { const input = document.querySelector('.search-overlay input, [data-testid="search-input"]') as HTMLInputElement | null; if (input) { input.value = 'zzz_nonexistent_xyz_999'; @@ -132,7 +133,7 @@ describe('Scenario C2 — Search Overlay (FTS5)', () => { }); await browser.pause(500); // 300ms debounce + render time - const resultCount = await browser.execute(() => { + const resultCount = await exec(() => { const results = document.querySelectorAll('.search-result, .search-result-item'); return results.length; }); @@ -143,7 +144,7 @@ describe('Scenario C2 — Search Overlay (FTS5)', () => { await browser.keys('Escape'); await browser.pause(300); - const overlay = await browser.execute(() => { + const overlay = await exec(() => { const el = document.querySelector('.search-overlay, [data-testid="search-overlay"]'); if (!el) return false; const style = window.getComputedStyle(el); @@ -157,7 +158,7 @@ describe('Scenario C2 — Search Overlay (FTS5)', () => { describe('Scenario C3 — Notification Center', () => { it('should render notification bell in status bar', async () => { - const hasBell = await browser.execute(() => { + 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"]'); @@ -167,13 +168,13 @@ describe('Scenario C3 — Notification Center', () => { }); it('should open notification panel on bell click', async () => { - await browser.execute(() => { + 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 browser.execute(() => { + 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); @@ -183,7 +184,7 @@ describe('Scenario C3 — Notification Center', () => { }); it('should show empty state or notification history', async () => { - const content = await browser.execute(() => { + const content = await exec(() => { const panel = document.querySelector('.notification-panel, .notification-dropdown, [data-testid="notification-panel"]'); return panel?.textContent ?? ''; }); @@ -193,13 +194,13 @@ describe('Scenario C3 — Notification Center', () => { it('should close notification panel on outside click', async () => { // Click the backdrop overlay to close the panel - await browser.execute(() => { + await exec(() => { const backdrop = document.querySelector('.notification-center .backdrop'); if (backdrop) (backdrop as HTMLElement).click(); }); await browser.pause(300); - const panelOpen = await browser.execute(() => { + 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); @@ -213,12 +214,12 @@ describe('Scenario C3 — Notification Center', () => { describe('Scenario C4 — Keyboard-First Navigation', () => { it('should toggle settings with Ctrl+Comma', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', ',']); await browser.pause(500); - const settingsVisible = await browser.execute(() => { + const settingsVisible = await exec(() => { const panel = document.querySelector('.sidebar-panel'); if (!panel) return false; const style = window.getComputedStyle(panel); @@ -232,14 +233,14 @@ describe('Scenario C4 — Keyboard-First Navigation', () => { }); it('should toggle sidebar with Ctrl+B', async () => { - await browser.execute(() => document.body.focus()); + 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 browser.execute(() => { + const initialState = await exec(() => { const panel = document.querySelector('.sidebar-panel'); return panel !== null && window.getComputedStyle(panel).display !== 'none'; }); @@ -248,7 +249,7 @@ describe('Scenario C4 — Keyboard-First Navigation', () => { await browser.keys(['Control', 'b']); await browser.pause(300); - const afterToggle = await browser.execute(() => { + const afterToggle = await exec(() => { const panel = document.querySelector('.sidebar-panel'); if (!panel) return false; return window.getComputedStyle(panel).display !== 'none'; @@ -265,12 +266,12 @@ describe('Scenario C4 — Keyboard-First Navigation', () => { }); it('should focus project with Alt+1', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Alt', '1']); await browser.pause(300); - const hasActive = await browser.execute(() => { + const hasActive = await exec(() => { const active = document.querySelector('.project-box.active'); return active !== null; }); diff --git a/tests/e2e/specs/phase-d-errors.test.ts b/tests/e2e/specs/phase-d-errors.test.ts index f53b095..eb21a86 100644 --- a/tests/e2e/specs/phase-d-errors.test.ts +++ b/tests/e2e/specs/phase-d-errors.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase D — Error Handling UI Tests (D4–D5) // Tests toast notifications, notification center, and error state handling. @@ -10,14 +11,14 @@ async function resetToHomeState(): Promise { // Close settings panel if open const panel = await browser.$('.sidebar-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.settings-close') || document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(300); } // Close notification panel if open - await browser.execute(() => { + await exec(() => { const panel = document.querySelector('[data-testid="notification-panel"]'); if (panel) { const backdrop = document.querySelector('.notification-center .backdrop'); @@ -50,7 +51,7 @@ describe('Scenario D4 — Toast Notifications', () => { it('should show notification dropdown when bell clicked', async () => { // Click bell via JS for reliability - await browser.execute(() => { + await exec(() => { const bell = document.querySelector('[data-testid="notification-bell"]'); if (bell) (bell as HTMLElement).click(); }); @@ -60,7 +61,7 @@ describe('Scenario D4 — Toast Notifications', () => { await expect(panel).toBeDisplayed(); // Verify panel has a title - const title = await browser.execute(() => { + const title = await exec(() => { const el = document.querySelector('[data-testid="notification-panel"] .panel-title'); return el?.textContent?.trim() ?? ''; }); @@ -69,11 +70,11 @@ describe('Scenario D4 — Toast Notifications', () => { it('should show panel actions area in notification center', async () => { // Panel should still be open from previous test - const panelExists = await browser.execute(() => { + const panelExists = await exec(() => { return document.querySelector('[data-testid="notification-panel"]') !== null; }); if (!panelExists) { - await browser.execute(() => { + await exec(() => { const bell = document.querySelector('[data-testid="notification-bell"]'); if (bell) (bell as HTMLElement).click(); }); @@ -89,7 +90,7 @@ describe('Scenario D4 — Toast Notifications', () => { await expect(list).toBeExisting(); // Close panel - await browser.execute(() => { + await exec(() => { const backdrop = document.querySelector('.notification-center .backdrop'); if (backdrop) (backdrop as HTMLElement).click(); }); @@ -98,7 +99,7 @@ describe('Scenario D4 — Toast Notifications', () => { it('should close notification panel on Escape', async () => { // Open panel - await browser.execute(() => { + await exec(() => { const bell = document.querySelector('[data-testid="notification-bell"]'); if (bell) (bell as HTMLElement).click(); }); @@ -112,7 +113,7 @@ describe('Scenario D4 — Toast Notifications', () => { await browser.pause(400); // Panel should be gone - const panelAfter = await browser.execute(() => { + const panelAfter = await exec(() => { return document.querySelector('[data-testid="notification-panel"]') !== null; }); expect(panelAfter).toBe(false); @@ -152,7 +153,7 @@ describe('Scenario D5 — Error States', () => { await expect(center).toBeDisplayed(); // Bell should be clickable without errors - await browser.execute(() => { + await exec(() => { const bell = document.querySelector('[data-testid="notification-bell"]'); if (bell) (bell as HTMLElement).click(); }); @@ -163,7 +164,7 @@ describe('Scenario D5 — Error States', () => { await expect(panel).toBeDisplayed(); // Close - await browser.execute(() => { + await exec(() => { const backdrop = document.querySelector('.notification-center .backdrop'); if (backdrop) (backdrop as HTMLElement).click(); }); @@ -175,7 +176,7 @@ describe('Scenario D5 — Error States', () => { expect(boxes.length).toBeGreaterThanOrEqual(1); // Verify no project box has an error overlay or error class - const errorBoxes = await browser.execute(() => { + const errorBoxes = await exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); let errorCount = 0; for (const box of boxes) { diff --git a/tests/e2e/specs/phase-d-settings.test.ts b/tests/e2e/specs/phase-d-settings.test.ts index 05654a8..fd0303e 100644 --- a/tests/e2e/specs/phase-d-settings.test.ts +++ b/tests/e2e/specs/phase-d-settings.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase D — Settings Panel Tests (D1–D3) // Tests the redesigned VS Code-style settings panel with 6+1 category tabs, @@ -9,14 +10,14 @@ import { browser, expect } from '@wdio/globals'; async function openSettings(): Promise { const panel = await browser.$('.settings-panel'); if (!(await panel.isDisplayed().catch(() => false))) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); await (await browser.$('.sidebar-panel')).waitForDisplayed({ timeout: 5000 }); } await browser.waitUntil( - async () => (await browser.execute(() => document.querySelectorAll('.settings-panel').length) as number) >= 1, + async () => (await exec(() => document.querySelectorAll('.settings-panel').length) as number) >= 1, { timeout: 5000, timeoutMsg: 'Settings panel did not render within 5s' }, ); await browser.pause(300); @@ -25,7 +26,7 @@ async function openSettings(): Promise { async function closeSettings(): Promise { const panel = await browser.$('.sidebar-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.settings-close') || document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -34,7 +35,7 @@ async function closeSettings(): Promise { } async function clickCategory(label: string): Promise { - await browser.execute((lbl) => { + await exec((lbl) => { const items = document.querySelectorAll('.sidebar-item'); for (const el of items) { if (el.textContent?.includes(lbl)) { (el as HTMLElement).click(); return; } @@ -44,7 +45,7 @@ async function clickCategory(label: string): Promise { } async function scrollToTop(): Promise { - await browser.execute(() => { document.querySelector('.settings-content')?.scrollTo(0, 0); }); + await exec(() => { document.querySelector('.settings-content')?.scrollTo(0, 0); }); await browser.pause(200); } @@ -80,20 +81,20 @@ describe('Scenario D1 — Settings Panel Categories', () => { it('should show search bar and filter results', async () => { await expect(await browser.$('.settings-search')).toBeDisplayed(); - await browser.execute(() => { + await exec(() => { const input = document.querySelector('.settings-search') as HTMLInputElement; if (input) { input.value = 'font'; input.dispatchEvent(new Event('input', { bubbles: true })); } }); await browser.pause(500); const results = await browser.$$('.search-result'); expect(results.length).toBeGreaterThan(0); - const hasFont = await browser.execute(() => { + const hasFont = await exec(() => { const labels = document.querySelectorAll('.search-result .sr-label'); return Array.from(labels).some(l => l.textContent?.toLowerCase().includes('font')); }); expect(hasFont).toBe(true); // Clear search - await browser.execute(() => { + await exec(() => { const input = document.querySelector('.settings-search') as HTMLInputElement; if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); } }); @@ -108,7 +109,7 @@ describe('Scenario D2 — Appearance Settings', () => { after(async () => { await closeSettings(); }); it('should show theme dropdown with 17+ built-in themes grouped by category', async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (btn) (btn as HTMLElement).click(); }); @@ -118,7 +119,7 @@ describe('Scenario D2 — Appearance Settings', () => { const items = await browser.$$('.theme-menu .dropdown-item'); expect(items.length).toBeGreaterThanOrEqual(17); // Close dropdown - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (btn) (btn as HTMLElement).click(); }); @@ -128,17 +129,17 @@ describe('Scenario D2 — Appearance Settings', () => { it('should show font size steppers with -/+ buttons', async () => { const steppers = await browser.$$('.stepper'); expect(steppers.length).toBeGreaterThanOrEqual(1); - const before = await browser.execute(() => document.querySelector('.stepper span')?.textContent ?? ''); + const before = await exec(() => document.querySelector('.stepper span')?.textContent ?? ''); const sizeBefore = parseInt(before as string, 10); - await browser.execute(() => { + await exec(() => { const btns = document.querySelectorAll('.stepper button'); if (btns.length >= 2) (btns[1] as HTMLElement).click(); // + button }); await browser.pause(300); - const after = await browser.execute(() => document.querySelector('.stepper span')?.textContent ?? ''); + const after = await exec(() => document.querySelector('.stepper span')?.textContent ?? ''); expect(parseInt(after as string, 10)).toBe(sizeBefore + 1); // Revert - await browser.execute(() => { + await exec(() => { const btns = document.querySelectorAll('.stepper button'); if (btns.length >= 1) (btns[0] as HTMLElement).click(); }); @@ -146,7 +147,7 @@ describe('Scenario D2 — Appearance Settings', () => { }); it('should show terminal cursor style selector (Block/Line/Underline)', async () => { - await browser.execute(() => { + await exec(() => { document.getElementById('setting-cursor-style')?.scrollIntoView({ behavior: 'instant', block: 'center' }); }); await browser.pause(300); @@ -154,14 +155,14 @@ describe('Scenario D2 — Appearance Settings', () => { await expect(segmented).toBeDisplayed(); const buttons = await browser.$$('.segmented button'); expect(buttons.length).toBe(3); - const activeText = await browser.execute(() => + const activeText = await exec(() => document.querySelector('.segmented button.active')?.textContent?.trim() ?? '', ); expect(activeText).toBe('Block'); }); it('should show scrollback lines input', async () => { - await browser.execute(() => { + await exec(() => { document.getElementById('setting-scrollback')?.scrollIntoView({ behavior: 'instant', block: 'center' }); }); await browser.pause(300); @@ -178,7 +179,7 @@ describe('Scenario D2 — Appearance Settings', () => { describe('Scenario D3 — Theme Editor', () => { before(async () => { await openSettings(); await clickCategory('Appearance'); await scrollToTop(); }); after(async () => { - await browser.execute(() => { + await exec(() => { const btn = Array.from(document.querySelectorAll('.editor .btn')) .find(b => b.textContent?.trim() === 'Cancel'); if (btn) (btn as HTMLElement).click(); @@ -194,7 +195,7 @@ describe('Scenario D3 — Theme Editor', () => { }); it('should open theme editor with color pickers when clicked', async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.new-theme-btn'); if (btn) (btn as HTMLElement).click(); }); @@ -215,11 +216,11 @@ describe('Scenario D3 — Theme Editor', () => { }); it('should have Cancel and Save buttons', async () => { - const hasCancel = await browser.execute(() => + const hasCancel = await exec(() => Array.from(document.querySelectorAll('.editor .footer .btn')).some(b => b.textContent?.trim() === 'Cancel'), ); expect(hasCancel).toBe(true); - const hasSave = await browser.execute(() => + const hasSave = await exec(() => Array.from(document.querySelectorAll('.editor .footer .btn')).some(b => b.textContent?.trim() === 'Save'), ); expect(hasSave).toBe(true); diff --git a/tests/e2e/specs/phase-e-agents.test.ts b/tests/e2e/specs/phase-e-agents.test.ts index c3d4eb5..9f0c1da 100644 --- a/tests/e2e/specs/phase-e-agents.test.ts +++ b/tests/e2e/specs/phase-e-agents.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase E — Part 1: Multi-agent orchestration, project tabs, and provider UI. // Tests ProjectBox tab bar, AgentPane state, provider config, status bar fleet state. @@ -6,7 +7,7 @@ import { browser, expect } from '@wdio/globals'; // ─── Helpers ────────────────────────────────────────────────────────── async function clickTabByText(tabText: string): Promise { - await browser.execute((text) => { + await exec((text) => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); for (const tab of tabs) { if (tab.textContent?.trim() === text) { @@ -19,7 +20,7 @@ async function clickTabByText(tabText: string): Promise { } async function getActiveTabText(): Promise { - return browser.execute(() => { + return exec(() => { const box = document.querySelector('[data-testid="project-box"]'); const active = box?.querySelector('[data-testid="project-tabs"] .ptab.active'); return active?.textContent?.trim() ?? ''; @@ -32,7 +33,7 @@ describe('Scenario E1 — ProjectBox Tab Bar', () => { before(async () => { await browser.waitUntil( async () => { - const count = await browser.execute(() => + const count = await exec(() => document.querySelectorAll('[data-testid="project-box"]').length, ); return (count as number) >= 1; @@ -43,7 +44,7 @@ describe('Scenario E1 — ProjectBox Tab Bar', () => { }); it('should render project-level tab bar with at least 7 tabs', async () => { - const tabCount = await browser.execute(() => { + const tabCount = await exec(() => { const box = document.querySelector('[data-testid="project-box"]'); return box?.querySelectorAll('[data-testid="project-tabs"] .ptab')?.length ?? 0; }); @@ -51,7 +52,7 @@ describe('Scenario E1 — ProjectBox Tab Bar', () => { }); it('should include expected base tab labels', async () => { - const tabTexts = await browser.execute(() => { + const tabTexts = await exec(() => { const box = document.querySelector('[data-testid="project-box"]'); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); return Array.from(tabs ?? []).map((t) => t.textContent?.trim() ?? ''); @@ -86,7 +87,7 @@ describe('Scenario E1 — ProjectBox Tab Bar', () => { await clickTabByText('Files'); expect(await getActiveTabText()).toBe('Files'); // Content panes should still be in DOM (display toggled, not unmounted) - const paneCount = await browser.execute(() => { + const paneCount = await exec(() => { const box = document.querySelector('[data-testid="project-box"]'); return box?.querySelectorAll('.content-pane')?.length ?? 0; }); @@ -121,7 +122,7 @@ describe('Scenario E2 — Agent Session UI', () => { }); it('should show agent pane in idle status initially', async () => { - const status = await browser.execute(() => { + const status = await exec(() => { const el = document.querySelector('[data-testid="agent-pane"]'); return el?.getAttribute('data-agent-status') ?? 'unknown'; }); @@ -129,7 +130,7 @@ describe('Scenario E2 — Agent Session UI', () => { }); it('should show CWD in ProjectHeader', async () => { - const cwd = await browser.execute(() => { + const cwd = await exec(() => { const header = document.querySelector('.project-header'); return header?.querySelector('.info-cwd')?.textContent?.trim() ?? ''; }); @@ -137,7 +138,7 @@ describe('Scenario E2 — Agent Session UI', () => { }); it('should show profile name in ProjectHeader if configured', async () => { - const profileInfo = await browser.execute(() => { + const profileInfo = await exec(() => { const el = document.querySelector('.project-header .info-profile'); return { exists: el !== null, text: el?.textContent?.trim() ?? '' }; }); @@ -151,7 +152,7 @@ describe('Scenario E2 — Agent Session UI', () => { describe('Scenario E3 — Provider Configuration', () => { before(async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); @@ -161,7 +162,7 @@ describe('Scenario E3 — Provider Configuration', () => { }); it('should show Providers section in Settings', async () => { - const hasProviders = await browser.execute(() => { + const hasProviders = await exec(() => { const headers = document.querySelectorAll('.settings-section h2'); return Array.from(headers).some((h) => h.textContent?.trim() === 'Providers'); }); @@ -169,14 +170,14 @@ describe('Scenario E3 — Provider Configuration', () => { }); it('should show at least one provider panel', async () => { - const count = await browser.execute(() => + const count = await exec(() => document.querySelectorAll('.provider-panel').length, ); expect(count).toBeGreaterThanOrEqual(1); }); it('should show provider name in panel header', async () => { - const name = await browser.execute(() => { + const name = await exec(() => { const panel = document.querySelector('.provider-panel'); return panel?.querySelector('.provider-name')?.textContent?.trim() ?? ''; }); @@ -184,12 +185,12 @@ describe('Scenario E3 — Provider Configuration', () => { }); it('should expand provider panel to show enabled toggle', async () => { - await browser.execute(() => { + await exec(() => { const header = document.querySelector('.provider-header'); if (header) (header as HTMLElement).click(); }); await browser.pause(300); - const hasToggle = await browser.execute(() => { + const hasToggle = await exec(() => { const body = document.querySelector('.provider-body'); return body?.querySelector('.toggle-switch') !== null; }); @@ -197,7 +198,7 @@ describe('Scenario E3 — Provider Configuration', () => { }); after(async () => { - await browser.execute(() => { + await exec(() => { const header = document.querySelector('.provider-header'); const expanded = document.querySelector('.provider-body'); if (expanded && header) (header as HTMLElement).click(); @@ -218,14 +219,14 @@ describe('Scenario E4 — Status Bar Fleet State', () => { }); it('should show project count', async () => { - const text = await browser.execute(() => { + const text = await exec(() => { return document.querySelector('[data-testid="status-bar"]')?.textContent ?? ''; }); expect(text).toMatch(/\d+ projects/); }); it('should show agent state or project info', async () => { - const hasState = await browser.execute(() => { + const hasState = await exec(() => { const text = document.querySelector('[data-testid="status-bar"]')?.textContent ?? ''; return text.includes('idle') || text.includes('running') || text.includes('projects'); }); @@ -233,7 +234,7 @@ describe('Scenario E4 — Status Bar Fleet State', () => { }); it('should not show burn rate when all agents idle', async () => { - const burnRate = await browser.execute(() => { + const burnRate = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); return bar?.querySelector('.burn-rate')?.textContent ?? null; }); @@ -248,7 +249,7 @@ describe('Scenario E4 — Status Bar Fleet State', () => { }); it('should conditionally show attention queue button', async () => { - const info = await browser.execute(() => { + const info = await exec(() => { const btn = document.querySelector('[data-testid="status-bar"] .attention-btn'); return { exists: btn !== null, text: btn?.textContent?.trim() ?? '' }; }); diff --git a/tests/e2e/specs/phase-e-health.test.ts b/tests/e2e/specs/phase-e-health.test.ts index a026e7d..6a28c41 100644 --- a/tests/e2e/specs/phase-e-health.test.ts +++ b/tests/e2e/specs/phase-e-health.test.ts @@ -1,11 +1,12 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // /** Switch to a tab by text content in the first project box. */ async function clickTabByText(tabText: string): Promise { - await browser.execute((text) => { + await exec((text) => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); for (const tab of tabs) { if (tab.textContent?.trim() === text) { @@ -19,7 +20,7 @@ async function clickTabByText(tabText: string): Promise { /** Get the active tab text in the first project box. */ async function getActiveTabText(): Promise { - return browser.execute(() => { + return exec(() => { const box = document.querySelector('[data-testid="project-box"]'); const active = box?.querySelector('[data-testid="project-tabs"] .ptab.active'); return active?.textContent?.trim() ?? ''; @@ -28,7 +29,7 @@ async function getActiveTabText(): Promise { /** Check if a tab with given text exists in any project box. */ async function tabExistsWithText(tabText: string): Promise { - return browser.execute((text) => { + return exec((text) => { const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); return Array.from(tabs).some((t) => t.textContent?.trim() === text); }, tabText); @@ -39,7 +40,7 @@ describe('Scenario E5 — Project Health Indicators', () => { before(async () => { await browser.waitUntil( async () => { - const count = await browser.execute(() => + const count = await exec(() => document.querySelectorAll('[data-testid="project-box"]').length, ); return (count as number) >= 1; @@ -50,7 +51,7 @@ describe('Scenario E5 — Project Health Indicators', () => { }); it('should show status dot in ProjectHeader', async () => { - const statusDot = await browser.execute(() => { + const statusDot = await exec(() => { const header = document.querySelector('.project-header'); const dot = header?.querySelector('.status-dot'); return { @@ -63,7 +64,7 @@ describe('Scenario E5 — Project Health Indicators', () => { }); it('should show status dot with appropriate state class', async () => { - const dotClass = await browser.execute(() => { + const dotClass = await exec(() => { const header = document.querySelector('.project-header'); const dot = header?.querySelector('.status-dot'); return dot?.className ?? ''; @@ -72,7 +73,7 @@ describe('Scenario E5 — Project Health Indicators', () => { }); it('should show CWD path in ProjectHeader info area', async () => { - const cwdText = await browser.execute(() => { + const cwdText = await exec(() => { const header = document.querySelector('.project-header'); const cwd = header?.querySelector('.info-cwd'); return cwd?.textContent?.trim() ?? ''; @@ -81,7 +82,7 @@ describe('Scenario E5 — Project Health Indicators', () => { }); it('should have context pressure info element when pressure exists', async () => { - const ctxInfo = await browser.execute(() => { + const ctxInfo = await exec(() => { const header = document.querySelector('.project-header'); const ctx = header?.querySelector('.info-ctx'); return { @@ -95,7 +96,7 @@ describe('Scenario E5 — Project Health Indicators', () => { }); it('should have burn rate info element when agents are active', async () => { - const rateInfo = await browser.execute(() => { + const rateInfo = await exec(() => { const header = document.querySelector('.project-header'); const rate = header?.querySelector('.info-rate'); return { @@ -109,7 +110,7 @@ describe('Scenario E5 — Project Health Indicators', () => { }); it('should render ProjectHeader with all structural elements', async () => { - const structure = await browser.execute(() => { + const structure = await exec(() => { const header = document.querySelector('.project-header'); return { hasMain: header?.querySelector('.header-main') !== null, @@ -148,7 +149,7 @@ describe('Scenario E6 — Metrics Tab', () => { await browser.waitUntil( async () => { - const exists = await browser.execute(() => + const exists = await exec(() => document.querySelector('.metrics-panel') !== null, ); return exists as boolean; @@ -158,7 +159,7 @@ describe('Scenario E6 — Metrics Tab', () => { }); it('should show Live view with fleet aggregates', async () => { - const liveView = await browser.execute(() => { + const liveView = await exec(() => { const panel = document.querySelector('.metrics-panel'); const live = panel?.querySelector('.live-view'); const aggBar = panel?.querySelector('.agg-bar'); @@ -172,7 +173,7 @@ describe('Scenario E6 — Metrics Tab', () => { }); it('should show fleet badges in aggregates bar', async () => { - const badges = await browser.execute(() => { + const badges = await exec(() => { const panel = document.querySelector('.metrics-panel'); const aggBadges = panel?.querySelectorAll('.agg-badge'); return Array.from(aggBadges ?? []).map((b) => b.textContent?.trim() ?? ''); @@ -181,7 +182,7 @@ describe('Scenario E6 — Metrics Tab', () => { }); it('should show health cards for current project', async () => { - const cardLabels = await browser.execute(() => { + const cardLabels = await exec(() => { const panel = document.querySelector('.metrics-panel'); const labels = panel?.querySelectorAll('.hc-label'); return Array.from(labels ?? []).map((l) => l.textContent?.trim() ?? ''); @@ -190,7 +191,7 @@ describe('Scenario E6 — Metrics Tab', () => { }); it('should show view tabs for Live and History toggle', async () => { - const viewTabs = await browser.execute(() => { + const viewTabs = await exec(() => { const panel = document.querySelector('.metrics-panel'); const tabs = panel?.querySelectorAll('.vtab'); return Array.from(tabs ?? []).map((t) => t.textContent?.trim() ?? ''); @@ -206,7 +207,7 @@ describe('Scenario E6 — Metrics Tab', () => { describe('Scenario E7 — Conflict Detection UI', () => { it('should NOT show external write badge on fresh launch', async () => { - const hasExternalBadge = await browser.execute(() => { + const hasExternalBadge = await exec(() => { const headers = document.querySelectorAll('.project-header'); for (const header of headers) { const ext = header.querySelector('.info-conflict-external'); @@ -218,7 +219,7 @@ describe('Scenario E7 — Conflict Detection UI', () => { }); it('should NOT show agent conflict badge on fresh launch', async () => { - const hasConflictBadge = await browser.execute(() => { + const hasConflictBadge = await exec(() => { const headers = document.querySelectorAll('.project-header'); for (const header of headers) { const conflict = header.querySelector('.info-conflict:not(.info-conflict-external)'); @@ -230,7 +231,7 @@ describe('Scenario E7 — Conflict Detection UI', () => { }); it('should NOT show file conflict count in status bar on fresh launch', async () => { - const hasConflict = await browser.execute(() => { + const hasConflict = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); const conflictEl = bar?.querySelector('.state-conflict'); return conflictEl !== null; @@ -242,7 +243,7 @@ describe('Scenario E7 — Conflict Detection UI', () => { describe('Scenario E8 — Audit Log Tab', () => { it('should show Audit tab only for manager role projects', async () => { - const auditTabInfo = await browser.execute(() => { + const auditTabInfo = await exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); const results: { projectId: string; hasAudit: boolean }[] = []; for (const box of boxes) { @@ -261,7 +262,7 @@ describe('Scenario E8 — Audit Log Tab', () => { }); it('should render audit log content when Audit tab is activated', async () => { - const auditProjectId = await browser.execute(() => { + const auditProjectId = await exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); for (const box of boxes) { const tabs = box.querySelectorAll('[data-testid="project-tabs"] .ptab'); @@ -277,7 +278,7 @@ describe('Scenario E8 — Audit Log Tab', () => { if (!auditProjectId) return; // No manager agent — skip await browser.pause(500); - const auditContent = await browser.execute(() => { + const auditContent = await exec(() => { const tab = document.querySelector('.audit-log-tab'); if (!tab) return { exists: false, hasToolbar: false, hasEntries: false }; return { diff --git a/tests/e2e/specs/phase-f-llm.test.ts b/tests/e2e/specs/phase-f-llm.test.ts index b485e1f..12042e0 100644 --- a/tests/e2e/specs/phase-f-llm.test.ts +++ b/tests/e2e/specs/phase-f-llm.test.ts @@ -1,5 +1,6 @@ import { browser, expect } from '@wdio/globals'; import { isJudgeAvailable, assertWithJudge } from '../infra/llm-judge'; +import { exec } from '../helpers/execute.ts'; // Phase F — LLM-Judged Tests (F4–F7) // Settings completeness, theme system quality, error handling, and UI consistency. @@ -11,7 +12,7 @@ async function openSettings(): Promise { const panel = await browser.$('.settings-panel'); const isOpen = await panel.isDisplayed().catch(() => false); if (!isOpen) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); @@ -31,7 +32,7 @@ async function openSettings(): Promise { async function closeSettings(): Promise { const panel = await browser.$('.settings-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.settings-close, .panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -41,7 +42,7 @@ async function closeSettings(): Promise { /** Click a settings category by label text. */ async function clickSettingsCategory(label: string): Promise { - return browser.execute((lbl) => { + return exec((lbl) => { const items = document.querySelectorAll('.settings-sidebar button, .settings-sidebar [role="tab"]'); for (const item of items) { if (item.textContent?.includes(lbl)) { @@ -55,7 +56,7 @@ async function clickSettingsCategory(label: string): Promise { /** Get visible text content of settings content area. */ async function getSettingsContent(): Promise { - return browser.execute(() => { + return exec(() => { const content = document.querySelector('.settings-content, .settings-panel'); return content?.textContent ?? ''; }); @@ -129,13 +130,13 @@ describe('Scenario F5 — LLM-Judged Theme System Quality', () => { await browser.pause(300); // Open theme dropdown to capture options - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.custom-dropdown .dropdown-trigger'); if (trigger) (trigger as HTMLElement).click(); }); await browser.pause(300); - const themeHtml = await browser.execute(() => { + const themeHtml = await exec(() => { const panel = document.querySelector('.settings-content, .settings-panel'); if (!panel) return ''; // Get appearance section HTML for structure analysis @@ -143,7 +144,7 @@ describe('Scenario F5 — LLM-Judged Theme System Quality', () => { }); // Close dropdown - await browser.execute(() => document.body.click()); + await exec(() => document.body.click()); await browser.pause(200); const verdict = await assertWithJudge( @@ -170,7 +171,7 @@ describe('Scenario F6 — LLM-Judged Error Handling Quality', () => { } // Capture any visible toast notifications, error states, or warnings - const errorContent = await browser.execute(() => { + const errorContent = await exec(() => { const results: string[] = []; // Check toast notifications @@ -216,7 +217,7 @@ describe('Scenario F7 — LLM-Judged Overall UI Quality', () => { } // Capture full page structure and key visual elements - const uiSnapshot = await browser.execute(() => { + const uiSnapshot = await exec(() => { const elements: string[] = []; // Sidebar rail diff --git a/tests/e2e/specs/phase-f-search.test.ts b/tests/e2e/specs/phase-f-search.test.ts index 0919d2a..c5a8adc 100644 --- a/tests/e2e/specs/phase-f-search.test.ts +++ b/tests/e2e/specs/phase-f-search.test.ts @@ -1,4 +1,5 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; // Phase F — Search Overlay, Context Tab, Anchors, SSH Tab Tests (F1–F3) // Tests FTS5 search overlay interactions, context tab with anchors, and SSH tab. @@ -7,7 +8,7 @@ import { browser, expect } from '@wdio/globals'; /** Get first project box ID. */ async function getFirstProjectId(): Promise { - return browser.execute(() => { + return exec(() => { const box = document.querySelector('[data-testid="project-box"]'); return box?.getAttribute('data-project-id') ?? null; }); @@ -15,7 +16,7 @@ async function getFirstProjectId(): Promise { /** Switch to a tab in the first project box by tab text label. */ async function switchProjectTabByLabel(projectId: string, label: string): Promise { - await browser.execute((id, lbl) => { + await exec((id, lbl) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (!tabs) return; @@ -56,7 +57,7 @@ describe('Scenario F1 — Search Overlay Advanced', () => { afterEach(async () => { // Ensure overlay is closed after each test try { - const isVisible = await browser.execute(() => { + const isVisible = await exec(() => { const el = document.querySelector('.search-overlay'); return el !== null && window.getComputedStyle(el).display !== 'none'; }); @@ -70,7 +71,7 @@ describe('Scenario F1 — Search Overlay Advanced', () => { }); it('should open search overlay with Ctrl+Shift+F', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', 'Shift', 'f']); await browser.pause(500); @@ -80,12 +81,12 @@ describe('Scenario F1 — Search Overlay Advanced', () => { }); it('should show search input focused and ready', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', 'Shift', 'f']); await browser.pause(500); - const isFocused = await browser.execute(() => { + const isFocused = await exec(() => { const input = document.querySelector('.search-input'); return input === document.activeElement; }); @@ -93,7 +94,7 @@ describe('Scenario F1 — Search Overlay Advanced', () => { }); it('should show empty state message when no results', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', 'Shift', 'f']); await browser.pause(500); @@ -102,7 +103,7 @@ describe('Scenario F1 — Search Overlay Advanced', () => { await input.setValue('xyznonexistent99999'); await browser.pause(500); - const emptyMsg = await browser.execute(() => { + const emptyMsg = await exec(() => { const el = document.querySelector('.search-empty'); return el?.textContent ?? ''; }); @@ -110,7 +111,7 @@ describe('Scenario F1 — Search Overlay Advanced', () => { }); it('should close search overlay on Escape', async () => { - await browser.execute(() => document.body.focus()); + await exec(() => document.body.focus()); await browser.pause(100); await browser.keys(['Control', 'Shift', 'f']); await browser.pause(500); @@ -124,7 +125,7 @@ describe('Scenario F1 — Search Overlay Advanced', () => { await browser.pause(400); // Verify it closed - const isHidden = await browser.execute(() => { + const isHidden = await exec(() => { const el = document.querySelector('.search-overlay'); if (!el) return true; return window.getComputedStyle(el).display === 'none'; @@ -153,7 +154,7 @@ describe('Scenario F2 — Context Tab & Anchors', () => { }); it('should show Context tab in project tab bar', async () => { - const hasContextTab = await browser.execute((id) => { + const hasContextTab = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (!tabs) return false; @@ -168,7 +169,7 @@ describe('Scenario F2 — Context Tab & Anchors', () => { it('should render context visualization when Context tab activated', async () => { await switchProjectTabByLabel(projectId, 'Context'); - const hasContent = await browser.execute((id) => { + const hasContent = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); // Look for context-tab component, stats, token meter, or anchors section const contextTab = box?.querySelector('.context-tab'); @@ -181,14 +182,14 @@ describe('Scenario F2 — Context Tab & Anchors', () => { it('should show anchor budget scale selector in Settings', async () => { // Open settings - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); // Navigate to Orchestration category - const clickedOrch = await browser.execute(() => { + const clickedOrch = await exec(() => { const items = document.querySelectorAll('.settings-sidebar button, .settings-sidebar [role="tab"]'); for (const item of items) { if (item.textContent?.includes('Orchestration')) { @@ -201,7 +202,7 @@ describe('Scenario F2 — Context Tab & Anchors', () => { await browser.pause(300); // Look for anchor budget setting - const hasAnchorBudget = await browser.execute(() => { + const hasAnchorBudget = await exec(() => { const panel = document.querySelector('.settings-panel, .settings-content'); if (!panel) return false; const text = panel.textContent ?? ''; @@ -243,7 +244,7 @@ describe('Scenario F3 — SSH Tab', () => { }); it('should show SSH tab in project tab bar', async () => { - const hasSshTab = await browser.execute((id) => { + const hasSshTab = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); if (!tabs) return false; @@ -258,7 +259,7 @@ describe('Scenario F3 — SSH Tab', () => { it('should render SSH content pane when tab activated', async () => { await switchProjectTabByLabel(projectId, 'SSH'); - const hasSshContent = await browser.execute((id) => { + const hasSshContent = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const sshTab = box?.querySelector('.ssh-tab'); const sshHeader = box?.querySelector('.ssh-header'); @@ -269,7 +270,7 @@ describe('Scenario F3 — SSH Tab', () => { it('should show SSH connection list or empty state', async () => { // SSH tab should show either connections or an empty state message - const sshState = await browser.execute((id) => { + const sshState = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); const list = box?.querySelector('.ssh-list'); const empty = box?.querySelector('.ssh-empty'); @@ -286,7 +287,7 @@ describe('Scenario F3 — SSH Tab', () => { }); it('should show add SSH connection button or form', async () => { - const hasAddControl = await browser.execute((id) => { + const hasAddControl = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); // Look for add button or SSH form const form = box?.querySelector('.ssh-form'); diff --git a/tests/e2e/specs/search.test.ts b/tests/e2e/specs/search.test.ts index e055092..8b32533 100644 --- a/tests/e2e/specs/search.test.ts +++ b/tests/e2e/specs/search.test.ts @@ -5,12 +5,13 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { openSearch, closeSearch } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Search overlay', () => { it('should open via Ctrl+Shift+F', async () => { await openSearch(); - const visible = await browser.execute((sel: string) => { + const visible = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return false; return getComputedStyle(el).display !== 'none'; @@ -21,7 +22,7 @@ describe('Search overlay', () => { }); it('should focus the search input on open', async () => { - const focused = await browser.execute((sel: string) => { + const focused = await exec((sel: string) => { return document.activeElement?.matches(sel) ?? false; }, S.SEARCH_INPUT); if (focused) { @@ -50,7 +51,7 @@ describe('Search overlay', () => { }); it('should show Esc hint badge', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.ESC_HINT); @@ -77,7 +78,7 @@ describe('Search overlay', () => { await input.setValue('test'); // Results should not appear instantly - const immediateCount = await browser.execute(() => { + const immediateCount = await exec(() => { return document.querySelectorAll('.result-item').length; }); expect(typeof immediateCount).toBe('number'); @@ -86,7 +87,7 @@ describe('Search overlay', () => { it('should close on Escape key', async () => { await closeSearch(); - const hidden = await browser.execute((sel: string) => { + const hidden = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return true; return getComputedStyle(el).display === 'none'; @@ -98,7 +99,7 @@ describe('Search overlay', () => { await openSearch(); await browser.pause(300); - const visible = await browser.execute(() => { + const visible = await exec(() => { // Search overlay uses class="search-backdrop" or "overlay-backdrop" const el = document.querySelector('.overlay-backdrop') ?? document.querySelector('.search-backdrop'); @@ -117,7 +118,7 @@ describe('Search overlay', () => { await openSearch(); const input = await browser.$(S.SEARCH_INPUT); if (await input.isExisting()) { - const value = await browser.execute((sel: string) => { + const value = await exec((sel: string) => { const el = document.querySelector(sel) as HTMLInputElement; return el?.value ?? ''; }, S.SEARCH_INPUT); @@ -128,7 +129,7 @@ describe('Search overlay', () => { it('should have proper overlay positioning', async () => { await openSearch(); - const dims = await browser.execute((sel: string) => { + const dims = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return null; const rect = el.getBoundingClientRect(); diff --git a/tests/e2e/specs/settings.test.ts b/tests/e2e/specs/settings.test.ts index 3109c8d..70bcb2d 100644 --- a/tests/e2e/specs/settings.test.ts +++ b/tests/e2e/specs/settings.test.ts @@ -8,10 +8,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { openSettings, closeSettings, switchSettingsCategory } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; /** Count settings category tabs across both UIs */ async function countSettingsTabs(): Promise { - return browser.execute(() => { + return exec(() => { // Tauri: .settings-sidebar .sidebar-item | Electrobun: .settings-tab or .cat-btn return (document.querySelectorAll('.settings-sidebar .sidebar-item').length || document.querySelectorAll('.settings-tab').length @@ -29,7 +30,7 @@ describe('Settings panel', () => { }); it('should open on gear icon click', async () => { - const visible = await browser.execute(() => { + const visible = await exec(() => { // Tauri: .sidebar-panel or .settings-panel | Electrobun: .settings-drawer const el = document.querySelector('.settings-panel') ?? document.querySelector('.settings-drawer') @@ -51,7 +52,7 @@ describe('Settings panel', () => { }); it('should highlight the active category', async () => { - const hasActive = await browser.execute(() => { + const hasActive = await exec(() => { return (document.querySelector('.sidebar-item.active') ?? document.querySelector('.settings-tab.active') ?? document.querySelector('.cat-btn.active')) !== null; @@ -61,7 +62,7 @@ describe('Settings panel', () => { it('should switch categories on tab click', async () => { await switchSettingsCategory(1); - const isActive = await browser.execute(() => { + const isActive = await exec(() => { const tabs = document.querySelectorAll('.settings-sidebar .sidebar-item, .settings-tab, .cat-btn'); if (tabs.length < 2) return false; return tabs[1].classList.contains('active'); @@ -72,7 +73,7 @@ describe('Settings panel', () => { it('should show theme dropdown in Appearance category', async () => { await switchSettingsCategory(0); - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.theme-section') ?? document.querySelector('.custom-dropdown') ?? document.querySelector('.dd-btn')) !== null; @@ -81,7 +82,7 @@ describe('Settings panel', () => { }); it('should show font size stepper', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.font-stepper') ?? document.querySelector('.stepper') ?? document.querySelector('.size-stepper')) !== null; @@ -90,7 +91,7 @@ describe('Settings panel', () => { }); it('should show font family dropdown', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.font-dropdown') ?? document.querySelector('.custom-dropdown')) !== null; }); @@ -98,7 +99,7 @@ describe('Settings panel', () => { }); it('should increment font size on stepper click', async () => { - const changed = await browser.execute(() => { + const changed = await exec(() => { const btn = document.querySelector('.font-stepper .step-up') ?? document.querySelector('.stepper .step-up') ?? document.querySelectorAll('.stepper button')[1]; @@ -116,7 +117,7 @@ describe('Settings panel', () => { }); it('should show provider panels', async () => { - const hasProviders = await browser.execute(() => { + const hasProviders = await exec(() => { return (document.querySelector('.provider-panel') ?? document.querySelector('.provider-settings') ?? document.querySelector('.providers-section')) !== null; @@ -129,7 +130,7 @@ describe('Settings panel', () => { if (tabCount > 0) { await switchSettingsCategory(tabCount - 1); } - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.update-row') ?? document.querySelector('.refresh-btn') ?? document.querySelector('.diagnostics')) !== null; @@ -138,7 +139,7 @@ describe('Settings panel', () => { }); it('should show version label', async () => { - const text = await browser.execute(() => { + const text = await exec(() => { const el = document.querySelector('.version-label'); return el?.textContent ?? ''; }); @@ -148,14 +149,14 @@ describe('Settings panel', () => { }); it('should close on close button click', async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.settings-close') ?? document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(400); - const hidden = await browser.execute(() => { + const hidden = await exec(() => { const el = document.querySelector('.settings-panel') ?? document.querySelector('.settings-drawer') ?? document.querySelector('.sidebar-panel'); @@ -170,7 +171,7 @@ describe('Settings panel', () => { await browser.keys('Escape'); await browser.pause(400); - const hidden = await browser.execute(() => { + const hidden = await exec(() => { const el = document.querySelector('.settings-panel') ?? document.querySelector('.settings-drawer') ?? document.querySelector('.sidebar-panel'); @@ -182,7 +183,7 @@ describe('Settings panel', () => { it('should show keyboard shortcuts info', async () => { await openSettings(); - const hasShortcuts = await browser.execute(() => { + const hasShortcuts = await exec(() => { const text = document.body.textContent ?? ''; return text.includes('Ctrl+K') || text.includes('shortcut') || text.includes('Keyboard'); }); @@ -194,7 +195,7 @@ describe('Settings panel', () => { if (tabCount > 0) { await switchSettingsCategory(tabCount - 1); } - const hasDiag = await browser.execute(() => { + const hasDiag = await exec(() => { return document.querySelector('.diagnostics') !== null; }); // Diagnostics is Electrobun-only; Tauri may not have it @@ -203,7 +204,7 @@ describe('Settings panel', () => { it('should have shell/CWD defaults section', async () => { await switchSettingsCategory(0); - const hasDefaults = await browser.execute(() => { + const hasDefaults = await exec(() => { const text = document.body.textContent ?? ''; return text.includes('Shell') || text.includes('CWD') || text.includes('Default'); }); @@ -211,7 +212,7 @@ describe('Settings panel', () => { }); it('should persist theme selection', async () => { - const value = await browser.execute(() => { + const value = await exec(() => { return getComputedStyle(document.documentElement).getPropertyValue('--ctp-base').trim(); }); expect(value.length).toBeGreaterThan(0); @@ -219,7 +220,7 @@ describe('Settings panel', () => { it('should show group/project CRUD section', async () => { await switchSettingsCategory(1); - const hasProjects = await browser.execute(() => { + const hasProjects = await exec(() => { const text = document.body.textContent ?? ''; return text.includes('Project') || text.includes('Group') || text.includes('Agent'); }); diff --git a/tests/e2e/specs/smoke.test.ts b/tests/e2e/specs/smoke.test.ts index 0b08430..49ca16d 100644 --- a/tests/e2e/specs/smoke.test.ts +++ b/tests/e2e/specs/smoke.test.ts @@ -7,6 +7,7 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; +import { exec } from '../helpers/execute.ts'; describe('Smoke tests', () => { it('should launch and have the correct title', async () => { @@ -22,7 +23,7 @@ describe('Smoke tests', () => { }); it('should render the app shell', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.APP_SHELL); if (exists) { @@ -35,7 +36,7 @@ describe('Smoke tests', () => { // Wait for sidebar to appear (may take time after splash screen) await browser.waitUntil( async () => - browser.execute(() => { + exec(() => { const el = document.querySelector('.sidebar-rail') ?? document.querySelector('.sidebar') ?? document.querySelector('[data-testid="sidebar-rail"]'); @@ -52,7 +53,7 @@ describe('Smoke tests', () => { }); it('should display the status bar', async () => { - const visible = await browser.execute(() => { + const visible = await exec(() => { const el = document.querySelector('[data-testid="status-bar"]') ?? document.querySelector('.status-bar'); if (!el) return false; @@ -62,7 +63,7 @@ describe('Smoke tests', () => { }); it('should show version text in status bar', async () => { - const text = await browser.execute(() => { + const text = await exec(() => { const el = document.querySelector('.status-bar .version'); return el?.textContent?.trim() ?? ''; }); @@ -72,10 +73,10 @@ describe('Smoke tests', () => { }); it('should show group buttons in sidebar (Electrobun) or tab bar (Tauri)', async function () { - const hasGroups = await browser.execute(() => { + const hasGroups = await exec(() => { return document.querySelectorAll('.group-btn').length > 0; }); - const hasTabBar = await browser.execute(() => { + const hasTabBar = await exec(() => { return document.querySelector('.sidebar-rail') !== null || document.querySelector('[data-testid="sidebar-rail"]') !== null; }); @@ -84,7 +85,7 @@ describe('Smoke tests', () => { }); it('should show the settings gear icon', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('[data-testid="settings-btn"]') ?? document.querySelector('.sidebar-icon') ?? document.querySelector('.rail-btn')) !== null; @@ -93,7 +94,7 @@ describe('Smoke tests', () => { }); it('should show the notification bell', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { // Tauri: .bell-btn | Electrobun: .notif-btn return (document.querySelector('.notif-btn') ?? document.querySelector('.bell-btn') @@ -111,7 +112,7 @@ describe('Smoke tests', () => { }); it('should toggle sidebar with settings button', async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]') ?? document.querySelector('.sidebar-icon') ?? document.querySelector('.rail-btn'); @@ -121,13 +122,13 @@ describe('Smoke tests', () => { // Wait for either panel (Tauri: .sidebar-panel, Electrobun: .settings-drawer) await browser.waitUntil( async () => - browser.execute(() => + exec(() => document.querySelector('.sidebar-panel, .settings-drawer, .settings-panel') !== null, ) as Promise, { timeout: 5_000 }, ).catch(() => {}); // may not appear in all configs - const visible = await browser.execute(() => { + const visible = await exec(() => { const el = document.querySelector('.sidebar-panel') ?? document.querySelector('.settings-drawer') ?? document.querySelector('.settings-panel'); @@ -139,7 +140,7 @@ describe('Smoke tests', () => { expect(visible).toBe(true); // Close it - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close') ?? document.querySelector('.settings-close'); if (btn) (btn as HTMLElement).click(); @@ -149,7 +150,7 @@ describe('Smoke tests', () => { }); it('should show project cards in grid', async () => { - const count = await browser.execute(() => { + const count = await exec(() => { // Tauri: .project-box | Electrobun: .project-card return document.querySelectorAll('.project-box, .project-card').length; }); @@ -158,7 +159,7 @@ describe('Smoke tests', () => { }); it('should show the AGOR title', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent?.trim() ?? ''; }, S.AGOR_TITLE); @@ -168,7 +169,7 @@ describe('Smoke tests', () => { }); it('should have terminal section in project card', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.TERMINAL_SECTION); // Terminal section may or may not be visible depending on card state @@ -177,7 +178,7 @@ describe('Smoke tests', () => { it('should have window close button (Electrobun) or native decorations', async () => { // Electrobun has a custom close button; Tauri uses native decorations - const hasClose = await browser.execute(() => { + const hasClose = await exec(() => { return document.querySelector('.close-btn') !== null; }); // Just verify the check completed — both stacks are valid diff --git a/tests/e2e/specs/splash.test.ts b/tests/e2e/specs/splash.test.ts index 4f49144..f7c808e 100644 --- a/tests/e2e/specs/splash.test.ts +++ b/tests/e2e/specs/splash.test.ts @@ -7,17 +7,18 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; +import { exec } from '../helpers/execute.ts'; describe('Splash screen', () => { it('should have splash element in DOM', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.SPLASH); expect(exists).toBe(true); }); it('should show the AGOR logo text', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.LOGO_TEXT); @@ -27,7 +28,7 @@ describe('Splash screen', () => { }); it('should show version string', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.SPLASH_VERSION); @@ -37,7 +38,7 @@ describe('Splash screen', () => { }); it('should have loading indicator dots', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.SPLASH_DOT); if (count > 0) { @@ -47,7 +48,7 @@ describe('Splash screen', () => { it('should use display toggle (not removed from DOM)', async () => { // Splash stays in DOM but gets display:none after load - const display = await browser.execute((sel: string) => { + const display = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return 'not-found'; return getComputedStyle(el).display; @@ -57,7 +58,7 @@ describe('Splash screen', () => { }); it('should have proper z-index for overlay', async () => { - const zIndex = await browser.execute((sel: string) => { + const zIndex = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return '0'; return getComputedStyle(el).zIndex; diff --git a/tests/e2e/specs/status-bar.test.ts b/tests/e2e/specs/status-bar.test.ts index b7171c4..7d1890f 100644 --- a/tests/e2e/specs/status-bar.test.ts +++ b/tests/e2e/specs/status-bar.test.ts @@ -7,6 +7,7 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { assertStatusBarComplete } from '../helpers/assertions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Status bar', () => { it('should be visible at the bottom', async () => { @@ -14,7 +15,7 @@ describe('Status bar', () => { }); it('should show version text', async () => { - const text = await browser.execute(() => { + const text = await exec(() => { const el = document.querySelector('.status-bar .version'); return el?.textContent?.trim() ?? ''; }); @@ -24,7 +25,7 @@ describe('Status bar', () => { }); it('should show agent state counts', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { // Tauri uses inline spans (.state-running, .state-idle, .state-stalled) // Electrobun uses .agent-counts return (document.querySelector('.agent-counts') @@ -35,14 +36,14 @@ describe('Status bar', () => { }); it('should show burn rate', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return document.querySelector('.burn-rate') !== null; }); expect(typeof exists).toBe('boolean'); }); it('should show attention queue dropdown', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { // Tauri: .attention-btn | Electrobun: .attention-queue return (document.querySelector('.attention-queue') ?? document.querySelector('.attention-btn')) !== null; @@ -51,7 +52,7 @@ describe('Status bar', () => { }); it('should show total tokens', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.fleet-tokens') ?? document.querySelector('.tokens')) !== null; }); @@ -59,7 +60,7 @@ describe('Status bar', () => { }); it('should show total cost', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.fleet-cost') ?? document.querySelector('.cost')) !== null; }); @@ -67,7 +68,7 @@ describe('Status bar', () => { }); it('should show project count', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { // Tauri embeds count in text, Electrobun uses .project-count const hasClass = document.querySelector('.project-count') !== null; const hasText = (document.querySelector('.status-bar')?.textContent ?? '').includes('project'); @@ -77,7 +78,7 @@ describe('Status bar', () => { }); it('should have proper height and layout', async () => { - const dims = await browser.execute(() => { + const dims = await exec(() => { const el = document.querySelector('[data-testid="status-bar"]') ?? document.querySelector('.status-bar'); if (!el) return null; @@ -92,7 +93,7 @@ describe('Status bar', () => { }); it('should use theme colors', async () => { - const bg = await browser.execute(() => { + const bg = await exec(() => { const el = document.querySelector('[data-testid="status-bar"]') ?? document.querySelector('.status-bar'); if (!el) return ''; @@ -104,7 +105,7 @@ describe('Status bar', () => { }); it('should show agent running/idle/stalled counts', async () => { - const text = await browser.execute(() => { + const text = await exec(() => { const el = document.querySelector('[data-testid="status-bar"]') ?? document.querySelector('.status-bar'); return el?.textContent ?? ''; @@ -113,7 +114,7 @@ describe('Status bar', () => { }); it('should show attention queue cards on click', async () => { - const dropdown = await browser.execute(() => { + const dropdown = await exec(() => { const btn = document.querySelector('.attention-queue') ?? document.querySelector('.attention-btn'); if (btn) (btn as HTMLElement).click(); @@ -122,7 +123,7 @@ describe('Status bar', () => { }); expect(dropdown !== undefined).toBe(true); // Close by clicking elsewhere - await browser.execute(() => document.body.click()); + await exec(() => document.body.click()); await browser.pause(200); }); }); diff --git a/tests/e2e/specs/tasks.test.ts b/tests/e2e/specs/tasks.test.ts index 1a4355f..61fcea4 100644 --- a/tests/e2e/specs/tasks.test.ts +++ b/tests/e2e/specs/tasks.test.ts @@ -4,10 +4,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; +import { exec } from '../helpers/execute.ts'; describe('Task board', () => { it('should render the task board container', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.TASK_BOARD); if (exists) { @@ -17,7 +18,7 @@ describe('Task board', () => { }); it('should show the toolbar with title', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.TB_TITLE); @@ -27,7 +28,7 @@ describe('Task board', () => { }); it('should have 5 kanban columns', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TB_COLUMN); if (count > 0) { @@ -36,7 +37,7 @@ describe('Task board', () => { }); it('should show column headers with labels', async () => { - const texts = await browser.execute((sel: string) => { + const texts = await exec((sel: string) => { const labels = document.querySelectorAll(sel); return Array.from(labels).map(l => l.textContent?.toUpperCase() ?? ''); }, S.TB_COL_LABEL); @@ -50,7 +51,7 @@ describe('Task board', () => { }); it('should show column counts', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TB_COL_COUNT); if (count > 0) { @@ -83,7 +84,7 @@ describe('Task board', () => { }); it('should show task count in toolbar', async () => { - const text = await browser.execute((sel: string) => { + const text = await exec((sel: string) => { const el = document.querySelector(sel); return el?.textContent ?? ''; }, S.TB_COUNT); @@ -93,7 +94,7 @@ describe('Task board', () => { }); it('should have task cards in columns', async () => { - const hasCards = await browser.execute(() => { + const hasCards = await exec(() => { return document.querySelector('.task-card') ?? document.querySelector('.tb-card'); }); @@ -101,7 +102,7 @@ describe('Task board', () => { }); it('should support drag handle on task cards', async () => { - const hasDrag = await browser.execute(() => { + const hasDrag = await exec(() => { return document.querySelector('.drag-handle') ?? document.querySelector('[draggable]'); }); diff --git a/tests/e2e/specs/terminal-theme.test.ts b/tests/e2e/specs/terminal-theme.test.ts index 8c7b37a..a2e86cc 100644 --- a/tests/e2e/specs/terminal-theme.test.ts +++ b/tests/e2e/specs/terminal-theme.test.ts @@ -1,10 +1,11 @@ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; /** Reset UI to home state (close any open panels/overlays). */ async function resetToHomeState(): Promise { const panel = await browser.$('.sidebar-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -19,7 +20,7 @@ async function openSettings(): Promise { const panel = await browser.$('.sidebar-panel'); const isOpen = await panel.isDisplayed().catch(() => false); if (!isOpen) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="settings-btn"]'); if (btn) (btn as HTMLElement).click(); }); @@ -27,7 +28,7 @@ async function openSettings(): Promise { } await browser.waitUntil( async () => { - const has = await browser.execute(() => + const has = await exec(() => document.querySelector('.settings-panel .settings-content') !== null, ); return has as boolean; @@ -41,7 +42,7 @@ async function openSettings(): Promise { async function closeSettings(): Promise { const panel = await browser.$('.sidebar-panel'); if (await panel.isDisplayed().catch(() => false)) { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.panel-close'); if (btn) (btn as HTMLElement).click(); }); @@ -53,7 +54,7 @@ describe('Agent Orchestrator — Terminal Tabs', () => { before(async () => { await resetToHomeState(); // Ensure Model tab is active so terminal section is visible - await browser.execute(() => { + await exec(() => { const tab = document.querySelector('.project-box .ptab'); if (tab) (tab as HTMLElement).click(); }); @@ -71,7 +72,7 @@ describe('Agent Orchestrator — Terminal Tabs', () => { it('should expand terminal area on toggle click', async () => { // Click terminal toggle via JS - await browser.execute(() => { + await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); if (toggle) (toggle as HTMLElement).click(); }); @@ -92,14 +93,14 @@ describe('Agent Orchestrator — Terminal Tabs', () => { it('should add a shell tab', async () => { // Click add tab button via JS - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="tab-add"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); // Verify tab title via JS to avoid stale element issues - const title = await browser.execute(() => { + const title = await exec(() => { const el = document.querySelector('.tab-bar .tab-title'); return el ? el.textContent : ''; }); @@ -113,25 +114,25 @@ describe('Agent Orchestrator — Terminal Tabs', () => { it('should add a second shell tab and switch between them', async () => { // Add second tab via JS - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('[data-testid="tab-add"]'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(500); - const tabCount = await browser.execute(() => { + const tabCount = await exec(() => { return document.querySelectorAll('.tab-bar .tab').length; }); expect(tabCount as number).toBeGreaterThanOrEqual(2); // Click first tab and verify it becomes active with Shell title - await browser.execute(() => { + await exec(() => { const tabs = document.querySelectorAll('.tab-bar .tab'); if (tabs[0]) (tabs[0] as HTMLElement).click(); }); await browser.pause(300); - const activeTitle = await browser.execute(() => { + const activeTitle = await exec(() => { const active = document.querySelector('.tab-bar .tab.active .tab-title'); return active ? active.textContent : ''; }); @@ -143,7 +144,7 @@ describe('Agent Orchestrator — Terminal Tabs', () => { const countBefore = tabsBefore.length; // Close the last tab - await browser.execute(() => { + await exec(() => { const closeBtns = document.querySelectorAll('.tab-close'); if (closeBtns.length > 0) { (closeBtns[closeBtns.length - 1] as HTMLElement).click(); @@ -157,14 +158,14 @@ describe('Agent Orchestrator — Terminal Tabs', () => { after(async () => { // Clean up: close remaining tabs and collapse terminal - await browser.execute(() => { + await exec(() => { const closeBtns = document.querySelectorAll('.tab-close'); closeBtns.forEach(btn => (btn as HTMLElement).click()); }); await browser.pause(300); // Collapse terminal - await browser.execute(() => { + await exec(() => { const toggle = document.querySelector('[data-testid="terminal-toggle"]'); if (toggle) { const chevron = toggle.querySelector('.toggle-chevron.expanded'); @@ -187,7 +188,7 @@ describe('Agent Orchestrator — Theme Switching', () => { it('should show theme dropdown with group labels', async () => { // Close any open dropdowns first - await browser.execute(() => { + await exec(() => { const openMenu = document.querySelector('.dropdown-menu'); if (openMenu) { const trigger = openMenu.closest('.custom-dropdown')?.querySelector('.dropdown-btn'); @@ -197,7 +198,7 @@ describe('Agent Orchestrator — Theme Switching', () => { await browser.pause(200); // Click the theme dropdown button (first dropdown in appearance) - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (trigger) (trigger as HTMLElement).click(); }); @@ -211,7 +212,7 @@ describe('Agent Orchestrator — Theme Switching', () => { expect(groupLabels.length).toBeGreaterThanOrEqual(2); // Close dropdown - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (trigger) (trigger as HTMLElement).click(); }); @@ -220,12 +221,12 @@ describe('Agent Orchestrator — Theme Switching', () => { it('should switch theme and update CSS variables', async () => { // Get current base color - const baseBefore = await browser.execute(() => { + const baseBefore = await exec(() => { return getComputedStyle(document.documentElement).getPropertyValue('--ctp-base').trim(); }); // Open theme dropdown - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (trigger) (trigger as HTMLElement).click(); }); @@ -235,7 +236,7 @@ describe('Agent Orchestrator — Theme Switching', () => { await menu.waitForExist({ timeout: 5000 }); // Click the first non-active theme option - const changed = await browser.execute(() => { + const changed = await exec(() => { const options = document.querySelectorAll('.dropdown-menu .dropdown-item:not(.active)'); if (options.length > 0) { (options[0] as HTMLElement).click(); @@ -247,18 +248,18 @@ describe('Agent Orchestrator — Theme Switching', () => { await browser.pause(500); // Verify CSS variable changed - const baseAfter = await browser.execute(() => { + const baseAfter = await exec(() => { return getComputedStyle(document.documentElement).getPropertyValue('--ctp-base').trim(); }); expect(baseAfter).not.toBe(baseBefore); // Switch back to Catppuccin Mocha (first option) to restore state - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (trigger) (trigger as HTMLElement).click(); }); await browser.pause(500); - await browser.execute(() => { + await exec(() => { const options = document.querySelectorAll('.dropdown-menu .dropdown-item'); if (options.length > 0) (options[0] as HTMLElement).click(); }); @@ -266,7 +267,7 @@ describe('Agent Orchestrator — Theme Switching', () => { }); it('should show active theme option', async () => { - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (trigger) (trigger as HTMLElement).click(); }); @@ -278,7 +279,7 @@ describe('Agent Orchestrator — Theme Switching', () => { const activeOption = await browser.$('.dropdown-item.active'); await expect(activeOption).toBeExisting(); - await browser.execute(() => { + await exec(() => { const trigger = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); if (trigger) (trigger as HTMLElement).click(); }); diff --git a/tests/e2e/specs/terminal.test.ts b/tests/e2e/specs/terminal.test.ts index 16b5188..07031bd 100644 --- a/tests/e2e/specs/terminal.test.ts +++ b/tests/e2e/specs/terminal.test.ts @@ -5,10 +5,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { addTerminalTab } from '../helpers/actions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Terminal section', () => { it('should show the terminal tab bar', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.TERMINAL_TABS); if (exists) { @@ -26,20 +27,20 @@ describe('Terminal section', () => { it('should create a new terminal tab on add click', async function () { // Terminal may be collapsed by default — expand first - const hasAddBtn = await browser.execute(() => { + const hasAddBtn = await exec(() => { const btn = document.querySelector('.tab-add-btn'); if (!btn) return false; return getComputedStyle(btn).display !== 'none'; }); if (!hasAddBtn) { this.skip(); return; } - const countBefore = await browser.execute((sel: string) => { + const countBefore = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB); await addTerminalTab(); - const countAfter = await browser.execute((sel: string) => { + const countAfter = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB); expect(countAfter).toBeGreaterThanOrEqual(countBefore + 1); @@ -62,7 +63,7 @@ describe('Terminal section', () => { }); it('should highlight active tab', async () => { - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB_ACTIVE); if (count > 0) { @@ -71,18 +72,18 @@ describe('Terminal section', () => { }); it('should switch tabs on click', async () => { - const tabCount = await browser.execute((sel: string) => { + const tabCount = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB); if (tabCount >= 2) { - await browser.execute((sel: string) => { + await exec((sel: string) => { const tabs = document.querySelectorAll(sel); if (tabs[1]) (tabs[1] as HTMLElement).click(); }, S.TERMINAL_TAB); await browser.pause(300); - const isActive = await browser.execute((sel: string) => { + const isActive = await exec((sel: string) => { const tabs = document.querySelectorAll(sel); return tabs[1]?.classList.contains('active') ?? false; }, S.TERMINAL_TAB); @@ -104,7 +105,7 @@ describe('Terminal section', () => { }); it('should close a tab on close button click', async () => { - const countBefore = await browser.execute((sel: string) => { + const countBefore = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB); if (countBefore < 2) return; @@ -119,7 +120,7 @@ describe('Terminal section', () => { await closeBtn.click(); await browser.pause(300); - const countAfter = await browser.execute((sel: string) => { + const countAfter = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB); expect(countAfter).toBeLessThan(countBefore); @@ -133,7 +134,7 @@ describe('Terminal section', () => { await collapseBtn.click(); await browser.pause(300); - const h1 = await browser.execute((sel: string) => { + const h1 = await exec((sel: string) => { const el = document.querySelector(sel); return el ? getComputedStyle(el).height : ''; }, S.TERMINAL_SECTION); @@ -141,7 +142,7 @@ describe('Terminal section', () => { await collapseBtn.click(); await browser.pause(300); - const h2 = await browser.execute((sel: string) => { + const h2 = await exec((sel: string) => { const el = document.querySelector(sel); return el ? getComputedStyle(el).height : ''; }, S.TERMINAL_SECTION); @@ -151,7 +152,7 @@ describe('Terminal section', () => { it('should handle multiple terminal tabs', async function () { // Terminal may be collapsed by default — skip if add button not visible - const hasAddBtn = await browser.execute(() => { + const hasAddBtn = await exec(() => { const btn = document.querySelector('.tab-add-btn'); if (!btn) return false; return getComputedStyle(btn).display !== 'none'; @@ -162,7 +163,7 @@ describe('Terminal section', () => { await addTerminalTab(); await addTerminalTab(); - const count = await browser.execute((sel: string) => { + const count = await exec((sel: string) => { return document.querySelectorAll(sel).length; }, S.TERMINAL_TAB); expect(count).toBeGreaterThanOrEqual(2); @@ -170,7 +171,7 @@ describe('Terminal section', () => { it('should handle PTY output display', async () => { // Verify xterm rows container exists (for PTY output rendering) - const hasRows = await browser.execute(() => { + const hasRows = await exec(() => { return document.querySelector('.xterm-rows') !== null; }); if (hasRows) { @@ -179,7 +180,7 @@ describe('Terminal section', () => { }); it('should have terminal container with correct dimensions', async () => { - const dims = await browser.execute((sel: string) => { + const dims = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return null; const rect = el.getBoundingClientRect(); @@ -192,7 +193,7 @@ describe('Terminal section', () => { }); it('should have resize handle', async () => { - const hasHandle = await browser.execute(() => { + const hasHandle = await exec(() => { return document.querySelector('.resize-handle') ?? document.querySelector('.terminal-resize') !== null; }); diff --git a/tests/e2e/specs/theme.test.ts b/tests/e2e/specs/theme.test.ts index 43e2d86..fc812b4 100644 --- a/tests/e2e/specs/theme.test.ts +++ b/tests/e2e/specs/theme.test.ts @@ -6,6 +6,7 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; import { openSettings, closeSettings, switchSettingsCategory } from '../helpers/actions.ts'; import { assertThemeApplied } from '../helpers/assertions.ts'; +import { exec } from '../helpers/execute.ts'; describe('Theme system', () => { before(async () => { @@ -19,7 +20,7 @@ describe('Theme system', () => { }); it('should show theme dropdown button', async () => { - const exists = await browser.execute(() => { + const exists = await exec(() => { return (document.querySelector('.dd-btn') ?? document.querySelector('.dropdown-btn') ?? document.querySelector('.custom-dropdown')) !== null; @@ -28,14 +29,14 @@ describe('Theme system', () => { }); it('should open theme dropdown on click', async () => { - await browser.execute(() => { + await exec(() => { const btn = document.querySelector('.dd-btn') ?? document.querySelector('.dropdown-btn'); if (btn) (btn as HTMLElement).click(); }); await browser.pause(300); - const listOpen = await browser.execute(() => { + const listOpen = await exec(() => { const list = document.querySelector('.dd-list') ?? document.querySelector('.dropdown-menu'); if (!list) return false; @@ -47,7 +48,7 @@ describe('Theme system', () => { }); it('should show theme groups (Catppuccin, Editor, Deep Dark)', async () => { - const texts = await browser.execute(() => { + const texts = await exec(() => { const labels = document.querySelectorAll('.dd-group-label, .dropdown-group-label'); return Array.from(labels).map(l => l.textContent ?? ''); }); @@ -60,7 +61,7 @@ describe('Theme system', () => { }); it('should list at least 17 theme options', async () => { - const count = await browser.execute(() => { + const count = await exec(() => { return (document.querySelectorAll('.dd-item').length || document.querySelectorAll('.dropdown-item').length); }); @@ -70,7 +71,7 @@ describe('Theme system', () => { }); it('should highlight the currently selected theme', async () => { - const hasSelected = await browser.execute(() => { + const hasSelected = await exec(() => { return (document.querySelector('.dd-item.selected') ?? document.querySelector('.dropdown-item.active')) !== null; }); @@ -84,7 +85,7 @@ describe('Theme system', () => { }); it('should have 4 Catppuccin themes', async () => { - const count = await browser.execute(() => { + const count = await exec(() => { const items = document.querySelectorAll('.dd-item, .dropdown-item'); let catCount = 0; const catNames = ['mocha', 'macchiato', 'frapp', 'latte']; @@ -100,7 +101,7 @@ describe('Theme system', () => { }); it('should have 7 Editor themes', async () => { - const count = await browser.execute(() => { + const count = await exec(() => { const items = document.querySelectorAll('.dd-item, .dropdown-item'); const editorNames = ['vscode', 'atom', 'monokai', 'dracula', 'nord', 'solarized', 'github']; let edCount = 0; @@ -116,7 +117,7 @@ describe('Theme system', () => { }); it('should have 6 Deep Dark themes', async () => { - const count = await browser.execute(() => { + const count = await exec(() => { const items = document.querySelectorAll('.dd-item, .dropdown-item'); const deepNames = ['tokyo', 'gruvbox', 'ayu', 'poimandres', 'vesper', 'midnight']; let deepCount = 0; @@ -136,7 +137,7 @@ describe('Theme system', () => { await browser.keys('Escape'); await browser.pause(200); - const count = await browser.execute(() => { + const count = await exec(() => { return (document.querySelectorAll('.size-stepper').length || document.querySelectorAll('.font-stepper').length || document.querySelectorAll('.stepper').length); @@ -147,7 +148,7 @@ describe('Theme system', () => { }); it('should show theme action buttons', async () => { - const count = await browser.execute(() => { + const count = await exec(() => { return document.querySelectorAll('.theme-action-btn').length; }); if (count > 0) { @@ -156,7 +157,7 @@ describe('Theme system', () => { }); it('should apply font changes to terminal', async () => { - const fontFamily = await browser.execute(() => { + const fontFamily = await exec(() => { return getComputedStyle(document.documentElement).getPropertyValue('--term-font-family').trim(); }); expect(typeof fontFamily).toBe('string'); diff --git a/tests/e2e/specs/workspace.test.ts b/tests/e2e/specs/workspace.test.ts index d755f56..492ab1b 100644 --- a/tests/e2e/specs/workspace.test.ts +++ b/tests/e2e/specs/workspace.test.ts @@ -6,6 +6,7 @@ */ import { browser, expect } from '@wdio/globals'; +import { exec } from '../helpers/execute.ts'; describe('BTerminal — Workspace & Projects', () => { it('should display the project grid', async () => { @@ -45,7 +46,7 @@ describe('BTerminal — Workspace & Projects', () => { it('should switch project tabs', async () => { // Use JS click — WebDriver clicks don't always trigger Svelte onclick - const switched = await browser.execute(() => { + const switched = await exec(() => { const box = document.querySelector('.project-box') ?? document.querySelector('.project-card'); if (!box) return false; const tabs = box.querySelectorAll('.ptab, .project-tab, .tab-btn'); @@ -62,7 +63,7 @@ describe('BTerminal — Workspace & Projects', () => { expect(text.toLowerCase()).toContain('docs'); // Switch back to Model tab - await browser.execute(() => { + await exec(() => { const box = document.querySelector('.project-box') ?? document.querySelector('.project-card'); const tab = box?.querySelector('.ptab, .project-tab, .tab-btn'); if (tab) (tab as HTMLElement).click(); diff --git a/tests/e2e/specs/worktree.test.ts b/tests/e2e/specs/worktree.test.ts index 9279894..141258f 100644 --- a/tests/e2e/specs/worktree.test.ts +++ b/tests/e2e/specs/worktree.test.ts @@ -4,10 +4,11 @@ import { browser, expect } from '@wdio/globals'; import * as S from '../helpers/selectors.ts'; +import { exec } from '../helpers/execute.ts'; describe('Worktree support', () => { it('should show clone/worktree button', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.CLONE_BTN); expect(typeof exists).toBe('boolean'); @@ -30,7 +31,7 @@ describe('Worktree support', () => { }); it('should show WT badge on worktree sessions', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.WT_BADGE); // Badge only appears when worktree is active @@ -38,14 +39,14 @@ describe('Worktree support', () => { }); it('should show clone group display', async () => { - const exists = await browser.execute((sel: string) => { + const exists = await exec((sel: string) => { return document.querySelector(sel) !== null; }, S.CLONE_GROUP); expect(typeof exists).toBe('boolean'); }); it('should have worktree toggle in settings', async () => { - const hasToggle = await browser.execute(() => { + const hasToggle = await exec(() => { const text = document.body.textContent ?? ''; return text.includes('Worktree') || text.includes('worktree'); }); @@ -53,7 +54,7 @@ describe('Worktree support', () => { }); it('should handle worktree path display', async () => { - const paths = await browser.execute(() => { + const paths = await exec(() => { const headers = document.querySelectorAll('.project-header'); return Array.from(headers).map(h => h.textContent ?? ''); }); @@ -61,7 +62,7 @@ describe('Worktree support', () => { }); it('should show worktree isolation toggle in settings', async () => { - const hasToggle = await browser.execute(() => { + const hasToggle = await exec(() => { return (document.querySelector('.worktree-toggle') ?? document.querySelector('[data-setting="useWorktrees"]')) !== null; }); @@ -70,7 +71,7 @@ describe('Worktree support', () => { it('should preserve worktree badge across tab switches', async () => { // Worktree badge uses display toggle, not {#if} - const badge = await browser.execute((sel: string) => { + const badge = await exec((sel: string) => { const el = document.querySelector(sel); if (!el) return 'absent'; return getComputedStyle(el).display; diff --git a/tests/e2e/wdio.conf.js b/tests/e2e/wdio.conf.js index 68c4ee2..f0b84b9 100644 --- a/tests/e2e/wdio.conf.js +++ b/tests/e2e/wdio.conf.js @@ -233,10 +233,10 @@ export const config = { await browser.waitUntil( async () => { const title = await browser.getTitle(); - const hasKnownEl = await browser.execute(() => - document.querySelector('[data-testid="status-bar"]') !== null - || document.querySelector('.project-grid') !== null - || document.querySelector('.settings-panel') !== null + const hasKnownEl = await browser.execute( + 'return document.querySelector(\'[data-testid="status-bar"]\') !== null' + + ' || document.querySelector(".project-grid") !== null' + + ' || document.querySelector(".settings-panel") !== null' ); return hasKnownEl || title.toLowerCase().includes('agor') || title.toLowerCase().includes('orchestrator'); }, @@ -279,10 +279,12 @@ export const config = { // 1. Check for unexpected error toasts let unexpected = []; try { - const errors = await browser.execute(() => { - const toasts = [...document.querySelectorAll('.toast-error, .load-error')]; - return toasts.map(t => t.textContent?.trim()).filter(Boolean); - }); + const errors = await browser.execute( + 'return (function() {' + + ' var toasts = Array.from(document.querySelectorAll(".toast-error, .load-error"));' + + ' return toasts.map(function(t) { return (t.textContent || "").trim(); }).filter(Boolean);' + + '})()' + ); const expected = browser.__expectedErrors || []; unexpected = errors.filter(e => !expected.some(exp => e.includes(exp))); diff --git a/tests/e2e/wdio.electrobun.conf.js b/tests/e2e/wdio.electrobun.conf.js index db23811..73e5b29 100644 --- a/tests/e2e/wdio.electrobun.conf.js +++ b/tests/e2e/wdio.electrobun.conf.js @@ -113,13 +113,13 @@ export const config = { }, async before() { - // Wait for Electrobun app to render + // Wait for Electrobun app to render — use string script for CDP compatibility await browser.waitUntil( async () => { - const hasEl = await browser.execute(() => - document.querySelector('.app-shell') !== null - || document.querySelector('.project-grid') !== null - || document.querySelector('.status-bar') !== null + const hasEl = await browser.execute( + 'return document.querySelector(".app-shell") !== null' + + ' || document.querySelector(".project-grid") !== null' + + ' || document.querySelector(".status-bar") !== null' ); return hasEl; }, diff --git a/tests/e2e/wdio.shared.conf.js b/tests/e2e/wdio.shared.conf.js index 2634753..c98933d 100644 --- a/tests/e2e/wdio.shared.conf.js +++ b/tests/e2e/wdio.shared.conf.js @@ -74,6 +74,7 @@ export const sharedConfig = { if (db.shouldSkip(specFile, test.title)) { const stats = db.getCacheStats(); console.log(`Skipping (3+ consecutive passes): ${test.title} [${stats.skippable}/${stats.total} skippable]`); + // this.skip() works in Mocha non-arrow context — safe for both protocols this.skip(); } }, @@ -82,10 +83,13 @@ export const sharedConfig = { async afterTest(test, _context, { passed }) { let unexpected = []; try { - const errors = await browser.execute(() => { - const toasts = [...document.querySelectorAll('.toast-error, .load-error')]; - return toasts.map(t => t.textContent?.trim()).filter(Boolean); - }); + // Use string script for cross-protocol compatibility (devtools + webdriver) + const errors = await browser.execute( + 'return (function() {' + + ' var toasts = Array.from(document.querySelectorAll(".toast-error, .load-error"));' + + ' return toasts.map(function(t) { return (t.textContent || "").trim(); }).filter(Boolean);' + + '})()' + ); const expected = browser.__expectedErrors || []; unexpected = errors.filter(e => !expected.some(exp => e.includes(exp))); diff --git a/tests/e2e/wdio.tauri.conf.js b/tests/e2e/wdio.tauri.conf.js index 72257b4..153a347 100644 --- a/tests/e2e/wdio.tauri.conf.js +++ b/tests/e2e/wdio.tauri.conf.js @@ -131,10 +131,10 @@ export const config = { await browser.waitUntil( async () => { const title = await browser.getTitle(); - const hasEl = await browser.execute(() => - document.querySelector('[data-testid="status-bar"]') !== null - || document.querySelector('.project-grid') !== null - || document.querySelector('.settings-panel') !== null + const hasEl = await browser.execute( + 'return document.querySelector(\'[data-testid="status-bar"]\') !== null' + + ' || document.querySelector(".project-grid") !== null' + + ' || document.querySelector(".settings-panel") !== null' ); return hasEl || title.toLowerCase().includes('agor') || title.toLowerCase().includes('orchestrator'); },