diff options
author | Kushal Pandya <kushalspandya@gmail.com> | 2017-10-04 17:10:24 +0300 |
---|---|---|
committer | Bob Van Landuyt <bob@vanlanduyt.co> | 2017-10-04 23:49:42 +0300 |
commit | de55396134e9e3de429c5c6df55ff06efb8ba329 (patch) | |
tree | 30060acfecfd668c29135d45958af820e7aaa840 /spec/javascripts/groups | |
parent | 67815272dceb971c03bea3490ec26529b48a52b4 (diff) |
Groups tree enhancements for Groups Dashboard and Group Homepage
Diffstat (limited to 'spec/javascripts/groups')
-rw-r--r-- | spec/javascripts/groups/components/app_spec.js | 440 | ||||
-rw-r--r-- | spec/javascripts/groups/components/group_folder_spec.js | 66 | ||||
-rw-r--r-- | spec/javascripts/groups/components/group_item_spec.js | 177 | ||||
-rw-r--r-- | spec/javascripts/groups/components/groups_spec.js | 70 | ||||
-rw-r--r-- | spec/javascripts/groups/components/item_actions_spec.js | 110 | ||||
-rw-r--r-- | spec/javascripts/groups/components/item_caret_spec.js | 40 | ||||
-rw-r--r-- | spec/javascripts/groups/components/item_stats_spec.js | 159 | ||||
-rw-r--r-- | spec/javascripts/groups/components/item_type_icon_spec.js | 54 | ||||
-rw-r--r-- | spec/javascripts/groups/group_item_spec.js | 102 | ||||
-rw-r--r-- | spec/javascripts/groups/groups_spec.js | 99 | ||||
-rw-r--r-- | spec/javascripts/groups/mock_data.js | 470 | ||||
-rw-r--r-- | spec/javascripts/groups/service/groups_service_spec.js | 41 | ||||
-rw-r--r-- | spec/javascripts/groups/store/groups_store_spec.js | 110 |
13 files changed, 1635 insertions, 303 deletions
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js new file mode 100644 index 00000000000..8472c726b08 --- /dev/null +++ b/spec/javascripts/groups/components/app_spec.js @@ -0,0 +1,440 @@ +import Vue from 'vue'; + +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 { + 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); + + return new Component({ + propsData: { + store, + service, + hideProjects, + }, + }); +}; + +const returnServicePromise = (data, failed) => new Promise((resolve, reject) => { + if (failed) { + reject(data); + } else { + resolve({ + json() { + return data; + }, + }); + } +}); + +describe('AppComponent', () => { + let vm; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + + Vue.nextTick(() => { + done(); + }); + }); + + describe('computed', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('groups', () => { + it('should return list of groups from store', () => { + spyOn(vm.store, 'getGroups'); + + const groups = vm.groups; + expect(vm.store.getGroups).toHaveBeenCalled(); + expect(groups).not.toBeDefined(); + }); + }); + + describe('pageInfo', () => { + it('should return pagination info from store', () => { + spyOn(vm.store, 'getPaginationInfo'); + + const pageInfo = vm.pageInfo; + 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', (done) => { + spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(mockGroups)); + + vm.fetchGroups({ + parentId: 1, + page: 2, + filterGroupsBy: 'git', + sortBy: 'created_desc', + }); + setTimeout(() => { + expect(vm.service.getGroups).toHaveBeenCalledWith(1, 2, 'git', 'created_desc'); + done(); + }, 0); + }); + + it('should set headers to store for building pagination info when called with `updatePagination`', (done) => { + spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise({ headers: mockRawPageInfo })); + spyOn(vm, 'updatePagination'); + + vm.fetchGroups({ updatePagination: true }); + setTimeout(() => { + expect(vm.service.getGroups).toHaveBeenCalled(); + expect(vm.updatePagination).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should show flash error when request fails', (done) => { + spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(null, true)); + spyOn($, 'scrollTo'); + spyOn(window, 'Flash'); + + vm.fetchGroups({}); + setTimeout(() => { + expect(vm.isLoading).toBeFalsy(); + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.'); + done(); + }, 0); + }); + }); + + describe('fetchAllGroups', () => { + it('should fetch default set of groups', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); + spyOn(vm, 'updatePagination').and.callThrough(); + spyOn(vm, 'updateGroups').and.callThrough(); + + vm.fetchAllGroups(); + expect(vm.isLoading).toBeTruthy(); + expect(vm.fetchGroups).toHaveBeenCalled(); + setTimeout(() => { + expect(vm.isLoading).toBeFalsy(); + expect(vm.updateGroups).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should fetch matching set of groups when app is loaded with search query', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockSearchedGroups)); + spyOn(vm, 'updateGroups').and.callThrough(); + + vm.fetchAllGroups(); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + page: null, + filterGroupsBy: null, + sortBy: null, + updatePagination: true, + }); + setTimeout(() => { + expect(vm.updateGroups).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('fetchPage', () => { + it('should fetch groups for provided page details and update window state', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); + spyOn(vm, 'updateGroups').and.callThrough(); + spyOn(gl.utils, 'mergeUrlParams').and.callThrough(); + spyOn(window.history, 'replaceState'); + spyOn($, 'scrollTo'); + + vm.fetchPage(2, null, null); + expect(vm.isLoading).toBeTruthy(); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + page: 2, + filterGroupsBy: null, + sortBy: null, + updatePagination: true, + }); + setTimeout(() => { + expect(vm.isLoading).toBeFalsy(); + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); + expect(window.history.replaceState).toHaveBeenCalledWith({ + page: jasmine.any(String), + }, jasmine.any(String), jasmine.any(String)); + expect(vm.updateGroups).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('toggleChildren', () => { + let groupItem; + + beforeEach(() => { + groupItem = Object.assign({}, 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', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockRawChildren)); + spyOn(vm.store, 'setGroupChildren'); + + vm.toggleChildren(groupItem); + expect(groupItem.isChildrenLoading).toBeTruthy(); + expect(vm.fetchGroups).toHaveBeenCalledWith({ + parentId: groupItem.id, + }); + setTimeout(() => { + expect(vm.store.setGroupChildren).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('should skip network request while expanding group if children are already loaded', () => { + spyOn(vm, 'fetchGroups'); + groupItem.children = mockRawChildren; + + vm.toggleChildren(groupItem); + expect(vm.fetchGroups).not.toHaveBeenCalled(); + expect(groupItem.isOpen).toBeTruthy(); + }); + + it('should collapse group if it is already expanded', () => { + spyOn(vm, 'fetchGroups'); + groupItem.isOpen = true; + + vm.toggleChildren(groupItem); + expect(vm.fetchGroups).not.toHaveBeenCalled(); + expect(groupItem.isOpen).toBeFalsy(); + }); + + it('should set `isChildrenLoading` back to `false` if load request fails', (done) => { + spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true)); + + vm.toggleChildren(groupItem); + expect(groupItem.isChildrenLoading).toBeTruthy(); + setTimeout(() => { + expect(groupItem.isChildrenLoading).toBeFalsy(); + done(); + }, 0); + }); + }); + + describe('leaveGroup', () => { + let groupItem; + let childGroupItem; + + beforeEach(() => { + groupItem = Object.assign({}, mockParentGroupItem); + groupItem.children = mockChildren; + childGroupItem = groupItem.children[0]; + groupItem.isChildrenLoading = false; + }); + + it('should leave group and remove group item from tree', (done) => { + const notice = `You left the "${childGroupItem.fullName}" group.`; + spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); + spyOn(vm.store, 'removeGroup').and.callThrough(); + spyOn(window, 'Flash'); + spyOn($, 'scrollTo'); + + vm.leaveGroup(childGroupItem, groupItem); + expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + setTimeout(() => { + expect($.scrollTo).toHaveBeenCalledWith(0); + expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem); + expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); + done(); + }, 0); + }); + + it('should show error flash message if request failed to leave group', (done) => { + const message = 'An error occurred. Please try again.'; + spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 500 }, true)); + spyOn(vm.store, 'removeGroup').and.callThrough(); + spyOn(window, 'Flash'); + + vm.leaveGroup(childGroupItem, groupItem); + expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + setTimeout(() => { + expect(vm.store.removeGroup).not.toHaveBeenCalled(); + expect(window.Flash).toHaveBeenCalledWith(message); + expect(childGroupItem.isBeingRemoved).toBeFalsy(); + done(); + }, 0); + }); + + it('should show appropriate error flash message if request forbids to leave group', (done) => { + const message = 'Failed to leave the group. Please make sure you are not the only owner.'; + spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 403 }, true)); + spyOn(vm.store, 'removeGroup').and.callThrough(); + spyOn(window, 'Flash'); + + vm.leaveGroup(childGroupItem, groupItem); + expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + setTimeout(() => { + expect(vm.store.removeGroup).not.toHaveBeenCalled(); + expect(window.Flash).toHaveBeenCalledWith(message); + expect(childGroupItem.isBeingRemoved).toBeFalsy(); + done(); + }, 0); + }); + }); + + describe('updatePagination', () => { + it('should set pagination info to store from provided headers', () => { + spyOn(vm.store, 'setPaginationInfo'); + + vm.updatePagination(mockRawPageInfo); + expect(vm.store.setPaginationInfo).toHaveBeenCalledWith(mockRawPageInfo); + }); + }); + + describe('updateGroups', () => { + it('should call setGroups on store if method was called directly', () => { + spyOn(vm.store, 'setGroups'); + + vm.updateGroups(mockGroups); + expect(vm.store.setGroups).toHaveBeenCalledWith(mockGroups); + }); + + it('should call setSearchedGroups on store if method was called with fromSearch param', () => { + spyOn(vm.store, 'setSearchedGroups'); + + 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).toBeFalsy(); + + vm.updateGroups([]); + expect(vm.isSearchEmpty).toBeTruthy(); + }); + }); + }); + + describe('created', () => { + it('should bind event listeners on eventHub', (done) => { + spyOn(eventHub, '$on'); + + const newVm = createComponent(); + newVm.$mount(); + + Vue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); + newVm.$destroy(); + done(); + }); + }); + + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', (done) => { + const newVm = createComponent(); + newVm.$mount(); + Vue.nextTick(() => { + expect(newVm.searchEmptyMessage).toBe('Sorry, no groups or projects matched your search'); + newVm.$destroy(); + done(); + }); + }); + + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', (done) => { + const newVm = createComponent(true); + newVm.$mount(); + Vue.nextTick(() => { + expect(newVm.searchEmptyMessage).toBe('Sorry, no groups matched your search'); + newVm.$destroy(); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', (done) => { + spyOn(eventHub, '$off'); + + const newVm = createComponent(); + newVm.$mount(); + newVm.$destroy(); + + Vue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('template', () => { + beforeEach(() => { + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render loading icon', (done) => { + vm.isLoading = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); + expect(vm.$el.querySelector('i.fa').getAttribute('aria-label')).toBe('Loading groups'); + done(); + }); + }); + + it('should render groups tree', (done) => { + vm.groups = [mockParentGroupItem]; + vm.isLoading = false; + vm.pageInfo = mockPageInfo; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/groups/components/group_folder_spec.js b/spec/javascripts/groups/components/group_folder_spec.js new file mode 100644 index 00000000000..4eb198595fb --- /dev/null +++ b/spec/javascripts/groups/components/group_folder_spec.js @@ -0,0 +1,66 @@ +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((done) => { + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + vm.$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + 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 = Object.assign({}, 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/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js new file mode 100644 index 00000000000..0f4fbdae445 --- /dev/null +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -0,0 +1,177 @@ +import Vue from 'vue'; + +import groupItemComponent from '~/groups/components/group_item.vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import eventHub from '~/groups/event_hub'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { + const Component = Vue.extend(groupItemComponent); + + return mountComponent(Component, { + group, + parentGroup, + }); +}; + +describe('GroupItemComponent', () => { + let vm; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + + vm = createComponent(); + + Vue.nextTick(() => { + done(); + }); + }); + + 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.rowClass; + + expect(Object.keys(rowClass).length).toBe(classes.length); + Object.keys(rowClass).forEach((className) => { + expect(classes.indexOf(className) > -1).toBeTruthy(); + }); + }); + }); + + describe('hasChildren', () => { + it('should return boolean value representing if group has any children present', () => { + let newVm; + const group = Object.assign({}, 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 = Object.assign({}, 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 = Object.assign({}, 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', () => { + spyOn(eventHub, '$emit'); + + 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', (done) => { + const group = Object.assign({}, mockParentGroupItem); + group.childrenCount = 0; + const newVm = createComponent(group); + spyOn(gl.utils, 'visitUrl').and.stub(); + spyOn(eventHub, '$emit'); + + newVm.onClickRowGroup(event); + setTimeout(() => { + expect(eventHub.$emit).not.toHaveBeenCalled(); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); + done(); + }, 0); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + 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(vm.$el.querySelector('.access-type')).toBeDefined(); + expect(vm.$el.querySelector('.description')).toBeDefined(); + + expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js new file mode 100644 index 00000000000..280376d4903 --- /dev/null +++ b/spec/javascripts/groups/components/groups_spec.js @@ -0,0 +1,70 @@ +import Vue from 'vue'; + +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'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +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((done) => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + vm = createComponent(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('change', () => { + it('should emit `fetchPage` event when page is changed via pagination', () => { + spyOn(eventHub, '$emit').and.stub(); + + vm.change(2); + expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', 2, jasmine.any(Object), jasmine.any(Object)); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', (done) => { + Vue.nextTick(() => { + 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 === 0).toBeTruthy(); + done(); + }); + }); + + it('should render empty search message when `searchEmpty` is `true`', (done) => { + vm.searchEmpty = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js new file mode 100644 index 00000000000..2ce1a749a96 --- /dev/null +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -0,0 +1,110 @@ +import Vue from 'vue'; + +import itemActionsComponent from '~/groups/components/item_actions.vue'; +import eventHub from '~/groups/event_hub'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +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('computed', () => { + describe('leaveConfirmationMessage', () => { + it('should return appropriate string for leave group confirmation', () => { + expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?'); + }); + }); + }); + + describe('methods', () => { + describe('onLeaveGroup', () => { + it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => { + expect(vm.dialogStatus).toBeFalsy(); + vm.onLeaveGroup(); + expect(vm.dialogStatus).toBeTruthy(); + }); + }); + + describe('leaveGroup', () => { + it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { + spyOn(eventHub, '$emit'); + vm.dialogStatus = true; + vm.leaveGroup(true); + expect(vm.dialogStatus).toBeFalsy(); + expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); + }); + + it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { + spyOn(eventHub, '$emit'); + vm.dialogStatus = true; + vm.leaveGroup(false); + expect(vm.dialogStatus).toBeFalsy(); + expect(eventHub.$emit).not.toHaveBeenCalled(); + }); + }); + }); + + 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 = Object.assign({}, 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.querySelector('i.fa.fa-cogs')).toBeDefined(); + + newVm.$destroy(); + }); + + it('should render Leave Group button with correct attribute values', () => { + const group = Object.assign({}, 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.querySelector('i.fa.fa-sign-out')).toBeDefined(); + + newVm.$destroy(); + }); + + it('should show modal dialog when `dialogStatus` is set to `true`', () => { + vm.dialogStatus = true; + const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog'); + expect(modalDialogEl).toBeDefined(); + 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/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js new file mode 100644 index 00000000000..4310a07e6e6 --- /dev/null +++ b/spec/javascripts/groups/components/item_caret_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; + +import itemCaretComponent from '~/groups/components/item_caret.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (isGroupOpen = false) => { + const Component = Vue.extend(itemCaretComponent); + + return mountComponent(Component, { + isGroupOpen, + }); +}; + +describe('ItemCaretComponent', () => { + describe('template', () => { + it('should render component template correctly', () => { + const vm = createComponent(); + vm.$mount(); + expect(vm.$el.classList.contains('folder-caret')).toBeTruthy(); + vm.$destroy(); + }); + + it('should render caret down icon if `isGroupOpen` prop is `true`', () => { + const vm = createComponent(true); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(1); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(0); + vm.$destroy(); + }); + + it('should render caret right icon if `isGroupOpen` prop is `false`', () => { + const vm = createComponent(); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(0); + expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(1); + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js new file mode 100644 index 00000000000..e200f9f08bd --- /dev/null +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -0,0 +1,159 @@ +import Vue from 'vue'; + +import itemStatsComponent from '~/groups/components/item_stats.vue'; +import { + mockParentGroupItem, + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, +} from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +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 = Object.assign({}, mockParentGroupItem, { visibility }); + const vm = createComponent(item); + vm.$mount(); + 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 = Object.assign({}, mockParentGroupItem, { + visibility, + type: ITEM_TYPE.GROUP, + }); + const vm = createComponent(item); + vm.$mount(); + 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 = Object.assign({}, mockParentGroupItem, { + visibility, + type: ITEM_TYPE.PROJECT, + }); + const vm = createComponent(item); + vm.$mount(); + 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 = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isProject).toBeTruthy(); + vm.$destroy(); + + item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + vm = createComponent(item); + vm.$mount(); + 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 = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isGroup).toBeTruthy(); + vm.$destroy(); + + item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); + vm = createComponent(item); + vm.$mount(); + expect(vm.isGroup).toBeFalsy(); + vm.$destroy(); + }); + }); + }); + + describe('template', () => { + it('should render component template correctly', () => { + const vm = createComponent(); + vm.$mount(); + + const visibilityIconEl = vm.$el.querySelector('.item-visibility'); + expect(vm.$el.classList.contains('.stats')).toBeDefined(); + expect(visibilityIconEl).toBeDefined(); + expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); + expect(visibilityIconEl.querySelector('i.fa')).toBeDefined(); + + vm.$destroy(); + }); + + it('should render stat icons if `item.type` is Group', () => { + const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + const vm = createComponent(item); + vm.$mount(); + + const subgroupIconEl = vm.$el.querySelector('span.number-subgroups'); + expect(subgroupIconEl).toBeDefined(); + expect(subgroupIconEl.dataset.originalTitle).toBe('Subgroups'); + expect(subgroupIconEl.querySelector('i.fa.fa-folder')).toBeDefined(); + expect(subgroupIconEl.innerText.trim()).toBe(`${vm.item.subgroupCount}`); + + const projectsIconEl = vm.$el.querySelector('span.number-projects'); + expect(projectsIconEl).toBeDefined(); + expect(projectsIconEl.dataset.originalTitle).toBe('Projects'); + expect(projectsIconEl.querySelector('i.fa.fa-bookmark')).toBeDefined(); + expect(projectsIconEl.innerText.trim()).toBe(`${vm.item.projectCount}`); + + const membersIconEl = vm.$el.querySelector('span.number-users'); + expect(membersIconEl).toBeDefined(); + expect(membersIconEl.dataset.originalTitle).toBe('Members'); + expect(membersIconEl.querySelector('i.fa.fa-users')).toBeDefined(); + expect(membersIconEl.innerText.trim()).toBe(`${vm.item.memberCount}`); + + vm.$destroy(); + }); + + it('should render stat icons if `item.type` is Project', () => { + const item = Object.assign({}, mockParentGroupItem, { + type: ITEM_TYPE.PROJECT, + starCount: 4, + }); + const vm = createComponent(item); + vm.$mount(); + + const projectStarIconEl = vm.$el.querySelector('.project-stars'); + expect(projectStarIconEl).toBeDefined(); + expect(projectStarIconEl.querySelector('i.fa.fa-star')).toBeDefined(); + expect(projectStarIconEl.innerText.trim()).toBe(`${vm.item.starCount}`); + + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js new file mode 100644 index 00000000000..528e6ed1b4c --- /dev/null +++ b/spec/javascripts/groups/components/item_type_icon_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; + +import itemTypeIconComponent from '~/groups/components/item_type_icon.vue'; +import { ITEM_TYPE } from '../mock_data'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +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(); + vm.$mount(); + 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); + vm.$mount(); + expect(vm.$el.querySelector('i.fa.fa-folder-open')).toBeDefined(); + vm.$destroy(); + + vm = createComponent(ITEM_TYPE.GROUP); + vm.$mount(); + expect(vm.$el.querySelector('i.fa.fa-folder')).toBeDefined(); + vm.$destroy(); + }); + + it('should render bookmark icon based on `isProject` prop value', () => { + let vm; + + vm = createComponent(ITEM_TYPE.PROJECT); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(1); + vm.$destroy(); + + vm = createComponent(ITEM_TYPE.GROUP); + vm.$mount(); + expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(0); + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js deleted file mode 100644 index 25e10552d95..00000000000 --- a/spec/javascripts/groups/group_item_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import Vue from 'vue'; -import groupItemComponent from '~/groups/components/group_item.vue'; -import GroupsStore from '~/groups/stores/groups_store'; -import { group1 } from './mock_data'; - -describe('Groups Component', () => { - let GroupItemComponent; - let component; - let store; - let group; - - describe('group with default data', () => { - beforeEach((done) => { - GroupItemComponent = Vue.extend(groupItemComponent); - store = new GroupsStore(); - group = store.decorateGroup(group1); - - component = new GroupItemComponent({ - propsData: { - group, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should render the group item correctly', () => { - expect(component.$el.classList.contains('group-row')).toBe(true); - expect(component.$el.classList.contains('.no-description')).toBe(false); - expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects); - expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers); - expect(component.$el.querySelector('.group-visibility')).toBeDefined(); - expect(component.$el.querySelector('.avatar-container')).toBeDefined(); - expect(component.$el.querySelector('.title').textContent).toContain(group.name); - expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess); - expect(component.$el.querySelector('.description').textContent).toContain(group.description); - expect(component.$el.querySelector('.edit-group')).toBeDefined(); - expect(component.$el.querySelector('.leave-group')).toBeDefined(); - }); - }); - - describe('group without description', () => { - beforeEach((done) => { - GroupItemComponent = Vue.extend(groupItemComponent); - store = new GroupsStore(); - group1.description = ''; - group = store.decorateGroup(group1); - - component = new GroupItemComponent({ - propsData: { - group, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should render group item correctly', () => { - expect(component.$el.querySelector('.description').textContent).toBe(''); - expect(component.$el.classList.contains('.no-description')).toBe(false); - }); - }); - - describe('user has not access to group', () => { - beforeEach((done) => { - GroupItemComponent = Vue.extend(groupItemComponent); - store = new GroupsStore(); - group1.permissions.human_group_access = null; - group = store.decorateGroup(group1); - - component = new GroupItemComponent({ - propsData: { - group, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should not display access type', () => { - expect(component.$el.querySelector('.access-type')).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js deleted file mode 100644 index b14153dbbfa..00000000000 --- a/spec/javascripts/groups/groups_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/groups/event_hub'; -import groupFolderComponent from '~/groups/components/group_folder.vue'; -import groupItemComponent from '~/groups/components/group_item.vue'; -import groupsComponent from '~/groups/components/groups.vue'; -import GroupsStore from '~/groups/stores/groups_store'; -import { groupsData } from './mock_data'; - -describe('Groups Component', () => { - let GroupsComponent; - let store; - let component; - let groups; - - beforeEach((done) => { - Vue.component('group-folder', groupFolderComponent); - Vue.component('group-item', groupItemComponent); - - store = new GroupsStore(); - groups = store.setGroups(groupsData.groups); - - store.storePagination(groupsData.pagination); - - GroupsComponent = Vue.extend(groupsComponent); - - component = new GroupsComponent({ - propsData: { - groups: store.state.groups, - pageInfo: store.state.pageInfo, - }, - }).$mount(); - - Vue.nextTick(() => { - done(); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - describe('with data', () => { - it('should render a list of groups', () => { - expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true); - expect(component.$el.querySelector('#group-12')).toBeDefined(); - expect(component.$el.querySelector('#group-1119')).toBeDefined(); - expect(component.$el.querySelector('#group-1120')).toBeDefined(); - }); - - it('should respect the order of groups', () => { - const wrap = component.$el.querySelector('.groups-list-tree-container > .group-list-tree'); - expect(wrap.querySelector('.group-row:nth-child(1)').id).toBe('group-12'); - expect(wrap.querySelector('.group-row:nth-child(2)').id).toBe('group-1119'); - }); - - it('should render group and its subgroup', () => { - const lists = component.$el.querySelectorAll('.group-list-tree'); - - expect(lists.length).toBe(3); // one parent and two subgroups - - expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true); - expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true); - - expect(lists[2].querySelector('#group-1120').textContent).toContain(groups.id1119.subGroups.id1120.name); - }); - - it('should render group identicon when group avatar is not present', () => { - const avatar = component.$el.querySelector('#group-12 .avatar-container .avatar'); - expect(avatar.nodeName).toBe('DIV'); - expect(avatar.classList.contains('identicon')).toBeTruthy(); - expect(avatar.getAttribute('style').indexOf('background-color') > -1).toBeTruthy(); - }); - - it('should render group avatar when group avatar is present', () => { - const avatar = component.$el.querySelector('#group-1120 .avatar-container .avatar'); - expect(avatar.nodeName).toBe('IMG'); - expect(avatar.classList.contains('identicon')).toBeFalsy(); - }); - - it('should remove prefix of parent group', () => { - expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4'); - }); - - it('should remove the group after leaving the group', (done) => { - spyOn(window, 'confirm').and.returnValue(true); - - eventHub.$on('leaveGroup', (group, collection) => { - store.removeGroup(group, collection); - }); - - component.$el.querySelector('#group-12 .leave-group').click(); - - Vue.nextTick(() => { - expect(component.$el.querySelector('#group-12')).toBeNull(); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index 5bb84b591f4..6184d671790 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -1,114 +1,380 @@ -const group1 = { - id: 12, - name: 'level1', - path: 'level1', - description: 'foo', - visibility: 'public', - avatar_url: null, - web_url: 'http://localhost:3000/groups/level1', - group_path: '/level1', - full_name: 'level1', - full_path: 'level1', - parent_id: null, - created_at: '2017-05-15T19:01:23.670Z', - updated_at: '2017-05-15T19:01:23.670Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, +export const mockEndpoint = '/dashboard/groups.json'; + +export const ITEM_TYPE = { + PROJECT: 'project', + GROUP: 'group', }; -// This group has no direct parent, should be placed as subgroup of group1 -const group14 = { - id: 1128, - name: 'level4', - path: 'level4', - description: 'foo', - visibility: 'public', - avatar_url: null, - web_url: 'http://localhost:3000/groups/level1/level2/level3/level4', - group_path: '/level1/level2/level3/level4', - full_name: 'level1 / level2 / level3 / level4', - full_path: 'level1/level2/level3/level4', - parent_id: 1127, - created_at: '2017-05-15T19:02:01.645Z', - updated_at: '2017-05-15T19:02:01.645Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, +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.', + private: 'Private - The group and its projects can only be viewed by members.', }; -const group2 = { - id: 1119, - name: 'devops', - path: 'devops', - description: 'foo', - visibility: 'public', - avatar_url: null, - web_url: 'http://localhost:3000/groups/devops', - group_path: '/devops', - full_name: 'devops', - full_path: 'devops', - parent_id: null, - created_at: '2017-05-11T19:35:09.635Z', - updated_at: '2017-05-11T19:35:09.635Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, +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.', + private: 'Private - Project access must be granted explicitly to each user.', +}; + +export const VISIBILITY_TYPE_ICON = { + public: 'fa-globe', + internal: 'fa-shield', + private: 'fa-lock', }; -const group21 = { - id: 1120, - name: 'chef', - path: 'chef', - description: 'foo', +export const mockParentGroupItem = { + id: 55, + name: 'hardware', + description: '', visibility: 'public', - avatar_url: '/uploads/-/system/group/avatar/2/GitLab.png', - web_url: 'http://localhost:3000/groups/devops/chef', - group_path: '/devops/chef', - full_name: 'devops / chef', - full_path: 'devops/chef', - parent_id: 1119, - created_at: '2017-05-11T19:51:04.060Z', - updated_at: '2017-05-11T19:51:04.060Z', - number_projects_with_delimiter: '1', - number_users_with_delimiter: '1', - has_subgroups: true, - permissions: { - human_group_access: 'Master', - }, + fullName: 'platform / hardware', + relativePath: '/platform/hardware', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/edit', + childrenCount: 3, + leavePath: '/groups/platform/hardware/group_members/leave', + parentId: 54, + memberCount: '1', + projectCount: 1, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, }; -const groupsData = { - groups: [group1, group14, group2, group21], - pagination: { - Date: 'Mon, 22 May 2017 22:31:52 GMT', - 'X-Prev-Page': '1', - 'X-Content-Type-Options': 'nosniff', - 'X-Total': '31', - 'Transfer-Encoding': 'chunked', - 'X-Runtime': '0.611144', - 'X-Xss-Protection': '1; mode=block', - 'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09', - 'X-Ua-Compatible': 'IE=edge', - 'X-Per-Page': '20', - Link: '<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"', - 'X-Next-Page': '', - Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"', - 'X-Frame-Options': 'DENY', - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'max-age=0, private, must-revalidate', - 'X-Total-Pages': '2', - 'X-Page': '2', +export const mockRawChildren = [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp', + relative_path: '/platform/hardware/bsp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/edit', + children_count: 6, + leave_path: '/groups/platform/hardware/bsp/group_members/leave', + parent_id: 55, + number_users_with_delimiter: '1', + project_count: 4, + subgroup_count: 2, + can_leave: false, + children: [], + }, +]; + +export const mockChildren = [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + fullName: 'platform / hardware / bsp', + relativePath: '/platform/hardware/bsp', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/bsp/edit', + childrenCount: 6, + leavePath: '/groups/platform/hardware/bsp/group_members/leave', + parentId: 55, + memberCount: '1', + projectCount: 4, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, }, +]; + +export const mockGroups = [ + { + id: 75, + name: 'test-group', + description: '', + visibility: 'public', + full_name: 'test-group', + relative_path: '/test-group', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/test-group/edit', + children_count: 2, + leave_path: '/groups/test-group/group_members/leave', + parent_id: null, + number_users_with_delimiter: '1', + project_count: 2, + subgroup_count: 0, + can_leave: false, + }, + { + id: 67, + name: 'open-source', + description: '', + visibility: 'private', + full_name: 'open-source', + relative_path: '/open-source', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/open-source/edit', + children_count: 0, + leave_path: '/groups/open-source/group_members/leave', + parent_id: null, + number_users_with_delimiter: '1', + project_count: 0, + subgroup_count: 0, + can_leave: false, + }, + { + id: 54, + name: 'platform', + description: '', + visibility: 'public', + full_name: 'platform', + relative_path: '/platform', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/edit', + children_count: 1, + leave_path: '/groups/platform/group_members/leave', + parent_id: null, + number_users_with_delimiter: '1', + project_count: 0, + subgroup_count: 1, + can_leave: false, + }, + { + id: 5, + name: 'H5bp', + description: 'Minus dolor consequuntur qui nam recusandae quam incidunt.', + visibility: 'public', + full_name: 'H5bp', + relative_path: '/h5bp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/h5bp/edit', + children_count: 1, + leave_path: '/groups/h5bp/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 1, + subgroup_count: 0, + can_leave: false, + }, + { + id: 4, + name: 'Twitter', + description: 'Deserunt hic nostrum placeat veniam.', + visibility: 'public', + full_name: 'Twitter', + relative_path: '/twitter', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/twitter/edit', + children_count: 2, + leave_path: '/groups/twitter/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 2, + subgroup_count: 0, + can_leave: false, + }, + { + id: 3, + name: 'Documentcloud', + description: 'Consequatur saepe totam ea pariatur maxime.', + visibility: 'public', + full_name: 'Documentcloud', + relative_path: '/documentcloud', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/documentcloud/edit', + children_count: 1, + leave_path: '/groups/documentcloud/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 1, + subgroup_count: 0, + can_leave: false, + }, + { + id: 2, + name: 'Gitlab Org', + description: 'Debitis ea quas aperiam velit doloremque ab.', + visibility: 'public', + full_name: 'Gitlab Org', + relative_path: '/gitlab-org', + can_edit: true, + type: 'group', + avatar_url: '/uploads/-/system/group/avatar/2/GitLab.png', + permission: 'Owner', + edit_path: '/groups/gitlab-org/edit', + children_count: 4, + leave_path: '/groups/gitlab-org/group_members/leave', + parent_id: null, + number_users_with_delimiter: '5', + project_count: 4, + subgroup_count: 0, + can_leave: false, + }, +]; + +export const mockSearchedGroups = [ + { + id: 55, + name: 'hardware', + description: '', + visibility: 'public', + full_name: 'platform / hardware', + relative_path: '/platform/hardware', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/edit', + children_count: 3, + leave_path: '/groups/platform/hardware/group_members/leave', + parent_id: 54, + number_users_with_delimiter: '1', + project_count: 1, + subgroup_count: 2, + can_leave: false, + children: [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp', + relative_path: '/platform/hardware/bsp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/edit', + children_count: 6, + leave_path: '/groups/platform/hardware/bsp/group_members/leave', + parent_id: 55, + number_users_with_delimiter: '1', + project_count: 4, + subgroup_count: 2, + can_leave: false, + children: [ + { + id: 60, + name: 'kernel', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel', + relative_path: '/platform/hardware/bsp/kernel', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/kernel/edit', + children_count: 1, + leave_path: '/groups/platform/hardware/bsp/kernel/group_members/leave', + parent_id: 57, + number_users_with_delimiter: '1', + project_count: 0, + subgroup_count: 1, + can_leave: false, + children: [ + { + id: 61, + name: 'common', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel / common', + relative_path: '/platform/hardware/bsp/kernel/common', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/kernel/common/edit', + children_count: 2, + leave_path: '/groups/platform/hardware/bsp/kernel/common/group_members/leave', + parent_id: 60, + number_users_with_delimiter: '1', + project_count: 2, + subgroup_count: 0, + can_leave: false, + children: [ + { + id: 17, + name: 'v4.4', + description: 'Voluptatem qui ea error aperiam veritatis doloremque consequatur temporibus.', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel / common / v4.4', + relative_path: '/platform/hardware/bsp/kernel/common/v4.4', + can_edit: true, + type: 'project', + avatar_url: null, + permission: null, + edit_path: '/platform/hardware/bsp/kernel/common/v4.4/edit', + star_count: 0, + }, + { + id: 16, + name: 'v4.1', + description: 'Rerum expedita voluptatem doloribus neque ducimus ut hic.', + visibility: 'public', + full_name: 'platform / hardware / bsp / kernel / common / v4.1', + relative_path: '/platform/hardware/bsp/kernel/common/v4.1', + can_edit: true, + type: 'project', + avatar_url: null, + permission: null, + edit_path: '/platform/hardware/bsp/kernel/common/v4.1/edit', + star_count: 0, + }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export const mockRawPageInfo = { + 'x-per-page': 10, + 'x-page': 10, + 'x-total': 10, + 'x-total-pages': 10, + 'x-next-page': 10, + 'x-prev-page': 10, }; -export { groupsData, group1 }; +export const mockPageInfo = { + perPage: 10, + page: 10, + total: 10, + totalPages: 10, + nextPage: 10, + prevPage: 10, +}; diff --git a/spec/javascripts/groups/service/groups_service_spec.js b/spec/javascripts/groups/service/groups_service_spec.js new file mode 100644 index 00000000000..222e75d24a4 --- /dev/null +++ b/spec/javascripts/groups/service/groups_service_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +import GroupsService from '~/groups/service/groups_service'; +import { mockEndpoint, mockParentGroupItem } from '../mock_data'; + +Vue.use(VueResource); + +describe('GroupsService', () => { + let service; + + beforeEach(() => { + service = new GroupsService(mockEndpoint); + }); + + describe('getGroups', () => { + it('should return promise for `GET` request on provided endpoint', () => { + spyOn(service.groups, 'get').and.stub(); + const queryParams = { + page: 2, + filter: 'git', + sort: 'created_asc', + }; + + service.getGroups(55, 2, 'git', 'created_asc'); + expect(service.groups.get).toHaveBeenCalledWith({ parent_id: 55 }); + + service.getGroups(null, 2, 'git', 'created_asc'); + expect(service.groups.get).toHaveBeenCalledWith(queryParams); + }); + }); + + describe('leaveGroup', () => { + it('should return promise for `DELETE` request on provided endpoint', () => { + spyOn(Vue.http, 'delete').and.stub(); + + service.leaveGroup(mockParentGroupItem.leavePath); + expect(Vue.http.delete).toHaveBeenCalledWith(mockParentGroupItem.leavePath); + }); + }); +}); diff --git a/spec/javascripts/groups/store/groups_store_spec.js b/spec/javascripts/groups/store/groups_store_spec.js new file mode 100644 index 00000000000..d74f38f476e --- /dev/null +++ b/spec/javascripts/groups/store/groups_store_spec.js @@ -0,0 +1,110 @@ +import GroupsStore from '~/groups/store/groups_store'; +import { + mockGroups, mockSearchedGroups, + mockParentGroupItem, mockRawChildren, + mockRawPageInfo, +} from '../mock_data'; + +describe('ProjectsStore', () => { + describe('constructor', () => { + it('should initialize default state', () => { + let store; + + store = new GroupsStore(); + expect(Object.keys(store.state).length).toBe(2); + expect(Array.isArray(store.state.groups)).toBeTruthy(); + expect(Object.keys(store.state.pageInfo).length).toBe(0); + expect(store.hideProjects).not.toBeDefined(); + + store = new GroupsStore(true); + expect(store.hideProjects).toBeTruthy(); + }); + }); + + describe('setGroups', () => { + it('should set groups to state', () => { + const store = new GroupsStore(); + spyOn(store, 'formatGroupItem').and.callThrough(); + + store.setGroups(mockGroups); + expect(store.state.groups.length).toBe(mockGroups.length); + expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); + expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy(); + }); + }); + + describe('setSearchedGroups', () => { + it('should set searched groups to state', () => { + const store = new GroupsStore(); + spyOn(store, 'formatGroupItem').and.callThrough(); + + store.setSearchedGroups(mockSearchedGroups); + expect(store.state.groups.length).toBe(mockSearchedGroups.length); + expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); + expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName') > -1).toBeTruthy(); + }); + }); + + describe('setGroupChildren', () => { + it('should set children to group item in state', () => { + const store = new GroupsStore(); + spyOn(store, 'formatGroupItem').and.callThrough(); + + store.setGroupChildren(mockParentGroupItem, mockRawChildren); + expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); + expect(mockParentGroupItem.children.length).toBe(1); + expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(mockParentGroupItem.isOpen).toBeTruthy(); + expect(mockParentGroupItem.isChildrenLoading).toBeFalsy(); + }); + }); + + describe('setPaginationInfo', () => { + it('should parse and set pagination info in state', () => { + const store = new GroupsStore(); + + store.setPaginationInfo(mockRawPageInfo); + expect(store.state.pageInfo.perPage).toBe(10); + expect(store.state.pageInfo.page).toBe(10); + expect(store.state.pageInfo.total).toBe(10); + expect(store.state.pageInfo.totalPages).toBe(10); + expect(store.state.pageInfo.nextPage).toBe(10); + expect(store.state.pageInfo.previousPage).toBe(10); + }); + }); + + describe('formatGroupItem', () => { + it('should parse group item object and return updated object', () => { + let store; + let updatedGroupItem; + + store = new GroupsStore(); + updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); + expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy(); + expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count); + expect(updatedGroupItem.isChildrenLoading).toBe(false); + expect(updatedGroupItem.isBeingRemoved).toBe(false); + + store = new GroupsStore(true); + updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); + expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy(); + expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count); + }); + }); + + describe('removeGroup', () => { + it('should remove children from group item in state', () => { + const store = new GroupsStore(); + const rawParentGroup = Object.assign({}, mockGroups[0]); + const rawChildGroup = Object.assign({}, mockGroups[1]); + + store.setGroups([rawParentGroup]); + store.setGroupChildren(store.state.groups[0], [rawChildGroup]); + const childItem = store.state.groups[0].children[0]; + + store.removeGroup(childItem, store.state.groups[0]); + expect(store.state.groups[0].children.length).toBe(0); + }); + }); +}); |