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')
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_spec.js2
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js14
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js21
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap115
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js84
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js2
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec_helper.js4
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js238
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js4
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js144
-rw-r--r--spec/frontend/content_editor/services/table_of_contents_utils_spec.js96
14 files changed, 669 insertions, 63 deletions
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
index ba6d8da9584..93204deb68c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
@@ -182,7 +182,7 @@ describe('content_editor/components/bubble_menus/link', () => {
it('updates prosemirror doc with new link', async () => {
expect(tiptapEditor.getHTML()).toBe(
- '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google" canonicalsrc="https://google.com">PDF File</a></p>',
+ '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google">PDF File</a></p>',
);
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
index 8839caea80e..fada4f06743 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
@@ -14,7 +14,7 @@ import {
} from '../../test_constants';
const TIPTAP_IMAGE_HTML = `<p>
- <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png">
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_AUDIO_HTML = `<p>
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 9ee3b017831..0ba2672100b 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -19,6 +19,7 @@ describe('ContentEditor', () => {
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
+ const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const createWrapper = (propsData = {}) => {
renderMarkdown = jest.fn();
@@ -119,4 +120,17 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(FormattingBubbleMenu).exists()).toBe(true);
});
+
+ it.each`
+ event
+ ${'loading'}
+ ${'loadingSuccess'}
+ ${'loadingError'}
+ `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => {
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit(event);
+
+ expect(wrapper.emitted(event)).toHaveLength(1);
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
index 351fd967719..62fec8d4e72 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -37,16 +37,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
describe.each`
- name | contentType | command | params
- ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
- ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
- ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
- ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
- ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
- ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
- ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
- ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
- `('when option $label is clicked', ({ name, command, contentType, params }) => {
+ name | contentType | command | params
+ ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
+ ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
+ ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
+ ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
+ ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
+ ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
+ ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
+ ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
+ ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
+ `('when option $name is clicked', ({ name, command, contentType, params }) => {
let commands;
let btn;
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index 2acb6e14ce0..8f194ff32e2 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -32,7 +32,7 @@ describe('content_editor/components/top_toolbar', () => {
${'link'} | ${{}}
${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
- ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a task list', editorCommand: 'toggleTaskList' }}
+ ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
${'image'} | ${{}}
${'table'} | ${{}}
${'more'} | ${{}}
diff --git a/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
new file mode 100644
index 00000000000..fb091419ad9
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`content/components/wrappers/table_of_contents collects all headings and renders a nested list of headings 1`] = `
+<div
+ class="table-of-contents gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5 gl-p-4!"
+ data-testid="table-of-contents"
+>
+
+ Table of contents
+
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.1
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.1.1
+
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.2
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.2.1
+
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.3
+
+ </a>
+
+ <!---->
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.4
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.4.1
+
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 2
+
+ </a>
+
+ <!---->
+ </li>
+</div>
+`;
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index 6017a145a87..1fdddce3962 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -1,12 +1,12 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
-import { selectedRect as getSelectedRect } from 'prosemirror-tables';
+import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
-jest.mock('prosemirror-tables');
+jest.mock('@_ueberdosis/prosemirror-tables');
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
diff --git a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
new file mode 100644
index 00000000000..bfda89a8b09
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
@@ -0,0 +1,84 @@
+import { nextTick } from 'vue';
+import { NodeViewWrapper } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Heading from '~/content_editor/extensions/heading';
+import Diagram from '~/content_editor/extensions/diagram';
+import TableOfContentsWrapper from '~/content_editor/components/wrappers/table_of_contents.vue';
+import { createTestEditor, createDocBuilder, emitEditorEvent } from '../../test_utils';
+
+describe('content/components/wrappers/table_of_contents', () => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Heading, Diagram] });
+ contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') };
+ eventHub = eventHubFactory();
+ };
+
+ const createWrapper = async () => {
+ wrapper = mountExtended(TableOfContentsWrapper, {
+ propsData: {
+ editor: tiptapEditor,
+ node: {
+ attrs: {},
+ },
+ },
+ stubs: {
+ NodeViewWrapper: stubComponent(NodeViewWrapper),
+ },
+ provide: {
+ contentEditor,
+ tiptapEditor,
+ eventHub,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ createWrapper();
+
+ const {
+ builders: { heading, doc },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ heading: { nodeType: Heading.name },
+ },
+ });
+
+ const initialDoc = doc(
+ heading({ level: 1 }, 'Heading 1'),
+ heading({ level: 2 }, 'Heading 1.1'),
+ heading({ level: 3 }, 'Heading 1.1.1'),
+ heading({ level: 2 }, 'Heading 1.2'),
+ heading({ level: 3 }, 'Heading 1.2.1'),
+ heading({ level: 2 }, 'Heading 1.3'),
+ heading({ level: 2 }, 'Heading 1.4'),
+ heading({ level: 3 }, 'Heading 1.4.1'),
+ heading({ level: 1 }, 'Heading 2'),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ await emitEditorEvent({ event: 'update', tiptapEditor });
+ await nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-wrapper as a ul element', () => {
+ expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('ul');
+ });
+
+ it('collects all headings and renders a nested list of headings', () => {
+ expect(wrapper.findComponent(NodeViewWrapper).element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js
index 256f7bad309..f73b0143fd9 100644
--- a/spec/frontend/content_editor/extensions/image_spec.js
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -35,7 +35,7 @@ describe('content_editor/extensions/image', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
expect(tiptapEditor.getHTML()).toEqual(
- '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image" data-canonical-src="uploads/image.jpg"></p>',
+ '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>',
);
});
});
diff --git a/spec/frontend/content_editor/markdown_processing_spec_helper.js b/spec/frontend/content_editor/markdown_processing_spec_helper.js
index 41442dd8388..228d009e42c 100644
--- a/spec/frontend/content_editor/markdown_processing_spec_helper.js
+++ b/spec/frontend/content_editor/markdown_processing_spec_helper.js
@@ -2,7 +2,6 @@ import fs from 'fs';
import jsYaml from 'js-yaml';
import { memoize } from 'lodash';
import { createContentEditor } from '~/content_editor';
-import { setTestTimeoutOnce } from 'helpers/timeout';
const getFocusedMarkdownExamples = memoize(
() => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [],
@@ -76,9 +75,6 @@ export const describeMarkdownProcessing = (description, markdownYamlPath) => {
}
it(exampleName, async () => {
- if (name === 'frontmatter_toml') {
- setTestTimeoutOnce(2000);
- }
await testSerializesHtmlToMarkdownForElement(example);
});
});
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 48adceaab58..7ae0a7c13c1 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -5,6 +5,7 @@ import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
import HardBreak from '~/content_editor/extensions/hard_break';
import HTMLNodes from '~/content_editor/extensions/html_nodes';
import Heading from '~/content_editor/extensions/heading';
@@ -15,6 +16,7 @@ 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 ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Sourcemap from '~/content_editor/extensions/sourcemap';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
@@ -37,6 +39,7 @@ const tiptapEditor = createTestEditor({
CodeBlockHighlight,
FootnoteDefinition,
FootnoteReference,
+ Frontmatter,
HardBreak,
Heading,
HorizontalRule,
@@ -45,6 +48,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ ReferenceDefinition,
Sourcemap,
Strike,
Table,
@@ -69,6 +73,7 @@ const {
div,
footnoteDefinition,
footnoteReference,
+ frontmatter,
hardBreak,
heading,
horizontalRule,
@@ -78,6 +83,7 @@ const {
listItem,
orderedList,
pre,
+ referenceDefinition,
strike,
table,
tableRow,
@@ -96,6 +102,7 @@ const {
codeBlock: { nodeType: CodeBlockHighlight.name },
footnoteDefinition: { nodeType: FootnoteDefinition.name },
footnoteReference: { nodeType: FootnoteReference.name },
+ frontmatter: { nodeType: Frontmatter.name },
hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
@@ -105,6 +112,7 @@ const {
listItem: { nodeType: ListItem.name },
orderedList: { nodeType: OrderedList.name },
paragraph: { nodeType: Paragraph.name },
+ referenceDefinition: { nodeType: ReferenceDefinition.name },
strike: { nodeType: Strike.name },
table: { nodeType: Table.name },
tableCell: { nodeType: TableCell.name },
@@ -253,7 +261,12 @@ describe('Client side Markdown processing', () => {
expectedDoc: doc(
paragraph(
source('<img src="bar" alt="foo" />'),
- image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
+ image({
+ ...source('<img src="bar" alt="foo" />'),
+ alt: 'foo',
+ canonicalSrc: 'bar',
+ src: 'bar',
+ }),
),
),
},
@@ -271,7 +284,12 @@ describe('Client side Markdown processing', () => {
),
paragraph(
source('<img src="bar" alt="foo" />'),
- image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
+ image({
+ ...source('<img src="bar" alt="foo" />'),
+ alt: 'foo',
+ src: 'bar',
+ canonicalSrc: 'bar',
+ }),
),
),
},
@@ -284,6 +302,7 @@ describe('Client side Markdown processing', () => {
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com',
+ canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
'GitLab',
@@ -302,6 +321,7 @@ describe('Client side Markdown processing', () => {
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com',
+ canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
'GitLab',
@@ -318,6 +338,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('www.commonmark.org'),
+ canonicalSrc: 'http://www.commonmark.org',
href: 'http://www.commonmark.org',
},
'www.commonmark.org',
@@ -334,6 +355,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('www.commonmark.org/help'),
+ canonicalSrc: 'http://www.commonmark.org/help',
href: 'http://www.commonmark.org/help',
},
'www.commonmark.org/help',
@@ -351,6 +373,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('hello+xyz@mail.example'),
+ canonicalSrc: 'mailto:hello+xyz@mail.example',
href: 'mailto:hello+xyz@mail.example',
},
'hello+xyz@mail.example',
@@ -369,6 +392,7 @@ describe('Client side Markdown processing', () => {
{
sourceMapKey: null,
sourceMarkdown: null,
+ canonicalSrc: 'https://gitlab.com',
href: 'https://gitlab.com',
},
'https://gitlab.com',
@@ -398,6 +422,7 @@ hard line break`,
image({
...source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
alt: 'GitLab Logo',
+ canonicalSrc: 'https://gitlab.com/logo.png',
src: 'https://gitlab.com/logo.png',
title: 'GitLab Logo',
}),
@@ -591,7 +616,12 @@ two
paragraph(
source('List item with an image ![bar](foo.png)'),
'List item with an image',
- image({ ...source('![bar](foo.png)'), alt: 'bar', src: 'foo.png' }),
+ image({
+ ...source('![bar](foo.png)'),
+ alt: 'bar',
+ canonicalSrc: 'foo.png',
+ src: 'foo.png',
+ }),
),
),
),
@@ -940,8 +970,17 @@ Paragraph
paragraph(
source('[![moon](moon.jpg)](/uri)'),
link(
- { ...source('[![moon](moon.jpg)](/uri)'), href: '/uri' },
- image({ ...source('![moon](moon.jpg)'), src: 'moon.jpg', alt: 'moon' }),
+ {
+ ...source('[![moon](moon.jpg)](/uri)'),
+ canonicalSrc: '/uri',
+ href: '/uri',
+ },
+ image({
+ ...source('![moon](moon.jpg)'),
+ canonicalSrc: 'moon.jpg',
+ src: 'moon.jpg',
+ alt: 'moon',
+ }),
),
),
),
@@ -971,12 +1010,26 @@ Paragraph
source('~[moon](moon.jpg) and [sun](sun.jpg)~'),
strike(
source('~[moon](moon.jpg) and [sun](sun.jpg)~'),
- link({ ...source('[moon](moon.jpg)'), href: 'moon.jpg' }, 'moon'),
+ link(
+ {
+ ...source('[moon](moon.jpg)'),
+ canonicalSrc: 'moon.jpg',
+ href: 'moon.jpg',
+ },
+ 'moon',
+ ),
),
strike(source('~[moon](moon.jpg) and [sun](sun.jpg)~'), ' and '),
strike(
source('~[moon](moon.jpg) and [sun](sun.jpg)~'),
- link({ ...source('[sun](sun.jpg)'), href: 'sun.jpg' }, 'sun'),
+ link(
+ {
+ ...source('[sun](sun.jpg)'),
+ href: 'sun.jpg',
+ canonicalSrc: 'sun.jpg',
+ },
+ 'sun',
+ ),
),
),
),
@@ -1079,6 +1132,107 @@ _world_.
),
),
},
+ {
+ markdown: `
+[GitLab][gitlab-url]
+
+[gitlab-url]: https://gitlab.com "GitLab"
+
+ `,
+ expectedDoc: doc(
+ paragraph(
+ source('[GitLab][gitlab-url]'),
+ link(
+ {
+ ...source('[GitLab][gitlab-url]'),
+ href: 'https://gitlab.com',
+ canonicalSrc: 'gitlab-url',
+ title: 'GitLab',
+ isReference: true,
+ },
+ 'GitLab',
+ ),
+ ),
+ referenceDefinition(
+ {
+ ...source('[gitlab-url]: https://gitlab.com "GitLab"'),
+ identifier: 'gitlab-url',
+ url: 'https://gitlab.com',
+ title: 'GitLab',
+ },
+ '[gitlab-url]: https://gitlab.com "GitLab"',
+ ),
+ ),
+ },
+ {
+ markdown: `
+![GitLab Logo][gitlab-logo]
+
+[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"
+
+ `,
+ expectedDoc: doc(
+ paragraph(
+ source('![GitLab Logo][gitlab-logo]'),
+ image({
+ ...source('![GitLab Logo][gitlab-logo]'),
+ src: 'https://gitlab.com/gitlab-logo.png',
+ canonicalSrc: 'gitlab-logo',
+ alt: 'GitLab Logo',
+ title: 'GitLab Logo',
+ isReference: true,
+ }),
+ ),
+ referenceDefinition(
+ {
+ ...source('[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"'),
+ identifier: 'gitlab-logo',
+ url: 'https://gitlab.com/gitlab-logo.png',
+ title: 'GitLab Logo',
+ },
+ '[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"',
+ ),
+ ),
+ },
+ {
+ markdown: `
+---
+title: 'layout'
+---
+ `,
+ expectedDoc: doc(
+ frontmatter(
+ { ...source("---\ntitle: 'layout'\n---"), language: 'yaml' },
+ "title: 'layout'",
+ ),
+ ),
+ },
+ {
+ markdown: `
++++
+title: 'layout'
++++
+ `,
+ expectedDoc: doc(
+ frontmatter(
+ { ...source("+++\ntitle: 'layout'\n+++"), language: 'toml' },
+ "title: 'layout'",
+ ),
+ ),
+ },
+ {
+ markdown: `
+;;;
+{ title: 'layout' }
+;;;
+ `,
+ expectedDoc: doc(
+ frontmatter(
+ { ...source(";;;\n{ title: 'layout' }\n;;;"), language: 'json' },
+ "{ title: 'layout' }",
+ ),
+ ),
+ },
];
const runOnly = examples.find((example) => example.only === true);
@@ -1090,7 +1244,7 @@ _world_.
const trimmed = markdown.trim();
const document = await deserialize(trimmed);
- expect(expectedDoc).not.toBeFalsy();
+ expect(expectedDoc).not.toBe(false);
expect(document.toJSON()).toEqual(expectedDoc.toJSON());
expect(serialize(document)).toEqual(expectedMarkdown ?? trimmed);
},
@@ -1155,4 +1309,72 @@ body {
expect(tiptapEditor.getHTML()).toEqual(expectedHtml);
},
);
+
+ describe('attribute sanitization', () => {
+ // eslint-disable-next-line no-script-url
+ const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');";
+ // eslint-disable-next-line no-script-url
+ const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');";
+
+ const docWithImageFactory = (urlInput, urlOutput) => {
+ const input = `<img src="${urlInput}">`;
+
+ return {
+ input,
+ expectedDoc: doc(
+ paragraph(
+ source(input),
+ image({
+ ...source(input),
+ src: urlOutput,
+ canonicalSrc: urlOutput,
+ }),
+ ),
+ ),
+ };
+ };
+
+ const docWithLinkFactory = (urlInput, urlOutput) => {
+ const input = `<a href="${urlInput}">foo</a>`;
+
+ return {
+ input,
+ expectedDoc: doc(
+ paragraph(
+ source(input),
+ link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'),
+ ),
+ ),
+ };
+ };
+
+ it.each`
+ desc | urlInput | urlOutput
+ ${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
+ ${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null}
+ ${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null}
+ ${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null}
+ ${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript&#58;'} | ${null}
+ ${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript&#0058;'} | ${null}
+ ${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041'} | ${null}
+ ${'protocol-based JS injection: hex encoding'} | ${'javascript&#x3A;'} | ${null}
+ ${'protocol-based JS injection: long hex encoding'} | ${'javascript&#x003A;'} | ${null}
+ ${'protocol-based JS injection: hex encoding without semicolons'} | ${'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29'} | ${null}
+ ${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null}
+ ${'protocol-based JS injection: spaces and entities'} | ${"&#14; javascript:alert('XSS');"} | ${null}
+ ${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null}
+ ${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
+ ${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"}
+ ${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
+ `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
+ const exampleFactories = [docWithImageFactory, docWithLinkFactory];
+
+ exampleFactories.forEach(async (exampleFactory) => {
+ const { input, expectedDoc } = exampleFactory(urlInput, urlOutput);
+ const document = await deserialize(input);
+
+ expect(document.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index 116a26cf7d5..4a57c7b1942 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -16,6 +16,7 @@ import FigureCaption from '~/content_editor/extensions/figure_caption';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
import FootnotesSection from '~/content_editor/extensions/footnotes_section';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
@@ -26,6 +27,7 @@ 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 ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
import TableCell from '~/content_editor/extensions/table_cell';
@@ -51,6 +53,7 @@ const tiptapEditor = createTestEditor({
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
+ Frontmatter,
Figure,
FigureCaption,
HardBreak,
@@ -63,6 +66,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ ReferenceDefinition,
Strike,
Table,
TableCell,
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 509cda3046c..0e5281be9bf 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -24,6 +24,7 @@ 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 ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Sourcemap from '~/content_editor/extensions/sourcemap';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
@@ -63,6 +64,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ ReferenceDefinition,
Sourcemap,
Strike,
Table,
@@ -104,6 +106,7 @@ const {
listItem,
orderedList,
paragraph,
+ referenceDefinition,
strike,
table,
tableCell,
@@ -139,6 +142,7 @@ const {
listItem: { nodeType: ListItem.name },
orderedList: { nodeType: OrderedList.name },
paragraph: { nodeType: Paragraph.name },
+ referenceDefinition: { nodeType: ReferenceDefinition.name },
strike: { markType: Strike.name },
table: { nodeType: Table.name },
tableCell: { nodeType: TableCell.name },
@@ -243,6 +247,37 @@ describe('markdownSerializer', () => {
).toBe('[download file](file.zip "click here to download")');
});
+ it('correctly serializes link references', () => {
+ expect(
+ serialize(
+ paragraph(
+ link(
+ {
+ href: 'gitlab-url',
+ isReference: true,
+ },
+ 'GitLab',
+ ),
+ ),
+ ),
+ ).toBe('[GitLab][gitlab-url]');
+ });
+
+ it('correctly serializes image references', () => {
+ expect(
+ serialize(
+ paragraph(
+ image({
+ canonicalSrc: 'gitlab-url',
+ src: 'image.svg',
+ alt: 'GitLab',
+ isReference: true,
+ }),
+ ),
+ ),
+ ).toBe('![GitLab][gitlab-url]');
+ });
+
it('correctly serializes strikethrough', () => {
expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
});
@@ -1163,6 +1198,38 @@ Oranges are orange [^1]
);
});
+ it('correctly serializes reference definition', () => {
+ expect(
+ serialize(
+ referenceDefinition('[gitlab]: https://gitlab.com'),
+ referenceDefinition('[foobar]: foobar.com'),
+ ),
+ ).toBe(
+ `
+[gitlab]: https://gitlab.com
+[foobar]: foobar.com`.trimLeft(),
+ );
+ });
+
+ it('correctly adds a space between a reference definition and a block content', () => {
+ expect(
+ serialize(
+ paragraph('paragraph'),
+ referenceDefinition('[gitlab]: https://gitlab.com'),
+ referenceDefinition('[foobar]: foobar.com'),
+ heading({ level: 2 }, 'heading'),
+ ),
+ ).toBe(
+ `
+paragraph
+
+[gitlab]: https://gitlab.com
+[foobar]: foobar.com
+
+## heading`.trimLeft(),
+ );
+ });
+
const defaultEditAction = (initialContent) => {
tiptapEditor.chain().setContent(initialContent.toJSON()).insertContent(' modified').run();
};
@@ -1177,42 +1244,49 @@ Oranges are orange [^1]
};
it.each`
- mark | markdown | modifiedMarkdown | editAction
- ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
- ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
- ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
- ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
- ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
- ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
- ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
- ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
- ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
- ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
- ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
- ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
- ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction}
- ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
- ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
- ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
- ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
- ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
- ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
- ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
- ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
- ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
- ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
- ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
- ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
- ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
- ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
+ mark | markdown | modifiedMarkdown | editAction
+ ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
+ ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
+ ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
+ ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
+ ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
+ ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
+ ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
+ ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
+ ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
+ ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction}
+ ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
+ ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
+ ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
+ ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
+ ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
+ ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
+ ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
+ ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
+ ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
+ ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
`(
- 'preserves original $mark syntax when sourceMarkdown is available for $content',
+ 'preserves original $mark syntax when sourceMarkdown is available for $markdown',
async ({ markdown, modifiedMarkdown, editAction }) => {
const { document } = await remarkMarkdownDeserializer().deserialize({
schema: tiptapEditor.schema,
diff --git a/spec/frontend/content_editor/services/table_of_contents_utils_spec.js b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
new file mode 100644
index 00000000000..7f63c2171c2
--- /dev/null
+++ b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
@@ -0,0 +1,96 @@
+import Heading from '~/content_editor/extensions/heading';
+import { toTree, getHeadings } from '~/content_editor/services/table_of_contents_utils';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/services/table_of_content_utils', () => {
+ describe('toTree', () => {
+ it('should fills in gaps in heading levels and convert headings to a tree', () => {
+ expect(
+ toTree([
+ { level: 3, text: '3' },
+ { level: 2, text: '2' },
+ ]),
+ ).toEqual([
+ expect.objectContaining({
+ level: 1,
+ text: '',
+ subHeadings: [
+ expect.objectContaining({
+ level: 2,
+ text: '',
+ subHeadings: [expect.objectContaining({ level: 3, text: '3', subHeadings: [] })],
+ }),
+ expect.objectContaining({ level: 2, text: '2', subHeadings: [] }),
+ ],
+ }),
+ ]);
+ });
+ });
+
+ describe('getHeadings', () => {
+ const tiptapEditor = createTestEditor({
+ extensions: [Heading],
+ });
+
+ const {
+ builders: { heading, doc },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ heading: { nodeType: Heading.name },
+ },
+ });
+
+ it('gets all headings as a tree in a tiptap document', () => {
+ const initialDoc = doc(
+ heading({ level: 1 }, 'Heading 1'),
+ heading({ level: 2 }, 'Heading 1.1'),
+ heading({ level: 3 }, 'Heading 1.1.1'),
+ heading({ level: 2 }, 'Heading 1.2'),
+ heading({ level: 3 }, 'Heading 1.2.1'),
+ heading({ level: 2 }, 'Heading 1.3'),
+ heading({ level: 2 }, 'Heading 1.4'),
+ heading({ level: 3 }, 'Heading 1.4.1'),
+ heading({ level: 1 }, 'Heading 2'),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ expect(getHeadings(tiptapEditor)).toEqual([
+ expect.objectContaining({
+ level: 1,
+ text: 'Heading 1',
+ subHeadings: [
+ expect.objectContaining({
+ level: 2,
+ text: 'Heading 1.1',
+ subHeadings: [
+ expect.objectContaining({ level: 3, text: 'Heading 1.1.1', subHeadings: [] }),
+ ],
+ }),
+ expect.objectContaining({
+ level: 2,
+ text: 'Heading 1.2',
+ subHeadings: [
+ expect.objectContaining({ level: 3, text: 'Heading 1.2.1', subHeadings: [] }),
+ ],
+ }),
+ expect.objectContaining({ level: 2, text: 'Heading 1.3', subHeadings: [] }),
+ expect.objectContaining({
+ level: 2,
+ text: 'Heading 1.4',
+ subHeadings: [
+ expect.objectContaining({ level: 3, text: 'Heading 1.4.1', subHeadings: [] }),
+ ],
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ level: 1,
+ text: 'Heading 2',
+ subHeadings: [],
+ }),
+ ]);
+ });
+ });
+});