diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-10 15:08:08 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-10 15:08:08 +0000 |
commit | 7c38405be9e79099f399aa429503ea7b463bbf5a (patch) | |
tree | 30944a8baf135021395574e081f53ed5f756ace0 /spec/frontend/vue_mr_widget | |
parent | 1fa79760ad2d4bd67f5c5a27f372a7533b9b7c69 (diff) | |
download | gitlab-ce-7c38405be9e79099f399aa429503ea7b463bbf5a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/vue_mr_widget')
5 files changed, 418 insertions, 42 deletions
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js new file mode 100644 index 00000000000..1b14ee694fe --- /dev/null +++ b/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js @@ -0,0 +1,124 @@ +import { mount } from '@vue/test-utils'; +import { GlIcon, GlLoadingIcon, GlButton } from '@gitlab/ui'; +import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue'; +import { + CREATED, + RUNNING, + DEPLOYING, + REDEPLOYING, +} from '~/vue_merge_request_widget/components/deployment/constants'; +import { actionButtonMocks } from './deployment_mock_data'; + +const baseProps = { + actionsConfiguration: actionButtonMocks[DEPLOYING], + actionInProgress: null, + computedDeploymentStatus: CREATED, +}; + +describe('Deployment action button', () => { + let wrapper; + + const factory = (options = {}) => { + wrapper = mount(DeploymentActionButton, { + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when passed only icon', () => { + beforeEach(() => { + factory({ + propsData: baseProps, + slots: { default: ['<gl-icon name="stop" />'] }, + stubs: { + 'gl-icon': GlIcon, + }, + }); + }); + + it('renders slot correctly', () => { + expect(wrapper.find(GlIcon).exists()).toBe(true); + }); + }); + + describe('when passed multiple items', () => { + beforeEach(() => { + factory({ + propsData: baseProps, + slots: { + default: ['<gl-icon name="play" />', `<span>${actionButtonMocks[DEPLOYING]}</span>`], + }, + stubs: { + 'gl-icon': GlIcon, + }, + }); + }); + + it('renders slot correctly', () => { + expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.text()).toContain(actionButtonMocks[DEPLOYING]); + }); + }); + + describe('when its action is in progress', () => { + beforeEach(() => { + factory({ + propsData: { + ...baseProps, + actionInProgress: actionButtonMocks[DEPLOYING].actionName, + }, + }); + }); + + it('is disabled and shows the loading icon', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.find(GlButton).props('disabled')).toBe(true); + }); + }); + + describe('when another action is in progress', () => { + beforeEach(() => { + factory({ + propsData: { + ...baseProps, + actionInProgress: actionButtonMocks[REDEPLOYING].actionName, + }, + }); + }); + it('is disabled and does not show the loading icon', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(GlButton).props('disabled')).toBe(true); + }); + }); + + describe('when action status is running', () => { + beforeEach(() => { + factory({ + propsData: { + ...baseProps, + actionInProgress: actionButtonMocks[REDEPLOYING].actionName, + computedDeploymentStatus: RUNNING, + }, + }); + }); + it('is disabled and does not show the loading icon', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(GlButton).props('disabled')).toBe(true); + }); + }); + + describe('when no action is in progress', () => { + beforeEach(() => { + factory({ + propsData: baseProps, + }); + }); + it('is not disabled nor does it show the loading icon', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(GlButton).props('disabled')).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js new file mode 100644 index 00000000000..6449272e6ed --- /dev/null +++ b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js @@ -0,0 +1,220 @@ +import { mount } from '@vue/test-utils'; +import createFlash from '~/flash'; +import { visitUrl } from '~/lib/utils/url_utility'; +import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; +import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue'; +import { + CREATED, + MANUAL_DEPLOY, + FAILED, + DEPLOYING, + REDEPLOYING, + STOPPING, +} from '~/vue_merge_request_widget/components/deployment/constants'; +import { + actionButtonMocks, + deploymentMockData, + playDetails, + retryDetails, +} from './deployment_mock_data'; + +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility'); + +describe('DeploymentAction component', () => { + let wrapper; + let executeActionSpy; + + const factory = (options = {}) => { + // This destroys any wrappers created before a nested call to factory reassigns it + if (wrapper && wrapper.destroy) { + wrapper.destroy(); + } + + wrapper = mount(DeploymentActions, { + ...options, + provide: { glFeatures: { deployFromFooter: true } }, + }); + }; + + const findStopButton = () => wrapper.find('.js-stop-env'); + const findDeployButton = () => wrapper.find('.js-manual-deploy-action'); + const findRedeployButton = () => wrapper.find('.js-manual-redeploy-action'); + + beforeEach(() => { + executeActionSpy = jest.spyOn(MRWidgetService, 'executeInlineAction'); + + factory({ + propsData: { + computedDeploymentStatus: CREATED, + deployment: deploymentMockData, + showVisualReviewApp: false, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('actions do not appear when conditions are unmet', () => { + describe('when there is no stop_url', () => { + beforeEach(() => { + factory({ + propsData: { + computedDeploymentStatus: CREATED, + deployment: { + ...deploymentMockData, + stop_url: null, + }, + showVisualReviewApp: false, + }, + }); + }); + + it('the stop button does not appear', () => { + expect(findStopButton().exists()).toBe(false); + }); + }); + + describe('when there is no play_path in details', () => { + it('the manual deploy button does not appear', () => { + expect(findDeployButton().exists()).toBe(false); + }); + }); + + describe('when there is no retry_path in details', () => { + it('the manual redeploy button does not appear', () => { + expect(findRedeployButton().exists()).toBe(false); + }); + }); + }); + + describe('when conditions are met', () => { + describe.each` + configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint + ${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url} + ${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path} + ${REDEPLOYING} | ${FAILED} | ${retryDetails} | ${findRedeployButton} | ${retryDetails.playable_build.retry_path} + `( + '$configConst action', + ({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => { + describe(`${configConst} action`, () => { + const confirmAction = () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + finderFn().trigger('click'); + }; + + const rejectAction = () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(false); + finderFn().trigger('click'); + }; + + beforeEach(() => { + factory({ + propsData: { + computedDeploymentStatus, + deployment: { + ...deploymentMockData, + details: displayConditionChanges, + }, + showVisualReviewApp: false, + }, + }); + }); + + it('the button is rendered', () => { + expect(finderFn().exists()).toBe(true); + }); + + describe('when clicked', () => { + describe('should show a confirm dialog but not call executeInlineAction when declined', () => { + beforeEach(() => { + executeActionSpy.mockResolvedValueOnce(); + rejectAction(); + }); + + it('should show the confirm dialog', () => { + expect(window.confirm).toHaveBeenCalled(); + expect(window.confirm).toHaveBeenCalledWith( + actionButtonMocks[configConst].confirmMessage, + ); + }); + + it('should not execute the action', () => { + expect(MRWidgetService.executeInlineAction).not.toHaveBeenCalled(); + }); + }); + + describe('should show a confirm dialog and call executeInlineAction when accepted', () => { + beforeEach(() => { + executeActionSpy.mockResolvedValueOnce(); + confirmAction(); + }); + + it('should show the confirm dialog', () => { + expect(window.confirm).toHaveBeenCalled(); + expect(window.confirm).toHaveBeenCalledWith( + actionButtonMocks[configConst].confirmMessage, + ); + }); + + it('should execute the action with expected URL', () => { + expect(MRWidgetService.executeInlineAction).toHaveBeenCalled(); + expect(MRWidgetService.executeInlineAction).toHaveBeenCalledWith(endpoint); + }); + + it('should not throw an error', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + + describe('response includes redirect_url', () => { + const url = '/root/example'; + beforeEach(() => { + executeActionSpy.mockResolvedValueOnce({ + data: { redirect_url: url }, + }); + confirmAction(); + }); + + it('calls visit url with the redirect_url', () => { + expect(visitUrl).toHaveBeenCalled(); + expect(visitUrl).toHaveBeenCalledWith(url); + }); + }); + + describe('it should call the executeAction method ', () => { + beforeEach(() => { + jest.spyOn(wrapper.vm, 'executeAction').mockImplementation(); + confirmAction(); + }); + + it('calls with the expected arguments', () => { + expect(wrapper.vm.executeAction).toHaveBeenCalled(); + expect(wrapper.vm.executeAction).toHaveBeenCalledWith( + endpoint, + actionButtonMocks[configConst], + ); + }); + }); + + describe('when executeInlineAction errors', () => { + beforeEach(() => { + executeActionSpy.mockRejectedValueOnce(); + confirmAction(); + }); + + it('should call createFlash with error message', () => { + expect(createFlash).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith( + actionButtonMocks[configConst].errorMessage, + ); + }); + }); + }); + }); + }); + }, + ); + }); +}); diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js index f8f4cb627dd..ff29022b75d 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js +++ b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js @@ -1,4 +1,33 @@ -import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; +import { + DEPLOYING, + REDEPLOYING, + SUCCESS, + STOPPING, +} from '~/vue_merge_request_widget/components/deployment/constants'; + +const actionButtonMocks = { + [STOPPING]: { + actionName: STOPPING, + buttonText: 'Stop environment', + busyText: 'This environment is being deployed', + confirmMessage: 'Are you sure you want to stop this environment?', + errorMessage: 'Something went wrong while stopping this environment. Please try again.', + }, + [DEPLOYING]: { + actionName: DEPLOYING, + buttonText: 'Deploy', + busyText: 'This environment is being deployed', + confirmMessage: 'Are you sure you want to deploy this environment?', + errorMessage: 'Something went wrong while deploying this environment. Please try again.', + }, + [REDEPLOYING]: { + actionName: REDEPLOYING, + buttonText: 'Re-deploy', + busyText: 'This environment is being re-deployed', + confirmMessage: 'Are you sure you want to re-deploy this environment?', + errorMessage: 'Something went wrong while deploying this environment. Please try again.', + }, +}; const deploymentMockData = { id: 15, @@ -29,4 +58,16 @@ const deploymentMockData = { ], }; -export default deploymentMockData; +const playDetails = { + playable_build: { + play_path: '/root/test-deployments/-/jobs/1131/play', + }, +}; + +const retryDetails = { + playable_build: { + retry_path: '/root/test-deployments/-/jobs/1131/retry', + }, +}; + +export { actionButtonMocks, deploymentMockData, playDetails, retryDetails }; diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js index ec7be6b64fc..ce395de3b5d 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js +++ b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js @@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils'; import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue'; import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue'; import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; -import DeploymentStopButton from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue'; import { CREATED, RUNNING, @@ -10,15 +9,7 @@ import { FAILED, CANCELED, } from '~/vue_merge_request_widget/components/deployment/constants'; -import deploymentMockData from './deployment_mock_data'; - -const deployDetail = { - playable_build: { - retry_path: '/root/test-deployments/-/jobs/1131/retry', - play_path: '/root/test-deployments/-/jobs/1131/play', - }, - isManual: true, -}; +import { deploymentMockData, playDetails, retryDetails } from './deployment_mock_data'; describe('Deployment component', () => { let wrapper; @@ -30,6 +21,7 @@ describe('Deployment component', () => { } wrapper = mount(DeploymentComponent, { ...options, + provide: { glFeatures: { deployFromFooter: true } }, }); }; @@ -53,28 +45,39 @@ describe('Deployment component', () => { describe('status message and buttons', () => { const noActions = []; const noDetails = { isManual: false }; - const deployGroup = [DeploymentViewButton, DeploymentStopButton]; + const deployDetail = { + ...playDetails, + isManual: true, + }; + + const retryDetail = { + ...retryDetails, + isManual: true, + }; + const defaultGroup = ['.js-deploy-url', '.js-stop-env']; + const manualDeployGroup = ['.js-manual-deploy-action', ...defaultGroup]; + const manualRedeployGroup = ['.js-manual-redeploy-action', ...defaultGroup]; describe.each` status | previous | deploymentDetails | text | actionButtons - ${CREATED} | ${true} | ${deployDetail} | ${'Can be manually deployed to'} | ${deployGroup} - ${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${deployGroup} + ${CREATED} | ${true} | ${deployDetail} | ${'Can be manually deployed to'} | ${manualDeployGroup} + ${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${defaultGroup} ${CREATED} | ${false} | ${deployDetail} | ${'Can be manually deployed to'} | ${noActions} ${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions} - ${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${deployGroup} - ${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${deployGroup} + ${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${defaultGroup} + ${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${defaultGroup} ${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions} ${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions} - ${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${deployGroup} - ${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${deployGroup} - ${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${deployGroup} - ${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${deployGroup} - ${FAILED} | ${true} | ${deployDetail} | ${'Failed to deploy to'} | ${deployGroup} - ${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${deployGroup} - ${FAILED} | ${false} | ${deployDetail} | ${'Failed to deploy to'} | ${noActions} + ${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup} + ${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${defaultGroup} + ${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup} + ${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${defaultGroup} + ${FAILED} | ${true} | ${retryDetail} | ${'Failed to deploy to'} | ${manualRedeployGroup} + ${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${defaultGroup} + ${FAILED} | ${false} | ${retryDetail} | ${'Failed to deploy to'} | ${noActions} ${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions} - ${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deployment to'} | ${deployGroup} - ${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${deployGroup} + ${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deployment to'} | ${defaultGroup} + ${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${defaultGroup} ${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deployment to'} | ${noActions} ${CANCELED} | ${false} | ${noDetails} | ${'Canceled deployment to'} | ${noActions} `( @@ -112,7 +115,7 @@ describe('Deployment component', () => { if (actionButtons.length > 0) { describe('renders the expected button group', () => { actionButtons.forEach(button => { - it(`renders ${button.name}`, () => { + it(`renders ${button}`, () => { expect(wrapper.find(button).exists()).toBe(true); }); }); @@ -121,8 +124,8 @@ describe('Deployment component', () => { if (actionButtons.length === 0) { describe('does not render the button group', () => { - [DeploymentViewButton, DeploymentStopButton].forEach(button => { - it(`does not render ${button.name}`, () => { + defaultGroup.forEach(button => { + it(`does not render ${button}`, () => { expect(wrapper.find(button).exists()).toBe(false); }); }); @@ -144,10 +147,6 @@ describe('Deployment component', () => { describe('hasExternalUrls', () => { describe('when deployment has both external_url_formatted and external_url', () => { - it('should return true', () => { - expect(wrapper.vm.hasExternalUrls).toEqual(true); - }); - it('should render the View Button', () => { expect(wrapper.find(DeploymentViewButton).exists()).toBe(true); }); @@ -163,10 +162,6 @@ describe('Deployment component', () => { }); }); - it('should return false', () => { - expect(wrapper.vm.hasExternalUrls).toEqual(false); - }); - it('should not render the View Button', () => { expect(wrapper.find(DeploymentViewButton).exists()).toBe(false); }); @@ -182,10 +177,6 @@ describe('Deployment component', () => { }); }); - it('should return false', () => { - expect(wrapper.vm.hasExternalUrls).toEqual(false); - }); - it('should not render the View Button', () => { expect(wrapper.find(DeploymentViewButton).exists()).toBe(false); }); diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js index 5e0f38459b0..a12757d4cce 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js +++ b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue'; -import deploymentMockData from './deployment_mock_data'; +import { deploymentMockData } from './deployment_mock_data'; const appButtonText = { text: 'View app', |