Commit e734c5cd authored by Filipa Lacerda's avatar Filipa Lacerda

Manage empty states in Pipelines page

Adds i18n
Adds test

Fix broken tests

Fixes empty tab state for external CI
parent 1618aa49
...@@ -20,10 +20,6 @@ ...@@ -20,10 +20,6 @@
type: String, type: String,
required: true, required: true,
}, },
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: { errorStateSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -45,23 +41,14 @@ ...@@ -45,23 +41,14 @@
}, },
computed: { computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() { shouldRenderTable() {
return !this.isLoading && return !this.isLoading &&
this.state.pipelines.length > 0 && this.state.pipelines.length > 0 &&
!this.hasError; !this.hasError;
}, },
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
...@@ -92,25 +79,22 @@ ...@@ -92,25 +79,22 @@
<div class="content-list pipelines"> <div class="content-list pipelines">
<loading-icon <loading-icon
label="Loading pipelines" :label="s__('Pipelines|Loading Pipelines')"
size="3" size="3"
v-if="isLoading" v-if="isLoading"
class="prepend-top-20"
/> />
<empty-state <svg-blank-state
v-if="shouldRenderEmptyState" v-else-if="shouldRenderErrorState"
:help-page-path="helpPagePath" :svg-path="errorStateSvgPath"
:empty-state-svg-path="emptyStateSvgPath" :message="s__(`Pipelines|There was an error with fetching the pipelines.
/> Try again in a few moments or contact your support team.`)"
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/> />
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable" v-else-if="shouldRenderTable"
> >
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate'; import Translate from '../../../../vue_shared/translate';
import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate); Vue.use(Translate);
...@@ -15,12 +16,25 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -15,12 +16,25 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return { return {
store, store,
dataset: document.querySelector(this.$options.el).dataset,
}; };
}, },
render(createElement) { render(createElement) {
return createElement('pipelines-component', { return createElement('pipelines-component', {
props: { props: {
store: this.store, store: this.store,
endpoint: this.dataset.endpoint,
helpPagePath: this.dataset.helpPagePath,
emptyStateSvgPath: this.dataset.emptyStateSvgPath,
errorStateSvgPath: this.dataset.errorStateSvgPath,
noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
autoDevopsPath: this.dataset.helpAutoDevopsPath,
newPipelinePath: this.dataset.newPipelinePath,
canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath,
}, },
}); });
}, },
......
<script> <script>
export default { export default {
props: { name: 'PipelinesSvgState',
errorStateSvgPath: { props: {
type: String, svgPath: {
required: true, type: String,
required: true,
},
message: {
type: String,
required: true,
},
}, },
}, };
};
</script> </script>
<template> <template>
<div class="row empty-state js-pipelines-error-state"> <div class="row empty-state">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="svg-content"> <div class="svg-content">
<img :src="errorStateSvgPath"/> <img :src="svgPath" />
</div> </div>
</div> </div>
<div class="col-xs-12 text-center"> <div class="col-xs-12 text-center">
<div class="text-content"> <div class="text-content">
<h4>The API failed to fetch the pipelines.</h4> <h4>{{ message }}</h4>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
export default { export default {
name: 'PipelinesEmptyState',
props: { props: {
helpPagePath: { helpPagePath: {
type: String, type: String,
...@@ -9,6 +10,10 @@ ...@@ -9,6 +10,10 @@
type: String, type: String,
required: true, required: true,
}, },
canSetCi: {
type: Boolean,
required: true,
},
}, },
}; };
</script> </script>
...@@ -22,22 +27,36 @@ ...@@ -22,22 +27,36 @@
<div class="col-xs-12"> <div class="col-xs-12">
<div class="text-content"> <div class="text-content">
<h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }} <template v-if="canSetCi">
</h4> <h4 class="text-center">
<p> {{ s__('Pipelines|Build with confidence') }}
{{ s__(`Pipelines|Continous Integration can help </h4>
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.`) }} <p>
{{ s__(`Pipelines|Continous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`) }}
</p>
<div class="text-center">
<a
:href="helpPagePath"
class="btn btn-info js-get-started-pipelines"
>
{{ s__('Pipelines|Get started with Pipelines') }}
</a>
</div>
</template>
<p
v-else
class="text-center"
>
{{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p> </p>
<div class="text-center">
<a
:href="helpPagePath"
class="btn btn-info"
>
{{ s__("Pipelines|Get started with Pipelines") }}
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
export default { export default {
name: 'PipelineNavControls', name: 'PipelineNavControls',
props: { props: {
newPipelinePath: { newPipelinePath: {
type: String, type: String,
required: true, required: false,
default: null,
},
resetCachePath: {
type: String,
required: false,
default: null,
},
ciLintPath: {
type: String,
required: false,
default: null,
},
}, },
};
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
resetCachePath: {
type: String,
required: true,
},
ciLintPath: {
type: String,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
},
},
};
</script> </script>
<template> <template>
<div class="nav-controls"> <div class="nav-controls">
<a <a
v-if="canCreatePipeline" v-if="newPipelinePath"
:href="newPipelinePath" :href="newPipelinePath"
class="btn btn-create"> class="btn btn-create js-run-pipeline"
Run Pipeline >
</a> {{ s__('Pipelines|Run Pipeline') }}
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a> </a>
<a <a
v-if="resetCachePath"
data-method="post" data-method="post"
rel="nofollow"
:href="resetCachePath" :href="resetCachePath"
class="btn btn-default"> class="btn btn-default js-clear-cache"
Clear runner caches >
{{ s__('Pipelines|Clear Runner Caches') }}
</a> </a>
<a <a
v-if="ciLintPath"
:href="ciLintPath" :href="ciLintPath"
class="btn btn-default"> class="btn btn-default js-ci-lint"
CI Lint >
{{ s__('Pipelines|CI Lint') }}
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { __, sprintf, s__ } from '../../locale';
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 '../../vue_shared/components/navigation_tabs.vue'; import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import NavigationControls from './nav_controls.vue';
import { import {
convertPermissionToBoolean,
getParameterByName, getParameterByName,
parseQueryStringIntoObject, parseQueryStringIntoObject,
} from '../../lib/utils/common_utils'; } from '../../lib/utils/common_utils';
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
export default { export default {
components: { components: {
tablePagination, TablePagination,
navigationTabs, NavigationTabs,
navigationControls, NavigationControls,
}, },
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
...@@ -36,111 +36,186 @@ ...@@ -36,111 +36,186 @@
required: false, required: false,
default: 'root', default: 'root',
}, },
endpoint: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
noPipelinesSvgPath: {
type: String,
required: true,
},
autoDevopsPath: {
type: String,
required: true,
},
hasGitlabCi: {
type: Boolean,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
},
ciLintPath: {
type: String,
required: false,
default: null,
},
resetCachePath: {
type: String,
required: false,
default: null,
},
newPipelinePath: {
type: String,
required: false,
default: null,
},
}, },
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return { return {
endpoint: pipelinesData.endpoint, // Start with loading state to avoid a glitch when the empty state will be rendered
helpPagePath: pipelinesData.helpPagePath, isLoading: true,
emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
errorStateSvgPath: pipelinesData.errorStateSvgPath,
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
resetCachePath: pipelinesData.resetCachePath,
state: this.store.state, state: this.store.state,
scope: getParameterByName('scope') || 'all', scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
requestData: {}, requestData: {},
}; };
}, },
computed: { stateMap: {
canCreatePipelineParsed() { // with tabs
return convertPermissionToBoolean(this.canCreatePipeline); loading: 'loading',
}, tableList: 'tableList',
error: 'error',
emptyTab: 'emptyTab',
// without tabs
emptyState: 'emptyState',
},
scopes: {
all: 'all',
pending: 'pending',
running: 'running',
finished: 'finished',
branches: 'branches',
tags: 'tags',
},
computed: {
/** /**
* The empty state should only be rendered when the request is made to fetch all pipelines * `hasGitlabCi` handles both internal and external CI.
* and none is returned. * The order on which the checks are made in this method is
* * important to guarantee we handle all the corner cases.
* @return {Boolean} */
*/ stateToRender() {
shouldRenderEmptyState() { const { stateMap } = this.$options;
return !this.isLoading &&
!this.hasError && if (this.isLoading) {
this.hasMadeRequest && return stateMap.loading;
!this.state.pipelines.length && }
(this.scope === 'all' || this.scope === null);
if (this.hasError) {
return stateMap.error;
}
if (this.state.pipelines.length) {
return stateMap.tableList;
}
if (this.hasGitlabCi) {
return stateMap.emptyTab;
}
return stateMap.emptyState;
}, },
/** /**
* When a specific scope does not have pipelines we render a message. * Tabs are rendered in all states except empty state.
* * They are not rendered before the first request to avoid a flicker on first load.
* @return {Boolean}
*/ */
shouldRenderNoPipelinesMessage() { shouldRenderTabs() {
return !this.isLoading && const { stateMap } = this.$options;
!this.hasError && return this.hasMadeRequest &&
!this.state.pipelines.length && [
this.scope !== 'all' && stateMap.loading,
this.scope !== null; stateMap.tableList,
stateMap.error,
stateMap.emptyTab,
].includes(this.stateToRender);
}, },
shouldRenderTable() { shouldRenderButtons() {
return !this.hasError && return (this.newPipelinePath ||
!this.isLoading && this.state.pipelines.length; this.resetCachePath ||
this.ciLintPath) && this.shouldRenderTabs;
}, },
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() { shouldRenderPagination() {
return !this.isLoading && return !this.isLoading &&
this.state.pipelines.length && this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage; this.state.pageInfo.total > this.state.pageInfo.perPage;
}, },
hasCiEnabled() {
return this.hasCi !== undefined; emptyTabMessage() {
const { scopes } = this.$options;
const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
if (possibleScopes.includes(this.scope)) {
return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
scope: this.scope,
});
}
return s__('Pipelines|There are currently no pipelines.');
}, },
tabs() { tabs() {
const { count } = this.state; const { count } = this.state;
const { scopes } = this.$options;
return [ return [
{ {
name: 'All', name: __('All'),
scope: 'all', scope: scopes.all,
count: count.all, count: count.all,
isActive: this.scope === 'all', isActive: this.scope === 'all',
}, },
{ {
name: 'Pending', name: __('Pending'),
scope: 'pending', scope: scopes.pending,
count: count.pending, count: count.pending,
isActive: this.scope === 'pending', isActive: this.scope === 'pending',
}, },
{ {
name: 'Running', name: __('Running'),
scope: 'running', scope: scopes.running,
count: count.running, count: count.running,
isActive: this.scope === 'running', isActive: this.scope === 'running',
}, },
{ {
name: 'Finished', name: __('Finished'),
scope: 'finished', scope: scopes.finished,
count: count.finished, count: count.finished,
isActive: this.scope === 'finished', isActive: this.scope === 'finished',
}, },
{ {
name: 'Branches', name: __('Branches'),
scope: 'branches', scope: scopes.branches,
isActive: this.scope === 'branches', isActive: this.scope === 'branches',
}, },
{ {
name: 'Tags', name: __('Tags'),
scope: 'tags', scope: scopes.tags,
isActive: this.scope === 'tags', isActive: this.scope === 'tags',
}, },
]; ];
...@@ -187,7 +262,7 @@ ...@@ -187,7 +262,7 @@
this.errorCallback(); this.errorCallback();
// restart polling // restart polling
this.poll.restart(); this.poll.restart({ data: this.requestData });
}); });
}, },
}, },
...@@ -197,69 +272,70 @@ ...@@ -197,69 +272,70 @@
<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="!shouldRenderEmptyState" v-if="shouldRenderTabs || shouldRenderButtons"
> >
<div class="fade-left"> <div class="fade-left">
<i <i
class="fa fa-angle-left" class="fa fa-angle-left"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</div> </div>
<div class="fade-right"> <div class="fade-right">
<i <i
class="fa fa-angle-right" class="fa fa-angle-right"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</div> </div>
<navigation-tabs <navigation-tabs
v-if="shouldRenderTabs"
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="pipelines" scope="pipelines"
/> />
<navigation-controls <navigation-controls
v-if="shouldRenderButtons"
:new-pipeline-path="newPipelinePath" :new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:reset-cache-path="resetCachePath" :reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath" :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/> />
</div> </div>
<div class="content-list pipelines"> <div class="content-list pipelines">
<loading-icon <loading-icon
label="Loading Pipelines" v-if="stateToRender === $options.stateMap.loading"
:label="s__('Pipelines|Loading Pipelines')"
size="3" size="3"
v-if="isLoading"
class="prepend-top-20" class="prepend-top-20"
/> />
<empty-state <empty-state
v-if="shouldRenderEmptyState" v-else-if="stateToRender === $options.stateMap.emptyState"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
/> />
<error-state <svg-blank-state
v-if="shouldRenderErrorState" v-else-if="stateToRender === $options.stateMap.error"
:error-state-svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
:message="s__(`Pipelines|There was an error with fetching the pipelines.
Try again in a few moments or contact your support team.`)"
/> />
<div <svg-blank-state
class="blank-state-row" v-else-if="stateToRender === $options.stateMap.emptyTab"
v-if="shouldRenderNoPipelinesMessage" :svg-path="noPipelinesSvgPath"
> :message="emptyTabMessage"
<div class="blank-state-center"> />
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
</div>
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable" v-else-if="stateToRender === $options.stateMap.tableList"
> >
<pipelines-table-component <pipelines-table-component
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue'; import EmptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue'; import SvgBlankState from '../components/blank_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import pipelinesTableComponent from '../components/pipelines_table.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
pipelinesTableComponent, PipelinesTableComponent,
errorState, SvgBlankState,
emptyState, EmptyState,
loadingIcon, LoadingIcon,
},
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
data() { data() {
return { return {
...@@ -85,6 +81,7 @@ export default { ...@@ -85,6 +81,7 @@ export default {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
this.hasMadeRequest = true;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
...@@ -96,7 +93,7 @@ export default { ...@@ -96,7 +93,7 @@ export default {
postAction(endpoint) { postAction(endpoint) {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines')) .then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occurred while making the request.')); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
}, },
}; };
...@@ -5,7 +5,12 @@ export default class PipelinesStore { ...@@ -5,7 +5,12 @@ export default class PipelinesStore {
this.state = {}; this.state = {};
this.state.pipelines = []; this.state.pipelines = [];
this.state.count = {}; this.state.count = {
all: 0,
finished: 0,
pending: 0,
running: 0,
};
this.state.pageInfo = {}; this.state.pageInfo = {};
} }
......
...@@ -10,8 +10,9 @@ ...@@ -10,8 +10,9 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"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), "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"has-ci" => @repository.gitlab_ci_yml, "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
"ci-lint-path" => ci_lint_path, "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
"has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
---
title: Handle empty state in Pipelines page
merge_request:
author:
type: fixed
...@@ -86,7 +86,22 @@ describe 'Pipelines', :js do ...@@ -86,7 +86,22 @@ describe 'Pipelines', :js do
it 'updates content when tab is clicked' do it 'updates content when tab is clicked' do
page.find('.js-pipelines-tab-pending').click page.find('.js-pipelines-tab-pending').click
wait_for_requests wait_for_requests
expect(page).to have_content('No pipelines to show.') expect(page).to have_content('There are currently no pending pipelines.')
end
end
context 'navigation links' do
before do
visit project_pipelines_path(project)
wait_for_requests
end
it 'renders run pipeline link' do
expect(page).to have_link('Run Pipeline')
end
it 'renders ci lint link' do
expect(page).to have_link('CI Lint')
end end
end end
...@@ -542,7 +557,7 @@ describe 'Pipelines', :js do ...@@ -542,7 +557,7 @@ describe 'Pipelines', :js do
end end
it 'has a clear caches button' do it 'has a clear caches button' do
expect(page).to have_link 'Clear runner caches' expect(page).to have_link 'Clear Runner Caches'
end end
describe 'user clicks the button' do describe 'user clicks the button' do
...@@ -552,19 +567,31 @@ describe 'Pipelines', :js do ...@@ -552,19 +567,31 @@ describe 'Pipelines', :js do
end end
it 'increments jobs_cache_index' do it 'increments jobs_cache_index' do
click_link 'Clear runner caches' click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end end
end end
context 'when project does not have jobs_cache_index' do context 'when project does not have jobs_cache_index' do
it 'sets jobs_cache_index to 1' do it 'sets jobs_cache_index to 1' do
click_link 'Clear runner caches' click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end end
end end
end end
end end
describe 'Empty State' do
let(:project) { create(:project, :repository) }
before do
visit project_pipelines_path(project)
end
it 'renders empty state' do
expect(page).to have_content 'Build with confidence'
end
end
end end
context 'when user is not logged in' do context 'when user is not logged in' do
...@@ -575,7 +602,9 @@ describe 'Pipelines', :js do ...@@ -575,7 +602,9 @@ describe 'Pipelines', :js do
context 'when project is public' do context 'when project is public' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
it { expect(page).to have_content 'Build with confidence' } context 'without pipelines' do
it { expect(page).to have_content 'This project is not currently set up to run pipelines.' }
end
end end
context 'when project is private' do context 'when project is private' do
......
import Vue from 'vue';
import component from '~/pipelines/components/blank_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Blank State', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(component);
vm = mountComponent(Component,
{
svgPath: 'foo',
message: 'Blank State',
},
);
});
it('should render svg', () => {
expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
});
it('should render message', () => {
expect(
vm.$el.querySelector('h4').textContent.trim(),
).toEqual('Blank State');
});
});
import Vue from 'vue'; import Vue from 'vue';
import emptyStateComp from '~/pipelines/components/empty_state.vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Empty State', () => { describe('Pipelines Empty State', () => {
let component; let component;
...@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => { ...@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => {
beforeEach(() => { beforeEach(() => {
EmptyStateComponent = Vue.extend(emptyStateComp); EmptyStateComponent = Vue.extend(emptyStateComp);
component = new EmptyStateComponent({ component = mountComponent(EmptyStateComponent, {
propsData: { helpPagePath: 'foo',
helpPagePath: 'foo', emptyStateSvgPath: 'foo',
emptyStateSvgPath: 'foo', canSetCi: true,
}, });
}).$mount(); });
afterEach(() => {
component.$destroy();
}); });
it('should render empty state SVG', () => { it('should render empty state SVG', () => {
...@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => { ...@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect( expect(
component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continous Integration can help catch bugs by running your tests automatically'); ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
expect( expect(
component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continuous Deployment can help you deliver code to your product environment'); ).toContain('while Continuous Deployment can help you deliver code to your product environment');
}); });
it('should render a link with provided help path', () => { it('should render a link with provided help path', () => {
expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo');
expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines');
}); });
}); });
import Vue from 'vue';
import errorStateComp from '~/pipelines/components/error_state.vue';
describe('Pipelines Error State', () => {
let component;
let ErrorStateComponent;
beforeEach(() => {
ErrorStateComponent = Vue.extend(errorStateComp);
component = new ErrorStateComponent({
propsData: {
errorStateSvgPath: 'foo',
},
}).$mount();
});
it('should render error state SVG', () => {
expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
});
it('should render emtpy state information', () => {
expect(
component.$el.querySelector('h4').textContent,
).toContain('The API failed to fetch the pipelines');
});
});
import Vue from 'vue'; import Vue from 'vue';
import navControlsComp from '~/pipelines/components/nav_controls.vue'; import navControlsComp from '~/pipelines/components/nav_controls.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Nav Controls', () => { describe('Pipelines Nav Controls', () => {
let NavControlsComponent; let NavControlsComponent;
let component;
beforeEach(() => { beforeEach(() => {
NavControlsComponent = Vue.extend(navControlsComp); NavControlsComponent = Vue.extend(navControlsComp);
}); });
afterEach(() => {
component.$destroy();
});
it('should render link to create a new pipeline', () => { it('should render link to create a new pipeline', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: true,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath);
}); });
it('should not render link to create pipeline if no permission is provided', () => { it('should not render link to create pipeline if no path is provided', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo', helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: false,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create')).toEqual(null); expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
}); });
it('should render link for resetting runner caches', () => { it('should render link for resetting runner caches', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: false,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches');
expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath);
}); });
it('should render link for CI lint', () => { it('should render link for CI lint', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint');
expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath);
});
it('should render link to help page when CI is not enabled', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: false,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
});
it('should not render link to help page when CI is enabled', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo', helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: true,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info')).toEqual(null); expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath);
}); });
}); });
...@@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; ...@@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines', () => { describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
preloadFixtures('static/pipelines.html.raw');
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
let PipelinesComponent; let PipelinesComponent;
let pipelines; let pipelines;
let component; let vm;
const paths = {
endpoint: 'twitter/flight/pipelines.json',
autoDevopsPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint',
resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
newPipelinePath: '/twitter/flight/pipelines/new',
};
const noPermissions = {
endpoint: 'twitter/flight/pipelines.json',
autoDevopsPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
};
beforeEach(() => { beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
pipelines = getJSONFixture(jsonFixtureName); pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp); PipelinesComponent = Vue.extend(pipelinesComp);
}); });
afterEach(() => { afterEach(() => {
component.$destroy(); vm.$destroy();
});
const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipelines), {
status: 200,
}));
};
const emptyStateInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
}), {
status: 200,
}));
};
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
describe('With permission', () => {
describe('With pipelines in main tab', () => {
beforeEach((done) => {
Vue.http.interceptors.push(pipelinesInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('renders Run Pipeline button', () => {
expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
});
it('renders CI Lint button', () => {
expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
});
it('renders Clear Runner Cache button', () => {
expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
});
it('renders pipelines table', () => {
expect(
vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
});
});
describe('Without pipelines on main tab with CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('renders Run Pipeline button', () => {
expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
});
it('renders CI Lint button', () => {
expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
});
it('renders Clear Runner Cache button', () => {
expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
});
it('renders tab empty state', () => {
expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
});
});
describe('Without pipelines nor CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders empty state', () => {
expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence');
expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath);
});
it('does not render tabs nor buttons', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBe(null);
expect(vm.$el.querySelector('.js-run-pipeline')).toBe(null);
expect(vm.$el.querySelector('.js-ci-lint')).toBe(null);
expect(vm.$el.querySelector('.js-clear-cache')).toBe(null);
});
});
describe('When API returns error', () => {
beforeEach((done) => {
Vue.http.interceptors.push(errorInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('renders buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
});
it('renders error state', () => {
expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error with fetching the pipelines.');
});
});
});
describe('Without permission', () => {
describe('With pipelines in main tab', () => {
beforeEach((done) => {
Vue.http.interceptors.push(pipelinesInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: false,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('does not render buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline')).toBe(null);
expect(vm.$el.querySelector('.js-ci-lint')).toBe(null);
expect(vm.$el.querySelector('.js-clear-cache')).toBe(null);
});
it('renders pipelines table', () => {
expect(
vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
});
});
describe('Without pipelines on main tab with CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: false,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('does not render buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline')).toBe(null);
expect(vm.$el.querySelector('.js-ci-lint')).toBe(null);
expect(vm.$el.querySelector('.js-clear-cache')).toBe(null);
});
it('renders tab empty state', () => {
expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
});
});
describe('Without pipelines nor CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: false,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders empty state without button to set CI', () => {
expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.');
expect(vm.$el.querySelector('.js-get-started-pipelines')).toBe(null);
});
it('does not render tabs nor buttons', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBe(null);
expect(vm.$el.querySelector('.js-run-pipeline')).toBe(null);
expect(vm.$el.querySelector('.js-ci-lint')).toBe(null);
expect(vm.$el.querySelector('.js-clear-cache')).toBe(null);
});
});
describe('When API returns error', () => {
beforeEach((done) => {
Vue.http.interceptors.push(errorInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: true,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('does not renders buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline')).toBe(null);
expect(vm.$el.querySelector('.js-ci-lint')).toBe(null);
expect(vm.$el.querySelector('.js-clear-cache')).toBe(null);
});
it('renders error state', () => {
expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error with fetching the pipelines.');
});
});
}); });
describe('successfull request', () => { describe('successfull request', () => {
describe('with pipelines', () => { describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipelines), {
status: 200,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor); Vue.http.interceptors.push(pipelinesInterceptor);
component = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
}); });
}); });
...@@ -48,9 +392,9 @@ describe('Pipelines', () => { ...@@ -48,9 +392,9 @@ describe('Pipelines', () => {
it('should render table', (done) => { it('should render table', (done) => {
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelector('.table-holder')).toBeDefined(); expect(vm.$el.querySelector('.table-holder')).toBeDefined();
expect( expect(
component.$el.querySelectorAll('.gl-responsive-table-row').length, vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1); ).toEqual(pipelines.pipelines.length + 1);
done(); done();
}); });
...@@ -59,22 +403,22 @@ describe('Pipelines', () => { ...@@ -59,22 +403,22 @@ describe('Pipelines', () => {
it('should render navigation tabs', (done) => { it('should render navigation tabs', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
).toContain('Pending'); ).toContain('Pending');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
).toContain('All'); ).toContain('All');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
).toContain('Running'); ).toContain('Running');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
).toContain('Finished'); ).toContain('Finished');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
).toContain('Branches'); ).toContain('Branches');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
).toContain('Tags'); ).toContain('Tags');
done(); done();
}); });
...@@ -82,10 +426,10 @@ describe('Pipelines', () => { ...@@ -82,10 +426,10 @@ describe('Pipelines', () => {
it('should make an API request when using tabs', (done) => { it('should make an API request when using tabs', (done) => {
setTimeout(() => { setTimeout(() => {
spyOn(component, 'updateContent'); spyOn(vm, 'updateContent');
component.$el.querySelector('.js-pipelines-tab-finished').click(); vm.$el.querySelector('.js-pipelines-tab-finished').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
done(); done();
}); });
}); });
...@@ -93,9 +437,9 @@ describe('Pipelines', () => { ...@@ -93,9 +437,9 @@ describe('Pipelines', () => {
describe('with pagination', () => { describe('with pagination', () => {
it('should make an API request when using pagination', (done) => { it('should make an API request when using pagination', (done) => {
setTimeout(() => { setTimeout(() => {
spyOn(component, 'updateContent'); spyOn(vm, 'updateContent');
// Mock pagination // Mock pagination
component.store.state.pageInfo = { vm.store.state.pageInfo = {
page: 1, page: 1,
total: 10, total: 10,
perPage: 2, perPage: 2,
...@@ -103,9 +447,9 @@ describe('Pipelines', () => { ...@@ -103,9 +447,9 @@ describe('Pipelines', () => {
totalPages: 5, totalPages: 5,
}; };
Vue.nextTick(() => { vm.$nextTick(() => {
component.$el.querySelector('.js-next-button a').click(); vm.$el.querySelector('.js-next-button a').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
done(); done();
}); });
...@@ -113,114 +457,249 @@ describe('Pipelines', () => { ...@@ -113,114 +457,249 @@ describe('Pipelines', () => {
}); });
}); });
}); });
});
describe('without pipelines', () => { describe('methods', () => {
const emptyInterceptor = (request, next) => { beforeEach(() => {
next(request.respondWith(JSON.stringify([]), { spyOn(history, 'pushState').and.stub();
status: 200, });
}));
};
beforeEach(() => { describe('updateContent', () => {
Vue.http.interceptors.push(emptyInterceptor); it('should set given parameters', () => {
}); vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
vm.updateContent({ scope: 'finished', page: '4' });
afterEach(() => { expect(vm.page).toEqual('4');
Vue.http.interceptors = _.without( expect(vm.scope).toEqual('finished');
Vue.http.interceptors, emptyInterceptor, expect(vm.requestData.scope).toEqual('finished');
); expect(vm.requestData.page).toEqual('4');
}); });
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
spyOn(vm, 'updateContent');
it('should render empty state', (done) => { vm.onChangeTab('running');
component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => { expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
expect(component.$el.querySelector('.empty-state')).not.toBe(null); });
done(); });
describe('onChangePage', () => {
it('should update page and keep scope', () => {
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
}); });
spyOn(vm, 'updateContent');
vm.onChangePage(4);
expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' });
}); });
}); });
}); });
describe('unsuccessfull request', () => { describe('computed properties', () => {
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(errorInterceptor); vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
}); });
afterEach(() => { describe('tabs', () => {
Vue.http.interceptors = _.without( it('returns default tabs', () => {
Vue.http.interceptors, errorInterceptor, expect(vm.tabs).toEqual([
); { name: 'All', scope: 'all', count: 0, isActive: true },
{ name: 'Pending', scope: 'pending', count: 0, isActive: false },
{ name: 'Running', scope: 'running', count: 0, isActive: false },
{ name: 'Finished', scope: 'finished', count: 0, isActive: false },
{ name: 'Branches', scope: 'branches', isActive: false },
{ name: 'Tags', scope: 'tags', isActive: false },
]);
});
}); });
it('should render error state', (done) => { describe('emptyTabMessage', () => {
component = new PipelinesComponent({ it('returns message with scope', (done) => {
propsData: { vm.scope = 'pending';
store: new Store(),
},
}).$mount();
setTimeout(() => { vm.$nextTick(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
done(); done();
});
}); });
});
});
describe('methods', () => { it('returns message without scope when scope is `all`', () => {
beforeEach(() => { expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
spyOn(history, 'pushState').and.stub(); });
}); });
describe('updateContent', () => { describe('stateToRender', () => {
it('should set given parameters', () => { it('returns loading state when the app is loading', () => {
component = mountComponent(PipelinesComponent, { expect(vm.stateToRender).toEqual('loading');
store: new Store(), });
it('returns error state when app has error', (done) => {
vm.hasError = true;
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('error');
done();
});
});
it('returns table list when app has pipelines', (done) => {
vm.isLoading = false;
vm.hasError = false;
vm.state.pipelines = pipelines.pipelines;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('tableList');
done();
}); });
component.updateContent({ scope: 'finished', page: '4' }); });
expect(component.page).toEqual('4'); it('returns empty tab when app does not have pipelines but project has pipelines', (done) => {
expect(component.scope).toEqual('finished'); vm.state.count.all = 10;
expect(component.requestData.scope).toEqual('finished'); vm.isLoading = false;
expect(component.requestData.page).toEqual('4');
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyTab');
done();
});
});
it('returns empty tab when project has CI', (done) => {
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyTab');
done();
});
});
it('returns empty state when project does not have pipelines nor CI', (done) => {
vm.isLoading = false;
vm.hasGitlabCi = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyState');
done();
});
}); });
}); });
describe('onChangeTab', () => { describe('shouldRenderTabs', () => {
it('should set page to 1', () => { it('returns true when state is loading & has already made the first request', (done) => {
component = mountComponent(PipelinesComponent, { vm.isLoading = true;
store: new Store(), vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
});
});
it('returns true when state is tableList & has already made the first request', (done) => {
vm.isLoading = false;
vm.state.pipelines = pipelines.pipelines;
vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
});
});
it('returns true when state is error & has already made the first request', (done) => {
vm.isLoading = false;
vm.hasError = true;
vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
}); });
});
spyOn(component, 'updateContent'); it('returs true when state is empty tab & has already made the first request', (done) => {
vm.isLoading = false;
vm.state.count.all = 10;
vm.hasMadeRequest = true;
component.onChangeTab('running'); vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); done();
});
});
it('returns false when has not made first request', (done) => {
vm.hasMadeRequest = false;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(false);
done();
});
});
it('returns false when state is emtpy state', (done) => {
vm.isLoading = false;
vm.hasMadeRequest = true;
vm.hasGitlabCi = false;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(false);
done();
});
}); });
}); });
describe('onChangePage', () => { describe('shouldRenderButtons', () => {
it('should update page and keep scope', () => { it('returns true when it has paths & has made the first request', (done) => {
component = mountComponent(PipelinesComponent, { vm.hasMadeRequest = true;
store: new Store(),
vm.$nextTick(() => {
expect(vm.shouldRenderButtons).toEqual(true);
done();
}); });
});
spyOn(component, 'updateContent'); it('returns false when it has not made the first request', (done) => {
vm.hasMadeRequest = false;
component.onChangePage(4); vm.$nextTick(() => {
expect(vm.shouldRenderButtons).toEqual(false);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); done();
});
}); });
}); });
}); });
......
...@@ -9,7 +9,12 @@ describe('Pipelines Store', () => { ...@@ -9,7 +9,12 @@ describe('Pipelines Store', () => {
it('should be initialized with an empty state', () => { it('should be initialized with an empty state', () => {
expect(store.state.pipelines).toEqual([]); expect(store.state.pipelines).toEqual([]);
expect(store.state.count).toEqual({}); expect(store.state.count).toEqual({
all: 0,
finished: 0,
pending: 0,
running: 0,
});
expect(store.state.pageInfo).toEqual({}); expect(store.state.pageInfo).toEqual({});
}); });
......
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