633 lines
26 KiB
TypeScript
633 lines
26 KiB
TypeScript
/**
|
|
* Full E2E Wiring Audit — tests every major UI flow against the live backend.
|
|
*
|
|
* FIXED: Updated from old apps/web desktop OS (port 8080, dock/windows paradigm)
|
|
* to current Tauri app shell (port 3333, sidebar navigation paradigm).
|
|
*
|
|
* Run: node node_modules\playwright\cli.js test tests/e2e/full-wiring-audit.spec.ts --reporter=list
|
|
*/
|
|
import { test, expect, type Page } from '@playwright/test';
|
|
|
|
const API = process.env.WAGGLE_E2E_BASE_URL ?? 'http://127.0.0.1:3333';
|
|
// App is served by the same server; Playwright can override the URL for isolated runs.
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
async function skipOnboarding(page: Page) {
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('waggle:onboarding', JSON.stringify({ completed: true, step: 7 }));
|
|
localStorage.setItem('waggle:first-run', 'done');
|
|
localStorage.setItem('waggle-booted', 'true');
|
|
});
|
|
}
|
|
|
|
async function waitForApp(page: Page) {
|
|
await page.waitForSelector(
|
|
'.waggle-app-shell, .waggle-sidebar, [role="navigation"], [class*="onboarding"]',
|
|
{ timeout: 15_000 },
|
|
).catch(() => {});
|
|
await page.waitForTimeout(600);
|
|
}
|
|
|
|
async function navigateSidebar(page: Page, label: string) {
|
|
const nav = page.locator('[role="navigation"]');
|
|
|
|
const sidebarSelectors: Record<string, string[]> = {
|
|
Chat: ['[data-testid="nav-chat"]', 'button[aria-label="Chat"]'],
|
|
Memory: ['[data-testid="nav-memory"]', 'button[aria-label="Memory"]'],
|
|
Settings: ['[data-testid="sidebar-user"]', 'button[aria-label="Account and settings"]'],
|
|
};
|
|
|
|
for (const selector of sidebarSelectors[label] ?? []) {
|
|
const btn = nav.locator(selector).first();
|
|
if (await btn.isVisible().catch(() => false)) {
|
|
await btn.click();
|
|
await page.waitForTimeout(500);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const textButton = nav.locator('button', { hasText: label }).first();
|
|
if (await textButton.isVisible().catch(() => false)) {
|
|
await textButton.click();
|
|
await page.waitForTimeout(500);
|
|
return true;
|
|
}
|
|
|
|
const routeByLabel: Record<string, string> = {
|
|
'Skills Hub': '/marketplace',
|
|
Events: '/settings/events',
|
|
Cockpit: '/settings/mission-control',
|
|
'Mission Control': '/settings/mission-control',
|
|
Settings: '/settings',
|
|
};
|
|
const route = routeByLabel[label];
|
|
if (route) {
|
|
await page.goto(`${route}?skipOnboarding=true&skipBoot=true`);
|
|
await waitForApp(page);
|
|
return true;
|
|
}
|
|
|
|
throw new Error(`No current navigation target configured for "${label}"`);
|
|
}
|
|
|
|
function collectErrors(page: Page): string[] {
|
|
const errors: string[] = [];
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') errors.push(msg.text());
|
|
});
|
|
page.on('pageerror', err => errors.push(err.message));
|
|
return errors;
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 1 — Backend API Health (all formerly passing — keep identical)
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Backend API Endpoints', () => {
|
|
test('GET /health returns ok', async ({ request }) => {
|
|
const res = await request.get(`${API}/health`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.status).toBeDefined();
|
|
expect(data.llm).toBeDefined();
|
|
});
|
|
|
|
test('GET /api/workspaces returns array', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/workspaces`);
|
|
expect(res.ok()).toBeTruthy();
|
|
expect(Array.isArray(await res.json())).toBeTruthy();
|
|
});
|
|
|
|
test('GET /api/events returns object with events array', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/events`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.events).toBeDefined();
|
|
expect(Array.isArray(data.events)).toBeTruthy();
|
|
});
|
|
|
|
test('GET /api/skills returns object with skills array', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/skills`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.skills)).toBeTruthy();
|
|
});
|
|
|
|
test('GET /api/memory/frames returns object with results array', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/memory/frames?limit=5&workspace=default`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.results)).toBeTruthy();
|
|
});
|
|
|
|
test('GET /api/connectors returns connectors', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/connectors`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.connectors).toBeDefined();
|
|
});
|
|
|
|
test('GET /api/personas returns personas', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/personas`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.personas).toBeDefined();
|
|
});
|
|
|
|
test('GET /api/fleet returns sessions', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/fleet`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.sessions).toBeDefined();
|
|
});
|
|
|
|
test('GET /api/cron returns schedules', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/cron`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.schedules).toBeDefined();
|
|
});
|
|
|
|
test('GET /api/marketplace/packs returns packs', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/marketplace/packs`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(data.packs).toBeDefined();
|
|
});
|
|
|
|
test('GET /api/settings returns settings object', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/settings`);
|
|
expect([200, 404]).toContain(res.status()); // May or may not exist
|
|
});
|
|
|
|
test('POST /api/workspaces creates workspace', async ({ request }) => {
|
|
const name = `E2E-Audit-${Date.now()}`;
|
|
const res = await request.post(`${API}/api/workspaces`, {
|
|
data: { name, group: 'Workspaces', description: 'Wiring audit test' },
|
|
});
|
|
expect([200, 201, 403, 409]).toContain(res.status());
|
|
if (res.ok()) {
|
|
const ws = await res.json();
|
|
const wsData = ws.workspace ?? ws.data ?? ws;
|
|
const id = wsData.id ?? wsData.name ?? wsData;
|
|
expect(id).toBeDefined();
|
|
}
|
|
});
|
|
|
|
test('GET /api/vault returns vault data', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/vault`);
|
|
expect([200, 404]).toContain(res.status());
|
|
});
|
|
|
|
test('GET /api/costs returns cost data or 403', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/costs`);
|
|
expect([200, 403]).toContain(res.status());
|
|
});
|
|
|
|
test('GET /api/tier returns tier info', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/tier`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(['TRIAL', 'FREE', 'TEAMS', 'ENTERPRISE']).toContain(data.tier);
|
|
});
|
|
|
|
test('GET /api/hooks returns rules array', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/hooks`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.rules)).toBeTruthy();
|
|
});
|
|
|
|
test('GET /api/cloud-sync returns sync status', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/cloud-sync`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(typeof data.available).toBe('boolean');
|
|
});
|
|
|
|
test('GET /api/marketplace/search returns packages', async ({ request }) => {
|
|
const res = await request.get(`${API}/api/marketplace/search?query=pdf&limit=3`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.packages)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 2 — App Shell Load (FIXED: port 3333, Tauri app shell selectors)
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Frontend App Load', () => {
|
|
test('app loads at port 3333 without crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
|
|
const fatalErrors = errors.filter(e =>
|
|
!e.includes('favicon') && !e.includes('net::ERR') &&
|
|
!e.includes('ResizeObserver') && !e.includes('404') &&
|
|
(e.includes('is not a function') || e.includes('Cannot read') || e.includes('Uncaught'))
|
|
);
|
|
expect(fatalErrors).toHaveLength(0);
|
|
});
|
|
|
|
test('onboarding or main shell renders — no blank screen', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForTimeout(1200);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.trim().length).toBeGreaterThan(20);
|
|
});
|
|
|
|
test('app shell renders after onboarding skip', async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
const html = await page.content();
|
|
expect(html.length).toBeGreaterThan(1000);
|
|
});
|
|
|
|
test('no 404 errors on static assets', async ({ page }) => {
|
|
const failed: string[] = [];
|
|
page.on('response', res => {
|
|
if (res.status() === 404 && !res.url().includes('/api/')) failed.push(res.url());
|
|
});
|
|
await page.goto('/');
|
|
await waitForApp(page);
|
|
expect(failed).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 3 — Sidebar Navigation (FIXED: sidebar paradigm, not dock/windows)
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Sidebar Navigation', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('sidebar navigation is visible with buttons', async ({ page }) => {
|
|
const nav = page.locator('[role="navigation"]');
|
|
const visible = await nav.isVisible().catch(() => false);
|
|
if (visible) {
|
|
const buttons = nav.locator('button');
|
|
expect(await buttons.count()).toBeGreaterThanOrEqual(5);
|
|
} else {
|
|
// App may be in onboarding — just verify it loaded
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
}
|
|
});
|
|
|
|
test('clicking Settings navigates to settings panel', async ({ page }) => {
|
|
const navigated = await navigateSidebar(page, 'Settings');
|
|
if (navigated) {
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.includes('General') || body.includes('Models') || body.includes('Settings')).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('clicking Capabilities navigates to marketplace', async ({ page }) => {
|
|
const navigated = await navigateSidebar(page, 'Skills Hub');
|
|
if (navigated) {
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
}
|
|
});
|
|
|
|
test('clicking Memory navigates to memory view', async ({ page }) => {
|
|
const navigated = await navigateSidebar(page, 'Memory');
|
|
if (navigated) {
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
}
|
|
});
|
|
|
|
test('clicking Chat navigates to chat view', async ({ page }) => {
|
|
const navigated = await navigateSidebar(page, 'Chat');
|
|
if (navigated) {
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
}
|
|
});
|
|
|
|
test('clicking Events navigates to events view', async ({ page }) => {
|
|
const navigated = await navigateSidebar(page, 'Events');
|
|
if (navigated) {
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
}
|
|
});
|
|
|
|
test('clicking Cockpit navigates to cockpit view', async ({ page }) => {
|
|
const navigated = await navigateSidebar(page, 'Cockpit');
|
|
if (navigated) {
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 4 — Workspace Wiring
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Workspace Wiring', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('workspaces load from API', async ({ page }) => {
|
|
const res = await page.request.get(`${API}/api/workspaces`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data)).toBeTruthy();
|
|
expect(data.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test('workspace list is not empty', async ({ page }) => {
|
|
const res = await page.request.get(`${API}/api/workspaces`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const workspaces = await res.json();
|
|
expect(Array.isArray(workspaces)).toBeTruthy();
|
|
expect(workspaces.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 5 — Chat Wiring
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Chat Wiring', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('chat view opens without JS crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Chat');
|
|
await page.waitForTimeout(800);
|
|
const fatal = errors.filter(e =>
|
|
e.includes('is not a function') || e.includes('Cannot read properties of') || e.includes('is not defined')
|
|
);
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
|
|
test('chat view renders message input or workspace selector', async ({ page }) => {
|
|
await navigateSidebar(page, 'Chat');
|
|
await page.waitForTimeout(600);
|
|
const hasTextarea = await page.locator('textarea').isVisible().catch(() => false);
|
|
const hasInput = await page.locator('input[type="text"]').isVisible().catch(() => false);
|
|
const body = await page.textContent('body') ?? '';
|
|
const hasContent = body.includes('Ask') || body.includes('message') || body.includes('workspace') || body.includes('Chat');
|
|
expect(hasTextarea || hasInput || hasContent).toBe(true);
|
|
});
|
|
|
|
test('chat textarea accepts text input', async ({ page }) => {
|
|
await navigateSidebar(page, 'Chat');
|
|
await page.waitForTimeout(600);
|
|
const textarea = page.locator('textarea').first();
|
|
if (await textarea.isVisible().catch(() => false)) {
|
|
await textarea.fill('Hello E2E test');
|
|
expect(await textarea.inputValue()).toContain('Hello');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 6 — Memory Wiring
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Memory Wiring', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('memory view opens without JS crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Memory');
|
|
await page.waitForTimeout(800);
|
|
const fatal = errors.filter(e =>
|
|
e.includes('is not a function') || e.includes('Cannot read properties of')
|
|
);
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
|
|
test('memory API responds when memory view is open', async ({ page }) => {
|
|
await navigateSidebar(page, 'Memory');
|
|
const res = await page.request.get(`${API}/api/memory/frames?workspace=default&limit=5`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.results)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 7 — Settings Wiring
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Settings Wiring', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('settings view opens without JS crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Settings');
|
|
await page.waitForTimeout(800);
|
|
const fatal = errors.filter(e =>
|
|
e.includes('is not a function') || e.includes('Cannot read properties of')
|
|
);
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
|
|
test('settings tabs are visible after navigation', async ({ page }) => {
|
|
await navigateSidebar(page, 'Settings');
|
|
await page.waitForTimeout(500);
|
|
const body = await page.textContent('body') ?? '';
|
|
// At least one settings tab label must appear
|
|
const hasTab = body.includes('General') || body.includes('Models') || body.includes('Keys') || body.includes('Advanced');
|
|
expect(hasTab).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 8 — Capabilities / Marketplace Wiring
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Capabilities Wiring', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('capabilities view opens without JS crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Skills Hub');
|
|
await page.waitForTimeout(800);
|
|
const fatal = errors.filter(e =>
|
|
e.includes('is not a function') || e.includes('Cannot read properties of')
|
|
);
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
|
|
test('marketplace search API is accessible from capabilities view', async ({ page }) => {
|
|
await navigateSidebar(page, 'Skills Hub');
|
|
const res = await page.request.get(`${API}/api/marketplace/search?query=&limit=5`);
|
|
expect(res.ok()).toBeTruthy();
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.packages)).toBeTruthy();
|
|
expect(data.packages.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 9 — Events + Cockpit + Mission Control Wiring
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Events / Cockpit / Mission Control Wiring', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
});
|
|
|
|
test('events view opens without crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Events');
|
|
await page.waitForTimeout(600);
|
|
const fatal = errors.filter(e => e.includes('is not a function') || e.includes('Cannot read'));
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
|
|
test('cockpit view opens without crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Cockpit');
|
|
await page.waitForTimeout(600);
|
|
const fatal = errors.filter(e => e.includes('is not a function') || e.includes('Cannot read'));
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
|
|
test('mission control view opens without crash', async ({ page }) => {
|
|
const errors = collectErrors(page);
|
|
await navigateSidebar(page, 'Mission Control');
|
|
await page.waitForTimeout(600);
|
|
const fatal = errors.filter(e => e.includes('is not a function') || e.includes('Cannot read'));
|
|
expect(fatal).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
// SECTION 10 — Console Error Audit (traverse all views, collect errors)
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
test.describe('Full Console Error Audit', () => {
|
|
test('traverse all sidebar views — zero critical JS errors', async ({ page }) => {
|
|
const criticalErrors: { view: string; error: string }[] = [];
|
|
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
const text = msg.text();
|
|
if (
|
|
text.includes('is not a function') ||
|
|
text.includes('Cannot read properties of') ||
|
|
text.includes('is not defined') ||
|
|
text.includes('Uncaught')
|
|
) {
|
|
criticalErrors.push({ view: 'unknown', error: text });
|
|
}
|
|
}
|
|
});
|
|
page.on('pageerror', err => {
|
|
criticalErrors.push({ view: 'pageerror', error: err.message });
|
|
});
|
|
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
|
|
const views = ['Chat', 'Memory', 'Events', 'Skills Hub', 'Cockpit', 'Mission Control', 'Settings'];
|
|
for (const view of views) {
|
|
const nav = page.locator('[role="navigation"]');
|
|
const btn = nav.locator('button', { hasText: view });
|
|
if (await btn.isVisible().catch(() => false)) {
|
|
const before = criticalErrors.length;
|
|
await btn.click();
|
|
await page.waitForTimeout(600);
|
|
// Tag any new errors with the view name
|
|
if (criticalErrors.length > before) {
|
|
criticalErrors.slice(before).forEach(e => e.view = view);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (criticalErrors.length > 0) {
|
|
const report = criticalErrors.map(e => `[${e.view}] ${e.error}`).join('\n');
|
|
expect(criticalErrors.length, `Critical JS errors found:\n${report}`).toBe(0);
|
|
}
|
|
});
|
|
|
|
test('no 404 on API calls made during app lifecycle', async ({ page }) => {
|
|
const apiErrors: string[] = [];
|
|
page.on('response', res => {
|
|
if (res.url().includes('/api/') && res.status() === 404) {
|
|
apiErrors.push(`404: ${res.url()}`);
|
|
}
|
|
});
|
|
|
|
await page.goto('/');
|
|
await skipOnboarding(page);
|
|
await page.reload();
|
|
await waitForApp(page);
|
|
|
|
// Navigate through key views to trigger their API calls
|
|
for (const view of ['Chat', 'Settings', 'Skills Hub']) {
|
|
const btn = page.locator('[role="navigation"] button', { hasText: view });
|
|
if (await btn.isVisible().catch(() => false)) {
|
|
await btn.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
}
|
|
|
|
// Filter out expected 404s (endpoints that are legitimately optional)
|
|
const unexpectedErrors = apiErrors.filter(url =>
|
|
!url.includes('favicon') && !url.includes('notifications/history')
|
|
);
|
|
|
|
if (unexpectedErrors.length > 0) {
|
|
console.log('API 404s detected:', unexpectedErrors);
|
|
}
|
|
// Warn but don't fail — some 404s may be expected for unimplemented optional endpoints
|
|
expect(unexpectedErrors.length).toBeLessThan(5);
|
|
});
|
|
});
|