import test from 'node:test'; import assert from 'node:assert/strict'; import { escHtml, attrEsc, jsonInject, scriptLiteral } from '../dist/html-escape.js'; // ─── escHtml ──────────────────────────────────────────────────────────── test('escHtml: plain text passes through', () => { assert.equal(escHtml('hello world'), 'hello world'); }); test('escHtml: ampersand escaped first to avoid double-escape', () => { assert.equal(escHtml('a & b'), 'a & b'); assert.equal(escHtml('<'), '&lt;'); }); test('escHtml: escapes HTML tag delimiters', () => { assert.equal(escHtml(''), '<script>alert(1)</script>'); }); test('escHtml: escapes both quote types', () => { assert.equal(escHtml(`"x" and 'y'`), '"x" and 'y''); }); test('escHtml: coerces non-string input via String()', () => { assert.equal(escHtml(123), '123'); assert.equal(escHtml(null), 'null'); assert.equal(escHtml(undefined), 'undefined'); assert.equal(escHtml(true), 'true'); }); test('escHtml: passes unicode and emoji through unchanged', () => { assert.equal(escHtml('héllo 🚀'), 'héllo 🚀'); }); test('escHtml: empty string', () => { assert.equal(escHtml(''), ''); }); test('escHtml: full XSS canary payload leaves no raw < or "', () => { const payload = '">'; const out = escHtml(payload); assert.equal(out, '"><img src=x onerror=alert(1)>'); assert.ok(!out.includes('<'), 'must contain no raw <'); assert.ok(!out.includes('>'), 'must contain no raw >'); assert.ok(!out.includes('"'), 'must contain no raw "'); }); // ─── attrEsc ──────────────────────────────────────────────────────────── test('attrEsc: matches escHtml on attribute-quoting canary', () => { const payload = `" onmouseover="alert(1)`; assert.equal(attrEsc(payload), escHtml(payload)); assert.ok(!attrEsc(payload).includes('"')); }); test('attrEsc: escapes both quote variants', () => { assert.equal(attrEsc(`a'b"c`), 'a'b"c'); }); // ─── jsonInject ───────────────────────────────────────────────────────── test('jsonInject: serialises a string with quotes', () => { assert.equal(jsonInject('hello'), '"hello"'); assert.equal(jsonInject(`it's "fine"`), `"it's \\"fine\\""`); }); test('jsonInject: serialises an object deterministically', () => { assert.equal(jsonInject({ a: 1, b: 'x' }), '{"a":1,"b":"x"}'); }); test('jsonInject: serialises array', () => { assert.equal(jsonInject([1, 2, 'x']), '[1,2,"x"]'); }); test('jsonInject: null round-trips as JSON null', () => { assert.equal(jsonInject(null), 'null'); }); test('jsonInject: escapes context (new in 0.8.9)', () => { const raw = ''; const injected = jsonInject(raw); // The literal sequence must NOT survive in the output — otherwise // the browser parser would end the surrounding '), 'jsonInject must escape for inline `; const literal = scriptLiteral(value); assert.equal(JSON.parse(literal), value); });