Commit 83709c7a authored by Mike Greiling's avatar Mike Greiling

Merge branch 'infinite-scroll' into 'master'

feat: add Infinite scroll to "Add Projects" modal in the operations dashboard

See merge request gitlab-org/gitlab!17842
parents 130792cf c6bb7584
...@@ -113,10 +113,9 @@ const Api = { ...@@ -113,10 +113,9 @@ const Api = {
.get(url, { .get(url, {
params: Object.assign(defaults, options), params: Object.assign(defaults, options),
}) })
.then(({ data }) => { .then(({ data, headers }) => {
callback(data); callback(data);
return { data, headers };
return data;
}); });
}, },
......
...@@ -47,7 +47,8 @@ export default { ...@@ -47,7 +47,8 @@ export default {
hasSearchQuery: true, hasSearchQuery: true,
}); });
}, },
[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) { [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) {
const rawItems = results.data;
Object.assign(state, { Object.assign(state, {
items: rawItems.map(rawItem => ({ items: rawItems.map(rawItem => ({
id: rawItem.id, id: rawItem.id,
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import { GlLoadingIcon, GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import ProjectListItem from './project_list_item.vue'; import ProjectListItem from './project_list_item.vue';
const SEARCH_INPUT_TIMEOUT_MS = 500; const SEARCH_INPUT_TIMEOUT_MS = 500;
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlInfiniteScroll,
ProjectListItem, ProjectListItem,
}, },
props: { props: {
...@@ -41,6 +42,11 @@ export default { ...@@ -41,6 +42,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
totalResults: {
type: Number,
required: false,
default: 0,
},
}, },
data() { data() {
return { return {
...@@ -51,6 +57,9 @@ export default { ...@@ -51,6 +57,9 @@ export default {
projectClicked(project) { projectClicked(project) {
this.$emit('projectClicked', project); this.$emit('projectClicked', project);
}, },
bottomReached() {
this.$emit('bottomReached');
},
isSelected(project) { isSelected(project) {
return Boolean(_.find(this.selectedProjects, { id: project.id })); return Boolean(_.find(this.selectedProjects, { id: project.id }));
}, },
...@@ -71,8 +80,14 @@ export default { ...@@ -71,8 +80,14 @@ export default {
@input="onInput" @input="onInput"
/> />
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" :size="2" class="py-2 px-4" /> <gl-loading-icon v-if="showLoadingIndicator" :size="1" class="py-2 px-4" />
<div v-if="!showLoadingIndicator" class="d-flex flex-column"> <gl-infinite-scroll
:max-list-height="402"
:fetched-items="projectSearchResults.length"
:total-items="totalResults"
@bottomReached="bottomReached"
>
<div v-if="!showLoadingIndicator" slot="items" class="d-flex flex-column">
<project-list-item <project-list-item
v-for="project in projectSearchResults" v-for="project in projectSearchResults"
:key="project.id" :key="project.id"
...@@ -83,6 +98,7 @@ export default { ...@@ -83,6 +98,7 @@ export default {
@click="projectClicked(project)" @click="projectClicked(project)"
/> />
</div> </div>
</gl-infinite-scroll>
<div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message"> <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message">
{{ __('Sorry, no projects matched your search') }} {{ __('Sorry, no projects matched your search') }}
</div> </div>
......
---
title: Add Infinite scroll to Add Projects modal in the operations dashboard
merge_request: 17842
author:
type: fixed
...@@ -51,6 +51,7 @@ export default { ...@@ -51,6 +51,7 @@ export default {
'projectSearchResults', 'projectSearchResults',
'searchCount', 'searchCount',
'messages', 'messages',
'pageInfo',
]), ]),
projects: { projects: {
get() { get() {
...@@ -76,6 +77,7 @@ export default { ...@@ -76,6 +77,7 @@ export default {
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'fetchNextPage',
'fetchSearchResults', 'fetchSearchResults',
'addProjectsToDashboard', 'addProjectsToDashboard',
'fetchProjects', 'fetchProjects',
...@@ -126,8 +128,10 @@ export default { ...@@ -126,8 +128,10 @@ export default {
:show-loading-indicator="isSearchingProjects" :show-loading-indicator="isSearchingProjects"
:show-minimum-search-query-message="messages.minimumQuery" :show-minimum-search-query-message="messages.minimumQuery"
:show-search-error-message="messages.searchError" :show-search-error-message="messages.searchError"
:total-results="pageInfo.totalResults"
@searched="searched" @searched="searched"
@projectClicked="projectClicked" @projectClicked="projectClicked"
@bottomReached="fetchNextPage"
/> />
</gl-modal> </gl-modal>
......
...@@ -38,7 +38,7 @@ const AdminEmailSelect = () => { ...@@ -38,7 +38,7 @@ const AdminEmailSelect = () => {
const all = { const all = {
id: 'all', id: 'all',
}; };
const data = [all].concat(groups, projects); const data = [all].concat(groups, projects.data);
return query.callback({ return query.callback({
results: data, results: data,
}); });
......
...@@ -163,8 +163,21 @@ export const fetchSearchResults = ({ state, dispatch }) => { ...@@ -163,8 +163,21 @@ export const fetchSearchResults = ({ state, dispatch }) => {
} }
}; };
export const fetchNextPage = ({ state, dispatch }) => {
if (state.pageInfo.totalPages <= state.pageInfo.currentPage) {
return;
}
Api.projects(state.searchQuery, { page: state.pageInfo.nextPage })
.then(results => dispatch('receiveNextPageSuccess', results))
.catch(() => dispatch('receiveSearchResultsError'));
};
export const requestSearchResults = ({ commit }) => commit(types.REQUEST_SEARCH_RESULTS); export const requestSearchResults = ({ commit }) => commit(types.REQUEST_SEARCH_RESULTS);
export const receiveNextPageSuccess = ({ commit }, results) => {
commit(types.RECEIVE_NEXT_PAGE_SUCCESS, results);
};
export const receiveSearchResultsSuccess = ({ commit }, results) => { export const receiveSearchResultsSuccess = ({ commit }, results) => {
commit(types.RECEIVE_SEARCH_RESULTS_SUCCESS, results); commit(types.RECEIVE_SEARCH_RESULTS_SUCCESS, results);
}; };
......
...@@ -17,6 +17,7 @@ export const CLEAR_SEARCH_RESULTS = 'CLEAR_SEARCH_RESULTS'; ...@@ -17,6 +17,7 @@ export const CLEAR_SEARCH_RESULTS = 'CLEAR_SEARCH_RESULTS';
export const REQUEST_SEARCH_RESULTS = 'REQUEST_SEARCH_RESULTS'; export const REQUEST_SEARCH_RESULTS = 'REQUEST_SEARCH_RESULTS';
export const RECEIVE_SEARCH_RESULTS_SUCCESS = 'RECEIVE_SEARCH_RESULTS_SUCCESS'; export const RECEIVE_SEARCH_RESULTS_SUCCESS = 'RECEIVE_SEARCH_RESULTS_SUCCESS';
export const RECEIVE_NEXT_PAGE_SUCCESS = 'RECEIVE_NEXT_PAGE_SUCCESS';
export const RECEIVE_SEARCH_RESULTS_ERROR = 'RECEIVE_SEARCH_RESULTS_ERROR'; export const RECEIVE_SEARCH_RESULTS_ERROR = 'RECEIVE_SEARCH_RESULTS_ERROR';
export const MINIMUM_QUERY_MESSAGE = 'MINIMUM_QUERY_MESSAGE'; export const MINIMUM_QUERY_MESSAGE = 'MINIMUM_QUERY_MESSAGE';
...@@ -3,6 +3,15 @@ import AccessorUtilities from '~/lib/utils/accessor'; ...@@ -3,6 +3,15 @@ import AccessorUtilities from '~/lib/utils/accessor';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
export const updatePageInfo = (state, headers) => {
const pageInfo = parseIntPagination(normalizeHeaders(headers));
Vue.set(state.pageInfo, 'currentPage', pageInfo.page);
Vue.set(state.pageInfo, 'nextPage', pageInfo.nextPage);
Vue.set(state.pageInfo, 'totalResults', pageInfo.total);
Vue.set(state.pageInfo, 'totalPages', pageInfo.totalPages);
};
export default { export default {
[types.SET_PROJECT_ENDPOINT_LIST](state, url) { [types.SET_PROJECT_ENDPOINT_LIST](state, url) {
...@@ -75,12 +84,18 @@ export default { ...@@ -75,12 +84,18 @@ export default {
state.searchCount += 1; state.searchCount += 1;
}, },
[types.RECEIVE_NEXT_PAGE_SUCCESS](state, { data, headers }) {
state.projectSearchResults = state.projectSearchResults.concat(data);
updatePageInfo(state, headers);
},
[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, results) { [types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, results) {
state.projectSearchResults = results; state.projectSearchResults = results.data;
Vue.set(state.messages, 'noResults', state.projectSearchResults.length === 0); Vue.set(state.messages, 'noResults', state.projectSearchResults.length === 0);
Vue.set(state.messages, 'searchError', false); Vue.set(state.messages, 'searchError', false);
Vue.set(state.messages, 'minimumQuery', false); Vue.set(state.messages, 'minimumQuery', false);
updatePageInfo(state, results.headers);
state.searchCount = Math.max(0, state.searchCount - 1); state.searchCount = Math.max(0, state.searchCount - 1);
}, },
[types.RECEIVE_SEARCH_RESULTS_ERROR](state, message) { [types.RECEIVE_SEARCH_RESULTS_ERROR](state, message) {
...@@ -93,6 +108,7 @@ export default { ...@@ -93,6 +108,7 @@ export default {
}, },
[types.MINIMUM_QUERY_MESSAGE](state) { [types.MINIMUM_QUERY_MESSAGE](state) {
state.projectSearchResults = []; state.projectSearchResults = [];
state.pageInfo.totalResults = 0;
Vue.set(state.messages, 'noResults', false); Vue.set(state.messages, 'noResults', false);
Vue.set(state.messages, 'minimumQuery', true); Vue.set(state.messages, 'minimumQuery', true);
Vue.set(state.messages, 'searchError', false); Vue.set(state.messages, 'searchError', false);
......
...@@ -6,6 +6,12 @@ export default () => ({ ...@@ -6,6 +6,12 @@ export default () => ({
add: null, add: null,
}, },
searchQuery: '', searchQuery: '',
pageInfo: {
totalPages: 0,
totalResults: 0,
nextPage: 0,
currentPage: 0,
},
projects: [], projects: [],
projectSearchResults: [], projectSearchResults: [],
selectedProjects: [], selectedProjects: [],
......
...@@ -16,6 +16,7 @@ exports[`dashboard should match the snapshot 1`] = ` ...@@ -16,6 +16,7 @@ exports[`dashboard should match the snapshot 1`] = `
<projectselector-stub <projectselector-stub
projectsearchresults="" projectsearchresults=""
selectedprojects="" selectedprojects=""
totalresults="0"
/> />
</glmodal-stub> </glmodal-stub>
......
...@@ -28,6 +28,7 @@ describe('dashboard', () => { ...@@ -28,6 +28,7 @@ describe('dashboard', () => {
fetchSearchResults: jest.fn(), fetchSearchResults: jest.fn(),
removeProject: jest.fn(), removeProject: jest.fn(),
toggleSelectedProject: jest.fn(), toggleSelectedProject: jest.fn(),
fetchNextPage: jest.fn(),
}; };
propsData = { propsData = {
addPath: 'mock-addPath', addPath: 'mock-addPath',
......
...@@ -9,6 +9,15 @@ export const mockText = { ...@@ -9,6 +9,15 @@ export const mockText = {
RECEIVE_PROJECTS_ERROR: 'Something went wrong, unable to get projects', RECEIVE_PROJECTS_ERROR: 'Something went wrong, unable to get projects',
}; };
export const mockHeaders = {
pageInfo: {
currentPage: 1,
nextPage: 2,
totalResults: 33,
totalPages: 2,
},
};
export function mockPipelineData( export function mockPipelineData(
status = 'success', status = 'success',
id = 1, id = 1,
......
...@@ -5,7 +5,7 @@ import * as types from 'ee/vue_shared/dashboards/store/mutation_types'; ...@@ -5,7 +5,7 @@ import * as types from 'ee/vue_shared/dashboards/store/mutation_types';
import defaultActions, * as actions from 'ee/vue_shared/dashboards/store/actions'; import defaultActions, * as actions from 'ee/vue_shared/dashboards/store/actions';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import clearState from '../helpers'; import clearState from '../helpers';
import { mockText, mockProjectData } from '../mock_data'; import { mockHeaders, mockText, mockProjectData } from '../mock_data';
describe('actions', () => { describe('actions', () => {
const mockAddEndpoint = 'mock-add_endpoint'; const mockAddEndpoint = 'mock-add_endpoint';
...@@ -396,7 +396,7 @@ describe('actions', () => { ...@@ -396,7 +396,7 @@ describe('actions', () => {
}); });
it(`dispatches the correct actions when the query is valid`, done => { it(`dispatches the correct actions when the query is valid`, done => {
mockAxios.onAny().replyOnce(200, mockProjects); mockAxios.onAny().reply(200, mockProjects, mockHeaders);
store.state.searchQuery = 'mock-query'; store.state.searchQuery = 'mock-query';
testAction( testAction(
...@@ -410,12 +410,39 @@ describe('actions', () => { ...@@ -410,12 +410,39 @@ describe('actions', () => {
}, },
{ {
type: 'receiveSearchResultsSuccess', type: 'receiveSearchResultsSuccess',
payload: mockProjects, payload: { data: mockProjects, headers: mockHeaders },
},
],
done,
);
});
});
describe('fetchNextPage', () => {
it(`fetches the next page`, done => {
mockAxios.onAny().reply(200, mockProjects, mockHeaders);
store.state.pageInfo = mockHeaders.pageInfo;
testAction(
actions.fetchNextPage,
null,
store.state,
[],
[
{
type: 'receiveNextPageSuccess',
payload: { data: mockProjects, headers: mockHeaders },
}, },
], ],
done, done,
); );
}); });
it(`stops fetching if current page is the last page`, done => {
mockAxios.onAny().reply(200, mockProjects, mockHeaders);
store.state.pageInfo.totalPages = 3;
store.state.pageInfo.currentPage = 3;
testAction(actions.fetchNextPage, mockHeaders, store.state, [], [], done);
});
}); });
describe('requestSearchResults', () => { describe('requestSearchResults', () => {
...@@ -435,6 +462,24 @@ describe('actions', () => { ...@@ -435,6 +462,24 @@ describe('actions', () => {
}); });
}); });
describe('receiveNextPageSuccess', () => {
it(`commits the RECEIVE_NEXT_PAGE_SUCCESS mutation`, done => {
testAction(
actions.receiveNextPageSuccess,
mockHeaders,
store.state,
[
{
type: types.RECEIVE_NEXT_PAGE_SUCCESS,
payload: mockHeaders,
},
],
[],
done,
);
});
});
describe('receiveSearchResultsSuccess', () => { describe('receiveSearchResultsSuccess', () => {
it('commits the RECEIVE_SEARCH_RESULTS_SUCCESS mutation', done => { it('commits the RECEIVE_SEARCH_RESULTS_SUCCESS mutation', done => {
testAction( testAction(
......
...@@ -194,8 +194,25 @@ describe('mutations', () => { ...@@ -194,8 +194,25 @@ describe('mutations', () => {
}); });
}); });
describe('RECEIEVE_NEXT_PAGE_SUCESS', () => {
it('sets the nextPage and currentPage of results', () => {
localState.projectSearchResults = [{ id: 1 }];
const headers = {
'x-next-page': '3',
'x-page': '2',
};
const results = { data: projects[1], headers };
mutations[types.RECEIVE_NEXT_PAGE_SUCCESS](localState, results);
expect(localState.projectSearchResults.length).toEqual(2);
expect(localState.pageInfo.currentPage).toEqual(2);
expect(localState.pageInfo.nextPage).toEqual(3);
});
});
describe('RECEIVE_SEARCH_RESULTS_SUCCESS', () => { describe('RECEIVE_SEARCH_RESULTS_SUCCESS', () => {
it('resets all messages and sets state.projectSearchResults to the results from the API', () => { it('resets all messages, sets page info, and sets state.projectSearchResults to the results from the API', () => {
localState.projectSearchResults = []; localState.projectSearchResults = [];
localState.messages = { localState.messages = {
noResults: true, noResults: true,
...@@ -203,20 +220,30 @@ describe('mutations', () => { ...@@ -203,20 +220,30 @@ describe('mutations', () => {
minimumQuery: true, minimumQuery: true,
}; };
const searchResults = [{ id: 1 }]; const results = {
data: [{ id: 1 }],
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, searchResults); headers: {
'x-next-page': '2',
'x-page': '1',
'X-Total': '37',
'X-Total-Pages': '2',
},
};
expect(localState.projectSearchResults).toEqual(searchResults); mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, results);
expect(localState.projectSearchResults).toEqual(results.data);
expect(localState.messages.noResults).toBe(false); expect(localState.messages.noResults).toBe(false);
expect(localState.messages.searchError).toBe(false); expect(localState.messages.searchError).toBe(false);
expect(localState.pageInfo).toEqual({
expect(localState.messages.minimumQuery).toBe(false); currentPage: 1,
nextPage: 2,
totalPages: 2,
totalResults: 37,
});
}); });
it('resets all messages and sets state.projectSearchResults to an empty array if no results', () => { it('resets all messages and pageInfo and sets state.projectSearchResults to an empty array if no results', () => {
localState.projectSearchResults = []; localState.projectSearchResults = [];
localState.messages = { localState.messages = {
noResults: false, noResults: false,
...@@ -224,31 +251,35 @@ describe('mutations', () => { ...@@ -224,31 +251,35 @@ describe('mutations', () => {
minimumQuery: true, minimumQuery: true,
}; };
const searchResults = []; const results = { data: [], headers: { 'x-total': 0 } };
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, searchResults); mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, results);
expect(localState.projectSearchResults).toEqual(searchResults); expect(localState.projectSearchResults).toEqual(results.data);
expect(localState.messages.noResults).toBe(true); expect(localState.messages.noResults).toBe(true);
expect(localState.messages.searchError).toBe(false); expect(localState.messages.searchError).toBe(false);
expect(localState.messages.minimumQuery).toBe(false); expect(localState.messages.minimumQuery).toBe(false);
expect(localState.pageInfo.totalResults).toEqual(0);
}); });
it('decrements the search count by one', () => { it('decrements the search count by one', () => {
localState.searchCount = 1; localState.searchCount = 1;
const results = { data: [], headers: {} };
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, []); mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, results);
expect(localState.searchCount).toBe(0); expect(localState.searchCount).toBe(0);
}); });
it('does not decrement the search count to be negative', () => { it('does not decrement the search count to be negative', () => {
localState.searchCount = 0; localState.searchCount = 0;
const results = { data: [], headers: {} };
mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, []); mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](localState, results);
expect(localState.searchCount).toBe(0); expect(localState.searchCount).toBe(0);
}); });
......
...@@ -247,7 +247,7 @@ describe('Frequent Items App Component', () => { ...@@ -247,7 +247,7 @@ describe('Frequent Items App Component', () => {
.then(() => { .then(() => {
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe( expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
mockSearchedProjects.length, mockSearchedProjects.data.length,
); );
}) })
.then(done) .then(done)
......
...@@ -68,7 +68,7 @@ export const mockFrequentGroups = [ ...@@ -68,7 +68,7 @@ export const mockFrequentGroups = [
}, },
]; ];
export const mockSearchedGroups = [mockRawGroup]; export const mockSearchedGroups = { data: [mockRawGroup] };
export const mockProcessedSearchedGroups = [mockGroup]; export const mockProcessedSearchedGroups = [mockGroup];
export const mockProject = { export const mockProject = {
...@@ -135,7 +135,7 @@ export const mockFrequentProjects = [ ...@@ -135,7 +135,7 @@ export const mockFrequentProjects = [
}, },
]; ];
export const mockSearchedProjects = [mockRawProject]; export const mockSearchedProjects = { data: [mockRawProject] };
export const mockProcessedSearchedProjects = [mockProject]; export const mockProcessedSearchedProjects = [mockProject];
export const unsortedFrequentItems = [ export const unsortedFrequentItems = [
......
...@@ -169,7 +169,7 @@ describe('Frequent Items Dropdown Store Actions', () => { ...@@ -169,7 +169,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
}); });
it('should dispatch `receiveSearchedItemsSuccess`', done => { it('should dispatch `receiveSearchedItemsSuccess`', done => {
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects); mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
testAction( testAction(
actions.fetchSearchedItems, actions.fetchSearchedItems,
...@@ -178,7 +178,10 @@ describe('Frequent Items Dropdown Store Actions', () => { ...@@ -178,7 +178,10 @@ describe('Frequent Items Dropdown Store Actions', () => {
[], [],
[ [
{ type: 'requestSearchedItems' }, { type: 'requestSearchedItems' },
{ type: 'receiveSearchedItemsSuccess', payload: mockSearchedProjects }, {
type: 'receiveSearchedItemsSuccess',
payload: { data: mockSearchedProjects, headers: {} },
},
], ],
done, done,
); );
......
...@@ -3,7 +3,7 @@ import _ from 'underscore'; ...@@ -3,7 +3,7 @@ import _ from 'underscore';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import { GlSearchBoxByType } from '@gitlab/ui'; import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper'; import { trimText } from 'spec/helpers/text_helper';
...@@ -91,6 +91,13 @@ describe('ProjectSelector component', () => { ...@@ -91,6 +91,13 @@ describe('ProjectSelector component', () => {
expect(searchInput.attributes('placeholder')).toBe('Search your projects'); expect(searchInput.attributes('placeholder')).toBe('Search your projects');
}); });
it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => {
spyOn(vm, '$emit');
wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached');
expect(vm.$emit).toHaveBeenCalledWith('bottomReached');
});
it(`triggers a "projectClicked" event when a project is clicked`, () => { it(`triggers a "projectClicked" event when a project is clicked`, () => {
spyOn(vm, '$emit'); spyOn(vm, '$emit');
wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults)); wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment