diff options
18 files changed, 996 insertions, 431 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index ce19069f103..3b3072ebd3e 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -20,10 +20,6 @@ type: String, required: true, }, - emptyStateSvgPath: { - type: String, - required: true, - }, errorStateSvgPath: { type: String, required: true, @@ -45,23 +41,14 @@ }, 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() { return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError; }, + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, }, created() { this.service = new PipelinesService(this.endpoint); @@ -92,25 +79,22 @@ <div class="content-list pipelines"> <loading-icon - label="Loading pipelines" + :label="s__('Pipelines|Loading Pipelines')" size="3" v-if="isLoading" + class="prepend-top-20" /> - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" - :empty-state-svg-path="emptyStateSvgPath" - /> - - <error-state - v-if="shouldRenderErrorState" - :error-state-svg-path="errorStateSvgPath" + <svg-blank-state + v-else-if="shouldRenderErrorState" + :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 class="table-holder" - v-if="shouldRenderTable" + v-else-if="shouldRenderTable" > <pipelines-table-component :pipelines="state.pipelines" diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index 25dfa99ad9c..4c092c60e72 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import Translate from '../../../../vue_shared/translate'; +import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils'; Vue.use(Translate); @@ -15,12 +16,25 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ return { store, + dataset: document.querySelector(this.$options.el).dataset, }; }, + render(createElement) { return createElement('pipelines-component', { props: { 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, }, }); }, diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue new file mode 100644 index 00000000000..8d3d6223d7b --- /dev/null +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -0,0 +1,32 @@ +<script> + export default { + name: 'PipelinesSvgState', + props: { + svgPath: { + type: String, + required: true, + }, + + message: { + type: String, + required: true, + }, + }, + }; +</script> + +<template> + <div class="row empty-state"> + <div class="col-xs-12"> + <div class="svg-content"> + <img :src="svgPath" /> + </div> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>{{ message }}</h4> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index dfaa2574091..51f05c7827e 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -1,5 +1,6 @@ <script> export default { + name: 'PipelinesEmptyState', props: { helpPagePath: { type: String, @@ -9,6 +10,10 @@ type: String, required: true, }, + canSetCi: { + type: Boolean, + required: true, + }, }, }; </script> @@ -22,22 +27,36 @@ <div class="col-xs-12"> <div class="text-content"> - <h4 class="text-center"> - {{ s__("Pipelines|Build with confidence") }} - </h4> - <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.`) }} + + <template v-if="canSetCi"> + <h4 class="text-center"> + {{ s__('Pipelines|Build with confidence') }} + </h4> + + <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> - <div class="text-center"> - <a - :href="helpPagePath" - class="btn btn-info" - > - {{ s__("Pipelines|Get started with Pipelines") }} - </a> - </div> + </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue deleted file mode 100644 index 012853b201d..00000000000 --- a/app/assets/javascripts/pipelines/components/error_state.vue +++ /dev/null @@ -1,26 +0,0 @@ -<script> -export default { - props: { - errorStateSvgPath: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div class="row empty-state js-pipelines-error-state"> - <div class="col-xs-12"> - <div class="svg-content"> - <img :src="errorStateSvgPath"/> - </div> - </div> - - <div class="col-xs-12 text-center"> - <div class="text-content"> - <h4>The API failed to fetch the pipelines.</h4> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index f31a91c3403..383ab51fe56 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -1,67 +1,52 @@ <script> -export default { - name: 'PipelineNavControls', - props: { - newPipelinePath: { - type: String, - required: true, + export default { + name: 'PipelineNavControls', + props: { + newPipelinePath: { + type: String, + 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> <template> <div class="nav-controls"> <a - v-if="canCreatePipeline" + v-if="newPipelinePath" :href="newPipelinePath" - class="btn btn-create"> - Run Pipeline - </a> - - <a - v-if="!hasCiEnabled" - :href="helpPagePath" - class="btn btn-info"> - Get started with Pipelines + class="btn btn-create js-run-pipeline" + > + {{ s__('Pipelines|Run Pipeline') }} </a> <a + v-if="resetCachePath" data-method="post" - rel="nofollow" :href="resetCachePath" - class="btn btn-default"> - Clear runner caches + class="btn btn-default js-clear-cache" + > + {{ s__('Pipelines|Clear Runner Caches') }} </a> <a + v-if="ciLintPath" :href="ciLintPath" - class="btn btn-default"> - CI Lint + class="btn btn-default js-ci-lint" + > + {{ s__('Pipelines|CI Lint') }} </a> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 90930d5ff44..d983d8c0af8 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,12 +1,12 @@ <script> import _ from 'underscore'; + import { __, sprintf, s__ } from '../../locale'; import PipelinesService from '../services/pipelines_service'; import pipelinesMixin from '../mixins/pipelines'; - import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import navigationTabs from '../../vue_shared/components/navigation_tabs.vue'; - import navigationControls from './nav_controls.vue'; + import TablePagination from '../../vue_shared/components/table_pagination.vue'; + import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; + import NavigationControls from './nav_controls.vue'; import { - convertPermissionToBoolean, getParameterByName, parseQueryStringIntoObject, } from '../../lib/utils/common_utils'; @@ -14,9 +14,9 @@ export default { components: { - tablePagination, - navigationTabs, - navigationControls, + TablePagination, + NavigationTabs, + NavigationControls, }, mixins: [ pipelinesMixin, @@ -36,111 +36,186 @@ required: false, 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() { - const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; - return { - endpoint: pipelinesData.endpoint, - helpPagePath: pipelinesData.helpPagePath, - emptyStateSvgPath: pipelinesData.emptyStateSvgPath, - errorStateSvgPath: pipelinesData.errorStateSvgPath, - autoDevopsPath: pipelinesData.helpAutoDevopsPath, - newPipelinePath: pipelinesData.newPipelinePath, - canCreatePipeline: pipelinesData.canCreatePipeline, - hasCi: pipelinesData.hasCi, - ciLintPath: pipelinesData.ciLintPath, - resetCachePath: pipelinesData.resetCachePath, + // Start with loading state to avoid a glitch when the empty state will be rendered + isLoading: true, state: this.store.state, scope: getParameterByName('scope') || 'all', page: getParameterByName('page') || '1', requestData: {}, }; }, - computed: { - canCreatePipelineParsed() { - return convertPermissionToBoolean(this.canCreatePipeline); - }, + stateMap: { + // with tabs + 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 - * and none is returned. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.isLoading && - !this.hasError && - this.hasMadeRequest && - !this.state.pipelines.length && - (this.scope === 'all' || this.scope === null); + * `hasGitlabCi` handles both internal and external CI. + * The order on which the checks are made in this method is + * important to guarantee we handle all the corner cases. + */ + stateToRender() { + const { stateMap } = this.$options; + + if (this.isLoading) { + return stateMap.loading; + } + + 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. - * - * @return {Boolean} + * Tabs are rendered in all states except empty state. + * They are not rendered before the first request to avoid a flicker on first load. */ - shouldRenderNoPipelinesMessage() { - return !this.isLoading && - !this.hasError && - !this.state.pipelines.length && - this.scope !== 'all' && - this.scope !== null; + shouldRenderTabs() { + const { stateMap } = this.$options; + return this.hasMadeRequest && + [ + stateMap.loading, + stateMap.tableList, + stateMap.error, + stateMap.emptyTab, + ].includes(this.stateToRender); }, - shouldRenderTable() { - return !this.hasError && - !this.isLoading && this.state.pipelines.length; + shouldRenderButtons() { + return (this.newPipelinePath || + this.resetCachePath || + this.ciLintPath) && this.shouldRenderTabs; }, - /** - * Pagination should only be rendered when there is more than one page. - * - * @return {Boolean} - */ + shouldRenderPagination() { return !this.isLoading && this.state.pipelines.length && 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() { const { count } = this.state; + const { scopes } = this.$options; + return [ { - name: 'All', - scope: 'all', + name: __('All'), + scope: scopes.all, count: count.all, isActive: this.scope === 'all', }, { - name: 'Pending', - scope: 'pending', + name: __('Pending'), + scope: scopes.pending, count: count.pending, isActive: this.scope === 'pending', }, { - name: 'Running', - scope: 'running', + name: __('Running'), + scope: scopes.running, count: count.running, isActive: this.scope === 'running', }, { - name: 'Finished', - scope: 'finished', + name: __('Finished'), + scope: scopes.finished, count: count.finished, isActive: this.scope === 'finished', }, { - name: 'Branches', - scope: 'branches', + name: __('Branches'), + scope: scopes.branches, isActive: this.scope === 'branches', }, { - name: 'Tags', - scope: 'tags', + name: __('Tags'), + scope: scopes.tags, isActive: this.scope === 'tags', }, ]; @@ -187,7 +262,7 @@ this.errorCallback(); // restart polling - this.poll.restart(); + this.poll.restart({ data: this.requestData }); }); }, }, @@ -197,69 +272,70 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!shouldRenderEmptyState" + v-if="shouldRenderTabs || shouldRenderButtons" > <div class="fade-left"> <i class="fa fa-angle-left" - aria-hidden="true"> + aria-hidden="true" + > </i> </div> <div class="fade-right"> <i class="fa fa-angle-right" - aria-hidden="true"> + aria-hidden="true" + > </i> </div> <navigation-tabs + v-if="shouldRenderTabs" :tabs="tabs" @onChangeTab="onChangeTab" scope="pipelines" /> <navigation-controls + v-if="shouldRenderButtons" :new-pipeline-path="newPipelinePath" - :has-ci-enabled="hasCiEnabled" - :help-page-path="helpPagePath" :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" - :can-create-pipeline="canCreatePipelineParsed " /> </div> <div class="content-list pipelines"> <loading-icon - label="Loading Pipelines" + v-if="stateToRender === $options.stateMap.loading" + :label="s__('Pipelines|Loading Pipelines')" size="3" - v-if="isLoading" class="prepend-top-20" /> <empty-state - v-if="shouldRenderEmptyState" + v-else-if="stateToRender === $options.stateMap.emptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" + :can-set-ci="canCreatePipeline" /> - <error-state - v-if="shouldRenderErrorState" - :error-state-svg-path="errorStateSvgPath" + <svg-blank-state + v-else-if="stateToRender === $options.stateMap.error" + :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 - class="blank-state-row" - v-if="shouldRenderNoPipelinesMessage" - > - <div class="blank-state-center"> - <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> - </div> - </div> + <svg-blank-state + v-else-if="stateToRender === $options.stateMap.emptyTab" + :svg-path="noPipelinesSvgPath" + :message="emptyTabMessage" + /> <div class="table-holder" - v-if="shouldRenderTable" + v-else-if="stateToRender === $options.stateMap.tableList" > <pipelines-table-component diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 50bdf80c3e3..9fcc07abee5 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -1,23 +1,19 @@ import Visibility from 'visibilityjs'; +import { __ } from '../../locale'; import Flash from '../../flash'; import Poll from '../../lib/utils/poll'; -import emptyState from '../components/empty_state.vue'; -import errorState from '../components/error_state.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import pipelinesTableComponent from '../components/pipelines_table.vue'; +import EmptyState from '../components/empty_state.vue'; +import SvgBlankState from '../components/blank_state.vue'; +import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; +import PipelinesTableComponent from '../components/pipelines_table.vue'; import eventHub from '../event_hub'; export default { components: { - pipelinesTableComponent, - errorState, - emptyState, - loadingIcon, - }, - computed: { - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, + PipelinesTableComponent, + SvgBlankState, + EmptyState, + LoadingIcon, }, data() { return { @@ -85,6 +81,7 @@ export default { this.hasError = true; this.isLoading = false; this.updateGraphDropdown = false; + this.hasMadeRequest = true; }, setIsMakingRequest(isMakingRequest) { this.isMakingRequest = isMakingRequest; @@ -96,7 +93,7 @@ export default { postAction(endpoint) { this.service.postAction(endpoint) .then(() => eventHub.$emit('refreshPipelines')) - .catch(() => new Flash('An error occurred while making the request.')); + .catch(() => Flash(__('An error occurred while making the request.'))); }, }, }; diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index 651251d2623..2f738edd241 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -5,7 +5,12 @@ export default class PipelinesStore { this.state = {}; this.state.pipelines = []; - this.state.count = {}; + this.state.count = { + all: 0, + finished: 0, + pending: 0, + running: 0, + }; this.state.pageInfo = {}; } diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index cf95cdbfec2..3e6b3346787 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -7,8 +7,9 @@ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.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, - "has-ci" => @repository.gitlab_ci_yml, - "ci-lint-path" => ci_lint_path, - "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } + "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project), + "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path, + "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 } } diff --git a/changelogs/unreleased/38587-pipelines-empty-state.yml b/changelogs/unreleased/38587-pipelines-empty-state.yml new file mode 100644 index 00000000000..58ea204d394 --- /dev/null +++ b/changelogs/unreleased/38587-pipelines-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Handle empty state in Pipelines page +merge_request: +author: +type: fixed diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 3a8e7c05cc4..849d85061df 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -86,7 +86,22 @@ describe 'Pipelines', :js do it 'updates content when tab is clicked' do page.find('.js-pipelines-tab-pending').click wait_for_requests - expect(page).to have_content('No pipelines to show.') + 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 @@ -542,7 +557,7 @@ describe 'Pipelines', :js do end it 'has a clear caches button' do - expect(page).to have_link 'Clear runner caches' + expect(page).to have_link 'Clear Runner Caches' end describe 'user clicks the button' do @@ -552,19 +567,31 @@ describe 'Pipelines', :js do end 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.' end end context 'when project does not have jobs_cache_index' 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.' 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 context 'when user is not logged in' do @@ -575,7 +602,9 @@ describe 'Pipelines', :js do context 'when project is public' do 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 context 'when project is private' do diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/javascripts/pipelines/blank_state_spec.js new file mode 100644 index 00000000000..b7a9b60d85c --- /dev/null +++ b/spec/javascripts/pipelines/blank_state_spec.js @@ -0,0 +1,29 @@ +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'); + }); +}); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 97f04844b3a..71f77e5f42e 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Empty State', () => { let component; @@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => { beforeEach(() => { EmptyStateComponent = Vue.extend(emptyStateComp); - component = new EmptyStateComponent({ - propsData: { - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - }, - }).$mount(); + component = mountComponent(EmptyStateComponent, { + helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + canSetCi: true, + }); + }); + + afterEach(() => { + component.$destroy(); }); it('should render empty state SVG', () => { @@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), - ).toContain('Continous Integration can help catch bugs by running your tests automatically'); + 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,'); expect( - component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), - ).toContain('Continuous Deployment can help you deliver code to your product environment'); + component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), + ).toContain('while Continuous Deployment can help you deliver code to your product environment'); }); it('should render a link with provided help path', () => { - expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); - expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines'); }); }); diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js deleted file mode 100644 index a402857a4d1..00000000000 --- a/spec/javascripts/pipelines/error_state_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -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'); - }); -}); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index 09a0c14d96c..77c5258f74c 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -1,116 +1,68 @@ import Vue from 'vue'; import navControlsComp from '~/pipelines/components/nav_controls.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Nav Controls', () => { let NavControlsComponent; + let component; beforeEach(() => { NavControlsComponent = Vue.extend(navControlsComp); }); + afterEach(() => { + component.$destroy(); + }); + it('should render link to create a new pipeline', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: true, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); - expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); + expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline'); + 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 = { - newPipelinePath: 'foo', - hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: false, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - 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', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: false, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); - expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); + expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches'); + expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath); }); it('should render link for CI lint', () => { const mockData = { 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', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: true, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - 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); }); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 54d5bfd51e6..043c8cfd8bc 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; - preloadFixtures('static/pipelines.html.raw'); preloadFixtures(jsonFixtureName); let PipelinesComponent; 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(() => { - loadFixtures('static/pipelines.html.raw'); pipelines = getJSONFixture(jsonFixtureName); PipelinesComponent = Vue.extend(pipelinesComp); }); 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('with pipelines', () => { - const pipelinesInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(pipelines), { - status: 200, - })); - }; - beforeEach(() => { Vue.http.interceptors.push(pipelinesInterceptor); - component = mountComponent(PipelinesComponent, { + vm = mountComponent(PipelinesComponent, { store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, }); }); @@ -48,9 +392,9 @@ describe('Pipelines', () => { it('should render table', (done) => { setTimeout(() => { - expect(component.$el.querySelector('.table-holder')).toBeDefined(); + expect(vm.$el.querySelector('.table-holder')).toBeDefined(); expect( - component.$el.querySelectorAll('.gl-responsive-table-row').length, + vm.$el.querySelectorAll('.gl-responsive-table-row').length, ).toEqual(pipelines.pipelines.length + 1); done(); }); @@ -59,22 +403,22 @@ describe('Pipelines', () => { it('should render navigation tabs', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), ).toContain('Pending'); expect( - component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), ).toContain('All'); expect( - component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), ).toContain('Running'); expect( - component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), ).toContain('Finished'); expect( - component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), ).toContain('Branches'); expect( - component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), ).toContain('Tags'); done(); }); @@ -82,10 +426,10 @@ describe('Pipelines', () => { it('should make an API request when using tabs', (done) => { setTimeout(() => { - spyOn(component, 'updateContent'); - component.$el.querySelector('.js-pipelines-tab-finished').click(); + spyOn(vm, 'updateContent'); + 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(); }); }); @@ -93,9 +437,9 @@ describe('Pipelines', () => { describe('with pagination', () => { it('should make an API request when using pagination', (done) => { setTimeout(() => { - spyOn(component, 'updateContent'); + spyOn(vm, 'updateContent'); // Mock pagination - component.store.state.pageInfo = { + vm.store.state.pageInfo = { page: 1, total: 10, perPage: 2, @@ -103,9 +447,9 @@ describe('Pipelines', () => { totalPages: 5, }; - Vue.nextTick(() => { - component.$el.querySelector('.js-next-button a').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + vm.$nextTick(() => { + vm.$el.querySelector('.js-next-button a').click(); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); done(); }); @@ -113,112 +457,249 @@ describe('Pipelines', () => { }); }); }); + }); - describe('without pipelines', () => { - const emptyInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; + describe('methods', () => { + beforeEach(() => { + spyOn(history, 'pushState').and.stub(); + }); - beforeEach(() => { - Vue.http.interceptors.push(emptyInterceptor); - }); + describe('updateContent', () => { + it('should set given parameters', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + vm.updateContent({ scope: 'finished', page: '4' }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, emptyInterceptor, - ); + expect(vm.page).toEqual('4'); + expect(vm.scope).toEqual('finished'); + 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) => { - component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); + vm.onChangeTab('running'); - setTimeout(() => { - expect(component.$el.querySelector('.empty-state')).not.toBe(null); - done(); + 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('unsuccessfull request', () => { - const errorInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 500, - })); - }; - + describe('computed properties', () => { beforeEach(() => { - Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, errorInterceptor, - ); + describe('tabs', () => { + it('returns default tabs', () => { + 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) => { - component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); + describe('emptyTabMessage', () => { + it('returns message with scope', (done) => { + vm.scope = 'pending'; - setTimeout(() => { - expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); - done(); + vm.$nextTick(() => { + expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + done(); + }); }); - }); - }); - describe('methods', () => { - beforeEach(() => { - spyOn(history, 'pushState').and.stub(); + it('returns message without scope when scope is `all`', () => { + expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + }); }); - describe('updateContent', () => { - it('should set given parameters', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('stateToRender', () => { + it('returns loading state when the app is loading', () => { + expect(vm.stateToRender).toEqual('loading'); + }); + + 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(); + }); + }); + + it('returns empty tab when app does not have pipelines but project has pipelines', (done) => { + vm.state.count.all = 10; + vm.isLoading = false; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyTab'); + + done(); }); - component.updateContent({ scope: 'finished', page: '4' }); + }); + + it('returns empty tab when project has CI', (done) => { + vm.isLoading = false; + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyTab'); - expect(component.page).toEqual('4'); - expect(component.scope).toEqual('finished'); - expect(component.requestData.scope).toEqual('finished'); - expect(component.requestData.page).toEqual('4'); + 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', () => { - it('should set page to 1', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('shouldRenderTabs', () => { + it('returns true when state is loading & has already made the first request', (done) => { + vm.isLoading = true; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); }); - spyOn(component, 'updateContent'); + }); - component.onChangeTab('running'); + 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.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + 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(); + }); + }); + + 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; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + 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', () => { - it('should update page and keep scope', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('shouldRenderButtons', () => { + it('returns true when it has paths & has made the first request', (done) => { + vm.hasMadeRequest = true; + + 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(); + }); }); }); }); diff --git a/spec/javascripts/pipelines/pipelines_store_spec.js b/spec/javascripts/pipelines/pipelines_store_spec.js index 10ff0c6bb84..0d628285a7b 100644 --- a/spec/javascripts/pipelines/pipelines_store_spec.js +++ b/spec/javascripts/pipelines/pipelines_store_spec.js @@ -9,7 +9,12 @@ describe('Pipelines Store', () => { it('should be initialized with an empty state', () => { 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({}); }); |