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-editor'; const result = { url, checks: [], errors: [] }; const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); page.on('pageerror', error => result.errors.push(error.message)); const editor = page.locator('#editor'), layer = page.locator('#editor-highlight'); async function write(name, content) { await page.evaluate(({ name, content }) => window.browserPi.rpc('write', { path: '/workspace/' + name, content }), { name, content }); } async function select(name) { await page.locator(`#files button[title="/workspace/${name}"]`).click(); } async function highlighted(text) { await page.waitForFunction(text => document.querySelector('.editor-input').classList.contains('is-highlighted') && document.querySelector('#editor-highlight').textContent === text + '\n', text); } try { await page.goto(url); await page.waitForFunction(() => window.browserPi); await page.evaluate(() => window.browserPi.ready); const html = '\n\n\n \n\n\n \n \n \n\n\n'; const fixtures = { 'snake-game.html': [html, '.token.tag, .token.keyword, .token.selector'], 'app.js': ['// café 🐈\nconst total = items.reduce((sum, item) => sum + item.price, 0);\n', '.token.keyword'], 'view.tsx': ['const View = (props: { label: string }) => ;\n', '.token.tag'], 'script.py': ['# Sales\ndef total(rows):\n\treturn sum(row["price"] for row in rows)\n', '.token.keyword'], 'style.css': ['body { color: #fff; margin: 0; }\n', '.token.property'], 'data.json': ['{"total": 274.88, "verified": true}\n', '.token.property'], 'notes.md': ['# Report\n\n**Total:** `274.88`\n', '.token.title'], 'config.yaml': ['version: 1\nfiles:\n - sales.csv\n', '.token.key'], 'query.sql': ['SELECT SUM(units * price) AS total FROM sales;\n', '.token.keyword'], 'run.sh': ['#!/bin/bash\nfor file in *.csv; do echo "$file"; done\n', '.token.keyword'], }; for (const [name, [content, token]] of Object.entries(fixtures)) { await write(name, content); await select(name); await highlighted(content); assert(await layer.locator(token).count(), name); assert.equal(await editor.inputValue(), content); } result.checks.push('Common language grammars render colors and preserve exact source, including Unicode, tabs and trailing newlines'); await select('snake-game.html'); await highlighted(html); for (const token of ['.token.tag', '.token.keyword', '.token.selector']) assert(await layer.locator(token).count()); assert.equal(await layer.locator('script, style, canvas, img, iframe').count(), 0); await page.screenshot({ path: `../validation/${label}-html.png`, fullPage: true }); // Real keyboard edits must retain selection, undo and the file-saving flow. await editor.focus(); await page.keyboard.press('Control+End'); await page.keyboard.insertText(''); await highlighted(html + ''); await page.keyboard.press('Control+z'); await highlighted(html); await page.keyboard.type(''); await highlighted(html + ''); await page.locator('#save').click(); await page.waitForFunction(() => document.querySelector('#save').disabled); assert.equal(await page.evaluate(() => window.browserPi.rpc('snapshot').then(state => state.files['/workspace/snake-game.html'])), html + ''); await editor.fill(''); await highlighted(await editor.inputValue()); assert.equal(await page.evaluate(() => window.sourceExecuted), undefined); assert.equal(await layer.locator('img, script').count(), 0); await editor.fill(html); await highlighted(html); await page.locator('#save').click(); await page.waitForFunction(() => document.querySelector('#save').disabled); result.checks.push('Native keyboard editing and undo work; Save persists exact content; HTML source remains inert'); const long = Array.from({ length: 500 }, (_, i) => `const row${i} = "${'x'.repeat(120)}";`).join('\n') + '\n'; await write('long.js', long); await select('long.js'); await highlighted(long); for (const [width, height] of [[1440,1000], [390,844], [320,568]]) { await page.setViewportSize({ width, height }); await editor.scrollIntoViewIfNeeded(); await editor.evaluate(el => { el.scrollTop = el.scrollHeight; el.scrollLeft = el.scrollWidth; }); await page.waitForFunction(() => { const input = document.querySelector('#editor'), pre = document.querySelector('#editor-highlight'); return pre.scrollTop === input.scrollTop && pre.scrollLeft === input.scrollLeft && pre.clientWidth === input.clientWidth && pre.clientHeight === input.clientHeight; }); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); const fonts = await page.evaluate(() => ['#editor', '#editor-highlight'].map(selector => { const style = getComputedStyle(document.querySelector(selector)); return [style.font, style.padding, style.tabSize, style.whiteSpace, style.letterSpacing]; })); assert.deepEqual(fonts[0], fonts[1]); } await select('snake-game.html'); await highlighted(html); await editor.scrollIntoViewIfNeeded(); assert.deepEqual(await editor.evaluate(el => [el.scrollTop, el.scrollLeft]), [0, 0]); await page.screenshot({ path: `../validation/${label}-mobile.png`, fullPage: true }); result.checks.push('Highlighted text stays aligned with the caret and both scroll axes on desktop and narrow mobile layouts'); // Rapid switching cannot let an old worker response overwrite the new file. await select('long.js'); await select('data.json'); await highlighted(fixtures['data.json'][0]); await write('plain.txt', 'plain text'); await select('plain.txt'); assert.equal(await page.locator('.is-highlighted').count(), 0); const huge = '// large file\n' + 'x'.repeat(250_000); await write('large.js', huge); await select('large.js'); assert.equal(await page.locator('.is-highlighted').count(), 0); const started = Date.now(); await editor.focus(); await page.keyboard.press('Control+End'); await page.keyboard.type(' edited'); assert((await editor.inputValue()).endsWith(' edited')); result.largeFileEditMs = Date.now() - started; await page.locator('#save').click(); await page.waitForFunction(() => document.querySelector('#save').disabled); await select('snake-game.html'); await highlighted(html); await page.emulateMedia({ forcedColors: 'active' }); assert.equal(await layer.evaluate(el => getComputedStyle(el).visibility), 'hidden'); assert.notEqual(await editor.evaluate(el => getComputedStyle(el).color), 'rgba(0, 0, 0, 0)'); result.checks.push('Rapid file switching avoids stale colors; plain/large files remain editable; forced-colors mode shows native text'); assert.deepEqual(result.errors, []); result.passed = true; console.log(JSON.stringify(result, null, 2)); } 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(); }