fix(e2e): cross-protocol browser.execute() — works with both WebDriver + CDP

Root cause: WebDriverIO devtools protocol wraps functions in a polyfill
that puts `return` inside eval() (not a function body) → "Illegal return".

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

- 35 spec files updated (browser.execute → exec)
- 4 config files updated (string-form expressions)
- helpers/actions.ts + assertions.ts updated
- 560 vitest + 116 cargo passing
This commit is contained in:
Hibryda 2026-03-22 06:33:55 +01:00
parent 407e49cc32
commit 6a8181f33a
42 changed files with 630 additions and 541 deletions

View file

@ -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<void>
/** Open settings panel via gear icon click */
export async function openSettings(): Promise<void> {
// 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<void> {
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<void> {
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<boolean>,
),
{ timeout: 5_000 },
);
}
/** Close settings panel */
export async function closeSettings(): Promise<void> {
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<void> {
/** Switch to a settings category by index (0-based) */
export async function switchSettingsCategory(index: number): Promise<void> {
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<void> {
/** Switch active group by clicking the nth group button (0-based) */
export async function switchGroup(index: number): Promise<void> {
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<void> {
/** Add a new terminal tab by clicking the add button */
export async function addTerminalTab(): Promise<void> {
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<void> {
/** Click a project-level tab (model, docs, files, etc.) */
export async function clickProjectTab(tabName: string): Promise<void> {
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<boolean> {
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<boolean> {
/** Get the display CSS value for an element (for display-toggle awareness) */
export async function getDisplay(selector: string): Promise<string> {
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<string> {
/** Open notification drawer by clicking bell */
export async function openNotifications(): Promise<void> {
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<void> {
/** Close notification drawer */
export async function closeNotifications(): Promise<void> {
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();

View file

@ -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<void> {
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<void> {
/** Assert that a CSS custom property has changed after a theme switch */
export async function assertThemeApplied(varName = '--ctp-base'): Promise<void> {
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<void> {
export async function assertStatusBarComplete(): Promise<void> {
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<boolean>,
}),
{ 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<void> {
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<void> {
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);

View file

@ -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<R>(fn: (...args: any[]) => R, ...args: any[]): Promise<R> {
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<R>;
}
/**
* 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();
}

View file

@ -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')); }
});

View file

@ -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);

View file

@ -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();

View file

@ -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<number> {
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;
});

View file

@ -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<void> {
@ -15,7 +16,7 @@ async function resetToHomeState(): Promise<void> {
async function closeSettings(): Promise<void> {
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<void> {
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<void> {
/** Close command palette if open — uses backdrop click (more reliable than Escape). */
async function closeCommandPalette(): Promise<void> {
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();
});

View file

@ -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');
});

View file

@ -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;

View file

@ -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';

View file

@ -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() ?? '';

View file

@ -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;

View file

@ -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<void> {
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<void> {
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<string> {
return browser.execute(() => {
return exec(() => {
const el = document.querySelector('[data-testid="agent-pane"]');
return el?.getAttribute('data-agent-status') ?? 'unknown';
});
}
async function getMessageCount(): Promise<number> {
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<number> {
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();
});

View file

@ -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<boolean>,
{ 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();
});

View file

@ -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;

View file

@ -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<string[]> {
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<void> {
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<void> {
}
async function switchProjectTab(id: string, tabIndex: number): Promise<void> {
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<void> {
}
async function getAgentStatus(id: string): Promise<string> {
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);

View file

@ -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<string[]> {
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<void> {
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<string> {
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<void> {
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<string> {
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<void> {
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;

View file

@ -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 ?? '';
});

View file

@ -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<string[]> {
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<string[]> {
/** Switch to a tab in a specific project box. */
async function switchProjectTab(projectId: string, tabIndex: number): Promise<void> {
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() ?? '');

View file

@ -1,4 +1,5 @@
import { browser, expect } from '@wdio/globals';
import { exec } from '../helpers/execute.ts';
// Phase C — UI Interaction Tests (C1C4)
// Command palette, search overlay, notification center, keyboard navigation.
@ -7,7 +8,7 @@ import { browser, expect } from '@wdio/globals';
/** Open command palette via Ctrl+K. */
async function openPalette(): Promise<void> {
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<string[]> {
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;
});

View file

@ -1,4 +1,5 @@
import { browser, expect } from '@wdio/globals';
import { exec } from '../helpers/execute.ts';
// Phase D — Error Handling UI Tests (D4D5)
// Tests toast notifications, notification center, and error state handling.
@ -10,14 +11,14 @@ async function resetToHomeState(): Promise<void> {
// 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) {

View file

@ -1,4 +1,5 @@
import { browser, expect } from '@wdio/globals';
import { exec } from '../helpers/execute.ts';
// Phase D — Settings Panel Tests (D1D3)
// 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<void> {
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<void> {
async function closeSettings(): Promise<void> {
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<void> {
}
async function clickCategory(label: string): Promise<void> {
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<void> {
}
async function scrollToTop(): Promise<void> {
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);

View file

@ -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<void> {
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<void> {
}
async function getActiveTabText(): Promise<string> {
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() ?? '' };
});

View file

@ -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<void> {
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<void> {
/** Get the active tab text in the first project box. */
async function getActiveTabText(): Promise<string> {
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<string> {
/** Check if a tab with given text exists in any project box. */
async function tabExistsWithText(tabText: string): Promise<boolean> {
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 {

View file

@ -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 (F4F7)
// Settings completeness, theme system quality, error handling, and UI consistency.
@ -11,7 +12,7 @@ async function openSettings(): Promise<void> {
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<void> {
async function closeSettings(): Promise<void> {
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<void> {
/** Click a settings category by label text. */
async function clickSettingsCategory(label: string): Promise<boolean> {
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<boolean> {
/** Get visible text content of settings content area. */
async function getSettingsContent(): Promise<string> {
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

View file

@ -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 (F1F3)
// 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<string | null> {
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<string | null> {
/** Switch to a tab in the first project box by tab text label. */
async function switchProjectTabByLabel(projectId: string, label: string): Promise<void> {
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');

View file

@ -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();

View file

@ -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<number> {
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');
});

View file

@ -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<boolean>,
{ 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

View file

@ -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;

View file

@ -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);
});
});

View file

@ -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]');
});

View file

@ -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<void> {
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<void> {
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<void> {
}
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<void> {
async function closeSettings(): Promise<void> {
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();
});

View file

@ -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;
});

View file

@ -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');

View file

@ -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();

View file

@ -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;

View file

@ -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)));

View file

@ -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;
},

View file

@ -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)));

View file

@ -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');
},