Files
sausage/userland/chat/render.test.cjs
T

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>', '&lt;script&gt;']) assert.ok(html.includes(expected), expected);
assert.ok(!html.includes('<script>'));
assert.ok(run("markdown('Text <b>raw</b> & more')").includes('&lt;b&gt;raw&lt;/b&gt;'));
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 &lt;missing&gt;</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('&lt;failure&gt;'));
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&lt;b</code>');
const edit = run("toolArguments({name:'edit',arguments:{path:'/file',edits:[{oldText:'<old>',newText:'<new>'}]}})");
assert.ok(edit.includes('Old text</div><pre>&lt;old&gt;</pre>'));
assert.ok(edit.includes('New text</div><pre>&lt;new&gt;</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');