diff options
Diffstat (limited to 'spec/frontend/groups/components')
-rw-r--r-- | spec/frontend/groups/components/app_spec.js | 507 | ||||
-rw-r--r-- | spec/frontend/groups/components/group_folder_spec.js | 65 | ||||
-rw-r--r-- | spec/frontend/groups/components/group_item_spec.js | 215 | ||||
-rw-r--r-- | spec/frontend/groups/components/groups_spec.js | 72 | ||||
-rw-r--r-- | spec/frontend/groups/components/item_actions_spec.js | 84 | ||||
-rw-r--r-- | spec/frontend/groups/components/item_caret_spec.js | 38 | ||||
-rw-r--r-- | spec/frontend/groups/components/item_stats_spec.js | 119 | ||||
-rw-r--r-- | spec/frontend/groups/components/item_stats_value_spec.js | 82 | ||||
-rw-r--r-- | spec/frontend/groups/components/item_type_icon_spec.js | 53 |
9 files changed, 1235 insertions, 0 deletions
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js new file mode 100644 index 00000000000..35eda21e047 --- /dev/null +++ b/spec/frontend/groups/components/app_spec.js @@ -0,0 +1,507 @@ +import '~/flash'; +import $ from 'jquery'; +import Vue from 'vue'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import appComponent from '~/groups/components/app.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import eventHub from '~/groups/event_hub'; +import GroupsStore from '~/groups/store/groups_store'; +import GroupsService from '~/groups/service/groups_service'; +import * as urlUtilities from '~/lib/utils/url_utility'; + +import { + mockEndpoint, + mockGroups, + mockSearchedGroups, + mockRawPageInfo, + mockParentGroupItem, + mockRawChildren, + mockChildren, + mockPageInfo, +} from '../mock_data'; + +const createComponent = (hideProjects = false) => { + const Component = Vue.extend(appComponent); + const store = new GroupsStore(false); + const service = new GroupsService(mockEndpoint); + + store.state.pageInfo = mockPageInfo; + + return new Component({ + propsData: { + store, + service, + hideProjects, + }, + }); +}; + +describe('AppComponent', () => { + let vm; + let mock; + let getGroupsSpy; + + beforeEach(() => { + mock = new AxiosMockAdapter(axios); + mock.onGet('/dashboard/groups.json').reply(200, mockGroups); + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + getGroupsSpy = jest.spyOn(vm.service, 'getGroups'); + return vm.$nextTick(); + }); + + describe('computed', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('groups', () => { + it('should return list of groups from store', () => { + jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {}); + + const { groups } = vm; + + expect(vm.store.getGroups).toHaveBeenCalled(); + expect(groups).not.toBeDefined(); + }); + }); + + describe('pageInfo', () => { + it('should return pagination info from store', () => { + jest.spyOn(vm.store, 'getPaginationInfo').mockImplementation(() => {}); + + const { pageInfo } = vm; + + expect(vm.store.getPaginationInfo).toHaveBeenCalled(); + expect(pageInfo).not.toBeDefined(); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('fetchGroups', () => { + it('should call `getGroups` with all the params provided', () => { + return vm + .fetchGroups({ + parentId: 1, + page: 2, + filterGroupsBy: 'git', + sortBy: 'created_desc', + archived: true, + }) + .then(() => { + expect(getGroupsSpy).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true); + }); + }); + + it('should set headers to store for building pagination info when called with `updatePagination`', () => { + mock.onGet('/dashboard/groups.json').reply(200, { headers: mockRawPageInfo }); + + jest.spyOn(vm, 'updatePagination').mockImplementation(() => {}); + + return vm.fetchGroups({ updatePagination: true }).then(() => { + expect(getGroupsSpy).toHaveBeenCalled(); + expect(vm.updatePagination).toHaveBeenCalled(); + }); + }); + + it('should show flash error when request fails', () => { + mock.onGet('/dashboard/groups.json').reply(400); + + jest.spyOn($, 'scrollTo').mockImplementation(() => {}); + jest.spyOn(window, 'Flash').mockImplementation(() => {}); + + return vm.fetchGroups({}).then(() => { + expect(vm.isLoading).toBe(false); + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.'); + }); + }); + }); + + describe('fetchAllGroups', () => { + beforeEach(() => { + jest.spyOn(vm, 'fetchGroups'); + jest.spyOn(vm, 'updateGroups'); + }); + + it('should fetch default set of groups', () => { + jest.spyOn(vm, 'updatePagination'); + + const fetchPromise = vm.fetchAllGroups(); + + expect(vm.isLoading).toBe(true); + + return fetchPromise.then(() => { + expect(vm.isLoading).toBe(false); + expect(vm.updateGroups).toHaveBeenCalled(); + }); + }); + + it('should fetch matching set of groups when app is loaded with search query', () => { + mock.onGet('/dashboard/groups.json').reply(200, mockSearchedGroups); + + const fetchPromise = vm.fetchAllGroups(); + + expect(vm.fetchGroups).toHaveBeenCalledWith({ + page: null, + filterGroupsBy: null, + sortBy: null, + updatePagination: true, + archived: null, + }); + return fetchPromise.then(() => { + expect(vm.updateGroups).toHaveBeenCalled(); + }); + }); + }); + + describe('fetchPage', () => { + beforeEach(() => { + jest.spyOn(vm, 'fetchGroups'); + jest.spyOn(vm, 'updateGroups'); + }); + + it('should fetch groups for provided page details and update window state', () => { + jest.spyOn(urlUtilities, 'mergeUrlParams'); + jest.spyOn(window.history, 'replaceState').mockImplementation(() => {}); + jest.spyOn($, 'scrollTo').mockImplementation(() => {}); + + const fetchPagePromise = vm.fetchPage(2, null, null, true); + + expect(vm.isLoading).toBe(true); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + page: 2, + filterGroupsBy: null, + sortBy: null, + updatePagination: true, + archived: true, + }); + + return fetchPagePromise.then(() => { + expect(vm.isLoading).toBe(false); + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(urlUtilities.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, expect.any(String)); + expect(window.history.replaceState).toHaveBeenCalledWith( + { + page: expect.any(String), + }, + expect.any(String), + expect.any(String), + ); + + expect(vm.updateGroups).toHaveBeenCalled(); + }); + }); + }); + + describe('toggleChildren', () => { + let groupItem; + + beforeEach(() => { + groupItem = { ...mockParentGroupItem }; + groupItem.isOpen = false; + groupItem.isChildrenLoading = false; + }); + + it('should fetch children of given group and expand it if group is collapsed and children are not loaded', () => { + mock.onGet('/dashboard/groups.json').reply(200, mockRawChildren); + jest.spyOn(vm, 'fetchGroups'); + jest.spyOn(vm.store, 'setGroupChildren').mockImplementation(() => {}); + + vm.toggleChildren(groupItem); + + expect(groupItem.isChildrenLoading).toBe(true); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + parentId: groupItem.id, + }); + return waitForPromises().then(() => { + expect(vm.store.setGroupChildren).toHaveBeenCalled(); + }); + }); + + it('should skip network request while expanding group if children are already loaded', () => { + jest.spyOn(vm, 'fetchGroups'); + groupItem.children = mockRawChildren; + + vm.toggleChildren(groupItem); + + expect(vm.fetchGroups).not.toHaveBeenCalled(); + expect(groupItem.isOpen).toBe(true); + }); + + it('should collapse group if it is already expanded', () => { + jest.spyOn(vm, 'fetchGroups'); + groupItem.isOpen = true; + + vm.toggleChildren(groupItem); + + expect(vm.fetchGroups).not.toHaveBeenCalled(); + expect(groupItem.isOpen).toBe(false); + }); + + it('should set `isChildrenLoading` back to `false` if load request fails', () => { + mock.onGet('/dashboard/groups.json').reply(400); + + vm.toggleChildren(groupItem); + + expect(groupItem.isChildrenLoading).toBe(true); + return waitForPromises().then(() => { + expect(groupItem.isChildrenLoading).toBe(false); + }); + }); + }); + + describe('showLeaveGroupModal', () => { + it('caches candidate group (as props) which is to be left', () => { + const group = { ...mockParentGroupItem }; + + expect(vm.targetGroup).toBe(null); + expect(vm.targetParentGroup).toBe(null); + vm.showLeaveGroupModal(group, mockParentGroupItem); + + expect(vm.targetGroup).not.toBe(null); + expect(vm.targetParentGroup).not.toBe(null); + }); + + it('updates props which show modal confirmation dialog', () => { + const group = { ...mockParentGroupItem }; + + expect(vm.showModal).toBe(false); + expect(vm.groupLeaveConfirmationMessage).toBe(''); + vm.showLeaveGroupModal(group, mockParentGroupItem); + + expect(vm.showModal).toBe(true); + expect(vm.groupLeaveConfirmationMessage).toBe( + `Are you sure you want to leave the "${group.fullName}" group?`, + ); + }); + }); + + describe('hideLeaveGroupModal', () => { + it('hides modal confirmation which is shown before leaving the group', () => { + const group = { ...mockParentGroupItem }; + vm.showLeaveGroupModal(group, mockParentGroupItem); + + expect(vm.showModal).toBe(true); + vm.hideLeaveGroupModal(); + + expect(vm.showModal).toBe(false); + }); + }); + + describe('leaveGroup', () => { + let groupItem; + let childGroupItem; + + beforeEach(() => { + groupItem = { ...mockParentGroupItem }; + groupItem.children = mockChildren; + [childGroupItem] = groupItem.children; + groupItem.isChildrenLoading = false; + vm.targetGroup = childGroupItem; + vm.targetParentGroup = groupItem; + }); + + it('hides modal confirmation leave group and remove group item from tree', () => { + const notice = `You left the "${childGroupItem.fullName}" group.`; + jest.spyOn(vm.service, 'leaveGroup').mockResolvedValue({ data: { notice } }); + jest.spyOn(vm.store, 'removeGroup'); + jest.spyOn(window, 'Flash').mockImplementation(() => {}); + jest.spyOn($, 'scrollTo').mockImplementation(() => {}); + + vm.leaveGroup(); + + expect(vm.showModal).toBe(false); + expect(vm.targetGroup.isBeingRemoved).toBe(true); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); + return waitForPromises().then(() => { + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup); + expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); + }); + }); + + it('should show error flash message if request failed to leave group', () => { + const message = 'An error occurred. Please try again.'; + jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 }); + jest.spyOn(vm.store, 'removeGroup'); + jest.spyOn(window, 'Flash').mockImplementation(() => {}); + + vm.leaveGroup(); + + expect(vm.targetGroup.isBeingRemoved).toBe(true); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + return waitForPromises().then(() => { + expect(vm.store.removeGroup).not.toHaveBeenCalled(); + expect(window.Flash).toHaveBeenCalledWith(message); + expect(vm.targetGroup.isBeingRemoved).toBe(false); + }); + }); + + it('should show appropriate error flash message if request forbids to leave group', () => { + const message = 'Failed to leave the group. Please make sure you are not the only owner.'; + jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 }); + jest.spyOn(vm.store, 'removeGroup'); + jest.spyOn(window, 'Flash').mockImplementation(() => {}); + + vm.leaveGroup(childGroupItem, groupItem); + + expect(vm.targetGroup.isBeingRemoved).toBe(true); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + return waitForPromises().then(() => { + expect(vm.store.removeGroup).not.toHaveBeenCalled(); + expect(window.Flash).toHaveBeenCalledWith(message); + expect(vm.targetGroup.isBeingRemoved).toBe(false); + }); + }); + }); + + describe('updatePagination', () => { + it('should set pagination info to store from provided headers', () => { + jest.spyOn(vm.store, 'setPaginationInfo').mockImplementation(() => {}); + + vm.updatePagination(mockRawPageInfo); + + expect(vm.store.setPaginationInfo).toHaveBeenCalledWith(mockRawPageInfo); + }); + }); + + describe('updateGroups', () => { + it('should call setGroups on store if method was called directly', () => { + jest.spyOn(vm.store, 'setGroups').mockImplementation(() => {}); + + vm.updateGroups(mockGroups); + + expect(vm.store.setGroups).toHaveBeenCalledWith(mockGroups); + }); + + it('should call setSearchedGroups on store if method was called with fromSearch param', () => { + jest.spyOn(vm.store, 'setSearchedGroups').mockImplementation(() => {}); + + vm.updateGroups(mockGroups, true); + + expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups); + }); + + it('should set `isSearchEmpty` prop based on groups count', () => { + vm.updateGroups(mockGroups); + + expect(vm.isSearchEmpty).toBe(false); + + vm.updateGroups([]); + + expect(vm.isSearchEmpty).toBe(true); + }); + }); + }); + + describe('created', () => { + it('should bind event listeners on eventHub', () => { + jest.spyOn(eventHub, '$on').mockImplementation(() => {}); + + const newVm = createComponent(); + newVm.$mount(); + + return vm.$nextTick().then(() => { + expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function)); + newVm.$destroy(); + }); + }); + + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', () => { + const newVm = createComponent(); + newVm.$mount(); + return vm.$nextTick().then(() => { + expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search'); + newVm.$destroy(); + }); + }); + + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', () => { + const newVm = createComponent(true); + newVm.$mount(); + return vm.$nextTick().then(() => { + expect(newVm.searchEmptyMessage).toBe('No groups matched your search'); + newVm.$destroy(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', () => { + jest.spyOn(eventHub, '$off').mockImplementation(() => {}); + + const newVm = createComponent(); + newVm.$mount(); + newVm.$destroy(); + + return vm.$nextTick().then(() => { + expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function)); + }); + }); + }); + + describe('template', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render loading icon', () => { + vm.isLoading = true; + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); + expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups'); + }); + }); + + it('should render groups tree', () => { + vm.store.state.groups = [mockParentGroupItem]; + vm.isLoading = false; + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); + }); + }); + + it('renders modal confirmation dialog', () => { + vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; + vm.showModal = true; + return vm.$nextTick().then(() => { + const modalDialogEl = vm.$el.querySelector('.modal'); + + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + }); + }); + }); +}); diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js new file mode 100644 index 00000000000..a40fa9bece8 --- /dev/null +++ b/spec/frontend/groups/components/group_folder_spec.js @@ -0,0 +1,65 @@ +import Vue from 'vue'; + +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import { mockGroups, mockParentGroupItem } from '../mock_data'; + +const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem) => { + const Component = Vue.extend(groupFolderComponent); + + return new Component({ + propsData: { + groups, + parentGroup, + }, + }); +}; + +describe('GroupFolderComponent', () => { + let vm; + + beforeEach(() => { + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + vm.$mount(); + + return Vue.nextTick(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hasMoreChildren', () => { + it('should return false when childrenCount of group is less than MAX_CHILDREN_COUNT', () => { + expect(vm.hasMoreChildren).toBeFalsy(); + }); + }); + + describe('moreChildrenStats', () => { + it('should return message with count of excess children over MAX_CHILDREN_COUNT limit', () => { + expect(vm.moreChildrenStats).toBe('3 more items'); + }); + }); + }); + + 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('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => { + const parentGroup = { ...mockParentGroupItem }; + parentGroup.childrenCount = 21; + + const newVm = createComponent(mockGroups, parentGroup); + newVm.$mount(); + + expect(newVm.$el.querySelector('li.group-row a.has-more-items')).toBeDefined(); + newVm.$destroy(); + }); + }); +}); diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js new file mode 100644 index 00000000000..7eb1c54ddb2 --- /dev/null +++ b/spec/frontend/groups/components/group_item_spec.js @@ -0,0 +1,215 @@ +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import eventHub from '~/groups/event_hub'; +import * as urlUtilities from '~/lib/utils/url_utility'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { + const Component = Vue.extend(groupItemComponent); + + return mountComponent(Component, { + group, + parentGroup, + }); +}; + +describe('GroupItemComponent', () => { + let vm; + + beforeEach(() => { + Vue.component('group-folder', groupFolderComponent); + + vm = createComponent(); + + return Vue.nextTick(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('groupDomId', () => { + it('should return ID string suffixed with group ID', () => { + expect(vm.groupDomId).toBe('group-55'); + }); + }); + + describe('rowClass', () => { + it('should return map of classes based on group details', () => { + const classes = ['is-open', 'has-children', 'has-description', 'being-removed']; + const { rowClass } = vm; + + expect(Object.keys(rowClass).length).toBe(classes.length); + Object.keys(rowClass).forEach(className => { + expect(classes.indexOf(className)).toBeGreaterThan(-1); + }); + }); + }); + + describe('hasChildren', () => { + it('should return boolean value representing if group has any children present', () => { + let newVm; + const group = { ...mockParentGroupItem }; + + group.childrenCount = 5; + newVm = createComponent(group); + + expect(newVm.hasChildren).toBeTruthy(); + newVm.$destroy(); + + group.childrenCount = 0; + newVm = createComponent(group); + + expect(newVm.hasChildren).toBeFalsy(); + newVm.$destroy(); + }); + }); + + describe('hasAvatar', () => { + it('should return boolean value representing if group has any avatar present', () => { + let newVm; + const group = { ...mockParentGroupItem }; + + group.avatarUrl = null; + newVm = createComponent(group); + + expect(newVm.hasAvatar).toBeFalsy(); + newVm.$destroy(); + + group.avatarUrl = '/uploads/group_avatar.png'; + newVm = createComponent(group); + + expect(newVm.hasAvatar).toBeTruthy(); + newVm.$destroy(); + }); + }); + + describe('isGroup', () => { + it('should return boolean value representing if group item is of type `group` or not', () => { + let newVm; + const group = { ...mockParentGroupItem }; + + group.type = 'group'; + newVm = createComponent(group); + + expect(newVm.isGroup).toBeTruthy(); + newVm.$destroy(); + + group.type = 'project'; + newVm = createComponent(group); + + expect(newVm.isGroup).toBeFalsy(); + newVm.$destroy(); + }); + }); + }); + + describe('methods', () => { + describe('onClickRowGroup', () => { + let event; + + beforeEach(() => { + const classList = { + contains() { + return false; + }, + }; + + event = { + target: { + classList, + parentElement: { + classList, + }, + }, + }; + }); + + it('should emit `toggleChildren` event when expand is clicked on a group and it has children present', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + vm.onClickRowGroup(event); + + expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', vm.group); + }); + + it('should navigate page to group homepage if group does not have any children present', () => { + jest.spyOn(urlUtilities, 'visitUrl').mockImplementation(); + const group = { ...mockParentGroupItem }; + group.childrenCount = 0; + const newVm = createComponent(group); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + newVm.onClickRowGroup(event); + + expect(eventHub.$emit).not.toHaveBeenCalled(); + expect(urlUtilities.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); + }); + }); + }); + + describe('template', () => { + let group = null; + + describe('for a group pending deletion', () => { + beforeEach(() => { + group = { ...mockParentGroupItem, pendingRemoval: true }; + vm = createComponent(group); + }); + + it('renders the group pending removal badge', () => { + const badgeEl = vm.$el.querySelector('.badge-warning'); + + expect(badgeEl).toBeDefined(); + expect(badgeEl.innerHTML).toContain('pending removal'); + }); + }); + + describe('for a group not scheduled for deletion', () => { + beforeEach(() => { + group = { ...mockParentGroupItem, pendingRemoval: false }; + vm = createComponent(group); + }); + + it('does not render the group pending removal badge', () => { + const groupTextContainer = vm.$el.querySelector('.group-text-container'); + + expect(groupTextContainer).not.toContain('pending removal'); + }); + }); + + it('should render component template correctly', () => { + const visibilityIconEl = vm.$el.querySelector('.item-visibility'); + + expect(vm.$el.getAttribute('id')).toBe('group-55'); + expect(vm.$el.classList.contains('group-row')).toBeTruthy(); + + expect(vm.$el.querySelector('.group-row-contents')).toBeDefined(); + expect(vm.$el.querySelector('.group-row-contents .controls')).toBeDefined(); + expect(vm.$el.querySelector('.group-row-contents .stats')).toBeDefined(); + + expect(vm.$el.querySelector('.folder-toggle-wrap')).toBeDefined(); + expect(vm.$el.querySelector('.folder-toggle-wrap .folder-caret')).toBeDefined(); + expect(vm.$el.querySelector('.folder-toggle-wrap .item-type-icon')).toBeDefined(); + + expect(vm.$el.querySelector('.avatar-container')).toBeDefined(); + expect(vm.$el.querySelector('.avatar-container a.no-expand')).toBeDefined(); + expect(vm.$el.querySelector('.avatar-container .avatar')).toBeDefined(); + + expect(vm.$el.querySelector('.title')).toBeDefined(); + expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined(); + + expect(visibilityIconEl).not.toBe(null); + expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); + expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); + + expect(vm.$el.querySelector('.access-type')).toBeDefined(); + expect(vm.$el.querySelector('.description')).toBeDefined(); + + expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); + }); + }); +}); diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js new file mode 100644 index 00000000000..6205400eb03 --- /dev/null +++ b/spec/frontend/groups/components/groups_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; + +import mountComponent from 'helpers/vue_mount_component_helper'; +import groupsComponent from '~/groups/components/groups.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import eventHub from '~/groups/event_hub'; +import { mockGroups, mockPageInfo } from '../mock_data'; + +const createComponent = (searchEmpty = false) => { + const Component = Vue.extend(groupsComponent); + + return mountComponent(Component, { + groups: mockGroups, + pageInfo: mockPageInfo, + searchEmptyMessage: 'No matching results', + searchEmpty, + }); +}; + +describe('GroupsComponent', () => { + let vm; + + beforeEach(() => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + + return vm.$nextTick(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('change', () => { + it('should emit `fetchPage` event when page is changed via pagination', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(); + + vm.change(2); + + expect(eventHub.$emit).toHaveBeenCalledWith( + 'fetchPage', + 2, + expect.any(Object), + expect.any(Object), + expect.any(Object), + ); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); + expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0); + }); + }); + + it('should render empty search message when `searchEmpty` is `true`', () => { + vm.searchEmpty = true; + return vm.$nextTick().then(() => { + expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); + }); + }); + }); +}); diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js new file mode 100644 index 00000000000..c0dc1a816e6 --- /dev/null +++ b/spec/frontend/groups/components/item_actions_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; + +import mountComponent from 'helpers/vue_mount_component_helper'; +import itemActionsComponent from '~/groups/components/item_actions.vue'; +import eventHub from '~/groups/event_hub'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { + const Component = Vue.extend(itemActionsComponent); + + return mountComponent(Component, { + group, + parentGroup, + }); +}; + +describe('ItemActionsComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('onLeaveGroup', () => { + it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + vm.onLeaveGroup(); + + expect(eventHub.$emit).toHaveBeenCalledWith( + 'showLeaveGroupModal', + vm.group, + vm.parentGroup, + ); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + expect(vm.$el.classList.contains('controls')).toBeTruthy(); + }); + + it('should render Edit Group button with correct attribute values', () => { + const group = { ...mockParentGroupItem }; + group.canEdit = true; + const newVm = createComponent(group); + + const editBtn = newVm.$el.querySelector('a.edit-group'); + + expect(editBtn).toBeDefined(); + expect(editBtn.classList.contains('no-expand')).toBeTruthy(); + expect(editBtn.getAttribute('href')).toBe(group.editPath); + expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); + expect(editBtn.dataset.originalTitle).toBe('Edit group'); + expect(editBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings'); + + newVm.$destroy(); + }); + + it('should render Leave Group button with correct attribute values', () => { + const group = { ...mockParentGroupItem }; + group.canLeave = true; + const newVm = createComponent(group); + + const leaveBtn = newVm.$el.querySelector('a.leave-group'); + + expect(leaveBtn).toBeDefined(); + expect(leaveBtn.classList.contains('no-expand')).toBeTruthy(); + expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); + expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); + expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); + expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave'); + + newVm.$destroy(); + }); + }); +}); diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js new file mode 100644 index 00000000000..bfe27be9b51 --- /dev/null +++ b/spec/frontend/groups/components/item_caret_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; + +import mountComponent from 'helpers/vue_mount_component_helper'; +import itemCaretComponent from '~/groups/components/item_caret.vue'; + +const createComponent = (isGroupOpen = false) => { + const Component = Vue.extend(itemCaretComponent); + + return mountComponent(Component, { + isGroupOpen, + }); +}; + +describe('ItemCaretComponent', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('should render component template correctly', () => { + vm = createComponent(); + expect(vm.$el.classList.contains('folder-caret')).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length).toBe(1); + }); + + it('should render caret down icon if `isGroupOpen` prop is `true`', () => { + vm = createComponent(true); + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down'); + }); + + it('should render caret right icon if `isGroupOpen` prop is `false`', () => { + vm = createComponent(); + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right'); + }); + }); +}); diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js new file mode 100644 index 00000000000..771643609ec --- /dev/null +++ b/spec/frontend/groups/components/item_stats_spec.js @@ -0,0 +1,119 @@ +import Vue from 'vue'; + +import mountComponent from 'helpers/vue_mount_component_helper'; +import itemStatsComponent from '~/groups/components/item_stats.vue'; +import { + mockParentGroupItem, + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, +} from '../mock_data'; + +const createComponent = (item = mockParentGroupItem) => { + const Component = Vue.extend(itemStatsComponent); + + return mountComponent(Component, { + item, + }); +}; + +describe('ItemStatsComponent', () => { + describe('computed', () => { + describe('visibilityIcon', () => { + it('should return icon class based on `item.visibility` value', () => { + Object.keys(VISIBILITY_TYPE_ICON).forEach(visibility => { + const item = { ...mockParentGroupItem, visibility }; + const vm = createComponent(item); + + expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]); + vm.$destroy(); + }); + }); + }); + + describe('visibilityTooltip', () => { + it('should return tooltip string for Group based on `item.visibility` value', () => { + Object.keys(GROUP_VISIBILITY_TYPE).forEach(visibility => { + const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.GROUP }; + const vm = createComponent(item); + + expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]); + vm.$destroy(); + }); + }); + + it('should return tooltip string for Project based on `item.visibility` value', () => { + Object.keys(PROJECT_VISIBILITY_TYPE).forEach(visibility => { + const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.PROJECT }; + const vm = createComponent(item); + + expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]); + vm.$destroy(); + }); + }); + }); + + describe('isProject', () => { + it('should return boolean value representing whether `item.type` is Project or not', () => { + let item; + let vm; + + item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT }; + vm = createComponent(item); + + expect(vm.isProject).toBeTruthy(); + vm.$destroy(); + + item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP }; + vm = createComponent(item); + + expect(vm.isProject).toBeFalsy(); + vm.$destroy(); + }); + }); + + describe('isGroup', () => { + it('should return boolean value representing whether `item.type` is Group or not', () => { + let item; + let vm; + + item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP }; + vm = createComponent(item); + + expect(vm.isGroup).toBeTruthy(); + vm.$destroy(); + + item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT }; + vm = createComponent(item); + + expect(vm.isGroup).toBeFalsy(); + vm.$destroy(); + }); + }); + }); + + describe('template', () => { + it('renders component container element correctly', () => { + const vm = createComponent(); + + expect(vm.$el.classList.contains('stats')).toBeTruthy(); + + vm.$destroy(); + }); + + it('renders start count and last updated information for project item correctly', () => { + const item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT, starCount: 4 }; + const vm = createComponent(item); + + const projectStarIconEl = vm.$el.querySelector('.project-stars'); + + expect(projectStarIconEl).not.toBeNull(); + expect(projectStarIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); + expect(projectStarIconEl.querySelectorAll('.stat-value').length).toBeGreaterThan(0); + expect(vm.$el.querySelectorAll('.last-updated').length).toBeGreaterThan(0); + + vm.$destroy(); + }); + }); +}); diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js new file mode 100644 index 00000000000..da6f145fa19 --- /dev/null +++ b/spec/frontend/groups/components/item_stats_value_spec.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; + +import mountComponent from 'helpers/vue_mount_component_helper'; +import itemStatsValueComponent from '~/groups/components/item_stats_value.vue'; + +const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => { + const Component = Vue.extend(itemStatsValueComponent); + + return mountComponent(Component, { + title, + cssClass, + iconName, + tooltipPlacement, + value, + }); +}; + +describe('ItemStatsValueComponent', () => { + describe('computed', () => { + let vm; + const itemConfig = { + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + }; + + describe('isValuePresent', () => { + it('returns true if non-empty `value` is present', () => { + vm = createComponent({ ...itemConfig, value: 10 }); + + expect(vm.isValuePresent).toBeTruthy(); + }); + + it('returns false if empty `value` is present', () => { + vm = createComponent(itemConfig); + + expect(vm.isValuePresent).toBeFalsy(); + }); + + afterEach(() => { + vm.$destroy(); + }); + }); + }); + + describe('template', () => { + let vm; + beforeEach(() => { + vm = createComponent({ + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + value: 10, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders component element correctly', () => { + expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length).toBeGreaterThan(0); + expect(vm.$el.querySelectorAll('.stat-value').length).toBeGreaterThan(0); + }); + + it('renders element tooltip correctly', () => { + expect(vm.$el.dataset.originalTitle).toBe('Subgroups'); + expect(vm.$el.dataset.placement).toBe('left'); + }); + + it('renders element icon correctly', () => { + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder'); + }); + + it('renders value count correctly', () => { + expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10'); + }); + }); +}); diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js new file mode 100644 index 00000000000..251b5b5ff4c --- /dev/null +++ b/spec/frontend/groups/components/item_type_icon_spec.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; + +import mountComponent from 'helpers/vue_mount_component_helper'; +import itemTypeIconComponent from '~/groups/components/item_type_icon.vue'; +import { ITEM_TYPE } from '../mock_data'; + +const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => { + const Component = Vue.extend(itemTypeIconComponent); + + return mountComponent(Component, { + itemType, + isGroupOpen, + }); +}; + +describe('ItemTypeIconComponent', () => { + describe('template', () => { + it('should render component template correctly', () => { + const vm = createComponent(); + + expect(vm.$el.classList.contains('item-type-icon')).toBeTruthy(); + vm.$destroy(); + }); + + it('should render folder open or close icon based `isGroupOpen` prop value', () => { + let vm; + + vm = createComponent(ITEM_TYPE.GROUP, true); + + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open'); + vm.$destroy(); + + vm = createComponent(ITEM_TYPE.GROUP); + + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder'); + vm.$destroy(); + }); + + it('should render bookmark icon based on `isProject` prop value', () => { + let vm; + + vm = createComponent(ITEM_TYPE.PROJECT); + + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark'); + vm.$destroy(); + + vm = createComponent(ITEM_TYPE.GROUP); + + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark'); + vm.$destroy(); + }); + }); +}); |