dorfteich/packages/shared/src/editor-schema/html.test.ts
Claude Fable 5 58c19abfdd #169: Nicht-Text-Inhalte — Task-Checkboxen, Wissensgraph
Task-Checkboxen tragen in beiden Renderpfaden einen Namen: docToHtml
setzt aria-label aus dem Aufgabentext, die Editor-NodeView ebenso. Die
NodeView rendert ihr Host-Element jetzt selbst als li (ReactNodeView-
Renderer as/attrs) — TipTaps zusätzliches div-Host-Element zwischen ul
und li brach die Listensemantik; der Wrapper flacht per display:contents
ab, die #137-Pixel-Abstimmung bleibt erhalten (Selektor auf die neue
Tiefe nachgeführt, Ausrichtung nachgemessen: 1px-Versatz unverändert).
Der Wissensgraph-SVG bekommt ein beschreibendes aria-label inklusive
Verweis auf die Backlinks als gleichwertige Listenform. Der
Bild-Alt-Editor existierte bereits (Bild-Controls bei Auswahl) — kein
Änderungsbedarf. Hinweis: gecachte Seiten übernehmen das
Checkbox-Label wie bei jeder docToHtml-Änderung erst mit dem nächsten
Persist ihrer Inhalte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:35:16 +02:00

78 lines
3.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { docToHtml } from './html';
import { markdownToDoc } from './markdown';
import { editorSchema } from './schema';
describe('docToHtml (issue #24)', () => {
it('escapes text content, including angle brackets and quotes', () => {
const doc = markdownToDoc('Contains <script>alert("x")</script> literally.');
const html = docToHtml(doc);
expect(html).not.toContain('<script>');
expect(html).toContain('&lt;script&gt;');
expect(html).toContain('&quot;x&quot;');
});
it('renders inline marks and a table', () => {
const doc = markdownToDoc('**bold** and *italic* and `code`');
expect(docToHtml(doc)).toBe(
'<p><strong>bold</strong> and <em>italic</em> and <code>code</code></p>',
);
const table = markdownToDoc('| A | B |\n| --- | --- |\n| 1 | 2 |');
expect(docToHtml(table)).toBe(
'<table><tr><th><p>A</p></th><th><p>B</p></th></tr><tr><td><p>1</p></td><td><p>2</p></td></tr></table>',
);
});
it('allowlists link protocols, neutralizing javascript: hrefs', () => {
const safe = markdownToDoc('[go](https://example.org)');
expect(docToHtml(safe)).toContain('href="https://example.org"');
// markdown-it itself already refuses to tokenize `javascript:` links
// (falls back to plain text), so the schema is built directly here to
// exercise docToHtml's own allowlist (security.md) independently of
// that upstream defense.
const linkMark = editorSchema.marks.link.create({ href: 'javascript:evil' });
const doc = editorSchema.node('doc', null, [
editorSchema.node('paragraph', null, [editorSchema.text('click me', [linkMark])]),
]);
const html = docToHtml(doc);
expect(html).not.toContain('javascript:');
expect(html).toContain('href="#"');
});
it('renders a plugin block as a data-carrying placeholder (issue #76)', () => {
const block = editorSchema.nodes.plugin_block.create({
pluginId: 'mermaid',
blockType: 'diagram',
data: { source: 'A-->B "quoted" <tag>' },
});
const html = docToHtml(editorSchema.node('doc', null, [block]));
expect(html).toContain('data-plugin-block="mermaid/diagram"');
// The data JSON is attribute-escaped — no raw quotes or angle brackets.
expect(html).toContain('&quot;quoted\\&quot;');
expect(html).not.toContain('<tag>');
expect(html).toContain('[mermaid/diagram]');
});
it('renders task list checkboxes with their checked state', () => {
const doc = markdownToDoc('- [ ] Todo\n- [x] Done');
const html = docToHtml(doc);
expect(html).toContain('data-checked="false"');
expect(html).toContain('data-checked="true"');
// The item text names the checkbox (#169, WCAG 4.1.2).
expect(html).toContain('<input type="checkbox" disabled checked aria-label="Done">');
expect(html).toContain('<input type="checkbox" disabled aria-label="Todo">');
});
it('gives wikilinks a relative href so public/static HTML is clickable', () => {
const html = docToHtml(markdownToDoc('See [[api-guide|the API guide]].'));
// Relative slug href resolves to the sibling page under both
// /public/<pond>/… and /p/<pond>/… without pond context in the renderer.
expect(html).toContain(
'<a class="wikilink" href="api-guide" data-wikilink="api-guide" data-display="the API guide">the API guide</a>',
);
});
});