diff options
Diffstat (limited to 'spec/frontend/groups/components')
6 files changed, 295 insertions, 52 deletions
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index a4a7530184d..091ec17d58e 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import appComponent from '~/groups/components/app.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; @@ -11,6 +11,7 @@ import eventHub from '~/groups/event_hub'; import GroupsService from '~/groups/service/groups_service'; import GroupsStore from '~/groups/store/groups_store'; import EmptyState from '~/groups/components/empty_state.vue'; +import GroupsComponent from '~/groups/components/groups.vue'; import axios from '~/lib/utils/axios_utils'; import * as urlUtilities from '~/lib/utils/url_utility'; import setWindowLocation from 'helpers/set_window_location_helper'; @@ -115,7 +116,7 @@ describe('AppComponent', () => { return vm.fetchGroups({}).then(() => { expect(vm.isLoading).toBe(false); expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 }); - expect(createFlash).toHaveBeenCalledWith({ + expect(createAlert).toHaveBeenCalledWith({ message: 'An error occurred. Please try again.', }); }); @@ -326,7 +327,7 @@ describe('AppComponent', () => { expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); return waitForPromises().then(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); - expect(createFlash).toHaveBeenCalledWith({ message }); + expect(createAlert).toHaveBeenCalledWith({ message }); expect(vm.targetGroup.isBeingRemoved).toBe(false); }); }); @@ -341,7 +342,7 @@ describe('AppComponent', () => { expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); return waitForPromises().then(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); - expect(createFlash).toHaveBeenCalledWith({ message }); + expect(createAlert).toHaveBeenCalledWith({ message }); expect(vm.targetGroup.isBeingRemoved).toBe(false); }); }); @@ -388,24 +389,27 @@ describe('AppComponent', () => { }); describe.each` - action | groups | fromSearch | renderEmptyState | expected - ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${true} - ${''} | ${[]} | ${false} | ${true} | ${false} - ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${true} | ${false} - ${'subgroups_and_projects'} | ${[]} | ${true} | ${true} | ${false} + action | groups | fromSearch | shouldRenderEmptyState | searchEmpty + ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${false} + ${''} | ${[]} | ${false} | ${false} | ${false} + ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${false} | ${false} + ${'subgroups_and_projects'} | ${[]} | ${true} | ${false} | ${true} `( - 'when `action` is $action, `groups` is $groups, `fromSearch` is $fromSearch, and `renderEmptyState` is $renderEmptyState', - ({ action, groups, fromSearch, renderEmptyState, expected }) => { - it(expected ? 'renders empty state' : 'does not render empty state', async () => { + 'when `action` is $action, `groups` is $groups, and `fromSearch` is $fromSearch', + ({ action, groups, fromSearch, shouldRenderEmptyState, searchEmpty }) => { + it(`${shouldRenderEmptyState ? 'renders' : 'does not render'} empty state`, async () => { createShallowComponent({ - propsData: { action, renderEmptyState }, + propsData: { action, renderEmptyState: true }, }); + await waitForPromises(); + vm.updateGroups(groups, fromSearch); await nextTick(); - expect(wrapper.findComponent(EmptyState).exists()).toBe(expected); + expect(wrapper.findComponent(EmptyState).exists()).toBe(shouldRenderEmptyState); + expect(wrapper.findComponent(GroupsComponent).props('searchEmpty')).toBe(searchEmpty); }); }, ); @@ -440,18 +444,10 @@ describe('AppComponent', () => { expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function)); - }); - - it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => { - createShallowComponent(); - await nextTick(); - expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search'); - }); - - it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => { - createShallowComponent({ propsData: { hideProjects: true } }); - await nextTick(); - expect(vm.searchEmptyMessage).toBe('No groups matched your search'); + expect(eventHub.$on).toHaveBeenCalledWith( + 'fetchFilteredAndSortedGroups', + expect.any(Function), + ); }); }); @@ -468,6 +464,46 @@ describe('AppComponent', () => { expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith( + 'fetchFilteredAndSortedGroups', + expect.any(Function), + ); + }); + }); + + describe('when `fetchFilteredAndSortedGroups` event is emitted', () => { + const search = 'Foo bar'; + const sort = 'created_asc'; + const emitFetchFilteredAndSortedGroups = () => { + eventHub.$emit('fetchFilteredAndSortedGroups', { + filterGroupsBy: search, + sortBy: sort, + }); + }; + let setPaginationInfoSpy; + + beforeEach(() => { + setPaginationInfoSpy = jest.spyOn(GroupsStore.prototype, 'setPaginationInfo'); + createShallowComponent(); + }); + + it('renders loading icon', async () => { + emitFetchFilteredAndSortedGroups(); + await nextTick(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + + it('calls API with expected params', () => { + emitFetchFilteredAndSortedGroups(); + + expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort, undefined); + }); + + it('updates pagination', () => { + emitFetchFilteredAndSortedGroups(); + + expect(setPaginationInfoSpy).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 3aa66644c19..4570aa33a6c 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -245,19 +245,14 @@ describe('GroupItemComponent', () => { expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); }); }); + describe('schema.org props', () => { describe('when showSchemaMarkup is disabled on the group', () => { - it.each(['itemprop', 'itemtype', 'itemscope'], 'it does not set %s', (attr) => { + it.each(['itemprop', 'itemtype', 'itemscope'])('does not set %s', (attr) => { expect(wrapper.attributes(attr)).toBeUndefined(); }); - it.each( - ['.js-group-avatar', '.js-group-name', '.js-group-description'], - 'it does not set `itemprop` on sub-nodes', - (selector) => { - expect(wrapper.find(selector).attributes('itemprop')).toBeUndefined(); - }, - ); }); + describe('when group has microdata', () => { beforeEach(() => { const group = withMicrodata({ diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index 866868eff36..0cbb6cc8309 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { GlEmptyState } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import GroupFolderComponent from '~/groups/components/group_folder.vue'; @@ -15,7 +16,6 @@ describe('GroupsComponent', () => { const defaultPropsData = { groups: mockGroups, pageInfo: mockPageInfo, - searchEmptyMessage: 'No matching results', searchEmpty: false, }; @@ -67,13 +67,16 @@ describe('GroupsComponent', () => { expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true); expect(findPaginationLinks().exists()).toBe(true); - expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false); + expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false); }); it('should render empty search message when `searchEmpty` is `true`', () => { createComponent({ propsData: { searchEmpty: true } }); - expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true); + expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + title: GroupsComponent.i18n.emptyStateTitle, + description: GroupsComponent.i18n.emptyStateDescription, + }); }); }); }); diff --git a/spec/frontend/groups/components/new_top_level_group_alert_spec.js b/spec/frontend/groups/components/new_top_level_group_alert_spec.js new file mode 100644 index 00000000000..db9a5c7b16b --- /dev/null +++ b/spec/frontend/groups/components/new_top_level_group_alert_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue'; +import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +describe('NewTopLevelGroupAlert', () => { + let wrapper; + let userCalloutDismissSpy; + + const findAlert = () => wrapper.findComponent({ ref: 'newTopLevelAlert' }); + const createSubGroupPath = '/groups/new?parent_id=1#create-group-pane'; + + const createComponent = ({ shouldShowCallout = true } = {}) => { + userCalloutDismissSpy = jest.fn(); + + wrapper = shallowMount(NewTopLevelGroupAlert, { + provide: { + createSubGroupPath, + }, + stubs: { + UserCalloutDismisser: makeMockUserCalloutDismisser({ + dismiss: userCalloutDismissSpy, + shouldShowCallout, + }), + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when the component is created', () => { + beforeEach(() => { + createComponent({ + shouldShowCallout: true, + }); + }); + + it('renders a button with a link to create a new sub-group', () => { + expect(findAlert().props('primaryButtonText')).toBe( + NewTopLevelGroupAlert.i18n.primaryBtnText, + ); + expect(findAlert().props('primaryButtonLink')).toBe( + helpPagePath('user/group/subgroups/index'), + ); + }); + }); + + describe('dismissing the alert', () => { + beforeEach(() => { + findAlert().vm.$emit('dismiss'); + }); + + it('calls the dismiss callback', () => { + expect(userCalloutDismissSpy).toHaveBeenCalled(); + }); + }); + + describe('when the alert has been dismissed', () => { + beforeEach(() => { + createComponent({ + shouldShowCallout: false, + }); + }); + + it('does not show the alert', () => { + expect(findAlert().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js index 352bf25b84f..93e087e10f2 100644 --- a/spec/frontend/groups/components/overview_tabs_spec.js +++ b/spec/frontend/groups/components/overview_tabs_spec.js @@ -1,28 +1,46 @@ -import { GlTab } from '@gitlab/ui'; +import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui'; import { nextTick } from 'vue'; +import { createLocalVue } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import OverviewTabs from '~/groups/components/overview_tabs.vue'; import GroupsApp from '~/groups/components/app.vue'; +import GroupFolderComponent from '~/groups/components/group_folder.vue'; import GroupsStore from '~/groups/store/groups_store'; import GroupsService from '~/groups/service/groups_service'; import { createRouter } from '~/groups/init_overview_tabs'; +import eventHub from '~/groups/event_hub'; import { ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED, + OVERVIEW_TABS_SORTING_ITEMS, } from '~/groups/constants'; import axios from '~/lib/utils/axios_utils'; +const localVue = createLocalVue(); +localVue.component('GroupFolder', GroupFolderComponent); const router = createRouter(); +const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS; describe('OverviewTabs', () => { let wrapper; + let axiosMock; - const endpoints = { - subgroups_and_projects: '/groups/foobar/-/children.json', - shared: '/groups/foobar/-/shared_projects.json', - archived: '/groups/foobar/-/children.json?archived=only', + const defaultProvide = { + endpoints: { + subgroups_and_projects: '/groups/foobar/-/children.json', + shared: '/groups/foobar/-/shared_projects.json', + archived: '/groups/foobar/-/children.json?archived=only', + }, + newSubgroupPath: '/groups/new', + newProjectPath: 'projects/new', + newSubgroupIllustration: '', + newProjectIllustration: '', + emptySubgroupIllustration: '', + canCreateSubgroups: false, + canCreateProjects: false, + initialSort: 'name_asc', }; const routerMock = { @@ -31,12 +49,15 @@ describe('OverviewTabs', () => { const createComponent = async ({ route = { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } }, + provide = {}, } = {}) => { wrapper = mountExtended(OverviewTabs, { router, provide: { - endpoints, + ...defaultProvide, + ...provide, }, + localVue, mocks: { $route: route, $router: routerMock }, }); @@ -47,13 +68,13 @@ describe('OverviewTabs', () => { const findTab = (name) => wrapper.findByRole('tab', { name }); const findSelectedTab = () => wrapper.findByRole('tab', { selected: true }); - afterEach(() => { - wrapper.destroy(); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); }); - beforeEach(async () => { - // eslint-disable-next-line no-new - new AxiosMockAdapter(axios); + afterEach(() => { + wrapper.destroy(); + axiosMock.restore(); }); it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => { @@ -68,7 +89,7 @@ describe('OverviewTabs', () => { expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, store: new GroupsStore({ showSchemaMarkup: true }), - service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), + service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), hideProjects: false, renderEmptyState: true, }); @@ -89,7 +110,7 @@ describe('OverviewTabs', () => { expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ action: ACTIVE_TAB_SHARED, store: new GroupsStore(), - service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]), + service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]), hideProjects: false, renderEmptyState: false, }); @@ -112,7 +133,7 @@ describe('OverviewTabs', () => { expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ action: ACTIVE_TAB_ARCHIVED, store: new GroupsStore(), - service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]), + service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]), hideProjects: false, renderEmptyState: false, }); @@ -120,6 +141,14 @@ describe('OverviewTabs', () => { expect(tabPanel.vm.$attrs.lazy).toBe(false); }); + it('sets `lazy` prop to `false` for initially active tab and `true` for all other tabs', async () => { + await createComponent({ route: { name: ACTIVE_TAB_SHARED, params: { group: 'foo/bar' } } }); + + expect(findTabPanels().at(0).vm.$attrs.lazy).toBe(true); + expect(findTabPanels().at(1).vm.$attrs.lazy).toBe(false); + expect(findTabPanels().at(2).vm.$attrs.lazy).toBe(true); + }); + describe.each([ [ { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } }, @@ -184,4 +213,109 @@ describe('OverviewTabs', () => { expect(routerMock.push).toHaveBeenCalledWith(expectedRoute); }); }); + + describe('searching and sorting', () => { + const setup = async () => { + jest.spyOn(eventHub, '$emit'); + await createComponent(); + + // Click through tabs so they are all loaded + await findTab(OverviewTabs.i18n[ACTIVE_TAB_SHARED]).trigger('click'); + await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click'); + await findTab(OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]).trigger('click'); + }; + + const sharedAssertions = ({ search, sort }) => { + it('sets `lazy` prop to `true` for all of the non-active tabs so they are reloaded after sort or search is applied', () => { + expect(findTabPanels().at(0).vm.$attrs.lazy).toBe(false); + expect(findTabPanels().at(1).vm.$attrs.lazy).toBe(true); + expect(findTabPanels().at(2).vm.$attrs.lazy).toBe(true); + }); + + it('emits `fetchFilteredAndSortedGroups` event from `eventHub`', () => { + expect(eventHub.$emit).toHaveBeenCalledWith( + `${ACTIVE_TAB_SUBGROUPS_AND_PROJECTS}fetchFilteredAndSortedGroups`, + { + filterGroupsBy: search, + sortBy: sort, + }, + ); + }); + }; + + describe('when search is typed in', () => { + const search = 'Foo bar'; + + beforeEach(async () => { + await setup(); + await wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).setValue(search); + }); + + it('updates query string with `filter` key', () => { + expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } }); + }); + + sharedAssertions({ search, sort: defaultProvide.initialSort }); + }); + + describe('when sort is changed', () => { + beforeEach(async () => { + await setup(); + wrapper.findAllComponents(GlSortingItem).at(2).vm.$emit('click'); + await nextTick(); + }); + + it('updates query string with `sort` key', () => { + expect(routerMock.push).toHaveBeenCalledWith({ + query: { sort: SORTING_ITEM_UPDATED.asc }, + }); + }); + + sharedAssertions({ search: '', sort: SORTING_ITEM_UPDATED.asc }); + }); + + describe('when sort direction is changed', () => { + beforeEach(async () => { + await setup(); + await wrapper + .findByRole('button', { name: 'Sorting Direction: Ascending' }) + .trigger('click'); + }); + + it('updates query string with `sort` key', () => { + expect(routerMock.push).toHaveBeenCalledWith({ + query: { sort: SORTING_ITEM_NAME.desc }, + }); + }); + + sharedAssertions({ search: '', sort: SORTING_ITEM_NAME.desc }); + }); + + describe('when `filter` and `sort` query strings are set', () => { + beforeEach(async () => { + await createComponent({ + route: { + name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + params: { group: 'foo/bar/baz' }, + query: { filter: 'Foo bar', sort: SORTING_ITEM_UPDATED.desc }, + }, + }); + }); + + it('sets value of search input', () => { + expect( + wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).element.value, + ).toBe('Foo bar'); + }); + + it('sets sort dropdown', () => { + expect(wrapper.findComponent(GlSorting).props()).toMatchObject({ + text: SORTING_ITEM_UPDATED.label, + isAscending: false, + }); + + expect(wrapper.findAllComponents(GlSortingItem).at(2).vm.$attrs.active).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js index 8cfe8ce8e18..7cbe6e5bbab 100644 --- a/spec/frontend/groups/components/transfer_group_form_spec.js +++ b/spec/frontend/groups/components/transfer_group_form_spec.js @@ -2,7 +2,7 @@ import { GlAlert, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import Component from '~/groups/components/transfer_group_form.vue'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; -import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue'; +import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue'; describe('Transfer group form', () => { let wrapper; |