From f34077e88198da754b4efecd1ce1d996ce982286 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 5 Jul 2022 12:09:46 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- spec/events/projects/project_deleted_event_spec.rb | 1 + .../search/user_uses_header_search_field_spec.rb | 4 +- .../components/design_sidebar_spec.js | 61 +-- .../pages/design/__snapshots__/index_spec.js.snap | 67 +-- .../diffs/components/diff_code_quality_spec.js | 66 +++ spec/frontend/diffs/components/diff_view_spec.js | 32 +- spec/frontend/diffs/mock_data/diff_code_quality.js | 62 +++ .../groups/components/group_folder_spec.js | 79 ++-- spec/frontend/groups/mock_data.js | 20 - spec/frontend/header_search/components/app_spec.js | 263 ++++++++--- .../components/header_search_scoped_items_spec.js | 47 +- spec/frontend/header_search/mock_data.js | 75 ++- spec/frontend/header_search/store/getters_spec.js | 8 +- .../components/commit_sidebar/empty_state_spec.js | 26 +- .../ide/components/commit_sidebar/list_spec.js | 56 +-- .../commit_sidebar/success_message_spec.js | 30 +- spec/frontend/ide/components/ide_tree_list_spec.js | 78 ++- spec/frontend/ide/components/repo_editor_spec.js | 9 +- .../issues/list/components/issues_list_app_spec.js | 61 ++- spec/frontend/issues/list/utils_spec.js | 71 +-- .../frontend/issues/show/components/edited_spec.js | 83 ++-- .../logs/components/environment_logs_spec.js | 370 --------------- .../logs/components/log_advanced_filters_spec.js | 175 ------- .../logs/components/log_simple_filters_spec.js | 134 ------ spec/frontend/logs/stores/actions_spec.js | 521 --------------------- spec/frontend/logs/stores/getters_spec.js | 75 --- spec/frontend/notebook/cells/prompt_spec.js | 42 +- .../components/note_signed_out_widget_spec.js | 37 +- spec/frontend/pdf/index_spec.js | 39 +- .../components/page_size_selector_spec.js | 44 ++ .../list/components/issuable_list_root_spec.js | 23 + spec/helpers/search_helper_spec.rb | 2 +- spec/lib/bulk_imports/pipeline/runner_spec.rb | 155 ++++-- spec/lib/bulk_imports/retry_pipeline_error_spec.rb | 13 + .../error_repository/open_api_strategy_spec.rb | 8 +- .../merge_request_activity_unique_counter_spec.rb | 34 +- .../issues/related_branches_service_spec.rb | 2 +- .../merge_requests/approval_service_spec.rb | 2 +- spec/services/projects/destroy_service_spec.rb | 8 +- spec/workers/bulk_imports/pipeline_worker_spec.rb | 66 +-- 40 files changed, 1017 insertions(+), 1932 deletions(-) create mode 100644 spec/frontend/diffs/components/diff_code_quality_spec.js create mode 100644 spec/frontend/diffs/mock_data/diff_code_quality.js delete mode 100644 spec/frontend/logs/components/environment_logs_spec.js delete mode 100644 spec/frontend/logs/components/log_advanced_filters_spec.js delete mode 100644 spec/frontend/logs/components/log_simple_filters_spec.js delete mode 100644 spec/frontend/logs/stores/actions_spec.js delete mode 100644 spec/frontend/logs/stores/getters_spec.js create mode 100644 spec/frontend/vue_shared/components/page_size_selector_spec.js create mode 100644 spec/lib/bulk_imports/retry_pipeline_error_spec.rb (limited to 'spec') diff --git a/spec/events/projects/project_deleted_event_spec.rb b/spec/events/projects/project_deleted_event_spec.rb index fd8cec7271b..c3de2b22224 100644 --- a/spec/events/projects/project_deleted_event_spec.rb +++ b/spec/events/projects/project_deleted_event_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Projects::ProjectDeletedEvent do where(:data, :valid) do [ [{ project_id: 1, namespace_id: 2 }, true], + [{ project_id: 1, namespace_id: 2, root_namespace_id: 3 }, true], [{ project_id: 1 }, false], [{ namespace_id: 1 }, false], [{ project_id: 'foo', namespace_id: 2 }, false], diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 7350a54e8df..1523586ab26 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -153,6 +153,7 @@ RSpec.describe 'User uses header search field', :js do it 'displays search options' do fill_in_search('test') + expect(page).to have_selector(scoped_search_link('test', search_code: true)) expect(page).to have_selector(scoped_search_link('test', group_id: group.id, search_code: true)) expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id, search_code: true)) @@ -167,6 +168,7 @@ RSpec.describe 'User uses header search field', :js do it 'displays search options' do fill_in_search('test') + sleep 0.5 expect(page).to have_selector(scoped_search_link('test', search_code: true, repository_ref: 'master')) expect(page).not_to have_selector(scoped_search_link('test', search_code: true, group_id: project.namespace_id, repository_ref: 'master')) expect(page).to have_selector(scoped_search_link('test', search_code: true, project_id: project.id, repository_ref: 'master')) @@ -184,7 +186,7 @@ RSpec.describe 'User uses header search field', :js do fill_in_search('Feature') within(dashboard_search_options_popup_menu) do - expect(page).to have_text('"Feature" in all GitLab') + expect(page).to have_text('Feature in all GitLab') expect(page).to have_no_text('Feature Flags') end end diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js index 40968d9204a..f13796138bd 100644 --- a/spec/frontend/design_management/components/design_sidebar_spec.js +++ b/spec/frontend/design_management/components/design_sidebar_spec.js @@ -1,7 +1,6 @@ -import { GlCollapse, GlPopover } from '@gitlab/ui'; +import { GlAccordionItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import Cookies from '~/lib/utils/cookies'; import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue'; import DesignSidebar from '~/design_management/components/design_sidebar.vue'; @@ -27,8 +26,6 @@ const $route = { }, }; -const cookieKey = 'hide_design_resolved_comments_popover'; - const mutate = jest.fn().mockResolvedValue(); describe('Design management design sidebar component', () => { @@ -40,9 +37,7 @@ describe('Design management design sidebar component', () => { const findUnresolvedDiscussions = () => wrapper.findAll('[data-testid="unresolved-discussion"]'); const findResolvedDiscussions = () => wrapper.findAll('[data-testid="resolved-discussion"]'); const findParticipants = () => wrapper.find(Participants); - const findCollapsible = () => wrapper.find(GlCollapse); - const findToggleResolvedCommentsButton = () => wrapper.find('[data-testid="resolved-comments"]'); - const findPopover = () => wrapper.find(GlPopover); + const findResolvedCommentsToggle = () => wrapper.find(GlAccordionItem); const findNewDiscussionDisclaimer = () => wrapper.find('[data-testid="new-discussion-disclaimer"]'); @@ -61,7 +56,6 @@ describe('Design management design sidebar component', () => { mutate, }, }, - stubs: { GlPopover }, provide: { registerPath: '/users/sign_up?redirect_to_referer=yes', signInPath: '/users/sign_in?redirect_to_referer=yes', @@ -119,7 +113,6 @@ describe('Design management design sidebar component', () => { describe('when has discussions', () => { beforeEach(() => { - Cookies.set(cookieKey, true); createComponent(); }); @@ -131,26 +124,23 @@ describe('Design management design sidebar component', () => { expect(findResolvedDiscussions()).toHaveLength(1); }); - it('has resolved comments collapsible collapsed', () => { - expect(findCollapsible().attributes('visible')).toBeUndefined(); + it('has resolved comments accordion item collapsed', () => { + expect(findResolvedCommentsToggle().props('visible')).toBe(false); }); - it('emits toggleResolveComments event on resolve comments button click', () => { - findToggleResolvedCommentsButton().vm.$emit('click'); + it('emits toggleResolveComments event on resolve comments button click', async () => { + findResolvedCommentsToggle().vm.$emit('input', true); + await nextTick(); expect(wrapper.emitted('toggleResolvedComments')).toHaveLength(1); }); - it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', async () => { - expect(findCollapsible().attributes('visible')).toBeUndefined(); + it('opens the accordion item when resolvedDiscussionsExpanded prop changes to true', async () => { + expect(findResolvedCommentsToggle().props('visible')).toBe(false); wrapper.setProps({ resolvedDiscussionsExpanded: true, }); await nextTick(); - expect(findCollapsible().attributes('visible')).toBe('true'); - }); - - it('does not popover about resolved comments', () => { - expect(findPopover().exists()).toBe(false); + expect(findResolvedCommentsToggle().props('visible')).toBe(true); }); it('sends a mutation to set an active discussion when clicking on a discussion', () => { @@ -232,36 +222,6 @@ describe('Design management design sidebar component', () => { }); }); - describe('when showing resolved discussions for the first time', () => { - beforeEach(() => { - Cookies.set(cookieKey, false); - createComponent(); - }); - - it('renders a popover if we show resolved comments collapsible for the first time', () => { - expect(findPopover().exists()).toBe(true); - }); - - it('scrolls to resolved threads link', () => { - expect(scrollIntoViewMock).toHaveBeenCalled(); - }); - - it('dismisses a popover on the outside click', async () => { - wrapper.trigger('click'); - await nextTick(); - expect(findPopover().exists()).toBe(false); - }); - - it(`sets a ${cookieKey} cookie on clicking outside the popover`, () => { - jest.spyOn(Cookies, 'set'); - wrapper.trigger('click'); - expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { - expires: 365 * 10, - secure: false, - }); - }); - }); - describe('when user is not logged in', () => { const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut); @@ -292,7 +252,6 @@ describe('Design management design sidebar component', () => { describe('design has discussions', () => { beforeEach(() => { - Cookies.set(cookieKey, true); createComponent(); }); diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap index 0f2857821ea..3177a5e016c 100644 --- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap @@ -88,57 +88,26 @@ exports[`Design management design index page renders design index 1`] = ` signinpath="" /> - - Resolved Comments (1) - - - - -

- - Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below - -

- - - Learn more about resolving comments - -
- - - - + + + diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js new file mode 100644 index 00000000000..81a817c47dc --- /dev/null +++ b/spec/frontend/diffs/components/diff_code_quality_spec.js @@ -0,0 +1,66 @@ +import { GlIcon } from '@gitlab/ui'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue'; +import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/reports/codequality_report/constants'; +import { multipleFindingsArr } from '../mock_data/diff_code_quality'; + +let wrapper; + +const findIcon = () => wrapper.findComponent(GlIcon); + +describe('DiffCodeQuality', () => { + afterEach(() => { + wrapper.destroy(); + }); + + const createWrapper = (codeQuality, mountFunction = mountExtended) => { + return mountFunction(DiffCodeQuality, { + propsData: { + expandedLines: [], + line: 1, + codeQuality, + }, + }); + }; + + it('hides details and throws hideCodeQualityFindings event on close click', async () => { + wrapper = createWrapper(multipleFindingsArr); + expect(wrapper.findByTestId('diff-codequality').exists()).toBe(true); + + await wrapper.findByTestId('diff-codequality-close').trigger('click'); + + expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1); + expect(wrapper.emitted().hideCodeQualityFindings[0][0]).toBe(wrapper.props('line')); + }); + + it('renders correct amount of list items for codequality array and their description', async () => { + wrapper = createWrapper(multipleFindingsArr); + const listItems = wrapper.findAll('li'); + + expect(wrapper.findAll('li').length).toBe(3); + + listItems.wrappers.map((e, i) => { + return expect(e.text()).toEqual(multipleFindingsArr[i].description); + }); + }); + + it.each` + severity + ${'info'} + ${'minor'} + ${'major'} + ${'critical'} + ${'blocker'} + ${'unknown'} + `('shows icon for $severity degradation', ({ severity }) => { + wrapper = createWrapper([{ severity }], shallowMountExtended); + + expect(findIcon().exists()).toBe(true); + + expect(findIcon().attributes()).toMatchObject({ + class: `codequality-severity-icon ${SEVERITY_CLASSES[severity]}`, + name: SEVERITY_ICONS[severity], + size: '12', + }); + }); +}); diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js index dfbe30e460b..15923a1c6de 100644 --- a/spec/frontend/diffs/components/diff_view_spec.js +++ b/spec/frontend/diffs/components/diff_view_spec.js @@ -1,7 +1,9 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DiffView from '~/diffs/components/diff_view.vue'; +import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue'; +import { diffCodeQuality } from '../mock_data/diff_code_quality'; describe('DiffView', () => { const DiffExpansionCell = { template: `
` }; @@ -12,7 +14,7 @@ describe('DiffView', () => { const setSelectedCommentPosition = jest.fn(); const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm; - const createWrapper = (props) => { + const createWrapper = (props, provide = {}) => { Vue.use(Vuex); const batchComments = { @@ -46,9 +48,33 @@ describe('DiffView', () => { ...props, }; const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote }; - return shallowMount(DiffView, { propsData, store, stubs }); + return shallowMount(DiffView, { propsData, store, stubs, provide }); }; + it('does not render a codeQuality diff view when there is no finding', () => { + const wrapper = createWrapper(); + expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(false); + }); + + it('does render a codeQuality diff view with the correct props when there is a finding & refactorCodeQualityInlineFindings flag is true ', async () => { + const wrapper = createWrapper(diffCodeQuality, { + glFeatures: { refactorCodeQualityInlineFindings: true }, + }); + wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2); + await nextTick(); + expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(true); + expect(wrapper.findComponent(DiffCodeQuality).props().codeQuality.length).not.toBe(0); + }); + + it('does not render a codeQuality diff view when there is a finding & refactorCodeQualityInlineFindings flag is false ', async () => { + const wrapper = createWrapper(diffCodeQuality, { + glFeatures: { refactorCodeQualityInlineFindings: false }, + }); + wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2); + await nextTick(); + expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(false); + }); + it.each` type | side | container | sides | total ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2} diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js new file mode 100644 index 00000000000..2ca421a20b4 --- /dev/null +++ b/spec/frontend/diffs/mock_data/diff_code_quality.js @@ -0,0 +1,62 @@ +export const multipleFindingsArr = [ + { + severity: 'minor', + description: 'Unexpected Debugger Statement.', + line: 2, + }, + { + severity: 'major', + description: + 'Function `aVeryLongFunction` has 52 lines of code (exceeds 25 allowed). Consider refactoring.', + line: 3, + }, + { + severity: 'minor', + description: 'Arrow function has too many statements (52). Maximum allowed is 30.', + line: 3, + }, +]; + +export const multipleFindings = { + filePath: 'index.js', + codequality: multipleFindingsArr, +}; + +export const singularFinding = { + filePath: 'index.js', + codequality: [multipleFindingsArr[0]], +}; + +export const diffCodeQuality = { + diffFile: { file_hash: '123' }, + diffLines: [ + { + left: { + type: 'old', + old_line: 1, + new_line: null, + codequality: [], + lineDraft: {}, + }, + }, + { + left: { + type: null, + old_line: 2, + new_line: 1, + codequality: [], + lineDraft: {}, + }, + }, + { + left: { + type: 'new', + old_line: null, + new_line: 2, + + codequality: [multipleFindingsArr[0]], + lineDraft: {}, + }, + }, + ], +}; diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js index 98b7c2dd6c6..f223333360d 100644 --- a/spec/frontend/groups/components/group_folder_spec.js +++ b/spec/frontend/groups/components/group_folder_spec.js @@ -1,65 +1,50 @@ -import Vue, { nextTick } from 'vue'; - -import groupFolderComponent from '~/groups/components/group_folder.vue'; -import groupItemComponent from '~/groups/components/group_item.vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import GroupFolder from '~/groups/components/group_folder.vue'; +import GroupItem from '~/groups/components/group_item.vue'; +import { MAX_CHILDREN_COUNT } from '~/groups/constants'; import { mockGroups, mockParentGroupItem } from '../mock_data'; -const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem) => { - const Component = Vue.extend(groupFolderComponent); - - return new Component({ - propsData: { - groups, - parentGroup, - }, - }); -}; +describe('GroupFolder component', () => { + let wrapper; -describe('GroupFolderComponent', () => { - let vm; + Vue.component('GroupItem', GroupItem); - beforeEach(async () => { - Vue.component('GroupItem', groupItemComponent); + const findLink = () => wrapper.find('a'); - vm = createComponent(); - vm.$mount(); - - await nextTick(); - }); + const createComponent = ({ groups = mockGroups, parentGroup = mockParentGroupItem } = {}) => + shallowMount(GroupFolder, { + propsData: { + groups, + parentGroup, + }, + }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - describe('computed', () => { - describe('hasMoreChildren', () => { - it('should return false when childrenCount of group is less than MAX_CHILDREN_COUNT', () => { - expect(vm.hasMoreChildren).toBeFalsy(); - }); - }); + it('does not render more children stats link when children count of group is under limit', () => { + wrapper = createComponent(); - describe('moreChildrenStats', () => { - it('should return message with count of excess children over MAX_CHILDREN_COUNT limit', () => { - expect(vm.moreChildrenStats).toBe('3 more items'); - }); - }); + expect(findLink().exists()).toBe(false); }); - describe('template', () => { - it('should render component template correctly', () => { - expect(vm.$el.classList.contains('group-list-tree')).toBeTruthy(); - expect(vm.$el.querySelectorAll('li.group-row').length).toBe(7); + it('renders text of count of excess children when children count of group is over limit', () => { + const childrenCount = MAX_CHILDREN_COUNT + 1; + wrapper = createComponent({ + parentGroup: { + ...mockParentGroupItem, + childrenCount, + }, }); - it('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => { - const parentGroup = { ...mockParentGroupItem }; - parentGroup.childrenCount = 21; + expect(findLink().text()).toBe(`${childrenCount} more items`); + }); - const newVm = createComponent(mockGroups, parentGroup); - newVm.$mount(); + it('renders group items', () => { + wrapper = createComponent(); - expect(newVm.$el.querySelector('li.group-row a.has-more-items')).toBeDefined(); - newVm.$destroy(); - }); + expect(wrapper.findAllComponents(GroupItem)).toHaveLength(7); }); }); diff --git a/spec/frontend/groups/mock_data.js b/spec/frontend/groups/mock_data.js index 603cb27deec..65a62876893 100644 --- a/spec/frontend/groups/mock_data.js +++ b/spec/frontend/groups/mock_data.js @@ -5,26 +5,6 @@ export const ITEM_TYPE = { GROUP: 'group', }; -export const GROUP_VISIBILITY_TYPE = { - public: 'Public - The group and any public projects can be viewed without any authentication.', - internal: - 'Internal - The group and any internal projects can be viewed by any logged in user except external users.', - private: 'Private - The group and its projects can only be viewed by members.', -}; - -export const PROJECT_VISIBILITY_TYPE = { - public: 'Public - The project can be accessed without any authentication.', - internal: 'Internal - The project can be accessed by any logged in user except external users.', - private: - 'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', -}; - -export const VISIBILITY_TYPE_ICON = { - public: 'earth', - internal: 'shield', - private: 'lock', -}; - export const mockParentGroupItem = { id: 55, name: 'hardware', diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index f0de5b083ae..5f2b71a22c5 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -1,22 +1,32 @@ -import { GlSearchBoxByType } from '@gitlab/ui'; +import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__, sprintf } from '~/locale'; import HeaderSearchApp from '~/header_search/components/app.vue'; import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; -import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } from '~/header_search/constants'; +import { + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, + SEARCH_BOX_INDEX, + ICON_PROJECT, + ICON_GROUP, + ICON_SUBGROUP, + SCOPE_TOKEN_MAX_LENGTH, +} from '~/header_search/constants'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { ENTER_KEY } from '~/lib/utils/keys'; import { visitUrl } from '~/lib/utils/url_utility'; +import { truncate } from '~/lib/utils/text_utility'; import { MOCK_SEARCH, MOCK_SEARCH_QUERY, MOCK_USERNAME, MOCK_DEFAULT_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_OPTIONS, - MOCK_SORTED_AUTOCOMPLETE_OPTIONS, + MOCK_SEARCH_CONTEXT_FULL, } from '../mock_data'; Vue.use(Vuex); @@ -52,11 +62,26 @@ describe('HeaderSearchApp', () => { }); }; + const formatScopeName = (scopeName) => { + if (!scopeName) { + return false; + } + const searchResultsScope = s__('GlobalSearch|in %{scope}'); + return truncate( + sprintf(searchResultsScope, { + scope: scopeName, + }), + SCOPE_TOKEN_MAX_LENGTH, + ); + }; + afterEach(() => { wrapper.destroy(); }); + const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form'); const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findScopeToken = () => wrapper.findComponent(GlToken); const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu'); const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems); const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); @@ -106,53 +131,38 @@ describe('HeaderSearchApp', () => { }); describe.each` - search | showDefault | showScoped | showAutocomplete | showDropdownNavigation - ${null} | ${true} | ${false} | ${false} | ${true} - ${''} | ${true} | ${false} | ${false} | ${true} - ${'1'} | ${false} | ${false} | ${false} | ${false} - ${')'} | ${false} | ${false} | ${false} | ${false} - ${'t'} | ${false} | ${false} | ${true} | ${true} - ${'te'} | ${false} | ${true} | ${true} | ${true} - ${'tes'} | ${false} | ${true} | ${true} | ${true} - ${MOCK_SEARCH} | ${false} | ${true} | ${true} | ${true} - `( - 'Header Search Dropdown Items', - ({ search, showDefault, showScoped, showAutocomplete, showDropdownNavigation }) => { - describe(`when search is ${search}`, () => { - beforeEach(() => { - window.gon.current_username = MOCK_USERNAME; - createComponent( - { search }, - { - autocompleteGroupedSearchOptions: () => - search.match(/^[A-Za-z]+$/g) ? MOCK_SORTED_AUTOCOMPLETE_OPTIONS : [], - }, - ); - findHeaderSearchInput().vm.$emit('click'); - }); + search | showDefault | showScoped | showAutocomplete + ${null} | ${true} | ${false} | ${false} + ${''} | ${true} | ${false} | ${false} + ${'t'} | ${false} | ${false} | ${true} + ${'te'} | ${false} | ${false} | ${true} + ${'tes'} | ${false} | ${true} | ${true} + ${MOCK_SEARCH} | ${false} | ${true} | ${true} + `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => { + describe(`when search is ${search}`, () => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent({ search }, {}); + findHeaderSearchInput().vm.$emit('click'); + }); - it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { - expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); - }); + it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { + expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); + }); - it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { - expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); - }); + it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { + expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + }); - it(`should${ - showAutocomplete ? '' : ' not' - } render the Autocomplete Dropdown Items`, () => { - expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); - }); + it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => { + expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); + }); - it(`should${ - showDropdownNavigation ? '' : ' not' - } render the Dropdown Navigation Component`, () => { - expect(findDropdownKeyboardNavigation().exists()).toBe(showDropdownNavigation); - }); + it(`should render the Dropdown Navigation Component`, () => { + expect(findDropdownKeyboardNavigation().exists()).toBe(true); }); - }, - ); + }); + }); describe.each` username | showDropdown | expectedDesc @@ -185,12 +195,18 @@ describe('HeaderSearchApp', () => { `( 'Search Results Description', ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { - describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${ - Boolean(username) && showDropdown - }`, () => { + describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => { beforeEach(() => { window.gon.current_username = username; - createComponent({ search, loading }, { searchOptions: () => searchOptions }); + createComponent( + { + search, + loading, + }, + { + searchOptions: () => searchOptions, + }, + ); findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); }); @@ -200,6 +216,121 @@ describe('HeaderSearchApp', () => { }); }, ); + + describe('input box', () => { + describe.each` + search | searchOptions | hasToken + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true} + ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false} + ${'x'} | ${[]} | ${false} + `('token', ({ search, searchOptions, hasToken }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + }); + + it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${ + searchOptions[0]?.html_id + }"`, () => { + expect(findScopeToken().exists()).toBe(hasToken); + }); + + it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${ + searchOptions[0]?.scope || searchOptions[0]?.description + }"`, () => { + expect(findScopeToken().exists() && findScopeToken().text()).toBe( + formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description), + ); + }); + }); + }); + + describe('form wrapper', () => { + describe.each` + searchContext | search | searchOptions + ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${null} | ${[]} + `('', ({ searchContext, search, searchOptions }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + + createComponent({ search, searchContext }, { searchOptions: () => searchOptions }); + + findHeaderSearchInput().vm.$emit('click'); + }); + + const hasIcon = Boolean(searchContext?.group); + const isSearching = Boolean(search); + const isActive = Boolean(searchOptions.length > 0); + + it(`${hasIcon ? 'with' : 'without'} search context classes contain "${ + hasIcon ? 'has-icon' : 'has-no-icon' + }"`, () => { + const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + + it(`${isSearching ? 'with' : 'without'} search string classes contain "${ + isSearching ? 'is-searching' : 'is-not-searching' + }"`, () => { + const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + + it(`${isActive ? 'with' : 'without'} search results classes contain "${ + isActive ? 'is-active' : 'is-not-active' + }"`, () => { + const iconClassRegex = isActive ? 'is-active' : 'is-not-active'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + }); + }); + + describe.each` + search | searchOptions | hasIcon | iconName + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false} + `('token', ({ search, searchOptions, hasIcon, iconName }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + }); + + it(`icon for data set type "${searchOptions[0]?.html_id}" ${ + hasIcon ? 'is' : 'is NOT' + } rendered`, () => { + expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon); + }); + + it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${ + searchOptions[0]?.html_id + }"`, () => { + expect( + findScopeToken().findComponent(GlIcon).exists() && + findScopeToken().findComponent(GlIcon).attributes('name'), + ).toBe(iconName); + }); + }); }); describe('events', () => { @@ -285,18 +416,20 @@ describe('HeaderSearchApp', () => { }); describe('computed', () => { - describe('currentFocusedOption', () => { - const MOCK_INDEX = 1; - + describe.each` + MOCK_INDEX | search + ${1} | ${null} + ${SEARCH_BOX_INDEX} | ${'test'} + ${2} | ${'test1'} + `('currentFocusedOption', ({ MOCK_INDEX, search }) => { beforeEach(() => { - createComponent(); + createComponent({ search }); window.gon.current_username = MOCK_USERNAME; findHeaderSearchInput().vm.$emit('click'); }); - it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, async () => { + it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => { findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); - await nextTick(); expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]); }); }); @@ -308,15 +441,25 @@ describe('HeaderSearchApp', () => { createComponent(); }); - it('onKey-enter submits a search', async () => { + it('onKey-enter submits a search', () => { findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); - await nextTick(); - expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY); }); }); + describe('with less than min characters and no dropdown results', () => { + beforeEach(() => { + createComponent({ search: 'x' }); + }); + + it('onKey-enter will NOT submit a search', () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + + expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY); + }); + }); + describe('with currentFocusedOption', () => { const MOCK_INDEX = 1; @@ -326,9 +469,9 @@ describe('HeaderSearchApp', () => { findHeaderSearchInput().vm.$emit('click'); }); - it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => { + it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => { findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); - await nextTick(); + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url); }); diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js index 8788fb23458..2db9f71d702 100644 --- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js +++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js @@ -1,9 +1,11 @@ -import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import { trimText } from 'helpers/text_helper'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; +import { truncate } from '~/lib/utils/text_utility'; +import { MSG_IN_ALL_GITLAB, SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants'; import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS, @@ -41,9 +43,12 @@ describe('HeaderSearchScopedItems', () => { }); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findGlDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findScopeTokens = () => wrapper.findAllComponents(GlToken); + const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text())); + const findScopeTokensIcons = () => + findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon)); const findDropdownItemAriaLabels = () => findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label'))); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); @@ -59,15 +64,31 @@ describe('HeaderSearchScopedItems', () => { }); it('renders titles correctly', () => { + findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH)); + }); + + it('renders scope names correctly', () => { const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => - trimText(`"${MOCK_SEARCH}" ${o.description} ${o.scope || ''}`), + truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH), ); - expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + + expect(findScopeTokensText()).toStrictEqual(expectedTitles); + }); + + it('renders scope icons correctly', () => { + findScopeTokensIcons().forEach((icon, i) => { + const w = icon.wrappers[0]; + expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon); + }); + }); + + it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => { + expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false); }); it('renders aria-labels correctly', () => { const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => - trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`), + trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`), ); expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels); }); @@ -98,21 +119,5 @@ describe('HeaderSearchScopedItems', () => { }); }); }); - - describe.each` - autosuggestResults | showDivider - ${[]} | ${false} - ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${true} - `('scoped search items', ({ autosuggestResults, showDivider }) => { - describe(`when when we have ${autosuggestResults.length} auto-sugest results`, () => { - beforeEach(() => { - createComponent({}, { autocompleteGroupedSearchOptions: () => autosuggestResults }, {}); - }); - - it(`divider should${showDivider ? '' : ' not'} be shown`, () => { - expect(findGlDropdownDivider().exists()).toBe(showDivider); - }); - }); - }); }); }); diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js index b6f0fdcc29d..8ccd7fb17e3 100644 --- a/spec/frontend/header_search/mock_data.js +++ b/spec/frontend/header_search/mock_data.js @@ -4,9 +4,12 @@ import { MSG_MR_ASSIGNED_TO_ME, MSG_MR_IM_REVIEWER, MSG_MR_IVE_CREATED, - MSG_IN_PROJECT, - MSG_IN_GROUP, MSG_IN_ALL_GITLAB, + PROJECTS_CATEGORY, + ICON_PROJECT, + GROUPS_CATEGORY, + ICON_GROUP, + ICON_SUBGROUP, } from '~/header_search/constants'; export const MOCK_USERNAME = 'anyone'; @@ -27,12 +30,24 @@ export const MOCK_PROJECT = { path: '/mock-project', }; +export const MOCK_PROJECT_LONG = { + id: 124, + name: 'Mock Project Name That Is Ridiculously Long And It Goes Forever', + path: '/mock-project-name-that-is-ridiculously-long-and-it-goes-forever', +}; + export const MOCK_GROUP = { id: 321, name: 'MockGroup', path: '/mock-group', }; +export const MOCK_SUBGROUP = { + id: 322, + name: 'MockSubGroup', + path: `${MOCK_GROUP}/mock-subgroup`, +}; + export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test'; export const MOCK_SEARCH = 'test'; @@ -44,6 +59,20 @@ export const MOCK_SEARCH_CONTEXT = { group_metadata: {}, }; +export const MOCK_SEARCH_CONTEXT_FULL = { + group: { + id: 31, + name: 'testGroup', + full_name: 'testGroup', + }, + group_metadata: { + group_path: 'testGroup', + name: 'testGroup', + issues_path: '/groups/testGroup/-/issues', + mr_path: '/groups/testGroup/-/merge_requests', + }, +}; + export const MOCK_DEFAULT_SEARCH_OPTIONS = [ { html_id: 'default-issues-assigned', @@ -76,13 +105,51 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [ { html_id: 'scoped-in-project', scope: MOCK_PROJECT.name, - description: MSG_IN_PROJECT, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT.path, + }, + { + html_id: 'scoped-in-project-long', + scope: MOCK_PROJECT_LONG.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, + url: MOCK_PROJECT_LONG.path, + }, + { + html_id: 'scoped-in-group', + scope: MOCK_GROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_GROUP, + url: MOCK_GROUP.path, + }, + { + html_id: 'scoped-in-subgroup', + scope: MOCK_SUBGROUP.name, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_SUBGROUP, + url: MOCK_SUBGROUP.path, + }, + { + html_id: 'scoped-in-all', + description: MSG_IN_ALL_GITLAB, + url: MOCK_ALL_PATH, + }, +]; + +export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [ + { + html_id: 'scoped-in-project', + scope: MOCK_PROJECT.name, + scopeCategory: PROJECTS_CATEGORY, + icon: ICON_PROJECT, url: MOCK_PROJECT.path, }, { html_id: 'scoped-in-group', scope: MOCK_GROUP.name, - description: MSG_IN_GROUP, + scopeCategory: GROUPS_CATEGORY, + icon: ICON_GROUP, url: MOCK_GROUP.path, }, { diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js index d3510de1439..c76be3c0360 100644 --- a/spec/frontend/header_search/store/getters_spec.js +++ b/spec/frontend/header_search/store/getters_spec.js @@ -9,6 +9,7 @@ import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS_DEF, MOCK_PROJECT, MOCK_GROUP, MOCK_ALL_PATH, @@ -284,7 +285,7 @@ describe('Header Search Store Getters', () => { it('returns the correct array', () => { expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual( - MOCK_SCOPED_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS_DEF, ); }); }); @@ -308,6 +309,11 @@ describe('Header Search Store Getters', () => { ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS} ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} + ${1} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]} + ${'('} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]} + ${'t'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${'te'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${'tes'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} `( 'searchOptions', ({ diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js index 4f81c0aa5d3..7c48c0e6f95 100644 --- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js @@ -1,29 +1,21 @@ -import Vue from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import emptyState from '~/ide/components/commit_sidebar/empty_state.vue'; +import { shallowMount } from '@vue/test-utils'; +import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue'; import { createStore } from '~/ide/stores'; -describe('IDE commit panel empty state', () => { - let vm; - let store; +describe('IDE commit panel EmptyState component', () => { + let wrapper; beforeEach(() => { - store = createStore(); - - const Component = Vue.extend(emptyState); - - Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes'); - - vm = createComponentWithStore(Component, store); - - vm.$mount(); + const store = createStore(); + store.state.noChangesStateSvgPath = 'no-changes'; + wrapper = shallowMount(EmptyState, { store }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('renders no changes text when last commit message is empty', () => { - expect(vm.$el.textContent).toContain('No changes'); + expect(wrapper.find('h4').text()).toBe('No changes'); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index 1d42512c9ee..81c81fc0a9f 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -1,51 +1,47 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import { createStore } from '~/ide/stores'; +import { shallowMount } from '@vue/test-utils'; +import CommitSidebarList from '~/ide/components/commit_sidebar/list.vue'; +import ListItem from '~/ide/components/commit_sidebar/list_item.vue'; import { file } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { - let store; - let vm; - - beforeEach(() => { - store = createStore(); - - const Component = Vue.extend(commitSidebarList); - - vm = createComponentWithStore(Component, store, { - title: 'Staged', - fileList: [], - action: 'stageAllChanges', - actionBtnText: 'stage all', - actionBtnIcon: 'history', - activeFileKey: 'staged-testing', - keyPrefix: 'staged', + let wrapper; + + const mountComponent = ({ fileList }) => + shallowMount(CommitSidebarList, { + propsData: { + title: 'Staged', + fileList, + action: 'stageAllChanges', + actionBtnText: 'stage all', + actionBtnIcon: 'history', + activeFileKey: 'staged-testing', + keyPrefix: 'staged', + }, }); - vm.$mount(); - }); - afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('with a list of files', () => { beforeEach(async () => { const f = file('file name'); f.changed = true; - vm.fileList.push(f); - await nextTick(); + wrapper = mountComponent({ fileList: [f] }); }); it('renders list', () => { - expect(vm.$el.querySelectorAll('.multi-file-commit-list > li').length).toBe(1); + expect(wrapper.findAllComponents(ListItem)).toHaveLength(1); }); }); - describe('empty files array', () => { - it('renders no changes text when empty', () => { - expect(vm.$el.textContent).toContain('No changes'); + describe('with empty files array', () => { + beforeEach(() => { + wrapper = mountComponent({ fileList: [] }); + }); + + it('renders no changes text ', () => { + expect(wrapper.text()).toContain('No changes'); }); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js index 52e35bdbb73..63d51953915 100644 --- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js @@ -1,32 +1,22 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import successMessage from '~/ide/components/commit_sidebar/success_message.vue'; +import { shallowMount } from '@vue/test-utils'; +import SuccessMessage from '~/ide/components/commit_sidebar/success_message.vue'; import { createStore } from '~/ide/stores'; describe('IDE commit panel successful commit state', () => { - let vm; - let store; + let wrapper; beforeEach(() => { - store = createStore(); - - const Component = Vue.extend(successMessage); - - vm = createComponentWithStore(Component, store, { - committedStateSvgPath: 'committed-state', - }); - - vm.$mount(); + const store = createStore(); + store.state.committedStateSvgPath = 'committed-state'; + store.state.lastCommitMsg = 'testing commit message'; + wrapper = shallowMount(SuccessMessage, { store }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - it('renders last commit message when it exists', async () => { - vm.$store.state.lastCommitMsg = 'testing commit message'; - - await nextTick(); - expect(vm.$el.textContent).toContain('testing commit message'); + it('renders last commit message when it exists', () => { + expect(wrapper.text()).toContain('testing commit message'); }); }); diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index a85c52f5e86..0f61aa80e53 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -1,82 +1,72 @@ -import Vue, { nextTick } from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import IdeTreeList from '~/ide/components/ide_tree_list.vue'; import { createStore } from '~/ide/stores'; +import FileTree from '~/vue_shared/components/file_tree.vue'; import { file } from '../helpers'; import { projectData } from '../mock_data'; -describe('IDE tree list', () => { - const Component = Vue.extend(IdeTreeList); - const normalBranchTree = [file('fileName')]; - const emptyBranchTree = []; - let vm; - let store; +describe('IdeTreeList component', () => { + let wrapper; - const bootstrapWithTree = (tree = normalBranchTree) => { + const mountComponent = ({ tree, loading = false } = {}) => { + const store = createStore(); store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'main'; store.state.projects.abcproject = { ...projectData }; - Vue.set(store.state.trees, 'abcproject/main', { - tree, - loading: false, - }); + Vue.set(store.state.trees, 'abcproject/main', { tree, loading }); - vm = createComponentWithStore(Component, store, { - viewerType: 'edit', + wrapper = shallowMount(IdeTreeList, { + propsData: { + viewerType: 'edit', + }, + store, }); }; - beforeEach(() => { - store = createStore(); - }); - afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('normal branch', () => { - beforeEach(() => { - bootstrapWithTree(); - - jest.spyOn(vm, '$emit').mockImplementation(() => {}); - - vm.$mount(); - }); + const tree = [file('fileName')]; it('emits tree-ready event', () => { - expect(vm.$emit).toHaveBeenCalledTimes(1); - expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); + mountComponent({ tree }); + + expect(wrapper.emitted('tree-ready')).toEqual([[]]); }); - it('renders loading indicator', async () => { - store.state.trees['abcproject/main'].loading = true; + it('renders loading indicator', () => { + mountComponent({ tree, loading: true }); - await nextTick(); - expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); }); it('renders list of files', () => { - expect(vm.$el.textContent).toContain('fileName'); + mountComponent({ tree }); + + expect(wrapper.findAllComponents(FileTree)).toHaveLength(1); + expect(wrapper.findComponent(FileTree).props('file')).toEqual(tree[0]); }); }); describe('empty-branch state', () => { beforeEach(() => { - bootstrapWithTree(emptyBranchTree); - - jest.spyOn(vm, '$emit').mockImplementation(() => {}); + mountComponent({ tree: [] }); + }); - vm.$mount(); + it('emits tree-ready event', () => { + expect(wrapper.emitted('tree-ready')).toEqual([[]]); }); - it('still emits tree-ready event', () => { - expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); + it('does not render files', () => { + expect(wrapper.findAllComponents(FileTree)).toHaveLength(0); }); - it('does not load files if the branch is empty', () => { - expect(vm.$el.textContent).not.toContain('fileName'); - expect(vm.$el.textContent).toContain('No files'); + it('renders empty state text', () => { + expect(wrapper.text()).toBe('No files'); }); }); }); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index b44651481e9..593fe6bf5a8 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -1,3 +1,4 @@ +import { GlTab } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { editor as monacoEditor, Range } from 'monaco-editor'; import Vue, { nextTick } from 'vue'; @@ -125,7 +126,7 @@ describe('RepoEditor', () => { }; const findEditor = () => wrapper.find('[data-testid="editor-container"]'); - const findTabs = () => wrapper.findAll('.ide-mode-tabs .nav-links li'); + const findTabs = () => wrapper.findAllComponents(GlTab); const findPreviewTab = () => wrapper.find('[data-testid="preview-tab"]'); beforeEach(() => { @@ -201,12 +202,12 @@ describe('RepoEditor', () => { const tabs = findTabs(); expect(tabs).toHaveLength(2); - expect(tabs.at(0).text()).toBe('Edit'); - expect(tabs.at(1).text()).toBe('Preview Markdown'); + expect(tabs.at(0).element.dataset.testid).toBe('edit-tab'); + expect(tabs.at(1).element.dataset.testid).toBe('preview-tab'); }); it('renders markdown for tempFile', async () => { - findPreviewTab().trigger('click'); + findPreviewTab().vm.$emit('click'); await waitForPromises(); expect(wrapper.find(ContentViewer).html()).toContain(dummyFile.text.content); }); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 3f2c3c3ec5f..3d3dbfa6853 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -29,6 +29,7 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue'; + import { CREATED_DESC, RELATIVE_POSITION, @@ -98,6 +99,7 @@ describe('CE IssuesListApp component', () => { }; let defaultQueryResponse = getIssuesQueryResponse; + let router; if (IS_EE) { defaultQueryResponse = cloneDeep(getIssuesQueryResponse); defaultQueryResponse.data.project.issues.nodes[0].blockingCount = 1; @@ -133,9 +135,11 @@ describe('CE IssuesListApp component', () => { [setSortPreferenceMutation, sortPreferenceMutationResponse], ]; + router = new VueRouter({ mode: 'history' }); + return mountFn(IssuesListApp, { apolloProvider: createMockApollo(requestHandlers), - router: new VueRouter({ mode: 'history' }), + router, provide: { ...defaultProvide, ...provide, @@ -736,7 +740,7 @@ describe('CE IssuesListApp component', () => { describe('when "click-tab" event is emitted by IssuableList', () => { beforeEach(() => { wrapper = mountComponent(); - jest.spyOn(wrapper.vm.$router, 'push'); + router.push = jest.fn(); findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); }); @@ -746,16 +750,26 @@ describe('CE IssuesListApp component', () => { }); it('updates url to the new tab', () => { - expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ state: IssuableStates.Closed }), }); }); }); describe.each` - event | params - ${'next-page'} | ${{ page_after: 'endCursor', page_before: undefined, first_page_size: 20, last_page_size: undefined }} - ${'previous-page'} | ${{ page_after: undefined, page_before: 'startCursor', first_page_size: undefined, last_page_size: 20 }} + event | params + ${'next-page'} | ${{ + page_after: 'endCursor', + page_before: undefined, + first_page_size: 20, + last_page_size: undefined, +}} + ${'previous-page'} | ${{ + page_after: undefined, + page_before: 'startCursor', + first_page_size: undefined, + last_page_size: 20, +}} `('when "$event" event is emitted by IssuableList', ({ event, params }) => { beforeEach(() => { wrapper = mountComponent({ @@ -766,7 +780,7 @@ describe('CE IssuesListApp component', () => { }, }, }); - jest.spyOn(wrapper.vm.$router, 'push'); + router.push = jest.fn(); findIssuableList().vm.$emit(event); }); @@ -776,7 +790,7 @@ describe('CE IssuesListApp component', () => { }); it(`updates url`, () => { - expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining(params), }); }); @@ -888,13 +902,13 @@ describe('CE IssuesListApp component', () => { 'updates to the new sort when payload is `%s`', async (sortKey) => { wrapper = mountComponent(); - jest.spyOn(wrapper.vm.$router, 'push'); + router.push = jest.fn(); findIssuableList().vm.$emit('sort', sortKey); jest.runOnlyPendingTimers(); await nextTick(); - expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ sort: urlSortParams[sortKey] }), }); }, @@ -907,13 +921,13 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ provide: { initialSort, isIssueRepositioningDisabled: true }, }); - jest.spyOn(wrapper.vm.$router, 'push'); + router.push = jest.fn(); findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC); }); it('does not update the sort to manual', () => { - expect(wrapper.vm.$router.push).not.toHaveBeenCalled(); + expect(router.push).not.toHaveBeenCalled(); }); it('shows an alert to tell the user that manual reordering is disabled', () => { @@ -978,12 +992,12 @@ describe('CE IssuesListApp component', () => { describe('when "filter" event is emitted by IssuableList', () => { it('updates IssuableList with url params', async () => { wrapper = mountComponent(); - jest.spyOn(wrapper.vm.$router, 'push'); + router.push = jest.fn(); findIssuableList().vm.$emit('filter', filteredTokens); await nextTick(); - expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining(urlParams), }); }); @@ -993,13 +1007,13 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ provide: { isAnonymousSearchDisabled: true, isSignedIn: false }, }); - jest.spyOn(wrapper.vm.$router, 'push'); + router.push = jest.fn(); findIssuableList().vm.$emit('filter', filteredTokens); }); it('does not update url params', () => { - expect(wrapper.vm.$router.push).not.toHaveBeenCalled(); + expect(router.push).not.toHaveBeenCalled(); }); it('shows an alert to tell the user they must be signed in to search', () => { @@ -1030,4 +1044,19 @@ describe('CE IssuesListApp component', () => { expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers })); }); }); + + describe('when "page-size-change" event is emitted by IssuableList', () => { + it('updates url params with new page size', async () => { + wrapper = mountComponent(); + router.push = jest.fn(); + + findIssuableList().vm.$emit('page-size-change', 50); + await nextTick(); + + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith({ + query: expect.objectContaining({ first_page_size: 50 }), + }); + }); + }); }); diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js index 90eab1f3754..3c6332d5728 100644 --- a/spec/frontend/issues/list/utils_spec.js +++ b/spec/frontend/issues/list/utils_spec.js @@ -10,12 +10,7 @@ import { urlParams, urlParamsWithSpecialValues, } from 'jest/issues/list/mock_data'; -import { - PAGE_SIZE, - PAGE_SIZE_MANUAL, - RELATIVE_POSITION_ASC, - urlSortParams, -} from '~/issues/list/constants'; +import { PAGE_SIZE, urlSortParams } from '~/issues/list/constants'; import { convertToApiParams, convertToSearchQuery, @@ -29,52 +24,30 @@ import { import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; describe('getInitialPageParams', () => { - it.each(Object.keys(urlSortParams))( - 'returns the correct page params for sort key %s', - (sortKey) => { - const firstPageSize = sortKey === RELATIVE_POSITION_ASC ? PAGE_SIZE_MANUAL : PAGE_SIZE; + it('returns page params with a default page size when no arguments are given', () => { + expect(getInitialPageParams()).toEqual({ firstPageSize: PAGE_SIZE }); + }); - expect(getInitialPageParams(sortKey)).toEqual({ firstPageSize }); - }, - ); + it('returns page params with the given page size', () => { + const pageSize = 100; + expect(getInitialPageParams(pageSize)).toEqual({ firstPageSize: pageSize }); + }); - it.each(Object.keys(urlSortParams))( - 'returns the correct page params for sort key %s with afterCursor', - (sortKey) => { - const firstPageSize = sortKey === RELATIVE_POSITION_ASC ? PAGE_SIZE_MANUAL : PAGE_SIZE; - const lastPageSize = undefined; - const afterCursor = 'randomCursorString'; - const beforeCursor = undefined; - const pageParams = getInitialPageParams( - sortKey, - firstPageSize, - lastPageSize, - afterCursor, - beforeCursor, - ); - - expect(pageParams).toEqual({ firstPageSize, afterCursor }); - }, - ); + it('does not return firstPageSize when lastPageSize is provided', () => { + const firstPageSize = 100; + const lastPageSize = 50; + const afterCursor = undefined; + const beforeCursor = 'randomCursorString'; + const pageParams = getInitialPageParams( + 100, + firstPageSize, + lastPageSize, + afterCursor, + beforeCursor, + ); - it.each(Object.keys(urlSortParams))( - 'returns the correct page params for sort key %s with beforeCursor', - (sortKey) => { - const firstPageSize = undefined; - const lastPageSize = PAGE_SIZE; - const afterCursor = undefined; - const beforeCursor = 'anotherRandomCursorString'; - const pageParams = getInitialPageParams( - sortKey, - firstPageSize, - lastPageSize, - afterCursor, - beforeCursor, - ); - - expect(pageParams).toEqual({ lastPageSize, beforeCursor }); - }, - ); + expect(pageParams).toEqual({ lastPageSize, beforeCursor }); + }); }); describe('getSortKey', () => { diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js index 8a8fe23230a..8a240c38b5f 100644 --- a/spec/frontend/issues/show/components/edited_spec.js +++ b/spec/frontend/issues/show/components/edited_spec.js @@ -1,49 +1,50 @@ -import Vue from 'vue'; -import edited from '~/issues/show/components/edited.vue'; - -function formatText(text) { - return text.trim().replace(/\s\s+/g, ' '); -} - -describe('edited', () => { - const EditedComponent = Vue.extend(edited); - - it('should render an edited at+by string', () => { - const editedComponent = new EditedComponent({ - propsData: { - updatedAt: '2017-05-15T12:31:04.428Z', - updatedByName: 'Some User', - updatedByPath: '/some_user', - }, - }).$mount(); - - expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited[\s\S]+?by Some User/); - expect(editedComponent.$el.querySelector('.author-link').href).toMatch(/\/some_user$/); - expect(editedComponent.$el.querySelector('time')).toBeTruthy(); +import { shallowMount } from '@vue/test-utils'; +import Edited from '~/issues/show/components/edited.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +describe('Edited component', () => { + let wrapper; + + const findAuthorLink = () => wrapper.find('a'); + const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); + const formatText = (text) => text.trim().replace(/\s\s+/g, ' '); + + const mountComponent = (propsData) => shallowMount(Edited, { propsData }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders an edited at+by string', () => { + wrapper = mountComponent({ + updatedAt: '2017-05-15T12:31:04.428Z', + updatedByName: 'Some User', + updatedByPath: '/some_user', + }); + + expect(formatText(wrapper.text())).toBe('Edited by Some User'); + expect(findAuthorLink().attributes('href')).toBe('/some_user'); + expect(findTimeAgoTooltip().exists()).toBe(true); }); it('if no updatedAt is provided, no time element will be rendered', () => { - const editedComponent = new EditedComponent({ - propsData: { - updatedByName: 'Some User', - updatedByPath: '/some_user', - }, - }).$mount(); - - expect(formatText(editedComponent.$el.innerText)).toMatch(/Edited by Some User/); - expect(editedComponent.$el.querySelector('.author-link').href).toMatch(/\/some_user$/); - expect(editedComponent.$el.querySelector('time')).toBeFalsy(); + wrapper = mountComponent({ + updatedByName: 'Some User', + updatedByPath: '/some_user', + }); + + expect(formatText(wrapper.text())).toBe('Edited by Some User'); + expect(findAuthorLink().attributes('href')).toBe('/some_user'); + expect(findTimeAgoTooltip().exists()).toBe(false); }); it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => { - const editedComponent = new EditedComponent({ - propsData: { - updatedAt: '2017-05-15T12:31:04.428Z', - }, - }).$mount(); - - expect(formatText(editedComponent.$el.innerText)).not.toMatch(/by Some User/); - expect(editedComponent.$el.querySelector('.author-link')).toBeFalsy(); - expect(editedComponent.$el.querySelector('time')).toBeTruthy(); + wrapper = mountComponent({ + updatedAt: '2017-05-15T12:31:04.428Z', + }); + + expect(formatText(wrapper.text())).toBe('Edited'); + expect(findAuthorLink().exists()).toBe(false); + expect(findTimeAgoTooltip().exists()).toBe(true); }); }); diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js deleted file mode 100644 index 84dc0bdf6cd..00000000000 --- a/spec/frontend/logs/components/environment_logs_spec.js +++ /dev/null @@ -1,370 +0,0 @@ -import { GlSprintf, GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { scrollDown } from '~/lib/utils/scroll_utils'; -import EnvironmentLogs from '~/logs/components/environment_logs.vue'; - -import { createStore } from '~/logs/stores'; -import { - mockEnvName, - mockEnvironments, - mockPods, - mockLogsResult, - mockTrace, - mockEnvironmentsEndpoint, - mockDocumentationPath, -} from '../mock_data'; - -jest.mock('~/lib/utils/scroll_utils'); - -const module = 'environmentLogs'; - -jest.mock('lodash/throttle', () => - jest.fn((func) => { - return func; - }), -); - -describe('EnvironmentLogs', () => { - let store; - let dispatch; - let wrapper; - let state; - - const propsData = { - environmentName: mockEnvName, - environmentsPath: mockEnvironmentsEndpoint, - clusterApplicationsDocumentationPath: mockDocumentationPath, - clustersPath: '/gitlab-org', - }; - - const updateControlBtnsMock = jest.fn(); - const LogControlButtonsStub = { - template: '
', - methods: { - update: updateControlBtnsMock, - }, - props: { - scrollDownButtonDisabled: false, - }, - }; - - const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown'); - - const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' }); - const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' }); - const findElasticsearchNotice = () => wrapper.find({ ref: 'elasticsearchNotice' }); - const findLogControlButtons = () => wrapper.find(LogControlButtonsStub); - - const findInfiniteScroll = () => wrapper.find({ ref: 'infiniteScroll' }); - const findLogTrace = () => wrapper.find({ ref: 'logTrace' }); - const findLogFooter = () => wrapper.find({ ref: 'logFooter' }); - const getInfiniteScrollAttr = (attr) => parseInt(findInfiniteScroll().attributes(attr), 10); - - const mockSetInitData = () => { - state.pods.options = mockPods; - state.environments.current = mockEnvName; - [state.pods.current] = state.pods.options; - - state.logs.lines = []; - }; - - const mockShowPodLogs = () => { - state.pods.options = mockPods; - [state.pods.current] = mockPods; - - state.logs.lines = mockLogsResult; - }; - - const mockFetchEnvs = () => { - state.environments.options = mockEnvironments; - }; - - const initWrapper = () => { - wrapper = shallowMount(EnvironmentLogs, { - propsData, - store, - stubs: { - LogControlButtons: LogControlButtonsStub, - GlInfiniteScroll: { - name: 'gl-infinite-scroll', - template: ` -
- - - -
- `, - }, - GlSprintf, - }, - }); - }; - - beforeEach(() => { - store = createStore(); - state = store.state.environmentLogs; - - jest.spyOn(store, 'dispatch').mockResolvedValue(); - - dispatch = store.dispatch; - }); - - afterEach(() => { - store.dispatch.mockReset(); - - if (wrapper) { - wrapper.destroy(); - } - }); - - it('displays UI elements', () => { - initWrapper(); - - expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true); - expect(findSimpleFilters().exists()).toBe(true); - expect(findLogControlButtons().exists()).toBe(true); - - expect(findInfiniteScroll().exists()).toBe(true); - expect(findLogTrace().exists()).toBe(true); - }); - - it('mounted inits data', () => { - initWrapper(); - - expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, { - timeRange: expect.objectContaining({ - default: true, - }), - environmentName: mockEnvName, - podName: null, - }); - - expect(dispatch).toHaveBeenCalledWith(`${module}/fetchEnvironments`, mockEnvironmentsEndpoint); - }); - - describe('loading state', () => { - beforeEach(() => { - state.pods.options = []; - - state.logs.lines = []; - state.logs.isLoading = true; - - state.environments = { - options: [], - isLoading: true, - }; - - initWrapper(); - }); - - it('does not display an alert to upgrade to ES', () => { - expect(findElasticsearchNotice().exists()).toBe(false); - }); - - it('displays a disabled environments dropdown', () => { - expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true'); - expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0); - }); - - it('does not update buttons state', () => { - expect(updateControlBtnsMock).not.toHaveBeenCalled(); - }); - - it('shows an infinite scroll with no content', () => { - expect(getInfiniteScrollAttr('fetched-items')).toBe(0); - }); - - it('shows an infinite scroll container with no set max-height ', () => { - expect(findInfiniteScroll().attributes('max-list-height')).toBeUndefined(); - }); - - it('shows a logs trace', () => { - expect(findLogTrace().text()).toBe(''); - expect(findLogTrace().find('.js-build-loader-animation').isVisible()).toBe(true); - }); - }); - - describe('k8s environment', () => { - beforeEach(() => { - state.pods.options = []; - - state.logs.lines = []; - state.logs.isLoading = false; - - state.environments = { - options: mockEnvironments, - current: 'staging', - isLoading: false, - }; - - initWrapper(); - }); - - it('displays an alert to upgrade to ES', () => { - expect(findElasticsearchNotice().exists()).toBe(true); - }); - - it('displays simple filters for kubernetes logs API', () => { - expect(findSimpleFilters().exists()).toBe(true); - expect(findAdvancedFilters().exists()).toBe(false); - }); - }); - - describe('state with data', () => { - beforeEach(() => { - dispatch.mockImplementation((actionName) => { - if (actionName === `${module}/setInitData`) { - mockSetInitData(); - } else if (actionName === `${module}/showPodLogs`) { - mockShowPodLogs(); - } else if (actionName === `${module}/fetchEnvironments`) { - mockFetchEnvs(); - mockShowPodLogs(); - } - }); - - initWrapper(); - }); - - afterEach(() => { - scrollDown.mockReset(); - updateControlBtnsMock.mockReset(); - }); - - it('does not display an alert to upgrade to ES', () => { - expect(findElasticsearchNotice().exists()).toBe(false); - }); - - it('populates environments dropdown', () => { - const items = findEnvironmentsDropdown().findAll(GlDropdownItem); - expect(findEnvironmentsDropdown().props('text')).toBe(mockEnvName); - expect(items.length).toBe(mockEnvironments.length); - mockEnvironments.forEach((env, i) => { - const item = items.at(i); - expect(item.text()).toBe(env.name); - }); - }); - - it('dropdown has one environment selected', () => { - const items = findEnvironmentsDropdown().findAll(GlDropdownItem); - mockEnvironments.forEach((env, i) => { - const item = items.at(i); - - if (item.text() !== mockEnvName) { - expect(item.find(GlDropdownItem).attributes('ischecked')).toBeFalsy(); - } else { - expect(item.find(GlDropdownItem).attributes('ischecked')).toBeTruthy(); - } - }); - }); - - it('displays advanced filters for elasticsearch logs API', () => { - expect(findSimpleFilters().exists()).toBe(false); - expect(findAdvancedFilters().exists()).toBe(true); - }); - - it('shows infinite scroll with content', () => { - expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length); - }); - - it('populates logs trace', () => { - const trace = findLogTrace(); - expect(trace.text().split('\n').length).toBe(mockTrace.length); - expect(trace.text().split('\n')).toEqual(mockTrace); - }); - - it('populates footer', () => { - const footer = findLogFooter().text(); - - expect(footer).toContain(`${mockLogsResult.length} results`); - }); - - describe('when user clicks', () => { - it('environment name, trace is refreshed', () => { - const items = findEnvironmentsDropdown().findAll(GlDropdownItem); - const index = 1; // any env - - expect(dispatch).not.toHaveBeenCalledWith(`${module}/showEnvironment`, expect.anything()); - - items.at(index).vm.$emit('click'); - - expect(dispatch).toHaveBeenCalledWith( - `${module}/showEnvironment`, - mockEnvironments[index].name, - ); - }); - - it('refresh button, trace is refreshed', () => { - expect(dispatch).not.toHaveBeenCalledWith(`${module}/refreshPodLogs`, undefined); - - findLogControlButtons().vm.$emit('refresh'); - - expect(dispatch).toHaveBeenCalledWith(`${module}/refreshPodLogs`, undefined); - }); - }); - }); - - describe('listeners', () => { - beforeEach(() => { - initWrapper(); - }); - - it('attaches listeners in components', () => { - expect(findInfiniteScroll().vm.$listeners).toEqual({ - topReached: expect.any(Function), - scroll: expect.any(Function), - }); - }); - - it('`topReached` when not loading', () => { - expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); - - findInfiniteScroll().vm.$emit('topReached'); - - expect(store.dispatch).toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); - }); - - it('`topReached` does not fetches more logs when already loading', () => { - state.logs.isLoading = true; - findInfiniteScroll().vm.$emit('topReached'); - - expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); - }); - - it('`topReached` fetches more logs', () => { - state.logs.isLoading = true; - findInfiniteScroll().vm.$emit('topReached'); - - expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); - }); - - it('`scroll` on a scrollable target results in enabled scroll buttons', async () => { - const target = { scrollTop: 10, clientHeight: 10, scrollHeight: 21 }; - - state.logs.isLoading = true; - findInfiniteScroll().vm.$emit('scroll', { target }); - - await nextTick(); - expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(false); - }); - - it('`scroll` on a non-scrollable target in disabled scroll buttons', async () => { - const target = { scrollTop: 10, clientHeight: 10, scrollHeight: 20 }; - - state.logs.isLoading = true; - findInfiniteScroll().vm.$emit('scroll', { target }); - - await nextTick(); - expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true); - }); - - it('`scroll` on no target results in disabled scroll buttons', async () => { - state.logs.isLoading = true; - findInfiniteScroll().vm.$emit('scroll', { target: undefined }); - - await nextTick(); - expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true); - }); - }); -}); diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js deleted file mode 100644 index 4e4052eb4d8..00000000000 --- a/spec/frontend/logs/components/log_advanced_filters_spec.js +++ /dev/null @@ -1,175 +0,0 @@ -import { GlFilteredSearch } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { convertToFixedRange } from '~/lib/utils/datetime_range'; -import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue'; -import { TOKEN_TYPE_POD_NAME } from '~/logs/constants'; -import { createStore } from '~/logs/stores'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; -import { defaultTimeRange } from '~/vue_shared/constants'; -import { mockPods, mockSearch } from '../mock_data'; - -const module = 'environmentLogs'; - -describe('LogAdvancedFilters', () => { - let store; - let dispatch; - let wrapper; - let state; - - const findFilteredSearch = () => wrapper.find(GlFilteredSearch); - const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' }); - const getSearchToken = (type) => - findFilteredSearch() - .props('availableTokens') - .filter((token) => token.type === type)[0]; - - const mockStateLoading = () => { - state.timeRange.selected = defaultTimeRange; - state.timeRange.current = convertToFixedRange(defaultTimeRange); - state.pods.options = []; - state.pods.current = null; - state.logs.isLoading = true; - }; - - const mockStateWithData = () => { - state.timeRange.selected = defaultTimeRange; - state.timeRange.current = convertToFixedRange(defaultTimeRange); - state.pods.options = mockPods; - state.pods.current = null; - state.logs.isLoading = false; - }; - - const initWrapper = (propsData = {}) => { - wrapper = shallowMount(LogAdvancedFilters, { - propsData: { - ...propsData, - }, - store, - }); - }; - - beforeEach(() => { - store = createStore(); - state = store.state.environmentLogs; - - jest.spyOn(store, 'dispatch').mockResolvedValue(); - - dispatch = store.dispatch; - }); - - afterEach(() => { - store.dispatch.mockReset(); - - if (wrapper) { - wrapper.destroy(); - } - }); - - it('displays UI elements', () => { - initWrapper(); - - expect(findFilteredSearch().exists()).toBe(true); - expect(findTimeRangePicker().exists()).toBe(true); - }); - - it('displays search tokens', () => { - initWrapper(); - - expect(getSearchToken(TOKEN_TYPE_POD_NAME)).toMatchObject({ - title: 'Pod name', - unique: true, - operators: OPERATOR_IS_ONLY, - }); - }); - - describe('disabled state', () => { - beforeEach(() => { - mockStateLoading(); - initWrapper({ - disabled: true, - }); - }); - - it('displays disabled filters', () => { - expect(findFilteredSearch().attributes('disabled')).toBeTruthy(); - expect(findTimeRangePicker().attributes('disabled')).toBeTruthy(); - }); - }); - - describe('when the state is loading', () => { - beforeEach(() => { - mockStateLoading(); - initWrapper(); - }); - - it('displays a disabled search', () => { - expect(findFilteredSearch().attributes('disabled')).toBeTruthy(); - }); - - it('displays an enable date filter', () => { - expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); - }); - - it('displays no pod options when no pods are available, so suggestions can be displayed', () => { - expect(getSearchToken(TOKEN_TYPE_POD_NAME).options).toBe(null); - expect(getSearchToken(TOKEN_TYPE_POD_NAME).loading).toBe(true); - }); - }); - - describe('when the state has data', () => { - beforeEach(() => { - mockStateWithData(); - initWrapper(); - }); - - it('displays a single token for pods', () => { - initWrapper(); - - const tokens = findFilteredSearch().props('availableTokens'); - - expect(tokens).toHaveLength(1); - expect(tokens[0].type).toBe(TOKEN_TYPE_POD_NAME); - }); - - it('displays a enabled filters', () => { - expect(findFilteredSearch().attributes('disabled')).toBeFalsy(); - expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); - }); - - it('displays options in the pods token', () => { - const { options } = getSearchToken(TOKEN_TYPE_POD_NAME); - - expect(options).toHaveLength(mockPods.length); - }); - - it('displays options in date time picker', () => { - const options = findTimeRangePicker().props('options'); - - expect(options).toEqual(expect.any(Array)); - expect(options.length).toBeGreaterThan(0); - }); - - describe('when the user interacts', () => { - it('clicks on the search button, showFilteredLogs is dispatched', () => { - findFilteredSearch().vm.$emit('submit', null); - - expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, null); - }); - - it('clicks on the search button, showFilteredLogs is dispatched with null', () => { - findFilteredSearch().vm.$emit('submit', [mockSearch]); - - expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, [mockSearch]); - }); - - it('selects a new time range', () => { - expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); - - const mockRange = { start: 'START_DATE', end: 'END_DATE' }; - findTimeRangePicker().vm.$emit('input', mockRange); - - expect(dispatch).toHaveBeenCalledWith(`${module}/setTimeRange`, mockRange); - }); - }); - }); -}); diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js deleted file mode 100644 index 04ad2e03542..00000000000 --- a/spec/frontend/logs/components/log_simple_filters_spec.js +++ /dev/null @@ -1,134 +0,0 @@ -import { GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import LogSimpleFilters from '~/logs/components/log_simple_filters.vue'; -import { createStore } from '~/logs/stores'; -import { mockPods, mockPodName } from '../mock_data'; - -const module = 'environmentLogs'; - -describe('LogSimpleFilters', () => { - let store; - let dispatch; - let wrapper; - let state; - - const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' }); - const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' }); - const findPodsDropdownItems = () => - findPodsDropdown() - .findAll(GlDropdownItem) - .filter((item) => !('disabled' in item.attributes())); - - const mockPodsLoading = () => { - state.pods.options = []; - state.pods.current = null; - }; - - const mockPodsLoaded = () => { - state.pods.options = mockPods; - state.pods.current = mockPodName; - }; - - const initWrapper = (propsData = {}) => { - wrapper = shallowMount(LogSimpleFilters, { - propsData: { - ...propsData, - }, - store, - }); - }; - - beforeEach(() => { - store = createStore(); - state = store.state.environmentLogs; - - jest.spyOn(store, 'dispatch').mockResolvedValue(); - - dispatch = store.dispatch; - }); - - afterEach(() => { - store.dispatch.mockReset(); - - if (wrapper) { - wrapper.destroy(); - } - }); - - it('displays UI elements', () => { - initWrapper(); - - expect(findPodsDropdown().exists()).toBe(true); - }); - - describe('disabled state', () => { - beforeEach(() => { - mockPodsLoading(); - initWrapper({ - disabled: true, - }); - }); - - it('displays a disabled pods dropdown', () => { - expect(findPodsDropdown().props('text')).toBe('No pod selected'); - expect(findPodsDropdown().attributes('disabled')).toBeTruthy(); - }); - }); - - describe('loading state', () => { - beforeEach(() => { - mockPodsLoading(); - initWrapper(); - }); - - it('displays an enabled pods dropdown', () => { - expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); - expect(findPodsDropdown().props('text')).toBe('No pod selected'); - }); - - it('displays an empty pods dropdown', () => { - expect(findPodsNoPodsText().exists()).toBe(true); - expect(findPodsDropdownItems()).toHaveLength(0); - }); - }); - - describe('pods available state', () => { - beforeEach(() => { - mockPodsLoaded(); - initWrapper(); - }); - - it('displays an enabled pods dropdown', () => { - expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); - expect(findPodsDropdown().props('text')).toBe(mockPods[0]); - }); - - it('displays a pods dropdown with items', () => { - expect(findPodsNoPodsText().exists()).toBe(false); - expect(findPodsDropdownItems()).toHaveLength(mockPods.length); - }); - - it('dropdown has one pod selected', () => { - const items = findPodsDropdownItems(); - mockPods.forEach((pod, i) => { - const item = items.at(i); - if (item.text() !== mockPodName) { - expect(item.find(GlDropdownItem).attributes('ischecked')).toBeFalsy(); - } else { - expect(item.find(GlDropdownItem).attributes('ischecked')).toBeTruthy(); - } - }); - }); - - it('when the user clicks on a pod, showPodLogs is dispatched', () => { - const items = findPodsDropdownItems(); - const index = 2; // any pod - - expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); - - items.at(index).vm.$emit('click'); - - expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]); - }); - }); -}); diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js deleted file mode 100644 index 46ef1500a20..00000000000 --- a/spec/frontend/logs/stores/actions_spec.js +++ /dev/null @@ -1,521 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import axios from '~/lib/utils/axios_utils'; -import { convertToFixedRange } from '~/lib/utils/datetime_range'; -import { TOKEN_TYPE_POD_NAME } from '~/logs/constants'; -import { - setInitData, - showFilteredLogs, - showPodLogs, - fetchEnvironments, - fetchLogs, - fetchMoreLogsPrepend, -} from '~/logs/stores/actions'; -import * as types from '~/logs/stores/mutation_types'; -import logsPageState from '~/logs/stores/state'; -import Tracking from '~/tracking'; - -import { defaultTimeRange } from '~/vue_shared/constants'; - -import { - mockPodName, - mockEnvironmentsEndpoint, - mockEnvironments, - mockPods, - mockLogsResult, - mockEnvName, - mockSearch, - mockLogsEndpoint, - mockResponse, - mockCursor, - mockNextCursor, -} from '../mock_data'; - -jest.mock('~/lib/utils/datetime_range'); -jest.mock('~/logs/utils'); - -const mockDefaultRange = { - start: '2020-01-10T18:00:00.000Z', - end: '2020-01-10T19:00:00.000Z', -}; -const mockFixedRange = { - start: '2020-01-09T18:06:20.000Z', - end: '2020-01-09T18:36:20.000Z', -}; -const mockRollingRange = { - duration: 120, -}; -const mockRollingRangeAsFixed = { - start: '2020-01-10T18:00:00.000Z', - end: '2020-01-10T17:58:00.000Z', -}; - -describe('Logs Store actions', () => { - let state; - let mock; - - const latestGetParams = () => mock.history.get[mock.history.get.length - 1].params; - - convertToFixedRange.mockImplementation((range) => { - if (range === defaultTimeRange) { - return { ...mockDefaultRange }; - } - if (range === mockFixedRange) { - return { ...mockFixedRange }; - } - if (range === mockRollingRange) { - return { ...mockRollingRangeAsFixed }; - } - throw new Error('Invalid time range'); - }); - - beforeEach(() => { - state = logsPageState(); - }); - - describe('setInitData', () => { - it('should commit environment and pod name mutation', () => - testAction( - setInitData, - { timeRange: mockFixedRange, environmentName: mockEnvName, podName: mockPodName }, - state, - [ - { type: types.SET_TIME_RANGE, payload: mockFixedRange }, - { type: types.SET_PROJECT_ENVIRONMENT, payload: mockEnvName }, - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - ], - )); - }); - - describe('showFilteredLogs', () => { - it('empty search should filter with defaults', () => - testAction( - showFilteredLogs, - undefined, - state, - [ - { type: types.SET_CURRENT_POD_NAME, payload: null }, - { type: types.SET_SEARCH, payload: '' }, - ], - [{ type: 'fetchLogs', payload: 'used_search_bar' }], - )); - - it('text search should filter with a search term', () => - testAction( - showFilteredLogs, - [mockSearch], - state, - [ - { type: types.SET_CURRENT_POD_NAME, payload: null }, - { type: types.SET_SEARCH, payload: mockSearch }, - ], - [{ type: 'fetchLogs', payload: 'used_search_bar' }], - )); - - it('pod search should filter with a search term', () => - testAction( - showFilteredLogs, - [{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }], - state, - [ - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.SET_SEARCH, payload: '' }, - ], - [{ type: 'fetchLogs', payload: 'used_search_bar' }], - )); - - it('pod search should filter with a pod selection and a search term', () => - testAction( - showFilteredLogs, - [{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }, mockSearch], - state, - [ - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.SET_SEARCH, payload: mockSearch }, - ], - [{ type: 'fetchLogs', payload: 'used_search_bar' }], - )); - - it('pod search should filter with a pod selection and two search terms', () => - testAction( - showFilteredLogs, - ['term1', 'term2'], - state, - [ - { type: types.SET_CURRENT_POD_NAME, payload: null }, - { type: types.SET_SEARCH, payload: `term1 term2` }, - ], - [{ type: 'fetchLogs', payload: 'used_search_bar' }], - )); - - it('pod search should filter with a pod selection and a search terms before and after', () => - testAction( - showFilteredLogs, - [ - 'term1', - { type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }, - 'term2', - ], - state, - [ - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.SET_SEARCH, payload: `term1 term2` }, - ], - [{ type: 'fetchLogs', payload: 'used_search_bar' }], - )); - }); - - describe('showPodLogs', () => { - it('should commit pod name', () => - testAction( - showPodLogs, - mockPodName, - state, - [{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName }], - [{ type: 'fetchLogs', payload: 'pod_log_changed' }], - )); - }); - - describe('fetchEnvironments', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - it('should commit RECEIVE_ENVIRONMENTS_DATA_SUCCESS mutation on correct data', () => { - mock.onGet(mockEnvironmentsEndpoint).replyOnce(200, mockEnvironments); - return testAction( - fetchEnvironments, - mockEnvironmentsEndpoint, - state, - [ - { type: types.REQUEST_ENVIRONMENTS_DATA }, - { type: types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, payload: mockEnvironments }, - ], - [{ type: 'fetchLogs', payload: 'environment_selected' }], - ); - }); - - it('should commit RECEIVE_ENVIRONMENTS_DATA_ERROR on wrong data', () => { - mock.onGet(mockEnvironmentsEndpoint).replyOnce(500); - return testAction( - fetchEnvironments, - mockEnvironmentsEndpoint, - state, - [ - { type: types.REQUEST_ENVIRONMENTS_DATA }, - { type: types.RECEIVE_ENVIRONMENTS_DATA_ERROR }, - ], - [], - ); - }); - }); - - describe('when the backend responds succesfully', () => { - let expectedMutations; - let expectedActions; - - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet(mockLogsEndpoint).reply(200, mockResponse); - mock.onGet(mockLogsEndpoint).replyOnce(202); // mock reactive cache - - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - }); - - afterEach(() => { - mock.reset(); - }); - - describe('fetchLogs', () => { - beforeEach(() => { - expectedMutations = [ - { type: types.REQUEST_LOGS_DATA }, - { - type: types.RECEIVE_LOGS_DATA_SUCCESS, - payload: { logs: mockLogsResult, cursor: mockNextCursor }, - }, - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods }, - ]; - - expectedActions = []; - }); - - it('should commit logs and pod data when there is pod name defined', () => { - state.pods.current = mockPodName; - state.timeRange.current = mockFixedRange; - - return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { - expect(latestGetParams()).toMatchObject({ - pod_name: mockPodName, - }); - }); - }); - - it('should commit logs and pod data when there is pod name defined and a non-default date range', () => { - state.pods.current = mockPodName; - state.timeRange.current = mockFixedRange; - state.logs.cursor = mockCursor; - - return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { - expect(latestGetParams()).toEqual({ - pod_name: mockPodName, - start_time: mockFixedRange.start, - end_time: mockFixedRange.end, - cursor: mockCursor, - }); - }); - }); - - it('should commit logs and pod data when there is pod name and search and a faulty date range', () => { - state.pods.current = mockPodName; - state.search = mockSearch; - state.timeRange.current = 'INVALID_TIME_RANGE'; - - expectedMutations.splice(1, 0, { - type: types.SHOW_TIME_RANGE_INVALID_WARNING, - }); - - return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { - expect(latestGetParams()).toEqual({ - pod_name: mockPodName, - search: mockSearch, - }); - }); - }); - - it('should commit logs and pod data when no pod name defined', () => { - state.timeRange.current = defaultTimeRange; - - return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { - expect(latestGetParams()).toEqual({ - start_time: expect.any(String), - end_time: expect.any(String), - }); - }); - }); - }); - - describe('fetchMoreLogsPrepend', () => { - beforeEach(() => { - expectedMutations = [ - { type: types.REQUEST_LOGS_DATA_PREPEND }, - { - type: types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, - payload: { logs: mockLogsResult, cursor: mockNextCursor }, - }, - ]; - - expectedActions = []; - }); - - it('should commit logs and pod data when there is pod name defined', () => { - state.pods.current = mockPodName; - state.timeRange.current = mockFixedRange; - - expectedActions = []; - - return testAction( - fetchMoreLogsPrepend, - null, - state, - expectedMutations, - expectedActions, - () => { - expect(latestGetParams()).toMatchObject({ - pod_name: mockPodName, - }); - }, - ); - }); - - it('should commit logs and pod data when there is pod name defined and a non-default date range', () => { - state.pods.current = mockPodName; - state.timeRange.current = mockFixedRange; - state.logs.cursor = mockCursor; - - return testAction( - fetchMoreLogsPrepend, - null, - state, - expectedMutations, - expectedActions, - () => { - expect(latestGetParams()).toEqual({ - pod_name: mockPodName, - start_time: mockFixedRange.start, - end_time: mockFixedRange.end, - cursor: mockCursor, - }); - }, - ); - }); - - it('should commit logs and pod data when there is pod name and search and a faulty date range', () => { - state.pods.current = mockPodName; - state.search = mockSearch; - state.timeRange.current = 'INVALID_TIME_RANGE'; - - expectedMutations.splice(1, 0, { - type: types.SHOW_TIME_RANGE_INVALID_WARNING, - }); - - return testAction( - fetchMoreLogsPrepend, - null, - state, - expectedMutations, - expectedActions, - () => { - expect(latestGetParams()).toEqual({ - pod_name: mockPodName, - search: mockSearch, - }); - }, - ); - }); - - it('should commit logs and pod data when no pod name defined', () => { - state.timeRange.current = defaultTimeRange; - - return testAction( - fetchMoreLogsPrepend, - null, - state, - expectedMutations, - expectedActions, - () => { - expect(latestGetParams()).toEqual({ - start_time: expect.any(String), - end_time: expect.any(String), - }); - }, - ); - }); - - it('should not commit logs or pod data when it has reached the end', () => { - state.logs.isComplete = true; - state.logs.cursor = null; - - return testAction( - fetchMoreLogsPrepend, - null, - state, - [], // no mutations done - [], // no actions dispatched - () => { - expect(mock.history.get).toHaveLength(0); - }, - ); - }); - }); - }); - - describe('when the backend responds with an error', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet(mockLogsEndpoint).reply(500); - }); - - afterEach(() => { - mock.reset(); - }); - - it('fetchLogs should commit logs and pod errors', () => { - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - state.timeRange.current = defaultTimeRange; - - return testAction( - fetchLogs, - null, - state, - [ - { type: types.REQUEST_LOGS_DATA }, - { type: types.RECEIVE_PODS_DATA_ERROR }, - { type: types.RECEIVE_LOGS_DATA_ERROR }, - ], - [], - () => { - expect(mock.history.get[0].url).toBe(mockLogsEndpoint); - }, - ); - }); - - it('fetchMoreLogsPrepend should commit logs and pod errors', () => { - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - state.timeRange.current = defaultTimeRange; - - return testAction( - fetchMoreLogsPrepend, - null, - state, - [ - { type: types.REQUEST_LOGS_DATA_PREPEND }, - { type: types.RECEIVE_LOGS_DATA_PREPEND_ERROR }, - ], - [], - () => { - expect(mock.history.get[0].url).toBe(mockLogsEndpoint); - }, - ); - }); - }); -}); - -describe('Tracking user interaction', () => { - let commit; - let dispatch; - let state; - let mock; - - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - commit = jest.fn(); - dispatch = jest.fn(); - state = logsPageState(); - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.reset(); - }); - - describe('Logs with data', () => { - beforeEach(() => { - mock.onGet(mockLogsEndpoint).reply(200, mockResponse); - mock.onGet(mockLogsEndpoint).replyOnce(202); // mock reactive cache - }); - - it('tracks fetched logs with data', () => { - return fetchLogs({ state, commit, dispatch }, 'environment_selected').then(() => { - expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'logs_view', { - label: 'environment_selected', - property: 'count', - value: 1, - }); - }); - }); - }); - - describe('Logs without data', () => { - beforeEach(() => { - mock.onGet(mockLogsEndpoint).reply(200, { - ...mockResponse, - logs: [], - }); - mock.onGet(mockLogsEndpoint).replyOnce(202); // mock reactive cache - }); - - it('does not track empty log responses', () => { - return fetchLogs({ state, commit, dispatch }).then(() => { - expect(Tracking.event).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js deleted file mode 100644 index 9d213d8c01f..00000000000 --- a/spec/frontend/logs/stores/getters_spec.js +++ /dev/null @@ -1,75 +0,0 @@ -import { trace, showAdvancedFilters } from '~/logs/stores/getters'; -import logsPageState from '~/logs/stores/state'; - -import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data'; - -describe('Logs Store getters', () => { - let state; - - beforeEach(() => { - state = logsPageState(); - }); - - describe('trace', () => { - describe('when state is initialized', () => { - it('returns an empty string', () => { - expect(trace(state)).toEqual(''); - }); - }); - - describe('when state logs are empty', () => { - beforeEach(() => { - state.logs.lines = []; - }); - - it('returns an empty string', () => { - expect(trace(state)).toEqual(''); - }); - }); - - describe('when state logs are set', () => { - beforeEach(() => { - state.logs.lines = mockLogsResult; - }); - - it('returns an empty string', () => { - expect(trace(state)).toEqual(mockTrace.join('\n')); - }); - }); - }); - - describe('showAdvancedFilters', () => { - describe('when no environments are set', () => { - beforeEach(() => { - state.environments.current = mockEnvName; - state.environments.options = []; - }); - - it('returns false', () => { - expect(showAdvancedFilters(state)).toBe(false); - }); - }); - - describe('when the environment supports filters', () => { - beforeEach(() => { - state.environments.current = mockEnvName; - state.environments.options = mockEnvironments; - }); - - it('returns true', () => { - expect(showAdvancedFilters(state)).toBe(true); - }); - }); - - describe('when the environment does not support filters', () => { - beforeEach(() => { - state.environments.options = mockEnvironments; - state.environments.current = mockEnvironments[1].name; - }); - - it('returns true', () => { - expect(showAdvancedFilters(state)).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js index 89b2d7b2b90..0cda0c5bc2b 100644 --- a/spec/frontend/notebook/cells/prompt_spec.js +++ b/spec/frontend/notebook/cells/prompt_spec.js @@ -1,52 +1,40 @@ -import Vue, { nextTick } from 'vue'; -import PromptComponent from '~/notebook/cells/prompt.vue'; - -const Component = Vue.extend(PromptComponent); +import { shallowMount } from '@vue/test-utils'; +import Prompt from '~/notebook/cells/prompt.vue'; describe('Prompt component', () => { - let vm; + let wrapper; + + const mountComponent = ({ type }) => shallowMount(Prompt, { propsData: { type, count: 1 } }); + + afterEach(() => { + wrapper.destroy(); + }); describe('input', () => { beforeEach(() => { - vm = new Component({ - propsData: { - type: 'In', - count: 1, - }, - }); - vm.$mount(); - - return nextTick(); + wrapper = mountComponent({ type: 'In' }); }); it('renders in label', () => { - expect(vm.$el.textContent.trim()).toContain('In'); + expect(wrapper.text()).toContain('In'); }); it('renders count', () => { - expect(vm.$el.textContent.trim()).toContain('1'); + expect(wrapper.text()).toContain('1'); }); }); describe('output', () => { beforeEach(() => { - vm = new Component({ - propsData: { - type: 'Out', - count: 1, - }, - }); - vm.$mount(); - - return nextTick(); + wrapper = mountComponent({ type: 'Out' }); }); it('renders in label', () => { - expect(vm.$el.textContent.trim()).toContain('Out'); + expect(wrapper.text()).toContain('Out'); }); it('renders count', () => { - expect(vm.$el.textContent.trim()).toContain('1'); + expect(wrapper.text()).toContain('1'); }); }); }); diff --git a/spec/frontend/notes/components/note_signed_out_widget_spec.js b/spec/frontend/notes/components/note_signed_out_widget_spec.js index e217a2caa73..84f20e4ad58 100644 --- a/spec/frontend/notes/components/note_signed_out_widget_spec.js +++ b/spec/frontend/notes/components/note_signed_out_widget_spec.js @@ -1,41 +1,30 @@ -import Vue from 'vue'; -import noteSignedOut from '~/notes/components/note_signed_out_widget.vue'; +import { shallowMount } from '@vue/test-utils'; +import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue'; import createStore from '~/notes/stores'; import { notesDataMock } from '../mock_data'; -describe('note_signed_out_widget component', () => { - let store; - let vm; +describe('NoteSignedOutWidget component', () => { + let wrapper; beforeEach(() => { - const Component = Vue.extend(noteSignedOut); - store = createStore(); + const store = createStore(); store.dispatch('setNotesData', notesDataMock); - - vm = new Component({ - store, - }).$mount(); + wrapper = shallowMount(NoteSignedOutWidget, { store }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - it('should render sign in link provided in the store', () => { - expect(vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent).toEqual( - 'sign in', - ); + it('renders sign in link provided in the store', () => { + expect(wrapper.find(`a[href="${notesDataMock.newSessionPath}"]`).text()).toBe('sign in'); }); - it('should render register link provided in the store', () => { - expect(vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent).toEqual( - 'register', - ); + it('renders register link provided in the store', () => { + expect(wrapper.find(`a[href="${notesDataMock.registerPath}"]`).text()).toBe('register'); }); - it('should render information text', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual( - 'Please register or sign in to reply', - ); + it('renders information text', () => { + expect(wrapper.text()).toContain('Please register or sign in to reply'); }); }); diff --git a/spec/frontend/pdf/index_spec.js b/spec/frontend/pdf/index_spec.js index 2b0932493bb..98946412264 100644 --- a/spec/frontend/pdf/index_spec.js +++ b/spec/frontend/pdf/index_spec.js @@ -1,48 +1,33 @@ -import Vue from 'vue'; - +import { shallowMount } from '@vue/test-utils'; import { FIXTURES_PATH } from 'spec/test_constants'; import PDFLab from '~/pdf/index.vue'; -jest.mock('pdfjs-dist/webpack', () => { - return { default: jest.requireActual('pdfjs-dist/build/pdf') }; -}); - -const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`; +describe('PDFLab component', () => { + let wrapper; -const Component = Vue.extend(PDFLab); + const mountComponent = ({ pdf }) => shallowMount(PDFLab, { propsData: { pdf } }); -describe('PDF component', () => { - let vm; + afterEach(() => { + wrapper.destroy(); + }); describe('without PDF data', () => { beforeEach(() => { - vm = new Component({ - propsData: { - pdf: '', - }, - }); - - vm.$mount(); + wrapper = mountComponent({ pdf: '' }); }); it('does not render', () => { - expect(vm.$el.tagName).toBeUndefined(); + expect(wrapper.isVisible()).toBe(false); }); }); describe('with PDF data', () => { beforeEach(() => { - vm = new Component({ - propsData: { - pdf, - }, - }); - - vm.$mount(); + wrapper = mountComponent({ pdf: `${FIXTURES_PATH}/blob/pdf/test.pdf` }); }); - it('renders pdf component', () => { - expect(vm.$el.tagName).toBeDefined(); + it('renders', () => { + expect(wrapper.isVisible()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/page_size_selector_spec.js b/spec/frontend/vue_shared/components/page_size_selector_spec.js new file mode 100644 index 00000000000..5ec0b863afd --- /dev/null +++ b/spec/frontend/vue_shared/components/page_size_selector_spec.js @@ -0,0 +1,44 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import PageSizeSelector, { PAGE_SIZES } from '~/vue_shared/components/page_size_selector.vue'; + +describe('Page size selector component', () => { + let wrapper; + + const createWrapper = ({ pageSize = 20 } = {}) => { + wrapper = shallowMount(PageSizeSelector, { + propsData: { value: pageSize }, + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + + afterEach(() => { + wrapper.destroy(); + }); + + it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => { + createWrapper({ pageSize }); + + expect(findDropdown().props('text')).toBe(`Show ${pageSize} items`); + }); + + it('shows the expected dropdown items', () => { + createWrapper(); + + PAGE_SIZES.forEach((pageSize, index) => { + expect(findDropdownItems().at(index).text()).toBe(`Show ${pageSize} items`); + }); + }); + + it('will emit the new page size when a dropdown item is clicked', () => { + createWrapper(); + + findDropdownItems().wrappers.forEach((itemWrapper, index) => { + itemWrapper.vm.$emit('click'); + + expect(wrapper.emitted('input')[index][0]).toBe(PAGE_SIZES[index]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js index 66f71c0b028..50e79dbe589 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js @@ -9,6 +9,7 @@ import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vu import IssuableListRoot from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; import { mockIssuableListProps, mockIssuables } from '../mock_data'; @@ -44,6 +45,7 @@ describe('IssuableListRoot', () => { const findIssuableItem = () => wrapper.findComponent(IssuableItem); const findIssuableTabs = () => wrapper.findComponent(IssuableTabs); const findVueDraggable = () => wrapper.findComponent(VueDraggable); + const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector); afterEach(() => { wrapper.destroy(); @@ -292,6 +294,7 @@ describe('IssuableListRoot', () => { }); expect(findGlKeysetPagination().exists()).toBe(false); + expect(findPageSizeSelector().exists()).toBe(false); expect(findGlPagination().props()).toMatchObject({ perPage: 20, value: 1, @@ -483,4 +486,24 @@ describe('IssuableListRoot', () => { }); }); }); + + describe('page size selector', () => { + beforeEach(() => { + wrapper = createComponent({ + props: { + showPageSizeChangeControls: true, + }, + }); + }); + + it('has the page size change component', async () => { + expect(findPageSizeSelector().exists()).toBe(true); + }); + + it('emits "page-size-change" event when its input is changed', () => { + const pageSize = 123; + findPageSizeSelector().vm.$emit('input', pageSize); + expect(wrapper.emitted('page-size-change')).toEqual([[pageSize]]); + }); + }); }); diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 4117d577f20..1ead1fc9b8b 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -741,7 +741,7 @@ RSpec.describe SearchHelper do let(:for_group) { true } it 'adds the :group and :group_metadata correctly to hash' do - expect(header_search_context[:group]).to eq({ id: group.id, name: group.name }) + expect(header_search_context[:group]).to eq({ id: group.id, name: group.name, full_name: group.full_name }) expect(header_search_context[:group_metadata]).to eq(group_metadata) end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 7235b7c95cd..810271818ae 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -15,7 +15,7 @@ RSpec.describe BulkImports::Pipeline::Runner do Class.new do def initialize(options = {}); end - def transform(context); end + def transform(context, data); end end end @@ -23,7 +23,7 @@ RSpec.describe BulkImports::Pipeline::Runner do Class.new do def initialize(options = {}); end - def load(context); end + def load(context, data); end end end @@ -44,11 +44,73 @@ RSpec.describe BulkImports::Pipeline::Runner do end let_it_be_with_reload(:entity) { create(:bulk_import_entity) } - let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } - let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker, extra: :data) } + + let(:tracker) { create(:bulk_import_tracker, entity: entity) } + let(:context) { BulkImports::Pipeline::Context.new(tracker, extra: :data) } subject { BulkImports::MyPipeline.new(context) } + shared_examples 'failed pipeline' do |exception_class, exception_message| + it 'logs import failure' do + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error) + .with( + log_params( + context, + pipeline_step: :extractor, + pipeline_class: 'BulkImports::MyPipeline', + exception_class: exception_class, + exception_message: exception_message + ) + ) + end + + expect { subject.run } + .to change(entity.failures, :count).by(1) + + failure = entity.failures.first + + expect(failure).to be_present + expect(failure.pipeline_class).to eq('BulkImports::MyPipeline') + expect(failure.pipeline_step).to eq('extractor') + expect(failure.exception_class).to eq(exception_class) + expect(failure.exception_message).to eq(exception_message) + end + + context 'when pipeline is marked to abort on failure' do + before do + BulkImports::MyPipeline.abort_on_failure! + end + + it 'logs a warn message and marks entity and tracker as failed' do + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:warn) + .with( + log_params( + context, + message: 'Aborting entity migration due to pipeline failure', + pipeline_class: 'BulkImports::MyPipeline' + ) + ) + end + + subject.run + + expect(entity.failed?).to eq(true) + expect(tracker.failed?).to eq(true) + end + end + + context 'when pipeline is not marked to abort on failure' do + it 'does not mark entity as failed' do + subject.run + + expect(tracker.failed?).to eq(true) + expect(entity.failed?).to eq(false) + end + end + end + describe 'pipeline runner' do context 'when entity is not marked as failed' do it 'runs pipeline extractor, transformer, loader' do @@ -145,70 +207,65 @@ RSpec.describe BulkImports::Pipeline::Runner do end end - context 'when exception is raised' do + context 'when the exception BulkImports::NetworkError is raised' do before do allow_next_instance_of(BulkImports::Extractor) do |extractor| - allow(extractor).to receive(:extract).with(context).and_raise(StandardError, 'Error!') + allow(extractor).to receive(:extract).with(context).and_raise( + BulkImports::NetworkError.new( + 'Net::ReadTimeout', + response: instance_double(HTTParty::Response, code: reponse_status_code, headers: {}) + ) + ) end end - it 'logs import failure' do - expect_next_instance_of(Gitlab::Import::Logger) do |logger| - expect(logger).to receive(:error) - .with( - log_params( - context, - pipeline_step: :extractor, - pipeline_class: 'BulkImports::MyPipeline', - exception_class: 'StandardError', - exception_message: 'Error!' - ) - ) - end + context 'when exception is retriable' do + let(:reponse_status_code) { 429 } - expect { subject.run } - .to change(entity.failures, :count).by(1) + it 'raises the exception BulkImports::RetryPipelineError' do + expect { subject.run }.to raise_error(BulkImports::RetryPipelineError) + end + end - failure = entity.failures.first + context 'when exception is not retriable' do + let(:reponse_status_code) { 503 } - expect(failure).to be_present - expect(failure.pipeline_class).to eq('BulkImports::MyPipeline') - expect(failure.pipeline_step).to eq('extractor') - expect(failure.exception_class).to eq('StandardError') - expect(failure.exception_message).to eq('Error!') + it_behaves_like 'failed pipeline', 'BulkImports::NetworkError', 'Net::ReadTimeout' end + end - context 'when pipeline is marked to abort on failure' do - before do - BulkImports::MyPipeline.abort_on_failure! - end - - it 'logs a warn message and marks entity as failed' do - expect_next_instance_of(Gitlab::Import::Logger) do |logger| - expect(logger).to receive(:warn) - .with( - log_params( - context, - message: 'Pipeline failed', - pipeline_class: 'BulkImports::MyPipeline' + context 'when a retriable BulkImports::NetworkError exception is raised while extracting the next page' do + before do + call_count = 0 + allow_next_instance_of(BulkImports::Extractor) do |extractor| + allow(extractor).to receive(:extract).with(context).twice do + if call_count.zero? + call_count += 1 + extracted_data(has_next_page: true) + else + raise( + BulkImports::NetworkError.new( + response: instance_double(HTTParty::Response, code: 429, headers: {}) ) ) + end end - - subject.run - - expect(entity.status_name).to eq(:failed) - expect(tracker.status_name).to eq(:failed) end end - context 'when pipeline is not marked to abort on failure' do - it 'does not mark entity as failed' do - subject.run + it 'raises the exception BulkImports::RetryPipelineError' do + expect { subject.run }.to raise_error(BulkImports::RetryPipelineError) + end + end - expect(entity.failed?).to eq(false) + context 'when the exception StandardError is raised' do + before do + allow_next_instance_of(BulkImports::Extractor) do |extractor| + allow(extractor).to receive(:extract).with(context).and_raise(StandardError, 'Error!') end end + + it_behaves_like 'failed pipeline', 'StandardError', 'Error!' end end diff --git a/spec/lib/bulk_imports/retry_pipeline_error_spec.rb b/spec/lib/bulk_imports/retry_pipeline_error_spec.rb new file mode 100644 index 00000000000..9d96407b03a --- /dev/null +++ b/spec/lib/bulk_imports/retry_pipeline_error_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::RetryPipelineError do + describe '#retry_delay' do + it 'returns retry_delay' do + exception = described_class.new('Error!', 60) + + expect(exception.retry_delay).to eq(60) + end + end +end diff --git a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb index 608040eaefb..81e2a410962 100644 --- a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb +++ b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb @@ -17,10 +17,10 @@ RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do shared_examples 'exception logging' do it 'logs error' do - expect(Gitlab::AppLogger).to receive(:error).with( + expect(Gitlab::AppLogger).to receive(:error).with({ 'open_api.http_code' => api_exception.code, 'open_api.response_body' => api_exception.response_body.truncate(100) - ) + }) subject end @@ -66,11 +66,11 @@ RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do .and_return(error) allow(open_api).to receive(:list_events) - .with(project.id, error.fingerprint, sort: 'occurred_at_asc', limit: 1) + .with(project.id, error.fingerprint, { sort: 'occurred_at_asc', limit: 1 }) .and_return(list_events_asc) allow(open_api).to receive(:list_events) - .with(project.id, error.fingerprint, sort: 'occurred_at_desc', limit: 1) + .with(project.id, error.fingerprint, { sort: 'occurred_at_desc', limit: 1 }) .and_return(list_events_desc) end end diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index cd3388701fe..3f44cfdcf27 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -82,11 +82,43 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end describe '.track_approve_mr_action' do - subject { described_class.track_approve_mr_action(user: user) } + include ProjectForksHelper + + let(:merge_request) { create(:merge_request, target_project: target_project, source_project: source_project) } + let(:source_project) { fork_project(target_project) } + let(:target_project) { create(:project) } + + subject { described_class.track_approve_mr_action(user: user, merge_request: merge_request) } it_behaves_like 'a tracked merge request unique event' do let(:action) { described_class::MR_APPROVE_ACTION } end + + it 'records correct payload with Snowplow event', :snowplow do + stub_feature_flags(route_hll_to_snowplow_phase2: true) + + subject + + expect_snowplow_event( + category: 'merge_requests', + action: 'i_code_review_user_approve_mr', + namespace: target_project.namespace, + user: user, + project: target_project + ) + end + + context 'when FF is disabled' do + before do + stub_feature_flags(route_hll_to_snowplow_phase2: false) + end + + it 'doesnt emit snowplow events', :snowplow do + subject + + expect_no_snowplow_event + end + end end describe '.track_unapprove_mr_action' do diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb index 7a4bae7f852..53474c61907 100644 --- a/spec/services/issues/related_branches_service_spec.rb +++ b/spec/services/issues/related_branches_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Issues::RelatedBranchesService do +RSpec.describe Issues::RelatedBranchesService, :clean_gitlab_redis_cache do let_it_be(:developer) { create(:user) } let_it_be(:issue) { create(:issue) } diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb index e500102a00c..e1fbb945ee3 100644 --- a/spec/services/merge_requests/approval_service_spec.rb +++ b/spec/services/merge_requests/approval_service_spec.rb @@ -90,7 +90,7 @@ RSpec.describe MergeRequests::ApprovalService do it 'tracks merge request approve action' do expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) - .to receive(:track_approve_mr_action).with(user: user) + .to receive(:track_approve_mr_action).with(user: user, merge_request: merge_request) service.execute(merge_request) end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 6a65376d64c..278ca148b74 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -25,8 +25,12 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey end - it 'publishes a ProjectDeleted event with project id and namespace id' do - expected_data = { project_id: project.id, namespace_id: project.namespace_id } + it 'publishes a ProjectDeletedEvent' do + expected_data = { + project_id: project.id, + namespace_id: project.namespace_id, + root_namespace_id: project.root_namespace.id + } expect { destroy_project(project, user, {}) }.to publish_event(Projects::ProjectDeletedEvent).with(expected_data) end diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb index b5f20e9ff76..fe2039bd79e 100644 --- a/spec/workers/bulk_imports/pipeline_worker_spec.rb +++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb @@ -189,7 +189,7 @@ RSpec.describe BulkImports::PipelineWorker do end end - context 'when network error is raised' do + context 'when retry pipeline error is raised' do let(:pipeline_tracker) do create( :bulk_import_tracker, @@ -200,7 +200,7 @@ RSpec.describe BulkImports::PipelineWorker do end let(:exception) do - BulkImports::NetworkError.new(response: instance_double(HTTParty::Response, code: 429, headers: {})) + BulkImports::RetryPipelineError.new('Error!', 60) end before do @@ -213,54 +213,36 @@ RSpec.describe BulkImports::PipelineWorker do end end - context 'when error is retriable' do - it 'reenqueues the worker' do - expect_any_instance_of(BulkImports::Tracker) do |tracker| - expect(tracker).to receive(:retry).and_call_original - end - - expect_next_instance_of(Gitlab::Import::Logger) do |logger| - expect(logger) - .to receive(:info) - .with( - hash_including( - 'pipeline_name' => 'FakePipeline', - 'entity_id' => entity.id - ) - ) - end + it 'reenqueues the worker' do + expect_any_instance_of(BulkImports::Tracker) do |tracker| + expect(tracker).to receive(:retry).and_call_original + end - expect(described_class) - .to receive(:perform_in) + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger) + .to receive(:info) .with( - 60.seconds, - pipeline_tracker.id, - pipeline_tracker.stage, - pipeline_tracker.entity.id + hash_including( + 'pipeline_name' => 'FakePipeline', + 'entity_id' => entity.id + ) ) - - subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) - - pipeline_tracker.reload - - expect(pipeline_tracker.enqueued?).to be_truthy end - context 'when error is not retriable' do - let(:exception) do - BulkImports::NetworkError.new(response: instance_double(HTTParty::Response, code: 503, headers: {})) - end - - it 'marks tracker as failed and logs the error' do - expect(described_class).not_to receive(:perform_in) + expect(described_class) + .to receive(:perform_in) + .with( + 60.seconds, + pipeline_tracker.id, + pipeline_tracker.stage, + pipeline_tracker.entity.id + ) - subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) + subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id) - pipeline_tracker.reload + pipeline_tracker.reload - expect(pipeline_tracker.failed?).to eq(true) - end - end + expect(pipeline_tracker.enqueued?).to be_truthy end end end -- cgit v1.2.3