66 lines
4.5 KiB
JavaScript
66 lines
4.5 KiB
JavaScript
// Test the extracted page script without a server or a browser dependency.
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
class Element {
|
|
constructor(tag) { this.tag = tag; this.childNodes = []; this.innerHTML = ''; this.textContent = ''; }
|
|
append(...nodes) { this.childNodes.push(...nodes); }
|
|
replaceChildren(...nodes) { this.childNodes = nodes; }
|
|
requestSubmit() { this.submits = (this.submits || 0) + 1; }
|
|
}
|
|
const elements = new Map();
|
|
const document = {
|
|
getElementById(id) { if (!elements.has(id)) elements.set(id, new Element(id)); return elements.get(id); },
|
|
createElement(tag) { return new Element(tag); },
|
|
};
|
|
const context = vm.createContext({ document, fetch: () => new Promise(() => {}), setTimeout() {} });
|
|
vm.runInContext(fs.readFileSync(process.argv[2], 'utf8'), context);
|
|
const run = source => vm.runInContext(source, context);
|
|
const render = value => { context.markdownInput = value; return run('markdown(markdownInput)'); };
|
|
const html = render('# Heading\n\n- **Bold** and *italic* [link](https://example.com) `inline`\n\n```js\nconst x = 1 < 2;\n```\n\n| Name | Value |\n| --- | --- |\n| one | two |\n\n<script>alert(1)</script>');
|
|
for (const expected of ['<h1>', '<ul>', '<strong>', '<em>', '<a href="https://example.com">', '<code>inline</code>', '<pre><code', '<table>', '<td>two</td>', '<script>']) assert.ok(html.includes(expected), expected);
|
|
assert.ok(!html.includes('<script>'));
|
|
assert.ok(run("markdown('Text <b>raw</b> & more')").includes('<b>raw</b>'));
|
|
const entry = record => { context.fixture = record; run('entry(fixture)'); };
|
|
entry({ kind: 'pi.user', model: { content: '# User heading' } });
|
|
assert.ok(elements.get('entries').childNodes[0].childNodes[1].innerHTML.includes('<h1>User heading</h1>'));
|
|
entry({ kind: 'pi.assistant', model: { content: [
|
|
{ type: 'thinking', thinking: '', signature: 'empty' },
|
|
{ type: 'thinking', thinking: 'Check the files.' },
|
|
{ type: 'toolCall', id: 'one', name: 'bash', arguments: { command: 'ls <missing>' } },
|
|
{ type: 'toolCall', id: 'two', name: 'bash', arguments: { command: 'pwd' } },
|
|
{ type: 'text', text: '**Answer**' },
|
|
] } });
|
|
assert.equal(run('tools.size'), 2);
|
|
assert.equal(run("tools.get('one').card.innerHTML"), '<strong>bash</strong><pre><code>ls <missing></code></pre>');
|
|
entry({ kind: 'pi.tool-result', model: [{ toolCallId: 'two', toolName: 'bash', content: [{ type: 'text', text: '/work' }] }] });
|
|
entry({ kind: 'pi.tool-result', model: [{ toolCallId: 'one', toolName: 'bash', isError: true, content: [{ type: 'text', text: '<failure>\n'.repeat(12) }] }] });
|
|
assert.equal(run('tools.size'), 2, 'Results reuse cards by ID, not by name');
|
|
assert.equal(run("tools.get('one').card.className"), 'tool error');
|
|
assert.ok(run("tools.get('one').result.innerHTML").includes('<details><summary>Error</summary>'));
|
|
assert.ok(run("tools.get('one').result.innerHTML").includes('<failure>'));
|
|
assert.ok(run("tools.get('two').result.innerHTML").includes('<details open><summary>Result</summary><pre>/work</pre>'));
|
|
const assistant = elements.get('entries').childNodes[1];
|
|
assert.equal(assistant.childNodes.filter(node => node.tag === 'em').length, 1);
|
|
assert.equal(assistant.childNodes[0].className, 'thinking');
|
|
assert.equal(assistant.childNodes[0].textContent, 'Check the files.');
|
|
assert.ok(assistant.childNodes.at(-1).innerHTML.includes('<strong>Answer</strong>'));
|
|
for (const name of ['read', 'write']) assert.equal(run(`toolArguments({name:'${name}',arguments:{path:'/a<b'}})`), '<code>/a<b</code>');
|
|
const edit = run("toolArguments({name:'edit',arguments:{path:'/file',edits:[{oldText:'<old>',newText:'<new>'}]}})");
|
|
assert.ok(edit.includes('Old text</div><pre><old></pre>'));
|
|
assert.ok(edit.includes('New text</div><pre><new></pre>'));
|
|
const keydown = elements.get('input').onkeydown;
|
|
for (const modifier of ['ctrlKey', 'metaKey']) {
|
|
let prevented = false;
|
|
keydown({ key: 'Enter', [modifier]: true, preventDefault() { prevented = true; } });
|
|
assert.ok(prevented);
|
|
}
|
|
assert.equal(elements.get('compose').submits, 2);
|
|
keydown({ key: 'Enter', preventDefault() { assert.fail('Plain Enter must insert a newline'); } });
|
|
keydown({ key: 'Enter', ctrlKey: true, isComposing: true, preventDefault() { assert.fail('Do not submit IME composition'); } });
|
|
assert.equal(elements.get('compose').submits, 2);
|
|
run('select(2)');
|
|
assert.equal(run('tools.size'), 0);
|
|
assert.equal(elements.get('entries').childNodes.length, 0);
|
|
console.log('PASS markdown, escaped HTML, paired tools, errors, long results, thinking, and keyboard shortcuts');
|