You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
130 lines
3.6 KiB
130 lines
3.6 KiB
/**
|
|
* R3B — serialize-html-v2 suite.
|
|
*
|
|
* Verifies: HTML output is well-formed, escapes correctly, preserves
|
|
* inline styles from `block.visual.*`.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { serializeHtmlV2 } from './serialize-html-v2';
|
|
import { WORDS_DOCUMENT_VERSION_V2, type WordsDocumentV2 } from './types-v2';
|
|
|
|
function doc(...children: WordsDocumentV2['children']): WordsDocumentV2 {
|
|
return { version: WORDS_DOCUMENT_VERSION_V2, children };
|
|
}
|
|
|
|
describe('serializeHtmlV2', () => {
|
|
it('paragraph with plain text', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({ type: 'paragraph', children: [{ type: 'text', text: 'hello' }] })
|
|
);
|
|
expect(html).toContain('<p ');
|
|
expect(html).toContain('data-words-block="paragraph"');
|
|
expect(html).toContain('>hello</p>');
|
|
});
|
|
|
|
it('escapes &, <, > in text content', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({ type: 'paragraph', children: [{ type: 'text', text: '<script>&</script>' }] })
|
|
);
|
|
expect(html).toContain('<script>&amp;</script>');
|
|
expect(html).not.toContain('<script>');
|
|
});
|
|
|
|
it('escapes " and & in attribute values', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({
|
|
type: 'paragraph',
|
|
children: [
|
|
{
|
|
type: 'link',
|
|
href: 'https://example.com?a=1&b="2"',
|
|
children: [{ type: 'text', text: 'link' }]
|
|
}
|
|
]
|
|
})
|
|
);
|
|
expect(html).toContain('href="https://example.com?a=1&b="2""');
|
|
});
|
|
|
|
it('emits <hr /> for divider (void element)', () => {
|
|
const html = serializeHtmlV2(doc({ type: 'divider' }));
|
|
expect(html).toContain('<hr ');
|
|
expect(html).toContain('/>');
|
|
expect(html).not.toContain('</hr>');
|
|
});
|
|
|
|
it('emits <img /> as void element', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({ type: 'image', src: 'https://x/cat.png', alt: 'cat' })
|
|
);
|
|
expect(html).toContain('<img src="https://x/cat.png"');
|
|
expect(html).toContain('alt="cat"');
|
|
expect(html).toContain('/>');
|
|
});
|
|
|
|
it('preserves inline styles from visual', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({
|
|
type: 'paragraph',
|
|
children: [{ type: 'text', text: 'x' }],
|
|
visual: { background: '#fafafa', padding: 12 }
|
|
})
|
|
);
|
|
// Canonical visualToStyle order: padding (layout) → background (fill)
|
|
expect(html).toContain('style="padding:12px;background-color:#fafafa"');
|
|
});
|
|
|
|
it('color mark produces <span style="...">', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({
|
|
type: 'paragraph',
|
|
children: [
|
|
{ type: 'text', text: 'red text', marks: [{ type: 'color', value: '#ff0000' }] }
|
|
]
|
|
})
|
|
);
|
|
expect(html).toContain('<span style="color:#ff0000">red text</span>');
|
|
});
|
|
|
|
it('callout emits <div role="note">', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({
|
|
type: 'callout',
|
|
intent: 'risk',
|
|
children: [{ type: 'paragraph', children: [{ type: 'text', text: 'warning' }] }]
|
|
})
|
|
);
|
|
expect(html).toContain('role="note"');
|
|
expect(html).toContain('data-words-callout-intent="risk"');
|
|
});
|
|
|
|
it('pretty mode adds newlines and indentation', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({
|
|
type: 'callout',
|
|
intent: 'neutral',
|
|
children: [{ type: 'paragraph', children: [{ type: 'text', text: 'x' }] }]
|
|
}),
|
|
{ pretty: true }
|
|
);
|
|
expect(html).toContain('\n');
|
|
expect(html).toContain(' <p');
|
|
});
|
|
|
|
it('table with header row emits <th> + <td>', () => {
|
|
const html = serializeHtmlV2(
|
|
doc({
|
|
type: 'table',
|
|
headerRow: true,
|
|
rows: [
|
|
{ cells: [{ children: [{ type: 'text', text: 'h1' }] }] },
|
|
{ cells: [{ children: [{ type: 'text', text: 'a' }] }] }
|
|
]
|
|
})
|
|
);
|
|
expect(html).toMatch(/<th [^>]*>h1<\/th>/);
|
|
expect(html).toMatch(/<td [^>]*>a<\/td>/);
|
|
});
|
|
});
|