// Local full-App browser check against built assets, with synthetic API data only. import assert from 'node:assert/strict'; import { once } from 'node:events'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import express from 'express'; import { chromium } from 'playwright'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const timestamp = new Date().toISOString(); const space = { id: 'browser-space', kind: 'personal', title: 'Browser Workspace', description: '', ownerId: 'browser-test', visibility: 'private', status: 'open', myRole: 'owner', createdAt: timestamp, updatedAt: timestamp }; const task = { id: 1, title: 'Inspector fixture', body: 'SECRET_BODY', pieceName: 'chat', profile: 'default', outputFormat: 'text', askPolicy: 'auto', priority: 'normal', state: 'succeeded', ownerId: 'browser-test', visibility: 'private', spaceId: space.id, createdAt: timestamp, updatedAt: timestamp, latestJob: null, subtasks: [], tags: [], allowedTools: [] }; const app = express(); app.get('/api/auth/me', (req, res) => res.json({ id: 'browser-test', name: 'Browser Test', email: 'browser@example.invalid', role: req.headers.cookie?.includes('nonadmin=1') ? 'user' : 'admin' })); app.get('/api/setup/status', (_req, res) => res.json({ needsSetup: false, authActive: true })); app.get('/api/config', (_req, res) => res.json({ config: { configVersion: 2, tools: {}, storage: {}, llm: { workers: [] } }, overriddenByEnv: {} })); app.get('/api/local/spaces', (_req, res) => res.json([space])); app.get('/api/local/spaces/:id', (_req, res) => res.json(space)); app.get('/api/local/spaces/:id/members', (_req, res) => res.json([{ userId: 'browser-test', name: 'Browser Test', role: 'owner', isOwner: true }])); app.get('/api/local/tasks', (_req, res) => res.json({ tasks: [task] })); app.get('/api/local/tasks/:id', (_req, res) => res.json({ task })); app.get('/api/local/tasks/:id/overview-metrics', (_req, res) => res.json({ metrics: { collecting: false } })); app.get('/api/local/tasks/:id/comments', (_req, res) => res.json({ comments: [] })); app.get('/api/local/tasks/:id/movement-history', (_req, res) => res.json({ events: [] })); app.get('/api/local/tasks/:id/executions', (_req, res) => res.json({ executions: [], active: false })); app.get(['/api/local/tasks/:id/files', '/api/local/spaces/:id/files'], (_req, res) => res.json({ basePath: '', path: '', entries: [], truncated: false })); app.get('/api/local/tasks/:id/console/status', (_req, res) => res.json({ active: false })); app.get('/api/pieces', (_req, res) => res.json({ pieces: [] })); app.get('/api/pieces/:name', (req, res) => res.json({ piece: { name: req.params.name, description: '', movements: [], max_movements: 1, initial_movement: '' }, source: 'builtin' })); app.get('/api/transcription/config', (_req, res) => res.json({ configured: false, finalizeTimeoutSeconds: 180 })); app.get('/api/transcription/recordings', (_req, res) => res.json({ recordings: [] })); app.get('/api/users/me/pets', (_req, res) => res.json({ pets: [], settings: { enabled: false, activePetId: null, size: 48, position: 'bottom-right', workerPets: {} } })); app.get('/api/notifications/capabilities', (_req, res) => res.json({})); app.use('/api', (_req, res) => res.status(404).json({ error: 'Not included in the local browser fixture' })); app.use('/ui', express.static(resolve(root, 'ui/dist'))); app.get('/ui/*', (_req, res) => res.sendFile(resolve(root, 'ui/dist/index.html'))); const server = app.listen(0, '127.0.0.1'); await once(server, 'listening'); const base = `http://127.0.0.1:${server.address().port}`; let browser, page; const errors = []; try { browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); await context.addInitScript(() => { localStorage.setItem('maestro.lang', 'ja'); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async () => { throw new Error('Synthetic clipboard denial'); } } }); }); page = await context.newPage(); page.setDefaultTimeout(15000); page.on('pageerror', error => errors.push(error.message)); await page.goto(`${base}/ui/?space=browser-space&chat=1&tab=transcription&secret=SECRET_QUERY`); const input = page.locator('[data-ui-id="transcription.title"]'); await input.waitFor(); await input.fill('SECRET_INPUT'); await page.evaluate(() => history.replaceState(null, '', location.href + '&secret=SECRET_QUERY')); const activate = async () => { await page.getByTestId('user-account-menu-trigger').click(); await page.getByRole('button', { name: 'UI調査モード', exact: true }).click(); await page.getByRole('region', { name: 'UI調査', exact: true }).waitFor(); }; await activate(); await input.click(); const panel = page.getByRole('region', { name: 'UI調査', exact: true }); await panel.locator('textarea:not([readonly])').fill('入力欄を広くしてください'); const report = await panel.locator('textarea[readonly]').inputValue(); assert.match(report, /ui\/src\/components\/transcription\/TranscriptionPage\.tsx:\d+:\d+/); assert.match(report, /transcription\.title/); assert.match(report, /コンポーネント: TranscriptionPage/); assert.match(report, /入力欄を広くしてください/); assert(!/SECRET_INPUT|SECRET_QUERY|SECRET_BODY|browser-test|\/home\//.test(report), 'report contains metadata and explicit wish only'); await panel.getByRole('button', { name: '修正依頼をコピー' }).click(); await panel.getByRole('status').filter({ hasText: '手動でコピー' }).waitFor(); await input.focus(); await page.keyboard.press('Control+k'); assert.equal(await page.getByRole('dialog').count(), 0, 'command palette stays closed during inspection'); await page.screenshot({ path: '/tmp/aao-ui-inspector-wide.png', fullPage: true }); await page.setViewportSize({ width: 390, height: 844 }); await panel.locator('textarea:not([readonly])').fill('狭い画面でも入力できる'); const bounds = await panel.boundingBox(); assert(bounds && bounds.x >= 0 && bounds.x + bounds.width <= 390, 'inspector fits narrow viewport'); await page.screenshot({ path: '/tmp/aao-ui-inspector-narrow.png', fullPage: true }); await page.keyboard.press('Escape'); await panel.waitFor({ state: 'hidden' }); await input.click(); await input.press('Control+a'); await input.pressSequentially('restored'); assert.equal(await input.inputValue(), 'restored'); // Radix disables outside pointer events and traps focus. Keep this regression mandatory. { await page.setViewportSize({ width: 1440, height: 1000 }); await page.getByRole('button', { name: '+ 新規', exact: true }).first().click(); await page.getByTestId('space-title-input').waitFor(); // The admin shortcut is reachable even when the modal covers the account menu. await page.keyboard.press('Control+Alt+i'); await page.getByTestId('space-title-input').click(); const modalWish = page.locator('section[data-ui-inspector-control] textarea:not([readonly])'); await modalWish.click({ timeout: 5000 }); assert(await page.getByTestId('space-title-input').isVisible(), 'inspector panel must not dismiss the inspected Radix modal'); await modalWish.press('Control+a'); await modalWish.pressSequentially('modal input works'); assert.equal(await modalWish.inputValue(), 'modal input works'); assert(await modalWish.evaluate(element => document.activeElement === element), 'inspector retains keyboard focus outside the Radix modal'); await page.screenshot({ path: '/tmp/aao-ui-inspector-modal.png', fullPage: true }); await page.keyboard.press('Escape'); await page.keyboard.press('Escape'); } await context.addCookies([{ name: 'nonadmin', value: '1', url: base }]); await page.reload(); await page.getByTestId('user-account-menu-trigger').click(); assert.equal(await page.getByRole('button', { name: 'UI調査モード', exact: true }).count(), 0, 'non-admin has no inspector entry'); await page.keyboard.press('Control+Alt+i'); assert.equal(await page.locator('section[data-ui-inspector-control]').count(), 0, 'non-admin cannot activate the inspector shortcut'); assert.equal(errors.length, 0, errors.join('\n')); console.log(JSON.stringify({ passed: true, checks: ['admin activation', 'source/component/id', 'private values excluded', 'clipboard fallback', 'keyboard shortcut blocked', '390px panel', 'Escape and normal editing', 'Radix modal preserved with real keyboard input', 'non-admin entry absent'], screenshots: ['/tmp/aao-ui-inspector-wide.png', '/tmp/aao-ui-inspector-narrow.png', '/tmp/aao-ui-inspector-modal.png'] })); } catch (error) { console.error(JSON.stringify({ url: page?.url(), errors, body: await page?.locator('body').innerText().catch(() => '') })); await page?.screenshot({ path: '/tmp/aao-ui-inspector-failure.png', fullPage: true }).catch(() => {}); throw error; } finally { await browser?.close(); server.closeAllConnections(); server.close(); }