feat: unified E2E testing engine — 205 tests, dual-stack support
Infrastructure: - adapters/: base, tauri (port 9750), electrobun (port 9761 + PTY daemon) - helpers/: 120+ centralized selectors, reusable actions, custom assertions - wdio.shared.conf.js + stack-specific configs 18 unified specs (205 tests): splash(6) smoke(15) settings(19) terminal(14) agent(15) search(12) files(15) comms(10) tasks(10) theme(12) groups(12) keyboard(8) notifications(10) diagnostics(8) status-bar(12) context(9) worktree(8) llm-judged(10) Daemon: --stack tauri|electrobun|both flag Scripts: test:e2e:tauri, test:e2e:electrobun, test:e2e:both
This commit is contained in:
parent
1995f03682
commit
77b9ce9f62
31 changed files with 3547 additions and 344 deletions
151
tests/e2e/helpers/actions.ts
Normal file
151
tests/e2e/helpers/actions.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* Reusable test actions — common UI operations used across spec files.
|
||||
*
|
||||
* All actions use browser.execute() for DOM queries where needed
|
||||
* (WebKitGTK reliability pattern).
|
||||
*/
|
||||
|
||||
import { browser } from '@wdio/globals';
|
||||
import * as S from './selectors.ts';
|
||||
|
||||
/** Open settings panel via gear icon click */
|
||||
export async function openSettings(): Promise<void> {
|
||||
await browser.execute(() => {
|
||||
const btn = document.querySelector('[data-testid="settings-btn"]')
|
||||
?? document.querySelector('.sidebar-icon');
|
||||
if (btn) (btn as HTMLElement).click();
|
||||
});
|
||||
const drawer = await browser.$(S.SETTINGS_DRAWER);
|
||||
await drawer.waitForDisplayed({ timeout: 5_000 });
|
||||
}
|
||||
|
||||
/** Close settings panel */
|
||||
export async function closeSettings(): Promise<void> {
|
||||
await browser.execute(() => {
|
||||
const btn = document.querySelector('.settings-close')
|
||||
?? document.querySelector('.panel-close');
|
||||
if (btn) (btn as HTMLElement).click();
|
||||
});
|
||||
await browser.pause(400);
|
||||
}
|
||||
|
||||
/** Switch to a settings category by index (0-based) */
|
||||
export async function switchSettingsCategory(index: number): Promise<void> {
|
||||
await browser.execute((idx: number) => {
|
||||
const tabs = document.querySelectorAll('.settings-tab, .cat-btn');
|
||||
if (tabs[idx]) (tabs[idx] as HTMLElement).click();
|
||||
}, index);
|
||||
await browser.pause(300);
|
||||
}
|
||||
|
||||
/** Switch active group by clicking the nth group button (0-based) */
|
||||
export async function switchGroup(index: number): Promise<void> {
|
||||
await browser.execute((idx: number) => {
|
||||
const groups = document.querySelectorAll('.group-btn:not(.add-group-btn)');
|
||||
if (groups[idx]) (groups[idx] as HTMLElement).click();
|
||||
}, index);
|
||||
await browser.pause(300);
|
||||
}
|
||||
|
||||
/** Type text into the agent prompt input */
|
||||
export async function sendPrompt(text: string): Promise<void> {
|
||||
const textarea = await browser.$(S.CHAT_INPUT_TEXTAREA);
|
||||
if (await textarea.isExisting()) {
|
||||
await textarea.setValue(text);
|
||||
return;
|
||||
}
|
||||
const input = await browser.$(S.CHAT_INPUT_ALT);
|
||||
if (await input.isExisting()) {
|
||||
await input.setValue(text);
|
||||
}
|
||||
}
|
||||
|
||||
/** Open command palette via Ctrl+K */
|
||||
export async function openCommandPalette(): Promise<void> {
|
||||
await browser.keys(['Control', 'k']);
|
||||
await browser.pause(400);
|
||||
}
|
||||
|
||||
/** Close command palette via Escape */
|
||||
export async function closeCommandPalette(): Promise<void> {
|
||||
await browser.keys('Escape');
|
||||
await browser.pause(300);
|
||||
}
|
||||
|
||||
/** Open search overlay via Ctrl+Shift+F */
|
||||
export async function openSearch(): Promise<void> {
|
||||
await browser.keys(['Control', 'Shift', 'f']);
|
||||
await browser.pause(400);
|
||||
}
|
||||
|
||||
/** Close search overlay via Escape */
|
||||
export async function closeSearch(): Promise<void> {
|
||||
await browser.keys('Escape');
|
||||
await browser.pause(300);
|
||||
}
|
||||
|
||||
/** Add a new terminal tab by clicking the add button */
|
||||
export async function addTerminalTab(): Promise<void> {
|
||||
await browser.execute(() => {
|
||||
const btn = document.querySelector('.tab-add-btn');
|
||||
if (btn) (btn as HTMLElement).click();
|
||||
});
|
||||
await browser.pause(500);
|
||||
}
|
||||
|
||||
/** Click a project-level tab (model, docs, files, etc.) */
|
||||
export async function clickProjectTab(tabName: string): Promise<void> {
|
||||
await browser.execute((name: string) => {
|
||||
const tabs = document.querySelectorAll('.project-tab, .tab-btn');
|
||||
for (const tab of tabs) {
|
||||
if ((tab as HTMLElement).textContent?.toLowerCase().includes(name.toLowerCase())) {
|
||||
(tab as HTMLElement).click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, tabName);
|
||||
await browser.pause(300);
|
||||
}
|
||||
|
||||
/** Wait until an element with given selector is displayed */
|
||||
export async function waitForElement(selector: string, timeout = 5_000): Promise<void> {
|
||||
const el = await browser.$(selector);
|
||||
await el.waitForDisplayed({ timeout });
|
||||
}
|
||||
|
||||
/** 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) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return false;
|
||||
const style = getComputedStyle(el);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}, selector);
|
||||
}
|
||||
|
||||
/** 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) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return 'not-found';
|
||||
return getComputedStyle(el).display;
|
||||
}, selector);
|
||||
}
|
||||
|
||||
/** Open notification drawer by clicking bell */
|
||||
export async function openNotifications(): Promise<void> {
|
||||
await browser.execute(() => {
|
||||
const btn = document.querySelector('.notif-btn');
|
||||
if (btn) (btn as HTMLElement).click();
|
||||
});
|
||||
await browser.pause(400);
|
||||
}
|
||||
|
||||
/** Close notification drawer */
|
||||
export async function closeNotifications(): Promise<void> {
|
||||
await browser.execute(() => {
|
||||
const backdrop = document.querySelector('.notif-backdrop');
|
||||
if (backdrop) (backdrop as HTMLElement).click();
|
||||
});
|
||||
await browser.pause(300);
|
||||
}
|
||||
76
tests/e2e/helpers/assertions.ts
Normal file
76
tests/e2e/helpers/assertions.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Custom E2E assertions — domain-specific checks for Agent Orchestrator.
|
||||
*
|
||||
* Uses browser.execute() for DOM queries (WebKitGTK reliability).
|
||||
*/
|
||||
|
||||
import { browser, expect } from '@wdio/globals';
|
||||
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) => {
|
||||
const cards = document.querySelectorAll('.project-card, .project-header');
|
||||
for (const card of cards) {
|
||||
if (card.textContent?.includes(n)) return true;
|
||||
}
|
||||
return false;
|
||||
}, name);
|
||||
expect(found).toBe(true);
|
||||
}
|
||||
|
||||
/** Assert that at least one terminal pane responds (xterm container exists) */
|
||||
export async function assertTerminalResponds(): Promise<void> {
|
||||
const xterm = await browser.$(S.XTERM);
|
||||
if (await xterm.isExisting()) {
|
||||
await expect(xterm).toBeDisplayed();
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) => {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(v).trim();
|
||||
}, varName);
|
||||
expect(value.length).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/** Assert that a settings value persists (read via computed style or DOM) */
|
||||
export async function assertSettingsPersist(selector: string): Promise<void> {
|
||||
const el = await browser.$(selector);
|
||||
if (await el.isExisting()) {
|
||||
await expect(el).toBeDisplayed();
|
||||
}
|
||||
}
|
||||
|
||||
/** Assert the status bar is visible and contains expected sections */
|
||||
export async function assertStatusBarComplete(): Promise<void> {
|
||||
const statusBar = await browser.$(S.STATUS_BAR);
|
||||
await expect(statusBar).toBeDisplayed();
|
||||
}
|
||||
|
||||
/** Assert element count matches expected via DOM query */
|
||||
export async function assertElementCount(
|
||||
selector: string,
|
||||
expected: number,
|
||||
comparison: 'eq' | 'gte' | 'lte' = 'eq',
|
||||
): Promise<void> {
|
||||
const count = await browser.execute((sel: string) => {
|
||||
return document.querySelectorAll(sel).length;
|
||||
}, selector);
|
||||
|
||||
switch (comparison) {
|
||||
case 'eq': expect(count).toBe(expected); break;
|
||||
case 'gte': expect(count).toBeGreaterThanOrEqual(expected); break;
|
||||
case 'lte': expect(count).toBeLessThanOrEqual(expected); break;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 el = document.querySelector(sel);
|
||||
return el?.classList.contains(cls) ?? false;
|
||||
}, selector, className);
|
||||
expect(hasIt).toBe(true);
|
||||
}
|
||||
186
tests/e2e/helpers/selectors.ts
Normal file
186
tests/e2e/helpers/selectors.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* Centralized CSS selectors for all E2E specs.
|
||||
*
|
||||
* Both Tauri (WebKit2GTK via tauri-driver) and Electrobun (WebKitGTK) render
|
||||
* the same Svelte frontend. These selectors work across both stacks.
|
||||
*
|
||||
* Convention: data-testid where available, CSS class fallback.
|
||||
*/
|
||||
|
||||
// ── App Shell ──
|
||||
export const APP_SHELL = '.app-shell';
|
||||
export const WORKSPACE = '.workspace';
|
||||
export const PROJECT_GRID = '.project-grid';
|
||||
|
||||
// ── Sidebar ──
|
||||
export const SIDEBAR = '.sidebar';
|
||||
export const SIDEBAR_RAIL = '[data-testid="sidebar-rail"]';
|
||||
export const SIDEBAR_PANEL = '.sidebar-panel';
|
||||
export const SIDEBAR_ICON = '.sidebar-icon';
|
||||
export const SETTINGS_BTN = '[data-testid="settings-btn"]';
|
||||
export const PANEL_CLOSE = '.panel-close';
|
||||
|
||||
// ── Groups ──
|
||||
export const GROUP_BTN = '.group-btn';
|
||||
export const GROUP_CIRCLE = '.group-circle';
|
||||
export const GROUP_BTN_ACTIVE = '.group-btn.active';
|
||||
export const ADD_GROUP_BTN = '.add-group-btn';
|
||||
export const GROUP_BADGE = '.group-badge';
|
||||
|
||||
// ── Project Cards ──
|
||||
export const PROJECT_CARD = '.project-card';
|
||||
export const PROJECT_HEADER = '.project-header';
|
||||
export const AGOR_TITLE = '.agor-title';
|
||||
|
||||
// ── Status Bar ──
|
||||
export const STATUS_BAR = '[data-testid="status-bar"]';
|
||||
export const STATUS_BAR_CLASS = '.status-bar';
|
||||
export const STATUS_BAR_VERSION = '.status-bar .version';
|
||||
export const BURN_RATE = '.burn-rate';
|
||||
export const AGENT_COUNTS = '.agent-counts';
|
||||
export const ATTENTION_QUEUE = '.attention-queue';
|
||||
export const FLEET_TOKENS = '.fleet-tokens';
|
||||
export const FLEET_COST = '.fleet-cost';
|
||||
export const PROJECT_COUNT = '.project-count';
|
||||
|
||||
// ── Settings ──
|
||||
export const SETTINGS_DRAWER = '.settings-drawer';
|
||||
export const SETTINGS_TAB = '.settings-tab';
|
||||
export const SETTINGS_TAB_ACTIVE = '.settings-tab.active';
|
||||
export const SETTINGS_CLOSE = '.settings-close';
|
||||
export const SETTINGS_CAT_BTN = '.cat-btn';
|
||||
export const THEME_SECTION = '.theme-section';
|
||||
export const FONT_STEPPER = '.font-stepper';
|
||||
export const FONT_DROPDOWN = '.font-dropdown';
|
||||
export const STEP_UP = '.font-stepper .step-up';
|
||||
export const SIZE_VALUE = '.font-stepper .size-value';
|
||||
export const UPDATE_ROW = '.update-row';
|
||||
export const VERSION_LABEL = '.version-label';
|
||||
|
||||
// ── Terminal ──
|
||||
export const TERMINAL_SECTION = '.terminal-section';
|
||||
export const TERMINAL_TABS = '.terminal-tabs';
|
||||
export const TERMINAL_TAB = '.terminal-tab';
|
||||
export const TERMINAL_TAB_ACTIVE = '.terminal-tab.active';
|
||||
export const TAB_ADD_BTN = '.tab-add-btn';
|
||||
export const TAB_CLOSE = '.tab-close';
|
||||
export const TERMINAL_COLLAPSE_BTN = '.terminal-collapse-btn';
|
||||
export const XTERM = '.xterm';
|
||||
export const XTERM_TEXTAREA = '.xterm-helper-textarea';
|
||||
|
||||
// ── Agent ──
|
||||
export const CHAT_INPUT = '.chat-input';
|
||||
export const CHAT_INPUT_TEXTAREA = '.chat-input textarea';
|
||||
export const CHAT_INPUT_ALT = '.chat-input input';
|
||||
export const SEND_BTN = '.send-btn';
|
||||
export const AGENT_MESSAGES = '.agent-messages';
|
||||
export const AGENT_STATUS = '.agent-status';
|
||||
export const AGENT_STATUS_TEXT = '.agent-status .status-text';
|
||||
export const PROVIDER_BADGE = '.provider-badge';
|
||||
export const AGENT_COST = '.agent-cost';
|
||||
export const MODEL_LABEL = '.model-label';
|
||||
export const STOP_BTN = '.stop-btn';
|
||||
|
||||
// ── Search Overlay ──
|
||||
export const OVERLAY_BACKDROP = '.overlay-backdrop';
|
||||
export const OVERLAY_PANEL = '.overlay-panel';
|
||||
export const SEARCH_INPUT = '.search-input';
|
||||
export const NO_RESULTS = '.no-results';
|
||||
export const ESC_HINT = '.esc-hint';
|
||||
export const LOADING_DOT = '.loading-dot';
|
||||
export const RESULTS_LIST = '.results-list';
|
||||
export const GROUP_LABEL = '.group-label';
|
||||
|
||||
// ── Command Palette ──
|
||||
export const PALETTE_BACKDROP = '.palette-backdrop';
|
||||
export const PALETTE_PANEL = '.palette-panel';
|
||||
export const PALETTE_INPUT = '.palette-input';
|
||||
export const PALETTE_ITEM = '.palette-item';
|
||||
export const CMD_LABEL = '.cmd-label';
|
||||
export const CMD_SHORTCUT = '.cmd-shortcut';
|
||||
|
||||
// ── File Browser ──
|
||||
export const FILE_BROWSER = '.file-browser';
|
||||
export const FB_TREE = '.fb-tree';
|
||||
export const FB_VIEWER = '.fb-viewer';
|
||||
export const FB_DIR = '.fb-dir';
|
||||
export const FB_FILE = '.fb-file';
|
||||
export const FB_EMPTY = '.fb-empty';
|
||||
export const FB_CHEVRON = '.fb-chevron';
|
||||
export const FB_EDITOR_HEADER = '.fb-editor-header';
|
||||
export const FB_IMAGE_WRAP = '.fb-image-wrap';
|
||||
export const FB_ERROR = '.fb-error';
|
||||
export const FILE_TYPE = '.file-type';
|
||||
|
||||
// ── Communications ──
|
||||
export const COMMS_TAB = '.comms-tab';
|
||||
export const COMMS_MODE_BAR = '.comms-mode-bar';
|
||||
export const MODE_BTN = '.mode-btn';
|
||||
export const MODE_BTN_ACTIVE = '.mode-btn.active';
|
||||
export const COMMS_SIDEBAR = '.comms-sidebar';
|
||||
export const CH_HASH = '.ch-hash';
|
||||
export const COMMS_MESSAGES = '.comms-messages';
|
||||
export const MSG_INPUT_BAR = '.msg-input-bar';
|
||||
export const MSG_SEND_BTN = '.msg-send-btn';
|
||||
|
||||
// ── Task Board ──
|
||||
export const TASK_BOARD = '.task-board';
|
||||
export const TB_TITLE = '.tb-title';
|
||||
export const TB_COLUMN = '.tb-column';
|
||||
export const TB_COL_LABEL = '.tb-col-label';
|
||||
export const TB_COL_COUNT = '.tb-col-count';
|
||||
export const TB_ADD_BTN = '.tb-add-btn';
|
||||
export const TB_CREATE_FORM = '.tb-create-form';
|
||||
export const TB_COUNT = '.tb-count';
|
||||
|
||||
// ── Theme ──
|
||||
export const DD_BTN = '.dd-btn';
|
||||
export const DD_LIST = '.dd-list';
|
||||
export const DD_GROUP_LABEL = '.dd-group-label';
|
||||
export const DD_ITEM = '.dd-item';
|
||||
export const DD_ITEM_SELECTED = '.dd-item.selected';
|
||||
export const SIZE_STEPPER = '.size-stepper';
|
||||
export const THEME_ACTION_BTN = '.theme-action-btn';
|
||||
|
||||
// ── Notifications ──
|
||||
export const NOTIF_BTN = '.notif-btn';
|
||||
export const NOTIF_DRAWER = '.notif-drawer';
|
||||
export const DRAWER_TITLE = '.drawer-title';
|
||||
export const CLEAR_BTN = '.clear-btn';
|
||||
export const NOTIF_EMPTY = '.notif-empty';
|
||||
export const NOTIF_ITEM = '.notif-item';
|
||||
export const NOTIF_BACKDROP = '.notif-backdrop';
|
||||
|
||||
// ── Splash ──
|
||||
export const SPLASH = '.splash';
|
||||
export const LOGO_TEXT = '.logo-text';
|
||||
export const SPLASH_VERSION = '.splash .version';
|
||||
export const SPLASH_DOT = '.splash .dot';
|
||||
|
||||
// ── Diagnostics ──
|
||||
export const DIAGNOSTICS = '.diagnostics';
|
||||
export const DIAG_HEADING = '.diagnostics .sh';
|
||||
export const DIAG_KEY = '.diag-key';
|
||||
export const DIAG_LABEL = '.diag-label';
|
||||
export const DIAG_FOOTER = '.diag-footer';
|
||||
export const REFRESH_BTN = '.refresh-btn';
|
||||
|
||||
// ── Right Bar (Electrobun) ──
|
||||
export const RIGHT_BAR = '.right-bar';
|
||||
export const CLOSE_BTN = '.close-btn';
|
||||
|
||||
// ── Context Tab ──
|
||||
export const CONTEXT_TAB = '.context-tab';
|
||||
export const TOKEN_METER = '.token-meter';
|
||||
export const FILE_REFS = '.file-refs';
|
||||
export const TURN_COUNT = '.turn-count';
|
||||
|
||||
// ── Worktree ──
|
||||
export const CLONE_BTN = '.clone-btn';
|
||||
export const BRANCH_DIALOG = '.branch-dialog';
|
||||
export const WT_BADGE = '.wt-badge';
|
||||
export const CLONE_GROUP = '.clone-group';
|
||||
|
||||
// ── Toast / Errors ──
|
||||
export const TOAST_ERROR = '.toast-error';
|
||||
export const LOAD_ERROR = '.load-error';
|
||||
Loading…
Add table
Add a link
Reference in a new issue