diff options
Diffstat (limited to 'spec/frontend/notes')
27 files changed, 347 insertions, 146 deletions
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 002c4f206cb..2f58f75ab70 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -1,15 +1,15 @@ -import { nextTick } from 'vue'; import { mount, shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; import Autosize from 'autosize'; +import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; 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 createStore from '~/notes/stores'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; jest.mock('autosize'); @@ -22,11 +22,25 @@ describe('issue_comment_form component', () => { let wrapper; let axiosMock; - const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]'); + const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button'); + const findCommentButton = () => wrapper.findByTestId('comment-button'); + const findTextArea = () => wrapper.findByTestId('comment-field'); + const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox'); - const findCommentButton = () => wrapper.find('[data-testid="comment-button"]'); + const createNotableDataMock = (data = {}) => { + return { + ...noteableDataMock, + ...data, + }; + }; - const findTextArea = () => wrapper.find('[data-testid="comment-field"]'); + const notableDataMockCanUpdateIssuable = createNotableDataMock({ + current_user: { can_update: true, can_create_note: true }, + }); + + const notableDataMockCannotUpdateIssuable = createNotableDataMock({ + current_user: { can_update: false, can_create_note: true }, + }); const mountComponent = ({ initialData = {}, @@ -34,23 +48,29 @@ describe('issue_comment_form component', () => { noteableData = noteableDataMock, notesData = notesDataMock, userData = userDataMock, + features = {}, mountFunction = shallowMount, } = {}) => { store.dispatch('setNoteableData', noteableData); store.dispatch('setNotesData', notesData); store.dispatch('setUserData', userData); - wrapper = mountFunction(CommentForm, { - propsData: { - noteableType, - }, - data() { - return { - ...initialData, - }; - }, - store, - }); + wrapper = extendedWrapper( + mountFunction(CommentForm, { + propsData: { + noteableType, + }, + data() { + return { + ...initialData, + }; + }, + store, + provide: { + glFeatures: features, + }, + }), + ); }; beforeEach(() => { @@ -64,14 +84,6 @@ describe('issue_comment_form component', () => { }); 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' } }); @@ -368,6 +380,83 @@ describe('issue_comment_form component', () => { }); }); }); + + describe('confidential notes checkbox', () => { + describe('when confidentialNotes feature flag is `false`', () => { + const features = { confidentialNotes: false }; + + it('should not render checkbox', () => { + mountComponent({ + mountFunction: mount, + initialData: { note: 'confidential note' }, + noteableData: { ...notableDataMockCanUpdateIssuable }, + features, + }); + + const checkbox = findConfidentialNoteCheckbox(); + expect(checkbox.exists()).toBe(false); + }); + }); + + describe('when confidentialNotes feature flag is `true`', () => { + const features = { confidentialNotes: true }; + + it('should render checkbox as unchecked by default', () => { + mountComponent({ + mountFunction: mount, + initialData: { note: 'confidential note' }, + noteableData: { ...notableDataMockCanUpdateIssuable }, + features, + }); + + const checkbox = findConfidentialNoteCheckbox(); + expect(checkbox.exists()).toBe(true); + expect(checkbox.element.checked).toBe(false); + }); + + describe.each` + shouldCheckboxBeChecked + ${true} + ${false} + `('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => { + it(`sets \`confidential\` to \`${shouldCheckboxBeChecked}\``, async () => { + mountComponent({ + mountFunction: mount, + initialData: { note: 'confidential note' }, + noteableData: { ...notableDataMockCanUpdateIssuable }, + features, + }); + + jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue({}); + + const checkbox = findConfidentialNoteCheckbox(); + + // check checkbox + checkbox.element.checked = shouldCheckboxBeChecked; + checkbox.trigger('change'); + await wrapper.vm.$nextTick(); + + // submit comment + wrapper.findByTestId('comment-button').trigger('click'); + + const [providedData] = wrapper.vm.saveNote.mock.calls[0]; + expect(providedData.data.note.confidential).toBe(shouldCheckboxBeChecked); + }); + }); + + describe('when user cannot update issuable', () => { + it('should not render checkbox', () => { + mountComponent({ + mountFunction: mount, + noteableData: { ...notableDataMockCannotUpdateIssuable }, + features, + }); + + expect(findConfidentialNoteCheckbox().exists()).toBe(false); + }); + }); + }); + }); }); describe('user is not logged in', () => { diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js index 3940439a32b..fdc89522901 100644 --- a/spec/frontend/notes/components/diff_discussion_header_spec.js +++ b/spec/frontend/notes/components/diff_discussion_header_spec.js @@ -1,10 +1,10 @@ import { mount } from '@vue/test-utils'; -import createStore from '~/notes/stores'; import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue'; +import createStore from '~/notes/stores'; -import { discussionMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_discussions'; +import { discussionMock } from '../mock_data'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js index 6480af015db..e997fc4da50 100644 --- a/spec/frontend/notes/components/diff_with_note_spec.js +++ b/spec/frontend/notes/components/diff_with_note_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import DiffWithNote from '~/notes/components/diff_with_note.vue'; import { createStore } from '~/mr_notes/stores'; +import DiffWithNote from '~/notes/components/diff_with_note.vue'; const discussionFixture = 'merge_requests/diff_discussion.json'; const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json'; diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index 48e569720e9..03e5842bb0f 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -1,10 +1,10 @@ import { shallowMount, mount } from '@vue/test-utils'; -import { discussionMock } from '../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'; import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; import createStore from '~/notes/stores'; +import { discussionMock } from '../mock_data'; // NOTE: clone mock_data so that it is not accidentally mutated const createDiscussionMock = (props = {}) => diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index ebf7d52f38b..9db0f823d84 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -1,10 +1,10 @@ -import Vuex from 'vuex'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; -import notesModule from '~/notes/stores/modules'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; import DiscussionCounter from '~/notes/components/discussion_counter.vue'; -import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data'; +import notesModule from '~/notes/stores/modules'; import * as types from '~/notes/stores/mutation_types'; +import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data'; describe('DiscussionCounter component', () => { let store; diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js index 9ae3f08df77..ad9a2e898eb 100644 --- a/spec/frontend/notes/components/discussion_filter_note_spec.js +++ b/spec/frontend/notes/components/discussion_filter_note_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlButton, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; import eventHub from '~/notes/event_hub'; diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js index aeba8e8056c..6f62b8ba528 100644 --- a/spec/frontend/notes/components/discussion_filter_spec.js +++ b/spec/frontend/notes/components/discussion_filter_spec.js @@ -1,13 +1,13 @@ -import Vuex from 'vuex'; import { createLocalVue, mount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; +import Vuex from 'vuex'; import { TEST_HOST } from 'helpers/test_constants'; import createEventHub from '~/helpers/event_hub_factory'; import axios from '~/lib/utils/axios_utils'; -import notesModule from '~/notes/stores/modules'; import DiscussionFilter from '~/notes/components/discussion_filter.vue'; import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; +import notesModule from '~/notes/stores/modules'; import { discussionFiltersMock, discussionMock } from '../mock_data'; diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js index 122814b8e3f..4d55eee2ffa 100644 --- a/spec/frontend/notes/components/discussion_navigator_spec.js +++ b/spec/frontend/notes/components/discussion_navigator_spec.js @@ -1,7 +1,7 @@ /* global Mousetrap */ import 'mousetrap'; -import Vue from 'vue'; import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vue from 'vue'; import DiscussionNavigator from '~/notes/components/discussion_navigator.vue'; import eventHub from '~/notes/event_hub'; diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index e803dcb7b4a..cd24b9afbdf 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -1,13 +1,13 @@ -import { shallowMount } from '@vue/test-utils'; import { getByRole } from '@testing-library/dom'; +import { shallowMount } from '@vue/test-utils'; import '~/behaviors/markdown/render_gfm'; -import { SYSTEM_NOTE } from '~/notes/constants'; import DiscussionNotes from '~/notes/components/discussion_notes.vue'; import NoteableNote from '~/notes/components/noteable_note.vue'; +import { SYSTEM_NOTE } from '~/notes/constants'; +import createStore from '~/notes/stores'; import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; import SystemNote from '~/vue_shared/components/notes/system_note.vue'; -import createStore from '~/notes/stores'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; const LINE_RANGE = {}; @@ -23,7 +23,7 @@ describe('DiscussionNotes', () => { let wrapper; const getList = () => getByRole(wrapper.element, 'list'); - const createComponent = (props, features = {}) => { + const createComponent = (props) => { wrapper = shallowMount(DiscussionNotes, { store, propsData: { @@ -38,9 +38,6 @@ describe('DiscussionNotes', () => { slots: { 'avatar-badge': '<span class="avatar-badge-slot-content" />', }, - provide: { - glFeatures: { multilineComments: true, ...features }, - }, }); }; @@ -177,16 +174,14 @@ describe('DiscussionNotes', () => { }); describe.each` - desc | props | features | event | expectedCalls - ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]} - ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]} - ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]} - ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]} - ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]} - ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]} - `('$desc and features $features', ({ props, event, features, expectedCalls }) => { + desc | props | event | expectedCalls + ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]} + ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]} + ${'without `discussion.position`'} | ${{}} | ${'mouseenter'} | ${[]} + ${'without `discussion.position`'} | ${{}} | ${'mouseleave'} | ${[]} + `('$desc', ({ props, event, expectedCalls }) => { beforeEach(() => { - createComponent(props, features); + createComponent(props); jest.spyOn(store, 'dispatch'); }); diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js index 5105e1013d3..64e061830b9 100644 --- a/spec/frontend/notes/components/discussion_resolve_button_spec.js +++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import resolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue'; const buttonTitle = 'Resolve discussion'; diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js index 081fd6e10ef..b6d603c6358 100644 --- a/spec/frontend/notes/components/multiline_comment_form_spec.js +++ b/spec/frontend/notes/components/multiline_comment_form_spec.js @@ -1,7 +1,7 @@ +import { GlFormSelect } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; -import { mount } from '@vue/test-utils'; -import { GlFormSelect } from '@gitlab/ui'; import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue'; import notesModule from '~/notes/stores/modules'; diff --git a/spec/frontend/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js index 720ab10b270..4993ded365d 100644 --- a/spec/frontend/notes/components/note_actions/reply_button_spec.js +++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js @@ -1,29 +1,22 @@ -import Vuex from 'vuex'; -import { createLocalVue, mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; -const localVue = createLocalVue(); -localVue.use(Vuex); - describe('ReplyButton', () => { let wrapper; beforeEach(() => { - wrapper = mount(localVue.extend(ReplyButton), { - localVue, - }); + wrapper = shallowMount(ReplyButton); }); afterEach(() => { wrapper.destroy(); + wrapper = null; }); it('emits startReplying on click', () => { - const button = wrapper.find({ ref: 'button' }); - - button.trigger('click'); + wrapper.find(GlButton).vm.$emit('click'); - expect(wrapper.emitted().startReplying).toBeTruthy(); - expect(wrapper.emitted().startReplying.length).toBe(1); + expect(wrapper.emitted('startReplying')).toEqual([[]]); }); }); diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index 3cfc1445cb8..17717ebd09a 100644 --- a/spec/frontend/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js @@ -1,11 +1,12 @@ -import Vue from 'vue'; import { mount, createLocalVue, createWrapper } from '@vue/test-utils'; -import { TEST_HOST } from 'spec/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; -import createStore from '~/notes/stores'; +import Vue from 'vue'; +import { TEST_HOST } from 'spec/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import noteActions from '~/notes/components/note_actions.vue'; +import createStore from '~/notes/stores'; import { userDataMock } from '../mock_data'; -import axios from '~/lib/utils/axios_utils'; describe('noteActions', () => { let wrapper; @@ -135,7 +136,7 @@ describe('noteActions', () => { .then(() => { const emitted = Object.keys(rootWrapper.emitted()); - expect(emitted).toEqual(['bv::hide::tooltip']); + expect(emitted).toEqual([BV_HIDE_TOOLTIP]); done(); }) .catch(done.fail); diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js index 13a817902e6..9fc89ffa473 100644 --- a/spec/frontend/notes/components/note_awards_list_spec.js +++ b/spec/frontend/notes/components/note_awards_list_spec.js @@ -1,9 +1,9 @@ -import Vue from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; +import Vue from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; -import createStore from '~/notes/stores'; import awardsNote from '~/notes/components/note_awards_list.vue'; +import createStore from '~/notes/stores'; import { noteableDataMock, notesDataMock } from '../mock_data'; describe('note_awards_list component', () => { diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index 3c11c266f90..4922de987fa 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -1,6 +1,14 @@ +import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; -import createStore from '~/notes/stores'; +import Vuex from 'vuex'; + +import { suggestionCommitMessage } from '~/diffs/store/getters'; import noteBody from '~/notes/components/note_body.vue'; +import createStore from '~/notes/stores'; +import notes from '~/notes/stores/modules/index'; + +import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; + import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note_body component', () => { @@ -54,4 +62,50 @@ describe('issue_note_body component', () => { expect(vm.autosave.key).toEqual(autosaveKey); }); }); + + describe('commitMessage', () => { + let wrapper; + + Vue.use(Vuex); + + beforeEach(() => { + const notesStore = notes(); + + notesStore.state.notes = {}; + + store = new Vuex.Store({ + modules: { + notes: notesStore, + diffs: { + namespaced: true, + state: { + defaultSuggestionCommitMessage: + '%{branch_name}%{project_path}%{project_name}%{username}%{user_full_name}%{file_paths}%{suggestions_count}%{files_count}', + branchName: 'branch', + projectPath: '/path', + projectName: 'name', + username: 'user', + userFullName: 'user userton', + }, + getters: { suggestionCommitMessage }, + }, + }, + }); + + wrapper = shallowMount(noteBody, { + store, + propsData: { + note: { ...note, suggestions: [12345] }, + canEdit: true, + file: { file_path: 'abc' }, + }, + }); + }); + + it('passes the correct default placeholder commit message for a suggestion to the suggestions component', () => { + const commitMessage = wrapper.find(Suggestions).attributes('defaultcommitmessage'); + + expect(commitMessage).toBe('branch/pathnameuseruser usertonabc11'); + }); + }); }); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index e64a75bede9..7615f3b70f1 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -1,13 +1,12 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import createStore from '~/notes/stores'; -import NoteForm from '~/notes/components/note_form.vue'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; +import { getDraft, updateDraft } from '~/lib/utils/autosave'; +import NoteForm from '~/notes/components/note_form.vue'; +import createStore from '~/notes/stores'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data'; -import { getDraft, updateDraft } from '~/lib/utils/autosave'; - jest.mock('~/lib/utils/autosave'); describe('issue_note_form component', () => { @@ -25,6 +24,8 @@ describe('issue_note_form component', () => { }); }; + const findCancelButton = () => wrapper.find('[data-testid="cancel"]'); + beforeEach(() => { getDraft.mockImplementation((key) => { if (key === dummyAutosaveKey) { @@ -160,8 +161,8 @@ describe('issue_note_form component', () => { }); await nextTick(); - const cancelButton = wrapper.find('[data-testid="cancel"]'); - cancelButton.trigger('click'); + const cancelButton = findCancelButton(); + cancelButton.vm.$emit('click'); await nextTick(); expect(wrapper.emitted().cancelForm).toHaveLength(1); @@ -177,7 +178,7 @@ describe('issue_note_form component', () => { const textarea = wrapper.find('textarea'); textarea.setValue('Foo'); const saveButton = wrapper.find('.js-vue-issue-save'); - saveButton.trigger('click'); + saveButton.vm.$emit('click'); expect(wrapper.vm.isSubmitting).toBe(true); }); @@ -272,7 +273,7 @@ describe('issue_note_form component', () => { await nextTick(); const cancelButton = wrapper.find('[data-testid="cancelBatchCommentsEnabled"]'); - cancelButton.trigger('click'); + cancelButton.vm.$emit('click'); expect(wrapper.vm.cancelHandler).toHaveBeenCalledWith(true); }); @@ -302,16 +303,16 @@ describe('issue_note_form component', () => { expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false); }); - it('hides actions for commits', () => { + it('hides actions for commits', async () => { wrapper.setProps({ discussion: { for_commit: true } }); - return nextTick(() => { - expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review'); - }); + await nextTick(); + + expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review'); }); describe('on enter', () => { - it('should start review or add to review when cmd+enter is pressed', () => { + it('should start review or add to review when cmd+enter is pressed', async () => { const textarea = wrapper.find('textarea'); jest.spyOn(wrapper.vm, 'handleAddToReview'); @@ -319,9 +320,8 @@ describe('issue_note_form component', () => { textarea.setValue('Foo'); textarea.trigger('keydown.enter', { metaKey: true }); - return nextTick(() => { - expect(wrapper.vm.handleAddToReview).toHaveBeenCalled(); - }); + await nextTick(); + expect(wrapper.vm.handleAddToReview).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js index 132e3d8aa7e..774d5aaa7d3 100644 --- a/spec/frontend/notes/components/note_header_spec.js +++ b/spec/frontend/notes/components/note_header_spec.js @@ -1,9 +1,10 @@ +import { GlSprintf } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import { nextTick } from 'vue'; import Vuex from 'vuex'; -import { GlSprintf } from '@gitlab/ui'; import NoteHeader from '~/notes/components/note_header.vue'; import { AVAILABILITY_STATUS } from '~/set_status_modal/utils'; +import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -36,9 +37,7 @@ describe('NoteHeader component', () => { username: 'root', show_status: true, status_tooltip_html: statusHtml, - status: { - availability: '', - }, + availability: '', }; const createComponent = (props) => { @@ -48,7 +47,7 @@ describe('NoteHeader component', () => { actions, }), propsData: { ...props }, - stubs: { GlSprintf }, + stubs: { GlSprintf, UserNameWithStatus }, }); }; @@ -110,7 +109,7 @@ describe('NoteHeader component', () => { }); it('renders busy status if author availability is set', () => { - createComponent({ author: { ...author, status: { availability: AVAILABILITY_STATUS.BUSY } } }); + createComponent({ author: { ...author, availability: AVAILABILITY_STATUS.BUSY } }); expect(wrapper.find('.js-user-link').text()).toContain('(Busy)'); }); diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index b87c6cd7f2b..87538279c3d 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -1,13 +1,13 @@ -import { nextTick } from 'vue'; import { mount } from '@vue/test-utils'; -import mockDiffFile from 'jest/diffs/mock_data/diff_file'; +import { nextTick } from 'vue'; import { trimText } from 'helpers/text_helper'; -import createStore from '~/notes/stores'; -import NoteableDiscussion from '~/notes/components/noteable_discussion.vue'; +import mockDiffFile from 'jest/diffs/mock_data/diff_file'; import DiscussionNotes from '~/notes/components/discussion_notes.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; import NoteForm from '~/notes/components/note_form.vue'; +import NoteableDiscussion from '~/notes/components/noteable_discussion.vue'; +import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index 6f06665f412..fe78e086403 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -1,22 +1,13 @@ -import { escape } from 'lodash'; import { mount, createLocalVue } from '@vue/test-utils'; -import createStore from '~/notes/stores'; -import issueNote from '~/notes/components/noteable_note.vue'; -import NoteHeader from '~/notes/components/note_header.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { escape } from 'lodash'; import NoteActions from '~/notes/components/note_actions.vue'; import NoteBody from '~/notes/components/note_body.vue'; +import NoteHeader from '~/notes/components/note_header.vue'; +import issueNote from '~/notes/components/noteable_note.vue'; +import createStore from '~/notes/stores'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; -jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({ - inject: { - glFeatures: { - from: 'glFeatures', - default: () => ({ multilineComments: true }), - }, - }, -})); - describe('issue_note', () => { let store; let wrapper; diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index e495a4738e0..efee72dea96 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -1,18 +1,18 @@ -import $ from 'jquery'; +import { mount, shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; +import $ from 'jquery'; import Vue from 'vue'; -import { mount, shallowMount } from '@vue/test-utils'; import { setTestTimeout } from 'helpers/timeout'; import axios from '~/lib/utils/axios_utils'; -import NotesApp from '~/notes/components/notes_app.vue'; +import * as urlUtility from '~/lib/utils/url_utility'; import CommentForm from '~/notes/components/comment_form.vue'; -import createStore from '~/notes/stores'; +import NotesApp from '~/notes/components/notes_app.vue'; import * as constants from '~/notes/constants'; +import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; // TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491) -import * as mockData from '../mock_data'; -import * as urlUtility from '~/lib/utils/url_utility'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; +import * as mockData from '../mock_data'; jest.mock('~/user_popovers', () => jest.fn()); diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js index 739e247735d..60f03a0f5b5 100644 --- a/spec/frontend/notes/components/sort_discussion_spec.js +++ b/spec/frontend/notes/components/sort_discussion_spec.js @@ -1,10 +1,10 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import SortDiscussion from '~/notes/components/sort_discussion.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import createStore from '~/notes/stores'; import { ASC, DESC } from '~/notes/constants'; +import createStore from '~/notes/stores'; import Tracking from '~/tracking'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; const localVue = createLocalVue(); localVue.use(Vuex); diff --git a/spec/frontend/notes/components/timeline_toggle_spec.js b/spec/frontend/notes/components/timeline_toggle_spec.js index b8df6fc7996..73fb2079e31 100644 --- a/spec/frontend/notes/components/timeline_toggle_spec.js +++ b/spec/frontend/notes/components/timeline_toggle_spec.js @@ -1,12 +1,12 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import TimelineToggle, { timelineEnabledTooltip, timelineDisabledTooltip, } from '~/notes/components/timeline_toggle.vue'; -import createStore from '~/notes/stores'; import { ASC, DESC } from '~/notes/constants'; +import createStore from '~/notes/stores'; import { trackToggleTimelineView } from '~/notes/utils'; import Tracking from '~/tracking'; diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js index 9c9a648d213..6a6e47ffcc5 100644 --- a/spec/frontend/notes/mixins/discussion_navigation_spec.js +++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js @@ -1,10 +1,10 @@ -import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; import { setHTMLFixture } from 'helpers/fixtures'; +import createEventHub from '~/helpers/event_hub_factory'; import * as utils from '~/lib/utils/common_utils'; -import discussionNavigation from '~/notes/mixins/discussion_navigation'; import eventHub from '~/notes/event_hub'; -import createEventHub from '~/helpers/event_hub_factory'; +import discussionNavigation from '~/notes/mixins/discussion_navigation'; import notesModule from '~/notes/stores/modules'; const discussion = (id, index) => ({ diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js index 00821980e8a..432b660c4b3 100644 --- a/spec/frontend/notes/old_notes_spec.js +++ b/spec/frontend/notes/old_notes_spec.js @@ -1,13 +1,13 @@ /* eslint-disable import/no-commonjs, no-new */ -import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; +import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { createSpyObj } from 'helpers/jest_helpers'; -import { setTestTimeoutOnce } from 'helpers/timeout'; import { TEST_HOST } from 'helpers/test_constants'; -import * as urlUtility from '~/lib/utils/url_utility'; +import { setTestTimeoutOnce } from 'helpers/timeout'; import axios from '~/lib/utils/axios_utils'; +import * as urlUtility from '~/lib/utils/url_utility'; // These must be imported synchronously because they pull dependencies // from the DOM. diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index f0e6a0a68dd..1852108b39f 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -1,13 +1,18 @@ -import { TEST_HOST } from 'spec/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; import Api from '~/api'; import { deprecatedCreateFlash as Flash } from '~/flash'; -import * as actions from '~/notes/stores/actions'; -import mutations from '~/notes/stores/mutations'; -import * as mutationTypes from '~/notes/stores/mutation_types'; +import axios from '~/lib/utils/axios_utils'; import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; +import * as actions from '~/notes/stores/actions'; +import * as mutationTypes from '~/notes/stores/mutation_types'; +import mutations from '~/notes/stores/mutations'; +import * as utils from '~/notes/stores/utils'; +import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql'; +import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql'; +import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; import { resetStore } from '../helpers'; import { @@ -18,11 +23,6 @@ import { individualNote, batchSuggestionsInfoMock, } from '../mock_data'; -import axios from '~/lib/utils/axios_utils'; -import * as utils from '~/notes/stores/utils'; -import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql'; -import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql'; -import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql'; const TEST_ERROR_MESSAGE = 'Test error message'; jest.mock('~/flash'); @@ -291,9 +291,45 @@ describe('Actions Notes Store', () => { [ { type: 'updateOrCreateNotes', payload: discussionMock.notes }, { type: 'startTaskList' }, + { type: 'updateResolvableDiscussionsCounts' }, ], )); }); + + describe('paginated notes feature flag enabled', () => { + const lastFetchedAt = '12358'; + + beforeEach(() => { + window.gon = { features: { paginatedNotes: true } }; + + axiosMock.onGet(notesDataMock.notesPath).replyOnce(200, { + notes: discussionMock.notes, + more: false, + last_fetched_at: lastFetchedAt, + }); + }); + + afterEach(() => { + window.gon = null; + }); + + it('should dispatch setFetchingState, setNotesFetchedState, setLoadingState, updateOrCreateNotes, startTaskList and commit SET_LAST_FETCHED_AT', () => { + return testAction( + actions.fetchData, + null, + { notesData: notesDataMock, isFetching: true }, + [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }], + [ + { type: 'setFetchingState', payload: false }, + { type: 'setNotesFetchedState', payload: true }, + { type: 'setLoadingState', payload: false }, + { type: 'updateOrCreateNotes', payload: discussionMock.notes }, + { type: 'startTaskList' }, + { type: 'updateResolvableDiscussionsCounts' }, + ], + ); + }); + }); }); describe('poll', () => { @@ -1276,6 +1312,7 @@ describe('Actions Notes Store', () => { return actions .updateConfidentialityOnIssuable({ commit: commitSpy, state, getters }, actionArgs) .then(() => { + expect(Flash).not.toHaveBeenCalled(); expect(commitSpy).toHaveBeenCalledWith( mutationTypes.SET_ISSUE_CONFIDENTIAL, confidential, @@ -1283,6 +1320,22 @@ describe('Actions Notes Store', () => { }); }); }); + + describe('on user recoverable error', () => { + it('sends the error to Flash', () => { + const error = 'error'; + + jest + .spyOn(utils.gqClient, 'mutate') + .mockResolvedValue({ data: { issueSetConfidential: { errors: [error] } } }); + + return actions + .updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs) + .then(() => { + expect(Flash).toHaveBeenCalledWith(error, 'alert'); + }); + }); + }); }); describe.each` @@ -1355,4 +1408,17 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('setFetchingState', () => { + it('commits SET_NOTES_FETCHING_STATE', (done) => { + testAction( + actions.setFetchingState, + true, + null, + [{ type: mutationTypes.SET_NOTES_FETCHING_STATE, payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js index fd04d08b6a5..4ebfc679310 100644 --- a/spec/frontend/notes/stores/getters_spec.js +++ b/spec/frontend/notes/stores/getters_spec.js @@ -1,5 +1,5 @@ -import * as getters from '~/notes/stores/getters'; import { DESC } from '~/notes/constants'; +import * as getters from '~/notes/stores/getters'; import { notesDataMock, userDataMock, diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index 66fc74525ad..99e24f724f4 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import mutations from '~/notes/stores/mutations'; import { DISCUSSION_NOTE, ASC, DESC } from '~/notes/constants'; +import mutations from '~/notes/stores/mutations'; import { note, discussionMock, @@ -400,6 +400,19 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].notes[0].note).toEqual('Foo'); }); + it('does not update existing note if it matches', () => { + const state = { + discussions: [{ ...individualNote, individual_note: false }], + }; + jest.spyOn(state.discussions[0].notes, 'splice'); + + const updated = individualNote.notes[0]; + + mutations.UPDATE_NOTE(state, updated); + + expect(state.discussions[0].notes.splice).not.toHaveBeenCalled(); + }); + it('transforms an individual note to discussion', () => { const state = { discussions: [individualNote], |