diff options
Diffstat (limited to 'spec/frontend/runner/components/cells/runner_actions_cell_spec.js')
-rw-r--r-- | spec/frontend/runner/components/cells/runner_actions_cell_spec.js | 350 |
1 files changed, 181 insertions, 169 deletions
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js index 2874bdbe280..95c212cb0a9 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -3,13 +3,17 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +import { captureException } from '~/runner/sentry_utils'; import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue'; +import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue'; import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql'; import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql'; import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql'; import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql'; -import { captureException } from '~/runner/sentry_utils'; import { runnersData } from '../../mock_data'; const mockRunner = runnersData.data.runners.nodes[0]; @@ -25,12 +29,16 @@ jest.mock('~/runner/sentry_utils'); describe('RunnerTypeCell', () => { let wrapper; + + const mockToastShow = jest.fn(); const runnerDeleteMutationHandler = jest.fn(); const runnerActionsUpdateMutationHandler = jest.fn(); const findEditBtn = () => wrapper.findByTestId('edit-runner'); const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner'); + const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal); const findDeleteBtn = () => wrapper.findByTestId('delete-runner'); + const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value; const createComponent = ({ active = true } = {}, options) => { wrapper = extendedWrapper( @@ -38,6 +46,7 @@ describe('RunnerTypeCell', () => { propsData: { runner: { id: mockRunner.id, + shortSha: mockRunner.shortSha, adminUrl: mockRunner.adminUrl, active, }, @@ -47,6 +56,15 @@ describe('RunnerTypeCell', () => { [runnerDeleteMutation, runnerDeleteMutationHandler], [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler], ]), + directives: { + GlTooltip: createMockDirective(), + GlModal: createMockDirective(), + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, ...options, }), ); @@ -72,197 +90,85 @@ describe('RunnerTypeCell', () => { }); afterEach(() => { + mockToastShow.mockReset(); runnerDeleteMutationHandler.mockReset(); runnerActionsUpdateMutationHandler.mockReset(); wrapper.destroy(); }); - it('Displays the runner edit link with the correct href', () => { - createComponent(); - - expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl); - }); - - describe.each` - state | label | icon | isActive | newActiveValue - ${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false} - ${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true} - `('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => { - beforeEach(() => { - createComponent({ active: isActive }); - }); - - it(`Displays a ${icon} button`, () => { - expect(findToggleActiveBtn().props('loading')).toBe(false); - expect(findToggleActiveBtn().props('icon')).toBe(icon); - expect(findToggleActiveBtn().attributes('title')).toBe(label); - expect(findToggleActiveBtn().attributes('aria-label')).toBe(label); - }); - - it(`After clicking the ${icon} button, the button has a loading state`, async () => { - await findToggleActiveBtn().vm.$emit('click'); - - expect(findToggleActiveBtn().props('loading')).toBe(true); - }); - - it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => { - await findToggleActiveBtn().vm.$emit('click'); + describe('Edit Action', () => { + it('Displays the runner edit link with the correct href', () => { + createComponent(); - expect(findToggleActiveBtn().attributes('title')).toBe(''); - expect(findToggleActiveBtn().attributes('aria-label')).toBe(''); + expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl); }); + }); - describe(`When clicking on the ${icon} button`, () => { - it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => { - expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0); - - await findToggleActiveBtn().vm.$emit('click'); + describe('Toggle active action', () => { + describe.each` + state | label | icon | isActive | newActiveValue + ${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false} + ${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true} + `('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => { + beforeEach(() => { + createComponent({ active: isActive }); + }); - expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1); - expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({ - input: { - id: mockRunner.id, - active: newActiveValue, - }, - }); + it(`Displays a ${icon} button`, () => { + expect(findToggleActiveBtn().props('loading')).toBe(false); + expect(findToggleActiveBtn().props('icon')).toBe(icon); + expect(getTooltip(findToggleActiveBtn())).toBe(label); + expect(findToggleActiveBtn().attributes('aria-label')).toBe(label); }); - it('The button does not have a loading state after the mutation occurs', async () => { + it(`After clicking the ${icon} button, the button has a loading state`, async () => { await findToggleActiveBtn().vm.$emit('click'); expect(findToggleActiveBtn().props('loading')).toBe(true); - - await waitForPromises(); - - expect(findToggleActiveBtn().props('loading')).toBe(false); }); - }); - describe('When update fails', () => { - describe('On a network error', () => { - const mockErrorMsg = 'Update error!'; - - beforeEach(async () => { - runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - - await findToggleActiveBtn().vm.$emit('click'); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(`Network error: ${mockErrorMsg}`), - component: 'RunnerActionsCell', - }); - }); + it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => { + await findToggleActiveBtn().vm.$emit('click'); - it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); - }); + expect(getTooltip(findToggleActiveBtn())).toBe(''); + expect(findToggleActiveBtn().attributes('aria-label')).toBe(''); }); - describe('On a validation error', () => { - const mockErrorMsg = 'Runner not found!'; - const mockErrorMsg2 = 'User not allowed!'; - - beforeEach(async () => { - runnerActionsUpdateMutationHandler.mockResolvedValue({ - data: { - runnerUpdate: { - runner: mockRunner, - errors: [mockErrorMsg, mockErrorMsg2], - }, - }, - }); + describe(`When clicking on the ${icon} button`, () => { + it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => { + expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0); await findToggleActiveBtn().vm.$emit('click'); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`), - component: 'RunnerActionsCell', - }); - }); - it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); - }); - }); - }); - }); - - describe('When the user clicks a runner', () => { - beforeEach(() => { - jest.spyOn(window, 'confirm'); - - createComponent(); - }); - - afterEach(() => { - window.confirm.mockRestore(); - }); - - describe('When the user confirms deletion', () => { - beforeEach(async () => { - window.confirm.mockReturnValue(true); - await findDeleteBtn().vm.$emit('click'); - }); - - it('The user sees a confirmation alert', () => { - expect(window.confirm).toHaveBeenCalledTimes(1); - expect(window.confirm).toHaveBeenCalledWith(expect.any(String)); - }); - - it('The delete mutation is called correctly', () => { - expect(runnerDeleteMutationHandler).toHaveBeenCalledTimes(1); - expect(runnerDeleteMutationHandler).toHaveBeenCalledWith({ - input: { id: mockRunner.id }, - }); - }); - - it('When delete mutation is called, current runners are refetched', async () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate'); - - await findDeleteBtn().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: runnerDeleteMutation, - variables: { + expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1); + expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({ input: { id: mockRunner.id, + active: newActiveValue, }, - }, - awaitRefetchQueries: true, - refetchQueries: [getRunnersQueryName, getGroupRunnersQueryName], + }); }); - }); - - it('The delete button does not have a loading state', () => { - expect(findDeleteBtn().props('loading')).toBe(false); - expect(findDeleteBtn().attributes('title')).toBe('Remove'); - }); - it('After the delete button is clicked, loading state is shown', async () => { - await findDeleteBtn().vm.$emit('click'); + it('The button does not have a loading state after the mutation occurs', async () => { + await findToggleActiveBtn().vm.$emit('click'); - expect(findDeleteBtn().props('loading')).toBe(true); - }); + expect(findToggleActiveBtn().props('loading')).toBe(true); - it('After the delete button is clicked, stale tooltip is removed', async () => { - await findDeleteBtn().vm.$emit('click'); + await waitForPromises(); - expect(findDeleteBtn().attributes('title')).toBe(''); + expect(findToggleActiveBtn().props('loading')).toBe(false); + }); }); - describe('When delete fails', () => { + describe('When update fails', () => { describe('On a network error', () => { - const mockErrorMsg = 'Delete error!'; + const mockErrorMsg = 'Update error!'; beforeEach(async () => { - runnerDeleteMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); + runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - await findDeleteBtn().vm.$emit('click'); + await findToggleActiveBtn().vm.$emit('click'); }); it('error is reported to sentry', () => { @@ -282,15 +188,16 @@ describe('RunnerTypeCell', () => { const mockErrorMsg2 = 'User not allowed!'; beforeEach(async () => { - runnerDeleteMutationHandler.mockResolvedValue({ + runnerActionsUpdateMutationHandler.mockResolvedValue({ data: { - runnerDelete: { + runnerUpdate: { + runner: mockRunner, errors: [mockErrorMsg, mockErrorMsg2], }, }, }); - await findDeleteBtn().vm.$emit('click'); + await findToggleActiveBtn().vm.$emit('click'); }); it('error is reported to sentry', () => { @@ -306,24 +213,129 @@ describe('RunnerTypeCell', () => { }); }); }); + }); - describe('When the user does not confirm deletion', () => { - beforeEach(async () => { - window.confirm.mockReturnValue(false); - await findDeleteBtn().vm.$emit('click'); + describe('Delete action', () => { + beforeEach(() => { + createComponent( + {}, + { + stubs: { RunnerDeleteModal }, + }, + ); + }); + + it('Delete button opens delete modal', () => { + const modalId = getBinding(findDeleteBtn().element, 'gl-modal').value; + + expect(findRunnerDeleteModal().attributes('modal-id')).toBeDefined(); + expect(findRunnerDeleteModal().attributes('modal-id')).toBe(modalId); + }); + + it('Delete modal shows the runner name', () => { + expect(findRunnerDeleteModal().props('runnerName')).toBe( + `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`, + ); + }); + it('The delete button does not have a loading icon', () => { + expect(findDeleteBtn().props('loading')).toBe(false); + expect(getTooltip(findDeleteBtn())).toBe('Delete runner'); + }); + + it('When delete mutation is called, current runners are refetched', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + + findRunnerDeleteModal().vm.$emit('primary'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: runnerDeleteMutation, + variables: { + input: { + id: mockRunner.id, + }, + }, + awaitRefetchQueries: true, + refetchQueries: [getRunnersQueryName, getGroupRunnersQueryName], }); + }); - it('The user sees a confirmation alert', () => { - expect(window.confirm).toHaveBeenCalledTimes(1); + describe('When delete is clicked', () => { + beforeEach(() => { + findRunnerDeleteModal().vm.$emit('primary'); }); - it('The delete mutation is not called', () => { - expect(runnerDeleteMutationHandler).toHaveBeenCalledTimes(0); + it('The delete mutation is called correctly', () => { + expect(runnerDeleteMutationHandler).toHaveBeenCalledTimes(1); + expect(runnerDeleteMutationHandler).toHaveBeenCalledWith({ + input: { id: mockRunner.id }, + }); }); - it('The delete button does not have a loading state', () => { - expect(findDeleteBtn().props('loading')).toBe(false); - expect(findDeleteBtn().attributes('title')).toBe('Remove'); + it('The delete button has a loading icon', () => { + expect(findDeleteBtn().props('loading')).toBe(true); + expect(getTooltip(findDeleteBtn())).toBe(''); + }); + + it('The toast notification is shown', () => { + expect(mockToastShow).toHaveBeenCalledTimes(1); + expect(mockToastShow).toHaveBeenCalledWith( + expect.stringContaining(`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`), + ); + }); + }); + + describe('When delete fails', () => { + describe('On a network error', () => { + const mockErrorMsg = 'Delete error!'; + + beforeEach(() => { + runnerDeleteMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); + + findRunnerDeleteModal().vm.$emit('primary'); + }); + + it('error is reported to sentry', () => { + expect(captureException).toHaveBeenCalledWith({ + error: new Error(`Network error: ${mockErrorMsg}`), + component: 'RunnerActionsCell', + }); + }); + + it('error is shown to the user', () => { + expect(createFlash).toHaveBeenCalledTimes(1); + }); + + it('toast notification is not shown', () => { + expect(mockToastShow).not.toHaveBeenCalled(); + }); + }); + + describe('On a validation error', () => { + const mockErrorMsg = 'Runner not found!'; + const mockErrorMsg2 = 'User not allowed!'; + + beforeEach(() => { + runnerDeleteMutationHandler.mockResolvedValue({ + data: { + runnerDelete: { + errors: [mockErrorMsg, mockErrorMsg2], + }, + }, + }); + + findRunnerDeleteModal().vm.$emit('primary'); + }); + + it('error is reported to sentry', () => { + expect(captureException).toHaveBeenCalledWith({ + error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`), + component: 'RunnerActionsCell', + }); + }); + + it('error is shown to the user', () => { + expect(createFlash).toHaveBeenCalledTimes(1); + }); }); }); }); |