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. // ─── Helpers ────────────────────────────────────────────────────────── async function getProjectIds(): Promise { return exec(() => { const boxes = document.querySelectorAll('[data-testid="project-box"]'); return Array.from(boxes).map((b) => b.getAttribute('data-project-id') ?? '').filter(Boolean); }); } async function focusProject(id: string): Promise { await exec((pid) => { const h = document.querySelector(`[data-project-id="${pid}"] .project-header`); if (h) (h as HTMLElement).click(); }, id); await browser.pause(300); } async function switchProjectTab(id: string, tabIndex: number): Promise { 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); await browser.pause(300); } async function getAgentStatus(id: string): Promise { return exec((pid) => { const p = document.querySelector(`[data-project-id="${pid}"] [data-testid="agent-pane"]`); return p?.getAttribute('data-agent-status') ?? 'not-found'; }, id); } async function resetToModelTabs(): Promise { for (const id of await getProjectIds()) await switchProjectTab(id, 0); } // ─── Scenario B1: Multi-project grid renders correctly ──────────────── describe('Scenario B1 — Multi-Project Grid', () => { before(async () => { // Reset: ensure all projects on Model tab await resetToModelTabs(); }); it('should render multiple project boxes', async () => { await browser.waitUntil( async () => { const count = await exec(() => document.querySelectorAll('[data-testid="project-box"]').length, ); return (count as number) >= 1; }, { timeout: 10_000, timeoutMsg: 'No project boxes rendered within 10s' }, ); const ids = await getProjectIds(); expect(ids.length).toBeGreaterThanOrEqual(1); const unique = new Set(ids); expect(unique.size).toBe(ids.length); }); it('should show project headers with CWD paths', async () => { const headers = await exec(() => { const els = document.querySelectorAll('.project-header .info-cwd'); return Array.from(els).map((e) => e.textContent?.trim() ?? ''); }); for (const cwd of headers) { expect(cwd.length).toBeGreaterThan(0); } }); it('should have independent agent panes per project', async () => { const ids = await getProjectIds(); for (const id of ids) { const status = await getAgentStatus(id); expect(['idle', 'running', 'stalled']).toContain(status); } }); it('should focus project on click and show active styling', async () => { const ids = await getProjectIds(); if (ids.length < 1) return; await focusProject(ids[0]); const isActive = await exec((id) => { const box = document.querySelector(`[data-project-id="${id}"]`); return box?.classList.contains('active') ?? false; }, ids[0]); expect(isActive).toBe(true); }); it('should show project-specific accent colors on each box border', async () => { 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()); }); for (const accent of accents) { expect(accent.length).toBeGreaterThan(0); } }); it('should render project icons (emoji) in headers', async () => { const icons = await exec(() => { const els = document.querySelectorAll('.project-header .project-icon, .project-header .emoji'); return Array.from(els).map((e) => e.textContent?.trim() ?? ''); }); if (icons.length > 0) { for (const icon of icons) { expect(icon.length).toBeGreaterThan(0); } } }); it('should show project CWD tooltip on hover', async () => { const ids = await getProjectIds(); if (ids.length < 1) return; 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]); expect(titleAttr.length).toBeGreaterThan(0); }); it('should highlight focused project with distinct border color', async () => { const ids = await getProjectIds(); if (ids.length < 2) return; await focusProject(ids[0]); const isActive = await exec((id) => { return document.querySelector(`[data-project-id="${id}"]`)?.classList.contains('active') ?? false; }, ids[0]); expect(isActive).toBe(true); }); it('should show all base tabs per project', async () => { const ids = await getProjectIds(); if (ids.length < 1) return; 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]); for (const tab of ['Model', 'Docs', 'Context', 'Files', 'SSH', 'Memory', 'Metrics']) { expect(tabLabels).toContain(tab); } }); it('should show terminal section at bottom of Model tab', async () => { const ids = await getProjectIds(); if (ids.length < 1) return; await switchProjectTab(ids[0], 0); 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); }); }); // ─── Scenario B2: Independent tab switching across projects ─────────── describe('Scenario B2 — Independent Tab Switching', () => { before(async () => { await resetToModelTabs(); }); it('should allow different tabs active in different projects', async function () { const ids = await getProjectIds(); 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) => exec((pid) => { return document.querySelector(`[data-project-id="${pid}"] [data-testid="project-tabs"] .ptab.active`)?.textContent?.trim() ?? ''; }, id); const firstActive = await getActiveTab(ids[0]); const secondActive = await getActiveTab(ids[1]); expect(firstActive).not.toBe(secondActive); await switchProjectTab(ids[0], 0); }); it('should preserve scroll position when switching between projects', async function () { const ids = await getProjectIds(); if (ids.length < 2) { this.skip(); return; } await focusProject(ids[0]); await focusProject(ids[1]); await focusProject(ids[0]); 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'); }); }); // ─── Scenario B3: Status bar reflects fleet state ──────────────────── describe('Scenario B3 — Status Bar Fleet State', () => { it('should show agent count in status bar', async () => { const barText = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); return bar?.textContent ?? ''; }); expect(barText.length).toBeGreaterThan(0); }); it('should show no burn rate when all agents idle', async () => { const hasBurnRate = await exec(() => { const bar = document.querySelector('[data-testid="status-bar"]'); const burnEl = bar?.querySelector('.burn-rate'); const costEl = bar?.querySelector('.cost'); return { burn: burnEl?.textContent ?? null, cost: costEl?.textContent ?? null }; }); if (hasBurnRate.burn !== null) { expect(hasBurnRate.burn).toMatch(/\$0|0\.00/); } if (hasBurnRate.cost !== null) { expect(hasBurnRate.cost).toMatch(/\$0|0\.00/); } }); it('should update status bar counts when project focus changes', async () => { const ids = await getProjectIds(); if (ids.length < 2) return; await focusProject(ids[1]); const barAfter = await exec(() => { return document.querySelector('[data-testid="status-bar"]')?.textContent ?? ''; }); expect(barAfter.length).toBeGreaterThan(0); }); });