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/__snapshots__/toolbar_button_spec.js.snap2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js1
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js14
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js188
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js7
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js3
-rw-r--r--spec/frontend/content_editor/extensions/copy_paste_spec.js19
-rw-r--r--spec/frontend/content_editor/extensions/reference_spec.js56
-rw-r--r--spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap256
-rw-r--r--spec/frontend/content_editor/services/asset_resolver_spec.js55
-rw-r--r--spec/frontend/content_editor/services/autocomplete_mock_data.js967
-rw-r--r--spec/frontend/content_editor/services/data_source_factory_spec.js202
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js107
-rw-r--r--spec/frontend/content_editor/services/markdown_sourcemap_spec.js109
-rw-r--r--spec/frontend/content_editor/test_constants.js15
15 files changed, 1894 insertions, 107 deletions
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
index a708f7d5f47..0fafd42095b 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
@@ -3,7 +3,7 @@
exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = `
<b-button-stub
aria-label="Bold"
- class="btn-default-tertiary btn-icon gl-button gl-mr-3"
+ class="btn-default-tertiary btn-icon gl-button gl-mr-2"
size="sm"
tag="button"
title="Bold"
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
index 2a6ab75227c..6e8a6092667 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
@@ -80,6 +80,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
await emitEditorEvent({ event: 'transaction', tiptapEditor });
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Plain text');
+ expect(wrapper.findComponent(GlDropdown).attributes('contenteditable')).toBe(String(false));
});
it('selects appropriate language based on the code block', async () => {
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 816c9458201..bbc0203344c 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,6 +1,8 @@
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { EditorContent, Editor } from '@tiptap/vue-2';
import { nextTick } from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
@@ -16,11 +18,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
-jest.mock('~/emoji');
-
describe('ContentEditor', () => {
let wrapper;
let renderMarkdown;
+ let mock;
const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
@@ -32,6 +33,7 @@ describe('ContentEditor', () => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
+ markdownDocsPath: '/docs/markdown',
uploadsPath,
markdown,
autofocus,
@@ -49,9 +51,17 @@ describe('ContentEditor', () => {
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ // ignore /-/emojis requests
+ mock.onGet().reply(200, []);
+
renderMarkdown = jest.fn();
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('triggers initialized event', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
index ee3ad59bf9a..b17a1b5fc11 100644
--- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -1,5 +1,6 @@
-import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatar, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
@@ -14,11 +15,17 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
command: jest.fn(),
...propsData,
},
+ stubs: ['gl-emoji'],
}),
);
};
- const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' };
+ const exampleUser = {
+ username: 'root',
+ avatar_url: 'root_avatar.png',
+ type: 'User',
+ name: 'Administrator',
+ };
const exampleIssue = { iid: 123, title: 'Test Issue' };
const exampleMergeRequest = { iid: 224, title: 'Test MR' };
const exampleMilestone1 = { iid: 21, title: '13' };
@@ -61,11 +68,14 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
title: 'Project creation QueryRecorder logs',
};
const exampleEmoji = {
- c: 'people',
- e: '😃',
- d: 'smiling face with open mouth',
- u: '6.0',
- name: 'smiley',
+ emoji: {
+ c: 'people',
+ e: '😃',
+ d: 'smiling face with open mouth',
+ u: '6.0',
+ name: 'smiley',
+ },
+ fieldValue: 'smiley',
};
const insertedEmojiProps = {
@@ -95,6 +105,68 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(loading);
});
+ it('selects first item if query is not empty and items are available', async () => {
+ buildWrapper({
+ propsData: {
+ char: '@',
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'member',
+ },
+ items: [exampleUser],
+ query: 'ro',
+ },
+ });
+
+ await nextTick();
+
+ expect(
+ wrapper.findByTestId('content-editor-suggestions-dropdown').find('li').classes(),
+ ).toContain('focused');
+ });
+
+ describe('when query is defined', () => {
+ it.each`
+ nodeType | referenceType | reference | query | expectedHTML
+ ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'<strong class="gl-text-body!">r</strong>oot'}
+ ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'Administ<strong class="gl-text-body!">r</strong>ator'}
+ ${'reference'} | ${'issue'} | ${exampleIssue} | ${'test'} | ${'<strong class="gl-text-body!">Test</strong> Issue'}
+ ${'reference'} | ${'issue'} | ${exampleIssue} | ${'12'} | ${'<strong class="gl-text-body!">12</strong>3'}
+ ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'test'} | ${'<strong class="gl-text-body!">Test</strong> MR'}
+ ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'22'} | ${'<strong class="gl-text-body!">22</strong>4'}
+ ${'reference'} | ${'epic'} | ${exampleEpic} | ${'rem'} | ${'❓ <strong class="gl-text-body!">Rem</strong>ote Development | Solution validation'}
+ ${'reference'} | ${'epic'} | ${exampleEpic} | ${'88'} | ${'gitlab-org&amp;<strong class="gl-text-body!">88</strong>84'}
+ ${'reference'} | ${'milestone'} | ${exampleMilestone1} | ${'1'} | ${'<strong class="gl-text-body!">1</strong>3'}
+ ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'<strong class="gl-text-body!">due</strong>'}
+ ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'Set <strong class="gl-text-body!">due</strong> date'}
+ ${'reference'} | ${'label'} | ${exampleLabel1} | ${'c'} | ${'<strong class="gl-text-body!">C</strong>reate'}
+ ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'network'} | ${'System procs <strong class="gl-text-body!">network</strong> activity'}
+ ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'85'} | ${'60<strong class="gl-text-body!">85</strong>0147'}
+ ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'project'} | ${'<strong class="gl-text-body!">Project</strong> creation QueryRecorder logs'}
+ ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'242'} | ${'<strong class="gl-text-body!">242</strong>0859'}
+ ${'emoji'} | ${'emoji'} | ${exampleEmoji} | ${'sm'} | ${'<strong class="gl-text-body!">sm</strong>iley'}
+ `(
+ 'highlights query as bolded in $referenceType text',
+ ({ nodeType, referenceType, reference, query, expectedHTML }) => {
+ buildWrapper({
+ propsData: {
+ char: '@',
+ nodeType,
+ nodeProps: {
+ referenceType,
+ },
+ items: [reference],
+ query,
+ },
+ });
+
+ expect(wrapper.findByTestId('content-editor-suggestions-dropdown').html()).toContain(
+ expectedHTML,
+ );
+ },
+ );
+ });
+
describe('on item select', () => {
it.each`
nodeType | referenceType | char | reference | insertedText | insertedProps
@@ -146,7 +218,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
});
describe('rendering user references', () => {
- it('displays avatar labeled component', () => {
+ it('displays avatar component', () => {
buildWrapper({
propsData: {
char: '@',
@@ -157,13 +229,11 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
},
});
- expect(wrapper.findComponent(GlAvatarLabeled).attributes()).toEqual(
- expect.objectContaining({
- label: exampleUser.username,
- shape: 'circle',
- src: exampleUser.avatar_url,
- }),
- );
+ expect(wrapper.findComponent(GlAvatar).attributes()).toMatchObject({
+ entityname: exampleUser.username,
+ shape: 'circle',
+ src: exampleUser.avatar_url,
+ });
});
describe.each`
@@ -273,20 +343,46 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
it('displays emoji', () => {
const testEmojis = [
{
- c: 'people',
- e: '😄',
- d: 'smiling face with open mouth and smiling eyes',
- u: '6.0',
- name: 'smile',
+ emoji: {
+ c: 'people',
+ e: '😄',
+ d: 'smiling face with open mouth and smiling eyes',
+ u: '6.0',
+ name: 'smile',
+ },
+ fieldValue: 'smile',
+ },
+ {
+ emoji: {
+ c: 'people',
+ e: '😸',
+ d: 'grinning cat face with smiling eyes',
+ u: '6.0',
+ name: 'smile_cat',
+ },
+ fieldValue: 'smile_cat',
+ },
+ {
+ emoji: {
+ c: 'people',
+ e: '😃',
+ d: 'smiling face with open mouth',
+ u: '6.0',
+ name: 'smiley',
+ },
+ fieldValue: 'smiley',
},
{
- c: 'people',
- e: '😸',
- d: 'grinning cat face with smiling eyes',
- u: '6.0',
- name: 'smile_cat',
+ emoji: {
+ c: 'custom',
+ e: null,
+ d: 'party-parrot',
+ u: 'custom',
+ name: 'party-parrot',
+ src: 'https://cultofthepartyparrot.com/parrots/hd/parrot.gif',
+ },
+ fieldValue: 'party-parrot',
},
- { c: 'people', e: '😃', d: 'smiling face with open mouth', u: '6.0', name: 'smiley' },
];
buildWrapper({
@@ -298,11 +394,41 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
},
});
- testEmojis.forEach((testEmoji) => {
- expect(wrapper.text()).toContain(testEmoji.e);
- expect(wrapper.text()).toContain(testEmoji.d);
- expect(wrapper.text()).toContain(testEmoji.name);
- });
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(0).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-name="smile"
+ data-unicode-version="6.0"
+ title="smiling face with open mouth and smiling eyes"
+ >
+ 😄
+ </gl-emoji-stub>
+ `);
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(1).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-name="smile_cat"
+ data-unicode-version="6.0"
+ title="grinning cat face with smiling eyes"
+ >
+ 😸
+ </gl-emoji-stub>
+ `);
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(2).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-name="smiley"
+ data-unicode-version="6.0"
+ title="smiling face with open mouth"
+ >
+ 😃
+ </gl-emoji-stub>
+ `);
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(3).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif"
+ data-name="party-parrot"
+ data-unicode-version="custom"
+ title="party-parrot"
+ />
+ `);
});
});
});
diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index 0093393eceb..1f15dc17f7f 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -97,6 +97,7 @@ describe('content/components/wrappers/code_block', () => {
const label = wrapper.findByTestId('frontmatter-label');
expect(label.text()).toEqual('frontmatter:yaml');
+ expect(label.attributes('contenteditable')).toBe('false');
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
});
@@ -128,6 +129,9 @@ describe('content/components/wrappers/code_block', () => {
await nextTick();
expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram');
+ expect(wrapper.findByTestId('sandbox-preview').attributes('contenteditable')).toBe(
+ String(false),
+ );
jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(false);
@@ -214,6 +218,9 @@ describe('content/components/wrappers/code_block', () => {
});
it('shows a code suggestion block', () => {
+ expect(wrapper.findByTestId('code-suggestion-box').attributes('contenteditable')).toBe(
+ 'false',
+ );
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 5');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
<code
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 275f48ea857..94628f2b2c5 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
@@ -165,6 +165,9 @@ describe('content/components/wrappers/table_cell_base', () => {
it('does not allow adding a row before the header', () => {
expect(findDropdown().text()).not.toContain('Insert row before');
+ expect(wrapper.findByTestId('actions-dropdown').attributes('contenteditable')).toBe(
+ 'false',
+ );
});
it('does not allow removing the header row', async () => {
diff --git a/spec/frontend/content_editor/extensions/copy_paste_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js
index e290b4e5137..6969f4985a1 100644
--- a/spec/frontend/content_editor/extensions/copy_paste_spec.js
+++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js
@@ -20,12 +20,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
-const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>';
-const CODE_SUGGESTION_HTML =
- '<pre data-lang-params="-0+0" class="js-syntax-highlight language-suggestion" lang="suggestion">Suggested code</pre>';
-const DIAGRAM_HTML =
- '<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">';
-const FRONTMATTER_HTML = '<pre lang="yaml" data-lang-params="frontmatter">key: value</pre>';
const PARAGRAPH_HTML =
'<p dir="auto">Some text with <strong>bold</strong> and <em>italic</em> text.</p>';
@@ -123,19 +117,6 @@ describe('content_editor/extensions/copy_paste', () => {
expect(await triggerPasteEventHandler(buildClipboardEvent({ types, data }))).toBe(true);
});
- it.each`
- nodeType | html | handled | desc
- ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'}
- ${'codeSuggestion'} | ${CODE_SUGGESTION_HTML} | ${false} | ${'does not handle'}
- ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'}
- ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'}
- ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'}
- `('$desc paste if currently a `$nodeType` is in focus', async ({ html, handled }) => {
- tiptapEditor.commands.insertContent(html);
-
- expect(await triggerPasteEventHandler(buildClipboardEvent())).toBe(handled);
- });
-
describe.each`
eventName | expectedDoc
${'cut'} | ${() => doc(p())}
diff --git a/spec/frontend/content_editor/extensions/reference_spec.js b/spec/frontend/content_editor/extensions/reference_spec.js
index c25c7c41d75..d4b07d5127e 100644
--- a/spec/frontend/content_editor/extensions/reference_spec.js
+++ b/spec/frontend/content_editor/extensions/reference_spec.js
@@ -1,9 +1,15 @@
import Reference from '~/content_editor/extensions/reference';
+import ReferenceLabel from '~/content_editor/extensions/reference_label';
import AssetResolver from '~/content_editor/services/asset_resolver';
import {
RESOLVED_ISSUE_HTML,
RESOLVED_MERGE_REQUEST_HTML,
RESOLVED_EPIC_HTML,
+ RESOLVED_LABEL_HTML,
+ RESOLVED_SNIPPET_HTML,
+ RESOLVED_MILESTONE_HTML,
+ RESOLVED_USER_HTML,
+ RESOLVED_VULNERABILITY_HTML,
} from '../test_constants';
import {
createTestEditor,
@@ -17,6 +23,7 @@ describe('content_editor/extensions/reference', () => {
let doc;
let p;
let reference;
+ let referenceLabel;
let renderMarkdown;
let assetResolver;
@@ -25,33 +32,54 @@ describe('content_editor/extensions/reference', () => {
assetResolver = new AssetResolver({ renderMarkdown });
tiptapEditor = createTestEditor({
- extensions: [Reference.configure({ assetResolver })],
+ extensions: [Reference.configure({ assetResolver }), ReferenceLabel],
});
({
- builders: { doc, p, reference },
+ builders: { doc, p, reference, referenceLabel },
} = createDocBuilder({
tiptapEditor,
names: {
reference: { nodeType: Reference.name },
+ referenceLabel: { nodeType: ReferenceLabel.name },
},
}));
});
describe('when typing a valid reference input rule', () => {
- const buildExpectedDoc = (href, originalText, referenceType, text) =>
+ const buildExpectedDoc = (href, originalText, referenceType, text = originalText) =>
doc(p(reference({ className: null, href, originalText, referenceType, text }), ' '));
+ const buildExpectedDocForLabel = (href, originalText, text, color) =>
+ doc(
+ p(
+ referenceLabel({
+ className: null,
+ referenceType: 'label',
+ href,
+ originalText,
+ text,
+ color,
+ }),
+ ' ',
+ ),
+ );
+
it.each`
- inputRuleText | mockReferenceHtml | expectedDoc
- ${'#1 '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1', 'issue', '#1 (closed)')}
- ${'#1+ '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+', 'issue', '500 error on MR approvers edit page (#1 - closed)')}
- ${'#1+s '} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+s', 'issue', '500 error on MR approvers edit page (#1 - closed) • Unassigned')}
- ${'!1 '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1', 'merge_request', '!1 (merged)')}
- ${'!1+ '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged)')}
- ${'!1+s '} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+s', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged) • John Doe')}
- ${'&1 '} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1', 'epic', '&1')}
- ${'&1+ '} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1+', 'epic', 'Approvals in merge request list (&1)')}
+ inputRuleText | mockReferenceHtml | expectedDoc
+ ${'#1'} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1', 'issue', '#1 (closed)')}
+ ${'#1+'} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+', 'issue', '500 error on MR approvers edit page (#1 - closed)')}
+ ${'#1+s'} | ${RESOLVED_ISSUE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/issues/1', '#1+s', 'issue', '500 error on MR approvers edit page (#1 - closed) • Unassigned')}
+ ${'!1'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1', 'merge_request', '!1 (merged)')}
+ ${'!1+'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged)')}
+ ${'!1+s'} | ${RESOLVED_MERGE_REQUEST_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab/-/merge_requests/1', '!1+s', 'merge_request', 'Enhance the LDAP group synchronization (!1 - merged) • John Doe')}
+ ${'&1'} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1', 'epic', '&1')}
+ ${'&1+'} | ${RESOLVED_EPIC_HTML} | ${() => buildExpectedDoc('/groups/gitlab-org/-/epics/1', '&1+', 'epic', 'Approvals in merge request list (&1)')}
+ ${'@root'} | ${RESOLVED_USER_HTML} | ${() => buildExpectedDoc('/root', '@root', 'user')}
+ ${'~Aquanix'} | ${RESOLVED_LABEL_HTML} | ${() => buildExpectedDocForLabel('/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix', '~Aquanix', 'Aquanix', 'rgb(230, 84, 49)')}
+ ${'%v4.0'} | ${RESOLVED_MILESTONE_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab-shell/-/milestones/5', '%v4.0', 'milestone')}
+ ${'$25'} | ${RESOLVED_SNIPPET_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab-shell/-/snippets/25', '$25', 'snippet')}
+ ${'[vulnerability:1]'} | ${RESOLVED_VULNERABILITY_HTML} | ${() => buildExpectedDoc('/gitlab-org/gitlab-shell/-/security/vulnerabilities/1', '[vulnerability:1]', 'vulnerability')}
`(
'replaces the input rule ($inputRuleText) with a reference node',
async ({ inputRuleText, mockReferenceHtml, expectedDoc }) => {
@@ -61,8 +89,8 @@ describe('content_editor/extensions/reference', () => {
action() {
renderMarkdown.mockResolvedValueOnce(mockReferenceHtml);
- tiptapEditor.commands.insertContent({ type: 'text', text: inputRuleText });
- triggerNodeInputRule({ tiptapEditor, inputRuleText });
+ tiptapEditor.commands.insertContent({ type: 'text', text: `${inputRuleText} ` });
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: `${inputRuleText} ` });
},
});
diff --git a/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap b/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap
new file mode 100644
index 00000000000..2d16c6b1a2f
--- /dev/null
+++ b/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap
@@ -0,0 +1,256 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DataSourceFactory filters items based on command "/assign" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+ "jashkenas",
+ "twitter",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/assign_reviewer" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "errol",
+ "evelynn_olson",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+ "jashkenas",
+ "twitter",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/label" for reference type "label" and command 1`] = `
+Array [
+ "Bronce",
+ "Contour",
+ "Corolla",
+ "Cygsync",
+ "Frontier",
+ "Grand Am",
+ "Onesync",
+ "Phone",
+ "Pynefunc",
+ "Trinix",
+ "Trounswood",
+ "group::knowledge",
+ "scoped label",
+ "type::one",
+ "type::two",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/reassign" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "errol",
+ "evelynn_olson",
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/reassign_reviewer" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "errol",
+ "evelynn_olson",
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/relabel" for reference type "label" and command 1`] = `
+Array [
+ "Amsche",
+ "Brioffe",
+ "Bronce",
+ "Bryncefunc",
+ "Contour",
+ "Corolla",
+ "Cygsync",
+ "Frontier",
+ "Ghost",
+ "Grand Am",
+ "Onesync",
+ "Phone",
+ "Pynefunc",
+ "Trinix",
+ "Trounswood",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/unassign" for reference type "user" and command 1`] = `
+Array [
+ "errol",
+ "evelynn_olson",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/unassign_reviewer" for reference type "user" and command 1`] = `
+Array [
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/unlabel" for reference type "label" and command 1`] = `
+Array [
+ "Amsche",
+ "Brioffe",
+ "Bryncefunc",
+ "Ghost",
+]
+`;
+
+exports[`DataSourceFactory for reference type "command", searches for "re" correctly 1`] = `
+Array [
+ "relabel",
+ "remove_milestone",
+ "remove_estimate",
+ "remove_time_spent",
+ "relate",
+ "remove_epic",
+ "reassign",
+ "create_merge_request",
+]
+`;
+
+exports[`DataSourceFactory for reference type "epic", searches for "n" correctly 1`] = `
+Array [
+ "Nobis quidem aspernatur reprehenderit sunt ut ipsum tempora sapiente sed iste.",
+ "Minus eius ut omnis quos sunt dicta ex ipsum.",
+ "Quae nostrum possimus rerum aliquam pariatur a eos aut id.",
+ "Dicta incidunt vel dignissimos sint sit esse est quibusdam quidem consequatur.",
+ "Doloremque a quisquam qui culpa numquam doloribus similique iure enim.",
+]
+`;
+
+exports[`DataSourceFactory for reference type "issue", searches for "q" correctly 1`] = `
+Array [
+ "Quasi id et et nihil sint autem.",
+ "Eaque omnis eius quas necessitatibus hic ut et corrupti.",
+ "Aut quisquam magnam eos distinctio incidunt perferendis fugit.",
+ "Dolorem quisquam cupiditate consequatur perspiciatis sequi eligendi ullam.",
+ "Nesciunt quia molestiae in aliquam amet et dolorem.",
+ "Porro tempore qui qui culpa saepe et nam quos.",
+ "Sed sint a est consequatur quae quasi autem debitis alias.",
+ "Molestiae minima maxime optio nihil quam eveniet dolor.",
+ "Et laboriosam aut ratione voluptatem quasi recusandae.",
+ "Et molestiae delectus voluptates velit vero illo aut rerum quo et.",
+]
+`;
+
+exports[`DataSourceFactory for reference type "label", searches for "c" correctly 1`] = `
+Array [
+ "Contour",
+ "Corolla",
+ "Cygsync",
+ "scoped label",
+ "Amsche",
+ "Bronce",
+ "Bryncefunc",
+ "Onesync",
+ "Pynefunc",
+]
+`;
+
+exports[`DataSourceFactory for reference type "merge_request", searches for "n" correctly 1`] = `
+Array [
+ "Blanditiis maxime voluptatem ut pariatur vel autem vero non quod libero.",
+ "Optio nemo qui dolorem sit ipsum qui saepe.",
+ "Draft: Alunny/publish lib",
+ "Draft: Fix event current target",
+ "Draft: Resolve \\"hgvbbvnnb\\"",
+ "Autem eaque et sed provident enim corrupti molestiae.",
+ "Always call registry's trigger method from withRegistration",
+]
+`;
+
+exports[`DataSourceFactory for reference type "milestone", searches for "16" correctly 1`] = `
+Array [
+ "16.7",
+ "16.8",
+ "16.9",
+ "16.10",
+ "16.11",
+ "16.0 (expired)",
+ "16.1 (expired)",
+ "16.2 (expired)",
+ "16.3 (expired)",
+ "16.4 (expired)",
+ "16.5 (expired)",
+ "16.6 (expired)",
+]
+`;
+
+exports[`DataSourceFactory for reference type "snippet", searches for "s" correctly 1`] = `
+Array [
+ "ss",
+ "test snippet",
+ "another test snippet",
+]
+`;
+
+exports[`DataSourceFactory for reference type "user", searches for "r" correctly 1`] = `
+Array [
+ "root",
+ "errol",
+ "lakeesha.batz",
+ "myrtis",
+ "florida.schoen",
+ "laurene_blick",
+ "all",
+ "twitter",
+ "gitlab-org",
+ "evelynn_olson",
+]
+`;
+
+exports[`DataSourceFactory for reference type "vulnerability", searches for "cross" correctly 1`] = `
+Array [
+ "Cross Site Scripting (Persistent)",
+ "Cross Site Scripting (Persistent)",
+ "Cross Site Scripting (Persistent)",
+]
+`;
diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js
index 292eec6db77..b0135a6bc9f 100644
--- a/spec/frontend/content_editor/services/asset_resolver_spec.js
+++ b/spec/frontend/content_editor/services/asset_resolver_spec.js
@@ -3,6 +3,11 @@ import {
RESOLVED_ISSUE_HTML,
RESOLVED_MERGE_REQUEST_HTML,
RESOLVED_EPIC_HTML,
+ RESOLVED_LABEL_HTML,
+ RESOLVED_SNIPPET_HTML,
+ RESOLVED_MILESTONE_HTML,
+ RESOLVED_USER_HTML,
+ RESOLVED_VULNERABILITY_HTML,
} from '../test_constants';
describe('content_editor/services/asset_resolver', () => {
@@ -48,6 +53,32 @@ describe('content_editor/services/asset_resolver', () => {
text: '!1 (merged)',
};
+ const resolvedLabel = {
+ backgroundColor: 'rgb(230, 84, 49)',
+ href: '/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix',
+ text: 'Aquanix',
+ };
+
+ const resolvedSnippet = {
+ href: '/gitlab-org/gitlab-shell/-/snippets/25',
+ text: '$25',
+ };
+
+ const resolvedMilestone = {
+ href: '/gitlab-org/gitlab-shell/-/milestones/5',
+ text: '%v4.0',
+ };
+
+ const resolvedUser = {
+ href: '/root',
+ text: '@root',
+ };
+
+ const resolvedVulnerability = {
+ href: '/gitlab-org/gitlab-shell/-/security/vulnerabilities/1',
+ text: '[vulnerability:1]',
+ };
+
describe.each`
referenceType | referenceId | sentMarkdown | returnedHtml | resolvedReference
${'issue'} | ${'#1'} | ${'#1 #1+ #1+s'} | ${RESOLVED_ISSUE_HTML} | ${resolvedIssue}
@@ -59,7 +90,9 @@ describe('content_editor/services/asset_resolver', () => {
it(`resolves ${referenceType} reference to href, text, title and summary`, async () => {
renderMarkdown.mockResolvedValue(returnedHtml);
- expect(await assetResolver.resolveReference(referenceId)).toEqual(resolvedReference);
+ expect(await assetResolver.resolveReference(referenceId)).toMatchObject(
+ resolvedReference,
+ );
});
it.each`
@@ -74,6 +107,26 @@ describe('content_editor/services/asset_resolver', () => {
},
);
+ describe.each`
+ referenceType | referenceId | returnedHtml | resolvedReference
+ ${'label'} | ${'~Aquanix'} | ${RESOLVED_LABEL_HTML} | ${resolvedLabel}
+ ${'snippet'} | ${'$25'} | ${RESOLVED_SNIPPET_HTML} | ${resolvedSnippet}
+ ${'milestone'} | ${'%v4.0'} | ${RESOLVED_MILESTONE_HTML} | ${resolvedMilestone}
+ ${'user'} | ${'@root'} | ${RESOLVED_USER_HTML} | ${resolvedUser}
+ ${'vulnerability'} | ${'[vulnerability:1]'} | ${RESOLVED_VULNERABILITY_HTML} | ${resolvedVulnerability}
+ `(
+ 'for reference type $referenceType',
+ ({ referenceType, referenceId, returnedHtml, resolvedReference }) => {
+ it(`resolves ${referenceType} reference to href, text and additional props (if any)`, async () => {
+ renderMarkdown.mockResolvedValue(returnedHtml);
+
+ expect(await assetResolver.resolveReference(referenceId)).toMatchObject(
+ resolvedReference,
+ );
+ });
+ },
+ );
+
it.each`
case | sentMarkdown | returnedHtml
${'no html is returned'} | ${''} | ${''}
diff --git a/spec/frontend/content_editor/services/autocomplete_mock_data.js b/spec/frontend/content_editor/services/autocomplete_mock_data.js
new file mode 100644
index 00000000000..c1bf2a6ae5b
--- /dev/null
+++ b/spec/frontend/content_editor/services/autocomplete_mock_data.js
@@ -0,0 +1,967 @@
+export const MOCK_MEMBERS = [
+ {
+ type: 'User',
+ username: 'florida.schoen',
+ name: 'Anglea Durgan',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/ac82b5615d3308ecbcacedad361af8e7?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'root',
+ name: 'Administrator',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ username: 'all',
+ name: 'All Project and Group Members',
+ count: 8,
+ },
+ {
+ type: 'User',
+ username: 'errol',
+ name: "Linnie O'Connell",
+ avatar_url:
+ 'https://www.gravatar.com/avatar/d3d9a468a9884eb217fad5ca5b2b9bd7?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'evelynn_olson',
+ name: 'Dimple Dare',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/bc1e51ee3512c2b4442f51732d655107?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'lakeesha.batz',
+ name: 'Larae Veum',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e5605cb9bbb1a28640d65f25f256e541?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'laurene_blick',
+ name: 'Evelina Murray',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/389768eef61b7b2d125c64ee01c240fb?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'myrtis',
+ name: 'Fernanda Adams',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/719d5569bd31d4a70e350b4205fa2cb5?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'patty',
+ name: 'Emily Toy',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/dca2077b662338808459dc11e70d6688?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'Group',
+ username: 'Commit451',
+ name: 'Commit451',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'flightjs',
+ name: 'Flightjs',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'gitlab-instance-ade037f9',
+ name: 'GitLab Instance',
+ avatar_url: null,
+ count: 1,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'gitlab-org',
+ name: 'Gitlab Org',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'gnuwget',
+ name: 'Gnuwget',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'h5bp',
+ name: 'H5bp',
+ avatar_url: null,
+ count: 4,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'jashkenas',
+ name: 'Jashkenas',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'twitter',
+ name: 'Twitter',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+];
+
+export const MOCK_ASSIGNEES = MOCK_MEMBERS.filter(
+ ({ username }) => username === 'errol' || username === 'evelynn_olson',
+);
+
+export const MOCK_REVIEWERS = MOCK_MEMBERS.filter(
+ ({ username }) =>
+ username === 'lakeesha.batz' ||
+ username === 'laurene_blick' ||
+ username === 'myrtis' ||
+ username === 'patty',
+);
+
+export const MOCK_ISSUES = [
+ {
+ iid: 31,
+ title: 'rdfhdfj',
+ id: null,
+ },
+ {
+ iid: 30,
+ title: 'incident1',
+ id: null,
+ },
+ {
+ iid: 29,
+ title: 'example feature rollout',
+ id: null,
+ },
+ {
+ iid: 28,
+ title: 'sagasg',
+ id: null,
+ },
+ {
+ iid: 26,
+ title: 'Quasi id et et nihil sint autem.',
+ id: null,
+ },
+ {
+ iid: 25,
+ title: 'Dolorem quisquam cupiditate consequatur perspiciatis sequi eligendi ullam.',
+ id: null,
+ },
+ {
+ iid: 24,
+ title: 'Et molestiae delectus voluptates velit vero illo aut rerum quo et.',
+ id: null,
+ },
+ {
+ iid: 23,
+ title: 'Nesciunt quia molestiae in aliquam amet et dolorem.',
+ id: null,
+ },
+ {
+ iid: 22,
+ title: 'Sint asperiores unde vel autem delectus ullam dolor nihil et.',
+ id: null,
+ },
+ {
+ iid: 21,
+ title: 'Eaque omnis eius quas necessitatibus hic ut et corrupti.',
+ id: null,
+ },
+ {
+ iid: 20,
+ title: 'Porro tempore qui qui culpa saepe et nam quos.',
+ id: null,
+ },
+ {
+ iid: 19,
+ title: 'Molestiae minima maxime optio nihil quam eveniet dolor.',
+ id: null,
+ },
+ {
+ iid: 18,
+ title: 'Sed sint a est consequatur quae quasi autem debitis alias.',
+ id: null,
+ },
+ {
+ iid: 6,
+ title: 'Et laboriosam aut ratione voluptatem quasi recusandae.',
+ id: null,
+ },
+ {
+ iid: 2,
+ title: 'Aut quisquam magnam eos distinctio incidunt perferendis fugit.',
+ id: null,
+ },
+];
+
+export const MOCK_EPICS = [
+ {
+ iid: 6,
+ title: 'sgs',
+ reference: 'flightjs\u00266',
+ },
+ {
+ iid: 5,
+ title: 'Doloremque a quisquam qui culpa numquam doloribus similique iure enim.',
+ reference: 'flightjs\u00265',
+ },
+ {
+ iid: 4,
+ title: 'Minus eius ut omnis quos sunt dicta ex ipsum.',
+ reference: 'flightjs\u00264',
+ },
+ {
+ iid: 3,
+ title: 'Quae nostrum possimus rerum aliquam pariatur a eos aut id.',
+ reference: 'flightjs\u00263',
+ },
+ {
+ iid: 2,
+ title: 'Nobis quidem aspernatur reprehenderit sunt ut ipsum tempora sapiente sed iste.',
+ reference: 'flightjs\u00262',
+ },
+ {
+ iid: 1,
+ title: 'Dicta incidunt vel dignissimos sint sit esse est quibusdam quidem consequatur.',
+ reference: 'flightjs\u00261',
+ },
+];
+
+export const MOCK_MERGE_REQUESTS = [
+ {
+ iid: 12,
+ title: "Always call registry's trigger method from withRegistration",
+ id: null,
+ },
+ {
+ iid: 11,
+ title: 'Draft: Alunny/publish lib',
+ id: null,
+ },
+ {
+ iid: 10,
+ title: 'Draft: Resolve "hgvbbvnnb"',
+ id: null,
+ },
+ {
+ iid: 9,
+ title: 'Draft: Fix event current target',
+ id: null,
+ },
+ {
+ iid: 3,
+ title: 'Autem eaque et sed provident enim corrupti molestiae.',
+ id: null,
+ },
+ {
+ iid: 2,
+ title: 'Blanditiis maxime voluptatem ut pariatur vel autem vero non quod libero.',
+ id: null,
+ },
+ {
+ iid: 1,
+ title: 'Optio nemo qui dolorem sit ipsum qui saepe.',
+ id: null,
+ },
+];
+
+export const MOCK_SNIPPETS = [
+ {
+ id: 24,
+ title: 'ss',
+ },
+ {
+ id: 22,
+ title: 'another test snippet',
+ },
+ {
+ id: 21,
+ title: 'test snippet',
+ },
+];
+
+export const MOCK_LABELS = [
+ {
+ title: 'Amsche',
+ color: '#9964cf',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Brioffe',
+ color: '#203e13',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Bronce',
+ color: '#c0b7f2',
+ type: 'GroupLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Bryncefunc',
+ color: '#8baa5e',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Contour',
+ color: '#8cf3a3',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Corolla',
+ color: '#0384f3',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Cygsync',
+ color: '#1308c3',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Frontier',
+ color: '#85db43',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Ghost',
+ color: '#df1bc4',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Grand Am',
+ color: '#a1d7ee',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Onesync',
+ color: '#a73ba0',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Phone',
+ color: '#63dceb',
+ type: 'GroupLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Pynefunc',
+ color: '#974b19',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Trinix',
+ color: '#2c894f',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Trounswood',
+ color: '#ad0370',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'group::knowledge',
+ color: '#8fbc8f',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'scoped label',
+ color: '#6699cc',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'type::one',
+ color: '#9400d3',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'type::two',
+ color: '#013220',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ },
+];
+
+export const MOCK_MILESTONES = [
+ {
+ iid: 65,
+ title: '15.0',
+ due_date: '2022-05-17',
+ id: null,
+ },
+ {
+ iid: 73,
+ title: '15.1',
+ due_date: '2022-06-17',
+ id: null,
+ },
+ {
+ iid: 74,
+ title: '15.2',
+ due_date: '2022-07-17',
+ id: null,
+ },
+ {
+ iid: 75,
+ title: '15.3',
+ due_date: '2022-08-17',
+ id: null,
+ },
+ {
+ iid: 76,
+ title: '15.4',
+ due_date: '2022-09-17',
+ id: null,
+ },
+ {
+ iid: 77,
+ title: '15.5',
+ due_date: '2022-10-17',
+ id: null,
+ },
+ {
+ iid: 81,
+ title: '15.6',
+ due_date: '2022-11-17',
+ id: null,
+ },
+ {
+ iid: 82,
+ title: '15.7',
+ due_date: '2022-12-17',
+ id: null,
+ },
+ {
+ iid: 83,
+ title: '15.8',
+ due_date: '2023-01-17',
+ id: null,
+ },
+ {
+ iid: 84,
+ title: '15.9',
+ due_date: '2023-02-17',
+ id: null,
+ },
+ {
+ iid: 85,
+ title: '15.10',
+ due_date: '2023-03-17',
+ id: null,
+ },
+ {
+ iid: 86,
+ title: '15.11',
+ due_date: '2023-04-17',
+ id: null,
+ },
+ {
+ iid: 80,
+ title: '16.0',
+ due_date: '2023-05-17',
+ id: null,
+ },
+ {
+ iid: 88,
+ title: '16.1',
+ due_date: '2023-06-17',
+ id: null,
+ },
+ {
+ iid: 89,
+ title: '16.2',
+ due_date: '2023-07-17',
+ id: null,
+ },
+ {
+ iid: 90,
+ title: '16.3',
+ due_date: '2023-08-17',
+ id: null,
+ },
+ {
+ iid: 91,
+ title: '16.4',
+ due_date: '2023-09-17',
+ id: null,
+ },
+ {
+ iid: 92,
+ title: '16.5',
+ due_date: '2023-10-17',
+ id: null,
+ },
+ {
+ iid: 93,
+ title: '16.6',
+ due_date: '2023-11-10',
+ id: null,
+ },
+ {
+ iid: 95,
+ title: '16.7',
+ due_date: '2023-12-15',
+ id: null,
+ },
+ {
+ iid: 94,
+ title: '16.8',
+ due_date: '2024-01-12',
+ id: null,
+ },
+ {
+ iid: 96,
+ title: '16.9',
+ due_date: '2024-02-09',
+ id: null,
+ },
+ {
+ iid: 97,
+ title: '16.10',
+ due_date: '2024-03-15',
+ id: null,
+ },
+ {
+ iid: 98,
+ title: '16.11',
+ due_date: '2024-04-12',
+ id: null,
+ },
+ {
+ iid: 87,
+ title: '17.0',
+ due_date: '2024-05-10',
+ id: null,
+ },
+ {
+ iid: 48,
+ title: 'Next 1-3 releases',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 24,
+ title: 'Awaiting further demand',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 14,
+ title: 'Backlog',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 11,
+ title: 'Next 4-7 releases',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 10,
+ title: 'Next 3-4 releases',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 6,
+ title: 'Next 7-13 releases',
+ due_date: null,
+ id: null,
+ },
+];
+
+export const MOCK_VULNERABILITIES = [
+ {
+ id: 99499903,
+ title: 'Cross Site Scripting (Persistent)',
+ },
+ {
+ id: 99495085,
+ title: 'Possible SQL injection',
+ },
+ {
+ id: 99490610,
+ title: 'GitLab Runner Authentication Token',
+ },
+ {
+ id: 99288920,
+ title: 'Cross Site Scripting (Persistent)',
+ },
+ {
+ id: 99258720,
+ title: 'Cross Site Scripting (Persistent)',
+ },
+];
+
+export const MOCK_COMMANDS = [
+ {
+ name: 'due',
+ aliases: [],
+ description: 'Set due date',
+ warning: '',
+ icon: '',
+ params: ['\u003cin 2 days | this Friday | December 31st\u003e'],
+ },
+ {
+ name: 'duplicate',
+ aliases: [],
+ description: 'Mark this issue as a duplicate of another issue',
+ warning: '',
+ icon: '',
+ params: ['#issue'],
+ },
+ {
+ name: 'clone',
+ aliases: [],
+ description: 'Clone this issue',
+ warning: '',
+ icon: '',
+ params: ['path/to/project [--with_notes]'],
+ },
+ {
+ name: 'move',
+ aliases: [],
+ description: 'Move this issue to another project.',
+ warning: '',
+ icon: '',
+ params: ['path/to/project'],
+ },
+ {
+ name: 'create_merge_request',
+ aliases: [],
+ description: 'Create a merge request',
+ warning: '',
+ icon: '',
+ params: ['\u003cbranch name\u003e'],
+ },
+ {
+ name: 'zoom',
+ aliases: [],
+ description: 'Add Zoom meeting',
+ warning: '',
+ icon: '',
+ params: ['\u003cZoom URL\u003e'],
+ },
+ {
+ name: 'promote_to_incident',
+ aliases: [],
+ description: 'Promote issue to incident',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'close',
+ aliases: [],
+ description: 'Close this issue',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'title',
+ aliases: [],
+ description: 'Change title',
+ warning: '',
+ icon: '',
+ params: ['\u003cNew title\u003e'],
+ },
+ {
+ name: 'label',
+ aliases: ['labels'],
+ description: 'Add labels',
+ warning: '',
+ icon: '',
+ params: ['~label1 ~"label 2"'],
+ },
+ {
+ name: 'unlabel',
+ aliases: ['remove_label'],
+ description: 'Remove all or specific labels',
+ warning: '',
+ icon: '',
+ params: ['~label1 ~"label 2"'],
+ },
+ {
+ name: 'relabel',
+ aliases: [],
+ description: 'Replace all labels',
+ warning: '',
+ icon: '',
+ params: ['~label1 ~"label 2"'],
+ },
+ {
+ name: 'todo',
+ aliases: [],
+ description: 'Add a to do',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'unsubscribe',
+ aliases: [],
+ description: 'Unsubscribe',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'award',
+ aliases: [],
+ description: 'Toggle emoji award',
+ warning: '',
+ icon: '',
+ params: [':emoji:'],
+ },
+ {
+ name: 'shrug',
+ aliases: [],
+ description: 'Append the comment with ¯\\_(ツ)_/¯',
+ warning: '',
+ icon: '',
+ params: ['\u003cComment\u003e'],
+ },
+ {
+ name: 'tableflip',
+ aliases: [],
+ description: 'Append the comment with (╯°□°)╯︵ ┻━┻',
+ warning: '',
+ icon: '',
+ params: ['\u003cComment\u003e'],
+ },
+ {
+ name: 'confidential',
+ aliases: [],
+ description: 'Make issue confidential',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'assign',
+ aliases: [],
+ description: 'Assign',
+ warning: '',
+ icon: '',
+ params: ['@user1 @user2'],
+ },
+ {
+ name: 'unassign',
+ aliases: [],
+ description: 'Remove all or specific assignees',
+ warning: '',
+ icon: '',
+ params: ['@user1 @user2'],
+ },
+ {
+ name: 'milestone',
+ aliases: [],
+ description: 'Set milestone',
+ warning: '',
+ icon: '',
+ params: ['%"milestone"'],
+ },
+ {
+ name: 'remove_milestone',
+ aliases: [],
+ description: 'Remove milestone',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'copy_metadata',
+ aliases: [],
+ description: 'Copy labels and milestone from other issue or merge request in this project',
+ warning: '',
+ icon: '',
+ params: ['#issue | !merge_request'],
+ },
+ {
+ name: 'estimate',
+ aliases: ['estimate_time'],
+ description: 'Set time estimate',
+ warning: '',
+ icon: '',
+ params: ['\u003c1w 3d 2h 14m\u003e'],
+ },
+ {
+ name: 'spend',
+ aliases: ['spent', 'spend_time'],
+ description: 'Add or subtract spent time',
+ warning: '',
+ icon: '',
+ params: ['\u003ctime(1h30m | -1h30m)\u003e \u003cdate(YYYY-MM-DD)\u003e'],
+ },
+ {
+ name: 'remove_estimate',
+ aliases: ['remove_time_estimate'],
+ description: 'Remove time estimate',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'remove_time_spent',
+ aliases: [],
+ description: 'Remove spent time',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'lock',
+ aliases: [],
+ description: 'Lock the discussion',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'cc',
+ aliases: [],
+ description: 'CC',
+ warning: '',
+ icon: '',
+ params: ['@user'],
+ },
+ {
+ name: 'relate',
+ aliases: [],
+ description: 'Mark this issue as related to another issue',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+ {
+ name: 'unlink',
+ aliases: [],
+ description: 'Remove link with another issue',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+ {
+ name: 'epic',
+ aliases: [],
+ description: 'Add to epic',
+ warning: '',
+ icon: '',
+ params: ['\u003c\u0026epic | group\u0026epic | Epic URL\u003e'],
+ },
+ {
+ name: 'remove_epic',
+ aliases: [],
+ description: 'Remove from epic',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'promote',
+ aliases: [],
+ description: 'Promote issue to an epic',
+ warning: '',
+ icon: 'confidential',
+ params: [],
+ },
+ {
+ name: 'iteration',
+ aliases: [],
+ description: 'Set iteration',
+ warning: '',
+ icon: '',
+ params: ['*iteration:"iteration name" | *iteration:\u003cID\u003e'],
+ },
+ {
+ name: 'health_status',
+ aliases: [],
+ description: 'Set health status',
+ warning: '',
+ icon: '',
+ params: ['\u003con_track|needs_attention|at_risk\u003e'],
+ },
+ {
+ name: 'reassign',
+ aliases: [],
+ description: 'Change assignees',
+ warning: '',
+ icon: '',
+ params: ['@user1 @user2'],
+ },
+ {
+ name: 'weight',
+ aliases: [],
+ description: 'Set weight',
+ warning: '',
+ icon: '',
+ params: ['0, 1, 2, …'],
+ },
+ {
+ name: 'blocks',
+ aliases: [],
+ description: 'Specifies that this issue blocks other issues',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+ {
+ name: 'blocked_by',
+ aliases: [],
+ description: 'Mark this issue as blocked by other issues',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+];
diff --git a/spec/frontend/content_editor/services/data_source_factory_spec.js b/spec/frontend/content_editor/services/data_source_factory_spec.js
new file mode 100644
index 00000000000..d540f11711d
--- /dev/null
+++ b/spec/frontend/content_editor/services/data_source_factory_spec.js
@@ -0,0 +1,202 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import DataSourceFactory, {
+ defaultSorter,
+ customSorter,
+ createDataSource,
+} from '~/content_editor/services/data_source_factory';
+import {
+ MOCK_MEMBERS,
+ MOCK_COMMANDS,
+ MOCK_EPICS,
+ MOCK_ISSUES,
+ MOCK_LABELS,
+ MOCK_MILESTONES,
+ MOCK_SNIPPETS,
+ MOCK_VULNERABILITIES,
+ MOCK_MERGE_REQUESTS,
+ MOCK_ASSIGNEES,
+ MOCK_REVIEWERS,
+} from './autocomplete_mock_data';
+
+jest.mock('~/emoji');
+
+describe('defaultSorter', () => {
+ it('returns items as is if query is empty', () => {
+ const items = [{ name: 'abc' }, { name: 'bcd' }, { name: 'cde' }];
+ const sorter = defaultSorter(['name']);
+ expect(sorter(items, '')).toEqual(items);
+ });
+
+ it('sorts items based on query match', () => {
+ const items = [{ name: 'abc' }, { name: 'bcd' }, { name: 'cde' }];
+ const sorter = defaultSorter(['name']);
+ expect(sorter(items, 'b')).toEqual([{ name: 'bcd' }, { name: 'abc' }, { name: 'cde' }]);
+ });
+
+ it('sorts items based on query match in multiple fields', () => {
+ const items = [
+ { name: 'wabc', description: 'xyz' },
+ { name: 'bcd', description: 'wxy' },
+ { name: 'cde', description: 'vwx' },
+ ];
+ const sorter = defaultSorter(['name', 'description']);
+ expect(sorter(items, 'w')).toEqual([
+ { name: 'wabc', description: 'xyz' },
+ { name: 'bcd', description: 'wxy' },
+ { name: 'cde', description: 'vwx' },
+ ]);
+ });
+});
+
+describe('customSorter', () => {
+ it('sorts items based on custom sorter function', () => {
+ const items = [3, 1, 2];
+ const sorter = customSorter((a, b) => a - b);
+ expect(sorter(items)).toEqual([1, 2, 3]);
+ });
+});
+
+describe('createDataSource', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('fetches data from source and filters based on query', async () => {
+ const data = [
+ { name: 'abc', description: 'xyz' },
+ { name: 'bcd', description: 'wxy' },
+ { name: 'cde', description: 'vwx' },
+ ];
+ mock.onGet('/source').reply(200, data);
+
+ const dataSource = createDataSource({
+ source: '/source',
+ searchFields: ['name', 'description'],
+ });
+
+ const results = await dataSource.search('b');
+ expect(results).toEqual([
+ { name: 'bcd', description: 'wxy' },
+ { name: 'abc', description: 'xyz' },
+ ]);
+ });
+
+ it('handles source fetch errors', async () => {
+ mock.onGet('/source').reply(500);
+
+ const dataSource = createDataSource({
+ source: '/source',
+ searchFields: ['name', 'description'],
+ sorter: (items) => items,
+ });
+
+ const results = await dataSource.search('b');
+ expect(results).toEqual([]);
+ });
+});
+
+describe('DataSourceFactory', () => {
+ let mock;
+ let autocompleteHelper;
+ let dateNowOld;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ const dataSourceUrls = {
+ members: '/members',
+ issues: '/issues',
+ snippets: '/snippets',
+ labels: '/labels',
+ epics: '/epics',
+ milestones: '/milestones',
+ mergeRequests: '/mergeRequests',
+ vulnerabilities: '/vulnerabilities',
+ commands: '/commands',
+ };
+
+ mock.onGet('/members').reply(200, MOCK_MEMBERS);
+ mock.onGet('/issues').reply(200, MOCK_ISSUES);
+ mock.onGet('/snippets').reply(200, MOCK_SNIPPETS);
+ mock.onGet('/labels').reply(200, MOCK_LABELS);
+ mock.onGet('/epics').reply(200, MOCK_EPICS);
+ mock.onGet('/milestones').reply(200, MOCK_MILESTONES);
+ mock.onGet('/mergeRequests').reply(200, MOCK_MERGE_REQUESTS);
+ mock.onGet('/vulnerabilities').reply(200, MOCK_VULNERABILITIES);
+ mock.onGet('/commands').reply(200, MOCK_COMMANDS);
+
+ const sidebarMediator = {
+ store: {
+ assignees: MOCK_ASSIGNEES,
+ reviewers: MOCK_REVIEWERS,
+ },
+ };
+
+ autocompleteHelper = new DataSourceFactory({
+ dataSourceUrls,
+ sidebarMediator,
+ });
+
+ dateNowOld = Date.now();
+
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2023-11-14').getTime());
+ });
+
+ afterEach(() => {
+ mock.restore();
+
+ jest.spyOn(Date, 'now').mockImplementation(() => dateNowOld);
+ });
+
+ it.each`
+ referenceType | query
+ ${'user'} | ${'r'}
+ ${'issue'} | ${'q'}
+ ${'snippet'} | ${'s'}
+ ${'label'} | ${'c'}
+ ${'epic'} | ${'n'}
+ ${'milestone'} | ${'16'}
+ ${'merge_request'} | ${'n'}
+ ${'vulnerability'} | ${'cross'}
+ ${'command'} | ${'re'}
+ `(
+ 'for reference type "$referenceType", searches for "$query" correctly',
+ async ({ referenceType, query }) => {
+ const dataSource = autocompleteHelper.getDataSource(referenceType);
+ const results = await dataSource.search(query);
+
+ expect(
+ results.map(({ title, name, username }) => username || name || title),
+ ).toMatchSnapshot();
+ },
+ );
+
+ it.each`
+ referenceType | command
+ ${'label'} | ${'/label'}
+ ${'label'} | ${'/unlabel'}
+ ${'label'} | ${'/relabel'}
+ ${'user'} | ${'/assign'}
+ ${'user'} | ${'/reassign'}
+ ${'user'} | ${'/unassign'}
+ ${'user'} | ${'/assign_reviewer'}
+ ${'user'} | ${'/unassign_reviewer'}
+ ${'user'} | ${'/reassign_reviewer'}
+ `(
+ 'filters items based on command "$command" for reference type "$referenceType" and command',
+ async ({ referenceType, command }) => {
+ const dataSource = autocompleteHelper.getDataSource(referenceType, { command });
+ const results = await dataSource.search();
+
+ expect(
+ results.map(({ username, name, title }) => username || name || title),
+ ).toMatchSnapshot();
+ },
+ );
+});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 548c6030ed7..c329a12bcc4 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -152,19 +152,26 @@ describe('markdownSerializer', () => {
expect(serialize(paragraph(italic('italics')))).toBe('_italics_');
});
- it('correctly serializes code blocks wrapped by italics and bold marks', () => {
- const codeBlockContent = 'code block';
-
- expect(serialize(paragraph(italic(code(codeBlockContent))))).toBe(`_\`${codeBlockContent}\`_`);
- expect(serialize(paragraph(code(italic(codeBlockContent))))).toBe(`_\`${codeBlockContent}\`_`);
- expect(serialize(paragraph(bold(code(codeBlockContent))))).toBe(`**\`${codeBlockContent}\`**`);
- expect(serialize(paragraph(code(bold(codeBlockContent))))).toBe(`**\`${codeBlockContent}\`**`);
- expect(serialize(paragraph(strike(code(codeBlockContent))))).toBe(
- `~~\`${codeBlockContent}\`~~`,
- );
- expect(serialize(paragraph(code(strike(codeBlockContent))))).toBe(
- `~~\`${codeBlockContent}\`~~`,
- );
+ it.each`
+ input | output
+ ${'code'} | ${'`code`'}
+ ${'code `with` backticks'} | ${'``code `with` backticks``'}
+ ${'this is `inline-code`'} | ${'`` this is `inline-code` ``'}
+ ${'`inline-code` in markdown'} | ${'`` `inline-code` in markdown ``'}
+ ${'```js'} | ${'`` ```js ``'}
+ `('correctly serializes inline code ("$input")', ({ input, output }) => {
+ expect(serialize(paragraph(code(input)))).toBe(output);
+ });
+
+ it('correctly serializes inline code wrapped by italics and bold marks', () => {
+ const content = 'code';
+
+ expect(serialize(paragraph(italic(code(content))))).toBe(`_\`${content}\`_`);
+ expect(serialize(paragraph(code(italic(content))))).toBe(`_\`${content}\`_`);
+ expect(serialize(paragraph(bold(code(content))))).toBe(`**\`${content}\`**`);
+ expect(serialize(paragraph(code(bold(content))))).toBe(`**\`${content}\`**`);
+ expect(serialize(paragraph(strike(code(content))))).toBe(`~~\`${content}\`~~`);
+ expect(serialize(paragraph(code(strike(content))))).toBe(`~~\`${content}\`~~`);
});
it('correctly serializes inline diff', () => {
@@ -461,6 +468,52 @@ this is not really json:table but just trying out whether this case works or not
);
});
+ it('correctly serializes a markdown code block containing a nested code block', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'markdown' },
+ 'markdown code block **bold** _italic_ `code`\n\n```js\nvar a = 0;\n```\n\nend markdown code block',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`\`markdown
+markdown code block **bold** _italic_ \`code\`
+
+\`\`\`js
+var a = 0;
+\`\`\`
+
+end markdown code block
+\`\`\`\`
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a markdown code block containing a markdown code block containing another code block', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'markdown' },
+ '````md\na nested code block\n\n```js\nvar a = 0;\n```\n````',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`\`\`markdown
+\`\`\`\`md
+a nested code block
+
+\`\`\`js
+var a = 0;
+\`\`\`
+\`\`\`\`
+\`\`\`\`\`
+ `.trim(),
+ );
+ });
+
it('correctly serializes emoji', () => {
expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
});
@@ -607,6 +660,34 @@ this is not really json:table but just trying out whether this case works or not
);
});
+ it('correctly serializes bullet task list with different bullet styles', () => {
+ expect(
+ serialize(
+ taskList(
+ { bullet: '+' },
+ taskItem({ checked: true }, paragraph('list item 1')),
+ taskItem(paragraph('list item 2')),
+ taskItem(
+ paragraph('list item 3'),
+ taskList(
+ { bullet: '-' },
+ 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 list', () => {
expect(
serialize(
diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
index 2efc73ddef8..4428fa682e7 100644
--- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
+++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
@@ -1,6 +1,8 @@
import { Extension } from '@tiptap/core';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
+import TaskList from '~/content_editor/extensions/task_list';
+import TaskItem from '~/content_editor/extensions/task_item';
import Paragraph from '~/content_editor/extensions/paragraph';
import markdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import { getMarkdownSource, getFullSource } from '~/content_editor/services/markdown_sourcemap';
@@ -18,6 +20,20 @@ const BULLET_LIST_HTML = `<ul data-sourcepos="1:1-3:24" dir="auto">
</li>
</ul>`;
+const BULLET_TASK_LIST_MARKDOWN = `- [ ] list item 1
++ [x] checked list item 2
+ + [ ] embedded list item 1
+ - [x] checked embedded list item 2`;
+const BULLET_TASK_LIST_HTML = `<ul data-sourcepos="1:1-4:36" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-1:17" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox"> list item 1</li>
+ <li data-sourcepos="2:1-4:36" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" checked> checked list item 2
+ <ul data-sourcepos="3:3-4:36" class="task-list">
+ <li data-sourcepos="3:3-3:28" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox"> embedded list item 1</li>
+ <li data-sourcepos="4:3-4:36" class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" checked> checked embedded list item 2</li>
+ </ul>
+ </li>
+</ul>`;
+
const SourcemapExtension = Extension.create({
// lets add `source` attribute to every element using `getMarkdownSource`
addGlobalAttributes() {
@@ -38,19 +54,68 @@ const SourcemapExtension = Extension.create({
});
const tiptapEditor = createTestEditor({
- extensions: [BulletList, ListItem, SourcemapExtension],
+ extensions: [BulletList, ListItem, TaskList, TaskItem, SourcemapExtension],
});
const {
- builders: { doc, bulletList, listItem, paragraph },
+ builders: { doc, bulletList, listItem, taskList, taskItem, paragraph },
} = createDocBuilder({
tiptapEditor,
names: {
bulletList: { nodeType: BulletList.name },
listItem: { nodeType: ListItem.name },
+ taskList: { nodeType: TaskList.name },
+ taskItem: { nodeType: TaskItem.name },
},
});
+const bulletListDoc = () =>
+ doc(
+ bulletList(
+ { bullet: '+', source: '+ list item 1\n+ list item 2\n - embedded list item 3' },
+ listItem({ source: '+ list item 1' }, paragraph('list item 1')),
+ listItem(
+ { source: '+ list item 2\n - embedded list item 3' },
+ paragraph('list item 2'),
+ bulletList(
+ { bullet: '-', source: '- embedded list item 3' },
+ listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')),
+ ),
+ ),
+ ),
+ );
+
+const bulletTaskListDoc = () =>
+ doc(
+ taskList(
+ {
+ bullet: '-',
+ source:
+ '- [ ] list item 1\n+ [x] checked list item 2\n + [ ] embedded list item 1\n - [x] checked embedded list item 2',
+ },
+ taskItem({ source: '- [ ] list item 1' }, paragraph('list item 1')),
+ taskItem(
+ {
+ source:
+ '+ [x] checked list item 2\n + [ ] embedded list item 1\n - [x] checked embedded list item 2',
+ checked: true,
+ },
+ paragraph('checked list item 2'),
+ taskList(
+ {
+ bullet: '+',
+ source: '+ [ ] embedded list item 1\n - [x] checked embedded list item 2',
+ },
+ taskItem({ source: '+ [ ] embedded list item 1' }, paragraph('embedded list item 1')),
+ taskItem(
+ { source: '- [x] checked embedded list item 2', checked: true },
+ paragraph('checked embedded list item 2'),
+ ),
+ ),
+ ),
+ ),
+ );
+
describe('content_editor/services/markdown_sourcemap', () => {
describe('getFullSource', () => {
it.each`
@@ -72,29 +137,21 @@ describe('content_editor/services/markdown_sourcemap', () => {
});
});
- it('gets markdown source for a rendered HTML element', async () => {
- const { document } = await markdownDeserializer({
- render: () => BULLET_LIST_HTML,
- }).deserialize({
- schema: tiptapEditor.schema,
- markdown: BULLET_LIST_MARKDOWN,
- });
-
- const expected = doc(
- bulletList(
- { bullet: '+', source: '+ list item 1\n+ list item 2' },
- listItem({ source: '+ list item 1' }, paragraph('list item 1')),
- listItem(
- { source: '+ list item 2' },
- paragraph('list item 2'),
- bulletList(
- { bullet: '-', source: '- embedded list item 3' },
- listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')),
- ),
- ),
- ),
- );
+ it.each`
+ description | sourceMarkdown | sourceHTML | expectedDoc
+ ${'bullet list'} | ${BULLET_LIST_MARKDOWN} | ${BULLET_LIST_HTML} | ${bulletListDoc}
+ ${'bullet task list'} | ${BULLET_TASK_LIST_MARKDOWN} | ${BULLET_TASK_LIST_HTML} | ${bulletTaskListDoc}
+ `(
+ 'gets markdown source for a rendered $description',
+ async ({ sourceMarkdown, sourceHTML, expectedDoc }) => {
+ const { document } = await markdownDeserializer({
+ render: () => sourceHTML,
+ }).deserialize({
+ schema: tiptapEditor.schema,
+ markdown: sourceMarkdown,
+ });
- expect(document.toJSON()).toEqual(expected.toJSON());
- });
+ expect(document.toJSON()).toEqual(expectedDoc().toJSON());
+ },
+ );
});
diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js
index cbd4f555e97..255a7104eaf 100644
--- a/spec/frontend/content_editor/test_constants.js
+++ b/spec/frontend/content_editor/test_constants.js
@@ -44,3 +44,18 @@ export const RESOLVED_MERGE_REQUEST_HTML =
export const RESOLVED_EPIC_HTML =
'<p data-sourcepos="1:1-1:11" dir="auto"><a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;amp;1" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">&amp;1</a> <a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;amp;1+" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-reference-format="+" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">Approvals in merge request list (&amp;1)</a> <a href="/groups/gitlab-org/-/epics/1" data-reference-type="epic" data-original="&amp;amp;1+s" data-link="false" data-link-reference="false" data-group="9970" data-epic="1" data-reference-format="+s" data-container="body" data-placement="top" title="Approvals in merge request list" class="gfm gfm-epic has-tooltip">Approvals in merge request list (&amp;1)</a></p>';
+
+export const RESOLVED_LABEL_HTML =
+ '<p data-sourcepos="1:1-1:29" dir="auto"><span class="gl-label gl-label-sm"><a href="/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix" data-reference-type="label" data-original="~Aquanix" data-link="false" data-link-reference="false" data-project="2" data-label="5" data-container="body" data-placement="top" title="" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #e65431">Aquanix</span></a></span> <span class="gl-label gl-label-sm"><a href="/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix" data-reference-type="label" data-original="~Aquanix" data-link="false" data-link-reference="false" data-project="2" data-label="5" data-container="body" data-placement="top" title="" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #e65431">Aquanix</span></a></span>+ <span class="gl-label gl-label-sm"><a href="/gitlab-org/gitlab-shell/-/issues?label_name=Aquanix" data-reference-type="label" data-original="~Aquanix" data-link="false" data-link-reference="false" data-project="2" data-label="5" data-container="body" data-placement="top" title="" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #e65431">Aquanix</span></a></span>+s</p>';
+
+export const RESOLVED_SNIPPET_HTML =
+ '<p data-sourcepos="1:1-1:14" dir="auto"><a href="/gitlab-org/gitlab-shell/-/snippets/25" data-reference-type="snippet" data-original="$25" data-link="false" data-link-reference="false" data-project="2" data-snippet="25" data-container="body" data-placement="top" title="test" class="gfm gfm-snippet has-tooltip">$25</a> <a href="/gitlab-org/gitlab-shell/-/snippets/25" data-reference-type="snippet" data-original="$25" data-link="false" data-link-reference="false" data-project="2" data-snippet="25" data-container="body" data-placement="top" title="test" class="gfm gfm-snippet has-tooltip">$25</a>+ <a href="/gitlab-org/gitlab-shell/-/snippets/25" data-reference-type="snippet" data-original="$25" data-link="false" data-link-reference="false" data-project="2" data-snippet="25" data-container="body" data-placement="top" title="test" class="gfm gfm-snippet has-tooltip">$25</a>+s</p>';
+
+export const RESOLVED_MILESTONE_HTML =
+ '<p data-sourcepos="1:1-1:20" dir="auto"><a href="/gitlab-org/gitlab-shell/-/milestones/5" data-reference-type="milestone" data-original="%v4.0" data-link="false" data-link-reference="false" data-project="2" data-milestone="10" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a> <a href="/gitlab-org/gitlab-shell/-/milestones/5" data-reference-type="milestone" data-original="%v4.0" data-link="false" data-link-reference="false" data-project="2" data-milestone="10" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a>+ %v4.0+s</p>';
+
+export const RESOLVED_USER_HTML =
+ '<p data-sourcepos="1:1-1:20" dir="auto"><a href="/root" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Administrator">@root</a> <a href="/root" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Administrator">@root</a>+ <a href="/root" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Administrator">@root</a>+s</p>';
+
+export const RESOLVED_VULNERABILITY_HTML =
+ '<p data-sourcepos="1:1-1:56" dir="auto"><a href="/gitlab-org/gitlab-shell/-/security/vulnerabilities/1" data-reference-type="vulnerability" data-original="[vulnerability:1]" data-link="false" data-link-reference="false" data-project="2" data-vulnerability="1" data-container="body" data-placement="top" title="oh no!" class="gfm gfm-vulnerability has-tooltip">[vulnerability:1]</a> <a href="/gitlab-org/gitlab-shell/-/security/vulnerabilities/1" data-reference-type="vulnerability" data-original="[vulnerability:1]" data-link="false" data-link-reference="false" data-project="2" data-vulnerability="1" data-container="body" data-placement="top" title="oh no!" class="gfm gfm-vulnerability has-tooltip">[vulnerability:1]</a>+ <a href="/gitlab-org/gitlab-shell/-/security/vulnerabilities/1" data-reference-type="vulnerability" data-original="[vulnerability:1]" data-link="false" data-link-reference="false" data-project="2" data-vulnerability="1" data-container="body" data-placement="top" title="oh no!" class="gfm gfm-vulnerability has-tooltip">[vulnerability:1]</a>+s</p>';