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('<'), '<');
});
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);
});