Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw History Blame Contribute Delete
7.33 kB
import { chromium } from 'playwright';
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const url = process.argv[2] ?? process.env.APP_URL ?? 'http://127.0.0.1:4173/';
const label = process.argv[3] ?? process.env.TEST_LABEL ?? 'agent-terminal';
const result = { date: new Date().toISOString(), url, errors: [], requests: [], checks: [] };
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
context.setDefaultTimeout(15_000);
const page = await context.newPage();
page.on('pageerror', error => { result.errors.push(error.stack); console.log('PAGEERROR', error.stack); });
page.on('console', message => { if (message.type() === 'error') console.log('CONSOLE', message.text().slice(0, 500)); });
context.on('request', request => result.requests.push(request.url()));
const screen = () => page.evaluate(() => window.browserPi.terminal.readScreen());
const text = () => page.evaluate(() => window.browserPi.terminal.text);
async function type(value) { await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(value); }
async function waitScreen(value) { await page.waitForFunction(value => window.browserPi.terminal.readScreen().includes(value), value); }
try {
await page.goto(url);
await page.waitForFunction(() => window.browserPi);
await page.evaluate(() => window.browserPi.ready);
await waitScreen('browser workspace');
result.initial = await screen();
assert(result.initial.includes('/load'));
result.checks.push('Original Pi TUI rendered through xterm');
await type('/help'); await page.keyboard.press('Enter'); await waitScreen('Ctrl+O');
result.checks.push('Enter submits /help without loading a model');
await page.keyboard.press('Control+Shift+f'); await type('browser');
await page.waitForFunction(() => window.browserPi.terminal.text === '' && /\d+\/\d+/.test(window.browserPi.terminal.readScreen()));
await page.keyboard.press('Escape'); await page.waitForTimeout(30);
result.checks.push('Pi fullscreen transcript search preserves the editor draft');
await type('one'); await page.keyboard.press('Shift+Enter'); await type('two');
assert.equal(await text(), 'one\ntwo');
await page.keyboard.press('Backspace'); await page.keyboard.press('Control+a'); await type('X');
assert.equal(await text(), 'one\nXtw');
await page.keyboard.press('Control+u'); await page.keyboard.press('Control+k');
await page.keyboard.press('Backspace'); await page.keyboard.press('Control+u');
assert.equal(await text(), '');
result.checks.push('Pi multiline editor: Shift+Enter, cursor, deletion, Ctrl+U/K');
await page.keyboard.press('Control+p'); assert.equal(await text(), '/help');
await page.keyboard.press('Control+n'); assert.equal(await text(), '');
result.checks.push('Pi prompt history');
await type('/he'); await page.keyboard.press('Tab');
await page.waitForFunction(() => window.browserPi.terminal.text === '/help ');
await page.keyboard.press('Enter'); await waitScreen('Ctrl+O');
result.checks.push('Pi autocomplete: /he → /help');
await type("!printf 'terminal-works\\n' > terminal.txt; cat terminal.txt");
await page.keyboard.press('Escape'); // Dismiss optional workspace completion.
await page.waitForTimeout(30); // A lone ESC is delimited by Pi's 10ms input timer.
await page.keyboard.press('Enter'); await waitScreen('[exit 0]');
await page.waitForFunction(() => !window.browserPi.busy);
const state = await page.evaluate(() => window.browserPi.rpc('snapshot'));
assert.equal(state.files['/workspace/terminal.txt'], 'terminal-works\n');
assert((await screen()).includes('[exit 0]'));
result.checks.push('xterm keyboard → Pi editor → real just-bash → workspace file');
// Model/file text must not be able to clear the terminal or inject OSC URLs.
await type("!printf 'before\\033[2Jafter\\n'");
await page.locator('#send').click(); await waitScreen('beforeafter');
result.checks.push('Shell escape sequences render as inert text');
// Paste uses xterm's actual bracketed-paste path through Pi's StdinBuffer.
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
const largePaste = '!cat > pasted.txt <<\'EOF\'\n' + Array.from({ length: 15 }, (_, i) => 'row-' + i).join('\n') + '\nEOF';
await page.evaluate(text => navigator.clipboard.writeText(text), largePaste);
await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+v');
await page.waitForFunction(text => window.browserPi.terminal.text === text, largePaste);
await page.locator('#send').click();
await page.waitForFunction(() => document.querySelector('#files').textContent.includes('pasted.txt'));
const pastedState = await page.evaluate(() => window.browserPi.rpc('snapshot'));
assert.equal(pastedState.files['/workspace/pasted.txt'], Array.from({ length: 15 }, (_, i) => 'row-' + i).join('\n') + '\n');
result.checks.push('Large Pi paste markers expand before Send');
const pasted = 'hello 世界 🦙\nline two';
await page.evaluate(text => navigator.clipboard.writeText(text), pasted);
await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+v');
await page.waitForFunction(text => window.browserPi.terminal.text === text, pasted);
result.checks.push('Unicode multiline clipboard paste');
await page.locator('#newline').click(); assert.equal(await text(), pasted + '\n');
result.checks.push('Touch-accessible newline button');
// Resize while preserving a multiline draft and Pi's own viewport renderer.
const desktop = await page.evaluate(() => ({ cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows }));
await page.screenshot({ path: `../validation/${label}-desktop.png`, fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
await page.waitForFunction(cols => window.browserPi.terminal.cols < cols, desktop.cols);
assert.equal(await text(), pasted + '\n');
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
result.sizes = { desktop, mobile: await page.evaluate(() => ({ cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows })) };
assert(result.sizes.mobile.cols >= 30); assert(result.sizes.mobile.rows >= 15);
await page.screenshot({ path: `../validation/${label}-mobile.png`, fullPage: true });
result.checks.push('390px layout and live terminal resize preserve the draft');
result.mobileLimitation = 'Desktop Chromium viewport simulation; physical mobile keyboards and GPUs not tested.';
assert.equal(result.errors.length, 0);
assert.equal(result.requests.filter(url => /^https?:/.test(url) && new URL(url).origin !== new URL(result.url).origin).length, 0);
result.passed = true; console.log(JSON.stringify({ passed: true, checks: result.checks, sizes: result.sizes }));
} catch (error) {
result.failure = error.stack;
result.screen = await screen().catch(() => 'App failed before initialization');
await page.screenshot({ path: `../validation/${label}-failure.png`, fullPage: true });
throw error;
} finally { await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result, null, 2)); await browser.close(); }