import { nextTick } from 'vue'; import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import Autosize from 'autosize'; 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 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', () => { let store; let wrapper; let axiosMock; 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', notesData); store.dispatch('setUserData', userData); wrapper = mountFunction(CommentForm, { propsData: { noteableType, }, data() { return { ...initialData, }; }, store, }); }; beforeEach(() => { axiosMock = new MockAdapter(axios); store = createStore(); }); afterEach(() => { axiosMock.restore(); wrapper.destroy(); }); describe('user is logged in', () => { describe('avatar', () => { it('should render user avatar with link', () => { mountComponent({ mountFunction: mount }); expect(wrapper.find(UserAvatarLink).attributes('href')).toBe(userDataMock.path); }); }); describe('handleSave', () => { it('should request to save note when note is entered', () => { mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } }); jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue(); jest.spyOn(wrapper.vm, 'resizeTextarea'); jest.spyOn(wrapper.vm, 'stopPolling'); findCloseReopenButton().trigger('click'); 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'); findCloseReopenButton().trigger('click'); expect(wrapper.vm.toggleIssueState).toHaveBeenCalled(); }); it('should disable action button while submitting', async () => { mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } }); const saveNotePromise = Promise.resolve(); jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise); jest.spyOn(wrapper.vm, 'stopPolling'); 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', () => { describe('general', () => { it('should render textarea with placeholder', () => { mountComponent({ mountFunction: mount }); expect(findTextArea().attributes('placeholder')).toBe( 'Write a comment or drag your files hereā€¦', ); }); it('should make textarea disabled while requesting', async () => { mountComponent({ mountFunction: mount }); 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', () => { mountComponent({ mountFunction: mount }); expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true'); }); it('should link to markdown docs', () => { mountComponent({ mountFunction: mount }); const { markdownDocsPath } = notesDataMock; 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' } }); jest.spyOn(wrapper.vm, 'discard'); wrapper.vm.discard(); await nextTick(); expect(Autosize.update).toHaveBeenCalled(); }); }); describe('edit mode', () => { beforeEach(() => { mountComponent(); }); it('should enter edit mode when arrow up is pressed', () => { jest.spyOn(wrapper.vm, 'editCurrentUserLastNote'); findTextArea().trigger('keydown.up'); expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled(); }); it('inits autosave', () => { expect(wrapper.vm.autosave).toBeDefined(); 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'); 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'); findTextArea().trigger('keydown.enter', { ctrlKey: true }); expect(wrapper.vm.handleSave).toHaveBeenCalled(); }); }); }); describe('actions', () => { it('should be possible to close the issue', () => { mountComponent(); expect(findCloseReopenButton().text()).toBe('Close issue'); }); it('should render comment button as disabled', () => { mountComponent(); expect(findCommentButton().props('disabled')).toBe(true); }); 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', () => { mountComponent({ initialData: { note: 'Foo' } }); expect(findCloseReopenButton().text()).toBe('Comment & close issue'); }); 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 show a loading spinner', async () => { mountComponent({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE, mountFunction: mount, }); await findCloseReopenButton().trigger('click'); expect(findCloseReopenButton().props('loading')).toBe(true); }); }); describe('when toggling state', () => { 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(); findCloseReopenButton().trigger('click'); 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'); 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', () => { mountComponent({ noteableData: { ...noteableDataMock, confidential: true }, mountFunction: mount, }); expect(wrapper.find('[data-testid="confidential-warning"]').exists()).toBe(true); }); }); }); describe('user is not logged in', () => { beforeEach(() => { mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount }); }); it('should render signed out widget', () => { expect(wrapper.text()).toBe('Please register or sign in to reply'); }); it('should not render submission form', () => { expect(findTextArea().exists()).toBe(false); }); }); 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 } }); expect(findCloseReopenButton().props('variant')).toBe(expected); }); }); });