Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/content_editor/services/markdown_serializer_spec.js')
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js1008
1 files changed, 1008 insertions, 0 deletions
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
new file mode 100644
index 00000000000..6f2c908c289
--- /dev/null
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -0,0 +1,1008 @@
+import Blockquote from '~/content_editor/extensions/blockquote';
+import Bold from '~/content_editor/extensions/bold';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import Code from '~/content_editor/extensions/code';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import DescriptionItem from '~/content_editor/extensions/description_item';
+import DescriptionList from '~/content_editor/extensions/description_list';
+import Division from '~/content_editor/extensions/division';
+import Emoji from '~/content_editor/extensions/emoji';
+import Figure from '~/content_editor/extensions/figure';
+import FigureCaption from '~/content_editor/extensions/figure_caption';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Image from '~/content_editor/extensions/image';
+import InlineDiff from '~/content_editor/extensions/inline_diff';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import Paragraph from '~/content_editor/extensions/paragraph';
+import Strike from '~/content_editor/extensions/strike';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableHeader from '~/content_editor/extensions/table_header';
+import TableRow from '~/content_editor/extensions/table_row';
+import TaskItem from '~/content_editor/extensions/task_item';
+import TaskList from '~/content_editor/extensions/task_list';
+import Text from '~/content_editor/extensions/text';
+import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+jest.mock('~/emoji');
+
+jest.mock('~/content_editor/services/feature_flags', () => ({
+ isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true),
+}));
+
+const tiptapEditor = createTestEditor({
+ extensions: [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ DescriptionItem,
+ DescriptionList,
+ Division,
+ Emoji,
+ Figure,
+ FigureCaption,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ Image,
+ InlineDiff,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ Paragraph,
+ Strike,
+ Table,
+ TableCell,
+ TableHeader,
+ TableRow,
+ TaskItem,
+ TaskList,
+ Text,
+ ],
+});
+
+const {
+ builders: {
+ doc,
+ blockquote,
+ bold,
+ bulletList,
+ code,
+ codeBlock,
+ division,
+ descriptionItem,
+ descriptionList,
+ emoji,
+ figure,
+ figureCaption,
+ heading,
+ hardBreak,
+ horizontalRule,
+ image,
+ inlineDiff,
+ italic,
+ link,
+ listItem,
+ orderedList,
+ paragraph,
+ strike,
+ table,
+ tableCell,
+ tableHeader,
+ tableRow,
+ taskItem,
+ taskList,
+ },
+} = createDocBuilder({
+ tiptapEditor,
+ names: {
+ blockquote: { nodeType: Blockquote.name },
+ bold: { markType: Bold.name },
+ bulletList: { nodeType: BulletList.name },
+ code: { markType: Code.name },
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ division: { nodeType: Division.name },
+ descriptionItem: { nodeType: DescriptionItem.name },
+ descriptionList: { nodeType: DescriptionList.name },
+ emoji: { markType: Emoji.name },
+ figure: { nodeType: Figure.name },
+ figureCaption: { nodeType: FigureCaption.name },
+ hardBreak: { nodeType: HardBreak.name },
+ heading: { nodeType: Heading.name },
+ horizontalRule: { nodeType: HorizontalRule.name },
+ image: { nodeType: Image.name },
+ inlineDiff: { markType: InlineDiff.name },
+ italic: { nodeType: Italic.name },
+ link: { markType: Link.name },
+ listItem: { nodeType: ListItem.name },
+ orderedList: { nodeType: OrderedList.name },
+ paragraph: { nodeType: Paragraph.name },
+ strike: { markType: Strike.name },
+ table: { nodeType: Table.name },
+ tableCell: { nodeType: TableCell.name },
+ tableHeader: { nodeType: TableHeader.name },
+ tableRow: { nodeType: TableRow.name },
+ taskItem: { nodeType: TaskItem.name },
+ taskList: { nodeType: TaskList.name },
+ },
+});
+
+const serialize = (...content) =>
+ markdownSerializer({}).serialize({
+ schema: tiptapEditor.schema,
+ content: doc(...content).toJSON(),
+ });
+
+describe('markdownSerializer', () => {
+ it('correctly serializes bold', () => {
+ expect(serialize(paragraph(bold('bold')))).toBe('**bold**');
+ });
+
+ it('correctly serializes italics', () => {
+ expect(serialize(paragraph(italic('italics')))).toBe('_italics_');
+ });
+
+ it('correctly serializes inline diff', () => {
+ expect(
+ serialize(
+ paragraph(
+ inlineDiff({ type: 'addition' }, '+30 lines'),
+ inlineDiff({ type: 'deletion' }, '-10 lines'),
+ ),
+ ),
+ ).toBe('{++30 lines+}{--10 lines-}');
+ });
+
+ it('correctly serializes a line break', () => {
+ expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
+ });
+
+ it('correctly serializes a link', () => {
+ expect(serialize(paragraph(link({ href: 'https://example.com' }, 'example url')))).toBe(
+ '[example url](https://example.com)',
+ );
+ });
+
+ it('correctly serializes a plain URL link', () => {
+ expect(serialize(paragraph(link({ href: 'https://example.com' }, 'https://example.com')))).toBe(
+ '<https://example.com>',
+ );
+ });
+
+ it('correctly serializes a link with a title', () => {
+ expect(
+ serialize(
+ paragraph(link({ href: 'https://example.com', title: 'click this link' }, 'example url')),
+ ),
+ ).toBe('[example url](https://example.com "click this link")');
+ });
+
+ it('correctly serializes a plain URL link with a title', () => {
+ expect(
+ serialize(
+ paragraph(
+ link({ href: 'https://example.com', title: 'link title' }, 'https://example.com'),
+ ),
+ ),
+ ).toBe('[https://example.com](https://example.com "link title")');
+ });
+
+ it('correctly serializes a link with a canonicalSrc', () => {
+ expect(
+ serialize(
+ paragraph(
+ link(
+ {
+ href: '/uploads/abcde/file.zip',
+ canonicalSrc: 'file.zip',
+ title: 'click here to download',
+ },
+ 'download file',
+ ),
+ ),
+ ),
+ ).toBe('[download file](file.zip "click here to download")');
+ });
+
+ it('correctly serializes strikethrough', () => {
+ expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
+ });
+
+ it('correctly serializes blockquotes with hard breaks', () => {
+ expect(serialize(blockquote('some text', hardBreak(), hardBreak(), 'new line'))).toBe(
+ `
+> some text\\
+> \\
+> new line
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes blockquote with multiple block nodes', () => {
+ expect(serialize(blockquote(paragraph('some paragraph'), codeBlock('var x = 10;')))).toBe(
+ `
+> some paragraph
+>
+> \`\`\`
+> var x = 10;
+> \`\`\`
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a multiline blockquote', () => {
+ expect(
+ serialize(
+ blockquote(
+ { multiline: true },
+ paragraph('some paragraph with ', bold('bold')),
+ codeBlock('var y = 10;'),
+ ),
+ ),
+ ).toBe(
+ `
+>>>
+some paragraph with **bold**
+
+\`\`\`
+var y = 10;
+\`\`\`
+
+>>>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a code block with language', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'json' },
+ 'this is not really json but just trying out whether this case works or not',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`json
+this is not really json but just trying out whether this case works or not
+\`\`\`
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes emoji', () => {
+ expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
+ });
+
+ it('correctly serializes headings', () => {
+ expect(
+ serialize(
+ heading({ level: 1 }, 'Heading 1'),
+ heading({ level: 2 }, 'Heading 2'),
+ heading({ level: 3 }, 'Heading 3'),
+ heading({ level: 4 }, 'Heading 4'),
+ heading({ level: 5 }, 'Heading 5'),
+ heading({ level: 6 }, 'Heading 6'),
+ ),
+ ).toBe(
+ `
+# Heading 1
+
+## Heading 2
+
+### Heading 3
+
+#### Heading 4
+
+##### Heading 5
+
+###### Heading 6
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes horizontal rule', () => {
+ expect(serialize(horizontalRule(), horizontalRule(), horizontalRule())).toBe(
+ `
+---
+
+---
+
+---
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes an image', () => {
+ expect(serialize(paragraph(image({ src: 'img.jpg', alt: 'foo bar' })))).toBe(
+ '![foo bar](img.jpg)',
+ );
+ });
+
+ it('correctly serializes an image with a title', () => {
+ expect(serialize(paragraph(image({ src: 'img.jpg', title: 'baz', alt: 'foo bar' })))).toBe(
+ '![foo bar](img.jpg "baz")',
+ );
+ });
+
+ it('correctly serializes an image with a canonicalSrc', () => {
+ expect(
+ serialize(
+ paragraph(
+ image({
+ src: '/uploads/abcde/file.png',
+ alt: 'this is an image',
+ canonicalSrc: 'file.png',
+ title: 'foo bar baz',
+ }),
+ ),
+ ),
+ ).toBe('![this is an image](file.png "foo bar baz")');
+ });
+
+ it('correctly serializes bullet list', () => {
+ expect(
+ serialize(
+ bulletList(
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+* list item 1
+* list item 2
+* list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes bullet list with different bullet styles', () => {
+ expect(
+ serialize(
+ bulletList(
+ { bullet: '+' },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(
+ paragraph('list item 3'),
+ bulletList(
+ { bullet: '-' },
+ listItem(paragraph('sub-list item 1')),
+ listItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
++ list item 1
++ list item 2
++ list item 3
+ - sub-list item 1
+ - sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list', () => {
+ expect(
+ serialize(
+ orderedList(
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+1. list item 1
+2. list item 2
+3. list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list with parens', () => {
+ expect(
+ serialize(
+ orderedList(
+ { parens: true },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+1) list item 1
+2) list item 2
+3) list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list with a different start order', () => {
+ expect(
+ serialize(
+ orderedList(
+ { start: 17 },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+17. list item 1
+18. list item 2
+19. list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list with an invalid start order', () => {
+ expect(
+ serialize(
+ orderedList(
+ { start: NaN },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+1. list item 1
+2. list item 2
+3. list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a bullet list inside an ordered list', () => {
+ expect(
+ serialize(
+ orderedList(
+ { start: 17 },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(
+ paragraph('list item 3'),
+ bulletList(
+ listItem(paragraph('sub-list item 1')),
+ listItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ // notice that 4 space indent works fine in this case,
+ // when it usually wouldn't
+ `
+17. list item 1
+18. list item 2
+19. list item 3
+ * sub-list item 1
+ * sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a task list', () => {
+ expect(
+ serialize(
+ taskList(
+ taskItem({ checked: true }, paragraph('list item 1')),
+ taskItem(paragraph('list item 2')),
+ taskItem(
+ paragraph('list item 3'),
+ taskList(
+ taskItem({ checked: true }, paragraph('sub-list item 1')),
+ taskItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+* [x] list item 1
+* [ ] list item 2
+* [ ] list item 3
+ * [x] sub-list item 1
+ * [ ] sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric task list + with start order', () => {
+ expect(
+ serialize(
+ taskList(
+ { numeric: true },
+ taskItem({ checked: true }, paragraph('list item 1')),
+ taskItem(paragraph('list item 2')),
+ taskItem(
+ paragraph('list item 3'),
+ taskList(
+ { numeric: true, start: 1351, parens: true },
+ taskItem({ checked: true }, paragraph('sub-list item 1')),
+ taskItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+1. [x] list item 1
+2. [ ] list item 2
+3. [ ] list item 3
+ 1351) [x] sub-list item 1
+ 1352) [ ] sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly renders a description list', () => {
+ expect(
+ serialize(
+ descriptionList(
+ descriptionItem(paragraph('Beast of Bodmin')),
+ descriptionItem({ isTerm: false }, paragraph('A large feline inhabiting Bodmin Moor.')),
+
+ descriptionItem(paragraph('Morgawr')),
+ descriptionItem({ isTerm: false }, paragraph('A sea serpent.')),
+
+ descriptionItem(paragraph('Owlman')),
+ descriptionItem(
+ { isTerm: false },
+ paragraph('A giant ', italic('owl-like'), ' creature.'),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+<dl>
+<dt>Beast of Bodmin</dt>
+<dd>A large feline inhabiting Bodmin Moor.</dd>
+<dt>Morgawr</dt>
+<dd>A sea serpent.</dd>
+<dt>Owlman</dt>
+<dd>
+
+A giant _owl-like_ creature.
+
+</dd>
+</dl>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders div', () => {
+ expect(
+ serialize(
+ division(paragraph('just a paragraph in a div')),
+ division(paragraph('just some ', bold('styled'), ' ', italic('content'), ' in a div')),
+ ),
+ ).toBe(
+ '<div>just a paragraph in a div</div>\n<div>\n\njust some **styled** _content_ in a div\n\n</div>',
+ );
+ });
+
+ it('correctly renders figure', () => {
+ expect(
+ serialize(
+ figure(
+ paragraph(image({ src: 'elephant.jpg', alt: 'An elephant at sunset' })),
+ figureCaption('An elephant at sunset'),
+ ),
+ ),
+ ).toBe(
+ `
+<figure>
+
+![An elephant at sunset](elephant.jpg)
+
+<figcaption>An elephant at sunset</figcaption>
+</figure>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders figure with styled caption', () => {
+ expect(
+ serialize(
+ figure(
+ paragraph(image({ src: 'elephant.jpg', alt: 'An elephant at sunset' })),
+ figureCaption(italic('An elephant at sunset')),
+ ),
+ ),
+ ).toBe(
+ `
+<figure>
+
+![An elephant at sunset](elephant.jpg)
+
+<figcaption>
+
+_An elephant at sunset_
+
+</figcaption>
+</figure>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with inline content', () => {
+ expect(
+ serialize(
+ table(
+ // each table cell must contain at least one paragraph
+ tableRow(
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ ),
+ tableRow(
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ ),
+ tableRow(
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header | header |
+|--------|--------|--------|
+| cell | cell | cell |
+| cell | cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with line breaks', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(
+ tableCell(paragraph('cell with', hardBreak(), 'line', hardBreak(), 'breaks')),
+ tableCell(paragraph('cell')),
+ ),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header |
+|--------|--------|
+| cell with<br>line<br>breaks | cell |
+| cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes two consecutive tables', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header |
+|--------|--------|
+| cell | cell |
+| cell | cell |
+
+| header | header |
+|--------|--------|
+| cell | cell |
+| cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with block content', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(
+ tableHeader(paragraph('examples of')),
+ tableHeader(paragraph('block content')),
+ tableHeader(paragraph('in tables')),
+ tableHeader(paragraph('in content editor')),
+ ),
+ tableRow(
+ tableCell(heading({ level: 1 }, 'heading 1')),
+ tableCell(heading({ level: 2 }, 'heading 2')),
+ tableCell(paragraph(bold('just bold'))),
+ tableCell(paragraph(bold('bold'), ' ', italic('italic'), ' ', code('code'))),
+ ),
+ tableRow(
+ tableCell(
+ paragraph('all marks in three paragraphs:'),
+ paragraph('the ', bold('quick'), ' ', italic('brown'), ' ', code('fox')),
+ paragraph(
+ link({ href: '/home' }, 'jumps'),
+ ' over the ',
+ strike('lazy'),
+ ' ',
+ emoji({ name: 'dog' }),
+ ),
+ ),
+ tableCell(
+ paragraph(image({ src: 'img.jpg', alt: 'some image' }), hardBreak(), 'image content'),
+ ),
+ tableCell(
+ blockquote('some text', hardBreak(), hardBreak(), 'in a multiline blockquote'),
+ ),
+ tableCell(
+ codeBlock(
+ { language: 'javascript' },
+ 'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);',
+ ),
+ ),
+ ),
+ tableRow(
+ tableCell(bulletList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
+ tableCell(orderedList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
+ tableCell(
+ paragraph('paragraphs separated by'),
+ horizontalRule(),
+ paragraph('a horizontal rule'),
+ ),
+ tableCell(
+ table(
+ tableRow(tableHeader(paragraph('table')), tableHeader(paragraph('inside'))),
+ tableRow(tableCell(paragraph('another')), tableCell(paragraph('table'))),
+ ),
+ ),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>examples of</th>
+<th>block content</th>
+<th>in tables</th>
+<th>in content editor</th>
+</tr>
+<tr>
+<td>
+
+# heading 1
+</td>
+<td>
+
+## heading 2
+</td>
+<td>
+
+**just bold**
+</td>
+<td>
+
+**bold** _italic_ \`code\`
+</td>
+</tr>
+<tr>
+<td>
+
+all marks in three paragraphs:
+
+the **quick** _brown_ \`fox\`
+
+[jumps](/home) over the ~~lazy~~ :dog:
+</td>
+<td>
+
+![some image](img.jpg)<br>image content
+</td>
+<td>
+
+> some text\\
+> \\
+> in a multiline blockquote
+</td>
+<td>
+
+\`\`\`javascript
+var a = 2;
+var b = 3;
+var c = a + d;
+
+console.log(c);
+\`\`\`
+</td>
+</tr>
+<tr>
+<td>
+
+* item 1
+* item 2
+* item 2
+</td>
+<td>
+
+1. item 1
+2. item 2
+3. item 2
+</td>
+<td>
+
+paragraphs separated by
+
+---
+
+a horizontal rule
+</td>
+<td>
+
+| table | inside |
+|-------|--------|
+| another | table |
+
+</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders content after a markdown table', () => {
+ expect(
+ serialize(
+ table(tableRow(tableHeader(paragraph('header'))), tableRow(tableCell(paragraph('cell')))),
+ heading({ level: 1 }, 'this is a heading'),
+ ).trim(),
+ ).toBe(
+ `
+| header |
+|--------|
+| cell |
+
+# this is a heading
+ `.trim(),
+ );
+ });
+
+ it('correctly renders content after an html table', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header'))),
+ tableRow(tableCell(blockquote('hi'), paragraph('there'))),
+ ),
+ heading({ level: 1 }, 'this is a heading'),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>header</th>
+</tr>
+<tr>
+<td>
+
+> hi
+
+there
+</td>
+</tr>
+</table>
+
+# this is a heading
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes tables with misplaced header cells', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableHeader(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>cell</th>
+<td>cell</td>
+</tr>
+<tr>
+<td>cell</td>
+<th>cell</th>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes table without any headers', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<td>cell</td>
+<td>cell</td>
+</tr>
+<tr>
+<td>cell</td>
+<td>cell</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes table with rowspan and colspan', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ ),
+ tableRow(
+ tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2')),
+ tableCell({ rowspan: 2 }, paragraph('cell')),
+ ),
+ tableRow(tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>header</th>
+<th>header</th>
+<th>header</th>
+</tr>
+<tr>
+<td colspan="2">cell with rowspan: 2</td>
+<td rowspan="2">cell</td>
+</tr>
+<tr>
+<td colspan="2">cell with rowspan: 2</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+});