Adds tests to new empty and error states

This commit is contained in:
Filipa Lacerda 2017-03-20 15:36:02 +00:00
parent 2c85a20482
commit 446b59dd4e
15 changed files with 465 additions and 54 deletions

View File

@ -5,6 +5,7 @@ import PipelinesTableComponent from '../../vue_shared/components/pipelines_table
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub';
import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
@ -24,6 +25,7 @@ export default Vue.component('pipelines-table', {
components: {
'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState,
'empty-state': EmptyState,
},
/**
@ -38,6 +40,7 @@ export default Vue.component('pipelines-table', {
return {
endpoint: pipelinesTableData.endpoint,
helpPagePath: pipelinesTableData.helpPagePath,
store,
state: store.state,
isLoading: false,
@ -49,6 +52,10 @@ export default Vue.component('pipelines-table', {
shouldRenderErrorState() {
return this.hasError && !this.pageRequest;
},
shouldRenderEmptyState() {
return !this.state.pipelines.length && !this.pageRequest;
},
},
/**
@ -102,6 +109,8 @@ export default Vue.component('pipelines-table', {
<i class="fa fa-spinner fa-spin"></i>
</div>
<empty-state v-if="shouldRenderEmptyState" :helpPagePath="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div class="table-holder pipelines"

View File

@ -15,7 +15,7 @@ export default {
},
template: `
<div class="row empty-state">
<div class="row empty-state js-pipelines-empty-state">
<div class="col-xs-12 pull-right">
<div class="svg-content">
${pipelinesEmptyStateSVG}
@ -28,10 +28,10 @@ export default {
<p>
Continous Integration can help catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
</a>
</p>
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
</a>
</div>
</div>
</div>

View File

@ -8,18 +8,18 @@ export default {
},
template: `
<div class="row empty-state">
<div class="col-xs-12 pull-right">
<div class="svg-content">
${pipelinesErrorStateSVG}
<div class="row empty-state js-pipelines-error-state">
<div class="col-xs-12 pull-right">
<div class="svg-content">
${pipelinesErrorStateSVG}
</div>
</div>
</div>
<div class="col-xs-12 center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
<div class="col-xs-12 center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
</div>
</div>
</div>
</div>
`,
};

View File

@ -18,7 +18,9 @@ export default {
template: `
<ul class="nav-links">
<li :class="{ 'active': scope === 'all'}">
<li
class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">

View File

@ -32,7 +32,19 @@ export default {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return {
...pipelinesData,
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
@ -172,8 +184,7 @@ export default {
template: `
<div
:class="cssClass"
class="pipelines">
:class="cssClass">
<div
class="top-area"
@ -191,37 +202,40 @@ export default {
:canCreatePipeline="canCreatePipelineParsed " />
</div>
<div
class="realtime-loading"
v-if="pageRequest">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<div class="content-list pipelines">
<div
class="realtime-loading"
v-if="pageRequest">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<empty-state v-if="shouldRenderEmptyState" :helpPagePath="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
</div>
<gl-pagination
v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"/>
</div>
<empty-state v-if="shouldRenderEmptyState" :helpPagePath="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
</div>
<gl-pagination
v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"/>
</div>
`,
};

View File

@ -1,6 +1,7 @@
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README')
} }
- content_for :page_specific_javascripts do

View File

@ -2,10 +2,6 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag("common_vue")
= page_specific_javascript_bundle_tag("vue_pipelines")
#pipelines-list-vue{ data: { endpoint: namespace_project_pipelines_path(@project.namespace, @project, format: :json),
"css-class" => container_class,
"help-page-path" => help_page_path('ci/quick_start/README'),
@ -19,3 +15,6 @@
"tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } }
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('vue_pipelines')

View File

@ -442,7 +442,7 @@ describe 'Pipelines', :feature, :js do
context 'when project is public' do
let(:project) { create(:project, :public) }
it { expect(page).to have_content 'No pipelines to show' }
it { expect(page).to have_content 'Build with confidence' }
it { expect(page).to have_http_status(:success) }
end

View File

@ -33,7 +33,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
setTimeout(() => {
expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
expect(component.$el.querySelector('.js-pipelines-empty-state')).toBeDefined();
done();
}, 1);
});
@ -92,7 +92,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
setTimeout(() => {
expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
done();
}, 0);
});

View File

@ -0,0 +1,14 @@
%div
#pipelines-list-vue{ data: { endpoint: 'foo',
"css-class" => 'foo',
"help-page-path" => 'foo',
"new-pipeline-path" => 'foo',
"can-create-pipeline" => 'true',
"all-path" => 'foo',
"pending-path" => 'foo',
"running-path" => 'foo',
"finished-path" => 'foo',
"branches-path" => 'foo',
"tags-path" => 'foo',
"has-ci" => 'foo',
"ci-lint-path" => 'foo' } }

View File

@ -0,0 +1,38 @@
import Vue from 'vue';
import emptyStateComp from '~/vue_pipelines_index/components/empty_state';
describe('Pipelines Empty State', () => {
let component;
let EmptyStateComponent;
beforeEach(() => {
EmptyStateComponent = Vue.extend(emptyStateComp);
component = new EmptyStateComponent({
propsData: {
helpPagePath: 'foo',
},
}).$mount();
});
it('should render empty state SVG', () => {
expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
});
it('should render emtpy state information', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect(
component.$el.querySelector('p').textContent,
).toContain('Continous Integration can help catch bugs by running your tests automatically');
expect(
component.$el.querySelector('p').textContent,
).toContain('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');
});
});

View File

@ -0,0 +1,23 @@
import Vue from 'vue';
import errorStateComp from '~/vue_pipelines_index/components/error_state';
describe('Pipelines Error State', () => {
let component;
let ErrorStateComponent;
beforeEach(() => {
ErrorStateComponent = Vue.extend(errorStateComp);
component = new ErrorStateComponent().$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');
});
});

View File

@ -0,0 +1,107 @@
export default {
pipelines: [{
id: 115,
user: {
name: 'Root',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
path: '/root/review-app/pipelines/115',
details: {
status: {
icon: 'icon_status_failed',
text: 'failed',
label: 'failed',
group: 'failed',
has_details: true,
details_path: '/root/review-app/pipelines/115',
},
duration: null,
finished_at: '2017-03-17T19:00:15.996Z',
stages: [{
name: 'build',
title: 'build: failed',
status: {
icon: 'icon_status_failed',
text: 'failed',
label: 'failed',
group: 'failed',
has_details: true,
details_path: '/root/review-app/pipelines/115#build',
},
path: '/root/review-app/pipelines/115#build',
dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build',
},
{
name: 'review',
title: 'review: skipped',
status: {
icon: 'icon_status_skipped',
text: 'skipped',
label: 'skipped',
group: 'skipped',
has_details: true,
details_path: '/root/review-app/pipelines/115#review',
},
path: '/root/review-app/pipelines/115#review',
dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review',
}],
artifacts: [],
manual_actions: [{
name: 'stop_review',
path: '/root/review-app/builds/3766/play',
}],
},
flags: {
latest: true,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: true,
cancelable: false,
},
ref: {
name: 'thisisabranch',
path: '/root/review-app/tree/thisisabranch',
tag: false,
branch: true,
},
commit: {
id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600',
short_id: '9e87f876',
title: 'Update README.md',
created_at: '2017-03-15T22:58:28.000+00:00',
parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'],
message: 'Update README.md',
author_name: 'Root',
author_email: 'admin@example.com',
authored_date: '2017-03-15T22:58:28.000+00:00',
committer_name: 'Root',
committer_email: 'admin@example.com',
committed_date: '2017-03-15T22:58:28.000+00:00',
author: {
name: 'Root',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
},
retry_path: '/root/review-app/pipelines/115/retry',
created_at: '2017-03-15T22:58:33.436Z',
updated_at: '2017-03-17T19:00:15.997Z',
}],
count: {
all: 52,
running: 0,
pending: 0,
finished: 52,
},
};

View File

@ -0,0 +1,93 @@
import Vue from 'vue';
import navControlsComp from '~/vue_pipelines_index/components/nav_controls';
describe('Pipelines Nav Controls', () => {
let NavControlsComponent;
beforeEach(() => {
NavControlsComponent = Vue.extend(navControlsComp);
});
it('should render link to create a new pipeline', () => {
const mockData = {
newPipelinePath: 'foo',
hasCIEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline');
expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath);
});
it('should not render link to create pipeline if no permission is provided', () => {
const mockData = {
newPipelinePath: 'foo',
hasCIEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
canCreatePipeline: false,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create')).toEqual(null);
});
it('should render link for CI lint', () => {
const mockData = {
newPipelinePath: 'foo',
hasCIEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-default').textContent).toContain('CI Lint');
expect(component.$el.querySelector('.btn-default').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',
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',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info')).toEqual(null);
});
});

View File

@ -0,0 +1,111 @@
import Vue from 'vue';
import pipelinesComp from '~/vue_pipelines_index/pipelines';
import Store from '~/vue_pipelines_index/stores/pipelines_store';
import pipelinesData from './mock_data';
describe('Pipelines', () => {
preloadFixtures('static/pipelines.html.raw');
let PipelinesComponent;
beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
PipelinesComponent = Vue.extend(pipelinesComp);
});
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipelinesData), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('should render table', (done) => {
const component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => {
expect(component.$el.querySelector('.table-holder')).toBeDefined();
done();
});
});
});
describe('without pipelines', () => {
const emptyInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(emptyInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyInterceptor,
);
});
it('should render empty state', (done) => {
const component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-empty-state')).toBeDefined();
done();
});
});
});
});
describe('unsuccessfull request', () => {
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(errorInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('should render error state', (done) => {
const component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
done();
});
});
});
});