diff options
author | Bryce Johnson <bryce@gitlab.com> | 2017-04-19 23:26:52 +0300 |
---|---|---|
committer | Bryce Johnson <bryce@gitlab.com> | 2017-04-25 18:09:11 +0300 |
commit | b29090ef871b0c45e4a05da9421e027846657658 (patch) | |
tree | 8391794b40d2f31aee837be6b9096dea4f2a08f0 | |
parent | 5a95bab45b55a25ba5c57f3d0a3487e863ec8405 (diff) |
Ensure pipeline action buttons are updated after action
6 files changed, 167 insertions, 143 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 68a1c1de1df..981ffbe3791 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -43,7 +43,7 @@ export default Vue.component('pipelines-table', { helpPagePath: null, store, state: store.state, - isLoading: false, + isLoading: true, hasError: false, isMakingRequest: false, }; @@ -91,7 +91,6 @@ export default Vue.component('pipelines-table', { }); if (!Visibility.hidden()) { - this.isLoading = true; this.poll.makeRequest(); } @@ -125,8 +124,6 @@ export default Vue.component('pipelines-table', { methods: { fetchPipelines() { - this.isLoading = true; - return this.service.getPipelines() .then(response => this.successCallback(response)) .catch(() => this.errorCallback()); diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index d1c60b570de..1c7bd3aea42 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,82 +1,30 @@ <script> -/* eslint-disable no-new, no-alert */ -/* global Flash */ -import '~/flash'; -import eventHub from '../event_hub'; - export default { props: { - endpoint: { - type: String, - required: true, - }, - - service: { - type: Object, - required: true, - }, - title: { type: String, required: true, }, - icon: { type: String, required: true, }, - - cssClass: { - type: String, + isLoading: { + type: Boolean, required: true, }, - - confirmActionMessage: { - type: String, - required: false, - }, - }, - - data() { - return { - isLoading: false, - }; }, computed: { iconClass() { return `fa fa-${this.icon}`; }, - buttonClass() { - return `btn has-tooltip ${this.cssClass}`; + return `btn has-tooltip`; }, }, - - methods: { - onClick() { - if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { - this.makeRequest(); - } else if (!this.confirmActionMessage) { - this.makeRequest(); - } - }, - - makeRequest() { - this.isLoading = true; - - $(this.$el).tooltip('destroy'); - - this.service.postAction(this.endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); - }, + updated() { + $(this.$el).tooltip('destroy'); }, }; </script> @@ -84,7 +32,6 @@ export default { <template> <button type="button" - @click="onClick" :class="buttonClass" :title="title" :aria-label="title" diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js index 6c2174a3717..ab57701c85a 100644 --- a/app/assets/javascripts/pipelines/pipelines.js +++ b/app/assets/javascripts/pipelines/pipelines.js @@ -208,6 +208,7 @@ export default { errorCallback() { this.hasError = true; + this.isLoading = false; }, setIsMakingRequest(isMakingRequest) { diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js index 62b7131de51..ab00bf33011 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js @@ -1,5 +1,6 @@ -/* eslint-disable no-param-reassign */ - +/* eslint-disable no-param-reassign, no-alert */ +import Flash from '~/flash'; +import eventHub from '../../pipelines/event_hub'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; @@ -26,7 +27,12 @@ export default { required: true, }, }, - + data() { + return { + isRetrying: false, + isCancelling: false, + }; + }, components: { 'async-button-component': AsyncButtonComponent, 'pipelines-actions-component': PipelinesActionsComponent, @@ -167,7 +173,34 @@ export default { return undefined; }, }, + watch: { + // we watch pipeline update bc we don't know when refreshPipelines is finished + pipeline: 'resetButtonLoadingState', + }, + methods: { + resetButtonLoadingState() { + this.isCancelling = false; + this.isRetrying = false; + }, + cancelPipeline() { + this.isCancelling = true; + const confirmCancelMessage = 'Are you sure you want to cancel this pipeline?'; + return this.makeRequest(this.pipeline.cancel_path, confirmCancelMessage); + }, + retryPipeline() { + this.isRetrying = true; + return this.makeRequest(this.pipeline.retry_path); + }, + makeRequest(endpoint, confirmMessage) { + if (confirmMessage && !confirm(confirmMessage)) { + return Promise.resolve(); + } + return this.service.postAction(endpoint) + .then(() => eventHub.$emit('refreshPipelines')) + .catch(() => new Flash('An error occured while making the request.')); + }, + }, template: ` <tr class="commit"> <status-scope :pipeline="pipeline"/> @@ -207,20 +240,19 @@ export default { <async-button-component v-if="pipeline.flags.retryable" - :service="service" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" + class="js-pipelines-retry-button btn-default btn-retry" + @click.native="retryPipeline" + :is-loading="isRetrying" title="Retry" - icon="repeat" /> + icon="repeat"/> <async-button-component v-if="pipeline.flags.cancelable" - :service="service" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" + class="js-pipelines-cancel-button btn-remove" + @click.native="cancelPipeline" + :is-loading="isCancelling" title="Cancel" - icon="remove" - confirm-action-message="Are you sure you want to cancel this pipeline?" /> + icon="remove"/> </div> </td> </tr> diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 28c9c7ab282..c1f8f671112 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -3,23 +3,16 @@ import asyncButtonComp from '~/pipelines/components/async_button.vue'; describe('Pipelines Async Button', () => { let component; - let spy; let AsyncButtonComponent; beforeEach(() => { AsyncButtonComponent = Vue.extend(asyncButtonComp); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new AsyncButtonComponent({ propsData: { - endpoint: '/foo', title: 'Foo', icon: 'fa fa-foo', - cssClass: 'bar', - service: { - postAction: spy, - }, + isLoading: false, }, }).$mount(); }); @@ -37,57 +30,16 @@ describe('Pipelines Async Button', () => { expect(component.$el.getAttribute('aria-label')).toContain('Foo'); }); - it('should render the provided cssClass', () => { - expect(component.$el.getAttribute('class')).toContain('bar'); - }); - - it('should call the service when it is clicked with the provided endpoint', () => { - component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - dataAttributes: { - 'data-foo': 'foo', - }, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.click(); - expect(component.$el.querySelector('.fa-spinner')).toBe(null); + it('should not render the spinner when not loading', () => { + expect(component.$el.querySelector('.fa-spinner')).toBeNull(); }); - describe('With confirm dialog', () => { - it('should call the service when confimation is positive', () => { - spyOn(window, 'confirm').and.returnValue(true); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - service: { - postAction: spy, - }, - confirmActionMessage: 'bar', - }, - }).$mount(); + it('should render the spinner when loading state changes', (done) => { + component.isLoading = true; - component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); + Vue.nextTick(() => { + expect(component.$el.querySelector('.fa-spinner')).not.toBe(null); + done(); }); }); }); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 699625cdbb7..cdb376492a6 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -3,34 +3,36 @@ import tableRowComp from '~/vue_shared/components/pipelines_table_row'; import pipeline from '../../commit/pipelines/mock_data'; describe('Pipelines Table Row', () => { - let component; + const postActionSpy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); beforeEach(() => { const PipelinesTableRowComponent = Vue.extend(tableRowComp); - component = new PipelinesTableRowComponent({ + this.component = new PipelinesTableRowComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipeline, - service: {}, + service: { + postAction: postActionSpy, + }, }, }).$mount(); }); it('should render a table row', () => { - expect(component.$el).toEqual('TR'); + expect(this.component.$el).toEqual('TR'); }); describe('status column', () => { it('should render a pipeline link', () => { expect( - component.$el.querySelector('td.commit-link a').getAttribute('href'), + this.component.$el.querySelector('td.commit-link a').getAttribute('href'), ).toEqual(pipeline.path); }); it('should render status text', () => { expect( - component.$el.querySelector('td.commit-link a').textContent, + this.component.$el.querySelector('td.commit-link a').textContent, ).toContain(pipeline.details.status.text); }); }); @@ -38,24 +40,24 @@ describe('Pipelines Table Row', () => { describe('information column', () => { it('should render a pipeline link', () => { expect( - component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + this.component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), ).toEqual(pipeline.path); }); it('should render pipeline ID', () => { expect( - component.$el.querySelector('td:nth-child(2) a > span').textContent, + this.component.$el.querySelector('td:nth-child(2) a > span').textContent, ).toEqual(`#${pipeline.id}`); }); describe('when a user is provided', () => { it('should render user information', () => { expect( - component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + this.component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), ).toEqual(pipeline.user.web_url); expect( - component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + this.component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), ).toEqual(pipeline.user.name); }); }); @@ -64,7 +66,7 @@ describe('Pipelines Table Row', () => { describe('commit column', () => { it('should render link to commit', () => { expect( - component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'), + this.component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'), ).toEqual(pipeline.commit.commit_path); }); }); @@ -72,7 +74,7 @@ describe('Pipelines Table Row', () => { describe('stages column', () => { it('should render an icon for each stage', () => { expect( - component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + this.component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, ).toEqual(pipeline.details.stages.length); }); }); @@ -80,8 +82,101 @@ describe('Pipelines Table Row', () => { describe('actions column', () => { it('should render the provided actions', () => { expect( - component.$el.querySelectorAll('td:nth-child(6) ul li').length, + this.component.$el.querySelectorAll('td:nth-child(6) ul li').length, ).toEqual(pipeline.details.manual_actions.length); }); }); + + describe('async button action methods', () => { + beforeEach(() => { + spyOn(window, 'confirm').and.returnValue(true); + }); + + it('#resetButtonLoadingState resets isCancelling', (done) => { + this.component.isCancelling = true; + + this.component.resetButtonLoadingState(); + + Vue.nextTick(() => { + expect(this.component.isCancelling).toBe(false); + done(); + }); + }); + + it('#resetButtonLoadingState resets isRetrying', (done) => { + this.component.isRetrying = true; + + this.component.resetButtonLoadingState(); + + Vue.nextTick(() => { + expect(this.component.isRetrying).toBe(false); + done(); + }); + }); + + it('#cancelPipeline sets isCancelling', (done) => { + spyOn(this.component, 'makeRequest'); + + this.component.cancelPipeline(); + + Vue.nextTick(() => { + expect(this.component.isCancelling).toBe(true); + done(); + }); + }); + + it('#cancelPipeline calls makeRequest', (done) => { + spyOn(this.component, 'makeRequest'); + + this.component.cancelPipeline(); + + Vue.nextTick(() => { + expect(this.component.makeRequest).toHaveBeenCalled(); + done(); + }); + }); + + it('#retryPipeline sets isRetrying', (done) => { + spyOn(this.component, 'makeRequest'); + + this.component.retryPipeline(); + + Vue.nextTick(() => { + expect(this.component.isRetrying).toBe(true); + done(); + }); + }); + + it('#retryPipeline calls makeRequest', (done) => { + spyOn(this.component, 'makeRequest'); + + this.component.retryPipeline(); + + Vue.nextTick(() => { + expect(this.component.makeRequest).toHaveBeenCalled(); + done(); + }); + }); + + + it('pipeline update triggers watcher to reset isCancelling', (done) => { + this.isCancelling = true; + this.component.$props.pipeline = Object.assign({}, pipeline, { created_at: new Date() }); + + Vue.nextTick(() => { + expect(this.component.isCancelling).toBe(false); + done(); + }); + }); + + it('pipeline update triggers watcher to reset isRetrying', (done) => { + this.isRetrying = true; + this.component.$props.pipeline = Object.assign({}, pipeline, { created_at: new Date() }); + + Vue.nextTick(() => { + expect(this.component.isRetrying).toBe(false); + done(); + }); + }); + }); }); |