523 lines
20 KiB
TypeScript
523 lines
20 KiB
TypeScript
/**
|
|
* Power User Stress Test — acts like a demanding user who clicks everything,
|
|
* types everywhere, opens 6 windows at once, switches contexts rapidly,
|
|
* and expects nothing to break.
|
|
*
|
|
* This is NOT a "does it render" test. This is a "can I actually USE this" test.
|
|
*/
|
|
import { test, expect, type Page } from '@playwright/test';
|
|
import { isDevNoiseWorkspace } from '../../apps/web/src/lib/workspace-counts';
|
|
|
|
const BASE = process.env.WAGGLE_E2E_BASE_URL ?? 'http://127.0.0.1:3333';
|
|
|
|
async function dismissOverlay(page: Page) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const overlay = page.locator('.fixed.backdrop-blur-sm');
|
|
if (!await overlay.isVisible({ timeout: 1000 }).catch(() => false)) break;
|
|
const btn = page.locator('button:has-text("Start Working")');
|
|
if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
await btn.click({ force: true });
|
|
await page.waitForTimeout(500);
|
|
continue;
|
|
}
|
|
await page.mouse.click(5, 5);
|
|
await page.waitForTimeout(500);
|
|
}
|
|
}
|
|
|
|
async function gotoDesktop(page: Page) {
|
|
await page.goto(`${BASE}/home?skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true`, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
await page.waitForSelector('[role="navigation"], main', { timeout: 10_000 });
|
|
await dismissOverlay(page);
|
|
}
|
|
|
|
async function openSurface(page: Page, label: string) {
|
|
const routes: Record<string, string> = {
|
|
Home: '/home',
|
|
Workspaces: '/workspaces',
|
|
Chat: '/workspaces/default-workspace/chat',
|
|
Memory: '/memory',
|
|
Room: '/room',
|
|
Agents: '/agents',
|
|
Files: '/files',
|
|
Approvals: '/approvals',
|
|
Settings: '/settings',
|
|
'API Keys': '/settings/vault',
|
|
};
|
|
const routePatterns: Record<string, RegExp> = {
|
|
Home: /\/home/,
|
|
Workspaces: /\/workspaces$/,
|
|
Chat: /\/workspaces\/[^/]+\/chat/,
|
|
Memory: /\/memory/,
|
|
Room: /\/room/,
|
|
Agents: /\/agents/,
|
|
Files: /\/files/,
|
|
Approvals: /\/approvals/,
|
|
Settings: /\/settings/,
|
|
'API Keys': /\/settings\/vault/,
|
|
};
|
|
const route = routes[label];
|
|
const btn = page.locator(`button[aria-label="${label}"]`);
|
|
if (await btn.isVisible({ timeout: 1_000 }).catch(() => false)) {
|
|
await btn.click();
|
|
const pattern = routePatterns[label];
|
|
if (route && pattern) {
|
|
await page.waitForURL(pattern, { timeout: 2_500 }).catch(async () => {
|
|
await page.goto(`${BASE}${route}?skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true`, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
});
|
|
await page.waitForSelector('[role="navigation"], main', { timeout: 10_000 });
|
|
} else {
|
|
await page.waitForTimeout(400);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!route) throw new Error(`No current route for ${label}`);
|
|
await page.goto(`${BASE}${route}?skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true`, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
await page.waitForSelector('[role="navigation"], main', { timeout: 10_000 });
|
|
}
|
|
|
|
function chatInput(page: Page) {
|
|
return page.getByRole('textbox', { name: /reply|ask waggle|message/i }).first();
|
|
}
|
|
|
|
function dispatch(page: Page, key: string, opts: { ctrl?: boolean; shift?: boolean } = {}) {
|
|
return page.evaluate(({ key, ctrl, shift }) => {
|
|
window.dispatchEvent(new KeyboardEvent('keydown', {
|
|
key, code: `Key${key.toUpperCase()}`,
|
|
ctrlKey: ctrl ?? false, shiftKey: shift ?? false, bubbles: true,
|
|
}));
|
|
}, { key, ctrl: opts.ctrl, shift: opts.shift });
|
|
}
|
|
|
|
// ── 1. Create a workspace from scratch ────────────────────────────────
|
|
|
|
test.describe('1. Workspace Creation', () => {
|
|
test('can create a workspace via API and see it in dashboard', async ({ page, request }) => {
|
|
const name = `Power Workspace ${Date.now()}`;
|
|
const res = await request.post(`${BASE}/api/workspaces`, {
|
|
data: { name, group: 'testing', persona: 'researcher' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
// Workspace creation might fail if tier limits reached — that's acceptable
|
|
if (res.ok()) {
|
|
const ws = await res.json();
|
|
expect(ws.id).toBeTruthy();
|
|
expect(ws.name).toBe(name);
|
|
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Workspaces');
|
|
await expect(page.locator('body')).toContainText(name, { timeout: 10_000 });
|
|
} else {
|
|
// If creation fails (tier limit, etc.), just verify the API returns a meaningful error
|
|
expect(res.status()).toBeLessThan(500);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── 2. Chat interaction stress ────────────────────────────────────────
|
|
|
|
test.describe('2. Chat Stress', () => {
|
|
test('can type in chat input and see it', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Chat');
|
|
|
|
const input = chatInput(page);
|
|
await expect(input).toBeVisible({ timeout: 5000 });
|
|
await input.fill('Hello from stress test! /help');
|
|
const val = await input.inputValue();
|
|
expect(val).toContain('Hello from stress test');
|
|
});
|
|
|
|
test('slash command menu appears on /', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Chat');
|
|
|
|
const input = chatInput(page);
|
|
await input.focus();
|
|
await input.fill('/');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Slash menu should appear — look for command options
|
|
const slashMenu = page.locator('text=/research|draft|plan|catchup|status|spawn/i');
|
|
const count = await slashMenu.count();
|
|
expect(count).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('persona picker opens and lists personas', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Chat');
|
|
|
|
// Click the persona dropdown in chat header
|
|
const personaBtn = page.locator('button', { hasText: /Persona/i }).first();
|
|
if (await personaBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await personaBtn.click();
|
|
await page.waitForTimeout(500);
|
|
// Should see persona list
|
|
const personas = page.locator('text=/Researcher|Writer|Analyst|Coder|Sales/i');
|
|
expect(await personas.count()).toBeGreaterThan(2);
|
|
}
|
|
});
|
|
|
|
test('model picker opens and lists models', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Chat');
|
|
|
|
// Click the model dropdown
|
|
const modelBtn = page.locator('button', { hasText: /sonnet|claude|model/i }).first();
|
|
if (await modelBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await modelBtn.click();
|
|
await page.waitForTimeout(500);
|
|
const text = await page.locator('body').innerText();
|
|
expect(text).toMatch(/ollama|minimax|sonnet|opus|haiku|gpt|gemini|model/i);
|
|
}
|
|
});
|
|
|
|
test('autonomy chip is clickable and cycles', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Chat');
|
|
|
|
const autonomyChip = page.getByRole('button', { name: /ask first|trusted|autopilot/i }).first();
|
|
if (await autonomyChip.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await autonomyChip.click();
|
|
await page.waitForTimeout(500);
|
|
// Should show autonomy options or cycle to Trusted
|
|
const text = await page.locator('body').innerText();
|
|
expect(text).toMatch(/ask first|trusted|autopilot|autonomy|minutes/i);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── 3. Multi-window chaos ─────────────────────────────────────────────
|
|
|
|
test.describe('3. Multi-Window Chaos', () => {
|
|
test('open 4 windows simultaneously without crash', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
|
|
await openSurface(page, 'Chat');
|
|
await openSurface(page, 'Room');
|
|
await openSurface(page, 'Agents');
|
|
await openSurface(page, 'Files');
|
|
|
|
// No crash — page should still be interactive
|
|
const text = await page.locator('body').innerText();
|
|
expect(text.length).toBeGreaterThan(100);
|
|
|
|
// Single-canvas navigation should leave the app usable on the final surface.
|
|
expect(text).toMatch(/file|folder|workspace|storage/i);
|
|
});
|
|
|
|
test('Ctrl+Shift+N opens the active workspace chat route without crash', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
|
|
await page.keyboard.press('Control+Shift+N');
|
|
await page.waitForURL(/\/workspaces\/[^/]+\/chat/, { timeout: 5_000 });
|
|
await expect(chatInput(page)).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test('close a window via title bar button', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Chat');
|
|
|
|
// Find a close button (the colored dots in the title bar)
|
|
const closeBtn = page.locator('button[aria-label="Close window"], button[title="Close"]');
|
|
if (await closeBtn.first().isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await closeBtn.first().click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
// Should not crash
|
|
const text = await page.locator('body').innerText();
|
|
expect(text).toContain('Waggle');
|
|
});
|
|
});
|
|
|
|
// ── 4. Global Search deep test ────────────────────────────────────────
|
|
|
|
test.describe('4. Global Search', () => {
|
|
test('Ctrl+K opens search, can type and see results', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await dispatch(page, 'k', { ctrl: true });
|
|
await page.waitForTimeout(500);
|
|
|
|
const searchInput = page.locator('input[placeholder*="Search"]');
|
|
await expect(searchInput).toBeVisible({ timeout: 3000 });
|
|
|
|
// Type a query
|
|
await searchInput.fill('chat');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Should see "Chat" command in results
|
|
const results = page.locator('text=/Chat/');
|
|
expect(await results.count()).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('search finds workspaces', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await dispatch(page, 'k', { ctrl: true });
|
|
await page.waitForTimeout(500);
|
|
|
|
const searchInput = page.locator('input[placeholder*="Search"]');
|
|
await searchInput.fill('default');
|
|
await page.waitForTimeout(800);
|
|
|
|
// Should find the seeded default workspace in a fresh data dir.
|
|
const text = await page.locator('body').innerText();
|
|
expect(text.toLowerCase()).toContain('default');
|
|
});
|
|
|
|
test('search finds memories', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await dispatch(page, 'k', { ctrl: true });
|
|
await page.waitForTimeout(500);
|
|
|
|
const searchInput = page.locator('input[placeholder*="Search"]');
|
|
await searchInput.fill('waggle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Should show memory results
|
|
const text = await page.locator('body').innerText();
|
|
expect(text.toLowerCase()).toContain('waggle');
|
|
});
|
|
|
|
test('Escape closes search', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await dispatch(page, 'k', { ctrl: true });
|
|
await page.waitForTimeout(500);
|
|
|
|
const searchInput = page.locator('input[placeholder*="Search"]');
|
|
await expect(searchInput).toBeVisible();
|
|
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(300);
|
|
|
|
await expect(searchInput).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ── 5. Settings deep dive ─────────────────────────────────────────────
|
|
|
|
test.describe('5. Settings', () => {
|
|
test('can navigate all settings tabs', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Settings');
|
|
await page.waitForTimeout(500);
|
|
|
|
for (const tab of ['General', 'Models', 'Billing']) {
|
|
const tabBtn = page.locator(`button[role="tab"]`, { hasText: tab });
|
|
if (await tabBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await tabBtn.click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
// Should not crash
|
|
const text = await page.locator('body').innerText();
|
|
expect(text.length).toBeGreaterThan(50);
|
|
});
|
|
});
|
|
|
|
// ── 6. Vault operations ──────────────────────────────────────────────
|
|
|
|
test.describe('6. Vault', () => {
|
|
test('vault shows keys or empty state', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'API Keys');
|
|
await page.waitForTimeout(1000);
|
|
const text = await page.locator('body').innerText();
|
|
expect(text).toMatch(/vault|key|api|provider|secret|add|anthropic|openai/i);
|
|
});
|
|
});
|
|
|
|
// ── 7. Rapid navigation stress ────────────────────────────────────────
|
|
|
|
test.describe('7. Rapid Navigation', () => {
|
|
test('open and close 5 apps rapidly without crash', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
const apps = ['Chat', 'Room', 'Agents', 'Files', 'Approvals'];
|
|
|
|
for (const app of apps) {
|
|
await openSurface(page, app);
|
|
}
|
|
|
|
// Close all via Ctrl+W
|
|
for (let i = 0; i < 5; i++) {
|
|
await dispatch(page, 'w', { ctrl: true });
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
// Desktop should be clean — hero visible
|
|
await page.waitForTimeout(500);
|
|
const text = await page.locator('body').innerText();
|
|
expect(text).toContain('Waggle AI');
|
|
});
|
|
|
|
test('keyboard shortcuts work: Ctrl+Shift+1 through 5', async ({ page }) => {
|
|
await gotoDesktop(page);
|
|
|
|
// Open Chat via Ctrl+Shift+1
|
|
await dispatch(page, '1', { ctrl: true, shift: true });
|
|
await page.waitForTimeout(500);
|
|
let text = await page.locator('body').innerText();
|
|
expect(text).toMatch(/message|persona|chat/i);
|
|
|
|
// Close it
|
|
await dispatch(page, 'w', { ctrl: true });
|
|
await page.waitForTimeout(300);
|
|
|
|
// Open Memory via Ctrl+Shift+5
|
|
await dispatch(page, '5', { ctrl: true, shift: true });
|
|
await page.waitForTimeout(500);
|
|
text = await page.locator('body').innerText();
|
|
expect(text).toMatch(/memory|frame|knowledge|harvest/i);
|
|
});
|
|
});
|
|
|
|
// ── 8. Data integrity ─────────────────────────────────────────────────
|
|
|
|
test.describe('8. Data Integrity', () => {
|
|
test('workspace list is consistent between API and UI', async ({ page, request }) => {
|
|
const apiRes = await request.get(`${BASE}/api/workspaces`);
|
|
const apiWorkspaces = await apiRes.json();
|
|
const apiNames = (Array.isArray(apiWorkspaces) ? apiWorkspaces : [])
|
|
.filter((workspace: { name: string; status?: string }) => (
|
|
workspace.status !== 'archived' && !isDevNoiseWorkspace(workspace.name)
|
|
))
|
|
.map((workspace: { name: string }) => workspace.name);
|
|
|
|
await gotoDesktop(page);
|
|
await openSurface(page, 'Workspaces');
|
|
const body = page.locator('body');
|
|
|
|
// Every sampled API workspace should appear in the complete workspace view.
|
|
for (const name of apiNames.slice(0, 3)) {
|
|
await expect(body).toContainText(name, { timeout: 10_000 });
|
|
}
|
|
});
|
|
|
|
test('memory frame count matches API', async ({ request }) => {
|
|
const res = await request.get(`${BASE}/api/memory/stats`);
|
|
if (res.ok()) {
|
|
const stats = await res.json();
|
|
const total = stats.total?.frameCount ?? stats.personal?.frameCount ?? 0;
|
|
expect(total).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
|
|
test('sessions endpoint returns valid data', async ({ request }) => {
|
|
const wsRes = await request.get(`${BASE}/api/workspaces`);
|
|
const workspaces = await wsRes.json();
|
|
if (Array.isArray(workspaces) && workspaces.length > 0) {
|
|
const sessRes = await request.get(`${BASE}/api/workspaces/${workspaces[0].id}/sessions`);
|
|
expect(sessRes.ok()).toBeTruthy();
|
|
const sessions = await sessRes.json();
|
|
expect(Array.isArray(sessions)).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── 9. Error resilience ───────────────────────────────────────────────
|
|
|
|
test.describe('9. Error Resilience', () => {
|
|
test('invalid API call returns error, does not crash server', async ({ request }) => {
|
|
const res = await request.get(`${BASE}/api/workspaces/nonexistent-id-12345`);
|
|
expect([404, 500]).toContain(res.status());
|
|
|
|
// Server should still be healthy after error
|
|
const health = await request.get(`${BASE}/health`);
|
|
expect(health.ok()).toBeTruthy();
|
|
});
|
|
|
|
test('sending empty chat message is handled gracefully', async ({ request }) => {
|
|
const res = await request.post(`${BASE}/api/chat`, {
|
|
data: { message: '', workspaceId: 'test' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
// Should return 400 or handle gracefully, not 500
|
|
expect(res.status()).toBeLessThan(500);
|
|
});
|
|
|
|
test('invalid route shows 404 page with recovery link', async ({ page }) => {
|
|
await page.goto(`${BASE}/this-does-not-exist?skipOnboarding=true&skipBoot=true&skipBriefing=true`, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
await page.waitForTimeout(1000);
|
|
const text = await page.locator('body').innerText();
|
|
// Should show a custom 404 page with a way to get back
|
|
expect(text).toMatch(/404|not found|return.*home/i);
|
|
});
|
|
});
|
|
|
|
// ── 10. Fresh User Onboarding ─────────────────────────────────────────
|
|
|
|
test.describe('10. Fresh User Onboarding', () => {
|
|
test('new user sees onboarding wizard', async ({ page }) => {
|
|
// Navigate WITHOUT skipOnboarding — simulate a brand new user
|
|
await page.goto(`${BASE}/`);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(3000);
|
|
|
|
const text = await page.locator('body').innerText();
|
|
// Should show either the onboarding wizard OR the desktop (if auto-skipped for returning user)
|
|
expect(text).toMatch(/waggle|welcome|workspace|get started|choose|chat/i);
|
|
});
|
|
|
|
test('onboarding wizard has template selection', async ({ page }) => {
|
|
// Clear onboarding state to force wizard
|
|
await page.goto(`${BASE}/`);
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem('waggle:onboarding');
|
|
localStorage.removeItem('waggle:first-run');
|
|
});
|
|
await page.reload();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
const onboarding = page.getByRole('region', { name: /waggle onboarding/i });
|
|
if (!await onboarding.isVisible({ timeout: 5_000 }).catch(() => false)) {
|
|
const text = await page.locator('body').innerText();
|
|
expect(text).toMatch(/waggle|workspace|continue|chat/i);
|
|
return;
|
|
}
|
|
|
|
await onboarding.getByRole('button', { name: /continue/i }).click();
|
|
await onboarding.getByRole('button', { name: /continue/i }).click();
|
|
await expect(onboarding.getByRole('button', { name: /continue/i })).toBeEnabled({ timeout: 10_000 });
|
|
await onboarding.getByRole('button', { name: /continue/i }).click();
|
|
await onboarding.getByRole('button', { name: /skip this step/i }).click();
|
|
|
|
await expect(page.getByText(/Research Hub|Engineering|Sales Pipeline/i).first()).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
});
|
|
|
|
// ── 11. Performance baseline ─────────────────────────────────────────
|
|
|
|
test.describe('10. Performance', () => {
|
|
test('initial load completes under 8 seconds', async ({ page }) => {
|
|
const start = Date.now();
|
|
await page.goto(`${BASE}/?skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true`);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
// Wait for dock to render as signal of "app ready"
|
|
await page.locator('button[aria-label="Chat"]').waitFor({ state: 'visible', timeout: 8000 });
|
|
const elapsed = Date.now() - start;
|
|
expect(elapsed).toBeLessThan(8000);
|
|
});
|
|
|
|
test('health endpoint responds under 2 seconds', async ({ request }) => {
|
|
const start = Date.now();
|
|
await request.get(`${BASE}/health`);
|
|
const elapsed = Date.now() - start;
|
|
expect(elapsed).toBeLessThan(2000);
|
|
});
|
|
|
|
test('memory search responds under 3 seconds', async ({ request }) => {
|
|
const start = Date.now();
|
|
await request.get(`${BASE}/api/memory/search?q=important&limit=5`);
|
|
const elapsed = Date.now() - start;
|
|
expect(elapsed).toBeLessThan(3000);
|
|
});
|
|
});
|