summaryrefslogtreecommitdiff
path: root/spec/frontend/content_editor
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/content_editor')
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap9
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js1
-rw-r--r--spec/frontend/content_editor/components/wrappers/details_spec.js40
-rw-r--r--spec/frontend/content_editor/components/wrappers/frontmatter_spec.js43
-rw-r--r--spec/frontend/content_editor/extensions/color_chip_spec.js33
-rw-r--r--spec/frontend/content_editor/extensions/details_content_spec.js76
-rw-r--r--spec/frontend/content_editor/extensions/details_spec.js92
-rw-r--r--spec/frontend/content_editor/extensions/math_inline_spec.js42
-rw-r--r--spec/frontend/content_editor/extensions/table_of_contents_spec.js35
-rw-r--r--spec/frontend/content_editor/markdown_processing_examples.js2
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js107
11 files changed, 472 insertions, 8 deletions
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
index 8f5516545eb..178c7d749c8 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -11,14 +11,7 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
<ul role=\\"menu\\" tabindex=\\"-1\\" class=\\"dropdown-menu\\">
<div class=\\"gl-new-dropdown-inner\\">
<!---->
- <div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5\\">
- <div class=\\"gl-display-flex\\">
- <!---->
- </div>
- <div class=\\"gl-display-flex\\">
- <!---->
- </div>
- </div>
+ <!---->
<div class=\\"gl-new-dropdown-contents\\">
<!---->
<li role=\\"presentation\\" class=\\"gl-px-3!\\">
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index a5df3d73289..ec58877470c 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -31,6 +31,7 @@ describe('content_editor/components/top_toolbar', () => {
${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
${'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' }}
+ ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }}
${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }}
${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
${'text-styles'} | ${{}}
diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js
new file mode 100644
index 00000000000..d746b9fa2f1
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/details_spec.js
@@ -0,0 +1,40 @@
+import { NodeViewContent } from '@tiptap/vue-2';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DetailsWrapper from '~/content_editor/components/wrappers/details.vue';
+
+describe('content/components/wrappers/details', () => {
+ let wrapper;
+
+ const createWrapper = async () => {
+ wrapper = shallowMountExtended(DetailsWrapper, {
+ propsData: {
+ node: {},
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-content as a ul element', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewContent).props().as).toBe('ul');
+ });
+
+ it('is "open" by default', () => {
+ createWrapper();
+
+ expect(wrapper.findByTestId('details-toggle-icon').classes()).toContain('is-open');
+ expect(wrapper.findComponent(NodeViewContent).classes()).toContain('is-open');
+ });
+
+ it('closes the details block on clicking the details toggle icon', async () => {
+ createWrapper();
+
+ await wrapper.findByTestId('details-toggle-icon').trigger('click');
+ expect(wrapper.findByTestId('details-toggle-icon').classes()).not.toContain('is-open');
+ expect(wrapper.findComponent(NodeViewContent).classes()).not.toContain('is-open');
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
new file mode 100644
index 00000000000..de8f8efd260
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
@@ -0,0 +1,43 @@
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+import { shallowMount } from '@vue/test-utils';
+import FrontmatterWrapper from '~/content_editor/components/wrappers/frontmatter.vue';
+
+describe('content/components/wrappers/frontmatter', () => {
+ let wrapper;
+
+ const createWrapper = async (nodeAttrs = { language: 'yaml' }) => {
+ wrapper = shallowMount(FrontmatterWrapper, {
+ propsData: {
+ node: {
+ attrs: nodeAttrs,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-wrapper as a pre element', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('pre');
+ expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
+ });
+
+ it('renders a node-view-content as a code element', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewContent).props().as).toBe('code');
+ });
+
+ it('renders label indicating that code block is frontmatter', () => {
+ createWrapper();
+
+ const label = wrapper.find('[data-testid="frontmatter-label"]');
+
+ expect(label.text()).toEqual('frontmatter:yaml');
+ expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/color_chip_spec.js b/spec/frontend/content_editor/extensions/color_chip_spec.js
new file mode 100644
index 00000000000..4bb6f344ab4
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/color_chip_spec.js
@@ -0,0 +1,33 @@
+import ColorChip, { colorDecoratorPlugin } from '~/content_editor/extensions/color_chip';
+import Code from '~/content_editor/extensions/code';
+import { createTestEditor } from '../test_utils';
+
+describe('content_editor/extensions/color_chip', () => {
+ let tiptapEditor;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [ColorChip, Code] });
+ });
+
+ describe.each`
+ colorExpression | decorated
+ ${'#F00'} | ${true}
+ ${'rgba(0,0,0,0)'} | ${true}
+ ${'hsl(540,70%,50%)'} | ${true}
+ ${'F00'} | ${false}
+ ${'F00'} | ${false}
+ ${'gba(0,0,0,0)'} | ${false}
+ ${'hls(540,70%,50%)'} | ${false}
+ ${'red'} | ${false}
+ `(
+ 'when a code span with $colorExpression color expression is found',
+ ({ colorExpression, decorated }) => {
+ it(`${decorated ? 'adds' : 'does not add'} a color chip decorator`, () => {
+ tiptapEditor.commands.setContent(`<p><code>${colorExpression}</code></p>`);
+ const pluginState = colorDecoratorPlugin.getState(tiptapEditor.state);
+
+ expect(pluginState.children).toHaveLength(decorated ? 3 : 0);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/content_editor/extensions/details_content_spec.js b/spec/frontend/content_editor/extensions/details_content_spec.js
new file mode 100644
index 00000000000..575f3bf65e4
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/details_content_spec.js
@@ -0,0 +1,76 @@
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/details_content', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let details;
+ let detailsContent;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Details, DetailsContent] });
+
+ ({
+ builders: { doc, p, details, detailsContent },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
+ },
+ }));
+ });
+
+ describe('shortcut: Enter', () => {
+ it('splits a details content into two items', () => {
+ const initialDoc = doc(
+ details(
+ detailsContent(p('Summary')),
+ detailsContent(p('Text content')),
+ detailsContent(p('Text content')),
+ ),
+ );
+ const expectedDoc = doc(
+ details(
+ detailsContent(p('Summary')),
+ detailsContent(p('')),
+ detailsContent(p('Text content')),
+ detailsContent(p('Text content')),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Enter');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('shortcut: Shift-Tab', () => {
+ it('lifts a details content and creates two separate details items', () => {
+ const initialDoc = doc(
+ details(
+ detailsContent(p('Summary')),
+ detailsContent(p('Text content')),
+ detailsContent(p('Text content')),
+ ),
+ );
+ const expectedDoc = doc(
+ details(detailsContent(p('Summary'))),
+ p('Text content'),
+ details(detailsContent(p('Text content'))),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.commands.setTextSelection(20);
+ tiptapEditor.commands.keyboardShortcut('Shift-Tab');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/details_spec.js b/spec/frontend/content_editor/extensions/details_spec.js
new file mode 100644
index 00000000000..cd59943982f
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/details_spec.js
@@ -0,0 +1,92 @@
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/details', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let details;
+ let detailsContent;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Details, DetailsContent] });
+
+ ({
+ builders: { doc, p, details, detailsContent },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
+ },
+ }));
+ });
+
+ describe('setDetails command', () => {
+ describe('when current block is a paragraph', () => {
+ it('converts current paragraph into a details block', () => {
+ const initialDoc = doc(p('Text content'));
+ const expectedDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when current block is a details block', () => {
+ it('maintains the same document structure', () => {
+ const initialDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(initialDoc.toJSON());
+ });
+ });
+ });
+
+ describe('toggleDetails command', () => {
+ describe('when current block is a paragraph', () => {
+ it('converts current paragraph into a details block', () => {
+ const initialDoc = doc(p('Text content'));
+ const expectedDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.toggleDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when current block is a details block', () => {
+ it('convert details block into a paragraph', () => {
+ const initialDoc = doc(details(detailsContent(p('Text content'))));
+ const expectedDoc = doc(p('Text content'));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.toggleDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+ });
+
+ it.each`
+ input | insertedNode
+ ${'<details>'} | ${(...args) => details(detailsContent(p(...args)))}
+ ${'<details'} | ${(...args) => p(...args)}
+ ${'details>'} | ${(...args) => p(...args)}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const { view } = tiptapEditor;
+ const { selection } = view.state;
+ const expectedDoc = doc(insertedNode());
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/math_inline_spec.js b/spec/frontend/content_editor/extensions/math_inline_spec.js
new file mode 100644
index 00000000000..82eb85477de
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/math_inline_spec.js
@@ -0,0 +1,42 @@
+import MathInline from '~/content_editor/extensions/math_inline';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/math_inline', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let mathInline;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [MathInline] });
+
+ ({
+ builders: { doc, p, mathInline },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ details: { markType: MathInline.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'$`a^2`$'} | ${() => p(mathInline('a^2'))}
+ ${'$`a^2`'} | ${() => p('$`a^2`')}
+ ${'`a^2`$'} | ${() => p('`a^2`$')}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const { view } = tiptapEditor;
+ const expectedDoc = doc(insertedNode());
+
+ tiptapEditor.chain().setContent(input).setTextSelection(0).run();
+
+ const { state } = tiptapEditor;
+ const { selection } = state;
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, input.length + 1, input));
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/table_of_contents_spec.js b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
new file mode 100644
index 00000000000..83818899c17
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
@@ -0,0 +1,35 @@
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/emoji', () => {
+ let tiptapEditor;
+ let builders;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [TableOfContents] });
+ ({ builders } = createDocBuilder({
+ tiptapEditor,
+ names: { tableOfContents: { nodeType: TableOfContents.name } },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'[[_TOC_]]'} | ${'tableOfContents'}
+ ${'[TOC]'} | ${'tableOfContents'}
+ ${'[toc]'} | ${'p'}
+ ${'TOC'} | ${'p'}
+ ${'[_TOC_]'} | ${'p'}
+ ${'[[TOC]]'} | ${'p'}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const { doc } = builders;
+ const { view } = tiptapEditor;
+ const { selection } = view.state;
+ const expectedDoc = doc(builders[insertedNode]());
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/markdown_processing_examples.js b/spec/frontend/content_editor/markdown_processing_examples.js
index b3aabfeb145..da895970289 100644
--- a/spec/frontend/content_editor/markdown_processing_examples.js
+++ b/spec/frontend/content_editor/markdown_processing_examples.js
@@ -1,11 +1,13 @@
import fs from 'fs';
import path from 'path';
import jsYaml from 'js-yaml';
+// eslint-disable-next-line import/no-deprecated
import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
const fixturePathPrefix = `api/markdown/${testName}.json`;
+ // eslint-disable-next-line import/no-deprecated
const fixture = getJSONFixture(fixturePathPrefix);
return fixture.body || fixture.html;
};
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 6f2c908c289..33056ab9e4a 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -5,6 +5,8 @@ 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 Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
import Division from '~/content_editor/extensions/division';
import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
@@ -45,6 +47,8 @@ const tiptapEditor = createTestEditor({
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
+ Details,
+ DetailsContent,
Division,
Emoji,
Figure,
@@ -78,6 +82,8 @@ const {
bulletList,
code,
codeBlock,
+ details,
+ detailsContent,
division,
descriptionItem,
descriptionList,
@@ -110,6 +116,8 @@ const {
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
division: { nodeType: Division.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
@@ -588,6 +596,105 @@ A giant _owl-like_ creature.
);
});
+ it('correctly renders a simple details/summary', () => {
+ expect(
+ serialize(
+ details(
+ detailsContent(paragraph('this is the summary')),
+ detailsContent(paragraph('this content will be hidden')),
+ ),
+ ),
+ ).toBe(
+ `
+<details>
+<summary>this is the summary</summary>
+this content will be hidden
+</details>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders details/summary with styled content', () => {
+ expect(
+ serialize(
+ details(
+ detailsContent(paragraph('this is the ', bold('summary'))),
+ detailsContent(
+ codeBlock(
+ { language: 'javascript' },
+ 'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);',
+ ),
+ ),
+ detailsContent(paragraph('this content will be ', italic('hidden'))),
+ ),
+ details(detailsContent(paragraph('summary 2')), detailsContent(paragraph('content 2'))),
+ ),
+ ).toBe(
+ `
+<details>
+<summary>
+
+this is the **summary**
+
+</summary>
+
+\`\`\`javascript
+var a = 2;
+var b = 3;
+var c = a + d;
+
+console.log(c);
+\`\`\`
+
+this content will be _hidden_
+
+</details>
+<details>
+<summary>summary 2</summary>
+content 2
+</details>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders nested details', () => {
+ expect(
+ serialize(
+ details(
+ detailsContent(paragraph('dream level 1')),
+ detailsContent(
+ details(
+ detailsContent(paragraph('dream level 2')),
+ detailsContent(
+ details(
+ detailsContent(paragraph('dream level 3')),
+ detailsContent(paragraph(italic('inception'))),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+<details>
+<summary>dream level 1</summary>
+
+<details>
+<summary>dream level 2</summary>
+
+<details>
+<summary>dream level 3</summary>
+
+_inception_
+
+</details>
+</details>
+</details>
+ `.trim(),
+ );
+ });
+
it('correctly renders div', () => {
expect(
serialize(