diff options
Diffstat (limited to 'spec/frontend/notes/components/comment_form_spec.js')
-rw-r--r-- | spec/frontend/notes/components/comment_form_spec.js | 443 |
1 files changed, 250 insertions, 193 deletions
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 59fa7b372ed..fca1beca999 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -1,18 +1,20 @@ -import $ from 'jquery'; -import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import Autosize from 'autosize'; -import { trimText } from 'helpers/text_helper'; +import { deprecatedCreateFlash as flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import createStore from '~/notes/stores'; import CommentForm from '~/notes/components/comment_form.vue'; import * as constants from '~/notes/constants'; +import eventHub from '~/notes/event_hub'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; -import { keyboardDownEvent } from '../../issue_show/helpers'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; jest.mock('autosize'); jest.mock('~/commons/nav/user_merge_requests'); +jest.mock('~/flash'); jest.mock('~/gl_form'); describe('issue_comment_form component', () => { @@ -20,17 +22,33 @@ describe('issue_comment_form component', () => { let wrapper; let axiosMock; - const setupStore = (userData, noteableData) => { - store.dispatch('setUserData', userData); + const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]'); + + const findCommentButton = () => wrapper.find('[data-testid="comment-button"]'); + + const findTextArea = () => wrapper.find('[data-testid="comment-field"]'); + + const mountComponent = ({ + initialData = {}, + noteableType = 'Issue', + noteableData = noteableDataMock, + notesData = notesDataMock, + userData = userDataMock, + mountFunction = shallowMount, + } = {}) => { store.dispatch('setNoteableData', noteableData); - store.dispatch('setNotesData', notesDataMock); - }; + store.dispatch('setNotesData', notesData); + store.dispatch('setUserData', userData); - const mountComponent = (noteableType = 'issue') => { - wrapper = mount(CommentForm, { + wrapper = mountFunction(CommentForm, { propsData: { noteableType, }, + data() { + return { + ...initialData, + }; + }, store, }); }; @@ -46,168 +64,157 @@ describe('issue_comment_form component', () => { }); describe('user is logged in', () => { - beforeEach(() => { - setupStore(userDataMock, noteableDataMock); - - mountComponent(); - }); + describe('avatar', () => { + it('should render user avatar with link', () => { + mountComponent({ mountFunction: mount }); - it('should render user avatar with link', () => { - expect(wrapper.find('.timeline-icon .user-avatar-link').attributes('href')).toEqual( - userDataMock.path, - ); + expect(wrapper.find(UserAvatarLink).attributes('href')).toBe(userDataMock.path); + }); }); describe('handleSave', () => { it('should request to save note when note is entered', () => { - wrapper.vm.note = 'hello world'; - jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {})); + mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } }); + + jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue(); jest.spyOn(wrapper.vm, 'resizeTextarea'); jest.spyOn(wrapper.vm, 'stopPolling'); - wrapper.vm.handleSave(); + findCloseReopenButton().trigger('click'); - expect(wrapper.vm.isSubmitting).toEqual(true); - expect(wrapper.vm.note).toEqual(''); + expect(wrapper.vm.isSubmitting).toBe(true); + expect(wrapper.vm.note).toBe(''); expect(wrapper.vm.saveNote).toHaveBeenCalled(); expect(wrapper.vm.stopPolling).toHaveBeenCalled(); expect(wrapper.vm.resizeTextarea).toHaveBeenCalled(); }); it('should toggle issue state when no note', () => { + mountComponent({ mountFunction: mount }); + jest.spyOn(wrapper.vm, 'toggleIssueState'); - wrapper.vm.handleSave(); + findCloseReopenButton().trigger('click'); expect(wrapper.vm.toggleIssueState).toHaveBeenCalled(); }); - it('should disable action button while submitting', done => { + it('should disable action button while submitting', async () => { + mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } }); + const saveNotePromise = Promise.resolve(); - wrapper.vm.note = 'hello world'; + jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise); jest.spyOn(wrapper.vm, 'stopPolling'); - const actionButton = wrapper.find('.js-action-button'); - - wrapper.vm.handleSave(); - - wrapper.vm - .$nextTick() - .then(() => { - expect(actionButton.vm.disabled).toBeTruthy(); - }) - .then(saveNotePromise) - .then(wrapper.vm.$nextTick) - .then(() => { - expect(actionButton.vm.disabled).toBeFalsy(); - }) - .then(done) - .catch(done.fail); + const actionButton = findCloseReopenButton(); + + await actionButton.trigger('click'); + + expect(actionButton.props('disabled')).toBe(true); + + await saveNotePromise; + + await nextTick(); + + expect(actionButton.props('disabled')).toBe(false); }); }); describe('textarea', () => { - it('should render textarea with placeholder', () => { - expect(wrapper.find('.js-main-target-form textarea').attributes('placeholder')).toEqual( - 'Write a comment or drag your files here…', - ); - }); + describe('general', () => { + it('should render textarea with placeholder', () => { + mountComponent({ mountFunction: mount }); - it('should make textarea disabled while requesting', done => { - const $submitButton = $(wrapper.find('.js-comment-submit-button').element); - wrapper.vm.note = 'hello world'; - jest.spyOn(wrapper.vm, 'stopPolling'); - jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {})); + expect(findTextArea().attributes('placeholder')).toBe( + 'Write a comment or drag your files here…', + ); + }); - wrapper.vm.$nextTick(() => { - // Wait for wrapper.vm.note change triggered. It should enable $submitButton. - $submitButton.trigger('click'); + it('should make textarea disabled while requesting', async () => { + mountComponent({ mountFunction: mount }); - wrapper.vm.$nextTick(() => { - // Wait for wrapper.isSubmitting triggered. It should disable textarea. - expect(wrapper.find('.js-main-target-form textarea').attributes('disabled')).toBe( - 'disabled', - ); - done(); - }); + jest.spyOn(wrapper.vm, 'stopPolling'); + jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue(); + + await wrapper.setData({ note: 'hello world' }); + + await findCommentButton().trigger('click'); + + expect(findTextArea().attributes('disabled')).toBe('disabled'); }); - }); - it('should support quick actions', () => { - expect( - wrapper.find('.js-main-target-form textarea').attributes('data-supports-quick-actions'), - ).toBe('true'); - }); + it('should support quick actions', () => { + mountComponent({ mountFunction: mount }); - it('should link to markdown docs', () => { - const { markdownDocsPath } = notesDataMock; + expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true'); + }); - expect( - wrapper - .find(`a[href="${markdownDocsPath}"]`) - .text() - .trim(), - ).toEqual('Markdown'); - }); + it('should link to markdown docs', () => { + mountComponent({ mountFunction: mount }); - it('should link to quick actions docs', () => { - const { quickActionsDocsPath } = notesDataMock; + const { markdownDocsPath } = notesDataMock; - expect( - wrapper - .find(`a[href="${quickActionsDocsPath}"]`) - .text() - .trim(), - ).toEqual('quick actions'); - }); + expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown'); + }); + + it('should link to quick actions docs', () => { + mountComponent({ mountFunction: mount }); + + const { quickActionsDocsPath } = notesDataMock; + + expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions'); + }); + + it('should resize textarea after note discarded', async () => { + mountComponent({ mountFunction: mount, initialData: { note: 'foo' } }); - it('should resize textarea after note discarded', done => { - jest.spyOn(wrapper.vm, 'discard'); + jest.spyOn(wrapper.vm, 'discard'); - wrapper.vm.note = 'foo'; - wrapper.vm.discard(); + wrapper.vm.discard(); + + await nextTick(); - wrapper.vm.$nextTick(() => { expect(Autosize.update).toHaveBeenCalled(); - done(); }); }); describe('edit mode', () => { + beforeEach(() => { + mountComponent(); + }); + it('should enter edit mode when arrow up is pressed', () => { jest.spyOn(wrapper.vm, 'editCurrentUserLastNote'); - wrapper.find('.js-main-target-form textarea').value = 'Foo'; - wrapper - .find('.js-main-target-form textarea') - .element.dispatchEvent(keyboardDownEvent(38, true)); + + findTextArea().trigger('keydown.up'); expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled(); }); it('inits autosave', () => { expect(wrapper.vm.autosave).toBeDefined(); - expect(wrapper.vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`); + expect(wrapper.vm.autosave.key).toBe(`autosave/Note/Issue/${noteableDataMock.id}`); }); }); describe('event enter', () => { + beforeEach(() => { + mountComponent(); + }); + it('should save note when cmd+enter is pressed', () => { jest.spyOn(wrapper.vm, 'handleSave'); - wrapper.find('.js-main-target-form textarea').value = 'Foo'; - wrapper - .find('.js-main-target-form textarea') - .element.dispatchEvent(keyboardDownEvent(13, true)); + + findTextArea().trigger('keydown.enter', { metaKey: true }); expect(wrapper.vm.handleSave).toHaveBeenCalled(); }); it('should save note when ctrl+enter is pressed', () => { jest.spyOn(wrapper.vm, 'handleSave'); - wrapper.find('.js-main-target-form textarea').value = 'Foo'; - wrapper - .find('.js-main-target-form textarea') - .element.dispatchEvent(keyboardDownEvent(13, false, true)); + + findTextArea().trigger('keydown.enter', { ctrlKey: true }); expect(wrapper.vm.handleSave).toHaveBeenCalled(); }); @@ -216,137 +223,187 @@ describe('issue_comment_form component', () => { describe('actions', () => { it('should be possible to close the issue', () => { - expect( - wrapper - .find('.btn-comment-and-close') - .text() - .trim(), - ).toEqual('Close issue'); + mountComponent(); + + expect(findCloseReopenButton().text()).toBe('Close issue'); }); it('should render comment button as disabled', () => { - expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toEqual( - 'disabled', - ); + mountComponent(); + + expect(findCommentButton().props('disabled')).toBe(true); }); - it('should enable comment button if it has note', done => { - wrapper.vm.note = 'Foo'; - wrapper.vm.$nextTick(() => { - expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toBeFalsy(); - done(); - }); + it('should enable comment button if it has note', async () => { + mountComponent(); + + await wrapper.setData({ note: 'Foo' }); + + expect(findCommentButton().props('disabled')).toBe(false); }); - it('should update buttons texts when it has note', done => { - wrapper.vm.note = 'Foo'; - wrapper.vm.$nextTick(() => { - expect( - wrapper - .find('.btn-comment-and-close') - .text() - .trim(), - ).toEqual('Comment & close issue'); - - done(); - }); + it('should update buttons texts when it has note', () => { + mountComponent({ initialData: { note: 'Foo' } }); + + expect(findCloseReopenButton().text()).toBe('Comment & close issue'); }); - it('updates button text with noteable type', done => { - wrapper.setProps({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE }); - - wrapper.vm.$nextTick(() => { - expect( - wrapper - .find('.btn-comment-and-close') - .text() - .trim(), - ).toEqual('Close merge request'); - done(); - }); + it('updates button text with noteable type', () => { + mountComponent({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE }); + + expect(findCloseReopenButton().text()).toBe('Close merge request'); }); describe('when clicking close/reopen button', () => { - it('should disable button and show a loading spinner', () => { - const toggleStateButton = wrapper.find('.js-action-button'); + it('should show a loading spinner', async () => { + mountComponent({ + noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE, + mountFunction: mount, + }); - toggleStateButton.trigger('click'); + await findCloseReopenButton().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(toggleStateButton.element.disabled).toEqual(true); - expect(toggleStateButton.props('loading')).toBe(true); - }); + expect(findCloseReopenButton().props('loading')).toBe(true); }); }); describe('when toggling state', () => { - it('should update MR count', done => { - jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue(); + describe('when issue', () => { + it('emits event to toggle state', () => { + mountComponent({ mountFunction: mount }); + + jest.spyOn(eventHub, '$emit'); + + findCloseReopenButton().trigger('click'); + + expect(eventHub.$emit).toHaveBeenCalledWith('toggle.issuable.state'); + }); + }); + + describe.each` + type | noteableType + ${'merge request'} | ${'MergeRequest'} + ${'epic'} | ${'Epic'} + `('when $type', ({ type, noteableType }) => { + describe('when open', () => { + it(`makes an API call to open it`, () => { + mountComponent({ + noteableType, + noteableData: { ...noteableDataMock, state: constants.OPENED }, + mountFunction: mount, + }); + + jest.spyOn(wrapper.vm, 'closeIssuable').mockResolvedValue(); + + findCloseReopenButton().trigger('click'); + + expect(wrapper.vm.closeIssuable).toHaveBeenCalled(); + }); + + it(`shows an error when the API call fails`, async () => { + mountComponent({ + noteableType, + noteableData: { ...noteableDataMock, state: constants.OPENED }, + mountFunction: mount, + }); + + jest.spyOn(wrapper.vm, 'closeIssuable').mockRejectedValue(); + + await findCloseReopenButton().trigger('click'); + + await wrapper.vm.$nextTick; + + expect(flash).toHaveBeenCalledWith( + `Something went wrong while closing the ${type}. Please try again later.`, + ); + }); + }); + + describe('when closed', () => { + it('makes an API call to close it', () => { + mountComponent({ + noteableType, + noteableData: { ...noteableDataMock, state: constants.CLOSED }, + mountFunction: mount, + }); + + jest.spyOn(wrapper.vm, 'reopenIssuable').mockResolvedValue(); - wrapper.vm.toggleIssueState(); + findCloseReopenButton().trigger('click'); - wrapper.vm.$nextTick(() => { - expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); + expect(wrapper.vm.reopenIssuable).toHaveBeenCalled(); + }); + }); + + it(`shows an error when the API call fails`, async () => { + mountComponent({ + noteableType, + noteableData: { ...noteableDataMock, state: constants.CLOSED }, + mountFunction: mount, + }); + + jest.spyOn(wrapper.vm, 'reopenIssuable').mockRejectedValue(); + + await findCloseReopenButton().trigger('click'); - done(); + await wrapper.vm.$nextTick; + + expect(flash).toHaveBeenCalledWith( + `Something went wrong while reopening the ${type}. Please try again later.`, + ); }); }); + + it('when merge request, should update MR count', async () => { + mountComponent({ + noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE, + mountFunction: mount, + }); + + jest.spyOn(wrapper.vm, 'closeIssuable').mockResolvedValue(); + + await findCloseReopenButton().trigger('click'); + + expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); + }); }); }); describe('issue is confidential', () => { - it('shows information warning', done => { - store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true })); - wrapper.vm.$nextTick(() => { - expect(wrapper.find('.confidential-issue-warning')).toBeDefined(); - done(); + it('shows information warning', () => { + mountComponent({ + noteableData: { ...noteableDataMock, confidential: true }, + mountFunction: mount, }); + + expect(wrapper.find('[data-testid="confidential-warning"]').exists()).toBe(true); }); }); }); describe('user is not logged in', () => { beforeEach(() => { - setupStore(null, loggedOutnoteableData); - - mountComponent(); + mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount }); }); it('should render signed out widget', () => { - expect(trimText(wrapper.text())).toEqual('Please register or sign in to reply'); + expect(wrapper.text()).toBe('Please register or sign in to reply'); }); it('should not render submission form', () => { - expect(wrapper.find('textarea').exists()).toBe(false); + expect(findTextArea().exists()).toBe(false); }); }); - describe('when issuable is open', () => { - beforeEach(() => { - setupStore(userDataMock, noteableDataMock); - }); - - it.each([['opened', 'warning'], ['reopened', 'warning']])( - 'when %i, it changes the variant of the btn to %i', - (a, expected) => { - store.state.noteableData.state = a; - - mountComponent(); - - expect(wrapper.find('.js-action-button').props('variant')).toBe(expected); - }, - ); - }); - - describe('when issuable is not open', () => { - beforeEach(() => { - setupStore(userDataMock, noteableDataMock); - - mountComponent(); - }); + describe('close/reopen button variants', () => { + it.each([ + [constants.OPENED, 'warning'], + [constants.REOPENED, 'warning'], + [constants.CLOSED, 'default'], + ])('when %s, the variant of the btn is %s', (state, expected) => { + mountComponent({ noteableData: { ...noteableDataMock, state } }); - it('should render the "default" variant of the button', () => { - expect(wrapper.find('.js-action-button').props('variant')).toBe('warning'); + expect(findCloseReopenButton().props('variant')).toBe(expected); }); }); }); |