Files
maestro/scripts/check-ui-inspector.mjs
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

124 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: '[email protected]', 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();
}