Files
waggle-os/tests/e2e/spawn-agent-flow.spec.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

147 lines
5.9 KiB
TypeScript

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