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_link_button_spec.js.snap36
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js9
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js151
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js131
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js19
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js37
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js61
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js45
-rw-r--r--spec/frontend/content_editor/test_utils.js132
9 files changed, 549 insertions, 72 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
new file mode 100644
index 00000000000..e56c37b0dc9
--- /dev/null
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`content_editor/components/toolbar_link_button renders dropdown component 1`] = `
+"<div class=\\"dropdown b-dropdown gl-new-dropdown btn-group\\" aria-label=\\"Insert link\\" title=\\"Insert link\\">
+ <!----><button aria-haspopup=\\"true\\" aria-expanded=\\"false\\" type=\\"button\\" class=\\"btn dropdown-toggle btn-default btn-sm gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only\\">
+ <!----> <svg data-testid=\\"link-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"dropdown-icon gl-icon s16\\">
+ <use href=\\"#link\\"></use>
+ </svg> <span class=\\"gl-new-dropdown-button-text\\"></span> <svg data-testid=\\"chevron-down-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-button-icon dropdown-chevron gl-icon s16\\">
+ <use href=\\"#chevron-down\\"></use>
+ </svg></button>
+ <ul role=\\"menu\\" tabindex=\\"-1\\" class=\\"dropdown-menu\\">
+ <div class=\\"gl-new-dropdown-inner\\">
+ <!---->
+ <div class=\\"gl-new-dropdown-contents\\">
+ <li role=\\"presentation\\" class=\\"gl-px-3!\\">
+ <form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
+ <div placeholder=\\"Link URL\\">
+ <div role=\\"group\\" class=\\"input-group\\">
+ <!---->
+ <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"gl-form-input form-control\\">
+ <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
+ <!---->
+ <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
+ <!---->
+ </div>
+ </div>
+ </form>
+ </li>
+ <!---->
+ <!---->
+ </div>
+ <!---->
+ </div>
+ </ul>
+</div>"
+`;
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index e3741032bf4..59c4190ad3a 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -27,7 +27,10 @@ describe('ContentEditor', () => {
it('renders editor content component and attaches editor instance', () => {
createWrapper(editor);
- expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor.tiptapEditor);
+ const editorContent = wrapper.findComponent(EditorContent);
+
+ expect(editorContent.props().editor).toBe(editor.tiptapEditor);
+ expect(editorContent.classes()).toContain('md');
});
it('renders top toolbar component and attaches editor instance', () => {
@@ -38,8 +41,8 @@ describe('ContentEditor', () => {
it.each`
isFocused | classes
- ${true} | ${['md', 'md-area', 'is-focused']}
- ${false} | ${['md', 'md-area']}
+ ${true} | ${['md-area', 'is-focused']}
+ ${false} | ${['md-area']}
`(
'has $classes class selectors when tiptapEditor.isFocused = $isFocused',
({ isFocused, classes }) => {
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
new file mode 100644
index 00000000000..812e769c891
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -0,0 +1,151 @@
+import { GlDropdown, GlDropdownDivider, GlFormInputGroup, GlButton } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
+import { tiptapExtension as Link } from '~/content_editor/extensions/link';
+import { hasSelection } from '~/content_editor/services/utils';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+jest.mock('~/content_editor/services/utils');
+
+describe('content_editor/components/toolbar_link_button', () => {
+ let wrapper;
+ let editor;
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ToolbarLinkButton, {
+ propsData: {
+ tiptapEditor: editor,
+ },
+ stubs: {
+ GlFormInputGroup,
+ },
+ });
+ };
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
+ const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
+ const findApplyLinkButton = () => wrapper.findComponent(GlButton);
+ const findRemoveLinkButton = () => wrapper.findByText('Remove link');
+
+ beforeEach(() => {
+ editor = createTestEditor({
+ extensions: [Link],
+ });
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ wrapper.destroy();
+ });
+
+ it('renders dropdown component', () => {
+ buildWrapper();
+
+ expect(findDropdown().html()).toMatchSnapshot();
+ });
+
+ describe('when there is an active link', () => {
+ beforeEach(() => {
+ jest.spyOn(editor, 'isActive');
+ editor.isActive.mockReturnValueOnce(true);
+ buildWrapper();
+ });
+
+ it('sets dropdown as active when link extension is active', () => {
+ expect(findDropdown().props('toggleClass')).toEqual({ active: true });
+ });
+
+ it('displays a remove link dropdown option', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ expect(wrapper.findByText('Remove link').exists()).toBe(true);
+ });
+
+ it('executes removeLink command when the remove link option is clicked', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'run']);
+
+ await findRemoveLinkButton().trigger('click');
+
+ expect(commands.unsetLink).toHaveBeenCalled();
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
+ });
+
+ it('updates the link with a new link when "Apply" button is clicked', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
+
+ await findLinkURLInput().setValue('https://example');
+ await findApplyLinkButton().trigger('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.unsetLink).toHaveBeenCalled();
+ expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('when there is not an active link', () => {
+ beforeEach(() => {
+ jest.spyOn(editor, 'isActive');
+ editor.isActive.mockReturnValueOnce(false);
+ buildWrapper();
+ });
+
+ it('does not set dropdown as active', () => {
+ expect(findDropdown().props('toggleClass')).toEqual({ active: false });
+ });
+
+ it('does not display a remove link dropdown option', () => {
+ expect(findDropdownDivider().exists()).toBe(false);
+ expect(wrapper.findByText('Remove link').exists()).toBe(false);
+ });
+
+ it('sets the link to the value in the URL input when "Apply" button is clicked', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
+
+ await findLinkURLInput().setValue('https://example');
+ await findApplyLinkButton().trigger('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the user displays the dropdown', () => {
+ let commands;
+
+ beforeEach(() => {
+ commands = mockChainedCommands(editor, ['focus', 'extendMarkRange', 'run']);
+ });
+
+ describe('given the user has not selected text', () => {
+ beforeEach(() => {
+ hasSelection.mockReturnValueOnce(false);
+ });
+
+ it('the editor selection is extended to the current mark extent', () => {
+ buildWrapper();
+
+ findDropdown().vm.$emit('show');
+ expect(commands.extendMarkRange).toHaveBeenCalledWith(Link.name);
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('given the user has selected text', () => {
+ beforeEach(() => {
+ hasSelection.mockReturnValueOnce(true);
+ });
+
+ it('the editor does not modify the current selection', () => {
+ buildWrapper();
+
+ findDropdown().vm.$emit('show');
+ expect(commands.extendMarkRange).not.toHaveBeenCalled();
+ expect(commands.focus).not.toHaveBeenCalled();
+ expect(commands.run).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
new file mode 100644
index 00000000000..8c54f6bb8bb
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -0,0 +1,131 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
+import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
+import { createTestContentEditorExtension, createTestEditor } from '../test_utils';
+
+describe('content_editor/components/toolbar_headings_dropdown', () => {
+ let wrapper;
+ let tiptapEditor;
+ let commandMocks;
+
+ const buildEditor = () => {
+ const testExtension = createTestContentEditorExtension({
+ commands: TEXT_STYLE_DROPDOWN_ITEMS.map((item) => item.editorCommand),
+ });
+
+ commandMocks = testExtension.commandMocks;
+ tiptapEditor = createTestEditor({
+ extensions: [testExtension.tiptapExtension],
+ });
+
+ jest.spyOn(tiptapEditor, 'isActive');
+ };
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMountExtended(ToolbarTextStyleDropdown, {
+ stubs: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ propsData: {
+ tiptapEditor,
+ ...propsData,
+ },
+ });
+ };
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
+ beforeEach(() => {
+ buildEditor();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders all text styles as dropdown items', () => {
+ buildWrapper();
+
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle) => {
+ expect(wrapper.findByText(textStyle.label).exists()).toBe(true);
+ });
+ });
+
+ describe('when there is an active item ', () => {
+ let activeTextStyle;
+
+ beforeEach(() => {
+ [, activeTextStyle] = TEXT_STYLE_DROPDOWN_ITEMS;
+
+ tiptapEditor.isActive.mockImplementation(
+ (contentType, params) =>
+ activeTextStyle.contentType === contentType && activeTextStyle.commandParams === params,
+ );
+
+ buildWrapper();
+ });
+
+ it('displays the active text style label as the dropdown toggle text ', () => {
+ expect(findDropdown().props().text).toBe(activeTextStyle.label);
+ });
+
+ it('sets dropdown as enabled', () => {
+ expect(findDropdown().props().disabled).toBe(false);
+ });
+
+ it('sets active item as active', () => {
+ const activeItem = wrapper
+ .findAllComponents(GlDropdownItem)
+ .filter((item) => item.text() === activeTextStyle.label)
+ .at(0);
+ expect(activeItem.props().isChecked).toBe(true);
+ });
+ });
+
+ describe('when there isn’t an active item', () => {
+ beforeEach(() => {
+ tiptapEditor.isActive.mockReturnValue(false);
+ buildWrapper();
+ });
+
+ it('sets dropdown as disabled', () => {
+ expect(findDropdown().props().disabled).toBe(true);
+ });
+
+ it('sets dropdown toggle text to Text style', () => {
+ expect(findDropdown().props().text).toBe('Text style');
+ });
+ });
+
+ describe('when a text style is selected', () => {
+ it('executes the tiptap command related to that text style', () => {
+ buildWrapper();
+
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
+ const { editorCommand, commandParams } = textStyle;
+
+ wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
+ expect(commandMocks[editorCommand]).toHaveBeenCalledWith(commandParams || {});
+ });
+ });
+
+ it('emits execute event with contentType and value params that indicates the heading level', () => {
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
+ buildWrapper();
+ const { contentType, commandParams } = textStyle;
+
+ wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
+ expect(wrapper.emitted('execute')).toEqual([
+ [
+ {
+ contentType,
+ value: commandParams?.level,
+ },
+ ],
+ ]);
+ wrapper.destroy();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index 8f47be3f489..0a1405a1774 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -39,32 +39,35 @@ describe('content_editor/components/top_toolbar', () => {
});
describe.each`
- testId | buttonProps
+ testId | controlProps
${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
${'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' }}
- `('given a $testId toolbar control', ({ testId, buttonProps }) => {
+ ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
+ ${'text-styles'} | ${{}}
+ ${'link'} | ${{}}
+ `('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
});
it('renders the toolbar control with the provided properties', () => {
expect(wrapper.findByTestId(testId).props()).toEqual({
- ...buttonProps,
+ ...controlProps,
tiptapEditor: contentEditor.tiptapEditor,
});
});
it.each`
- control | eventData
- ${'bold'} | ${{ contentType: 'bold' }}
- ${'blockquote'} | ${{ contentType: 'blockquote', value: 1 }}
- `('tracks the execution of toolbar controls', ({ control, eventData }) => {
+ eventData
+ ${{ contentType: 'bold' }}
+ ${{ contentType: 'blockquote', value: 1 }}
+ `('tracks the execution of toolbar controls', ({ eventData }) => {
const { contentType, value } = eventData;
- wrapper.findByTestId(control).vm.$emit('execute', eventData);
+ wrapper.findByTestId(testId).vm.$emit('execute', eventData);
expect(trackingSpy).toHaveBeenCalledWith(undefined, TOOLBAR_CONTROL_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL,
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
new file mode 100644
index 00000000000..cc695ffe241
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -0,0 +1,37 @@
+import { tiptapExtension as CodeBlockHighlight } from '~/content_editor/extensions/code_block_highlight';
+import { loadMarkdownApiResult } from '../markdown_processing_examples';
+import { createTestEditor } from '../test_utils';
+
+describe('content_editor/extensions/code_block_highlight', () => {
+ let codeBlockHtmlFixture;
+ let parsedCodeBlockHtmlFixture;
+ let tiptapEditor;
+
+ const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
+ const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
+
+ beforeEach(() => {
+ const { html } = loadMarkdownApiResult('code_block');
+
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
+ codeBlockHtmlFixture = html;
+ parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
+
+ tiptapEditor.commands.setContent(codeBlockHtmlFixture);
+ });
+
+ it('extracts language and params attributes from Markdown API output', () => {
+ const language = preElement().getAttribute('lang');
+
+ expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
+ language,
+ params: language,
+ });
+ });
+
+ it('adds code, highlight, and js-syntax-highlight to code block element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+
+ expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
new file mode 100644
index 00000000000..026b2a06df3
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -0,0 +1,61 @@
+import {
+ markdownLinkSyntaxInputRuleRegExp,
+ urlSyntaxRegExp,
+ extractHrefFromMarkdownLink,
+} from '~/content_editor/extensions/link';
+
+describe('content_editor/extensions/link', () => {
+ describe.each`
+ input | matches
+ ${'[gitlab](https://gitlab.com)'} | ${true}
+ ${'[documentation](readme.md)'} | ${true}
+ ${'[link 123](readme.md)'} | ${true}
+ ${'[link 123](read me.md)'} | ${true}
+ ${'text'} | ${false}
+ ${'documentation](readme.md'} | ${false}
+ ${'https://www.google.com'} | ${false}
+ `('markdownLinkSyntaxInputRuleRegExp', ({ input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
+ const match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
+
+ expect(Boolean(match?.groups.href)).toBe(matches);
+ });
+ });
+
+ describe.each`
+ input | matches
+ ${'http://example.com '} | ${true}
+ ${'https://example.com '} | ${true}
+ ${'www.example.com '} | ${true}
+ ${'example.com/ab.html '} | ${false}
+ ${'text'} | ${false}
+ ${' http://example.com '} | ${true}
+ ${'https://www.google.com '} | ${true}
+ `('urlSyntaxRegExp', ({ input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
+ const match = new RegExp(urlSyntaxRegExp).exec(input);
+
+ expect(Boolean(match?.groups.href)).toBe(matches);
+ });
+ });
+
+ describe('extractHrefFromMarkdownLink', () => {
+ const input = '[gitlab](https://gitlab.com)';
+ const href = 'https://gitlab.com';
+ let match;
+ let result;
+
+ beforeEach(() => {
+ match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
+ result = extractHrefFromMarkdownLink(match);
+ });
+
+ it('extracts the url from a markdown link captured by markdownLinkSyntaxInputRuleRegExp', () => {
+ expect(result).toEqual({ href });
+ });
+
+ it('makes sure that url text is the last capture group', () => {
+ expect(match[match.length - 1]).toEqual('gitlab');
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index 437714ba938..cf74b5c56c9 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -5,11 +5,8 @@ import { Heading } from '@tiptap/extension-heading';
import { ListItem } from '@tiptap/extension-list-item';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Text } from '@tiptap/extension-text';
-import { Editor, EditorContent } from '@tiptap/vue-2';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { Editor } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
KEYBOARD_SHORTCUT_TRACKING_ACTION,
INPUT_RULE_TRACKING_ACTION,
@@ -19,47 +16,33 @@ import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_r
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
describe('content_editor/services/track_input_rules_and_shortcuts', () => {
- let wrapper;
let trackingSpy;
let editor;
+ let trackedExtensions;
const HEADING_TEXT = 'Heading text';
-
- const buildWrapper = () => {
- wrapper = extendedWrapper(
- mount(EditorContent, {
- propsData: {
- editor,
- },
- }),
- );
- };
+ const extensions = [Document, Paragraph, Text, Heading, CodeBlockLowlight, BulletList, ListItem];
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('given the heading extension is instrumented', () => {
beforeEach(() => {
+ trackedExtensions = extensions.map(trackInputRulesAndShortcuts);
editor = new Editor({
- extensions: [
- Document,
- Paragraph,
- Text,
- Heading,
- CodeBlockLowlight,
- BulletList,
- ListItem,
- ].map(trackInputRulesAndShortcuts),
+ extensions: extensions.map(trackInputRulesAndShortcuts),
});
});
- beforeEach(async () => {
- buildWrapper();
- await nextTick();
+ it('does not remove existing keyboard shortcuts', () => {
+ extensions.forEach((extension, index) => {
+ const originalShortcuts = Object.keys(extension.addKeyboardShortcuts?.() || {});
+ const trackedShortcuts = Object.keys(
+ trackedExtensions[index].addKeyboardShortcuts?.() || {},
+ );
+
+ expect(originalShortcuts).toEqual(trackedShortcuts);
+ });
});
describe('when creating a heading using an keyboard shortcut', () => {
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index a92ceb6d058..8e73aef678b 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -1,34 +1,106 @@
import { Node } from '@tiptap/core';
+import { Document } from '@tiptap/extension-document';
+import { Paragraph } from '@tiptap/extension-paragraph';
+import { Text } from '@tiptap/extension-text';
+import { Editor } from '@tiptap/vue-2';
-export const createTestContentEditorExtension = () => ({
- tiptapExtension: Node.create({
- name: 'label',
- priority: 101,
- inline: true,
- group: 'inline',
- addAttributes() {
- return {
- labelName: {
- default: null,
- parseHTML: (element) => {
- return { labelName: element.dataset.labelName };
+/**
+ * Creates an instance of the Tiptap Editor class
+ * with a minimal configuration for testing purposes.
+ *
+ * It only includes the Document, Text, and Paragraph
+ * extensions.
+ *
+ * @param {Array} config.extensions One or more extensions to
+ * include in the editor
+ * @returns An instance of a Tiptap’s Editor class
+ */
+export const createTestEditor = ({ extensions = [] }) => {
+ return new Editor({
+ extensions: [Document, Text, Paragraph, ...extensions],
+ });
+};
+
+export const mockChainedCommands = (editor, commandNames = []) => {
+ const commandMocks = commandNames.reduce(
+ (accum, commandName) => ({
+ ...accum,
+ [commandName]: jest.fn(),
+ }),
+ {},
+ );
+
+ Object.keys(commandMocks).forEach((commandName) => {
+ commandMocks[commandName].mockReturnValue(commandMocks);
+ });
+
+ jest.spyOn(editor, 'chain').mockImplementation(() => commandMocks);
+
+ return commandMocks;
+};
+
+/**
+ * Creates a Content Editor extension for testing
+ * purposes.
+ *
+ * @param {Array} config.commands A list of command names
+ * to include in the test extension. This utility will create
+ * Jest mock functions for each command name.
+ * @returns An object with the following properties:
+ *
+ * tiptapExtension A Node tiptap extension
+ * commandMocks Jest mock functions for each created command
+ * serializer A markdown serializer for the extension
+ */
+export const createTestContentEditorExtension = ({ commands = [] } = {}) => {
+ const commandMocks = commands.reduce(
+ (accum, commandName) => ({
+ ...accum,
+ [commandName]: jest.fn(),
+ }),
+ {},
+ );
+
+ return {
+ commandMocks,
+ tiptapExtension: Node.create({
+ name: 'label',
+ priority: 101,
+ inline: true,
+ group: 'inline',
+ addCommands() {
+ return commands.reduce(
+ (accum, commandName) => ({
+ ...accum,
+ [commandName]: (...params) => () => commandMocks[commandName](...params),
+ }),
+ {},
+ );
+ },
+ addAttributes() {
+ return {
+ labelName: {
+ default: null,
+ parseHTML: (element) => {
+ return { labelName: element.dataset.labelName };
+ },
},
- },
- };
- },
- parseHTML() {
- return [
- {
- tag: 'span[data-reference="label"]',
- },
- ];
- },
- renderHTML({ HTMLAttributes }) {
- return ['span', HTMLAttributes, 0];
+ };
+ },
+ parseHTML() {
+ return [
+ {
+ tag: 'span[data-reference="label"]',
+ },
+ ];
+ },
+ renderHTML({ HTMLAttributes }) {
+ return ['span', HTMLAttributes, 0];
+ },
+ }),
+ serializer: (state, node) => {
+ state.write(`~${node.attrs.labelName}`);
+ state.closeBlock(node);
},
- }),
- serializer: (state, node) => {
- state.write(`~${node.attrs.labelName}`);
- state.closeBlock(node);
- },
-});
+ };
+};