Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw History Blame Contribute Delete
7.86 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-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 = '<!DOCTYPE html>\n<html lang="en">\n<head>\n <style>body { background: #18181e; color: white; }</style>\n</head>\n<body>\n <!-- A tiny browser game -->\n <canvas id="game" width="600" height="600"></canvas>\n <script>\n const canvas = document.getElementById("game");\n function draw(score) {\n return score + 1;\n }\n </script>\n</body>\n</html>\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 }) => <button>{props.label}</button>;\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('<!-- edited -->'); await highlighted(html + '<!-- edited -->');
await page.keyboard.press('Control+z'); await highlighted(html);
await page.keyboard.type('<!-- saved -->'); await highlighted(html + '<!-- saved -->');
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 + '<!-- saved -->');
await editor.fill('<img src=x onerror="window.sourceExecuted=true"><script>window.sourceExecuted=true</script>');
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', '<b>plain text</b>'); 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();
}