Commit 149460b8 authored by Phil Hughes's avatar Phil Hughes

Merge branch '32098-pipelines-navigation' into 'master'

Stops page reload when changing tabs or pages - uses API requests instead

See merge request gitlab-org/gitlab-ce!15280
parents 62287fec be4abe77
...@@ -309,6 +309,42 @@ export const setParamInURL = (param, value) => { ...@@ -309,6 +309,42 @@ export const setParamInURL = (param, value) => {
return search; return search;
}; };
/**
* Given a string of query parameters creates an object.
*
* @example
* `scope=all&page=2` -> { scope: 'all', page: '2'}
* `scope=all` -> { scope: 'all' }
* ``-> {}
* @param {String} query
* @returns {Object}
*/
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
return query
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
Object.assign(acc, {
[val[0]]: decodeURIComponent(val[1]),
});
return acc;
}, {});
};
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
* creates a new entry in the history without reloading the page.
*
* @param {String} param
*/
export const historyPushState = (newUrl) => {
window.history.pushState({}, document.title, newUrl);
};
/** /**
* Converts permission provided as strings to booleans. * Converts permission provided as strings to booleans.
* *
......
...@@ -60,7 +60,6 @@ export default class Poll { ...@@ -60,7 +60,6 @@ export default class Poll {
checkConditions(response) { checkConditions(response) {
const headers = normalizeHeaders(response.headers); const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10); const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => { this.timeoutID = setTimeout(() => {
this.makeRequest(); this.makeRequest();
...@@ -102,7 +101,12 @@ export default class Poll { ...@@ -102,7 +101,12 @@ export default class Poll {
/** /**
* Restarts polling after it has been stoped * Restarts polling after it has been stoped
*/ */
restart() { restart(options) {
// update data
if (options && options.data) {
this.options.data = options.data;
}
this.canPoll = true; this.canPoll = true;
this.makeRequest(); this.makeRequest();
} }
......
...@@ -2,16 +2,8 @@ ...@@ -2,16 +2,8 @@
export default { export default {
name: 'PipelineNavigationTabs', name: 'PipelineNavigationTabs',
props: { props: {
scope: { tabs: {
type: String, type: Array,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true, required: true,
}, },
}, },
...@@ -23,68 +15,37 @@ ...@@ -23,68 +15,37 @@
// 0 is valid in a badge, but evaluates to false, we need to check for undefined // 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined; return count !== undefined;
}, },
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
}, },
}; };
</script> </script>
<template> <template>
<ul class="nav-links scrolling-tabs"> <ul class="nav-links scrolling-tabs">
<li <li
class="js-pipelines-tab-all" v-for="(tab, i) in tabs"
:class="{ active: scope === 'all'}"> :key="i"
<a :href="paths.allPath"> :class="{
All active: tab.isActive,
<span }"
v-if="shouldRenderBadge(count.all)" >
class="badge js-totalbuilds-count"> <a
{{count.all}} role="button"
</span> @click="onTabClick(tab)"
</a> :class="`js-pipelines-tab-${tab.scope}`"
</li> >
<li {{ tab.name }}
class="js-pipelines-tab-pending"
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span
v-if="shouldRenderBadge(count.pending)"
class="badge">
{{count.pending}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-running"
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span
v-if="shouldRenderBadge(count.running)"
class="badge">
{{count.running}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-finished"
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span <span
v-if="shouldRenderBadge(count.finished)" v-if="shouldRenderBadge(tab.count)"
class="badge"> class="badge"
{{count.finished}} >
{{tab.count}}
</span> </span>
</a> </a>
</li> </li>
<li
class="js-pipelines-tab-branches"
:class="{ active: scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li
class="js-pipelines-tab-tags"
:class="{ active: scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul> </ul>
</template> </template>
<script> <script>
import _ from 'underscore';
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines'; import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue'; import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import navigationControls from './nav_controls.vue';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; import {
convertPermissionToBoolean,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
export default { export default {
props: { props: {
...@@ -41,27 +48,18 @@ ...@@ -41,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath, autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath, newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline, canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi, hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath, ciLintPath: pipelinesData.ciLintPath,
state: this.store.state, state: this.store.state,
apiScope: 'all', scope: getParameterByName('scope') || 'all',
pagenum: 1, page: getParameterByName('page') || '1',
requestData: {},
}; };
}, },
computed: { computed: {
canCreatePipelineParsed() { canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline); return convertPermissionToBoolean(this.canCreatePipeline);
}, },
scope() {
const scope = getParameterByName('scope');
return scope === null ? 'all' : scope;
},
/** /**
* The empty state should only be rendered when the request is made to fetch all pipelines * The empty state should only be rendered when the request is made to fetch all pipelines
...@@ -106,46 +104,112 @@ ...@@ -106,46 +104,112 @@
hasCiEnabled() { hasCiEnabled() {
return this.hasCi !== undefined; return this.hasCi !== undefined;
}, },
paths() {
return { tabs() {
allPath: this.allPath, const { count } = this.state;
pendingPath: this.pendingPath, return [
finishedPath: this.finishedPath, {
runningPath: this.runningPath, name: 'All',
branchesPath: this.branchesPath, scope: 'all',
tagsPath: this.tagsPath, count: count.all,
}; isActive: this.scope === 'all',
}, },
pageParameter() { {
return getParameterByName('page') || this.pagenum; name: 'Pending',
}, scope: 'pending',
scopeParameter() { count: count.pending,
return getParameterByName('scope') || this.apiScope; isActive: this.scope === 'pending',
},
{
name: 'Running',
scope: 'running',
count: count.running,
isActive: this.scope === 'running',
},
{
name: 'Finished',
scope: 'finished',
count: count.finished,
isActive: this.scope === 'finished',
},
{
name: 'Branches',
scope: 'branches',
isActive: this.scope === 'branches',
},
{
name: 'Tags',
scope: 'tags',
isActive: this.scope === 'tags',
},
];
}, },
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; this.requestData = { page: this.page, scope: this.scope };
}, },
methods: { methods: {
successCallback(resp) {
return resp.json().then((response) => {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeCount(response.count);
this.store.storePagination(resp.headers);
this.setCommonData(response.pipelines);
}
});
},
/** /**
* Will change the page number and update the URL. * Handles URL and query parameter changes.
* * When the user uses the pagination or the tabs,
* @param {Number} pageNumber desired page to go to. * - update URL
* - Make API request to the server with new parameters
* - Update the polling function
* - Update the internal state
*/ */
change(pageNumber) { updateContent(parameters) {
const param = setParamInURL('page', pageNumber); // stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
gl.utils.visitUrl(param); // update polling parameters
return param; this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
this.isLoading = false;
this.successCallback(response);
// restart polling
this.poll.restart({ data: this.requestData });
})
.catch(() => {
this.isLoading = false;
this.errorCallback();
// restart polling
this.poll.restart();
});
}, },
successCallback(resp) { onChangeTab(scope) {
return resp.json().then((response) => { this.updateContent({ scope, page: '1' });
this.store.storeCount(response.count); },
this.store.storePagination(resp.headers); onChangePage(page) {
this.setCommonData(response.pipelines); /* URLS parameters are strings, we need to parse to match types */
}); this.updateContent({ scope: this.scope, page: Number(page).toString() });
}, },
}, },
}; };
...@@ -154,7 +218,7 @@ ...@@ -154,7 +218,7 @@
<div class="pipelines-container"> <div class="pipelines-container">
<div <div
class="top-area scrolling-tabs-container inner-page-scroll-tabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState"> v-if="!shouldRenderEmptyState">
<div class="fade-left"> <div class="fade-left">
<i <i
class="fa fa-angle-left" class="fa fa-angle-left"
...@@ -167,17 +231,17 @@ ...@@ -167,17 +231,17 @@
aria-hidden="true"> aria-hidden="true">
</i> </i>
</div> </div>
<navigation-tabs <navigation-tabs
:scope="scope" :tabs="tabs"
:count="state.count" @onChangeTab="onChangeTab"
:paths="paths"
/> />
<navigation-controls <navigation-controls
:new-pipeline-path="newPipelinePath" :new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled" :has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:ciLintPath="ciLintPath" :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " :can-create-pipeline="canCreatePipelineParsed "
/> />
</div> </div>
...@@ -188,6 +252,7 @@ ...@@ -188,6 +252,7 @@
label="Loading Pipelines" label="Loading Pipelines"
size="3" size="3"
v-if="isLoading" v-if="isLoading"
class="prepend-top-20"
/> />
<empty-state <empty-state
...@@ -221,8 +286,8 @@ ...@@ -221,8 +286,8 @@
<table-pagination <table-pagination
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:change="change" :change="onChangePage"
:pageInfo="state.pageInfo" :page-info="state.pageInfo"
/> />
</div> </div>
</div> </div>
......
...@@ -9,12 +9,6 @@ ...@@ -9,12 +9,6 @@
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project), "new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project),
"pending-path" => project_pipelines_path(@project, scope: :pending),
"running-path" => project_pipelines_path(@project, scope: :running),
"finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml, "has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } } "ci-lint-path" => ci_lint_path } }
......
---
title: Stop reloading the page when using pagination and tabs - use API calls - in
Pipelines table
merge_request:
author:
type: other
...@@ -56,31 +56,37 @@ describe 'Pipelines', :js do ...@@ -56,31 +56,37 @@ describe 'Pipelines', :js do
end end
it 'shows a tab for All pipelines and count' do it 'shows a tab for All pipelines and count' do
expect(page.find('.js-pipelines-tab-all a').text).to include('All') expect(page.find('.js-pipelines-tab-all').text).to include('All')
expect(page.find('.js-pipelines-tab-all .badge').text).to include('1') expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
end end
it 'shows a tab for Pending pipelines and count' do it 'shows a tab for Pending pipelines and count' do
expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending') expect(page.find('.js-pipelines-tab-pending').text).to include('Pending')
expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0') expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0')
end end
it 'shows a tab for Running pipelines and count' do it 'shows a tab for Running pipelines and count' do
expect(page.find('.js-pipelines-tab-running a').text).to include('Running') expect(page.find('.js-pipelines-tab-running').text).to include('Running')
expect(page.find('.js-pipelines-tab-running .badge').text).to include('1') expect(page.find('.js-pipelines-tab-running .badge').text).to include('1')
end end
it 'shows a tab for Finished pipelines and count' do it 'shows a tab for Finished pipelines and count' do
expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished') expect(page.find('.js-pipelines-tab-finished').text).to include('Finished')
expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0') expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0')
end end
it 'shows a tab for Branches' do it 'shows a tab for Branches' do
expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches') expect(page.find('.js-pipelines-tab-branches').text).to include('Branches')
end end
it 'shows a tab for Tags' do it 'shows a tab for Tags' do
expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags') expect(page.find('.js-pipelines-tab-tags').text).to include('Tags')
end
it 'updates content when tab is clicked' do
page.find('.js-pipelines-tab-pending').click
wait_for_requests
expect(page).to have_content('No pipelines to show.')
end end
end end
...@@ -396,6 +402,14 @@ describe 'Pipelines', :js do ...@@ -396,6 +402,14 @@ describe 'Pipelines', :js do
expect(page).to have_selector('.gl-pagination .page', count: 2) expect(page).to have_selector('.gl-pagination .page', count: 2)
end end
it 'should show updated content' do
visit project_pipelines_path(project)
wait_for_requests
page.find('.js-next-button a').click
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
end end
end end
......
%div %div
#pipelines-list-vue{ data: { endpoint: 'foo', #pipelines-list-vue{ data: { endpoint: 'foo',
"css-class" => 'foo',
"help-page-path" => 'foo', "help-page-path" => 'foo',
"help-auto-devops-path" => 'foo',
"empty-state-svg-path" => 'foo', "empty-state-svg-path" => 'foo',
"error-state-svg-path" => 'foo', "error-state-svg-path" => 'foo',
"new-pipeline-path" => 'foo', "new-pipeline-path" => 'foo',
"can-create-pipeline" => 'true', "can-create-pipeline" => 'true',
"all-path" => 'foo',
"pending-path" => 'foo',
"running-path" => 'foo',
"finished-path" => 'foo',
"branches-path" => 'foo',
"tags-path" => 'foo',
"has-ci" => 'foo', "has-ci" => 'foo',
"ci-lint-path" => 'foo' } } "ci-lint-path" => 'foo' } }
...@@ -183,6 +183,36 @@ describe('common_utils', () => { ...@@ -183,6 +183,36 @@ describe('common_utils', () => {
}); });
}); });
describe('historyPushState', () => {
afterEach(() => {
window.history.replaceState({}, null, null);
});
it('should call pushState with the correct path', () => {
spyOn(window.history, 'pushState');
commonUtils.historyPushState('newpath?page=2');
expect(window.history.pushState).toHaveBeenCalled();
expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2');
});
});
describe('parseQueryStringIntoObject', () => {
it('should return object with query parameters', () => {
expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' });
expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
});
});
describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`);
});
});
describe('getParameterByName', () => { describe('getParameterByName', () => {
beforeEach(() => { beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2'); window.history.pushState({}, null, '?scope=all&p=2');
......
...@@ -155,7 +155,7 @@ describe('Poll', () => { ...@@ -155,7 +155,7 @@ describe('Poll', () => {
successCallback: () => { successCallback: () => {
Polling.stop(); Polling.stop();
setTimeout(() => { setTimeout(() => {
Polling.restart(); Polling.restart({ data: { page: 4 } });
}, 0); }, 0);
}, },
errorCallback: callbacks.error, errorCallback: callbacks.error,
...@@ -170,10 +170,10 @@ describe('Poll', () => { ...@@ -170,10 +170,10 @@ describe('Poll', () => {
Polling.stop(); Polling.stop();
expect(service.fetch.calls.count()).toEqual(2); expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
expect(Polling.stop).toHaveBeenCalled(); expect(Polling.stop).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled();
expect(Polling.options.data).toEqual({ page: 4 });
done(); done();
}); });
}); });
......
...@@ -8,120 +8,48 @@ describe('navigation tabs pipeline component', () => { ...@@ -8,120 +8,48 @@ describe('navigation tabs pipeline component', () => {
let data; let data;
beforeEach(() => { beforeEach(() => {
data = { data = [
scope: 'all', {
count: { name: 'All',
all: 16, scope: 'all',
running: 1, count: 1,
pending: 10, isActive: true,
finished: 0, },
{
name: 'Pending',
scope: 'pending',
count: 0,
isActive: false,
}, },
paths: { {
allPath: '/gitlab-org/gitlab-ce/pipelines', name: 'Running',
pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending', scope: 'running',
finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished', isActive: false,
runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running',
branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches',
tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags',
}, },
}; ];
Component = Vue.extend(navigationTabs); Component = Vue.extend(navigationTabs);
vm = mountComponent(Component, { tabs: data });
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
}); });
it('should render tabs with correct paths', () => { it('should render tabs', () => {
vm = mountComponent(Component, data); expect(vm.$el.querySelectorAll('li').length).toEqual(data.length);
// All
const allTab = vm.$el.querySelector('.js-pipelines-tab-all a');
expect(allTab.textContent.trim()).toContain('All');
expect(allTab.getAttribute('href')).toEqual(data.paths.allPath);
// Pending
const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a');
expect(pendingTab.textContent.trim()).toContain('Pending');
expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath);
// Running
const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a');
expect(runningTab.textContent.trim()).toContain('Running');
expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath);
// Finished
const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a');
expect(finishedTab.textContent.trim()).toContain('Finished');
expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath);
// Branches
const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a');
expect(branchesTab.textContent.trim()).toContain('Branches');
// Tags
const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a');
expect(tagsTab.textContent.trim()).toContain('Tags');
}); });
describe('scope', () => { it('should render active tab', () => {
it('should render scope provided as active tab', () => { expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined();
vm = mountComponent(Component, data);
expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active');
});
}); });
describe('badges', () => { it('should render badge', () => {
it('should render provided number', () => { expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1');
vm = mountComponent(Component, data); expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual('0');
// All });
expect(
vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(),
).toContain(data.count.all);
// Pending
expect(
vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(),
).toContain(data.count.pending);
// Running
expect(
vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(),
).toContain(data.count.running);
// Finished
expect(
vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(),
).toContain(data.count.finished);
});
it('should not render badge when number is undefined', () => {
vm = mountComponent(Component, {
scope: 'all',
paths: {},
count: {},
});
// All
expect(
vm.$el.querySelector('.js-totalbuilds-count'),
).toEqual(null);
// Pending
expect(
vm.$el.querySelector('.js-pipelines-tab-pending .badge'),
).toEqual(null);
// Running
expect(
vm.$el.querySelector('.js-pipelines-tab-running .badge'),
).toEqual(null);
// Finished it('should not render badge', () => {
expect( expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
vm.$el.querySelector('.js-pipelines-tab-finished .badge'),
).toEqual(null);
});
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import pipelinesComp from '~/pipelines/components/pipelines.vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store'; import Store from '~/pipelines/stores/pipelines_store';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines', () => { describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
...@@ -9,26 +10,33 @@ describe('Pipelines', () => { ...@@ -9,26 +10,33 @@ describe('Pipelines', () => {
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
let PipelinesComponent; let PipelinesComponent;
let pipeline; let pipelines;
let component;
beforeEach(() => { beforeEach(() => {
loadFixtures('static/pipelines.html.raw'); loadFixtures('static/pipelines.html.raw');
const pipelines = getJSONFixture(jsonFixtureName).pipelines; pipelines = getJSONFixture(jsonFixtureName);
pipeline = pipelines.find(p => p.id === 1);
PipelinesComponent = Vue.extend(pipelinesComp); PipelinesComponent = Vue.extend(pipelinesComp);
}); });
afterEach(() => {
component.$destroy();
});
describe('successfull request', () => { describe('successfull request', () => {
describe('with pipelines', () => { describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => { const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipeline), { next(request.respondWith(JSON.stringify(pipelines), {
status: 200, status: 200,
})); }));
}; };
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor); Vue.http.interceptors.push(pipelinesInterceptor);
component = mountComponent(PipelinesComponent, {
store: new Store(),
});
}); });
afterEach(() => { afterEach(() => {
...@@ -38,18 +46,71 @@ describe('Pipelines', () => { ...@@ -38,18 +46,71 @@ describe('Pipelines', () => {
}); });
it('should render table', (done) => { it('should render table', (done) => {
const component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelector('.table-holder')).toBeDefined(); expect(component.$el.querySelector('.table-holder')).toBeDefined();
expect(component.$el.querySelector('.realtime-loading')).toBe(null); expect(
component.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
done(); done();
}); });
}); });
it('should render navigation tabs', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
).toContain('Pending');
expect(
component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
).toContain('All');
expect(
component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
).toContain('Running');
expect(
component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
).toContain('Finished');
expect(
component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
).toContain('Branches');
expect(
component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
).toContain('Tags');
done();
});
});
it('should make an API request when using tabs', (done) => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-pipelines-tab-finished').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
done();
});
});
describe('with pagination', () => {
it('should make an API request when using pagination', (done) => {
setTimeout(() => {
spyOn(component, 'updateContent');
// Mock pagination
component.store.state.pageInfo = {
page: 1,
total: 10,
perPage: 2,
nextPage: 2,
totalPages: 5,
};
Vue.nextTick(() => {
component.$el.querySelector('.js-next-button a').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
done();
});
});
});
});
}); });
describe('without pipelines', () => { describe('without pipelines', () => {
...@@ -70,15 +131,14 @@ describe('Pipelines', () => { ...@@ -70,15 +131,14 @@ describe('Pipelines', () => {
}); });
it('should render empty state', (done) => { it('should render empty state', (done) => {
const component = new PipelinesComponent({ component = new PipelinesComponent({
propsData: { propsData: {
store: new Store(), store: new Store(),
}, },
}).$mount(); }).$mount();
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelector('.empty-state')).toBeDefined(); expect(component.$el.querySelector('.empty-state')).not.toBe(null);
expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done(); done();
}); });
}); });
...@@ -103,7 +163,7 @@ describe('Pipelines', () => { ...@@ -103,7 +163,7 @@ describe('Pipelines', () => {
}); });
it('should render error state', (done) => { it('should render error state', (done) => {
const component = new PipelinesComponent({ component = new PipelinesComponent({
propsData: { propsData: {
store: new Store(), store: new Store(),
}, },
...@@ -111,9 +171,50 @@ describe('Pipelines', () => { ...@@ -111,9 +171,50 @@ describe('Pipelines', () => {
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done(); done();
}); });
}); });
}); });
describe('updateContent', () => {
it('should set given parameters', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
});
component.updateContent({ scope: 'finished', page: '4' });
expect(component.page).toEqual('4');
expect(component.scope).toEqual('finished');
expect(component.requestData.scope).toEqual('finished');
expect(component.requestData.page).toEqual('4');
});
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
});
spyOn(component, 'updateContent');
component.onChangeTab('running');
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
});
});
describe('onChangePage', () => {
it('should update page and keep scope', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
});
spyOn(component, 'updateContent');
component.onChangePage(4);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
});
});
}); });
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