Commit 64857a9b authored by Filipa Lacerda's avatar Filipa Lacerda

Manage empty states in Pipelines page

Adds i18n
Adds test

Fix broken tests

Fixes empty tab state for external CI
parent 67feb7cd
...@@ -20,10 +20,6 @@ ...@@ -20,10 +20,6 @@
type: String, type: String,
required: true, required: true,
}, },
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: { errorStateSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -45,23 +41,14 @@ ...@@ -45,23 +41,14 @@
}, },
computed: { computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() { shouldRenderTable() {
return !this.isLoading && return !this.isLoading &&
this.state.pipelines.length > 0 && this.state.pipelines.length > 0 &&
!this.hasError; !this.hasError;
}, },
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
...@@ -92,25 +79,22 @@ ...@@ -92,25 +79,22 @@
<div class="content-list pipelines"> <div class="content-list pipelines">
<loading-icon <loading-icon
label="Loading pipelines" :label="s__('Pipelines|Loading Pipelines')"
size="3" size="3"
v-if="isLoading" v-if="isLoading"
class="prepend-top-20"
/> />
<empty-state <svg-blank-state
v-if="shouldRenderEmptyState" v-else-if="shouldRenderErrorState"
:help-page-path="helpPagePath" :svg-path="errorStateSvgPath"
:empty-state-svg-path="emptyStateSvgPath" :message="s__(`Pipelines|There was an error with fetching the pipelines.
/> Try again in a few moments or contact your support team.`)"
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/> />
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable" v-else-if="shouldRenderTable"
> >
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate'; import Translate from '../../../../vue_shared/translate';
import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate); Vue.use(Translate);
...@@ -15,12 +16,25 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -15,12 +16,25 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return { return {
store, store,
dataset: document.querySelector(this.$options.el).dataset,
}; };
}, },
render(createElement) { render(createElement) {
return createElement('pipelines-component', { return createElement('pipelines-component', {
props: { props: {
store: this.store, store: this.store,
endpoint: this.dataset.endpoint,
helpPagePath: this.dataset.helpPagePath,
emptyStateSvgPath: this.dataset.emptyStateSvgPath,
errorStateSvgPath: this.dataset.errorStateSvgPath,
noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
autoDevopsPath: this.dataset.helpAutoDevopsPath,
newPipelinePath: this.dataset.newPipelinePath,
canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath,
}, },
}); });
}, },
......
<script> <script>
export default { export default {
name: 'PipelinesSvgState',
props: { props: {
errorStateSvgPath: { svgPath: {
type: String,
required: true,
},
message: {
type: String, type: String,
required: true, required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="row empty-state js-pipelines-error-state"> <div class="row empty-state">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="svg-content"> <div class="svg-content">
<img :src="errorStateSvgPath"/> <img :src="svgPath" />
</div> </div>
</div> </div>
<div class="col-xs-12 text-center"> <div class="col-xs-12 text-center">
<div class="text-content"> <div class="text-content">
<h4>The API failed to fetch the pipelines.</h4> <h4>{{ message }}</h4>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
export default { export default {
name: 'PipelinesEmptyState',
props: { props: {
helpPagePath: { helpPagePath: {
type: String, type: String,
...@@ -9,6 +10,10 @@ ...@@ -9,6 +10,10 @@
type: String, type: String,
required: true, required: true,
}, },
canSetCi: {
type: Boolean,
required: true,
},
}, },
}; };
</script> </script>
...@@ -22,22 +27,36 @@ ...@@ -22,22 +27,36 @@
<div class="col-xs-12"> <div class="col-xs-12">
<div class="text-content"> <div class="text-content">
<template v-if="canSetCi">
<h4 class="text-center"> <h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }} {{ s__('Pipelines|Build with confidence') }}
</h4> </h4>
<p> <p>
{{ s__(`Pipelines|Continous Integration can help {{ s__(`Pipelines|Continous Integration can help
catch bugs by running your tests automatically, catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.`) }} while Continuous Deployment can help you deliver
code to your product environment.`) }}
</p> </p>
<div class="text-center"> <div class="text-center">
<a <a
:href="helpPagePath" :href="helpPagePath"
class="btn btn-info" class="btn btn-info js-get-started-pipelines"
> >
{{ s__("Pipelines|Get started with Pipelines") }} {{ s__('Pipelines|Get started with Pipelines') }}
</a> </a>
</div> </div>
</template>
<p
v-else
class="text-center"
>
{{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
export default { export default {
name: 'PipelineNavControls', name: 'PipelineNavControls',
props: { props: {
newPipelinePath: { newPipelinePath: {
type: String, type: String,
required: true, required: false,
}, default: null,
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
}, },
resetCachePath: { resetCachePath: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
ciLintPath: { ciLintPath: {
type: String, type: String,
required: true, required: false,
}, default: null,
canCreatePipeline: {
type: Boolean,
required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="nav-controls"> <div class="nav-controls">
<a <a
v-if="canCreatePipeline" v-if="newPipelinePath"
:href="newPipelinePath" :href="newPipelinePath"
class="btn btn-create"> class="btn btn-create js-run-pipeline"
Run Pipeline >
</a> {{ s__('Pipelines|Run Pipeline') }}
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a> </a>
<a <a
v-if="resetCachePath"
data-method="post" data-method="post"
rel="nofollow"
:href="resetCachePath" :href="resetCachePath"
class="btn btn-default"> class="btn btn-default js-clear-cache"
Clear runner caches >
{{ s__('Pipelines|Clear Runner Caches') }}
</a> </a>
<a <a
v-if="ciLintPath"
:href="ciLintPath" :href="ciLintPath"
class="btn btn-default"> class="btn btn-default js-ci-lint"
CI Lint >
{{ s__('Pipelines|CI Lint') }}
</a> </a>
</div> </div>
</template> </template>
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue'; import EmptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue'; import SvgBlankState from '../components/blank_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import pipelinesTableComponent from '../components/pipelines_table.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
pipelinesTableComponent, PipelinesTableComponent,
errorState, SvgBlankState,
emptyState, EmptyState,
loadingIcon, LoadingIcon,
},
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
data() { data() {
return { return {
...@@ -85,6 +81,7 @@ export default { ...@@ -85,6 +81,7 @@ export default {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
this.hasMadeRequest = true;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
...@@ -96,7 +93,7 @@ export default { ...@@ -96,7 +93,7 @@ export default {
postAction(endpoint) { postAction(endpoint) {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines')) .then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occurred while making the request.')); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
}, },
}; };
...@@ -5,7 +5,12 @@ export default class PipelinesStore { ...@@ -5,7 +5,12 @@ export default class PipelinesStore {
this.state = {}; this.state = {};
this.state.pipelines = []; this.state.pipelines = [];
this.state.count = {}; this.state.count = {
all: 0,
finished: 0,
pending: 0,
running: 0,
};
this.state.pageInfo = {}; this.state.pageInfo = {};
} }
......
...@@ -7,8 +7,9 @@ ...@@ -7,8 +7,9 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project), "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"has-ci" => @repository.gitlab_ci_yml, "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
"ci-lint-path" => ci_lint_path, "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
"has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
---
title: Handle empty state in Pipelines page
merge_request:
author:
type: fixed
...@@ -86,7 +86,22 @@ describe 'Pipelines', :js do ...@@ -86,7 +86,22 @@ describe 'Pipelines', :js do
it 'updates content when tab is clicked' do it 'updates content when tab is clicked' do
page.find('.js-pipelines-tab-pending').click page.find('.js-pipelines-tab-pending').click
wait_for_requests wait_for_requests
expect(page).to have_content('No pipelines to show.') expect(page).to have_content('There are currently no pending pipelines.')
end
end
context 'navigation links' do
before do
visit project_pipelines_path(project)
wait_for_requests
end
it 'renders run pipeline link' do
expect(page).to have_link('Run Pipeline')
end
it 'renders ci lint link' do
expect(page).to have_link('CI Lint')
end end
end end
...@@ -542,7 +557,7 @@ describe 'Pipelines', :js do ...@@ -542,7 +557,7 @@ describe 'Pipelines', :js do
end end
it 'has a clear caches button' do it 'has a clear caches button' do
expect(page).to have_link 'Clear runner caches' expect(page).to have_link 'Clear Runner Caches'
end end
describe 'user clicks the button' do describe 'user clicks the button' do
...@@ -552,19 +567,31 @@ describe 'Pipelines', :js do ...@@ -552,19 +567,31 @@ describe 'Pipelines', :js do
end end
it 'increments jobs_cache_index' do it 'increments jobs_cache_index' do
click_link 'Clear runner caches' click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end end
end end
context 'when project does not have jobs_cache_index' do context 'when project does not have jobs_cache_index' do
it 'sets jobs_cache_index to 1' do it 'sets jobs_cache_index to 1' do
click_link 'Clear runner caches' click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end end
end end
end end
end end
describe 'Empty State' do
let(:project) { create(:project, :repository) }
before do
visit project_pipelines_path(project)
end
it 'renders empty state' do
expect(page).to have_content 'Build with confidence'
end
end
end end
context 'when user is not logged in' do context 'when user is not logged in' do
...@@ -575,7 +602,9 @@ describe 'Pipelines', :js do ...@@ -575,7 +602,9 @@ describe 'Pipelines', :js do
context 'when project is public' do context 'when project is public' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
it { expect(page).to have_content 'Build with confidence' } context 'without pipelines' do
it { expect(page).to have_content 'This project is not currently set up to run pipelines.' }
end
end end
context 'when project is private' do context 'when project is private' do
......
import Vue from 'vue';
import component from '~/pipelines/components/blank_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Blank State', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(component);
vm = mountComponent(Component,
{
svgPath: 'foo',
message: 'Blank State',
},
);
});
it('should render svg', () => {
expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
});
it('should render message', () => {
expect(
vm.$el.querySelector('h4').textContent.trim(),
).toEqual('Blank State');
});
});
import Vue from 'vue'; import Vue from 'vue';
import emptyStateComp from '~/pipelines/components/empty_state.vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Empty State', () => { describe('Pipelines Empty State', () => {
let component; let component;
...@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => { ...@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => {
beforeEach(() => { beforeEach(() => {
EmptyStateComponent = Vue.extend(emptyStateComp); EmptyStateComponent = Vue.extend(emptyStateComp);
component = new EmptyStateComponent({ component = mountComponent(EmptyStateComponent, {
propsData: {
helpPagePath: 'foo', helpPagePath: 'foo',
emptyStateSvgPath: 'foo', emptyStateSvgPath: 'foo',
}, canSetCi: true,
}).$mount(); });
});
afterEach(() => {
component.$destroy();
}); });
it('should render empty state SVG', () => { it('should render empty state SVG', () => {
...@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => { ...@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect( expect(
component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continous Integration can help catch bugs by running your tests automatically'); ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
expect( expect(
component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continuous Deployment can help you deliver code to your product environment'); ).toContain('while Continuous Deployment can help you deliver code to your product environment');
}); });
it('should render a link with provided help path', () => { it('should render a link with provided help path', () => {
expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo');
expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines');
}); });
}); });
import Vue from 'vue';
import errorStateComp from '~/pipelines/components/error_state.vue';
describe('Pipelines Error State', () => {
let component;
let ErrorStateComponent;
beforeEach(() => {
ErrorStateComponent = Vue.extend(errorStateComp);
component = new ErrorStateComponent({
propsData: {
errorStateSvgPath: 'foo',
},
}).$mount();
});
it('should render error state SVG', () => {
expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
});
it('should render emtpy state information', () => {
expect(
component.$el.querySelector('h4').textContent,
).toContain('The API failed to fetch the pipelines');
});
});
import Vue from 'vue'; import Vue from 'vue';
import navControlsComp from '~/pipelines/components/nav_controls.vue'; import navControlsComp from '~/pipelines/components/nav_controls.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Nav Controls', () => { describe('Pipelines Nav Controls', () => {
let NavControlsComponent; let NavControlsComponent;
let component;
beforeEach(() => { beforeEach(() => {
NavControlsComponent = Vue.extend(navControlsComp); NavControlsComponent = Vue.extend(navControlsComp);
}); });
afterEach(() => {
component.$destroy();
});
it('should render link to create a new pipeline', () => { it('should render link to create a new pipeline', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: true,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath);
}); });
it('should not render link to create pipeline if no permission is provided', () => { it('should not render link to create pipeline if no path is provided', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo', helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: false,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create')).toEqual(null); expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
}); });
it('should render link for resetting runner caches', () => { it('should render link for resetting runner caches', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: false,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches');
expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath);
}); });
it('should render link for CI lint', () => { it('should render link for CI lint', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint');
expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath);
});
it('should render link to help page when CI is not enabled', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: false,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
});
it('should not render link to help page when CI is enabled', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo', helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: true,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info')).toEqual(null); expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath);
}); });
}); });
...@@ -9,7 +9,12 @@ describe('Pipelines Store', () => { ...@@ -9,7 +9,12 @@ describe('Pipelines Store', () => {
it('should be initialized with an empty state', () => { it('should be initialized with an empty state', () => {
expect(store.state.pipelines).toEqual([]); expect(store.state.pipelines).toEqual([]);
expect(store.state.count).toEqual({}); expect(store.state.count).toEqual({
all: 0,
finished: 0,
pending: 0,
running: 0,
});
expect(store.state.pageInfo).toEqual({}); expect(store.state.pageInfo).toEqual({});
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment