Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw
History Blame Contribute Delete
8.25 kB
import { chromium } from 'playwright';
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const url = process.argv[2] ?? 'http://127.0.0.1:4173/';
const label = process.argv[3] ?? 'agent-shell';
const result = { date: new Date().toISOString(), url, checks: [], errors: [] };
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', e => result.errors.push(e.stack));
const input = page.locator('#command');
async function run(command) {
await input.fill(command); await input.press('Enter');
await page.waitForFunction(() => !window.browserPi.busy && document.querySelector('#shell-run').textContent === 'Run');
await page.waitForFunction(() => !!document.querySelector('.shell-entry:last-child .shell-status.success, .shell-entry:last-child .shell-status.error'));
}
async function geometry() {
return page.evaluate(() => {
const i = document.querySelector('#command'), h=document.querySelector('#command-highlight');
const ir=i.getBoundingClientRect(), hr=h.getBoundingClientRect();
const shell=document.querySelector('.workspace > .terminal').getBoundingClientRect();
const field=document.querySelector('#shell-form').getBoundingClientRect();
return { sameOrigin:ir.x===hr.x && ir.y===hr.y, sameWidth:ir.width===hr.width,
sameFont:getComputedStyle(i).font===getComputedStyle(h).font,
samePadding:getComputedStyle(i).padding===getComputedStyle(h).padding,
sameScroll:i.scrollTop===h.scrollTop,
sameText:h.textContent===i.value+'\n',
fits:field.left>=shell.left && field.right<=shell.right,
overflow:document.documentElement.scrollWidth>innerWidth };
});
}
try {
await page.goto(url); await page.waitForFunction(()=>window.browserPi?.terminal); await page.evaluate(()=>window.browserPi.ready);
assert.equal(await page.locator('header #load').count(),1);
assert.equal(await page.locator('.conversation .model-panel').count(),0);
assert.equal(await page.locator('header').evaluate(el=>getComputedStyle(el).borderBottomWidth),'0px');
const command='NAME="world"; printf "Hello %s\\n" "$NAME" | cat -n # greeting';
await input.fill(command);
assert(await page.locator('#command-highlight .token').count()>4);
const colorCount=await page.locator('#command-highlight .token').evaluateAll(items=>new Set(items.map(el=>getComputedStyle(el).color)).size);
assert(colorCount>=3);
assert((await geometry()).sameText);
await input.press('Enter');
await page.waitForFunction(()=>document.querySelector('.shell-stdout')?.textContent.includes('Hello world'));
assert.equal(await page.locator('.shell-entry .shell-code').textContent(),command);
assert.equal(await page.locator('.shell-status').textContent(),'[exit 0]');
result.checks.push('Top-bar model status replaces the card; Bash highlighting preserves exact command text and real execution');
await input.fill('unsent draft'); await input.press('ArrowUp'); assert.equal(await input.inputValue(),command);
await input.press('ArrowDown'); assert.equal(await input.inputValue(),'unsent draft');
await input.fill('printf "one\\n"'); await input.press('Shift+Enter'); await page.keyboard.type('printf "two\\n"');
assert.equal(await input.inputValue(),'printf "one\\n"\nprintf "two\\n"');
assert((await geometry()).sameText);
await input.press('Enter'); await page.waitForFunction(()=>document.querySelector('.shell-entry:last-child .shell-stdout')?.textContent==='one\ntwo\n');
await run('printf "problem\\n" >&2; exit 7');
assert.equal(await page.locator('.shell-entry:last-child .shell-stderr').textContent(),'problem\n');
assert.equal(await page.locator('.shell-entry:last-child .shell-status.error').textContent(),'[exit 7]');
result.checks.push('History restores drafts; Shift+Enter runs multiline commands; stderr and nonzero exit status are distinguished');
const hostile='<img src=x onerror=alert(1)><script>alert(2)</script>';
await run("printf '%s\\n' '"+hostile+"'");
assert.equal(await page.locator('.shell-entry:last-child .shell-stdout').textContent(),hostile+'\n');
assert.equal(await page.locator('#terminal-output img, #terminal-output script, #command-highlight img').count(),0);
await run("printf 'before\\033[2Jafter\\n'");
assert.equal(await page.locator('.shell-entry:last-child .shell-stdout').textContent(),'beforeafter\n');
result.checks.push('Command/file HTML is inert text; terminal escape sequences cannot alter the page');
await page.locator('#editor').fill('An unsaved edit');
await run('printf should-not-run');
assert.match(await page.locator('.shell-entry:last-child .shell-stderr').textContent(),/Save your file edits/);
assert.equal(await input.inputValue(),'printf should-not-run');
await page.locator('#save').click(); await page.waitForFunction(()=>!window.browserPi.busy && document.querySelector('#save').disabled);
await page.locator('#shell-clear').click(); assert.equal(await page.locator('.shell-entry').count(),0);
await run('printf "workspace-ready\\n"');
await input.press('Control+l'); assert.equal(await page.locator('.shell-entry').count(),0);
result.checks.push('Unsaved file edits block execution without losing the command; Clear and Ctrl+L clear only output');
await run('ls -la');
await run('printf "Hello from the browser\\n"');
const draft='NAME="世界 🦙"\nprintf "%s\\n" "$NAME" | cat -n';
await input.fill(draft);
for(const [width,height] of [[1440,1000],[851,640],[850,640],[390,844],[320,568]]) {
await page.setViewportSize({width,height}); await page.waitForTimeout(100);
const g=await geometry(); assert(g.sameOrigin && g.sameWidth && g.sameFont && g.samePadding && g.sameScroll && g.sameText && g.fits && !g.overflow,JSON.stringify({width,...g}));
if([1440,390,320].includes(width)) await page.screenshot({path:`../validation/${label}-${width}x${height}.png`,fullPage:true});
}
const long=Array.from({length:16},(_,i)=>`printf 'line ${i} 世界\\n'`).join('\n');
await input.fill(long); await input.evaluate(el=>{el.scrollTop=el.scrollHeight;el.dispatchEvent(new Event('scroll'));});
assert((await geometry()).sameScroll);
await page.locator('.workspace > .terminal summary').click(); await page.locator('.workspace > .terminal summary').click();
assert.equal(await input.inputValue(),long);
result.checks.push('Five viewport sizes preserve input/highlight alignment, Unicode, scrolling and drafts through collapse/expand');
const touch = await browser.newContext({ viewport: {width:390,height:844}, isMobile:true, hasTouch:true });
const phone = await touch.newPage(); phone.on('pageerror', e=>result.errors.push(e.stack));
await phone.goto(url); await phone.waitForFunction(()=>window.browserPi?.terminal); await phone.evaluate(()=>window.browserPi.ready);
await phone.locator('#command').fill(draft);
const touchLayout = await phone.evaluate(()=>{
const load=document.querySelector('#load').getBoundingClientRect(), header=document.querySelector('header').getBoundingClientRect();
const input=getComputedStyle(document.querySelector('#command')), highlight=getComputedStyle(document.querySelector('#command-highlight'));
return { fits:load.bottom<=header.bottom && document.documentElement.scrollWidth<=innerWidth, font:input.fontSize, matches:input.font===highlight.font };
});
assert(touchLayout.fits && touchLayout.matches && touchLayout.font==='16px',JSON.stringify(touchLayout));
await phone.screenshot({path:`../validation/${label}-touch.png`,fullPage:true}); await touch.close();
result.checks.push('Touch simulation keeps model status within the top bar and uses aligned 16px command text');
assert.equal(result.errors.length,0); result.passed=true; console.log(JSON.stringify({passed:true,checks:result.checks}));
} catch(error) {result.failure=error.stack; 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();}