import { describe, test, expect } from 'vitest';
import { purifyHtml } from './HtmlBlock';
// Vite/Vitest `?raw` import -- ships the exact bytes of the file as a
// string, declared by node_modules/vite/client.d.ts. This is a checked-in
// copy of the reference acceptance fixture used for Task 24 (widening the
// Custom HTML block's sanitiser allow-list); keep it byte-identical to the
// external fixture used to drive this task so these tests cannot silently
// drift from the thing they are supposed to be testing against.
import fixtureHtml from './__fixtures__/html-block-test-body.html?raw';
/**
* Task 24: the site owner tested a broad HTML fixture against the shipped
* sanitiser config and found 38% of it silently deleted -- merged table
* cells collapsing (colspan/rowspan/scope stripped),
///
* inline // dropped wholesale, lang/dir/role stripped
* (breaking RTL rendering), flattened. The fix widens
* ALLOWED_TAGS/ALLOWED_ATTR in HtmlBlock.tsx. These tests run the *actual*
* reference fixture through the *actual* purifyHtml() and assert the
* previously-broken constructs now survive with their meaningful
* attributes intact, while re-confirming (with attack payloads spliced
* into the newly-widened surface -- forms, media, inline svg) that the
* four non-negotiable security properties still hold.
*/
describe('purifyHtml -- Task 24 fixture regression (formerly-dropped constructs survive)', () => {
const out = purifyHtml(fixtureHtml);
test('table merged cells keep colspan/rowspan/scope', () => {
expect(out).toContain('');
expect(out).toContain(' ');
expect(out).toContain(' ');
expect(out).toContain(' ');
});
test('definition list keeps its dl/dt/dd structure (was flattened to "TermDef")', () => {
expect(out).toMatch(/[\s\S]*Term one<\/dt>[\s\S]* Definition of the first term\.<\/dd>[\s\S]*<\/dl>/);
});
test('menu list survives with nested buttons', () => {
expect(out).toMatch(/[\s\S]*Copy<\/button>[\s\S]*<\/menu>/);
});
test('sub/sup survive (was flattened to "H2O")', () => {
expect(out).toContain('H2 O');
expect(out).toContain('x2 ');
});
test('details/summary survive with the open attribute (was flattened)', () => {
expect(out).toContain('Collapsed disclosure ');
expect(out).toContain('');
});
test('hgroup survives', () => {
expect(out).toMatch(/[\s\S]*Grouped heading<\/h2>/);
});
test('inline svg survives with its shape children and role/aria-label (was deleted entirely)', () => {
expect(out).toMatch(/]*role="img"[^>]*aria-label="Two shapes"[^>]*>/);
expect(out).toMatch(/]*fill="none"[^>]*stroke="currentColor"[^>]*>/);
expect(out).toMatch(/]*cx="135"[^>]*cy="45"[^>]*r="40"[^>]*>/);
expect(out).toMatch(/]*text-anchor="middle"[^>]*>svg<\/text>/);
});
test('picture/source with media+srcset survive', () => {
expect(out).toContain('');
expect(out).toContain('');
});
test('video/audio survive with source/track children (was deleted entirely)', () => {
expect(out).toMatch(/]*controls=""[^>]*poster="poster\.jpg"[^>]*>/);
expect(out).toContain('');
expect(out).toContain('');
expect(out).toMatch(/]*controls=""[^>]*>/);
});
test('canvas survives with its fallback text', () => {
expect(out).toContain('Canvas fallback text ');
});
test('mark/small/del/ins survive as distinct elements (was flattened to "msdi")', () => {
expect(out).toContain('mark ');
expect(out).toContain('small ');
expect(out).toContain('del');
expect(out).toContain('ins ');
});
test('lang/dir preserved for RTL text (was stripped, breaking Arabic/Hebrew rendering)', () => {
expect(out).toContain('lang="ar" dir="rtl"');
expect(out).toContain('lang="he" dir="rtl"');
});
test('role attribute preserved alongside aria-* (role was stripped)', () => {
expect(out).toMatch(//);
expect(out).toMatch(/role="img"/);
});
test('ol start/reversed preserved (was flattened to plain )', () => {
expect(out).toContain('');
});
test('text semantics survive: abbr/cite/q/time/data/kbd/samp/var/dfn/address/bdi/bdo/ruby', () => {
expect(out).toContain('HTML ');
expect(out).toContain('Ctrl ');
expect(out).toContain('output text ');
expect(out).toContain('variable ');
expect(out).toContain('definition term ');
expect(out).toContain('');
expect(out).toContain('');
expect(out).toContain('');
expect(out).toContain('');
expect(out).toContain('kan ');
expect(out).toContain('');
expect(out).toContain('');
});
test('wbr survives (word-break opportunity)', () => {
expect(out).toContain('supercalifragilistic');
});
test('hidden attribute survives', () => {
expect(out).toContain('');
});
test('forms survive end-to-end: fieldset/legend/label/select/optgroup/option/textarea/datalist/output/progress/meter', () => {
expect(out).toContain('
');
expect(out).not.toMatch(/