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
', 'outer
inner'], ['a
b
', 'a
b'], ['t', 't'], ['t', 't'], ['t', 't'], ['x', 'x'], ['<script>alert(1)</script>', '<script>alert(1)</script>'], ['t', 't'], ['x

unwrapped

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