Commit 60d95d8f authored by Phil Hughes's avatar Phil Hughes

Merge branch '38587-pipelines-empty-state' into 'master'

Resolve "Pipelines view should handle empty state and buttons properly"

Closes #38587

See merge request gitlab-org/gitlab-ce!17433
parents a2804512 e3251a40
...@@ -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 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);
...@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
pipelinesComponent, pipelinesComponent,
}, },
data() { data() {
const store = new PipelinesStore();
return { return {
store, store: new PipelinesStore(),
}; };
}, },
created() {
this.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 {
name: 'PipelinesSvgState',
props: { props: {
errorStateSvgPath: { svgPath: {
type: String,
required: true,
},
message: {
type: String, type: String,
required: true, 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">
<template v-if="canSetCi">
<h4 class="text-center"> <h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }} {{ s__('Pipelines|Build with confidence') }}
</h4> </h4>
<p> <p>
{{ s__(`Pipelines|Continous Integration can help {{ s__(`Pipelines|Continous Integration can help
catch bugs by running your tests automatically, catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.`) }} while Continuous Deployment can help you deliver
code to your product environment.`) }}
</p> </p>
<div class="text-center"> <div class="text-center">
<a <a
:href="helpPagePath" :href="helpPagePath"
class="btn btn-info" class="btn btn-primary js-get-started-pipelines"
> >
{{ s__("Pipelines|Get started with Pipelines") }} {{ s__('Pipelines|Get started with Pipelines') }}
</a> </a>
</div> </div>
</template>
<p
v-else
class="text-center"
>
{{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
</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,
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
}, },
resetCachePath: { resetCachePath: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
ciLintPath: { ciLintPath: {
type: String, type: String,
required: true, required: false,
}, default: null,
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}
*/ */
shouldRenderEmptyState() { stateToRender() {
return !this.isLoading && const { stateMap } = this.$options;
!this.hasError &&
this.hasMadeRequest && if (this.isLoading) {
!this.state.pipelines.length && return stateMap.loading;
(this.scope === 'all' || this.scope === null); }
if (this.hasError) {
return stateMap.error;
}
if (this.state.pipelines.length) {
return stateMap.tableList;
}
if ((this.scope !== 'all' && this.scope !== null) || 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 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.')));
}, },
}, },
}; };
...@@ -7,8 +7,9 @@ ...@@ -7,8 +7,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();
}); });
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => { const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipelines), { next(request.respondWith(JSON.stringify(pipelines), {
status: 200, 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')).toBeNull();
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
});
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 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')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
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')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
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')).toBeNull();
});
it('does not render tabs or buttons', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
});
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')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
it('renders error state', () => {
expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
});
});
});
describe('successfull request', () => {
describe('with pipelines', () => {
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,112 +457,249 @@ describe('Pipelines', () => { ...@@ -113,112 +457,249 @@ describe('Pipelines', () => {
}); });
}); });
}); });
});
describe('without pipelines', () => { describe('methods', () => {
const emptyInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(emptyInterceptor); spyOn(history, 'pushState').and.stub();
}); });
afterEach(() => { describe('updateContent', () => {
Vue.http.interceptors = _.without( it('should set given parameters', () => {
Vue.http.interceptors, emptyInterceptor, vm = mountComponent(PipelinesComponent, {
); store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
}); });
vm.updateContent({ scope: 'finished', page: '4' });
it('should render empty state', (done) => { expect(vm.page).toEqual('4');
component = new PipelinesComponent({ expect(vm.scope).toEqual('finished');
propsData: { 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(), store: new Store(),
}, hasGitlabCi: true,
}).$mount(); canCreatePipeline: true,
...paths,
});
spyOn(vm, 'updateContent');
setTimeout(() => { vm.onChangeTab('running');
expect(component.$el.querySelector('.empty-state')).not.toBe(null);
expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
});
});
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('computed properties', () => {
beforeEach(() => {
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
});
describe('tabs', () => {
it('returns default tabs', () => {
expect(vm.tabs).toEqual([
{ name: 'All', scope: 'all', count: undefined, isActive: true },
{ name: 'Pending', scope: 'pending', count: undefined, isActive: false },
{ name: 'Running', scope: 'running', count: undefined, isActive: false },
{ name: 'Finished', scope: 'finished', count: undefined, isActive: false },
{ name: 'Branches', scope: 'branches', isActive: false },
{ name: 'Tags', scope: 'tags', isActive: false },
]);
});
});
describe('emptyTabMessage', () => {
it('returns message with scope', (done) => {
vm.scope = 'pending';
vm.$nextTick(() => {
expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
done(); done();
}); });
}); });
it('returns message without scope when scope is `all`', () => {
expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
}); });
}); });
describe('unsuccessfull request', () => { describe('stateToRender', () => {
const errorInterceptor = (request, next) => { it('returns loading state when the app is loading', () => {
next(request.respondWith(JSON.stringify([]), { expect(vm.stateToRender).toEqual('loading');
status: 500, });
}));
};
beforeEach(() => { it('returns error state when app has error', (done) => {
Vue.http.interceptors.push(errorInterceptor); vm.hasError = true;
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('error');
done();
});
}); });
afterEach(() => { it('returns table list when app has pipelines', (done) => {
Vue.http.interceptors = _.without( vm.isLoading = false;
Vue.http.interceptors, errorInterceptor, vm.hasError = false;
); vm.state.pipelines = pipelines.pipelines;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('tableList');
done();
});
}); });
it('should render error state', (done) => { it('returns empty tab when app does not have pipelines but project has pipelines', (done) => {
component = new PipelinesComponent({ vm.state.count.all = 10;
propsData: { vm.isLoading = false;
store: new Store(),
}, vm.$nextTick(() => {
}).$mount(); expect(vm.stateToRender).toEqual('emptyTab');
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
done(); done();
}); });
}); });
it('returns empty tab when project has CI', (done) => {
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyTab');
done();
});
}); });
describe('methods', () => { it('returns empty state when project does not have pipelines nor CI', (done) => {
beforeEach(() => { vm.isLoading = false;
spyOn(history, 'pushState').and.stub(); vm.hasGitlabCi = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyState');
done();
});
});
}); });
describe('updateContent', () => { describe('shouldRenderTabs', () => {
it('should set given parameters', () => { 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();
}); });
component.updateContent({ scope: 'finished', page: '4' }); });
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;
expect(component.page).toEqual('4'); vm.$nextTick(() => {
expect(component.scope).toEqual('finished'); expect(vm.shouldRenderTabs).toEqual(true);
expect(component.requestData.scope).toEqual('finished');
expect(component.requestData.page).toEqual('4'); done();
}); });
}); });
describe('onChangeTab', () => { it('returns true when state is error & has already made the first request', (done) => {
it('should set page to 1', () => { vm.isLoading = false;
component = mountComponent(PipelinesComponent, { vm.hasError = true;
store: new Store(), vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
}); });
spyOn(component, 'updateContent'); });
it('returns 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();
}); });
}); });
describe('onChangePage', () => { it('returns false when has not made first request', (done) => {
it('should update page and keep scope', () => { vm.hasMadeRequest = false;
component = mountComponent(PipelinesComponent, {
store: new Store(), 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();
});
});
}); });
spyOn(component, 'updateContent');
component.onChangePage(4); describe('shouldRenderButtons', () => {
it('returns true when it has paths & has made the first request', (done) => {
vm.hasMadeRequest = true;
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); vm.$nextTick(() => {
expect(vm.shouldRenderButtons).toEqual(true);
done();
});
});
it('returns false when it has not made the first request', (done) => {
vm.hasMadeRequest = false;
vm.$nextTick(() => {
expect(vm.shouldRenderButtons).toEqual(false);
done();
});
}); });
}); });
}); });
......
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