File size: 7,855 Bytes
39371ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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();
}