Commit a0a2d536 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '325201-content-editor-wiki' into 'master'

Add Content Editor as an option when creating or editing a Wiki Page [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!57370
parents 7c36265f cb69a12b
...@@ -17,10 +17,8 @@ export default { ...@@ -17,10 +17,8 @@ export default {
}; };
</script> </script>
<template> <template>
<div <div class="md md-area" :class="{ 'is-focused': editor.focused }">
class="gl-display-flex gl-flex-direction-column gl-p-3 gl-border-solid gl-border-1 gl-border-gray-200 gl-rounded-base" <top-toolbar class="gl-mb-4" :editor="editor" />
> <editor-content :editor="editor" />
<top-toolbar class="gl-mb-3" :editor="editor" />
<editor-content class="md" :editor="editor" />
</div> </div>
</template> </template>
...@@ -18,7 +18,12 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants'; ...@@ -18,7 +18,12 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import createMarkdownSerializer from './markdown_serializer'; import createMarkdownSerializer from './markdown_serializer';
const createEditor = async ({ content, renderMarkdown, serializer: customSerializer } = {}) => { const createEditor = async ({
content,
renderMarkdown,
serializer: customSerializer,
...options
} = {}) => {
if (!customSerializer && !isFunction(renderMarkdown)) { if (!customSerializer && !isFunction(renderMarkdown)) {
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR); throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
} }
...@@ -41,14 +46,10 @@ const createEditor = async ({ content, renderMarkdown, serializer: customSeriali ...@@ -41,14 +46,10 @@ const createEditor = async ({ content, renderMarkdown, serializer: customSeriali
], ],
editorProps: { editorProps: {
attributes: { attributes: {
/* class: 'gl-outline-0!',
* Adds some padding to the contenteditable element where the user types.
* Otherwise, the text cursor is not visible when its position is at the
* beginning of a line.
*/
class: 'gl-py-4 gl-px-5',
}, },
}, },
...options,
}); });
const serializer = customSerializer || createMarkdownSerializer({ render: renderMarkdown }); const serializer = customSerializer || createMarkdownSerializer({ render: renderMarkdown });
......
<script> <script>
import { GlForm, GlIcon, GlLink, GlButton, GlSprintf } from '@gitlab/ui'; import { GlForm, GlIcon, GlLink, GlButton, GlSprintf, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility'; import { setUrlFragment } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const MARKDOWN_LINK_TEXT = { const MARKDOWN_LINK_TEXT = {
markdown: '[Link Title](page-slug)', markdown: '[Link Title](page-slug)',
...@@ -14,20 +16,31 @@ const MARKDOWN_LINK_TEXT = { ...@@ -14,20 +16,31 @@ const MARKDOWN_LINK_TEXT = {
export default { export default {
components: { components: {
GlAlert,
GlForm, GlForm,
GlSprintf, GlSprintf,
GlIcon, GlIcon,
GlLink, GlLink,
GlButton, GlButton,
MarkdownField, MarkdownField,
GlLoadingIcon,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
),
}, },
mixins: [glFeatureFlagMixin()],
inject: ['formatOptions', 'pageInfo'], inject: ['formatOptions', 'pageInfo'],
data() { data() {
return { return {
title: this.pageInfo.title?.trim() || '', title: this.pageInfo.title?.trim() || '',
format: this.pageInfo.format || 'markdown', format: this.pageInfo.format || 'markdown',
content: this.pageInfo.content?.trim() || '', content: this.pageInfo.content?.trim() || '',
isContentEditorLoading: true,
useContentEditor: false,
commitMessage: '', commitMessage: '',
editor: null,
isDirty: false,
}; };
}, },
computed: { computed: {
...@@ -52,7 +65,7 @@ export default { ...@@ -52,7 +65,7 @@ export default {
return MARKDOWN_LINK_TEXT[this.format]; return MARKDOWN_LINK_TEXT[this.format];
}, },
submitButtonText() { submitButtonText() {
if (this.pageInfo.persisted) return __('Save changes'); if (this.pageInfo.persisted) return s__('WikiPage|Save changes');
return s__('WikiPage|Create page'); return s__('WikiPage|Create page');
}, },
cancelFormPath() { cancelFormPath() {
...@@ -62,20 +75,50 @@ export default { ...@@ -62,20 +75,50 @@ export default {
wikiSpecificMarkdownHelpPath() { wikiSpecificMarkdownHelpPath() {
return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown'); return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown');
}, },
isMarkdownFormat() {
return this.format === 'markdown';
},
showContentEditorButton() {
return this.isMarkdownFormat && !this.useContentEditor && this.glFeatures.wikiContentEditor;
},
isContentEditorActive() {
return this.isMarkdownFormat && this.useContentEditor;
},
}, },
mounted() { mounted() {
this.updateCommitMessage(); this.updateCommitMessage();
window.addEventListener('beforeunload', this.onPageUnload);
},
destroyed() {
window.removeEventListener('beforeunload', this.onPageUnload);
}, },
methods: { methods: {
getContentHTML(content) {
return axios
.post(this.pageInfo.markdownPreviewPath, { text: content })
.then(({ data }) => data.body);
},
handleFormSubmit() { handleFormSubmit() {
window.removeEventListener('beforeunload', this.onBeforeUnload); if (this.useContentEditor) {
this.content = this.editor.getSerializedContent();
}
this.isDirty = false;
}, },
handleContentChange() { handleContentChange() {
window.addEventListener('beforeunload', this.onBeforeUnload); this.isDirty = true;
}, },
onBeforeUnload() { onPageUnload(event) {
if (!this.isDirty) return undefined;
event.preventDefault();
// eslint-disable-next-line no-param-reassign
event.returnValue = '';
return ''; return '';
}, },
...@@ -88,6 +131,28 @@ export default { ...@@ -88,6 +131,28 @@ export default {
const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: newTitle }, false); const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: newTitle }, false);
this.commitMessage = newCommitMessage; this.commitMessage = newCommitMessage;
}, },
async initContentEditor() {
this.isContentEditorLoading = true;
this.useContentEditor = true;
const createEditor = await import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_editor'
);
this.editor =
this.editor ||
(await createEditor.default({
renderMarkdown: (markdown) => this.getContentHTML(markdown),
onUpdate: () => this.handleContentChange(),
}));
await this.editor.setSerializedContent(this.content);
this.isContentEditorLoading = false;
},
switchToOldEditor() {
this.useContentEditor = false;
},
}, },
}; };
</script> </script>
...@@ -99,6 +164,30 @@ export default { ...@@ -99,6 +164,30 @@ export default {
class="wiki-form common-note-form gl-mt-3 js-quick-submit" class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit" @submit="handleFormSubmit"
> >
<gl-alert
v-if="isContentEditorActive"
class="gl-mb-6"
:dismissible="false"
variant="danger"
:primary-button-text="s__('WikiPage|Switch to old editor')"
@primaryAction="switchToOldEditor()"
>
<p>
{{
s__(
"WikiPage|You are editing this page with Content Editor. This editor is in beta and may not display the page's contents properly.",
)
}}
</p>
<p>
{{
s__(
"WikiPage|Switching to the old editor will discard any changes you've made in the new editor.",
)
}}
</p>
</gl-alert>
<input :value="csrfToken" type="hidden" name="authenticity_token" /> <input :value="csrfToken" type="hidden" name="authenticity_token" />
<input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" /> <input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
<input <input
...@@ -135,8 +224,8 @@ export default { ...@@ -135,8 +224,8 @@ export default {
'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.', 'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
) )
}} }}
<gl-link :href="helpPath" target="_blank" data-testid="wiki-title-help-link" <gl-link :href="helpPath" target="_blank"
><gl-icon name="question-o" /> {{ __('More Information.') }}</gl-link ><gl-icon name="question-o" /> {{ s__('WikiPage|More Information.') }}</gl-link
> >
</span> </span>
</div> </div>
...@@ -147,12 +236,26 @@ export default { ...@@ -147,12 +236,26 @@ export default {
s__('WikiPage|Format') s__('WikiPage|Format')
}}</label> }}</label>
</div> </div>
<div class="col-sm-10"> <div class="col-sm-10 gl-display-flex gl-flex-wrap">
<select id="wiki_format" v-model="format" class="form-control" name="wiki[format]"> <select
id="wiki_format"
v-model="format"
class="form-control"
name="wiki[format]"
:disabled="isContentEditorActive"
>
<option v-for="(key, label) of formatOptions" :key="key" :value="key"> <option v-for="(key, label) of formatOptions" :key="key" :value="key">
{{ label }} {{ label }}
</option> </option>
</select> </select>
<gl-button
v-if="showContentEditorButton"
category="secondary"
variant="confirm"
class="gl-mt-4"
@click="initContentEditor"
>{{ s__('WikiPage|Use new editor') }}</gl-button
>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
...@@ -163,6 +266,7 @@ export default { ...@@ -163,6 +266,7 @@ export default {
</div> </div>
<div class="col-sm-10"> <div class="col-sm-10">
<markdown-field <markdown-field
v-if="!isContentEditorActive"
:markdown-preview-path="pageInfo.markdownPreviewPath" :markdown-preview-path="pageInfo.markdownPreviewPath"
:can-attach-file="true" :can-attach-file="true"
:enable-autocomplete="true" :enable-autocomplete="true"
...@@ -189,10 +293,17 @@ export default { ...@@ -189,10 +293,17 @@ export default {
</textarea> </textarea>
</template> </template>
</markdown-field> </markdown-field>
<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" />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
</div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="error-alert"></div> <div class="error-alert"></div>
<div class="form-text gl-text-gray-600"> <div v-if="!isContentEditorActive" class="form-text gl-text-gray-600">
<gl-sprintf <gl-sprintf
:message=" :message="
s__( s__(
...@@ -245,9 +356,7 @@ export default { ...@@ -245,9 +356,7 @@ export default {
:disabled="!content || !title" :disabled="!content || !title"
>{{ submitButtonText }}</gl-button >{{ submitButtonText }}</gl-button
> >
<gl-button :href="cancelFormPath" class="float-right" data-testid="wiki-cancel-button">{{ <gl-button :href="cancelFormPath" class="float-right">{{ s__('WikiPage|Cancel') }}</gl-button>
__('Cancel')
}}</gl-button>
</div> </div>
</gl-form> </gl-form>
</template> </template>
...@@ -6,4 +6,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -6,4 +6,8 @@ class Projects::WikisController < Projects::ApplicationController
alias_method :container, :project alias_method :container, :project
feature_category :wiki feature_category :wiki
before_action do
push_frontend_feature_flag(:wiki_content_editor, project, default_enabled: :yaml)
end
end end
---
name: wiki_content_editor
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57370
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255919
group: group::editor
type: development
default_enabled: false
...@@ -21038,9 +21038,6 @@ msgstr "" ...@@ -21038,9 +21038,6 @@ msgstr ""
msgid "More Information" msgid "More Information"
msgstr "" msgstr ""
msgid "More Information."
msgstr ""
msgid "More Slack commands" msgid "More Slack commands"
msgstr "" msgstr ""
...@@ -36083,6 +36080,9 @@ msgstr "" ...@@ -36083,6 +36080,9 @@ msgstr ""
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs." msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs."
msgstr "" msgstr ""
msgid "WikiPage|Cancel"
msgstr ""
msgid "WikiPage|Commit message" msgid "WikiPage|Commit message"
msgstr "" msgstr ""
...@@ -36098,9 +36098,21 @@ msgstr "" ...@@ -36098,9 +36098,21 @@ msgstr ""
msgid "WikiPage|Format" msgid "WikiPage|Format"
msgstr "" msgstr ""
msgid "WikiPage|More Information."
msgstr ""
msgid "WikiPage|Page title" msgid "WikiPage|Page title"
msgstr "" msgstr ""
msgid "WikiPage|Save changes"
msgstr ""
msgid "WikiPage|Switch to old editor"
msgstr ""
msgid "WikiPage|Switching to the old editor will discard any changes you've made in the new editor."
msgstr ""
msgid "WikiPage|Tip: You can move this page by adding the path to the beginning of the title." msgid "WikiPage|Tip: You can move this page by adding the path to the beginning of the title."
msgstr "" msgstr ""
...@@ -36116,9 +36128,15 @@ msgstr "" ...@@ -36116,9 +36128,15 @@ msgstr ""
msgid "WikiPage|Update %{pageTitle}" msgid "WikiPage|Update %{pageTitle}"
msgstr "" msgstr ""
msgid "WikiPage|Use new editor"
msgstr ""
msgid "WikiPage|Write your content or drag files here…" msgid "WikiPage|Write your content or drag files here…"
msgstr "" msgstr ""
msgid "WikiPage|You are editing this page with Content Editor. This editor is in beta and may not display the page's contents properly."
msgstr ""
msgid "Wikis" msgid "Wikis"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { EditorContent } from 'tiptap'; import { EditorContent } from 'tiptap';
import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue'; import ContentEditor from '~/content_editor/components/content_editor.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import createEditor from '~/content_editor/services/create_editor'; import createEditor from '~/content_editor/services/create_editor';
import createMarkdownSerializer from '~/content_editor/services/markdown_serializer';
describe('ContentEditor', () => { describe('ContentEditor', () => {
let wrapper; let wrapper;
let editor; let editor;
const buildWrapper = async () => { const createWrapper = async (_editor) => {
editor = await createEditor({ serializer: createMarkdownSerializer({ toHTML: () => '' }) }); wrapper = mount(ContentEditor, {
wrapper = shallowMount(ContentEditor, {
propsData: { propsData: {
editor, editor: _editor,
}, },
}); });
}; };
beforeEach(async () => {
editor = await createEditor({ renderMarkdown: () => 'sample text' });
createWrapper(editor);
await waitForPromises();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders editor content component and attaches editor instance', async () => { it('renders editor content component and attaches editor instance', async () => {
await buildWrapper();
expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor); expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor);
}); });
it('renders top toolbar component and attaches editor instance', async () => { it('renders top toolbar component and attaches editor instance', async () => {
await buildWrapper();
expect(wrapper.findComponent(TopToolbar).props().editor).toBe(editor); expect(wrapper.findComponent(TopToolbar).props().editor).toBe(editor);
}); });
}); });
...@@ -11,12 +11,12 @@ describe('content_editor/services/create_editor', () => { ...@@ -11,12 +11,12 @@ describe('content_editor/services/create_editor', () => {
deserialize: jest.fn(), deserialize: jest.fn(),
}); });
it('sets gl-py-4 gl-px-5 class selectors to editor attributes', async () => { it('sets gl-outline-0! class selector to editor attributes', async () => {
const editor = await createEditor({ renderMarkdown }); const editor = await createEditor({ renderMarkdown });
expect(editor.options.editorProps).toMatchObject({ expect(editor.options.editorProps).toMatchObject({
attributes: { attributes: {
class: 'gl-py-4 gl-px-5', class: 'gl-outline-0!',
}, },
}); });
}); });
......
import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue'; import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('WikiForm', () => { describe('WikiForm', () => {
let wrapper; let wrapper;
...@@ -11,10 +17,26 @@ describe('WikiForm', () => { ...@@ -11,10 +17,26 @@ describe('WikiForm', () => {
const findContent = () => wrapper.find('#wiki_content'); const findContent = () => wrapper.find('#wiki_content');
const findMessage = () => wrapper.find('#wiki_message'); const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button'); const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button'); const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
const findTitleHelpLink = () => wrapper.findByTestId('wiki-title-help-link'); const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use new editor' });
const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'More Information.' });
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link'); const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
const setFormat = (value) => {
const format = findFormat();
format.find(`option[value=${value}]`).setSelected();
format.element.dispatchEvent(new Event('change'));
};
const triggerFormSubmit = () => findForm().element.dispatchEvent(new Event('submit'));
const dispatchBeforeUnload = () => {
const e = new Event('beforeunload');
jest.spyOn(e, 'preventDefault');
window.dispatchEvent(e);
return e;
};
const pageInfoNew = { const pageInfoNew = {
persisted: false, persisted: false,
uploadsPath: '/project/path/-/wikis/attachments', uploadsPath: '/project/path/-/wikis/attachments',
...@@ -35,7 +57,10 @@ describe('WikiForm', () => { ...@@ -35,7 +57,10 @@ describe('WikiForm', () => {
path: '/project/path/-/wikis/home', path: '/project/path/-/wikis/home',
}; };
function createWrapper(persisted = false, pageInfo = {}) { function createWrapper(
persisted = false,
{ pageInfo, glFeatures } = { glFeatures: { wikiContentEditor: false } },
) {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount( mount(
WikiForm, WikiForm,
...@@ -51,13 +76,12 @@ describe('WikiForm', () => { ...@@ -51,13 +76,12 @@ describe('WikiForm', () => {
...(persisted ? pageInfoPersisted : pageInfoNew), ...(persisted ? pageInfoPersisted : pageInfoNew),
...pageInfo, ...pageInfo,
}, },
glFeatures,
}, },
}, },
{ attachToDocument: true }, { attachToDocument: true },
), ),
); );
jest.spyOn(wrapper.vm, 'onBeforeUnload');
} }
afterEach(() => { afterEach(() => {
...@@ -101,7 +125,7 @@ describe('WikiForm', () => { ...@@ -101,7 +125,7 @@ describe('WikiForm', () => {
`('updates the link help message when format=$value is selected', async ({ value, text }) => { `('updates the link help message when format=$value is selected', async ({ value, text }) => {
createWrapper(); createWrapper();
findFormat().find(`option[value=${value}]`).setSelected(); setFormat(value);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -113,9 +137,9 @@ describe('WikiForm', () => { ...@@ -113,9 +137,9 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
window.dispatchEvent(new Event('beforeunload')); const e = dispatchBeforeUnload();
expect(typeof e.returnValue).not.toBe('string');
expect(wrapper.vm.onBeforeUnload).not.toHaveBeenCalled(); expect(e.preventDefault).not.toHaveBeenCalled();
}); });
it.each` it.each`
...@@ -156,19 +180,18 @@ describe('WikiForm', () => { ...@@ -156,19 +180,18 @@ describe('WikiForm', () => {
}); });
it('sets before unload warning', () => { it('sets before unload warning', () => {
window.dispatchEvent(new Event('beforeunload')); const e = dispatchBeforeUnload();
expect(wrapper.vm.onBeforeUnload).toHaveBeenCalled(); expect(e.preventDefault).toHaveBeenCalledTimes(1);
}); });
it('when form submitted, unsets before unload warning', async () => { it('when form submitted, unsets before unload warning', async () => {
findForm().element.dispatchEvent(new Event('submit')); triggerFormSubmit();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
window.dispatchEvent(new Event('beforeunload')); const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
expect(wrapper.vm.onBeforeUnload).not.toHaveBeenCalled();
}); });
}); });
...@@ -219,4 +242,161 @@ describe('WikiForm', () => { ...@@ -219,4 +242,161 @@ describe('WikiForm', () => {
}, },
); );
}); });
describe('when feature flag wikiContentEditor is enabled', () => {
beforeEach(() => {
createWrapper(true, { glFeatures: { wikiContentEditor: true } });
});
it.each`
format | buttonExists
${'markdown'} | ${true}
${'rdoc'} | ${false}
`(
'switch to new editor button exists: $buttonExists if format is $format',
async ({ format, buttonExists }) => {
setFormat(format);
await wrapper.vm.$nextTick();
expect(findUseNewEditorButton().exists()).toBe(buttonExists);
},
);
const assertOldEditorIsVisible = () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
};
it('shows old editor by default', assertOldEditorIsVisible);
describe('switch format to rdoc', () => {
beforeEach(async () => {
setFormat('rdoc');
await wrapper.vm.$nextTick();
});
it('continues to show the old editor', assertOldEditorIsVisible);
describe('switch format back to markdown', () => {
beforeEach(async () => {
setFormat('rdoc');
await wrapper.vm.$nextTick();
});
it(
'still shows the old editor and does not automatically switch to the content editor ',
assertOldEditorIsVisible,
);
});
});
describe('clicking "use new editor"', () => {
let mock;
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
findUseNewEditorButton().trigger('click');
await wrapper.vm.$nextTick();
});
afterEach(() => {
mock.restore();
});
it('shows a loading indicator for the rich text editor', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('shows a warning alert that the rich text editor is in beta', () => {
expect(wrapper.findComponent(GlAlert).text()).toContain(
"You are editing this page with Content Editor. This editor is in beta and may not display the page's contents properly.",
);
});
it('shows the rich text editor when loading finishes', async () => {
// wait for content editor to load
await waitForPromises();
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.findComponent(ContentEditor).exists()).toBe(true);
});
it('disables the format dropdown', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
describe('when wiki content is updated', () => {
beforeEach(async () => {
// wait for content editor to load
await waitForPromises();
wrapper.vm.editor.setContent('<p>hello __world__ from content editor</p>', true);
await waitForPromises();
return wrapper.vm.$nextTick();
});
it('sets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
it('unsets before unload warning on form submit', async () => {
triggerFormSubmit();
await wrapper.vm.$nextTick();
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
});
it('updates content from content editor on form submit', async () => {
// old value
expect(findContent().element.value).toBe('My page content');
// wait for content editor to load
await waitForPromises();
triggerFormSubmit();
await wrapper.vm.$nextTick();
expect(findContent().element.value).toBe('hello **world**');
});
describe('clicking "switch to old editor"', () => {
beforeEach(async () => {
// wait for content editor to load
await waitForPromises();
wrapper.vm.editor.setContent('<p>hello __world__ from content editor</p>', true);
wrapper.findComponent(GlAlert).findComponent(GlButton).trigger('click');
await wrapper.vm.$nextTick();
});
it('switches to old editor', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
it('does not show a warning alert about content editor', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
it('the old editor retains its old value and does not use the content from the content editor', () => {
expect(findContent().element.value).toBe('My page content');
});
});
});
});
}); });
...@@ -24,8 +24,8 @@ RSpec.shared_examples 'User creates wiki page' do ...@@ -24,8 +24,8 @@ RSpec.shared_examples 'User creates wiki page' do
page.within(".wiki-form") do page.within(".wiki-form") do
fill_in(:wiki_content, with: "") fill_in(:wiki_content, with: "")
page.execute_script("window.onbeforeunload = null")
page.execute_script("document.querySelector('.wiki-form').submit()") page.execute_script("document.querySelector('.wiki-form').submit()")
page.accept_alert # manually force form submit
end end
expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank")
......
...@@ -93,8 +93,8 @@ RSpec.shared_examples 'User updates wiki page' do ...@@ -93,8 +93,8 @@ RSpec.shared_examples 'User updates wiki page' do
it 'shows a validation error message if the form is force submitted', :js do it 'shows a validation error message if the form is force submitted', :js do
fill_in(:wiki_content, with: '') fill_in(:wiki_content, with: '')
page.execute_script("window.onbeforeunload = null")
page.execute_script("document.querySelector('.wiki-form').submit()") page.execute_script("document.querySelector('.wiki-form').submit()")
page.accept_alert # manually force form submit
expect(page).to have_selector('.wiki-form') expect(page).to have_selector('.wiki-form')
expect(page).to have_content('Edit Page') expect(page).to have_content('Edit Page')
......
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