import { describe, test, expect } from 'vitest';
import {
sanitizeInlineHtml,
inlineHtmlToText,
textToInlineHtml,
isPlainInlineHtml,
} from './inline-html';
describe('sanitizeInlineHtml allowlist', () => {
test.each([
['hi', 'hi'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x', 'x'],
['x y z w v', 'x y z w v'],
['
', 'outer
inner'],
['ab
', 'a
b'],
['
t', 't'],
['t', 't'],
['t', 't'],
['x', 'x'],
['<script>alert(1)</script>', '<script>alert(1)</script>'],
['t', 't'],
['xunwrapped
', 'xunwrapped'],
])('%s -> %s', (input, expected) => {
expect(sanitizeInlineHtml(input)).toBe(expected);
});
test('empty / non-string input -> empty string', () => {
expect(sanitizeInlineHtml('')).toBe('');
expect(sanitizeInlineHtml(' ')).toBe('');
expect(sanitizeInlineHtml(undefined as any)).toBe('');
expect(sanitizeInlineHtml({} as any)).toBe('');
});
});
describe('sanitizeInlineHtml links', () => {
test.each([
'x',
'x',
'x',
'x',
'x',
'x',
'x',
'x',
'x',
'x',
])('unsafe/missing href unwraps to text: %s', (input) => {
expect(sanitizeInlineHtml(input)).toBe('x');
});
test('target=_blank forces rel=noopener (author rel is replaced)', () => {
expect(sanitizeInlineHtml('x'))
.toBe('x');
expect(sanitizeInlineHtml('x'))
.toBe('x');
});
test('non-_blank targets and stray rel are dropped', () => {
expect(sanitizeInlineHtml('x')).toBe('x');
});
test('href/target/rel on a non-anchor are stripped', () => {
expect(sanitizeInlineHtml('x')).toBe('x');
});
test('safe relative, mailto and https links survive', () => {
expect(sanitizeInlineHtml('m')).toBe('m');
expect(sanitizeInlineHtml('q')).toBe('q');
});
test('a link carrying an event handler keeps only the href', () => {
expect(sanitizeInlineHtml('x')).toBe('x');
});
test('quote breakout in an attribute cannot inject markup', () => {
const out = sanitizeInlineHtml('x');
const tpl = document.createElement('template');
tpl.innerHTML = out;
expect(tpl.content.querySelector('a')!.getAttributeNames()).toEqual(['href']);
});
test('output is idempotent', () => {
const once = sanitizeInlineHtml('x yz
');
expect(sanitizeInlineHtml(once)).toBe(once);
});
});
describe('text <-> inline html', () => {
test.each(['plain', 'ae', 'line 1\nline 2', '<already>', '', 'x\n\ny'])(
'round-trip inlineHtmlToText(textToInlineHtml(%j))',
(s) => {
expect(inlineHtmlToText(textToInlineHtml(s))).toBe(s);
},
);
test('inlineHtmlToText strips tags, decodes entities,
-> newline', () => {
expect(inlineHtmlToText('Hello World')).toBe('Hello World');
expect(inlineHtmlToText('a & b
c <d>')).toBe('a & b\nc ');
});
test('textToInlineHtml escapes and turns newlines into
', () => {
expect(textToInlineHtml('ac');
});
test('isPlainInlineHtml: only text and
count as no marks', () => {
expect(isPlainInlineHtml('a & b
c')).toBe(true);
expect(isPlainInlineHtml('a b')).toBe(false);
expect(isPlainInlineHtml('
')).toBe(true);
});
});