| 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();} |
|
|