diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-19 22:11:55 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-19 22:11:55 +0000 |
commit | 5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch) | |
tree | e5df8e0ceee60f4af8093f5c4c2f934b8abced05 /spec/frontend/notes/components | |
parent | 4d477238500c347c6553d335d920bedfc5a46869 (diff) | |
download | gitlab-ce-5a8431feceba47fd8e1804d9aa1b1730606b71d5.tar.gz |
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
Diffstat (limited to 'spec/frontend/notes/components')
5 files changed, 498 insertions, 8 deletions
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js new file mode 100644 index 00000000000..45b99b71e06 --- /dev/null +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -0,0 +1,331 @@ +import $ from 'jquery'; +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import Autosize from 'autosize'; +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 { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import { trimText } from 'helpers/text_helper'; +import { keyboardDownEvent } from '../../issue_show/helpers'; +import { + loggedOutnoteableData, + notesDataMock, + userDataMock, + noteableDataMock, +} from '../../notes/mock_data'; + +jest.mock('autosize'); +jest.mock('~/commons/nav/user_merge_requests'); +jest.mock('~/gl_form'); + +describe('issue_comment_form component', () => { + let store; + let wrapper; + let axiosMock; + + const setupStore = (userData, noteableData) => { + store.dispatch('setUserData', userData); + store.dispatch('setNoteableData', noteableData); + store.dispatch('setNotesData', notesDataMock); + }; + + const mountComponent = (noteableType = 'issue') => { + wrapper = mount(CommentForm, { + propsData: { + noteableType, + }, + store, + sync: false, + }); + }; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + store = createStore(); + }); + + afterEach(() => { + axiosMock.restore(); + wrapper.destroy(); + jest.clearAllMocks(); + }); + + describe('user is logged in', () => { + beforeEach(() => { + setupStore(userDataMock, noteableDataMock); + + mountComponent(); + }); + + it('should render user avatar with link', () => { + expect(wrapper.find('.timeline-icon .user-avatar-link').attributes('href')).toEqual( + 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(() => {})); + jest.spyOn(wrapper.vm, 'resizeTextarea'); + jest.spyOn(wrapper.vm, 'stopPolling'); + + wrapper.vm.handleSave(); + + expect(wrapper.vm.isSubmitting).toEqual(true); + expect(wrapper.vm.note).toEqual(''); + expect(wrapper.vm.saveNote).toHaveBeenCalled(); + expect(wrapper.vm.stopPolling).toHaveBeenCalled(); + expect(wrapper.vm.resizeTextarea).toHaveBeenCalled(); + }); + + it('should toggle issue state when no note', () => { + jest.spyOn(wrapper.vm, 'toggleIssueState'); + + wrapper.vm.handleSave(); + + expect(wrapper.vm.toggleIssueState).toHaveBeenCalled(); + }); + + it('should disable action button whilst submitting', done => { + 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); + }); + }); + + 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ā¦', + ); + }); + + 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(() => {})); + + wrapper.vm.$nextTick(() => { + // Wait for wrapper.vm.note change triggered. It should enable $submitButton. + $submitButton.trigger('click'); + + 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(); + }); + }); + }); + + it('should support quick actions', () => { + expect( + wrapper.find('.js-main-target-form textarea').attributes('data-supports-quick-actions'), + ).toBe('true'); + }); + + it('should link to markdown docs', () => { + const { markdownDocsPath } = notesDataMock; + + expect( + wrapper + .find(`a[href="${markdownDocsPath}"]`) + .text() + .trim(), + ).toEqual('Markdown'); + }); + + it('should link to quick actions docs', () => { + const { quickActionsDocsPath } = notesDataMock; + + expect( + wrapper + .find(`a[href="${quickActionsDocsPath}"]`) + .text() + .trim(), + ).toEqual('quick actions'); + }); + + it('should resize textarea after note discarded', done => { + jest.spyOn(wrapper.vm, 'discard'); + + wrapper.vm.note = 'foo'; + wrapper.vm.discard(); + + wrapper.vm.$nextTick(() => { + expect(Autosize.update).toHaveBeenCalled(); + done(); + }); + }); + + describe('edit mode', () => { + 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)); + + expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled(); + }); + + it('inits autosave', () => { + expect(wrapper.vm.autosave).toBeDefined(); + expect(wrapper.vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`); + }); + }); + + describe('event enter', () => { + 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)); + + 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)); + + expect(wrapper.vm.handleSave).toHaveBeenCalled(); + }); + }); + }); + + describe('actions', () => { + it('should be possible to close the issue', () => { + expect( + wrapper + .find('.btn-comment-and-close') + .text() + .trim(), + ).toEqual('Close issue'); + }); + + it('should render comment button as disabled', () => { + expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toEqual( + 'disabled', + ); + }); + + 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 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('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(); + }); + }); + + describe('when clicking close/reopen button', () => { + it('should disable button and show a loading spinner', done => { + const toggleStateButton = wrapper.find('.js-action-button'); + + toggleStateButton.trigger('click'); + wrapper.vm.$nextTick(() => { + expect(toggleStateButton.element.disabled).toEqual(true); + expect(toggleStateButton.find('.js-loading-button-icon').exists()).toBe(true); + + done(); + }); + }); + }); + + describe('when toggling state', () => { + it('should update MR count', done => { + jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue(); + + wrapper.vm.toggleIssueState(); + + wrapper.vm.$nextTick(() => { + expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); + + done(); + }); + }); + }); + }); + + 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(); + }); + }); + }); + }); + + describe('user is not logged in', () => { + beforeEach(() => { + setupStore(null, loggedOutnoteableData); + + mountComponent(); + }); + + it('should render signed out widget', () => { + expect(trimText(wrapper.text())).toEqual('Please register or sign in to reply'); + }); + + it('should not render submission form', () => { + expect(wrapper.find('textarea').exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js new file mode 100644 index 00000000000..f90147f9105 --- /dev/null +++ b/spec/frontend/notes/components/diff_discussion_header_spec.js @@ -0,0 +1,141 @@ +import { mount, createLocalVue } from '@vue/test-utils'; + +import createStore from '~/notes/stores'; +import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue'; + +import { discussionMock } from '../../../javascripts/notes/mock_data'; +import mockDiffFile from '../../diffs/mock_data/diff_discussions'; + +const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; + +describe('diff_discussion_header component', () => { + let store; + let wrapper; + + preloadFixtures(discussionWithTwoUnresolvedNotes); + + beforeEach(() => { + window.mrTabs = {}; + store = createStore(); + + const localVue = createLocalVue(); + wrapper = mount(diffDiscussionHeader, { + store, + propsData: { discussion: discussionMock }, + localVue, + sync: false, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render user avatar', () => { + const discussion = { ...discussionMock }; + discussion.diff_file = mockDiffFile; + discussion.diff_discussion = true; + + wrapper.setProps({ discussion }); + + expect(wrapper.find('.user-avatar-link').exists()).toBe(true); + }); + + describe('action text', () => { + const commitId = 'razupaltuff'; + const truncatedCommitId = commitId.substr(0, 8); + let commitElement; + + beforeEach(done => { + store.state.diffs = { + projectPath: 'something', + }; + + wrapper.setProps({ + discussion: { + ...discussionMock, + for_commit: true, + commit_id: commitId, + diff_discussion: true, + diff_file: { + ...mockDiffFile, + }, + }, + }); + + wrapper.vm + .$nextTick() + .then(() => { + commitElement = wrapper.find('.commit-sha'); + }) + .then(done) + .catch(done.fail); + }); + + describe('for diff threads without a commit id', () => { + it('should show started a thread on the diff text', done => { + Object.assign(wrapper.vm.discussion, { + for_commit: false, + commit_id: null, + }); + + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain('started a thread on the diff'); + + done(); + }); + }); + + it('should show thread on older version text', done => { + Object.assign(wrapper.vm.discussion, { + for_commit: false, + commit_id: null, + active: false, + }); + + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain('started a thread on an old version of the diff'); + + done(); + }); + }); + }); + + describe('for commit threads', () => { + it('should display a monospace started a thread on commit', () => { + expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`); + expect(commitElement.exists()).toBe(true); + expect(commitElement.text()).toContain(truncatedCommitId); + }); + }); + + describe('for diff thread with a commit id', () => { + it('should display started thread on commit header', done => { + wrapper.vm.discussion.for_commit = false; + + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`); + + expect(commitElement).not.toBe(null); + + done(); + }); + }); + + it('should display outdated change on commit header', done => { + wrapper.vm.discussion.for_commit = false; + wrapper.vm.discussion.active = false; + + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain( + `started a thread on an outdated change in commit ${truncatedCommitId}`, + ); + + expect(commitElement).not.toBe(null); + + done(); + }); + }); + }); + }); +}); diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index d3c8cf72376..91f9dab2530 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -1,6 +1,6 @@ import createStore from '~/notes/stores'; import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; -import { discussionMock } from '../../../javascripts/notes/mock_data'; +import { discussionMock } from '../../notes/mock_data'; import DiscussionActions from '~/notes/components/discussion_actions.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue'; diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 58d367077e8..f77236b14bc 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -8,11 +8,7 @@ import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_sys import SystemNote from '~/vue_shared/components/notes/system_note.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import createStore from '~/notes/stores'; -import { - noteableDataMock, - discussionMock, - notesDataMock, -} from '../../../javascripts/notes/mock_data'; +import { noteableDataMock, discussionMock, notesDataMock } from '../../notes/mock_data'; const localVue = createLocalVue(); diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js index a8ec47fd44f..3716b349210 100644 --- a/spec/frontend/notes/components/note_app_spec.js +++ b/spec/frontend/notes/components/note_app_spec.js @@ -9,7 +9,8 @@ import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; import { setTestTimeout } from 'helpers/timeout'; // TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491) -import * as mockData from '../../../javascripts/notes/mock_data'; +import * as mockData from '../../notes/mock_data'; +import * as urlUtility from '~/lib/utils/url_utility'; setTestTimeout(1000); @@ -54,7 +55,9 @@ describe('note_app', () => { components: { NotesApp, }, - template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>', + template: `<div class="js-vue-notes-event"> + <notes-app ref="notesApp" v-bind="$attrs" /> + </div>`, }, { attachToDocument: true, @@ -313,4 +316,23 @@ describe('note_app', () => { }); }); }); + + describe('mounted', () => { + beforeEach(() => { + axiosMock.onAny().reply(mockData.getIndividualNoteResponse); + wrapper = mountComponent(); + return waitForDiscussionsRequest(); + }); + + it('should listen hashchange event', () => { + const notesApp = wrapper.find(NotesApp); + const hash = 'some dummy hash'; + jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash); + const setTargetNoteHash = jest.spyOn(notesApp.vm, 'setTargetNoteHash'); + + window.dispatchEvent(new Event('hashchange'), hash); + + expect(setTargetNoteHash).toHaveBeenCalled(); + }); + }); }); |