diff --git a/tests/e2e/specs/phase-d-errors.test.ts b/tests/e2e/specs/phase-d-errors.test.ts new file mode 100644 index 0000000..f53b095 --- /dev/null +++ b/tests/e2e/specs/phase-d-errors.test.ts @@ -0,0 +1,190 @@ +import { browser, expect } from '@wdio/globals'; + +// Phase D — Error Handling UI Tests (D4–D5) +// Tests toast notifications, notification center, and error state handling. + +// ─── Helpers ────────────────────────────────────────────────────────── + +/** Close any open overlays/panels to reset UI state. */ +async function resetToHomeState(): Promise { + // Close settings panel if open + const panel = await browser.$('.sidebar-panel'); + if (await panel.isDisplayed().catch(() => false)) { + await browser.execute(() => { + 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(() => { + const panel = document.querySelector('[data-testid="notification-panel"]'); + if (panel) { + const backdrop = document.querySelector('.notification-center .backdrop'); + if (backdrop) (backdrop as HTMLElement).click(); + } + }); + await browser.pause(200); + // Dismiss search overlay + const overlay = await browser.$('.search-overlay'); + if (await overlay.isExisting()) await browser.keys('Escape'); + await browser.pause(200); +} + +// ─── Scenario D4: Toast Notifications ──────────────────────────────── + +describe('Scenario D4 — Toast Notifications', () => { + before(async () => { + await resetToHomeState(); + }); + + it('should render ToastContainer in the app', async () => { + const container = await browser.$('.toast-container'); + await expect(container).toBeExisting(); + }); + + it('should display notification center bell icon', async () => { + const bell = await browser.$('[data-testid="notification-bell"]'); + await expect(bell).toBeDisplayed(); + }); + + it('should show notification dropdown when bell clicked', async () => { + // Click bell via JS for reliability + await browser.execute(() => { + const bell = document.querySelector('[data-testid="notification-bell"]'); + if (bell) (bell as HTMLElement).click(); + }); + await browser.pause(500); + + const panel = await browser.$('[data-testid="notification-panel"]'); + await expect(panel).toBeDisplayed(); + + // Verify panel has a title + const title = await browser.execute(() => { + const el = document.querySelector('[data-testid="notification-panel"] .panel-title'); + return el?.textContent?.trim() ?? ''; + }); + expect(title).toBe('Notifications'); + }); + + it('should show panel actions area in notification center', async () => { + // Panel should still be open from previous test + const panelExists = await browser.execute(() => { + return document.querySelector('[data-testid="notification-panel"]') !== null; + }); + if (!panelExists) { + await browser.execute(() => { + const bell = document.querySelector('[data-testid="notification-bell"]'); + if (bell) (bell as HTMLElement).click(); + }); + await browser.pause(500); + } + + // Verify panel-actions div exists (buttons may be conditional on having notifications) + const actionsDiv = await browser.$('[data-testid="notification-panel"] .panel-actions'); + await expect(actionsDiv).toBeExisting(); + + // Verify the panel list area exists (may show empty state) + const list = await browser.$('[data-testid="notification-panel"] .panel-list'); + await expect(list).toBeExisting(); + + // Close panel + await browser.execute(() => { + const backdrop = document.querySelector('.notification-center .backdrop'); + if (backdrop) (backdrop as HTMLElement).click(); + }); + await browser.pause(300); + }); + + it('should close notification panel on Escape', async () => { + // Open panel + await browser.execute(() => { + const bell = document.querySelector('[data-testid="notification-bell"]'); + if (bell) (bell as HTMLElement).click(); + }); + await browser.pause(400); + + const panelBefore = await browser.$('[data-testid="notification-panel"]'); + await expect(panelBefore).toBeDisplayed(); + + // Press Escape + await browser.keys('Escape'); + await browser.pause(400); + + // Panel should be gone + const panelAfter = await browser.execute(() => { + return document.querySelector('[data-testid="notification-panel"]') !== null; + }); + expect(panelAfter).toBe(false); + }); +}); + +// ─── Scenario D5: Error States ─────────────────────────────────────── + +describe('Scenario D5 — Error States', () => { + before(async () => { + await resetToHomeState(); + }); + + it('should not show any loadError warnings on fresh launch', async () => { + // Check that no .load-error elements are visible + const loadErrors = await browser.$$('.load-error'); + let visibleCount = 0; + for (const el of loadErrors) { + if (await el.isDisplayed().catch(() => false)) { + visibleCount++; + } + } + expect(visibleCount).toBe(0); + }); + + it('should show status bar with agent state indicators', async () => { + const statusBar = await browser.$('[data-testid="status-bar"]'); + await expect(statusBar).toBeDisplayed(); + + // Verify status bar contains project count text + const text = await statusBar.getText(); + expect(text).toContain('projects'); + }); + + it('should show notification center in a functional state', async () => { + const center = await browser.$('[data-testid="notification-center"]'); + await expect(center).toBeDisplayed(); + + // Bell should be clickable without errors + await browser.execute(() => { + const bell = document.querySelector('[data-testid="notification-bell"]'); + if (bell) (bell as HTMLElement).click(); + }); + await browser.pause(400); + + // Verify panel rendered without crash + const panel = await browser.$('[data-testid="notification-panel"]'); + await expect(panel).toBeDisplayed(); + + // Close + await browser.execute(() => { + const backdrop = document.querySelector('.notification-center .backdrop'); + if (backdrop) (backdrop as HTMLElement).click(); + }); + await browser.pause(200); + }); + + it('should render project boxes without error indicators', async () => { + const boxes = await browser.$$('[data-testid="project-box"]'); + expect(boxes.length).toBeGreaterThanOrEqual(1); + + // Verify no project box has an error overlay or error class + const errorBoxes = await browser.execute(() => { + const boxes = document.querySelectorAll('[data-testid="project-box"]'); + let errorCount = 0; + for (const box of boxes) { + if (box.querySelector('.project-error') || box.classList.contains('error')) { + errorCount++; + } + } + return errorCount; + }); + expect(errorBoxes).toBe(0); + }); +}); diff --git a/tests/e2e/specs/phase-d-settings.test.ts b/tests/e2e/specs/phase-d-settings.test.ts new file mode 100644 index 0000000..05654a8 --- /dev/null +++ b/tests/e2e/specs/phase-d-settings.test.ts @@ -0,0 +1,227 @@ +import { browser, expect } from '@wdio/globals'; + +// Phase D — Settings Panel Tests (D1–D3) +// Tests the redesigned VS Code-style settings panel with 6+1 category tabs, +// appearance controls, and theme editor. + +// ─── Helpers ────────────────────────────────────────────────────────── + +async function openSettings(): Promise { + const panel = await browser.$('.settings-panel'); + if (!(await panel.isDisplayed().catch(() => false))) { + await browser.execute(() => { + 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, + { timeout: 5000, timeoutMsg: 'Settings panel did not render within 5s' }, + ); + await browser.pause(300); +} + +async function closeSettings(): Promise { + const panel = await browser.$('.sidebar-panel'); + if (await panel.isDisplayed().catch(() => false)) { + await browser.execute(() => { + const btn = document.querySelector('.settings-close') || document.querySelector('.panel-close'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(500); + } +} + +async function clickCategory(label: string): Promise { + await browser.execute((lbl) => { + const items = document.querySelectorAll('.sidebar-item'); + for (const el of items) { + if (el.textContent?.includes(lbl)) { (el as HTMLElement).click(); return; } + } + }, label); + await browser.pause(300); +} + +async function scrollToTop(): Promise { + await browser.execute(() => { document.querySelector('.settings-content')?.scrollTo(0, 0); }); + await browser.pause(200); +} + +// ─── Scenario D1: Settings Panel Categories ────────────────────────── + +describe('Scenario D1 — Settings Panel Categories', () => { + before(async () => { await openSettings(); }); + after(async () => { await closeSettings(); }); + + it('should render settings sidebar with 6+ category buttons', async () => { + const sidebar = await browser.$('.settings-sidebar'); + await expect(sidebar).toBeDisplayed(); + const items = await browser.$$('.sidebar-item'); + expect(items.length).toBeGreaterThanOrEqual(6); + }); + + it('should switch between all 6 categories', async () => { + for (const cat of ['Appearance', 'Agents', 'Security', 'Projects', 'Orchestration', 'Advanced']) { + await clickCategory(cat); + const content = await browser.$('.settings-content'); + await expect(content).toBeDisplayed(); + } + await clickCategory('Appearance'); + }); + + it('should highlight active category with blue accent', async () => { + await clickCategory('Agents'); + const activeItem = await browser.$('.sidebar-item.active'); + await expect(activeItem).toBeExisting(); + expect(await activeItem.getText()).toContain('Agents'); + await clickCategory('Appearance'); + }); + + it('should show search bar and filter results', async () => { + await expect(await browser.$('.settings-search')).toBeDisplayed(); + await browser.execute(() => { + 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 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(() => { + const input = document.querySelector('.settings-search') as HTMLInputElement; + if (input) { input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); } + }); + await browser.pause(300); + }); +}); + +// ─── Scenario D2: Appearance Settings ──────────────────────────────── + +describe('Scenario D2 — Appearance Settings', () => { + before(async () => { await openSettings(); await clickCategory('Appearance'); await scrollToTop(); }); + after(async () => { await closeSettings(); }); + + it('should show theme dropdown with 17+ built-in themes grouped by category', async () => { + await browser.execute(() => { + const btn = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(500); + const groupLabels = await browser.$$('.theme-menu .group-label'); + expect(groupLabels.length).toBeGreaterThanOrEqual(3); + const items = await browser.$$('.theme-menu .dropdown-item'); + expect(items.length).toBeGreaterThanOrEqual(17); + // Close dropdown + await browser.execute(() => { + const btn = document.querySelector('.appearance .custom-dropdown .dropdown-btn'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(300); + }); + + 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 sizeBefore = parseInt(before as string, 10); + await browser.execute(() => { + 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 ?? ''); + expect(parseInt(after as string, 10)).toBe(sizeBefore + 1); + // Revert + await browser.execute(() => { + const btns = document.querySelectorAll('.stepper button'); + if (btns.length >= 1) (btns[0] as HTMLElement).click(); + }); + await browser.pause(200); + }); + + it('should show terminal cursor style selector (Block/Line/Underline)', async () => { + await browser.execute(() => { + document.getElementById('setting-cursor-style')?.scrollIntoView({ behavior: 'instant', block: 'center' }); + }); + await browser.pause(300); + const segmented = await browser.$('.segmented'); + await expect(segmented).toBeDisplayed(); + const buttons = await browser.$$('.segmented button'); + expect(buttons.length).toBe(3); + const activeText = await browser.execute(() => + document.querySelector('.segmented button.active')?.textContent?.trim() ?? '', + ); + expect(activeText).toBe('Block'); + }); + + it('should show scrollback lines input', async () => { + await browser.execute(() => { + document.getElementById('setting-scrollback')?.scrollIntoView({ behavior: 'instant', block: 'center' }); + }); + await browser.pause(300); + const input = await browser.$('#setting-scrollback input[type="number"]'); + await expect(input).toBeExisting(); + const num = parseInt(await input.getValue() as string, 10); + expect(num).toBeGreaterThanOrEqual(100); + expect(num).toBeLessThanOrEqual(100000); + }); +}); + +// ─── Scenario D3: Theme Editor ─────────────────────────────────────── + +describe('Scenario D3 — Theme Editor', () => { + before(async () => { await openSettings(); await clickCategory('Appearance'); await scrollToTop(); }); + after(async () => { + await browser.execute(() => { + const btn = Array.from(document.querySelectorAll('.editor .btn')) + .find(b => b.textContent?.trim() === 'Cancel'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(300); + await closeSettings(); + }); + + it('should show "+ New Custom Theme" button', async () => { + const btn = await browser.$('.new-theme-btn'); + await expect(btn).toBeDisplayed(); + expect(await btn.getText()).toContain('New Custom Theme'); + }); + + it('should open theme editor with color pickers when clicked', async () => { + await browser.execute(() => { + const btn = document.querySelector('.new-theme-btn'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(500); + const editor = await browser.$('.editor'); + await expect(editor).toBeDisplayed(); + const colorInputs = await browser.$$('.editor input[type="color"]'); + expect(colorInputs.length).toBeGreaterThan(0); + const nameInput = await browser.$('.editor .name-input'); + await expect(nameInput).toBeExisting(); + }); + + it('should show 26 color pickers grouped in Accents and Neutrals', async () => { + const groups = await browser.$$('.editor details.group'); + expect(groups.length).toBe(2); + const colorRows = await browser.$$('.editor .color-row'); + expect(colorRows.length).toBe(26); + }); + + it('should have Cancel and Save buttons', async () => { + const hasCancel = await browser.execute(() => + Array.from(document.querySelectorAll('.editor .footer .btn')).some(b => b.textContent?.trim() === 'Cancel'), + ); + expect(hasCancel).toBe(true); + const hasSave = await browser.execute(() => + Array.from(document.querySelectorAll('.editor .footer .btn')).some(b => b.textContent?.trim() === 'Save'), + ); + expect(hasSave).toBe(true); + }); +}); diff --git a/tests/e2e/specs/phase-e-agents.test.ts b/tests/e2e/specs/phase-e-agents.test.ts new file mode 100644 index 0000000..c3d4eb5 --- /dev/null +++ b/tests/e2e/specs/phase-e-agents.test.ts @@ -0,0 +1,259 @@ +import { browser, expect } from '@wdio/globals'; + +// Phase E — Part 1: Multi-agent orchestration, project tabs, and provider UI. +// Tests ProjectBox tab bar, AgentPane state, provider config, status bar fleet state. + +// ─── Helpers ────────────────────────────────────────────────────────── + +async function clickTabByText(tabText: string): Promise { + await browser.execute((text) => { + const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); + for (const tab of tabs) { + if (tab.textContent?.trim() === text) { + (tab as HTMLElement).click(); + return; + } + } + }, tabText); + await browser.pause(400); +} + +async function getActiveTabText(): Promise { + return browser.execute(() => { + const box = document.querySelector('[data-testid="project-box"]'); + const active = box?.querySelector('[data-testid="project-tabs"] .ptab.active'); + return active?.textContent?.trim() ?? ''; + }); +} + +// ─── Scenario E1: ProjectBox Tab Bar ───────────────────────────────── + +describe('Scenario E1 — ProjectBox Tab Bar', () => { + before(async () => { + await browser.waitUntil( + async () => { + const count = await browser.execute(() => + document.querySelectorAll('[data-testid="project-box"]').length, + ); + return (count as number) >= 1; + }, + { timeout: 10_000, timeoutMsg: 'No project boxes rendered within 10s' }, + ); + await clickTabByText('Model'); + }); + + it('should render project-level tab bar with at least 7 tabs', async () => { + const tabCount = await browser.execute(() => { + const box = document.querySelector('[data-testid="project-box"]'); + return box?.querySelectorAll('[data-testid="project-tabs"] .ptab')?.length ?? 0; + }); + expect(tabCount).toBeGreaterThanOrEqual(7); + }); + + it('should include expected base tab labels', async () => { + const tabTexts = await browser.execute(() => { + 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() ?? ''); + }); + for (const label of ['Model', 'Docs', 'Context', 'Files', 'SSH', 'Memory', 'Metrics']) { + expect(tabTexts).toContain(label); + } + }); + + it('should switch to Files tab and activate it', async () => { + await clickTabByText('Files'); + expect(await getActiveTabText()).toBe('Files'); + }); + + it('should switch to Memory tab and activate it', async () => { + await clickTabByText('Memory'); + expect(await getActiveTabText()).toBe('Memory'); + }); + + it('should switch back to Model and show agent session pane', async () => { + await clickTabByText('Model'); + expect(await getActiveTabText()).toBe('Model'); + const session = await browser.$('[data-testid="agent-session"]'); + await expect(session).toBeDisplayed(); + }); + + it('should use PERSISTED-LAZY mount (Files content persists across tab switches)', async () => { + await clickTabByText('Files'); + await browser.pause(300); + await clickTabByText('Model'); + await browser.pause(200); + 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 box = document.querySelector('[data-testid="project-box"]'); + return box?.querySelectorAll('.content-pane')?.length ?? 0; + }); + expect(paneCount).toBeGreaterThan(1); + await clickTabByText('Model'); + }); +}); + +// ─── Scenario E2: Agent Session UI ─────────────────────────────────── + +describe('Scenario E2 — Agent Session UI', () => { + before(async () => { + await clickTabByText('Model'); + await browser.pause(300); + }); + + it('should show agent pane with prompt input area', async () => { + const pane = await browser.$('[data-testid="agent-pane"]'); + await expect(pane).toBeExisting(); + const prompt = await browser.$('[data-testid="agent-prompt"]'); + await expect(prompt).toBeExisting(); + }); + + it('should show submit button', async () => { + const btn = await browser.$('[data-testid="agent-submit"]'); + await expect(btn).toBeExisting(); + }); + + it('should show agent messages area', async () => { + const messages = await browser.$('[data-testid="agent-messages"]'); + await expect(messages).toBeExisting(); + }); + + it('should show agent pane in idle status initially', async () => { + const status = await browser.execute(() => { + const el = document.querySelector('[data-testid="agent-pane"]'); + return el?.getAttribute('data-agent-status') ?? 'unknown'; + }); + expect(status).toBe('idle'); + }); + + it('should show CWD in ProjectHeader', async () => { + const cwd = await browser.execute(() => { + const header = document.querySelector('.project-header'); + return header?.querySelector('.info-cwd')?.textContent?.trim() ?? ''; + }); + expect(cwd.length).toBeGreaterThan(0); + }); + + it('should show profile name in ProjectHeader if configured', async () => { + const profileInfo = await browser.execute(() => { + const el = document.querySelector('.project-header .info-profile'); + return { exists: el !== null, text: el?.textContent?.trim() ?? '' }; + }); + if (profileInfo.exists) { + expect(profileInfo.text.length).toBeGreaterThan(0); + } + }); +}); + +// ─── Scenario E3: Provider Configuration ───────────────────────────── + +describe('Scenario E3 — Provider Configuration', () => { + before(async () => { + await browser.execute(() => { + const btn = document.querySelector('[data-testid="settings-btn"]'); + if (btn) (btn as HTMLElement).click(); + }); + const panel = await browser.$('.sidebar-panel'); + await panel.waitForDisplayed({ timeout: 5000 }); + await browser.pause(500); + }); + + it('should show Providers section in Settings', async () => { + const hasProviders = await browser.execute(() => { + const headers = document.querySelectorAll('.settings-section h2'); + return Array.from(headers).some((h) => h.textContent?.trim() === 'Providers'); + }); + expect(hasProviders).toBe(true); + }); + + it('should show at least one provider panel', async () => { + const count = await browser.execute(() => + document.querySelectorAll('.provider-panel').length, + ); + expect(count).toBeGreaterThanOrEqual(1); + }); + + it('should show provider name in panel header', async () => { + const name = await browser.execute(() => { + const panel = document.querySelector('.provider-panel'); + return panel?.querySelector('.provider-name')?.textContent?.trim() ?? ''; + }); + expect(name.length).toBeGreaterThan(0); + }); + + it('should expand provider panel to show enabled toggle', async () => { + await browser.execute(() => { + const header = document.querySelector('.provider-header'); + if (header) (header as HTMLElement).click(); + }); + await browser.pause(300); + const hasToggle = await browser.execute(() => { + const body = document.querySelector('.provider-body'); + return body?.querySelector('.toggle-switch') !== null; + }); + expect(hasToggle).toBe(true); + }); + + after(async () => { + await browser.execute(() => { + const header = document.querySelector('.provider-header'); + const expanded = document.querySelector('.provider-body'); + if (expanded && header) (header as HTMLElement).click(); + }); + await browser.pause(200); + await browser.keys('Escape'); + const panel = await browser.$('.sidebar-panel'); + await panel.waitForDisplayed({ timeout: 3000, reverse: true }); + }); +}); + +// ─── Scenario E4: Status Bar Fleet State ───────────────────────────── + +describe('Scenario E4 — Status Bar Fleet State', () => { + it('should render status bar', async () => { + const bar = await browser.$('[data-testid="status-bar"]'); + await expect(bar).toBeDisplayed(); + }); + + it('should show project count', async () => { + const text = await browser.execute(() => { + 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 text = document.querySelector('[data-testid="status-bar"]')?.textContent ?? ''; + return text.includes('idle') || text.includes('running') || text.includes('projects'); + }); + expect(hasState).toBe(true); + }); + + it('should not show burn rate when all agents idle', async () => { + const burnRate = await browser.execute(() => { + const bar = document.querySelector('[data-testid="status-bar"]'); + return bar?.querySelector('.burn-rate')?.textContent ?? null; + }); + if (burnRate !== null) { + expect(burnRate).toMatch(/\$0|0\.00/); + } + }); + + it('should show notification center bell', async () => { + const bell = await browser.$('[data-testid="notification-bell"]'); + await expect(bell).toBeExisting(); + }); + + it('should conditionally show attention queue button', async () => { + const info = await browser.execute(() => { + const btn = document.querySelector('[data-testid="status-bar"] .attention-btn'); + return { exists: btn !== null, text: btn?.textContent?.trim() ?? '' }; + }); + if (info.exists) { + expect(info.text).toContain('attention'); + } + }); +}); diff --git a/tests/e2e/specs/phase-e-health.test.ts b/tests/e2e/specs/phase-e-health.test.ts new file mode 100644 index 0000000..a026e7d --- /dev/null +++ b/tests/e2e/specs/phase-e-health.test.ts @@ -0,0 +1,296 @@ +import { browser, expect } from '@wdio/globals'; + +// + + +/** Switch to a tab by text content in the first project box. */ +async function clickTabByText(tabText: string): Promise { + await browser.execute((text) => { + const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); + for (const tab of tabs) { + if (tab.textContent?.trim() === text) { + (tab as HTMLElement).click(); + return; + } + } + }, tabText); + await browser.pause(400); +} + +/** Get the active tab text in the first project box. */ +async function getActiveTabText(): Promise { + return browser.execute(() => { + const box = document.querySelector('[data-testid="project-box"]'); + const active = box?.querySelector('[data-testid="project-tabs"] .ptab.active'); + return active?.textContent?.trim() ?? ''; + }); +} + +/** Check if a tab with given text exists in any project box. */ +async function tabExistsWithText(tabText: string): Promise { + return browser.execute((text) => { + const tabs = document.querySelectorAll('[data-testid="project-tabs"] .ptab'); + return Array.from(tabs).some((t) => t.textContent?.trim() === text); + }, tabText); +} + + +describe('Scenario E5 — Project Health Indicators', () => { + before(async () => { + await browser.waitUntil( + async () => { + const count = await browser.execute(() => + document.querySelectorAll('[data-testid="project-box"]').length, + ); + return (count as number) >= 1; + }, + { timeout: 10_000, timeoutMsg: 'No project boxes rendered within 10s' }, + ); + await clickTabByText('Model'); + }); + + it('should show status dot in ProjectHeader', async () => { + const statusDot = await browser.execute(() => { + const header = document.querySelector('.project-header'); + const dot = header?.querySelector('.status-dot'); + return { + exists: dot !== null, + classes: dot?.className ?? '', + }; + }); + expect(statusDot.exists).toBe(true); + expect(statusDot.classes).toContain('status-dot'); + }); + + it('should show status dot with appropriate state class', async () => { + const dotClass = await browser.execute(() => { + const header = document.querySelector('.project-header'); + const dot = header?.querySelector('.status-dot'); + return dot?.className ?? ''; + }); + expect(dotClass).toContain('status-dot'); + }); + + it('should show CWD path in ProjectHeader info area', async () => { + const cwdText = await browser.execute(() => { + const header = document.querySelector('.project-header'); + const cwd = header?.querySelector('.info-cwd'); + return cwd?.textContent?.trim() ?? ''; + }); + expect(cwdText.length).toBeGreaterThan(0); + }); + + it('should have context pressure info element when pressure exists', async () => { + const ctxInfo = await browser.execute(() => { + const header = document.querySelector('.project-header'); + const ctx = header?.querySelector('.info-ctx'); + return { + exists: ctx !== null, + text: ctx?.textContent?.trim() ?? '', + }; + }); + if (ctxInfo.exists) { + expect(ctxInfo.text).toMatch(/ctx \d+%/); + } + }); + + it('should have burn rate info element when agents are active', async () => { + const rateInfo = await browser.execute(() => { + const header = document.querySelector('.project-header'); + const rate = header?.querySelector('.info-rate'); + return { + exists: rate !== null, + text: rate?.textContent?.trim() ?? '', + }; + }); + if (rateInfo.exists) { + expect(rateInfo.text).toMatch(/\$[\d.]+\/hr/); + } + }); + + it('should render ProjectHeader with all structural elements', async () => { + const structure = await browser.execute(() => { + const header = document.querySelector('.project-header'); + return { + hasMain: header?.querySelector('.header-main') !== null, + hasInfo: header?.querySelector('.header-info') !== null, + hasStatusDot: header?.querySelector('.status-dot') !== null, + hasIcon: header?.querySelector('.project-icon') !== null, + hasName: header?.querySelector('.project-name') !== null, + hasCwd: header?.querySelector('.info-cwd') !== null, + }; + }); + expect(structure.hasMain).toBe(true); + expect(structure.hasInfo).toBe(true); + expect(structure.hasStatusDot).toBe(true); + expect(structure.hasIcon).toBe(true); + expect(structure.hasName).toBe(true); + expect(structure.hasCwd).toBe(true); + }); +}); + + +describe('Scenario E6 — Metrics Tab', () => { + before(async () => { + await clickTabByText('Model'); + await browser.pause(200); + }); + + it('should show Metrics tab button in project tab bar', async () => { + const hasMetrics = await tabExistsWithText('Metrics'); + expect(hasMetrics).toBe(true); + }); + + it('should switch to Metrics tab and show metrics panel', async () => { + await clickTabByText('Metrics'); + const activeTab = await getActiveTabText(); + expect(activeTab).toBe('Metrics'); + + await browser.waitUntil( + async () => { + const exists = await browser.execute(() => + document.querySelector('.metrics-panel') !== null, + ); + return exists as boolean; + }, + { timeout: 5000, timeoutMsg: 'Metrics panel did not render within 5s' }, + ); + }); + + it('should show Live view with fleet aggregates', async () => { + const liveView = await browser.execute(() => { + const panel = document.querySelector('.metrics-panel'); + const live = panel?.querySelector('.live-view'); + const aggBar = panel?.querySelector('.agg-bar'); + return { + hasLive: live !== null, + hasAgg: aggBar !== null, + }; + }); + expect(liveView.hasLive).toBe(true); + expect(liveView.hasAgg).toBe(true); + }); + + it('should show fleet badges in aggregates bar', async () => { + const badges = await browser.execute(() => { + const panel = document.querySelector('.metrics-panel'); + const aggBadges = panel?.querySelectorAll('.agg-badge'); + return Array.from(aggBadges ?? []).map((b) => b.textContent?.trim() ?? ''); + }); + expect(badges.length).toBeGreaterThanOrEqual(1); + }); + + it('should show health cards for current project', async () => { + const cardLabels = await browser.execute(() => { + const panel = document.querySelector('.metrics-panel'); + const labels = panel?.querySelectorAll('.hc-label'); + return Array.from(labels ?? []).map((l) => l.textContent?.trim() ?? ''); + }); + expect(cardLabels).toContain('Status'); + }); + + it('should show view tabs for Live and History toggle', async () => { + const viewTabs = await browser.execute(() => { + const panel = document.querySelector('.metrics-panel'); + const tabs = panel?.querySelectorAll('.vtab'); + return Array.from(tabs ?? []).map((t) => t.textContent?.trim() ?? ''); + }); + expect(viewTabs.length).toBeGreaterThanOrEqual(2); + }); + + after(async () => { + await clickTabByText('Model'); + }); +}); + + +describe('Scenario E7 — Conflict Detection UI', () => { + it('should NOT show external write badge on fresh launch', async () => { + const hasExternalBadge = await browser.execute(() => { + const headers = document.querySelectorAll('.project-header'); + for (const header of headers) { + const ext = header.querySelector('.info-conflict-external'); + if (ext) return true; + } + return false; + }); + expect(hasExternalBadge).toBe(false); + }); + + it('should NOT show agent conflict badge on fresh launch', async () => { + const hasConflictBadge = await browser.execute(() => { + const headers = document.querySelectorAll('.project-header'); + for (const header of headers) { + const conflict = header.querySelector('.info-conflict:not(.info-conflict-external)'); + if (conflict) return true; + } + return false; + }); + expect(hasConflictBadge).toBe(false); + }); + + it('should NOT show file conflict count in status bar on fresh launch', async () => { + const hasConflict = await browser.execute(() => { + const bar = document.querySelector('[data-testid="status-bar"]'); + const conflictEl = bar?.querySelector('.state-conflict'); + return conflictEl !== null; + }); + expect(hasConflict).toBe(false); + }); +}); + + +describe('Scenario E8 — Audit Log Tab', () => { + it('should show Audit tab only for manager role projects', async () => { + const auditTabInfo = await browser.execute(() => { + const boxes = document.querySelectorAll('[data-testid="project-box"]'); + const results: { projectId: string; hasAudit: boolean }[] = []; + for (const box of boxes) { + const id = box.getAttribute('data-project-id') ?? ''; + const tabs = box.querySelectorAll('[data-testid="project-tabs"] .ptab'); + const hasAudit = Array.from(tabs).some((t) => t.textContent?.trim() === 'Audit'); + results.push({ projectId: id, hasAudit }); + } + return results; + }); + + expect(auditTabInfo.length).toBeGreaterThanOrEqual(1); + for (const info of auditTabInfo) { + expect(typeof info.hasAudit).toBe('boolean'); + } + }); + + it('should render audit log content when Audit tab is activated', async () => { + const auditProjectId = await browser.execute(() => { + const boxes = document.querySelectorAll('[data-testid="project-box"]'); + for (const box of boxes) { + const tabs = box.querySelectorAll('[data-testid="project-tabs"] .ptab'); + const auditTab = Array.from(tabs).find((t) => t.textContent?.trim() === 'Audit'); + if (auditTab) { + (auditTab as HTMLElement).click(); + return box.getAttribute('data-project-id') ?? ''; + } + } + return ''; + }); + + if (!auditProjectId) return; // No manager agent — skip + + await browser.pause(500); + const auditContent = await browser.execute(() => { + const tab = document.querySelector('.audit-log-tab'); + if (!tab) return { exists: false, hasToolbar: false, hasEntries: false }; + return { + exists: true, + hasToolbar: tab.querySelector('.audit-toolbar') !== null, + hasEntries: tab.querySelector('.audit-entries') !== null, + }; + }); + + expect(auditContent.exists).toBe(true); + expect(auditContent.hasToolbar).toBe(true); + expect(auditContent.hasEntries).toBe(true); + + await clickTabByText('Model'); + }); +}); diff --git a/tests/e2e/specs/phase-f-llm.test.ts b/tests/e2e/specs/phase-f-llm.test.ts new file mode 100644 index 0000000..b485e1f --- /dev/null +++ b/tests/e2e/specs/phase-f-llm.test.ts @@ -0,0 +1,265 @@ +import { browser, expect } from '@wdio/globals'; +import { isJudgeAvailable, assertWithJudge } from '../infra/llm-judge'; + +// Phase F — LLM-Judged Tests (F4–F7) +// Settings completeness, theme system quality, error handling, and UI consistency. + +// ─── Helpers ────────────────────────────────────────────────────────── + +/** Open settings panel and wait for content to render. */ +async function openSettings(): Promise { + const panel = await browser.$('.settings-panel'); + const isOpen = await panel.isDisplayed().catch(() => false); + if (!isOpen) { + await browser.execute(() => { + const btn = document.querySelector('[data-testid="settings-btn"]'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(500); + await browser.waitUntil( + async () => { + const el = await browser.$('.settings-panel'); + return el.isDisplayed().catch(() => false); + }, + { timeout: 5000, timeoutMsg: 'Settings panel did not open within 5s' }, + ); + } + await browser.pause(300); +} + +/** Close settings panel. */ +async function closeSettings(): Promise { + const panel = await browser.$('.settings-panel'); + if (await panel.isDisplayed().catch(() => false)) { + await browser.execute(() => { + const btn = document.querySelector('.settings-close, .panel-close'); + if (btn) (btn as HTMLElement).click(); + }); + await browser.pause(400); + } +} + +/** Click a settings category by label text. */ +async function clickSettingsCategory(label: string): Promise { + return browser.execute((lbl) => { + const items = document.querySelectorAll('.settings-sidebar button, .settings-sidebar [role="tab"]'); + for (const item of items) { + if (item.textContent?.includes(lbl)) { + (item as HTMLElement).click(); + return true; + } + } + return false; + }, label); +} + +/** Get visible text content of settings content area. */ +async function getSettingsContent(): Promise { + return browser.execute(() => { + const content = document.querySelector('.settings-content, .settings-panel'); + return content?.textContent ?? ''; + }); +} + +const SKIP_MSG = 'Skipping — LLM judge not available (no CLI or API key)'; + +// ─── Scenario F4: LLM-Judged Settings Completeness (Extended) ──────── + +describe('Scenario F4 — LLM-Judged Settings Completeness', () => { + after(async () => { + await closeSettings(); + }); + + it('should have all 6 settings categories with meaningful content', async function () { + if (!isJudgeAvailable()) { + console.log(SKIP_MSG); + this.skip(); + return; + } + + await openSettings(); + + // Collect content from each category + const categories = ['Appearance', 'Agents', 'Security', 'Projects', 'Orchestration', 'Advanced']; + const categoryContents: Record = {}; + + for (const cat of categories) { + const clicked = await clickSettingsCategory(cat); + if (clicked) { + await browser.pause(300); + categoryContents[cat] = await getSettingsContent(); + } else { + categoryContents[cat] = '(category not found in sidebar)'; + } + } + + const summary = Object.entries(categoryContents) + .map(([cat, text]) => `## ${cat}\n${text.slice(0, 500)}`) + .join('\n\n'); + + const verdict = await assertWithJudge( + 'The settings panel should have 6 categories: Appearance, Agents, Security, Projects, Orchestration, and Advanced. Each category should have at least 2 configurable settings visible (dropdowns, inputs, toggles, sliders, etc.). Are all categories populated with real settings, not empty or error states?', + summary, + { context: 'AGOR v3 settings panel with sidebar navigation between 6 categories. Each has dedicated settings components.' }, + ); + + expect(verdict.pass).toBe(true); + if (!verdict.pass) { + console.log(`LLM Judge: ${verdict.reasoning} (confidence: ${verdict.confidence})`); + } + }); +}); + +// ─── Scenario F5: LLM-Judged Theme System Quality ──────────────────── + +describe('Scenario F5 — LLM-Judged Theme System Quality', () => { + after(async () => { + await closeSettings(); + }); + + it('should present a comprehensive theme selection interface', async function () { + if (!isJudgeAvailable()) { + console.log(SKIP_MSG); + this.skip(); + return; + } + + await openSettings(); + await clickSettingsCategory('Appearance'); + await browser.pause(300); + + // Open theme dropdown to capture options + await browser.execute(() => { + const trigger = document.querySelector('.custom-dropdown .dropdown-trigger'); + if (trigger) (trigger as HTMLElement).click(); + }); + await browser.pause(300); + + const themeHtml = await browser.execute(() => { + const panel = document.querySelector('.settings-content, .settings-panel'); + if (!panel) return ''; + // Get appearance section HTML for structure analysis + return panel.innerHTML.slice(0, 3000); + }); + + // Close dropdown + await browser.execute(() => document.body.click()); + await browser.pause(200); + + const verdict = await assertWithJudge( + 'This is the Appearance settings section of a desktop app. Does it have: (1) a theme selector with multiple theme options organized in groups (Catppuccin, Editor, Deep Dark), (2) font settings for both UI and terminal with family dropdowns and size controls, (3) visual organization with clear labels and sections? It should look like a polished settings interface.', + themeHtml, + { context: 'AGOR v3 has 17 themes in 3 groups (4 Catppuccin + 7 Editor + 6 Deep Dark), custom dropdown UI, UI font + terminal font dropdowns with size steppers.' }, + ); + + expect(verdict.pass).toBe(true); + if (!verdict.pass) { + console.log(`LLM Judge: ${verdict.reasoning} (confidence: ${verdict.confidence})`); + } + }); +}); + +// ─── Scenario F6: LLM-Judged Error Handling Quality ────────────────── + +describe('Scenario F6 — LLM-Judged Error Handling Quality', () => { + it('should show user-friendly error messages, not raw errors', async function () { + if (!isJudgeAvailable()) { + console.log(SKIP_MSG); + this.skip(); + return; + } + + // Capture any visible toast notifications, error states, or warnings + const errorContent = await browser.execute(() => { + const results: string[] = []; + + // Check toast notifications + const toasts = document.querySelectorAll('.toast, .notification, [data-testid="toast"]'); + toasts.forEach(t => results.push(`Toast: ${t.textContent?.trim()}`)); + + // Check for error states in agent panes + const errorEls = document.querySelectorAll('.error, .error-message, [data-agent-status="error"]'); + errorEls.forEach(e => results.push(`Error: ${e.textContent?.trim()}`)); + + // Check status bar for error indicators + const statusBar = document.querySelector('[data-testid="status-bar"]'); + if (statusBar) results.push(`StatusBar: ${statusBar.textContent?.trim()}`); + + // Check for any visible alerts or warnings + const alerts = document.querySelectorAll('[role="alert"], .alert, .warning'); + alerts.forEach(a => results.push(`Alert: ${a.textContent?.trim()}`)); + + return results.length > 0 ? results.join('\n') : 'No error messages currently visible. The app is in a clean state.'; + }); + + const verdict = await assertWithJudge( + 'These are the currently visible error/notification/status messages from a desktop developer tools app. Evaluate: (1) Are any messages raw stack traces or "[object Object]"? (2) If error messages exist, are they user-friendly with actionable guidance? (3) If no errors are visible, is that a reasonable state for an app with idle agents? The app should NOT show raw internal errors to users.', + errorContent, + { context: 'AGOR v3 uses toast notifications for agent events, status bar for fleet state. Error classifier categorizes API errors into 6 types with user-friendly messages.' }, + ); + + expect(verdict.pass).toBe(true); + if (!verdict.pass) { + console.log(`LLM Judge: ${verdict.reasoning} (confidence: ${verdict.confidence})`); + } + }); +}); + +// ─── Scenario F7: LLM-Judged Overall UI Quality ───────────────────── + +describe('Scenario F7 — LLM-Judged Overall UI Quality', () => { + it('should present a professional, consistent dark-theme UI', async function () { + if (!isJudgeAvailable()) { + console.log(SKIP_MSG); + this.skip(); + return; + } + + // Capture full page structure and key visual elements + const uiSnapshot = await browser.execute(() => { + const elements: string[] = []; + + // Sidebar rail + const rail = document.querySelector('[data-testid="sidebar-rail"]'); + if (rail) elements.push(`Sidebar: ${rail.innerHTML.slice(0, 300)}`); + + // Status bar + const bar = document.querySelector('[data-testid="status-bar"]'); + if (bar) elements.push(`StatusBar: ${bar.innerHTML.slice(0, 500)}`); + + // Project boxes + const boxes = document.querySelectorAll('[data-testid="project-box"]'); + elements.push(`ProjectBoxes: ${boxes.length} rendered`); + if (boxes[0]) { + const header = boxes[0].querySelector('.project-header'); + const tabs = boxes[0].querySelector('[data-testid="project-tabs"]'); + if (header) elements.push(`Header: ${header.innerHTML.slice(0, 300)}`); + if (tabs) elements.push(`Tabs: ${tabs.innerHTML.slice(0, 400)}`); + } + + // Overall body styles + const body = document.body; + const styles = window.getComputedStyle(body); + elements.push(`Body bg: ${styles.backgroundColor}, color: ${styles.color}, font: ${styles.fontFamily.slice(0, 60)}`); + + // Check CSS custom properties are applied + const root = document.documentElement; + const rootStyles = window.getComputedStyle(root); + const ctp = rootStyles.getPropertyValue('--ctp-base'); + elements.push(`Theme var --ctp-base: ${ctp || 'not set'}`); + + return elements.join('\n\n'); + }); + + const verdict = await assertWithJudge( + 'This is a structural snapshot of a developer tools dashboard UI. Rate the visual consistency: (1) Are CSS custom properties (--ctp-*) being used for theming (indicating consistent color system)? (2) Does the layout have clear structure (sidebar, status bar, project boxes with tabs)? (3) Is the font family set to a proper UI font (not monospace for the main UI)? (4) Is the information hierarchy clear (header, tabs, content areas)? A professional app should have all of these.', + uiSnapshot, + { context: 'AGOR v3 uses Catppuccin theme system with 26 --ctp-* CSS vars, VSCode-style sidebar layout, sans-serif UI font, project boxes with tab bars.' }, + ); + + expect(verdict.pass).toBe(true); + if (!verdict.pass) { + console.log(`LLM Judge: ${verdict.reasoning} (confidence: ${verdict.confidence})`); + } + }); +}); diff --git a/tests/e2e/specs/phase-f-search.test.ts b/tests/e2e/specs/phase-f-search.test.ts new file mode 100644 index 0000000..0919d2a --- /dev/null +++ b/tests/e2e/specs/phase-f-search.test.ts @@ -0,0 +1,298 @@ +import { browser, expect } from '@wdio/globals'; + +// Phase F — Search Overlay, Context Tab, Anchors, SSH Tab Tests (F1–F3) +// Tests FTS5 search overlay interactions, context tab with anchors, and SSH tab. + +// ─── Helpers ────────────────────────────────────────────────────────── + +/** Get first project box ID. */ +async function getFirstProjectId(): Promise { + return browser.execute(() => { + const box = document.querySelector('[data-testid="project-box"]'); + return box?.getAttribute('data-project-id') ?? null; + }); +} + +/** Switch to a tab in the first project box by tab text label. */ +async function switchProjectTabByLabel(projectId: string, label: string): Promise { + await browser.execute((id, lbl) => { + const box = document.querySelector(`[data-project-id="${id}"]`); + const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); + if (!tabs) return; + for (const tab of tabs) { + if (tab.textContent?.trim() === lbl) { + (tab as HTMLElement).click(); + return; + } + } + }, projectId, label); + await browser.pause(400); +} + +/** Close any open overlays/panels to reset state. */ +async function resetOverlays(): Promise { + // Close search overlay if open + const overlay = await browser.$('.search-overlay'); + if (await overlay.isExisting()) { + await browser.keys('Escape'); + await browser.pause(300); + } + // Close settings panel if open + const settingsPanel = await browser.$('.settings-panel'); + if (await settingsPanel.isExisting()) { + const closeBtn = await browser.$('.settings-close, .panel-close'); + if (await closeBtn.isExisting()) await closeBtn.click(); + await browser.pause(300); + } +} + +// ─── Scenario F1: Search Overlay Advanced ───────────────────────────── + +describe('Scenario F1 — Search Overlay Advanced', () => { + before(async () => { + await resetOverlays(); + }); + + afterEach(async () => { + // Ensure overlay is closed after each test + try { + const isVisible = await browser.execute(() => { + const el = document.querySelector('.search-overlay'); + return el !== null && window.getComputedStyle(el).display !== 'none'; + }); + if (isVisible) { + await browser.keys('Escape'); + await browser.pause(300); + } + } catch { + // Ignore if overlay doesn't exist + } + }); + + it('should open search overlay with Ctrl+Shift+F', async () => { + await browser.execute(() => document.body.focus()); + await browser.pause(100); + await browser.keys(['Control', 'Shift', 'f']); + await browser.pause(500); + + const overlay = await browser.$('.search-overlay'); + await expect(overlay).toBeDisplayed(); + }); + + it('should show search input focused and ready', async () => { + await browser.execute(() => document.body.focus()); + await browser.pause(100); + await browser.keys(['Control', 'Shift', 'f']); + await browser.pause(500); + + const isFocused = await browser.execute(() => { + const input = document.querySelector('.search-input'); + return input === document.activeElement; + }); + expect(isFocused).toBe(true); + }); + + it('should show empty state message when no results', async () => { + await browser.execute(() => document.body.focus()); + await browser.pause(100); + await browser.keys(['Control', 'Shift', 'f']); + await browser.pause(500); + + const input = await browser.$('.search-input'); + await input.setValue('xyznonexistent99999'); + await browser.pause(500); + + const emptyMsg = await browser.execute(() => { + const el = document.querySelector('.search-empty'); + return el?.textContent ?? ''; + }); + expect(emptyMsg.toLowerCase()).toContain('no results'); + }); + + it('should close search overlay on Escape', async () => { + await browser.execute(() => document.body.focus()); + await browser.pause(100); + await browser.keys(['Control', 'Shift', 'f']); + await browser.pause(500); + + // Verify it opened + const overlayBefore = await browser.$('.search-overlay'); + await expect(overlayBefore).toBeDisplayed(); + + // Press Escape + await browser.keys('Escape'); + await browser.pause(400); + + // Verify it closed + const isHidden = await browser.execute(() => { + const el = document.querySelector('.search-overlay'); + if (!el) return true; + return window.getComputedStyle(el).display === 'none'; + }); + expect(isHidden).toBe(true); + }); +}); + +// ─── Scenario F2: Context Tab & Anchors ─────────────────────────────── + +describe('Scenario F2 — Context Tab & Anchors', () => { + let projectId: string; + + before(async () => { + await resetOverlays(); + const id = await getFirstProjectId(); + if (!id) throw new Error('No project box found'); + projectId = id; + }); + + after(async () => { + // Restore to Model tab + if (projectId) { + await switchProjectTabByLabel(projectId, 'Model'); + } + }); + + it('should show Context tab in project tab bar', async () => { + const hasContextTab = await browser.execute((id) => { + const box = document.querySelector(`[data-project-id="${id}"]`); + const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); + if (!tabs) return false; + for (const tab of tabs) { + if (tab.textContent?.trim() === 'Context') return true; + } + return false; + }, projectId); + expect(hasContextTab).toBe(true); + }); + + it('should render context visualization when Context tab activated', async () => { + await switchProjectTabByLabel(projectId, 'Context'); + + const hasContent = await browser.execute((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'); + const stats = box?.querySelector('.context-stats, .token-meter, .stat-value'); + const anchors = box?.querySelector('.anchors-section'); + return (contextTab !== null) || (stats !== null) || (anchors !== null); + }, projectId); + expect(hasContent).toBe(true); + }); + + it('should show anchor budget scale selector in Settings', async () => { + // Open settings + await browser.execute(() => { + 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 items = document.querySelectorAll('.settings-sidebar button, .settings-sidebar [role="tab"]'); + for (const item of items) { + if (item.textContent?.includes('Orchestration')) { + (item as HTMLElement).click(); + return true; + } + } + return false; + }); + await browser.pause(300); + + // Look for anchor budget setting + const hasAnchorBudget = await browser.execute(() => { + const panel = document.querySelector('.settings-panel, .settings-content'); + if (!panel) return false; + const text = panel.textContent ?? ''; + return text.includes('Anchor') || text.includes('anchor') || + document.querySelector('#setting-anchor-budget') !== null; + }); + + // Close settings + await browser.keys('Escape'); + await browser.pause(300); + + if (clickedOrch) { + expect(hasAnchorBudget).toBe(true); + } + // If Orchestration nav not found, test passes but logs info + if (!clickedOrch) { + console.log('Orchestration category not found in settings nav — may use different layout'); + } + }); +}); + +// ─── Scenario F3: SSH Tab ───────────────────────────────────────────── + +describe('Scenario F3 — SSH Tab', () => { + let projectId: string; + + before(async () => { + await resetOverlays(); + const id = await getFirstProjectId(); + if (!id) throw new Error('No project box found'); + projectId = id; + }); + + after(async () => { + // Restore to Model tab + if (projectId) { + await switchProjectTabByLabel(projectId, 'Model'); + } + }); + + it('should show SSH tab in project tab bar', async () => { + const hasSshTab = await browser.execute((id) => { + const box = document.querySelector(`[data-project-id="${id}"]`); + const tabs = box?.querySelectorAll('[data-testid="project-tabs"] .ptab'); + if (!tabs) return false; + for (const tab of tabs) { + if (tab.textContent?.trim() === 'SSH') return true; + } + return false; + }, projectId); + expect(hasSshTab).toBe(true); + }); + + it('should render SSH content pane when tab activated', async () => { + await switchProjectTabByLabel(projectId, 'SSH'); + + const hasSshContent = await browser.execute((id) => { + const box = document.querySelector(`[data-project-id="${id}"]`); + const sshTab = box?.querySelector('.ssh-tab'); + const sshHeader = box?.querySelector('.ssh-header'); + return (sshTab !== null) || (sshHeader !== null); + }, projectId); + expect(hasSshContent).toBe(true); + }); + + 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 box = document.querySelector(`[data-project-id="${id}"]`); + const list = box?.querySelector('.ssh-list'); + const empty = box?.querySelector('.ssh-empty'); + const cards = box?.querySelectorAll('.ssh-card'); + return { + hasList: list !== null, + hasEmpty: empty !== null, + cardCount: cards?.length ?? 0, + }; + }, projectId); + + // Either we have a list container or cards or an empty state + expect(sshState.hasList || sshState.hasEmpty || sshState.cardCount >= 0).toBe(true); + }); + + it('should show add SSH connection button or form', async () => { + const hasAddControl = await browser.execute((id) => { + const box = document.querySelector(`[data-project-id="${id}"]`); + // Look for add button or SSH form + const form = box?.querySelector('.ssh-form'); + const addBtn = box?.querySelector('.ssh-header button, .ssh-add, [title*="Add"]'); + return (form !== null) || (addBtn !== null); + }, projectId); + expect(hasAddControl).toBe(true); + }); +}); diff --git a/tests/e2e/wdio.conf.js b/tests/e2e/wdio.conf.js index 3a2fbeb..48d5860 100644 --- a/tests/e2e/wdio.conf.js +++ b/tests/e2e/wdio.conf.js @@ -50,6 +50,12 @@ export const config = { resolve(projectRoot, 'tests/e2e/specs/phase-c-ui.test.ts'), resolve(projectRoot, 'tests/e2e/specs/phase-c-tabs.test.ts'), resolve(projectRoot, 'tests/e2e/specs/phase-c-llm.test.ts'), + resolve(projectRoot, 'tests/e2e/specs/phase-d-settings.test.ts'), + resolve(projectRoot, 'tests/e2e/specs/phase-d-errors.test.ts'), + resolve(projectRoot, 'tests/e2e/specs/phase-e-agents.test.ts'), + resolve(projectRoot, 'tests/e2e/specs/phase-e-health.test.ts'), + resolve(projectRoot, 'tests/e2e/specs/phase-f-search.test.ts'), + resolve(projectRoot, 'tests/e2e/specs/phase-f-llm.test.ts'), ], // ── Capabilities ──