diff options
Diffstat (limited to 'spec/frontend')
5 files changed, 299 insertions, 83 deletions
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index 72e1149a01c..9b9a1a84b1d 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -1,7 +1,11 @@ -import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import waitForPromises from 'helpers/wait_for_promises'; +import { sprintf } from '~/locale'; +import { createAlert } from '~/alert'; import DiffContentComponent from 'jh_else_ce/diffs/components/diff_content.vue'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; @@ -11,19 +15,33 @@ import { EVT_EXPAND_ALL_FILES, EVT_PERF_MARK_DIFF_FILES_END, EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, + FILE_DIFF_POSITION_TYPE, } from '~/diffs/constants'; import eventHub from '~/diffs/event_hub'; -import createDiffsStore from '~/diffs/store/modules'; import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import axios from '~/lib/utils/axios_utils'; import { scrollToElement } from '~/lib/utils/common_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import createNotesStore from '~/notes/stores/modules'; +import diffsModule from '~/diffs/store/modules'; +import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n'; +import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import { getDiffFileMock } from '../mock_data/diff_file'; import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable'; +import diffsMockData from '../mock_data/merge_request_diffs'; jest.mock('~/lib/utils/common_utils'); +jest.mock('~/alert'); +jest.mock('~/notes/mixins/diff_line_note_form', () => ({ + methods: { + addToReview: jest.fn(), + }, +})); + +Vue.use(Vuex); + +const saveDiffDiscussionMock = jest.fn(); function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) { const file = store.state.diffs.diffFiles[index]; @@ -70,18 +88,29 @@ function markFileToBeRendered(store, index = 0) { } function createComponent({ file, first = false, last = false, options = {}, props = {} }) { - Vue.use(Vuex); + const diffs = diffsModule(); + diffs.actions = { + ...diffs.actions, + saveDiffDiscussion: saveDiffDiscussionMock, + }; + + diffs.getters = { + ...diffs.getters, + diffCompareDropdownTargetVersions: () => [], + diffCompareDropdownSourceVersions: () => [], + }; const store = new Vuex.Store({ ...createNotesStore(), - modules: { - diffs: createDiffsStore(), - }, + modules: { diffs }, }); - store.state.diffs.diffFiles = [file]; + store.state.diffs = { + mergeRequestDiff: diffsMockData[0], + diffFiles: [file], + }; - const wrapper = shallowMount(DiffFileComponent, { + const wrapper = shallowMountExtended(DiffFileComponent, { store, propsData: { file, @@ -101,9 +130,10 @@ function createComponent({ file, first = false, last = false, options = {}, prop } const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent); -const findDiffContentArea = (wrapper) => wrapper.find('[data-testid="content-area"]'); -const findLoader = (wrapper) => wrapper.find('[data-testid="loader-icon"]'); -const findToggleButton = (wrapper) => wrapper.find('[data-testid="expand-button"]'); +const findDiffContentArea = (wrapper) => wrapper.findByTestId('content-area'); +const findLoader = (wrapper) => wrapper.findByTestId('loader-icon'); +const findToggleButton = (wrapper) => wrapper.findByTestId('expand-button'); +const findNoteForm = (wrapper) => wrapper.findByTestId('file-note-form'); const toggleFile = (wrapper) => findDiffHeader(wrapper).vm.$emit('toggleFile'); const getReadableFile = () => getDiffFileMock(); @@ -118,6 +148,12 @@ const makeFileManuallyCollapsed = (store, index = 0) => const changeViewerType = (store, newType, index = 0) => changeViewer(store, index, { name: diffViewerModes[newType] }); +const triggerSaveNote = (wrapper, note, parent, error) => + findNoteForm(wrapper).vm.$emit('handleFormUpdate', note, parent, error); + +const triggerSaveDraftNote = (wrapper, note, parent, error) => + findNoteForm(wrapper).vm.$emit('handleFormUpdateAddToReview', note, false, parent, error); + describe('DiffFile', () => { let wrapper; let store; @@ -502,7 +538,7 @@ describe('DiffFile', () => { await nextTick(); - const button = wrapper.find('[data-testid="blob-button"]'); + const button = wrapper.findByTestId('blob-button'); expect(wrapper.text()).toContain('Changes are too large to be shown.'); expect(button.html()).toContain('View file @'); @@ -538,7 +574,7 @@ describe('DiffFile', () => { ({ wrapper, store } = createComponent({ file })); - expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(false); + expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(false); }); it('renders conflict alert when conflict_type is present', () => { @@ -550,7 +586,7 @@ describe('DiffFile', () => { ({ wrapper, store } = createComponent({ file })); - expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(true); + expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(true); }); }); @@ -574,7 +610,7 @@ describe('DiffFile', () => { file, })); - expect(wrapper.find('[data-testid="file-discussions"]').exists()).toEqual(exists); + expect(wrapper.findByTestId('file-discussions').exists()).toEqual(exists); }, ); @@ -594,7 +630,7 @@ describe('DiffFile', () => { file, })); - expect(wrapper.find('[data-testid="file-note-form"]').exists()).toEqual(exists); + expect(findNoteForm(wrapper).exists()).toEqual(exists); }, ); @@ -612,7 +648,97 @@ describe('DiffFile', () => { file, })); - expect(wrapper.find('[data-testid="diff-file-discussions"]').exists()).toEqual(exists); + expect(wrapper.findByTestId('diff-file-discussions').exists()).toEqual(exists); + }); + + describe('when note-form emits `handleFormUpdate`', () => { + const file = { + ...getReadableFile(), + hasCommentForm: true, + }; + + const note = {}; + const parentElement = null; + const errorCallback = jest.fn(); + + beforeEach(() => { + ({ wrapper, store } = createComponent({ + file, + options: { provide: { glFeatures: { commentOnFiles: true } } }, + })); + }); + + it('calls saveDiffDiscussionMock', () => { + triggerSaveNote(wrapper, note, parentElement, errorCallback); + + expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), { + note, + formData: { + noteableData: expect.any(Object), + diffFile: file, + positionType: FILE_DIFF_POSITION_TYPE, + noteableType: store.getters.noteableType, + }, + }); + }); + + describe('when saveDiffDiscussionMock throws an error', () => { + describe.each` + scenario | serverError | message + ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED} + ${'without server error'} | ${{}} | ${SOMETHING_WENT_WRONG} + `('$scenario', ({ serverError, message }) => { + beforeEach(async () => { + saveDiffDiscussionMock.mockRejectedValue({ response: serverError }); + + triggerSaveNote(wrapper, note, parentElement, errorCallback); + + await waitForPromises(); + }); + + it(`renders ${serverError ? 'server' : 'generic'} error message`, () => { + expect(createAlert).toHaveBeenCalledWith({ + message: sprintf(message, { reason: serverError?.data?.errors }), + parent: parentElement, + }); + }); + + it('calls errorCallback', () => { + expect(errorCallback).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('when note-form emits `handleFormUpdateAddToReview`', () => { + const file = { + ...getReadableFile(), + hasCommentForm: true, + }; + + const note = {}; + const parentElement = null; + const errorCallback = jest.fn(); + + beforeEach(async () => { + ({ wrapper, store } = createComponent({ + file, + options: { provide: { glFeatures: { commentOnFiles: true } } }, + })); + + triggerSaveDraftNote(wrapper, note, parentElement, errorCallback); + + await nextTick(); + }); + + it('calls addToReview mixin', () => { + expect(diffLineNoteFormMixin.methods.addToReview).toHaveBeenCalledWith( + note, + FILE_DIFF_POSITION_TYPE, + parentElement, + errorCallback, + ); + }); }); }); }); diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js index 630b8feafbc..50e3f2d0f37 100644 --- a/spec/frontend/projects/commits/components/author_select_spec.js +++ b/spec/frontend/projects/commits/components/author_select_spec.js @@ -1,12 +1,17 @@ -import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; +import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; import setWindowLocation from 'helpers/set_window_location_helper'; -import * as urlUtility from '~/lib/utils/url_utility'; import AuthorSelect from '~/projects/commits/components/author_select.vue'; import { createStore } from '~/projects/commits/store'; +import { visitUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); Vue.use(Vuex); @@ -44,6 +49,10 @@ describe('Author Select', () => { propsData: { projectCommitsEl: document.querySelector('.js-project-commits-show'), }, + stubs: { + GlCollapsibleListbox, + GlListboxItem, + }, }); }; @@ -58,11 +67,9 @@ describe('Author Select', () => { resetHTMLFixture(); }); - const findDropdownContainer = () => wrapper.findComponent({ ref: 'dropdownContainer' }); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findListboxContainer = () => wrapper.findComponent({ ref: 'listboxContainer' }); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findListboxItems = () => wrapper.findAllComponents(GlListboxItem); describe('user is searching via "filter by commit message"', () => { beforeEach(() => { @@ -70,24 +77,28 @@ describe('Author Select', () => { createComponent(); }); - it('does not disable dropdown container', () => { - expect(findDropdownContainer().attributes('disabled')).toBeUndefined(); + it('does not disable listbox container', () => { + expect(findListboxContainer().attributes('disabled')).toBeUndefined(); }); it('has correct tooltip message', () => { - expect(findDropdownContainer().attributes('title')).toBe( + expect(findListboxContainer().attributes('title')).toBe( 'Searching by both author and message is currently not supported.', ); }); - it('disables dropdown', () => { - expect(findDropdown().attributes('disabled')).toBeDefined(); + it('disables listbox', () => { + expect(findListbox().attributes('disabled')).toBeDefined(); }); }); - describe('dropdown', () => { + describe('listbox', () => { + beforeEach(() => { + store.state.commitsPath = commitsPath; + }); + it('displays correct default text', () => { - expect(findDropdown().attributes('text')).toBe('Author'); + expect(findListbox().props('toggleText')).toBe('Author'); }); it('displays the current selected author', async () => { @@ -95,81 +106,62 @@ describe('Author Select', () => { createComponent(); await nextTick(); - expect(findDropdown().attributes('text')).toBe(currentAuthor); + expect(findListbox().props('toggleText')).toBe(currentAuthor); }); it('displays correct header text', () => { - expect(findDropdownHeader().text()).toBe('Search by author'); + expect(findListbox().props('headerText')).toBe('Search by author'); }); it('does not have popover text by default', () => { expect(wrapper.attributes('title')).toBeUndefined(); }); + + it('passes selected author to redirectPath', () => { + const redirectPath = `${commitsPath}?author=${currentAuthor}`; + + findListbox().vm.$emit('select', currentAuthor); + + expect(visitUrl).toHaveBeenCalledWith(redirectPath); + }); + + it('does not pass any author to redirectPath', () => { + const redirectPath = commitsPath; + + findListbox().vm.$emit('select', ''); + + expect(visitUrl).toHaveBeenCalledWith(redirectPath); + }); }); - describe('dropdown search box', () => { + describe('listbox search box', () => { it('has correct placeholder', () => { - expect(findSearchBox().attributes('placeholder')).toBe('Search'); + expect(findListbox().props('searchPlaceholder')).toBe('Search'); }); it('fetch authors on input change', () => { const authorName = 'lorem'; - findSearchBox().vm.$emit('input', authorName); + findListbox().vm.$emit('search', authorName); expect(store.actions.fetchAuthors).toHaveBeenCalledWith(expect.anything(), authorName); }); }); - describe('dropdown list', () => { + describe('listbox list', () => { beforeEach(() => { store.state.commitsAuthors = authors; - store.state.commitsPath = commitsPath; }); it('has a "Any Author" as the first list item', () => { - expect(findDropdownItems().at(0).text()).toBe('Any Author'); + expect(findListboxItems().at(0).text()).toBe('Any Author'); }); it('displays the project authors', () => { - expect(findDropdownItems()).toHaveLength(authors.length + 1); - }); - - it('has the correct props', async () => { - setWindowLocation(`?author=${currentAuthor}`); - createComponent(); - - const [{ avatar_url: avatarUrl, username }] = authors; - const result = { - avatarUrl, - secondaryText: username, - isChecked: true, - }; - - await nextTick(); - expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result)); + expect(findListboxItems()).toHaveLength(authors.length + 1); }); it("display the author's name", () => { - expect(findDropdownItems().at(1).text()).toBe(currentAuthor); - }); - - it('passes selected author to redirectPath', () => { - const redirectToUrl = `${commitsPath}?author=${currentAuthor}`; - const spy = jest.spyOn(urlUtility, 'redirectTo'); - spy.mockImplementation(() => 'mock'); - - findDropdownItems().at(1).vm.$emit('click'); - - expect(spy).toHaveBeenCalledWith(redirectToUrl); - }); - - it('does not pass any author to redirectPath', () => { - const redirectToUrl = commitsPath; - const spy = jest.spyOn(urlUtility, 'redirectTo'); - spy.mockImplementation(); - - findDropdownItems().at(0).vm.$emit('click'); - expect(spy).toHaveBeenCalledWith(redirectToUrl); + expect(findListboxItems().at(1).text()).toContain(currentAuthor); }); }); }); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index f4baa817d32..46a7f2ee1bb 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -1,6 +1,6 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlDropdown } from '@gitlab/ui'; +import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; @@ -11,6 +11,7 @@ import permissionsQuery from 'shared_queries/repository/permissions.query.graphq import projectPathQuery from '~/repository/queries/project_path.query.graphql'; import createApolloProvider from 'helpers/mock_apollo_helper'; +import { __ } from '~/locale'; const defaultMockRoute = { name: 'blobPath', @@ -61,6 +62,7 @@ describe('Repository breadcrumbs component', () => { }, stubs: { RouterLink: RouterLinkStub, + GlDisclosureDropdown, }, mocks: { $route: { @@ -71,7 +73,8 @@ describe('Repository breadcrumbs component', () => { }); }; - const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal); const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub); @@ -146,7 +149,11 @@ describe('Repository breadcrumbs component', () => { `( 'does render add to tree dropdown $isRendered when route is $routeName', ({ routeName, isRendered }) => { - factory('app/assets/javascripts.js', { canCollaborate: true }, { name: routeName }); + factory( + 'app/assets/javascripts.js', + { canCollaborate: true, canEditTree: true }, + { name: routeName }, + ); expect(findDropdown().exists()).toBe(isRendered); }, ); @@ -156,7 +163,7 @@ describe('Repository breadcrumbs component', () => { createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }), ); - factory('/', { canCollaborate: true }); + factory('/', { canCollaborate: true, canEditTree: true }); await nextTick(); expect(findDropdown().exists()).toBe(true); @@ -193,4 +200,32 @@ describe('Repository breadcrumbs component', () => { expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir'); }); }); + + describe('"this repository" dropdown group', () => { + it('renders when user has pushCode permissions', async () => { + permissionsQuerySpy.mockResolvedValue( + createPermissionsQueryResponse({ + pushCode: true, + }), + ); + + factory('/', { canCollaborate: true }); + await waitForPromises(); + + expect(findDropdownGroup().props('group').name).toBe(__('This repository')); + }); + + it('does not render when user does not have pushCode permissions', async () => { + permissionsQuerySpy.mockResolvedValue( + createPermissionsQueryResponse({ + pushCode: false, + }), + ); + + factory('/', { canCollaborate: true }); + await waitForPromises(); + + expect(findDropdownGroup().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index 6af1172e4d8..c92f8a68678 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -104,7 +104,7 @@ describe('HelpCenter component', () => { createWrapper({ ...sidebarData, show_tanuki_bot: true }); }); - it('shows Ask GitLab Chat with the help items', () => { + it('shows Ask GitLab Duo with the help items', () => { expect(findDropdownGroup(0).props('group').items).toEqual([ expect.objectContaining({ icon: 'tanuki-ai', @@ -115,9 +115,9 @@ describe('HelpCenter component', () => { ]); }); - describe('when Ask GitLab Chat button is clicked', () => { + describe('when Ask GitLab Duo button is clicked', () => { beforeEach(() => { - findButton('Ask GitLab Chat').click(); + findButton('Ask GitLab Duo').click(); }); it('sets helpCenterState.showTanukiBotChatDrawer to true', () => { diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js new file mode 100644 index 00000000000..2179b2e489e --- /dev/null +++ b/spec/frontend/tracking/internal_events_spec.js @@ -0,0 +1,63 @@ +import API from '~/api'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import InternalEvents from '~/tracking/internal_events'; +import { GITLAB_INTERNAL_EVENT_CATEGORY, SERVICE_PING_SCHEMA } from '~/tracking/constants'; + +jest.mock('~/api', () => ({ + trackRedisHllUserEvent: jest.fn(), +})); + +describe('InternalEvents', () => { + describe('track_event', () => { + it('track_event calls trackRedisHllUserEvent with correct arguments', () => { + const event = 'TestEvent'; + + InternalEvents.track_event(event); + + expect(API.trackRedisHllUserEvent).toHaveBeenCalledTimes(1); + expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event); + }); + + it('track_event calls tracking.event functions with correct arguments', () => { + const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn); + + const event = 'TestEvent'; + + InternalEvents.track_event(event); + + expect(trackingSpy).toHaveBeenCalledTimes(1); + expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, { + context: { + schema: SERVICE_PING_SCHEMA, + data: { + event_name: event, + data_source: 'redis_hll', + }, + }, + }); + }); + }); + + describe('mixin', () => { + let wrapper; + + beforeEach(() => { + const Component = { + render() {}, + mixins: [InternalEvents.mixin()], + }; + wrapper = shallowMountExtended(Component); + }); + + it('this.track_event function calls InternalEvent`s track function with an event', () => { + const event = 'TestEvent'; + const trackEventSpy = jest.spyOn(InternalEvents, 'track_event'); + + wrapper.vm.track_event(event); + + expect(trackEventSpy).toHaveBeenCalledTimes(1); + expect(trackEventSpy).toHaveBeenCalledWith(event); + }); + }); +}); |