summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-09-05 13:19:27 +0000
committerFilipa Lacerda <filipa@gitlab.com>2017-09-05 13:19:27 +0000
commit6e4949dba199d9fbc3070225e51f07657a81fbd7 (patch)
treebbe57cd4c3480fef6e84a032ff43d0cb21db6456 /spec
parent75d56cec1bb28af32f471d6ba39e09216bcad3ce (diff)
parentde10551ed5df72dffe9c4dfc21eba61aa19db77e (diff)
downloadgitlab-ce-6e4949dba199d9fbc3070225e51f07657a81fbd7.tar.gz
Merge branch '35010-projects-nav-dropdown' into 'master'
Add dropdown to Projects nav item Closes #35010 See merge request !13866
Diffstat (limited to 'spec')
-rw-r--r--spec/javascripts/api_spec.js6
-rw-r--r--spec/javascripts/project_title_spec.js3
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js348
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js72
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_item_spec.js65
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_search_spec.js84
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js101
-rw-r--r--spec/javascripts/projects_dropdown/mock_data.js96
-rw-r--r--spec/javascripts/projects_dropdown/service/projects_service_spec.js178
-rw-r--r--spec/javascripts/projects_dropdown/store/projects_store_spec.js41
-rw-r--r--spec/javascripts/vue_shared/components/identicon_spec.js28
11 files changed, 1013 insertions, 9 deletions
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 8c68ceff914..2aa4fb1f6c6 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -101,12 +101,13 @@ describe('Api', () => {
it('fetches projects with membership when logged in', (done) => {
const query = 'dummy query';
const options = { unused: 'option' };
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
const expectedData = Object.assign({
search: query,
per_page: 20,
membership: true,
+ simple: true,
}, options);
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
@@ -124,10 +125,11 @@ describe('Api', () => {
it('fetches projects without membership when not logged in', (done) => {
const query = 'dummy query';
const options = { unused: 'option' };
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
const expectedData = Object.assign({
search: query,
per_page: 20,
+ simple: true,
}, options);
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 3d36bb3e4d4..9ea8414ae40 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -41,12 +41,13 @@ describe('Project Title', () => {
window.gon.current_user_id = 1;
$('.js-projects-dropdown-toggle').click();
expect($menu).toHaveClass('open');
- expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`);
+ expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json`);
expect(reqData).toEqual({
search: '',
order_by: 'last_activity_at',
per_page: 20,
membership: true,
+ simple: true,
});
$menu.find('.dropdown-menu-close-icon').click();
expect($menu).not.toHaveClass('open');
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
new file mode 100644
index 00000000000..42f0f6fc1af
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/app_spec.js
@@ -0,0 +1,348 @@
+import Vue from 'vue';
+
+import bp from '~/breakpoints';
+import appComponent from '~/projects_dropdown/components/app.vue';
+import eventHub from '~/projects_dropdown/event_hub';
+import ProjectsStore from '~/projects_dropdown/store/projects_store';
+import ProjectsService from '~/projects_dropdown/service/projects_service';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { currentSession, mockProject, mockRawProject } from '../mock_data';
+
+const createComponent = () => {
+ gon.api_version = currentSession.apiVersion;
+ const Component = Vue.extend(appComponent);
+ const store = new ProjectsStore();
+ const service = new ProjectsService(currentSession.username);
+
+ return mountComponent(Component, {
+ store,
+ service,
+ currentUserName: currentSession.username,
+ currentProject: currentSession.project,
+ });
+};
+
+const returnServicePromise = (data, failed) => new Promise((resolve, reject) => {
+ if (failed) {
+ reject(data);
+ } else {
+ resolve({
+ json() {
+ return data;
+ },
+ });
+ }
+});
+
+describe('AppComponent', () => {
+ describe('computed', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('frequentProjects', () => {
+ it('should return list of frequently accessed projects from store', () => {
+ expect(vm.frequentProjects).toBeDefined();
+ expect(vm.frequentProjects.length).toBe(0);
+
+ vm.store.setFrequentProjects([mockProject]);
+ expect(vm.frequentProjects).toBeDefined();
+ expect(vm.frequentProjects.length).toBe(1);
+ });
+ });
+
+ describe('searchProjects', () => {
+ it('should return list of frequently accessed projects from store', () => {
+ expect(vm.searchProjects).toBeDefined();
+ expect(vm.searchProjects.length).toBe(0);
+
+ vm.store.setSearchedProjects([mockRawProject]);
+ expect(vm.searchProjects).toBeDefined();
+ expect(vm.searchProjects.length).toBe(1);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggleFrequentProjectsList', () => {
+ it('should toggle props which control visibility of Frequent Projects list from state passed', () => {
+ vm.toggleFrequentProjectsList(true);
+ expect(vm.isLoadingProjects).toBeFalsy();
+ expect(vm.isSearchListVisible).toBeFalsy();
+ expect(vm.isFrequentsListVisible).toBeTruthy();
+
+ vm.toggleFrequentProjectsList(false);
+ expect(vm.isLoadingProjects).toBeTruthy();
+ expect(vm.isSearchListVisible).toBeTruthy();
+ expect(vm.isFrequentsListVisible).toBeFalsy();
+ });
+ });
+
+ describe('toggleSearchProjectsList', () => {
+ it('should toggle props which control visibility of Searched Projects list from state passed', () => {
+ vm.toggleSearchProjectsList(true);
+ expect(vm.isLoadingProjects).toBeFalsy();
+ expect(vm.isFrequentsListVisible).toBeFalsy();
+ expect(vm.isSearchListVisible).toBeTruthy();
+
+ vm.toggleSearchProjectsList(false);
+ expect(vm.isLoadingProjects).toBeTruthy();
+ expect(vm.isFrequentsListVisible).toBeTruthy();
+ expect(vm.isSearchListVisible).toBeFalsy();
+ });
+ });
+
+ describe('toggleLoader', () => {
+ it('should toggle props which control visibility of list loading animation from state passed', () => {
+ vm.toggleLoader(true);
+ expect(vm.isFrequentsListVisible).toBeFalsy();
+ expect(vm.isSearchListVisible).toBeFalsy();
+ expect(vm.isLoadingProjects).toBeTruthy();
+
+ vm.toggleLoader(false);
+ expect(vm.isFrequentsListVisible).toBeTruthy();
+ expect(vm.isSearchListVisible).toBeTruthy();
+ expect(vm.isLoadingProjects).toBeFalsy();
+ });
+ });
+
+ describe('fetchFrequentProjects', () => {
+ it('should set props for loading animation to `true` while frequent projects list is being loaded', () => {
+ spyOn(vm, 'toggleLoader');
+
+ vm.fetchFrequentProjects();
+ expect(vm.isLocalStorageFailed).toBeFalsy();
+ expect(vm.toggleLoader).toHaveBeenCalledWith(true);
+ });
+
+ it('should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded', () => {
+ const mockData = [mockProject];
+
+ spyOn(vm.service, 'getFrequentProjects').and.returnValue(mockData);
+ spyOn(vm.store, 'setFrequentProjects');
+ spyOn(vm, 'toggleFrequentProjectsList');
+
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).toHaveBeenCalled();
+ expect(vm.store.setFrequentProjects).toHaveBeenCalledWith(mockData);
+ expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
+ });
+
+ it('should set props for failure message to `true` when method fails to fetch frequent projects list', () => {
+ spyOn(vm.service, 'getFrequentProjects').and.returnValue(null);
+ spyOn(vm.store, 'setFrequentProjects');
+ spyOn(vm, 'toggleFrequentProjectsList');
+
+ expect(vm.isLocalStorageFailed).toBeFalsy();
+
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).toHaveBeenCalled();
+ expect(vm.store.setFrequentProjects).toHaveBeenCalledWith([]);
+ expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.isLocalStorageFailed).toBeTruthy();
+ });
+
+ it('should set props for search results list to `true` if search query was already made previously', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+ spyOn(vm.service, 'getFrequentProjects');
+ spyOn(vm, 'toggleSearchProjectsList');
+
+ vm.searchQuery = 'test';
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).not.toHaveBeenCalled();
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ });
+
+ it('should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+ spyOn(vm, 'toggleSearchProjectsList');
+ spyOn(vm.service, 'getFrequentProjects');
+
+ vm.searchQuery = 'test';
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).toHaveBeenCalled();
+ expect(vm.toggleSearchProjectsList).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('fetchSearchedProjects', () => {
+ const searchQuery = 'test';
+
+ it('should perform search with provided search query', (done) => {
+ const mockData = [mockRawProject];
+ spyOn(vm, 'toggleLoader');
+ spyOn(vm, 'toggleSearchProjectsList');
+ spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise(mockData));
+ spyOn(vm.store, 'setSearchedProjects');
+
+ vm.fetchSearchedProjects(searchQuery);
+ setTimeout(() => {
+ expect(vm.searchQuery).toBe(searchQuery);
+ expect(vm.toggleLoader).toHaveBeenCalledWith(true);
+ expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.store.setSearchedProjects).toHaveBeenCalledWith(mockData);
+ done();
+ }, 0);
+ });
+
+ it('should update props for showing search failure', (done) => {
+ spyOn(vm, 'toggleSearchProjectsList');
+ spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true));
+
+ vm.fetchSearchedProjects(searchQuery);
+ setTimeout(() => {
+ expect(vm.searchQuery).toBe(searchQuery);
+ expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
+ expect(vm.isSearchFailed).toBeTruthy();
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('logCurrentProjectAccess', () => {
+ it('should log current project access via service', (done) => {
+ spyOn(vm.service, 'logProjectAccess');
+
+ vm.currentProject = mockProject;
+ vm.logCurrentProjectAccess();
+
+ setTimeout(() => {
+ expect(vm.service.logProjectAccess).toHaveBeenCalledWith(mockProject);
+ done();
+ }, 1);
+ });
+ });
+
+ describe('handleSearchClear', () => {
+ it('should show frequent projects list when search input is cleared', () => {
+ spyOn(vm.store, 'clearSearchedProjects');
+ spyOn(vm, 'toggleFrequentProjectsList');
+
+ vm.handleSearchClear();
+
+ expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.store.clearSearchedProjects).toHaveBeenCalled();
+ expect(vm.searchQuery).toBe('');
+ });
+ });
+
+ describe('handleSearchFailure', () => {
+ it('should show failure message within dropdown', () => {
+ spyOn(vm, 'toggleSearchProjectsList');
+
+ vm.handleSearchFailure();
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.isSearchFailed).toBeTruthy();
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', (done) => {
+ spyOn(eventHub, '$on');
+
+ createComponent().$mount();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', (done) => {
+ const vm = createComponent();
+ spyOn(eventHub, '$off');
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render search input', () => {
+ expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
+ });
+
+ it('should render loading animation', (done) => {
+ vm.toggleLoader(true);
+ Vue.nextTick(() => {
+ const loadingEl = vm.$el.querySelector('.loading-animation');
+
+ expect(loadingEl).toBeDefined();
+ expect(loadingEl.classList.contains('prepend-top-20')).toBeTruthy();
+ expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
+ done();
+ });
+ });
+
+ it('should render frequent projects list header', (done) => {
+ vm.toggleFrequentProjectsList(true);
+ Vue.nextTick(() => {
+ const sectionHeaderEl = vm.$el.querySelector('.section-header');
+
+ expect(sectionHeaderEl).toBeDefined();
+ expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
+ done();
+ });
+ });
+
+ it('should render frequent projects list', (done) => {
+ vm.toggleFrequentProjectsList(true);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined();
+ done();
+ });
+ });
+
+ it('should render searched projects list', (done) => {
+ vm.toggleSearchProjectsList(true);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.section-header')).toBe(null);
+ expect(vm.$el.querySelector('.projects-list-search-container')).toBeDefined();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
new file mode 100644
index 00000000000..fcd0f6a3630
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+
+import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mockFrequents } from '../mock_data';
+
+const createComponent = () => {
+ const Component = Vue.extend(projectsListFrequentComponent);
+
+ return mountComponent(Component, {
+ projects: mockFrequents,
+ localStorageFailed: false,
+ });
+};
+
+describe('ProjectsListFrequentComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
+ vm.projects = [];
+ expect(vm.isListEmpty).toBeTruthy();
+
+ vm.projects = mockFrequents;
+ expect(vm.isListEmpty).toBeFalsy();
+ });
+ });
+
+ describe('listEmptyMessage', () => {
+ it('should return appropriate empty list message based on value of `localStorageFailed` prop', () => {
+ vm.localStorageFailed = true;
+ expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
+
+ vm.localStorageFailed = false;
+ expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', (done) => {
+ vm.projects = mockFrequents;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('projects-list-frequent-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(5);
+ done();
+ });
+ });
+
+ it('should render component element with empty message', (done) => {
+ vm.projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
new file mode 100644
index 00000000000..171629fcd6b
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
@@ -0,0 +1,65 @@
+import Vue from 'vue';
+
+import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mockProject } from '../mock_data';
+
+const createComponent = () => {
+ const Component = Vue.extend(projectsListItemComponent);
+
+ return mountComponent(Component, {
+ projectId: mockProject.id,
+ projectName: mockProject.name,
+ namespace: mockProject.namespace,
+ webUrl: mockProject.webUrl,
+ avatarUrl: mockProject.avatarUrl,
+ });
+};
+
+describe('ProjectsListItemComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hasAvatar', () => {
+ it('should return `true` or `false` if whether avatar is present or not', () => {
+ vm.avatarUrl = 'path/to/avatar.png';
+ expect(vm.hasAvatar).toBeTruthy();
+
+ vm.avatarUrl = null;
+ expect(vm.hasAvatar).toBeFalsy();
+ });
+ });
+
+ describe('highlightedProjectName', () => {
+ it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
+ vm.matcher = 'lab';
+ expect(vm.highlightedProjectName).toContain('<b>Lab</b>');
+ });
+
+ it('should return project name as it is if `matcher` is not available', () => {
+ vm.matcher = null;
+ expect(vm.highlightedProjectName).toBe(mockProject.name);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element', () => {
+ expect(vm.$el.classList.contains('projects-list-item-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('a').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-item-avatar-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-item-metadata-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-title').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-namespace').length).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
new file mode 100644
index 00000000000..59fc2dedba5
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+
+import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mockProject } from '../mock_data';
+
+const createComponent = () => {
+ const Component = Vue.extend(projectsListSearchComponent);
+
+ return mountComponent(Component, {
+ projects: [mockProject],
+ matcher: 'lab',
+ searchFailed: false,
+ });
+};
+
+describe('ProjectsListSearchComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
+ vm.projects = [];
+ expect(vm.isListEmpty).toBeTruthy();
+
+ vm.projects = [mockProject];
+ expect(vm.isListEmpty).toBeFalsy();
+ });
+ });
+
+ describe('listEmptyMessage', () => {
+ it('should return appropriate empty list message based on value of `searchFailed` prop', () => {
+ vm.searchFailed = true;
+ expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
+
+ vm.searchFailed = false;
+ expect(vm.listEmptyMessage).toBe('No projects matched your query');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', (done) => {
+ vm.projects = [mockProject];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('projects-list-search-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(1);
+ done();
+ });
+ });
+
+ it('should render component element with empty message', (done) => {
+ vm.projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+
+ it('should render component element with failure message', (done) => {
+ vm.searchFailed = true;
+ vm.projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty.section-failure').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
new file mode 100644
index 00000000000..f2a23e33325
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/search_spec.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+
+import searchComponent from '~/projects_dropdown/components/search.vue';
+import eventHub from '~/projects_dropdown/event_hub';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(searchComponent);
+
+ return mountComponent(Component);
+};
+
+describe('SearchComponent', () => {
+ describe('methods', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('setFocus', () => {
+ it('should set focus to search input', () => {
+ spyOn(vm.$refs.search, 'focus');
+
+ vm.setFocus();
+ expect(vm.$refs.search.focus).toHaveBeenCalled();
+ });
+ });
+
+ describe('emitSearchEvents', () => {
+ it('should emit `searchProjects` event via eventHub when `searchQuery` present', () => {
+ const searchQuery = 'test';
+ spyOn(eventHub, '$emit');
+ vm.searchQuery = searchQuery;
+ vm.emitSearchEvents();
+ expect(eventHub.$emit).toHaveBeenCalledWith('searchProjects', searchQuery);
+ });
+
+ it('should emit `searchCleared` event via eventHub when `searchQuery` is cleared', () => {
+ spyOn(eventHub, '$emit');
+ vm.searchQuery = '';
+ vm.emitSearchEvents();
+ expect(eventHub.$emit).toHaveBeenCalledWith('searchCleared');
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('should listen `dropdownOpen` event', (done) => {
+ spyOn(eventHub, '$on');
+ createComponent();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', (done) => {
+ const vm = createComponent();
+ spyOn(eventHub, '$off');
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render component element', () => {
+ const inputEl = vm.$el.querySelector('input.form-control');
+
+ expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
+ expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy();
+ expect(inputEl).not.toBe(null);
+ expect(inputEl.getAttribute('placeholder')).toBe('Search projects');
+ expect(vm.$el.querySelector('.search-icon')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/mock_data.js b/spec/javascripts/projects_dropdown/mock_data.js
new file mode 100644
index 00000000000..d6a79fb8ac1
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/mock_data.js
@@ -0,0 +1,96 @@
+export const currentSession = {
+ username: 'root',
+ storageKey: 'root/frequent-projects',
+ apiVersion: 'v4',
+ project: {
+ id: 1,
+ name: 'dummy-project',
+ namespace: 'SamepleGroup / Dummy-Project',
+ webUrl: 'http://127.0.0.1/samplegroup/dummy-project',
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+};
+
+export const mockProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
+ avatarUrl: null,
+};
+
+export const mockRawProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ name_with_namespace: 'gitlab-org / gitlab-ce',
+ web_url: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
+ avatar_url: null,
+};
+
+export const mockFrequents = [
+ {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
+ avatarUrl: null,
+ },
+ {
+ id: 2,
+ name: 'GitLab CI',
+ namespace: 'gitlab-org / gitlab-ci',
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ci',
+ avatarUrl: null,
+ },
+ {
+ id: 3,
+ name: 'Typeahead.Js',
+ namespace: 'twitter / typeahead-js',
+ webUrl: 'http://127.0.0.1:3000/twitter/typeahead-js',
+ avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
+ },
+ {
+ id: 4,
+ name: 'Intel',
+ namespace: 'platform / hardware / bsp / intel',
+ webUrl: 'http://127.0.0.1:3000/platform/hardware/bsp/intel',
+ avatarUrl: null,
+ },
+ {
+ id: 5,
+ name: 'v4.4',
+ namespace: 'platform / hardware / bsp / kernel / common / v4.4',
+ webUrl: 'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4',
+ avatarUrl: null,
+ },
+];
+
+export const unsortedFrequents = [
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+];
+
+/**
+ * This const has a specific order which tests authenticity
+ * of `ProjectsService.getTopFrequentProjects` method so
+ * DO NOT change order of items in this const.
+ */
+export const sortedFrequents = [
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+];
diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
new file mode 100644
index 00000000000..e9aabfaec1f
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
@@ -0,0 +1,178 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+import bp from '~/breakpoints';
+import ProjectsService from '~/projects_dropdown/service/projects_service';
+import { FREQUENT_PROJECTS } from '~/projects_dropdown/constants';
+import { currentSession, unsortedFrequents, sortedFrequents } from '../mock_data';
+
+Vue.use(VueResource);
+
+FREQUENT_PROJECTS.MAX_COUNT = 3;
+
+describe('ProjectsService', () => {
+ let service;
+
+ beforeEach(() => {
+ gon.api_version = currentSession.apiVersion;
+ service = new ProjectsService(currentSession.username);
+ });
+
+ describe('contructor', () => {
+ it('should initialize default properties of class', () => {
+ expect(service.isLocalStorageAvailable).toBeTruthy();
+ expect(service.currentUserName).toBe(currentSession.username);
+ expect(service.storageKey).toBe(currentSession.storageKey);
+ expect(service.projectsPath).toBeDefined();
+ });
+ });
+
+ describe('getSearchedProjects', () => {
+ it('should return promise from VueResource HTTP GET', () => {
+ spyOn(service.projectsPath, 'get').and.stub();
+
+ const searchQuery = 'lab';
+ const queryParams = {
+ simple: false,
+ per_page: 20,
+ membership: false,
+ order_by: 'last_activity_at',
+ search: searchQuery,
+ };
+
+ service.getSearchedProjects(searchQuery);
+ expect(service.projectsPath.get).toHaveBeenCalledWith(queryParams);
+ });
+ });
+
+ describe('logProjectAccess', () => {
+ let storage;
+
+ beforeEach(() => {
+ storage = {};
+
+ spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
+ storage[storageKey] = value;
+ });
+
+ spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should create a project store if it does not exist and adds a project', () => {
+ service.logProjectAccess(currentSession.project);
+
+ const projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects.length).toBe(1);
+ expect(projects[0].frequency).toBe(1);
+ expect(projects[0].lastAccessedOn).toBeDefined();
+ });
+
+ it('should prevent inserting same report multiple times into store', () => {
+ service.logProjectAccess(currentSession.project);
+ service.logProjectAccess(currentSession.project);
+
+ const projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects.length).toBe(1);
+ });
+
+ it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
+ let projects;
+ spyOn(Math, 'abs').and.returnValue(3600001); // this will lead to `diff` > 1;
+ service.logProjectAccess(currentSession.project);
+
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].frequency).toBe(1);
+
+ service.logProjectAccess(currentSession.project);
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].frequency).toBe(2);
+ expect(projects[0].lastAccessedOn).not.toBe(currentSession.project.lastAccessedOn);
+ });
+
+ it('should always update project metadata', () => {
+ let projects;
+ const oldProject = {
+ ...currentSession.project,
+ };
+
+ const newProject = {
+ ...currentSession.project,
+ name: 'New Name',
+ avatarUrl: 'new/avatar.png',
+ namespace: 'New / Namespace',
+ webUrl: 'http://localhost/new/web/url',
+ };
+
+ service.logProjectAccess(oldProject);
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].name).toBe(oldProject.name);
+ expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
+ expect(projects[0].namespace).toBe(oldProject.namespace);
+ expect(projects[0].webUrl).toBe(oldProject.webUrl);
+
+ service.logProjectAccess(newProject);
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].name).toBe(newProject.name);
+ expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
+ expect(projects[0].namespace).toBe(newProject.namespace);
+ expect(projects[0].webUrl).toBe(newProject.webUrl);
+ });
+
+ it('should not add more than 20 projects in store', () => {
+ for (let i = 1; i <= 5; i += 1) {
+ const project = Object.assign(currentSession.project, { id: i });
+ service.logProjectAccess(project);
+ }
+
+ const projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects.length).toBe(3);
+ });
+ });
+
+ describe('getTopFrequentProjects', () => {
+ let storage = {};
+
+ beforeEach(() => {
+ storage[currentSession.storageKey] = JSON.stringify(unsortedFrequents);
+
+ spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should return top 5 frequently accessed projects for desktop screens', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+ const frequentProjects = service.getTopFrequentProjects();
+
+ expect(frequentProjects.length).toBe(5);
+ frequentProjects.forEach((project, index) => {
+ expect(project.id).toBe(sortedFrequents[index].id);
+ });
+ });
+
+ it('should return top 3 frequently accessed projects for mobile screens', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+ const frequentProjects = service.getTopFrequentProjects();
+
+ expect(frequentProjects.length).toBe(3);
+ frequentProjects.forEach((project, index) => {
+ expect(project.id).toBe(sortedFrequents[index].id);
+ });
+ });
+
+ it('should return empty array if there are no projects available in store', () => {
+ storage = {};
+ expect(service.getTopFrequentProjects().length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/store/projects_store_spec.js b/spec/javascripts/projects_dropdown/store/projects_store_spec.js
new file mode 100644
index 00000000000..e57399d37cd
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/store/projects_store_spec.js
@@ -0,0 +1,41 @@
+import ProjectsStore from '~/projects_dropdown/store/projects_store';
+import { mockProject, mockRawProject } from '../mock_data';
+
+describe('ProjectsStore', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new ProjectsStore();
+ });
+
+ describe('setFrequentProjects', () => {
+ it('should set frequent projects list to state', () => {
+ store.setFrequentProjects([mockProject]);
+
+ expect(store.getFrequentProjects().length).toBe(1);
+ expect(store.getFrequentProjects()[0].id).toBe(mockProject.id);
+ });
+ });
+
+ describe('setSearchedProjects', () => {
+ it('should set searched projects list to state', () => {
+ store.setSearchedProjects([mockRawProject]);
+
+ const processedProjects = store.getSearchedProjects();
+ expect(processedProjects.length).toBe(1);
+ expect(processedProjects[0].id).toBe(mockRawProject.id);
+ expect(processedProjects[0].namespace).toBe(mockRawProject.name_with_namespace);
+ expect(processedProjects[0].webUrl).toBe(mockRawProject.web_url);
+ expect(processedProjects[0].avatarUrl).toBe(mockRawProject.avatar_url);
+ });
+ });
+
+ describe('clearSearchedProjects', () => {
+ it('should clear searched projects list from state', () => {
+ store.setSearchedProjects([mockRawProject]);
+ expect(store.getSearchedProjects().length).toBe(1);
+ store.clearSearchedProjects();
+ expect(store.getSearchedProjects().length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/identicon_spec.js b/spec/javascripts/vue_shared/components/identicon_spec.js
index 4f194e5a64e..647680f00f7 100644
--- a/spec/javascripts/vue_shared/components/identicon_spec.js
+++ b/spec/javascripts/vue_shared/components/identicon_spec.js
@@ -1,25 +1,30 @@
import Vue from 'vue';
import identiconComponent from '~/vue_shared/components/identicon.vue';
-const createComponent = () => {
+const createComponent = (sizeClass) => {
const Component = Vue.extend(identiconComponent);
return new Component({
propsData: {
entityId: 1,
entityName: 'entity-name',
+ sizeClass,
},
}).$mount();
};
describe('IdenticonComponent', () => {
- let vm;
+ describe('computed', () => {
+ let vm;
- beforeEach(() => {
- vm = createComponent();
- });
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
- describe('computed', () => {
describe('identiconStyles', () => {
it('should return styles attribute value with `background-color` property', () => {
vm.entityId = 4;
@@ -48,9 +53,20 @@ describe('IdenticonComponent', () => {
describe('template', () => {
it('should render identicon', () => {
+ const vm = createComponent();
+
expect(vm.$el.nodeName).toBe('DIV');
expect(vm.$el.classList.contains('identicon')).toBeTruthy();
+ expect(vm.$el.classList.contains('s40')).toBeTruthy();
expect(vm.$el.getAttribute('style').indexOf('background-color') > -1).toBeTruthy();
+ vm.$destroy();
+ });
+
+ it('should render identicon with provided sizing class', () => {
+ const vm = createComponent('s32');
+
+ expect(vm.$el.classList.contains('s32')).toBeTruthy();
+ vm.$destroy();
});
});
});