diff options
Diffstat (limited to 'spec/frontend')
6 files changed, 244 insertions, 200 deletions
diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js index 12484cb13c6..ee9ead8f8a7 100644 --- a/spec/frontend/content_editor/components/content_editor_alert_spec.js +++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js @@ -51,6 +51,16 @@ describe('content_editor/components/content_editor_alert', () => { }, ); + it('does not show primary action by default', async () => { + const message = 'error message'; + + createWrapper(); + eventHub.$emit(ALERT_EVENT, { message }); + await nextTick(); + + expect(findErrorAlert().attributes().primaryButtonText).toBeUndefined(); + }); + it('allows dismissing the error', async () => { const message = 'error message'; @@ -62,4 +72,19 @@ describe('content_editor/components/content_editor_alert', () => { expect(findErrorAlert().exists()).toBe(false); }); + + it('allows dismissing the error with a primary action button', async () => { + const message = 'error message'; + const actionLabel = 'Retry'; + const action = jest.fn(); + + createWrapper(); + eventHub.$emit(ALERT_EVENT, { message, action, actionLabel }); + await nextTick(); + findErrorAlert().vm.$emit('primaryAction'); + await nextTick(); + + expect(action).toHaveBeenCalled(); + expect(findErrorAlert().exists()).toBe(false); + }); }); diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 4c87ccca85b..ae52cb05eaf 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -1,4 +1,6 @@ -import { EditorContent } from '@tiptap/vue-2'; +import { GlAlert } from '@gitlab/ui'; +import { EditorContent, Editor } from '@tiptap/vue-2'; +import { nextTick } from 'vue'; 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'; @@ -10,112 +12,205 @@ import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue'; import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; -import { emitEditorEvent } from '../test_utils'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/emoji'); describe('ContentEditor', () => { let wrapper; - let contentEditor; let renderMarkdown; const uploadsPath = '/uploads'; const findEditorElement = () => wrapper.findByTestId('content-editor'); const findEditorContent = () => wrapper.findComponent(EditorContent); const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); - const createWrapper = (propsData = {}) => { - renderMarkdown = jest.fn(); - + const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator); + const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert); + const createWrapper = ({ markdown } = {}) => { wrapper = shallowMountExtended(ContentEditor, { propsData: { renderMarkdown, uploadsPath, - ...propsData, + markdown, }, stubs: { EditorStateObserver, ContentEditorProvider, - }, - listeners: { - initialized(editor) { - contentEditor = editor; - }, + ContentEditorAlert, }, }); }; + beforeEach(() => { + renderMarkdown = jest.fn(); + }); + afterEach(() => { wrapper.destroy(); }); - it('triggers initialized event and provides contentEditor instance as event data', () => { + it('triggers initialized event', () => { createWrapper(); - expect(contentEditor).not.toBe(false); + expect(wrapper.emitted('initialized')).toHaveLength(1); }); - it('renders EditorContent component and provides tiptapEditor instance', () => { - createWrapper(); + it('renders EditorContent component and provides tiptapEditor instance', async () => { + const markdown = 'hello world'; + + createWrapper({ markdown }); + + renderMarkdown.mockResolvedValueOnce(markdown); + + await nextTick(); const editorContent = findEditorContent(); - expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor); + expect(editorContent.props().editor).toBeInstanceOf(Editor); expect(editorContent.classes()).toContain('md'); }); - it('renders ContentEditorProvider component', () => { - createWrapper(); + it('renders ContentEditorProvider component', async () => { + await createWrapper(); expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true); }); - it('renders top toolbar component', () => { - createWrapper(); + it('renders top toolbar component', async () => { + await createWrapper(); expect(wrapper.findComponent(TopToolbar).exists()).toBe(true); }); - it('adds is-focused class when focus event is emitted', async () => { - createWrapper(); + describe('when setting initial content', () => { + it('displays loading indicator', async () => { + createWrapper(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' }); + await nextTick(); - expect(findEditorElement().classes()).toContain('is-focused'); - }); + expect(findLoadingIndicator().exists()).toBe(true); + }); - it('removes is-focused class when blur event is emitted', async () => { - createWrapper(); + it('emits loading event', async () => { + createWrapper(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' }); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' }); + await nextTick(); - expect(findEditorElement().classes()).not.toContain('is-focused'); - }); + expect(wrapper.emitted('loading')).toHaveLength(1); + }); - it('emits change event when document is updated', async () => { - createWrapper(); + describe('succeeds', () => { + beforeEach(async () => { + renderMarkdown.mockResolvedValueOnce('hello world'); + + createWrapper({ markddown: 'hello world' }); + await nextTick(); + }); + + it('hides loading indicator', async () => { + await nextTick(); + expect(findLoadingIndicator().exists()).toBe(false); + }); + + it('emits loadingSuccess event', () => { + expect(wrapper.emitted('loadingSuccess')).toHaveLength(1); + }); + }); + + describe('fails', () => { + beforeEach(async () => { + renderMarkdown.mockRejectedValueOnce(new Error()); + + createWrapper({ markddown: 'hello world' }); + await nextTick(); + }); + + it('sets the content editor as read only when loading content fails', async () => { + await nextTick(); + + expect(findEditorContent().props().editor.isEditable).toBe(false); + }); + + it('hides loading indicator', async () => { + await nextTick(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' }); + expect(findLoadingIndicator().exists()).toBe(false); + }); - expect(wrapper.emitted('change')).toEqual([ - [ - { - empty: contentEditor.empty, - }, - ], - ]); + it('emits loadingError event', () => { + expect(wrapper.emitted('loadingError')).toHaveLength(1); + }); + + it('displays error alert indicating that the content editor failed to load', () => { + expect(findContentEditorAlert().text()).toContain( + 'An error occurred while trying to render the content editor. Please try again.', + ); + }); + + describe('when clicking the retry button in the loading error alert and loading succeeds', () => { + beforeEach(async () => { + renderMarkdown.mockResolvedValueOnce('hello markdown'); + await wrapper.findComponent(GlAlert).vm.$emit('primaryAction'); + }); + + it('hides the loading error alert', () => { + expect(findContentEditorAlert().text()).toBe(''); + }); + + it('sets the content editor as writable', async () => { + await nextTick(); + + expect(findEditorContent().props().editor.isEditable).toBe(true); + }); + }); + }); }); - it('renders content_editor_alert component', () => { - createWrapper(); + describe('when focused event is emitted', () => { + beforeEach(async () => { + createWrapper(); + + findEditorStateObserver().vm.$emit('focus'); + + await nextTick(); + }); + + it('adds is-focused class when focus event is emitted', () => { + expect(findEditorElement().classes()).toContain('is-focused'); + }); + + it('removes is-focused class when blur event is emitted', async () => { + findEditorStateObserver().vm.$emit('blur'); + + await nextTick(); - expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true); + expect(findEditorElement().classes()).not.toContain('is-focused'); + }); }); - it('renders loading indicator component', () => { - createWrapper(); + describe('when editorStateObserver emits docUpdate event', () => { + it('emits change event with the latest markdown', async () => { + const markdown = 'Loaded content'; + + renderMarkdown.mockResolvedValueOnce(markdown); + + createWrapper({ markdown: 'initial content' }); - expect(wrapper.findComponent(LoadingIndicator).exists()).toBe(true); + await nextTick(); + await waitForPromises(); + + findEditorStateObserver().vm.$emit('docUpdate'); + + expect(wrapper.emitted('change')).toEqual([ + [ + { + markdown, + changed: false, + empty: false, + }, + ], + ]); + }); }); it.each` @@ -129,17 +224,4 @@ describe('ContentEditor', () => { expect(wrapper.findComponent(component).exists()).toBe(true); }); - - it.each` - event - ${'loading'} - ${'loadingSuccess'} - ${'loadingError'} - `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => { - createWrapper(); - - findEditorStateObserver().vm.$emit(event); - - expect(wrapper.emitted(event)).toHaveLength(1); - }); }); diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js index 51a594a606b..e8c2d8c8793 100644 --- a/spec/frontend/content_editor/components/editor_state_observer_spec.js +++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js @@ -4,12 +4,7 @@ import EditorStateObserver, { tiptapToComponentMap, } from '~/content_editor/components/editor_state_observer.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, - ALERT_EVENT, -} from '~/content_editor/constants'; +import { ALERT_EVENT } from '~/content_editor/constants'; import { createTestEditor } from '../test_utils'; describe('content_editor/components/editor_state_observer', () => { @@ -18,9 +13,6 @@ describe('content_editor/components/editor_state_observer', () => { let onDocUpdateListener; let onSelectionUpdateListener; let onTransactionListener; - let onLoadingContentListener; - let onLoadingSuccessListener; - let onLoadingErrorListener; let onAlertListener; let eventHub; @@ -38,9 +30,6 @@ describe('content_editor/components/editor_state_observer', () => { selectionUpdate: onSelectionUpdateListener, transaction: onTransactionListener, [ALERT_EVENT]: onAlertListener, - [LOADING_CONTENT_EVENT]: onLoadingContentListener, - [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener, - [LOADING_ERROR_EVENT]: onLoadingErrorListener, }, }); }; @@ -50,9 +39,6 @@ describe('content_editor/components/editor_state_observer', () => { onSelectionUpdateListener = jest.fn(); onTransactionListener = jest.fn(); onAlertListener = jest.fn(); - onLoadingSuccessListener = jest.fn(); - onLoadingContentListener = jest.fn(); - onLoadingErrorListener = jest.fn(); buildEditor(); }); @@ -81,11 +67,8 @@ describe('content_editor/components/editor_state_observer', () => { }); it.each` - event | listener - ${ALERT_EVENT} | ${() => onAlertListener} - ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener} - ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener} - ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener} + event | listener + ${ALERT_EVENT} | ${() => onAlertListener} `('listens to $event event in the eventBus object', ({ event, listener }) => { const args = {}; @@ -114,9 +97,6 @@ describe('content_editor/components/editor_state_observer', () => { it.each` event ${ALERT_EVENT} - ${LOADING_CONTENT_EVENT} - ${LOADING_SUCCESS_EVENT} - ${LOADING_ERROR_EVENT} `('removes $event event hook from eventHub', ({ event }) => { jest.spyOn(eventHub, '$off'); jest.spyOn(eventHub, '$on'); diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js index e4fb09b70a4..0065103d01b 100644 --- a/spec/frontend/content_editor/components/loading_indicator_spec.js +++ b/spec/frontend/content_editor/components/loading_indicator_spec.js @@ -1,18 +1,10 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; -import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; describe('content_editor/components/loading_indicator', () => { let wrapper; - const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const createWrapper = () => { @@ -24,48 +16,12 @@ describe('content_editor/components/loading_indicator', () => { }); describe('when loading content', () => { - beforeEach(async () => { + beforeEach(() => { createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - - await nextTick(); }); it('displays loading indicator', () => { expect(findLoadingIcon().exists()).toBe(true); }); }); - - describe('when loading content succeeds', () => { - beforeEach(async () => { - createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - await nextTick(); - findEditorStateObserver().vm.$emit(LOADING_SUCCESS_EVENT); - await nextTick(); - }); - - it('hides loading indicator', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('when loading content fails', () => { - const error = 'error'; - - beforeEach(async () => { - createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - await nextTick(); - findEditorStateObserver().vm.$emit(LOADING_ERROR_EVENT, error); - await nextTick(); - }); - - it('hides loading indicator', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index a3553e612ca..6175cbdd3d4 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -1,8 +1,3 @@ -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; import { ContentEditor } from '~/content_editor/services/content_editor'; import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, createDocBuilder } from '../test_utils'; @@ -14,6 +9,7 @@ describe('content_editor/services/content_editor', () => { let eventHub; let doc; let p; + const testMarkdown = '**bold text**'; beforeEach(() => { const tiptapEditor = createTestEditor(); @@ -36,6 +32,9 @@ describe('content_editor/services/content_editor', () => { }); }); + const testDoc = () => doc(p('document')); + const testEmptyDoc = () => doc(); + describe('.dispose', () => { it('destroys the tiptapEditor', () => { expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled(); @@ -46,51 +45,77 @@ describe('content_editor/services/content_editor', () => { }); }); - describe('when setSerializedContent succeeds', () => { - let document; - const languages = ['javascript']; - const testMarkdown = '**bold text**'; + describe('empty', () => { + it('returns true when tiptapEditor is empty', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testEmptyDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); - beforeEach(() => { - document = doc(p('document')); - deserializer.deserialize.mockResolvedValueOnce({ document, languages }); + expect(contentEditor.empty).toBe(true); }); - it('emits loadingContent and loadingSuccess event in the eventHub', () => { - let loadingContentEmitted = false; + it('returns false when tiptapEditor is not empty', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); - eventHub.$on(LOADING_CONTENT_EVENT, () => { - loadingContentEmitted = true; - }); - eventHub.$on(LOADING_SUCCESS_EVENT, () => { - expect(loadingContentEmitted).toBe(true); - }); + await contentEditor.setSerializedContent(testMarkdown); - contentEditor.setSerializedContent(testMarkdown); + expect(contentEditor.empty).toBe(false); }); + }); - it('sets the deserialized document in the tiptap editor object', async () => { - await contentEditor.setSerializedContent(testMarkdown); + describe('editable', () => { + it('returns true when tiptapEditor is editable', async () => { + contentEditor.setEditable(true); - expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON()); + expect(contentEditor.editable).toBe(true); + }); + + it('returns false when tiptapEditor is readonly', async () => { + contentEditor.setEditable(false); + + expect(contentEditor.editable).toBe(false); }); }); - describe('when setSerializedContent fails', () => { - const error = 'error'; + describe('changed', () => { + it('returns true when the initial document changes', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + contentEditor.tiptapEditor.commands.insertContent(' new content'); + + expect(contentEditor.changed).toBe(true); + }); + + it('returns false when the initial document hasn’t changed', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + expect(contentEditor.changed).toBe(false); + }); + + it('returns false when an initial document is not set and the document is empty', () => { + expect(contentEditor.changed).toBe(false); + }); - beforeEach(() => { - deserializer.deserialize.mockRejectedValueOnce(error); + it('returns true when an initial document is not set and the document is not empty', () => { + contentEditor.tiptapEditor.commands.insertContent('new content'); + + expect(contentEditor.changed).toBe(true); }); + }); + + describe('when setSerializedContent succeeds', () => { + it('sets the deserialized document in the tiptap editor object', async () => { + const document = testDoc(); + + deserializer.deserialize.mockResolvedValueOnce({ document }); - it('emits loadingError event', async () => { - eventHub.$on(LOADING_ERROR_EVENT, (e) => { - expect(e).toBe('error'); - }); + await contentEditor.setSerializedContent(testMarkdown); - await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual( - error, - ); + expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON()); }); }); }); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js index 204c48f8de1..36a926990f2 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -302,19 +302,15 @@ describe('WikiForm', () => { }); it.each` - format | enabled | action + format | exists | action ${'markdown'} | ${true} | ${'displays'} ${'rdoc'} | ${false} | ${'hides'} ${'asciidoc'} | ${false} | ${'hides'} ${'org'} | ${false} | ${'hides'} - `('$action toggle editing mode button when format is $format', async ({ format, enabled }) => { + `('$action toggle editing mode button when format is $format', async ({ format, exists }) => { await setFormat(format); - expect(findToggleEditingModeButton().exists()).toBe(enabled); - }); - - it('displays toggle editing mode button', () => { - expect(findToggleEditingModeButton().exists()).toBe(true); + expect(findToggleEditingModeButton().exists()).toBe(exists); }); describe('when content editor is not active', () => { @@ -351,15 +347,8 @@ describe('WikiForm', () => { }); describe('when content editor is active', () => { - let mockContentEditor; - beforeEach(() => { createWrapper(); - mockContentEditor = { - getSerializedContent: jest.fn(), - setSerializedContent: jest.fn(), - }; - findToggleEditingModeButton().vm.$emit('input', 'richText'); }); @@ -368,14 +357,7 @@ describe('WikiForm', () => { }); describe('when clicking the toggle editing mode button', () => { - const contentEditorFakeSerializedContent = 'fake content'; - beforeEach(async () => { - mockContentEditor.getSerializedContent.mockReturnValueOnce( - contentEditorFakeSerializedContent, - ); - - findContentEditor().vm.$emit('initialized', mockContentEditor); await findToggleEditingModeButton().vm.$emit('input', 'source'); await nextTick(); }); @@ -387,10 +369,6 @@ describe('WikiForm', () => { it('displays the classic editor', () => { expect(findClassicEditor().exists()).toBe(true); }); - - it('updates the classic editor content field', () => { - expect(findContent().element.value).toBe(contentEditorFakeSerializedContent); - }); }); describe('when content editor is loading', () => { @@ -480,8 +458,14 @@ describe('WikiForm', () => { }); describe('when wiki content is updated', () => { + const updatedMarkdown = 'hello **world**'; + beforeEach(() => { - findContentEditor().vm.$emit('change', { empty: false }); + findContentEditor().vm.$emit('change', { + empty: false, + changed: true, + markdown: updatedMarkdown, + }); }); it('sets before unload warning', () => { @@ -512,16 +496,8 @@ describe('WikiForm', () => { }); }); - 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(); - - await triggerFormSubmit(); - - expect(findContent().element.value).toBe('hello **world**'); + it('sets content field to the content editor updated markdown', async () => { + expect(findContent().element.value).toBe(updatedMarkdown); }); }); }); |