/** * Phase A/B polish verification — boot skip + spawn-agent flow. * * Covers: * H-01 (QW-3): BootScreen shows on first visit, skipped on subsequent visits. * H-02 (P35): SpawnAgentDialog empty-state branches correctly: * - no keys configured → Settings → Vault CTA with Key icon * - keys configured but no models → retry CTA * - models present → model chips rendered * H-03 (P36): Dock spawn-agent icon click opens SpawnAgentDialog. * * Run: npx playwright test tests/e2e/spawn-agent-flow.spec.ts --reporter=list */ import { test, expect, type Page } from '@playwright/test'; /** * Navigate to the Desktop with onboarding + boot screen both skipped. * Uses the supported `?skipOnboarding=true&tier=power` bypass defined in * `hooks/useOnboarding.ts:25-32` — that branch writes the completed state * to localStorage synchronously before the first render so the Desktop * mounts immediately with the power-tier dock (which includes the * spawn-agent shortcut). */ async function gotoDesktop(page: Page, url = '/home?skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true') { await page.addInitScript(() => { localStorage.setItem('waggle-booted', 'true'); }); await page.goto(url, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('[role="navigation"], main', { timeout: 10_000 }); } async function clickSpawnAgent(page: Page) { const spawnButton = page.getByTestId('nav-spawn-agent'); await expect(spawnButton).toBeVisible({ timeout: 10_000 }); await spawnButton.click(); } // ── H-01 · BootScreen skip on return visits ──────────────────────────── test.describe('H-01 QW-3 · BootScreen skip', () => { test('first visit renders BootScreen', async ({ page }) => { // Do NOT set waggle-booted — we want the fresh-state path. // skipOnboarding bypass still applies so nothing else blocks. await page.addInitScript(() => { localStorage.removeItem('waggle-booted'); }); await page.goto('/', { waitUntil: 'domcontentloaded' }); await expect(page.getByTestId('boot-screen')).toBeVisible({ timeout: 5_000 }); }); test('return visit skips BootScreen', async ({ page }) => { await gotoDesktop(page); await page.waitForLoadState('domcontentloaded'); await expect(page.getByTestId('boot-screen')).toHaveCount(0); }); }); // ── H-03 · Dock spawn-agent click opens the dialog ───────────────────── test.describe('H-03 P36 · Dock spawn-agent wiring', () => { test('clicking the dock rocket icon opens SpawnAgentDialog', async ({ page }) => { await gotoDesktop(page); await clickSpawnAgent(page); await expect(page.getByTestId('spawn-agent-dialog')).toBeVisible(); }); }); // ── H-02 · SpawnAgentDialog empty-state branches ─────────────────────── test.describe('H-02 P35 · Spawn-agent models empty-state', () => { test('no keys configured → Settings→Vault CTA', async ({ page }) => { // Mock both endpoints BEFORE navigation so the dialog's useEffect hits them. await page.route('**/api/litellm/models', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '[]' }), ); await page.route('**/api/agent/model', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ model: '' }) }), ); await page.route('**/api/providers', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ providers: [ { id: 'anthropic', name: 'Anthropic', hasKey: false, models: [] }, { id: 'openai', name: 'OpenAI', hasKey: false, models: [] }, ], }), }), ); await gotoDesktop(page); await clickSpawnAgent(page); await expect(page.getByTestId('spawn-no-keys-cta')).toBeVisible(); await expect(page.getByTestId('spawn-no-keys-cta')).toContainText(/Settings → Vault|Ollama/i); }); test('keys configured but no models → retry CTA', async ({ page }) => { await page.route('**/api/litellm/models', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '[]' }), ); await page.route('**/api/agent/model', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ model: '' }) }), ); await page.route('**/api/providers', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ providers: [ { id: 'anthropic', name: 'Anthropic', hasKey: true, models: [] }, ], }), }), ); await gotoDesktop(page); await clickSpawnAgent(page); await expect(page.getByTestId('spawn-no-models-cta')).toBeVisible(); await expect(page.getByTestId('spawn-no-models-cta')).toContainText(/Retry/i); }); test('models present → model chip list', async ({ page }) => { await page.route('**/api/litellm/models', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(['claude-sonnet-4-6', 'gpt-4.1', 'gemma4:31b']), }), ); await page.route('**/api/providers', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ providers: [{ id: 'anthropic', name: 'Anthropic', hasKey: true, models: [] }], }), }), ); await gotoDesktop(page); await clickSpawnAgent(page); await expect(page.getByTestId('spawn-models-list')).toBeVisible(); const claude = page.getByTestId('spawn-models-list') .getByRole('button', { name: 'Claude Sonnet 4.6' }); await expect(claude).toBeVisible(); await expect(claude).toHaveAttribute('title', 'claude-sonnet-4-6'); }); });