/**
* RENDER-level sanitizer fixtures — the counterpart to
* ./sanitize-invariant.test.ts, which asserts the SCHEMA.
*
* Why both: the schema is not the observable. `hast-util-sanitize` falls back
* from `attributes[tagName]` to `attributes['*']` for any property the per-tag
* list omits, so a schema assertion ("`attributes.input` omits `name`") can be
* green while the attribute still lands in the DOM. That exact gap shipped —
* `attributes.input` was documented as "EXACTLY the GFM task-list contract and
* nothing else" and `` kept both attributes,
* because defaultSchema carries the whole form vocabulary on `*`.
*
* So the rule these fixtures encode: for anything that matters, assert on what
* comes out of `SimpleMarkdownRenderer` (the CHAT surface — untrusted model
* output), not on the schema that is supposed to produce it.
*/
import React from 'react'
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { SimpleMarkdownRenderer } from '../index'
describe('sanitizer — rendered output on the chat surface', () => {
it('renders a credential form INERT: no action, no method, no named inputs', () => {
const md =
'
'}
/>,
)
// Both attributes are absent — and were absent BEFORE `*` was narrowed,
// because the base renderers rebuild these elements. The content itself is
// untouched, which is the property that matters.
expect(container.querySelector('a')?.textContent).toBe('anchor')
expect(container.querySelector('a')?.getAttribute('name')).toBeNull()
expect(container.querySelector('li')?.textContent).toBe('three')
expect(container.querySelector('li')?.getAttribute('value')).toBeNull()
})
})
/**
* UI-REDRESS / OVERLAY class. `style` is allowed on the tags a content audit
* found using it, but the POSITIONING subset of CSS is not decoration — it lifts
* untrusted markup out of the message body and puts it over the application.
*
* All three payloads below rendered verbatim before the guard: an
* attacker-controlled cross-origin document (or a plain opaque ``)
* covering the whole viewport above every piece of app chrome, from ONE chat
* message, i.e. from model output.
*/
describe('sanitizer — positioning styles cannot lift content out of the flow', () => {
const OVERLAYS: Record = {
'cross-origin iframe':
'',
'bare span': 'fake ui',
'div with sticky': '
bar
',
}
for (const [name, md] of Object.entries(OVERLAYS)) {
it(`strips the positioning declarations: ${name}`, () => {
const { container } = render()
const html = container.innerHTML
expect(html).not.toMatch(/position\s*:\s*(fixed|absolute|sticky)/i)
expect(html).not.toMatch(/z-index/i)
expect(html).not.toMatch(/\binset\s*:/i)
})
}
it('keeps ordinary decorative styling on the same element', () => {
const { container } = render(
note'}
/>,
)
const style = container.querySelector('div[style]')?.getAttribute('style') ?? ''
expect(style).toMatch(/background/)
expect(style).toMatch(/padding/)
expect(style).toMatch(/text-align/)
expect(style).not.toMatch(/position/)
})
})