diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 317fe6442d4286a5c9b9c346255e1ce83c5a1765..7eba491430b60cc3205b0a43fc6b667b3456c4e9 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -1,58 +1,10 @@ -import { ASC, MR_FILTER_OPTIONS } from '../../constants'; import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; +import createState from '../state'; export default () => ({ - state: { - discussions: [], - discussionSortOrder: ASC, - persistSortOrder: true, - convertedDisscussionIds: [], - targetNoteHash: null, - lastFetchedAt: null, - currentDiscussionId: null, - batchSuggestionsInfo: [], - currentlyFetchingDiscussions: false, - doneFetchingBatchDiscussions: false, - /** - * selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`: - * { - * start: { line_code: string, new_line: number, old_line:number, type: string }, - * end: { line_code: string, new_line: number, old_line:number, type: string }, - * } - */ - selectedCommentPosition: null, - selectedCommentPositionHover: null, - - // View layer - isToggleStateButtonLoading: false, - isNotesFetched: false, - isLoading: true, - isLoadingDescriptionVersion: false, - isPromoteCommentToTimelineEventInProgress: false, - - // holds endpoints and permissions provided through haml - notesData: { - markdownDocsPath: '', - }, - userData: {}, - noteableData: { - discussion_locked: false, - confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes. - current_user: {}, - preview_note_path: 'path/to/preview', - }, - isResolvingDiscussion: false, - commentsDisabled: false, - resolvableDiscussionsCount: 0, - unresolvedDiscussionsCount: 0, - descriptionVersions: {}, - isTimelineEnabled: false, - isFetching: false, - isPollingInitialized: false, - mergeRequestFilters: MR_FILTER_OPTIONS.map((f) => f.value), - }, + state: createState(), actions, getters, mutations, diff --git a/app/assets/javascripts/notes/stores/state.js b/app/assets/javascripts/notes/stores/state.js new file mode 100644 index 0000000000000000000000000000000000000000..8e49cd861a1f36469315a69bd579c4ef152c9d34 --- /dev/null +++ b/app/assets/javascripts/notes/stores/state.js @@ -0,0 +1,53 @@ +import { ASC, MR_FILTER_OPTIONS } from '../constants'; + +const createState = () => ({ + discussions: [], + discussionSortOrder: ASC, + persistSortOrder: true, + convertedDisscussionIds: [], + targetNoteHash: null, + lastFetchedAt: null, + currentDiscussionId: null, + batchSuggestionsInfo: [], + currentlyFetchingDiscussions: false, + doneFetchingBatchDiscussions: false, + /** + * selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`: + * { + * start: { line_code: string, new_line: number, old_line:number, type: string }, + * end: { line_code: string, new_line: number, old_line:number, type: string }, + * } + */ + selectedCommentPosition: null, + selectedCommentPositionHover: null, + + // View layer + isToggleStateButtonLoading: false, + isNotesFetched: false, + isLoading: true, + isLoadingDescriptionVersion: false, + isPromoteCommentToTimelineEventInProgress: false, + + // holds endpoints and permissions provided through haml + notesData: { + markdownDocsPath: '', + }, + userData: {}, + noteableData: { + discussion_locked: false, + confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes. + current_user: {}, + preview_note_path: 'path/to/preview', + }, + isResolvingDiscussion: false, + commentsDisabled: false, + resolvableDiscussionsCount: 0, + unresolvedDiscussionsCount: 0, + descriptionVersions: {}, + isTimelineEnabled: false, + isFetching: false, + isPollingInitialized: false, + mergeRequestFilters: MR_FILTER_OPTIONS.map((f) => f.value), +}); + +export default createState; diff --git a/ee/spec/frontend/diffs/components/app_spec.js b/ee/spec/frontend/diffs/components/app_spec.js index c179446aa283b4f7c25f5979bb2a17a63ecb9be5..b0c39be577531ef7e94768331e0c1e9bfb11db79 100644 --- a/ee/spec/frontend/diffs/components/app_spec.js +++ b/ee/spec/frontend/diffs/components/app_spec.js @@ -1,34 +1,35 @@ import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; -import Vuex from 'vuex'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import createDiffsStore from 'jest/diffs/create_diffs_store'; import { TEST_HOST } from 'spec/test_constants'; import App from '~/diffs/components/app.vue'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import store from '~/mr_notes/stores'; const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; -Vue.use(Vuex); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); Vue.config.ignoredElements = ['copy-code']; describe('diffs/components/app', () => { - let store; - let wrapper; - let mock; + const createComponent = (props = {}) => { + store.reset(); + + store.getters.isNotesFetched = false; + store.getters.getNoteableData = { + current_user: { + can_create_note: true, + }, + }; + store.getters['diffs/flatBlobsList'] = []; + store.getters['diffs/isBatchLoading'] = false; + store.getters['diffs/isBatchLoadingError'] = false; + store.getters['diffs/whichCollapsedTypes'] = { any: false }; - function createComponent(props = {}, extendStore = () => {}) { - store = createDiffsStore(); store.state.diffs.isLoading = false; store.state.diffs.isTreeLoaded = true; - extendStore(store); - - wrapper = shallowMount(App, { + return shallowMount(App, { propsData: { endpoint: TEST_ENDPOINT, endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, @@ -44,20 +45,15 @@ describe('diffs/components/app', () => { fileByFileUserPreference: false, ...props, }, - store, + mocks: { + $store: store, + }, }); - } - - beforeEach(() => { - stubPerformanceWebAPI(); - - mock = new MockAdapter(axios); - mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, {}); - }); + }; describe('EE codequality diff', () => { it('fetches code quality data when endpoint is provided', () => { - createComponent(); + const wrapper = createComponent(); jest.spyOn(wrapper.vm, 'fetchCodequality'); wrapper.vm.fetchData(false); @@ -65,7 +61,7 @@ describe('diffs/components/app', () => { }); it('does not fetch code quality data when endpoint is blank', () => { - createComponent({ endpointCodequality: '' }); + const wrapper = createComponent({ endpointCodequality: '' }); jest.spyOn(wrapper.vm, 'fetchCodequality'); wrapper.vm.fetchData(false); diff --git a/ee/spec/frontend/diffs/components/code_quality_gutter_icon_spec.js b/ee/spec/frontend/diffs/components/code_quality_gutter_icon_spec.js index 3cd550fda3129334ed60b9aafc055f41acad0336..db2f6737a44e9c0a5802949eb9f0251bdb186898 100644 --- a/ee/spec/frontend/diffs/components/code_quality_gutter_icon_spec.js +++ b/ee/spec/frontend/diffs/components/code_quality_gutter_icon_spec.js @@ -1,9 +1,8 @@ import { GlIcon, GlTooltip } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import CodeQualityGutterIcon from 'ee/diffs/components/code_quality_gutter_icon.vue'; -import createDiffsStore from 'jest/diffs/create_diffs_store'; +import store from '~/mr_notes/stores'; import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants'; import { fiveFindings, @@ -11,23 +10,24 @@ import { singularFinding, } from '../../../../../spec/frontend/diffs/mock_data/diff_code_quality'; -Vue.use(Vuex); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); let wrapper; const findIcon = () => wrapper.findComponent(GlIcon); const findIcons = () => wrapper.findAllComponents(GlIcon); const findFirstIcon = () => wrapper.findComponent({ ref: 'firstCodeQualityIcon' }); -let store; let codequalityDiff; const createComponent = (props = {}) => { - store = createDiffsStore(); + store.reset(); store.state.diffs.codequalityDiff = codequalityDiff; const payload = { propsData: props, - store, + mocks: { + $store: store, + }, }; wrapper = shallowMountExtended(CodeQualityGutterIcon, payload); diff --git a/package.json b/package.json index 30dafb88268cd082dc7343618fcb36b9d473cb18..fed66bb53c536e9d7a848a3feff720f2fa060952 100644 --- a/package.json +++ b/package.json @@ -279,6 +279,7 @@ "timezone-mock": "^1.0.8", "vue-loader-vue3": "npm:vue-loader@17", "vue-test-utils-compat": "0.0.13", + "vuex-mock-store": "^0.1.0", "webpack-dev-server": "4.15.0", "xhr-mock": "^2.5.1", "yarn-check-webpack-plugin": "^1.2.0", diff --git a/spec/frontend/__helpers__/mocks/mr_notes/stores/index.js b/spec/frontend/__helpers__/mocks/mr_notes/stores/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a983edbbb72633d2e96472e75a370b23ae4354db --- /dev/null +++ b/spec/frontend/__helpers__/mocks/mr_notes/stores/index.js @@ -0,0 +1,15 @@ +import { Store } from 'vuex-mock-store'; +import createDiffState from 'ee_else_ce/diffs/store/modules/diff_state'; +import createNotesState from '~/notes/stores/state'; + +const store = new Store({ + state: { + diffs: createDiffState(), + notes: createNotesState(), + }, + spy: { + create: (handler) => jest.fn(handler).mockImplementation(() => Promise.resolve()), + }, +}); + +export default store; diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js index a19a72af813ad4c18035ed839ee8cf2c48054941..191586e44cc6152ad7f25b33d2d9e425213c319c 100644 --- a/spec/frontend/batch_comments/components/preview_item_spec.js +++ b/spec/frontend/batch_comments/components/preview_item_spec.js @@ -1,29 +1,33 @@ import { mount } from '@vue/test-utils'; import PreviewItem from '~/batch_comments/components/preview_item.vue'; -import { createStore } from '~/batch_comments/stores'; -import diffsModule from '~/diffs/store/modules'; -import notesModule from '~/notes/stores/modules'; +import store from '~/mr_notes/stores'; import { createDraft } from '../mock_data'; jest.mock('~/behaviors/markdown/render_gfm'); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); describe('Batch comments draft preview item component', () => { let wrapper; let draft; - function createComponent(isLast = false, extra = {}, extendStore = () => {}) { - const store = createStore(); - store.registerModule('diffs', diffsModule()); - store.registerModule('notes', notesModule()); + beforeEach(() => { + store.reset(); - extendStore(store); + store.getters.getDiscussion = jest.fn(() => null); + }); + function createComponent(isLast = false, extra = {}) { draft = { ...createDraft(), ...extra, }; - wrapper = mount(PreviewItem, { store, propsData: { draft, isLast } }); + wrapper = mount(PreviewItem, { + mocks: { + $store: store, + }, + propsData: { draft, isLast }, + }); } it('renders text content', () => { @@ -87,18 +91,19 @@ describe('Batch comments draft preview item component', () => { describe('for thread', () => { beforeEach(() => { - createComponent(false, { discussion_id: '1', resolve_discussion: true }, (store) => { - store.state.notes.discussions.push({ - id: '1', - notes: [ - { - author: { - name: "Author 'Nick' Name", - }, + store.getters.getDiscussion.mockReturnValue({ + id: '1', + notes: [ + { + author: { + name: "Author 'Nick' Name", }, - ], - }); + }, + ], }); + store.getters.isDiscussionResolved = jest.fn().mockReturnValue(false); + + createComponent(false, { discussion_id: '1', resolve_discussion: true }); }); it('renders title', () => { @@ -114,9 +119,7 @@ describe('Batch comments draft preview item component', () => { describe('for new comment', () => { it('renders title', () => { - createComponent(false, {}, (store) => { - store.state.notes.discussions.push({}); - }); + createComponent(); expect(wrapper.find('.review-preview-item-header-text').text()).toContain('Your new comment'); }); diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js index 47a266c2e365dee74ba436f14bf8d865c3b55033..cbbfd88260bb3b3d14e526fcfbf56c6f42619a02 100644 --- a/spec/frontend/diffs/components/compare_versions_spec.js +++ b/spec/frontend/diffs/components/compare_versions_spec.js @@ -1,15 +1,14 @@ import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; +import { nextTick } from 'vue'; import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import { trimText } from 'helpers/text_helper'; import CompareVersionsComponent from '~/diffs/components/compare_versions.vue'; -import { createStore } from '~/mr_notes/stores'; +import store from '~/mr_notes/stores'; import diffsMockData from '../mock_data/merge_request_diffs'; -Vue.use(Vuex); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); const NEXT_COMMIT_URL = `${TEST_HOST}/?commit_id=next`; const PREV_COMMIT_URL = `${TEST_HOST}/?commit_id=prev`; @@ -20,8 +19,6 @@ beforeEach(() => { describe('CompareVersions', () => { let wrapper; - let store; - let dispatchMock; const targetBranchName = 'tmp-wine-dev'; const { commit } = getDiffWithCommit; @@ -30,10 +27,10 @@ describe('CompareVersions', () => { store.state.diffs.commit = { ...store.state.diffs.commit, ...commitArgs }; } - dispatchMock = jest.spyOn(store, 'dispatch'); - wrapper = mount(CompareVersionsComponent, { - store, + mocks: { + $store: store, + }, propsData: { mergeRequestDiffs: diffsMockData, diffFilesCountText: '1', @@ -50,8 +47,25 @@ describe('CompareVersions', () => { getCommitNavButtonsElement().find('.btn-group > *:first-child'); beforeEach(() => { - store = createStore(); + store.reset(); + const mergeRequestDiff = diffsMockData[0]; + const version = { + ...mergeRequestDiff, + href: `${TEST_HOST}/latest/version`, + versionName: 'latest version', + }; + store.getters['diffs/diffCompareDropdownSourceVersions'] = [version]; + store.getters['diffs/diffCompareDropdownTargetVersions'] = [ + { + ...version, + selected: true, + versionName: targetBranchName, + }, + ]; + store.getters['diffs/whichCollapsedTypes'] = { any: false }; + store.getters['diffs/isInlineView'] = false; + store.getters['diffs/isParallelView'] = false; store.state.diffs.addedLines = 10; store.state.diffs.removedLines = 20; @@ -104,7 +118,6 @@ describe('CompareVersions', () => { it('should not render Tree List toggle button when there are no changes', () => { createWrapper(); - const treeListBtn = wrapper.find('.js-toggle-tree-list'); expect(treeListBtn.exists()).toBe(false); @@ -118,7 +131,10 @@ describe('CompareVersions', () => { const viewTypeBtn = wrapper.find('#inline-diff-btn'); viewTypeBtn.trigger('click'); - expect(window.location.toString()).toContain('?view=inline'); + expect(store.dispatch).toHaveBeenCalledWith( + 'diffs/setInlineDiffViewType', + expect.any(MouseEvent), + ); }); }); @@ -128,13 +144,16 @@ describe('CompareVersions', () => { const viewTypeBtn = wrapper.find('#parallel-diff-btn'); viewTypeBtn.trigger('click'); - expect(window.location.toString()).toContain('?view=parallel'); + expect(store.dispatch).toHaveBeenCalledWith( + 'diffs/setParallelDiffViewType', + expect.any(MouseEvent), + ); }); }); describe('commit', () => { beforeEach(() => { - store.state.diffs.commit = getDiffWithCommit.commit; + store.state.diffs.commit = commit; createWrapper(); }); @@ -218,7 +237,7 @@ describe('CompareVersions', () => { link.trigger('click'); await nextTick(); - expect(dispatchMock).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', { + expect(store.dispatch).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', { direction: 'previous', }); }); @@ -248,7 +267,7 @@ describe('CompareVersions', () => { link.trigger('click'); await nextTick(); - expect(dispatchMock).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', { + expect(store.dispatch).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', { direction: 'next', }); }); diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js index eb895bd90573ac08ab5498793a40bc8d016eeb9a..e42b98e4d68bd5213e89ec75d714a1f18c52549e 100644 --- a/spec/frontend/diffs/components/diff_line_note_form_spec.js +++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js @@ -1,8 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import Vuex from 'vuex'; import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; -import { createModules } from '~/mr_notes/stores'; +import store from '~/mr_notes/stores'; import NoteForm from '~/notes/components/note_form.vue'; import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; @@ -10,51 +9,25 @@ import { noteableDataMock } from 'jest/notes/mock_data'; import { getDiffFileMock } from '../mock_data/diff_file'; jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); describe('DiffLineNoteForm', () => { let wrapper; let diffFile; let diffLines; - let actions; - let store; - const getSelectedLine = () => { - const lineCode = diffLines[1].line_code; - return diffFile.highlighted_diff_lines.find((l) => l.line_code === lineCode); - }; - - const createStore = (state) => { - const modules = createModules(); - modules.diffs.actions = { - ...modules.diffs.actions, - saveDiffDiscussion: jest.fn(() => Promise.resolve()), - }; - modules.diffs.getters = { - ...modules.diffs.getters, - diffCompareDropdownTargetVersions: jest.fn(), - diffCompareDropdownSourceVersions: jest.fn(), - selectedSourceIndex: jest.fn(), - }; - modules.notes.getters = { - ...modules.notes.getters, - noteableType: jest.fn(), - }; - actions = modules.diffs.actions; + beforeEach(() => { + diffFile = getDiffFileMock(); + diffLines = diffFile.highlighted_diff_lines; - store = new Vuex.Store({ modules }); - store.state.notes.userData.id = 1; store.state.notes.noteableData = noteableDataMock; - store.replaceState({ ...store.state, ...state }); - }; + store.getters.isLoggedIn = jest.fn().mockReturnValue(true); + store.getters['diffs/getDiffFileByHash'] = jest.fn().mockReturnValue(diffFile); + }); - const createComponent = ({ props, state } = {}) => { + const createComponent = ({ props } = {}) => { wrapper?.destroy(); - diffFile = getDiffFileMock(); - diffLines = diffFile.highlighted_diff_lines; - - createStore(state); - store.state.diffs.diffFiles = [diffFile]; const propsData = { diffFileHash: diffFile.file_hash, @@ -66,7 +39,9 @@ describe('DiffLineNoteForm', () => { }; wrapper = shallowMount(DiffLineNoteForm, { - store, + mocks: { + $store: store, + }, propsData, }); }; @@ -129,7 +104,10 @@ describe('DiffLineNoteForm', () => { expect(confirmAction).toHaveBeenCalled(); await nextTick(); - expect(getSelectedLine().hasForm).toBe(false); + expect(store.dispatch).toHaveBeenCalledWith('diffs/cancelCommentForm', { + lineCode: diffLines[1].line_code, + fileHash: diffFile.file_hash, + }); }); }); @@ -157,6 +135,10 @@ describe('DiffLineNoteForm', () => { }); describe('saving note', () => { + beforeEach(() => { + store.getters.noteableType = 'merge-request'; + }); + it('should save original line', async () => { const lineRange = { start: { @@ -172,20 +154,65 @@ describe('DiffLineNoteForm', () => { old_line: null, }, }; - await findNoteForm().vm.$emit('handleFormUpdate', 'note body'); - expect(actions.saveDiffDiscussion.mock.calls[0][1].formData).toMatchObject({ - lineRange, + + const noteBody = 'note body'; + await findNoteForm().vm.$emit('handleFormUpdate', noteBody); + + expect(store.dispatch).toHaveBeenCalledWith('diffs/saveDiffDiscussion', { + note: noteBody, + formData: { + noteableData: noteableDataMock, + noteableType: store.getters.noteableType, + noteTargetLine: diffLines[1], + diffViewType: store.state.diffs.diffViewType, + diffFile, + linePosition: '', + lineRange, + }, + }); + expect(store.dispatch).toHaveBeenCalledWith('diffs/cancelCommentForm', { + lineCode: diffLines[1].line_code, + fileHash: diffFile.file_hash, }); }); it('should save selected line from the store', async () => { const lineCode = 'test'; store.state.notes.selectedCommentPosition = { start: { line_code: lineCode } }; - createComponent({ state: store.state }); - await findNoteForm().vm.$emit('handleFormUpdate', 'note body'); - expect(actions.saveDiffDiscussion.mock.calls[0][1].formData.lineRange.start.line_code).toBe( - lineCode, - ); + createComponent(); + const noteBody = 'note body'; + + await findNoteForm().vm.$emit('handleFormUpdate', noteBody); + + expect(store.dispatch).toHaveBeenCalledWith('diffs/saveDiffDiscussion', { + note: noteBody, + formData: { + noteableData: noteableDataMock, + noteableType: store.getters.noteableType, + noteTargetLine: diffLines[1], + diffViewType: store.state.diffs.diffViewType, + diffFile, + linePosition: '', + lineRange: { + start: { + line_code: lineCode, + new_line: undefined, + old_line: undefined, + type: undefined, + }, + end: { + line_code: diffLines[1].line_code, + new_line: diffLines[1].new_line, + old_line: diffLines[1].old_line, + type: diffLines[1].type, + }, + }, + }, + }); + expect(store.dispatch).toHaveBeenCalledWith('diffs/cancelCommentForm', { + lineCode: diffLines[1].line_code, + fileHash: diffFile.file_hash, + }); }); }); }); diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js index e637b1dd43db08e92f032007e636c791215fbade..fd89d52a59e0e8c368268db3014faed03dbfaf5a 100644 --- a/spec/frontend/diffs/components/no_changes_spec.js +++ b/spec/frontend/diffs/components/no_changes_spec.js @@ -1,55 +1,53 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; import NoChanges from '~/diffs/components/no_changes.vue'; -import { createStore } from '~/mr_notes/stores'; +import store from '~/mr_notes/stores'; import diffsMockData from '../mock_data/merge_request_diffs'; -Vue.use(Vuex); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); const TEST_TARGET_BRANCH = 'foo'; const TEST_SOURCE_BRANCH = 'dev/update'; +const latestVersionNumber = Math.max(...diffsMockData.map((version) => version.version_index)); describe('Diff no changes empty state', () => { - let wrapper; - let store; - - function createComponent(mountFn = shallowMount) { - wrapper = mountFn(NoChanges, { - store, + const createComponent = (mountFn = shallowMount) => + mountFn(NoChanges, { + mocks: { + $store: store, + }, propsData: { changesEmptyStateIllustration: '', }, }); - } beforeEach(() => { - store = createStore(); - store.state.diffs.mergeRequestDiff = {}; - store.state.notes.noteableData = { + store.reset(); + + store.getters.getNoteableData = { target_branch: TEST_TARGET_BRANCH, source_branch: TEST_SOURCE_BRANCH, }; - store.state.diffs.mergeRequestDiffs = diffsMockData; + store.getters['diffs/diffCompareDropdownSourceVersions'] = []; + store.getters['diffs/diffCompareDropdownTargetVersions'] = []; }); - const findMessage = () => wrapper.find('[data-testid="no-changes-message"]'); + const findMessage = (wrapper) => wrapper.find('[data-testid="no-changes-message"]'); it('prevents XSS', () => { - store.state.notes.noteableData = { + store.getters.getNoteableData = { source_branch: '<script>alert("test");</script>', target_branch: '<script>alert("test");</script>', }; - createComponent(); + const wrapper = createComponent(); expect(wrapper.find('script').exists()).toBe(false); }); describe('Renders', () => { it('Show create commit button', () => { - createComponent(); + const wrapper = createComponent(); expect(wrapper.findComponent(GlButton).exists()).toBe(true); }); @@ -64,15 +62,28 @@ describe('Diff no changes empty state', () => { 'renders text "$expectedText" (sourceIndex=$sourceIndex and targetIndex=$targetIndex)', ({ expectedText, targetIndex, sourceIndex }) => { if (targetIndex !== null) { - store.state.diffs.startVersion = { version_index: targetIndex }; + store.getters['diffs/diffCompareDropdownTargetVersions'] = [ + { + selected: true, + version_index: targetIndex, + versionName: `version ${targetIndex}`, + }, + ]; } if (sourceIndex !== null) { - store.state.diffs.mergeRequestDiff.version_index = sourceIndex; + store.getters['diffs/diffCompareDropdownSourceVersions'] = [ + { + isLatestVersion: sourceIndex === latestVersionNumber, + selected: true, + version_index: targetIndex, + versionName: `version ${sourceIndex}`, + }, + ]; } - createComponent(mount); + const wrapper = createComponent(mount); - expect(findMessage().text()).toBe(expectedText); + expect(findMessage(wrapper).text()).toBe(expectedText); }, ); }); diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js index 3d2bbe437467e54d9b5d131d9ae0ae208bd7b45a..cbd2ae3e525eb45e0a91f5d7f1e7324f6e133b49 100644 --- a/spec/frontend/diffs/components/settings_dropdown_spec.js +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -5,44 +5,34 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; import eventHub from '~/diffs/event_hub'; +import store from '~/mr_notes/stores'; -import createDiffsStore from '../create_diffs_store'; +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); describe('Diff settings dropdown component', () => { - let wrapper; - let store; - - function createComponent(extendStore = () => {}) { - store = createDiffsStore(); - - extendStore(store); - - wrapper = extendedWrapper( + const createComponent = () => + extendedWrapper( mount(SettingsDropdown, { - store, + mocks: { + $store: store, + }, }), ); - } function getFileByFileCheckbox(vueWrapper) { return vueWrapper.findByTestId('file-by-file'); } - function setup({ storeUpdater } = {}) { - createComponent(storeUpdater); - jest.spyOn(store, 'dispatch').mockImplementation(() => {}); - } - beforeEach(() => { - setup(); - }); + store.reset(); - afterEach(() => { - store.dispatch.mockRestore(); + store.getters['diffs/isInlineView'] = false; + store.getters['diffs/isParallelView'] = false; }); describe('tree view buttons', () => { it('list view button dispatches setRenderTreeList with false', () => { + const wrapper = createComponent(); wrapper.find('.js-list-view').trigger('click'); expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', { @@ -51,6 +41,7 @@ describe('Diff settings dropdown component', () => { }); it('tree view button dispatches setRenderTreeList with true', () => { + const wrapper = createComponent(); wrapper.find('.js-tree-view').trigger('click'); expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', { @@ -59,19 +50,18 @@ describe('Diff settings dropdown component', () => { }); it('sets list button as selected when renderTreeList is false', () => { - setup({ - storeUpdater: (origStore) => - Object.assign(origStore.state.diffs, { renderTreeList: false }), - }); + store.state.diffs = { renderTreeList: false }; + + const wrapper = createComponent(); expect(wrapper.find('.js-list-view').classes('selected')).toBe(true); expect(wrapper.find('.js-tree-view').classes('selected')).toBe(false); }); it('sets tree button as selected when renderTreeList is true', () => { - setup({ - storeUpdater: (origStore) => Object.assign(origStore.state.diffs, { renderTreeList: true }), - }); + store.state.diffs = { renderTreeList: true }; + + const wrapper = createComponent(); expect(wrapper.find('.js-list-view').classes('selected')).toBe(false); expect(wrapper.find('.js-tree-view').classes('selected')).toBe(true); @@ -80,32 +70,36 @@ describe('Diff settings dropdown component', () => { describe('compare changes', () => { it('sets inline button as selected', () => { - setup({ - storeUpdater: (origStore) => - Object.assign(origStore.state.diffs, { diffViewType: INLINE_DIFF_VIEW_TYPE }), - }); + store.state.diffs = { diffViewType: INLINE_DIFF_VIEW_TYPE }; + store.getters['diffs/isInlineView'] = true; + + const wrapper = createComponent(); expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(true); expect(wrapper.find('.js-parallel-diff-button').classes('selected')).toBe(false); }); it('sets parallel button as selected', () => { - setup({ - storeUpdater: (origStore) => - Object.assign(origStore.state.diffs, { diffViewType: PARALLEL_DIFF_VIEW_TYPE }), - }); + store.state.diffs = { diffViewType: PARALLEL_DIFF_VIEW_TYPE }; + store.getters['diffs/isParallelView'] = true; + + const wrapper = createComponent(); expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(false); expect(wrapper.find('.js-parallel-diff-button').classes('selected')).toBe(true); }); it('calls setInlineDiffViewType when clicking inline button', () => { + const wrapper = createComponent(); + wrapper.find('.js-inline-diff-button').trigger('click'); expect(store.dispatch).toHaveBeenCalledWith('diffs/setInlineDiffViewType', expect.anything()); }); it('calls setParallelDiffViewType when clicking parallel button', () => { + const wrapper = createComponent(); + wrapper.find('.js-parallel-diff-button').trigger('click'); expect(store.dispatch).toHaveBeenCalledWith( @@ -117,23 +111,23 @@ describe('Diff settings dropdown component', () => { describe('whitespace toggle', () => { it('does not set as checked when showWhitespace is false', () => { - setup({ - storeUpdater: (origStore) => - Object.assign(origStore.state.diffs, { showWhitespace: false }), - }); + store.state.diffs = { showWhitespace: false }; + + const wrapper = createComponent(); expect(wrapper.findByTestId('show-whitespace').element.checked).toBe(false); }); it('sets as checked when showWhitespace is true', () => { - setup({ - storeUpdater: (origStore) => Object.assign(origStore.state.diffs, { showWhitespace: true }), - }); + store.state.diffs = { showWhitespace: true }; + + const wrapper = createComponent(); expect(wrapper.findByTestId('show-whitespace').element.checked).toBe(true); }); it('calls setShowWhitespace on change', async () => { + const wrapper = createComponent(); const checkbox = wrapper.findByTestId('show-whitespace'); const { checked } = checkbox.element; @@ -157,10 +151,9 @@ describe('Diff settings dropdown component', () => { `( 'sets the checkbox to { checked: $checked } if the fileByFile setting is $fileByFile', ({ fileByFile, checked }) => { - setup({ - storeUpdater: (origStore) => - Object.assign(origStore.state.diffs, { viewDiffsFileByFile: fileByFile }), - }); + store.state.diffs = { viewDiffsFileByFile: fileByFile }; + + const wrapper = createComponent(); expect(getFileByFileCheckbox(wrapper).element.checked).toBe(checked); }, @@ -173,11 +166,9 @@ describe('Diff settings dropdown component', () => { `( 'when the file by file setting starts as $start, toggling the checkbox should call setFileByFile with $setting', async ({ start, setting }) => { - setup({ - storeUpdater: (origStore) => - Object.assign(origStore.state.diffs, { viewDiffsFileByFile: start }), - }); + store.state.diffs = { viewDiffsFileByFile: start }; + const wrapper = createComponent(); await getFileByFileCheckbox(wrapper).setChecked(setting); expect(store.dispatch).toHaveBeenCalledWith('diffs/setFileByFile', { diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index f883aea764fd203e6d45b6fe17f5b5e01656fdf0..f084458b5c77681d26b3be3c60698f85f3061e82 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -707,6 +707,7 @@ describe('DiffsStoreActions', () => { [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }], [], ); + expect(window.location.toString()).toContain('?view=inline'); expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE); }); }); @@ -720,6 +721,7 @@ describe('DiffsStoreActions', () => { [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], [], ); + expect(window.location.toString()).toContain('?view=parallel'); expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE); }); }); diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js index ff772040d22d384d0d16046ffaeb6f35ad8b84a4..cbe2810a44395146bda2108faad60ead5154188e 100644 --- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js +++ b/spec/frontend/issuable/components/issuable_header_warnings_spec.js @@ -1,15 +1,13 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createStore as createMrStore } from '~/mr_notes/stores'; +import mrStore from '~/mr_notes/stores'; import createIssueStore from '~/notes/stores'; import IssuableHeaderWarnings from '~/issuable/components/issuable_header_warnings.vue'; const ISSUABLE_TYPE_ISSUE = 'issue'; const ISSUABLE_TYPE_MR = 'merge_request'; -Vue.use(Vuex); +jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores')); describe('IssuableHeaderWarnings', () => { let wrapper; @@ -22,7 +20,9 @@ describe('IssuableHeaderWarnings', () => { const createComponent = ({ store, provide }) => { wrapper = shallowMountExtended(IssuableHeaderWarnings, { - store, + mocks: { + $store: store, + }, provide, directives: { GlTooltip: createMockDirective('gl-tooltip'), @@ -47,9 +47,14 @@ describe('IssuableHeaderWarnings', () => { `( `when locked=$lockStatus, confidential=$confidentialStatus, and hidden=$hiddenStatus`, ({ lockStatus, confidentialStatus, hiddenStatus }) => { - const store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : createMrStore(); + const store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : mrStore; beforeEach(() => { + // TODO: simplify to single assignment after issue store is mock + if (store === mrStore) { + store.getters.getNoteableData = {}; + } + store.getters.getNoteableData.confidential = confidentialStatus; store.getters.getNoteableData.discussion_locked = lockStatus; store.getters.getNoteableData.targetType = issuableType; diff --git a/yarn.lock b/yarn.lock index 9c9a98d41842fc67a17ec1d43403bc5aff17a7a2..645a7f2b97f25c588bea77cbf99ecb2552bab6d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12826,6 +12826,13 @@ vuedraggable@^2.23.0: dependencies: sortablejs "^1.9.0" +vuex-mock-store@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/vuex-mock-store/-/vuex-mock-store-0.1.0.tgz#eddd85d32184e93926b3693866ab43efdba95c9d" + integrity sha512-NWbZcw91sxDiCsFt4v6jSF6lr7GUBaVmdLvkYOIXIKtfbEUIN3OO+LuByaAzjmrskr5Xg7QQLueteQZoFLMBoA== + dependencies: + lodash.clonedeep "^4.5.0" + "vuex-vue3@npm:vuex@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0.tgz#ac877aa76a9c45368c979471e461b520d38e6cf5"