diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-03-20 15:36:02 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-03-20 16:16:19 +0000 |
commit | 446b59dd4ee88f8ef86272c39408560a0107c48a (patch) | |
tree | dc6eef4b89111775dd4176f6db08fd95af57c293 | |
parent | 2c85a20482c2e33f307ad8faf60e41e199aa25b8 (diff) | |
download | gitlab-ce-446b59dd4ee88f8ef86272c39408560a0107c48a.tar.gz |
Adds tests to new empty and error states
15 files changed, 460 insertions, 49 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 29ee3e5e67b..189dd2c7dce 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -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" diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js index cfe23b65d55..78f111c74bc 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js +++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js @@ -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> diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js index 9071ecdea73..f197395a6db 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/error_state.js +++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js @@ -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> `, }; diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js index a494d2459aa..b4480bd98c7 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js +++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js @@ -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"> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js index 87242ff0369..885ad465179 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -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> + <div class="content-list pipelines"> - <empty-state v-if="shouldRenderEmptyState" :helpPagePath="helpPagePath" /> + <div + class="realtime-loading" + v-if="pageRequest"> + <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </div> - <error-state v-if="shouldRenderErrorState" /> + <empty-state v-if="shouldRenderEmptyState" :helpPagePath="helpPagePath" /> - <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> + <error-state v-if="shouldRenderErrorState" /> - <div - class="table-holder" - v-if="shouldRenderTable"> + <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> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service"/> - </div> + <div + class="table-holder" + v-if="shouldRenderTable"> - <gl-pagination - v-if="shouldRenderPagination" - :pagenum="pagenum" - :change="change" - :count="state.count.all" - :pageInfo="state.pageInfo"/> + <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> </div> `, }; diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index da5a676274f..5e157db3396 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -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 diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 38cea4429ba..3d73284699f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -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') diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 162056671e0..2272b19bc8f 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -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 diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 75efcc06585..cb51e6f3ef3 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -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); }); diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml new file mode 100644 index 00000000000..418a38a0e2e --- /dev/null +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -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' } } diff --git a/spec/javascripts/vue_pipelines_index/empty_state_spec.js b/spec/javascripts/vue_pipelines_index/empty_state_spec.js new file mode 100644 index 00000000000..733337168dc --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/empty_state_spec.js @@ -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'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/error_state_spec.js b/spec/javascripts/vue_pipelines_index/error_state_spec.js new file mode 100644 index 00000000000..524e018b1fa --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/error_state_spec.js @@ -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'); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/mock_data.js b/spec/javascripts/vue_pipelines_index/mock_data.js new file mode 100644 index 00000000000..2365a662b9f --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/mock_data.js @@ -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, + }, +}; diff --git a/spec/javascripts/vue_pipelines_index/nav_controls_spec.js b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js new file mode 100644 index 00000000000..8283e53c8b1 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/nav_controls_spec.js @@ -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); + }); +}); diff --git a/spec/javascripts/vue_pipelines_index/pipelines_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_spec.js new file mode 100644 index 00000000000..22d1c4e16a3 --- /dev/null +++ b/spec/javascripts/vue_pipelines_index/pipelines_spec.js @@ -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(); + }); + }); + }); +}); |