// Browser e2e against the @agent-desk/server's served UI. // Validates the dual-target architecture from the renderer's perspective: // the same vanilla-JS app that runs under Electron also loads from a browser // hitting the WS server, with token gating enforced. import { test, expect } from '@playwright/test'; const TOKEN = 'playwright-e2e-token'; test.describe('@agent-desk/server browser UI', () => { test('healthz responds without a token', async ({ request }) => { const res = await request.get('/healthz'); expect(res.status()).toBe(200); const body = await res.json(); expect(body.ok).toBe(true); }); test('protected route blocks without a token', async ({ request }) => { const res = await request.get('/index.html'); expect(res.status()).toBe(401); }); test('protected route serves UI shell with a valid token', async ({ request }) => { const res = await request.get(`/index.html?t=${TOKEN}`); expect(res.status()).toBe(200); const body = await res.text(); expect(body).toContain(' { const consoleErrors: string[] = []; page.on('pageerror', (err) => consoleErrors.push(`pageerror: ${err.message}`)); page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(`console: ${msg.text()}`); }); await page.goto(`/?t=${TOKEN}`); // The vanilla-JS renderer was built for Electron's preload bridge // (window.agentDesk). Without a WS-shim preload it won't fully boot — // we assert the HTML shell loads and the title is set, which is what // the dual-target architecture guarantees today. The transport-ws // preload shim is a Phase F follow-up. await expect(page).toHaveTitle(/Agent Desk/i); // The body should at least be present and have rendered the dockview // root or an error message about missing window.agentDesk — both are // acceptable proof the static asset pipeline works. const bodyHtml = await page.content(); expect(bodyHtml.length).toBeGreaterThan(500); }); test('serves the @agent-desk/ui web shim with a valid token', async ({ request }) => { const res = await request.get(`/ui/web-entry.js?t=${TOKEN}`); expect(res.status()).toBe(200); const body = await res.text(); expect(body).toContain('window.agentDesk'); expect(body).toContain('rpc'); }); test('injects the web-entry script tag into the served index.html', async ({ request }) => { const res = await request.get(`/index.html?t=${TOKEN}`); expect(res.status()).toBe(200); const body = await res.text(); expect(body).toContain('/ui/web-entry.js'); expect(body).toContain('type="module"'); }); test('serves the placeholder PWA icons with a valid token', async ({ request }) => { const res192 = await request.get(`/ui/../../pwa/public/icons/192.png?t=${TOKEN}`); // The path traversal above should NOT escape the static root. // We expect a 404 (security) or a file from inside the ui static root. expect([200, 401, 403, 404]).toContain(res192.status()); }); test('rejects WS upgrade without a token', async ({ playwright }) => { // Playwright's APIRequestContext doesn't speak WS; use fetch with // Upgrade headers and assert the server refuses the handshake. const ctx = await playwright.request.newContext(); const res = await ctx.fetch('/ws', { headers: { Upgrade: 'websocket', Connection: 'Upgrade', 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', 'Sec-WebSocket-Version': '13', }, }); expect(res.status()).toBe(401); await ctx.dispose(); }); });