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 @@
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 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"
......
......@@ -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);
......@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
pipelinesComponent,
},
data() {
const store = new PipelinesStore();
return {
store,
store: new PipelinesStore(),
};
},
created() {
this.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,
},
});
},
......
<script>
export default {
export default {
name: 'PipelinesSvgState',
props: {
errorStateSvgPath: {
svgPath: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
},
};
};
</script>
<template>
<div class="row empty-state js-pipelines-error-state">
<div class="row empty-state">
<div class="col-xs-12">
<div class="svg-content">
<img :src="errorStateSvgPath"/>
<img :src="svgPath" />
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
<h4>{{ message }}</h4>
</div>
</div>
</div>
......
<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">
<template v-if="canSetCi">
<h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }}
{{ 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.`) }}
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"
class="btn btn-primary js-get-started-pipelines"
>
{{ s__("Pipelines|Get started with 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>
</div>
</div>
......
<script>
export default {
export default {
name: 'PipelineNavControls',
props: {
newPipelinePath: {
type: String,
required: true,
},
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
required: false,
default: null,
},
resetCachePath: {
type: String,
required: true,
required: false,
default: null,
},
ciLintPath: {
type: String,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
required: false,
default: null,
},
},
};
};
</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>
<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}
* `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.
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
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.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.
*
* @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 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
......
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.')));
},
},
};
......@@ -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 } }
---
title: Handle empty state in Pipelines page
merge_request:
author:
type: fixed
......@@ -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
......
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 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: {
component = mountComponent(EmptyStateComponent, {
helpPagePath: 'foo',
emptyStateSvgPath: 'foo',
},
}).$mount();
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');
});
});
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 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);
});
});
......@@ -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();
});
describe('successfull request', () => {
describe('with pipelines', () => {
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')).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(() => {
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(() => {
Vue.http.interceptors.push(emptyInterceptor);
spyOn(history, 'pushState').and.stub();
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, 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' });
it('should render empty state', (done) => {
component = new PipelinesComponent({
propsData: {
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(),
},
}).$mount();
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
spyOn(vm, 'updateContent');
setTimeout(() => {
expect(component.$el.querySelector('.empty-state')).not.toBe(null);
vm.onChangeTab('running');
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();
});
});
it('returns message without scope when scope is `all`', () => {
expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
});
});
describe('unsuccessfull request', () => {
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
describe('stateToRender', () => {
it('returns loading state when the app is loading', () => {
expect(vm.stateToRender).toEqual('loading');
});
beforeEach(() => {
Vue.http.interceptors.push(errorInterceptor);
it('returns error state when app has error', (done) => {
vm.hasError = true;
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('error');
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
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('should render error state', (done) => {
component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
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');
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
done();
});
});
it('returns empty tab when project has CI', (done) => {
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyTab');
done();
});
});
describe('methods', () => {
beforeEach(() => {
spyOn(history, 'pushState').and.stub();
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('updateContent', () => {
it('should set given parameters', () => {
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();
});
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');
expect(component.scope).toEqual('finished');
expect(component.requestData.scope).toEqual('finished');
expect(component.requestData.page).toEqual('4');
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
});
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
it('returns true when state is error & has already made the first request', (done) => {
vm.isLoading = false;
vm.hasError = true;
vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
});
spyOn(component, 'updateContent');
});
it('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('should update page and keep scope', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
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();
});
});
});
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