Files
panel/controller/.test/ui.spec.mjs
T
2026-07-14 23:18:05 -07:00

123 lines
5.3 KiB
JavaScript

// ui.spec.mjs — browser regression gate for the panel controller UI.
// Runs with @playwright/test: npx playwright test ui.spec.mjs
//
// Requires a LOCALLY RUNNING controller (see ../../memory/build.md).
// BASE_URL controller origin (default http://127.0.0.1:8080)
// TEST_EMAIL panel account email \ needed for the authenticated
// TEST_PASSWORD panel account password / tests; without them those
// tests self-skip (annotated).
//
// Anything that would need a live agent (server cards with real data,
// consoles, etc.) is deliberately NOT asserted — those checks self-skip.
// The point of this suite is the 2026-05-08 class of regression: the page
// serves, the inline JS boots with zero console errors, the root route's
// panel_ui cookie logic doesn't loop, and the command-center chrome
// (palette, hero stats) exists.
import { test, expect } from '@playwright/test';
const BASE = process.env.BASE_URL || 'http://127.0.0.1:8080';
const EMAIL = process.env.TEST_EMAIL || '';
const PASSWORD = process.env.TEST_PASSWORD || '';
// Collect console errors + page errors on a page. Filters nothing: the gate
// is "0 console errors" on first paint, matching the redroid-console house
// boilerplate (page.on('console'/'pageerror') capture).
function captureErrors(page) {
const errs = [];
page.on('console', m => { if (m.type() === 'error') errs.push(`[console.error] ${m.text().slice(0, 300)}`); });
page.on('pageerror', e => errs.push(`[pageerror] ${e.message.slice(0, 300)}`));
return errs;
}
async function login(page) {
await page.goto(BASE + '/login');
await page.fill('#email', EMAIL);
await page.fill('#password', PASSWORD);
await page.click('#submit');
await page.waitForURL(u => !u.pathname.startsWith('/login'), { timeout: 10_000 });
}
test.describe('unauthenticated', () => {
test('login page renders with its form and 0 console errors', async ({ page }) => {
const errs = captureErrors(page);
const resp = await page.goto(BASE + '/login');
expect(resp.ok()).toBeTruthy();
await expect(page.locator('#email')).toBeVisible();
await expect(page.locator('#password')).toBeVisible();
await expect(page.locator('#submit')).toBeVisible();
await page.waitForTimeout(500);
expect(errs, errs.join('\n')).toHaveLength(0);
});
test('root without session 302s to /login (no loop)', async ({ page }) => {
await page.goto(BASE + '/');
await expect(page).toHaveURL(/\/login/);
});
});
test.describe('authenticated', () => {
test.skip(!EMAIL || !PASSWORD,
'TEST_EMAIL / TEST_PASSWORD not set — authenticated UI checks skipped');
test('root serves new.html by default, boots with 0 console errors', async ({ page }, testInfo) => {
const errs = captureErrors(page);
await login(page);
await page.goto(BASE + '/');
// new.html markers: hero-stat tiles + command-palette hint in the topbar.
await expect(page.locator('#stat-running')).toBeAttached();
await expect(page.locator('#topbar-cmdk')).toBeAttached();
await page.waitForTimeout(1500); // let the boot JS run
// WebSocket/agent-dependent noise would need a live agent — annotate
// rather than fail on errors that are clearly missing-agent-shaped.
const agentish = errs.filter(e => /websocket|ws:|agent|ECONNREFUSED/i.test(e));
const hard = errs.filter(e => !agentish.includes(e));
if (agentish.length) {
testInfo.annotations.push({ type: 'needs-live-agent', description: agentish.join(' | ') });
}
expect(hard, hard.join('\n')).toHaveLength(0);
});
test('panel_ui=classic cookie serves index.html with no redirect loop', async ({ browser }) => {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await login(page);
await ctx.addCookies([{
name: 'panel_ui', value: 'classic',
url: BASE,
}]);
// The 2026-05-31 gotcha: FileServer canonicalization made "/" 301-loop
// for authenticated classic users (ERR_TOO_MANY_REDIRECTS). A successful
// goto with a 200 final response proves no loop.
const resp = await page.goto(BASE + '/');
expect(resp.status()).toBe(200);
// Classic UI must NOT show the new-brand hero-stat tiles.
await expect(page.locator('#stat-running')).toHaveCount(0);
await ctx.close();
});
test('command palette opens on Ctrl+K and via the topbar hint', async ({ page }) => {
await login(page);
await page.goto(BASE + '/');
const modal = page.locator('#cmdk-modal');
await expect(modal).toBeHidden();
await page.keyboard.press('Control+KeyK');
await expect(modal).toBeVisible();
await expect(page.locator('#cmdk-input')).toBeFocused();
await page.keyboard.press('Escape');
await expect(modal).toBeHidden();
await page.click('#topbar-cmdk');
await expect(modal).toBeVisible();
});
test('hero-stat element IDs present', async ({ page }) => {
await login(page);
await page.goto(BASE + '/');
for (const id of ['stat-running', 'stat-players', 'stat-agents', 'stat-cpu',
'stat-running-hint', 'stat-players-hint', 'stat-agents-hint', 'stat-cpu-hint',
'status-dot', 'status']) {
await expect(page.locator('#' + id), `#${id} missing from new.html DOM`).toBeAttached();
}
});
});