Commit d2ddb757 authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Paul Slaughter
parent f1a40767
<script>
import { EditorContent, Editor } from 'tiptap';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { ContentEditor } from '../services/content_editor';
import TopToolbar from './top_toolbar.vue';
export default {
components: {
EditorContent,
TiptapEditorContent,
TopToolbar,
},
props: {
editor: {
type: Object,
contentEditor: {
type: ContentEditor,
required: true,
validator: (editor) => editor instanceof Editor,
},
},
};
</script>
<template>
<div class="md md-area" :class="{ 'is-focused': editor.focused }">
<top-toolbar class="gl-mb-4" :editor="editor" />
<editor-content :editor="editor" />
<div class="md md-area" :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }">
<top-toolbar class="gl-mb-4" :content-editor="contentEditor" />
<tiptap-editor-content :editor="contentEditor.tiptapEditor" />
</div>
</template>
<script>
import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2';
export default {
components: {
......@@ -13,8 +14,8 @@ export default {
type: String,
required: true,
},
editor: {
type: Object,
tiptapEditor: {
type: TiptapEditor,
required: true,
},
contentType: {
......@@ -25,23 +26,23 @@ export default {
type: String,
required: true,
},
executeCommand: {
type: Boolean,
editorCommand: {
type: String,
required: false,
default: true,
default: '',
},
},
computed: {
isActive() {
return this.editor.isActive[this.contentType]() && this.editor.focused;
return this.tiptapEditor.isActive(this.contentType) && this.tiptapEditor.isFocused;
},
},
methods: {
execute() {
const { contentType } = this;
if (this.executeCommand) {
this.editor.commands[contentType]();
if (this.editorCommand) {
this.tiptapEditor.chain()[this.editorCommand]().focus().run();
}
this.$emit('click', { contentType });
......
<script>
import { ContentEditor } from '../services/content_editor';
import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
......@@ -8,8 +9,8 @@ export default {
Divider,
},
props: {
editor: {
type: Object,
contentEditor: {
type: ContentEditor,
required: true,
},
},
......@@ -23,44 +24,50 @@ export default {
data-testid="bold"
content-type="bold"
icon-name="bold"
editor-command="toggleBold"
:label="__('Bold text')"
:editor="editor"
:tiptap-editor="contentEditor.tiptapEditor"
/>
<toolbar-button
data-testid="italic"
content-type="italic"
icon-name="italic"
editor-command="toggleItalic"
:label="__('Italic text')"
:editor="editor"
:tiptap-editor="contentEditor.tiptapEditor"
/>
<toolbar-button
data-testid="code"
content-type="code"
icon-name="code"
editor-command="toggleCode"
:label="__('Code')"
:editor="editor"
:tiptap-editor="contentEditor.tiptapEditor"
/>
<divider />
<toolbar-button
data-testid="blockquote"
content-type="blockquote"
icon-name="quote"
editor-command="toggleBlockquote"
:label="__('Insert a quote')"
:editor="editor"
:tiptap-editor="contentEditor.tiptapEditor"
/>
<toolbar-button
data-testid="bullet-list"
content-type="bullet_list"
content-type="bulletList"
icon-name="list-bulleted"
editor-command="toggleBulletList"
:label="__('Add a bullet list')"
:editor="editor"
:tiptap-editor="contentEditor.tiptapEditor"
/>
<toolbar-button
data-testid="ordered-list"
content-type="ordered_list"
content-type="orderedList"
icon-name="list-numbered"
editor-command="toggleOrderedList"
:label="__('Add a numbered list')"
:editor="editor"
:tiptap-editor="contentEditor.tiptapEditor"
/>
</div>
</template>
import { CodeBlockHighlight as BaseCodeBlockHighlight } from 'tiptap-extensions';
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
export default class GlCodeBlockHighlight extends BaseCodeBlockHighlight {
get schema() {
const baseSchema = super.schema;
const extractLanguage = (element) => element.firstElementChild?.getAttribute('lang');
export default CodeBlockLowlight.extend({
addAttributes() {
return {
...baseSchema,
attrs: {
params: {
default: null,
...this.parent(),
/* `params` is the name of the attribute that
prosemirror-markdown uses to extract the language
of a codeblock.
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.js#L62
*/
params: {
parseHTML: (element) => {
return {
params: extractLanguage(element),
};
},
},
parseDOM: [
{
tag: 'pre',
preserveWhitespace: 'full',
getAttrs: (node) => {
const code = node.querySelector('code');
if (!code) {
return null;
}
return {
/* `params` is the name of the attribute that
prosemirror-markdown uses to extract the language
of a codeblock.
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.js#L62
*/
params: code.getAttribute('lang'),
};
},
},
],
};
}
}
},
});
export { default as createEditor } from './services/create_editor';
export * from './services/create_content_editor';
export { default as ContentEditor } from './components/content_editor.vue';
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
}
get tiptapEditor() {
return this._tiptapEditor;
}
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _serializer: serializer } = this;
editor.commands.setContent(
await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
);
}
getSerializedContent() {
const { _tiptapEditor: editor, _serializer: serializer } = this;
return serializer.serialize({ schema: editor.schema, content: editor.getJSON() });
}
}
import Blockquote from '@tiptap/extension-blockquote';
import Bold from '@tiptap/extension-bold';
import BulletList from '@tiptap/extension-bullet-list';
import Code from '@tiptap/extension-code';
import Document from '@tiptap/extension-document';
import Dropcursor from '@tiptap/extension-dropcursor';
import Gapcursor from '@tiptap/extension-gapcursor';
import HardBreak from '@tiptap/extension-hard-break';
import Heading from '@tiptap/extension-heading';
import History from '@tiptap/extension-history';
import HorizontalRule from '@tiptap/extension-horizontal-rule';
import Image from '@tiptap/extension-image';
import Italic from '@tiptap/extension-italic';
import Link from '@tiptap/extension-link';
import ListItem from '@tiptap/extension-list-item';
import OrderedList from '@tiptap/extension-ordered-list';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
const createTiptapEditor = ({ extensions = [], options } = {}) =>
new Editor({
extensions: [
Dropcursor,
Gapcursor,
History,
Document,
Text,
Paragraph,
Bold,
Italic,
Code,
Link,
Heading,
HardBreak,
Blockquote,
HorizontalRule,
BulletList,
OrderedList,
ListItem,
Image.configure({ inline: true }),
CodeBlockHighlight,
...extensions,
],
editorProps: {
attributes: {
class: 'gl-outline-0!',
},
},
...options,
});
export const createContentEditor = ({ renderMarkdown, extensions = [], tiptapOptions } = {}) => {
if (!isFunction(renderMarkdown)) {
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}
const tiptapEditor = createTiptapEditor({ extensions, options: tiptapOptions });
const serializer = createMarkdownSerializer({ render: renderMarkdown });
return new ContentEditor({ tiptapEditor, serializer });
};
import { isFunction, isString } from 'lodash';
import { Editor } from 'tiptap';
import {
Bold,
Italic,
Code,
Link,
Image,
Heading,
Blockquote,
HorizontalRule,
BulletList,
OrderedList,
ListItem,
HardBreak,
} from 'tiptap-extensions';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import createMarkdownSerializer from './markdown_serializer';
const createEditor = async ({
content,
renderMarkdown,
serializer: customSerializer,
...options
} = {}) => {
if (!customSerializer && !isFunction(renderMarkdown)) {
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}
const editor = new Editor({
extensions: [
new Bold(),
new Italic(),
new Code(),
new Link(),
new Image(),
new Heading({ levels: [1, 2, 3, 4, 5, 6] }),
new Blockquote(),
new HorizontalRule(),
new BulletList(),
new ListItem(),
new OrderedList(),
new CodeBlockHighlight(),
new HardBreak(),
],
editorProps: {
attributes: {
class: 'gl-outline-0!',
},
},
...options,
});
const serializer = customSerializer || createMarkdownSerializer({ render: renderMarkdown });
editor.setSerializedContent = async (serializedContent) => {
editor.setContent(
await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
);
};
editor.getSerializedContent = () => {
return serializer.serialize({ schema: editor.schema, content: editor.getJSON() });
};
if (isString(content)) {
await editor.setSerializedContent(content);
}
return editor;
};
export default createEditor;
......@@ -54,14 +54,24 @@ const create = ({ render = () => null }) => {
*/
serialize: ({ schema, content }) => {
const document = schema.nodeFromJSON(content);
const serializer = new ProseMirrorMarkdownSerializer(defaultMarkdownSerializer.nodes, {
...defaultMarkdownSerializer.marks,
bold: {
// creates a bold alias for the strong mark converter
...defaultMarkdownSerializer.marks.strong,
const { nodes, marks } = defaultMarkdownSerializer;
const serializer = new ProseMirrorMarkdownSerializer(
{
...defaultMarkdownSerializer.nodes,
horizontalRule: nodes.horizontal_rule,
bulletList: nodes.bullet_list,
listItem: nodes.list_item,
orderedList: nodes.ordered_list,
codeBlock: nodes.code_block,
hardBreak: nodes.hard_break,
},
italic: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
});
{
...defaultMarkdownSerializer.marks,
bold: marks.strong,
italic: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
},
);
return serializer.serialize(document, {
tightLists: true,
......
......@@ -39,7 +39,7 @@ export default {
isContentEditorLoading: true,
useContentEditor: false,
commitMessage: '',
editor: null,
contentEditor: null,
isDirty: false,
};
},
......@@ -102,7 +102,7 @@ export default {
handleFormSubmit() {
if (this.useContentEditor) {
this.content = this.editor.getSerializedContent();
this.content = this.contentEditor.getSerializedContent();
}
this.isDirty = false;
......@@ -136,16 +136,18 @@ export default {
this.isContentEditorLoading = true;
this.useContentEditor = true;
const createEditor = await import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_editor'
const { createContentEditor } = await import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_content_editor'
);
this.editor =
this.editor ||
(await createEditor.default({
this.contentEditor =
this.contentEditor ||
createContentEditor({
renderMarkdown: (markdown) => this.getContentHTML(markdown),
onUpdate: () => this.handleContentChange(),
}));
await this.editor.setSerializedContent(this.content);
tiptapOptions: {
onUpdate: () => this.handleContentChange(),
},
});
await this.contentEditor.setSerializedContent(this.content);
this.isContentEditorLoading = false;
},
......@@ -296,7 +298,7 @@ export default {
<div v-if="isContentEditorActive">
<gl-loading-icon v-if="isContentEditorLoading" class="bordered-box gl-w-full gl-py-6" />
<content-editor v-else :editor="editor" />
<content-editor v-else :content-editor="contentEditor" />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
</div>
......
......@@ -307,11 +307,11 @@ module.exports = {
chunks: 'initial',
minChunks: autoEntriesCount * 0.9,
}),
tiptap: {
prosemirror: {
priority: 17,
name: 'tiptap',
name: 'prosemirror',
chunks: 'all',
test: /[\\/]node_modules[\\/](tiptap|prosemirror)-?\w*[\\/]/,
test: /[\\/]node_modules[\\/]prosemirror.*?[\\/]/,
minChunks: 2,
reuseExistingChunk: true,
},
......
import { mount } from '@vue/test-utils';
import { EditorContent } from 'tiptap';
import waitForPromises from 'helpers/wait_for_promises';
import { EditorContent } from '@tiptap/vue-2';
import { shallowMount } from '@vue/test-utils';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import createEditor from '~/content_editor/services/create_editor';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
describe('ContentEditor', () => {
let wrapper;
let editor;
const createWrapper = async (_editor) => {
wrapper = mount(ContentEditor, {
const createWrapper = async (contentEditor) => {
wrapper = shallowMount(ContentEditor, {
propsData: {
editor: _editor,
contentEditor,
},
});
};
beforeEach(async () => {
editor = await createEditor({ renderMarkdown: () => 'sample text' });
createWrapper(editor);
await waitForPromises();
beforeEach(() => {
editor = createContentEditor({ renderMarkdown: () => true });
});
afterEach(() => {
wrapper.destroy();
});
it('renders editor content component and attaches editor instance', async () => {
expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor);
it('renders editor content component and attaches editor instance', () => {
createWrapper(editor);
expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor.tiptapEditor);
});
it('renders top toolbar component and attaches editor instance', () => {
createWrapper(editor);
expect(wrapper.findComponent(TopToolbar).props().contentEditor).toBe(editor);
});
it('renders top toolbar component and attaches editor instance', async () => {
expect(wrapper.findComponent(TopToolbar).props().editor).toBe(editor);
it.each`
isFocused | classes
${true} | ${['md', 'md-area', 'is-focused']}
${false} | ${['md', 'md-area']}
`(
'has $classes class selectors when tiptapEditor.isFocused = $isFocused',
({ isFocused, classes }) => {
editor.tiptapEditor.isFocused = isFocused;
createWrapper(editor);
expect(wrapper.classes()).toStrictEqual(classes);
},
);
it('adds isFocused class when tiptapEditor is focused', () => {
editor.tiptapEditor.isFocused = true;
createWrapper(editor);
expect(wrapper.classes()).toContain('is-focused');
});
});
import { GlButton } from '@gitlab/ui';
import { Extension } from '@tiptap/core';
import { shallowMount } from '@vue/test-utils';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
describe('content_editor/components/toolbar_button', () => {
let wrapper;
let editor;
let tiptapEditor;
let toggleFooSpy;
const CONTENT_TYPE = 'bold';
const ICON_NAME = 'bold';
const LABEL = 'Bold';
const buildEditor = () => {
editor = {
isActive: {
[CONTENT_TYPE]: jest.fn(),
},
commands: {
[CONTENT_TYPE]: jest.fn(),
},
};
toggleFooSpy = jest.fn();
tiptapEditor = createContentEditor({
extensions: [
Extension.create({
addCommands() {
return {
toggleFoo: () => toggleFooSpy,
};
},
}),
],
renderMarkdown: () => true,
}).tiptapEditor;
jest.spyOn(tiptapEditor, 'isActive');
};
const buildWrapper = (propsData = {}) => {
......@@ -26,7 +36,7 @@ describe('content_editor/components/toolbar_button', () => {
GlButton,
},
propsData: {
editor,
tiptapEditor,
contentType: CONTENT_TYPE,
iconName: ICON_NAME,
label: LABEL,
......@@ -51,34 +61,35 @@ describe('content_editor/components/toolbar_button', () => {
});
it.each`
editorState | outcomeDescription | outcome
${{ isActive: true, focused: true }} | ${'button is active'} | ${true}
${{ isActive: false, focused: true }} | ${'button is not active'} | ${false}
${{ isActive: true, focused: false }} | ${'button is not active '} | ${false}
editorState | outcomeDescription | outcome
${{ isActive: true, isFocused: true }} | ${'button is active'} | ${true}
${{ isActive: false, isFocused: true }} | ${'button is not active'} | ${false}
${{ isActive: true, isFocused: false }} | ${'button is not active '} | ${false}
`('$outcomeDescription when when editor state is $editorState', ({ editorState, outcome }) => {
editor.isActive[CONTENT_TYPE].mockReturnValueOnce(editorState.isActive);
editor.focused = editorState.focused;
tiptapEditor.isActive.mockReturnValueOnce(editorState.isActive);
tiptapEditor.isFocused = editorState.isFocused;
buildWrapper();
expect(findButton().classes().includes('active')).toBe(outcome);
expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE);
});
describe('when button is clicked', () => {
it('executes the content type command when executeCommand = true', async () => {
buildWrapper({ executeCommand: true });
buildWrapper({ editorCommand: 'toggleFoo' });
await findButton().trigger('click');
expect(editor.commands[CONTENT_TYPE]).toHaveBeenCalled();
expect(toggleFooSpy).toHaveBeenCalled();
expect(wrapper.emitted().click).toHaveLength(1);
});
it('does not executes the content type command when executeCommand = false', async () => {
buildWrapper({ executeCommand: false });
buildWrapper();
await findButton().trigger('click');
expect(editor.commands[CONTENT_TYPE]).not.toHaveBeenCalled();
expect(toggleFooSpy).not.toHaveBeenCalled();
expect(wrapper.emitted().click).toHaveLength(1);
});
});
......
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
describe('content_editor/components/top_toolbar', () => {
let wrapper;
let editor;
let contentEditor;
const buildEditor = () => {
editor = {};
contentEditor = createContentEditor({ renderMarkdown: () => true });
};
const buildWrapper = () => {
wrapper = extendedWrapper(
shallowMount(TopToolbar, {
propsData: {
editor,
contentEditor,
},
}),
);
......@@ -29,18 +30,18 @@ describe('content_editor/components/top_toolbar', () => {
});
it.each`
testId | button
${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold' }}
${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic' }}
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code' }}
${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote' }}
${'bullet-list'} | ${{ contentType: 'bullet_list', iconName: 'list-bulleted', label: 'Add a bullet list' }}
${'ordered-list'} | ${{ contentType: 'ordered_list', iconName: 'list-numbered', label: 'Add a numbered list' }}
testId | buttonProps
${'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' }}
`('renders $testId button', ({ testId, buttonProps }) => {
buildWrapper();
expect(wrapper.findByTestId(testId).props()).toMatchObject({
expect(wrapper.findByTestId(testId).props()).toEqual({
...buttonProps,
editor,
tiptapEditor: contentEditor.tiptapEditor,
});
});
});
import { createEditor } from '~/content_editor';
import { createContentEditor } from '~/content_editor';
import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples';
describe('markdown processing', () => {
// Ensure we generate same markdown that was provided to Markdown API.
it.each(loadMarkdownApiExamples())('correctly handles %s', async (testName, markdown) => {
const { html } = loadMarkdownApiResult(testName);
const editor = await createEditor({ content: markdown, renderMarkdown: () => html });
const contentEditor = createContentEditor({ renderMarkdown: () => html });
await contentEditor.setSerializedContent(markdown);
expect(editor.getSerializedContent()).toBe(markdown);
expect(contentEditor.getSerializedContent()).toBe(markdown);
});
});
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
describe('content_editor/services/create_editor', () => {
let renderMarkdown;
let editor;
beforeEach(() => {
renderMarkdown = jest.fn();
editor = createContentEditor({ renderMarkdown });
});
it('sets gl-outline-0! class selector to the tiptapEditor instance', () => {
expect(editor.tiptapEditor.options.editorProps).toMatchObject({
attributes: {
class: 'gl-outline-0!',
},
});
});
it('provides the renderMarkdown function to the markdown serializer', async () => {
const serializedContent = '**bold text**';
renderMarkdown.mockReturnValueOnce('<p><b>bold text</b></p>');
await editor.setSerializedContent(serializedContent);
expect(renderMarkdown).toHaveBeenCalledWith(serializedContent);
});
it('throws an error when a renderMarkdown fn is not provided', () => {
expect(() => createContentEditor()).toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
});
});
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants';
import createEditor from '~/content_editor/services/create_editor';
import createMarkdownSerializer from '~/content_editor/services/markdown_serializer';
jest.mock('~/content_editor/services/markdown_serializer');
describe('content_editor/services/create_editor', () => {
const renderMarkdown = () => true;
const buildMockSerializer = () => ({
serialize: jest.fn(),
deserialize: jest.fn(),
});
it('sets gl-outline-0! class selector to editor attributes', async () => {
const editor = await createEditor({ renderMarkdown });
expect(editor.options.editorProps).toMatchObject({
attributes: {
class: 'gl-outline-0!',
},
});
});
describe('creating an editor', () => {
it('uses markdown serializer when a renderMarkdown function is provided', async () => {
const mockSerializer = buildMockSerializer();
createMarkdownSerializer.mockReturnValueOnce(mockSerializer);
await createEditor({ renderMarkdown });
expect(createMarkdownSerializer).toHaveBeenCalledWith({ render: renderMarkdown });
});
it('uses custom serializer when it is provided', async () => {
const mockSerializer = buildMockSerializer();
const serializedContent = '**bold**';
mockSerializer.serialize.mockReturnValueOnce(serializedContent);
const editor = await createEditor({ serializer: mockSerializer });
expect(editor.getSerializedContent()).toBe(serializedContent);
});
it('throws an error when neither a serializer or renderMarkdown fn are provided', async () => {
await expect(createEditor()).rejects.toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
});
});
});
......@@ -337,7 +337,10 @@ describe('WikiForm', () => {
// wait for content editor to load
await waitForPromises();
wrapper.vm.editor.setContent('<p>hello __world__ from content editor</p>', true);
wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
'<p>hello __world__ from content editor</p>',
true,
);
await waitForPromises();
......@@ -378,7 +381,10 @@ describe('WikiForm', () => {
// wait for content editor to load
await waitForPromises();
wrapper.vm.editor.setContent('<p>hello __world__ from content editor</p>', true);
wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
'<p>hello __world__ from content editor</p>',
true,
);
wrapper.findComponent(GlAlert).findComponent(GlButton).trigger('click');
await wrapper.vm.$nextTick();
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment