diff options
Diffstat (limited to 'spec/frontend/ci/runner/components/runner_pause_button_spec.js')
-rw-r--r-- | spec/frontend/ci/runner/components/runner_pause_button_spec.js | 282 |
1 files changed, 86 insertions, 196 deletions
diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js index 1ea870e004a..f1ceecd4ae4 100644 --- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js @@ -1,13 +1,7 @@ -import Vue, { nextTick } from 'vue'; import { GlButton } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; +import { stubComponent } from 'helpers/stub_component'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; -import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; -import waitForPromises from 'helpers/wait_for_promises'; -import { captureException } from '~/ci/runner/sentry_utils'; -import { createAlert } from '~/alert'; import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, @@ -16,244 +10,140 @@ import { } from '~/ci/runner/constants'; import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue'; -import { allRunnersData } from '../mock_data'; - -const mockRunner = allRunnersData.data.runners.nodes[0]; - -Vue.use(VueApollo); - -jest.mock('~/alert'); -jest.mock('~/ci/runner/sentry_utils'); +import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue'; describe('RunnerPauseButton', () => { let wrapper; - let runnerTogglePausedHandler; const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value; + const findRunnerPauseAction = () => wrapper.findComponent(RunnerPauseAction); const findBtn = () => wrapper.findComponent(GlButton); - const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { - const { runner, ...propsData } = props; - + const createComponent = ({ + props = {}, + loading, + onClick = jest.fn(), + mountFn = shallowMountExtended, + } = {}) => { wrapper = mountFn(RunnerPauseButton, { propsData: { - runner: { - id: mockRunner.id, - paused: mockRunner.paused, - ...runner, - }, - ...propsData, + runner: {}, + ...props, }, - apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]), directives: { GlTooltip: createMockDirective('gl-tooltip'), }, + stubs: { + RunnerPauseAction: stubComponent(RunnerPauseAction, { + render() { + return this.$scopedSlots.default({ + loading, + onClick, + }); + }, + }), + }, }); }; - const clickAndWait = async () => { - findBtn().vm.$emit('click'); - await waitForPromises(); - }; - beforeEach(() => { - runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => { - return Promise.resolve({ - data: { - runnerUpdate: { - runner: { - id: input.id, - paused: !input.paused, - }, - errors: [], - }, - }, - }); - }); - createComponent(); }); - describe('Pause/Resume action', () => { + describe('Pause/Resume button', () => { describe.each` - runnerState | icon | content | tooltip | isPaused | newPausedValue - ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${true} | ${false} - ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${false} | ${true} - `('When the runner is $runnerState', ({ icon, content, tooltip, isPaused, newPausedValue }) => { - beforeEach(() => { - createComponent({ - props: { - runner: { - paused: isPaused, + runnerState | paused | expectedIcon | expectedContent | expectedTooltip + ${'paused'} | ${true} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} + ${'active'} | ${false} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} + `( + 'When the runner is $runnerState', + ({ paused, expectedIcon, expectedContent, expectedTooltip }) => { + beforeEach(() => { + createComponent({ + props: { + runner: { paused }, }, - }, - }); - }); - - it(`Displays a ${icon} button`, () => { - expect(findBtn().props('loading')).toBe(false); - expect(findBtn().props('icon')).toBe(icon); - }); - - it('Displays button content', () => { - expect(findBtn().text()).toBe(content); - expect(getTooltip()).toBe(tooltip); - }); - - it('Does not display redundant text for screen readers', () => { - expect(findBtn().attributes('aria-label')).toBe(undefined); - }); - - describe(`Before the ${icon} button is clicked`, () => { - it('The mutation has not been called', () => { - expect(runnerTogglePausedHandler).not.toHaveBeenCalled(); + }); }); - }); - - describe(`Immediately after the ${icon} button is clicked`, () => { - const setup = async () => { - findBtn().vm.$emit('click'); - await nextTick(); - }; - it('The button has a loading state', async () => { - await setup(); - - expect(findBtn().props('loading')).toBe(true); + it(`Displays a ${expectedIcon} button`, () => { + expect(findBtn().props('loading')).toBe(false); + expect(findBtn().props('icon')).toBe(expectedIcon); }); - it('The stale tooltip is removed', async () => { - await setup(); - - expect(getTooltip()).toBe(''); + it('Displays button content', () => { + expect(findBtn().text()).toBe(expectedContent); + expect(getTooltip()).toBe(expectedTooltip); }); - }); - describe(`After clicking on the ${icon} button`, () => { - beforeEach(async () => { - await clickAndWait(); + it('Does not display redundant text for screen readers', () => { + expect(findBtn().attributes('aria-label')).toBe(undefined); }); + }, + ); + }); - it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => { - expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1); - expect(runnerTogglePausedHandler).toHaveBeenCalledWith({ - input: { - id: mockRunner.id, - paused: newPausedValue, + describe('Compact button', () => { + describe.each` + runnerState | paused | expectedIcon | expectedContent | expectedTooltip + ${'paused'} | ${true} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} + ${'active'} | ${false} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} + `( + 'When the runner is $runnerState', + ({ paused, expectedIcon, expectedContent, expectedTooltip }) => { + beforeEach(() => { + createComponent({ + props: { + runner: { paused }, + compact: true, }, + mountFn: mountExtended, }); }); - it('The button does not have a loading state', () => { + it(`Displays a ${expectedIcon} button`, () => { expect(findBtn().props('loading')).toBe(false); + expect(findBtn().props('icon')).toBe(expectedIcon); }); - it('The button emits toggledPaused', () => { - expect(wrapper.emitted('toggledPaused')).toHaveLength(1); - }); - }); - - describe('When update fails', () => { - describe('On a network error', () => { - const mockErrorMsg = 'Update error!'; - - beforeEach(async () => { - runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - - await clickAndWait(); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(mockErrorMsg), - component: 'RunnerPauseButton', - }); - }); + it('Displays button content', () => { + expect(findBtn().text()).toBe(''); + // Note: Use <template v-if> to ensure rendering a + // text-less button. Ensure we don't send even empty an + // content slot to prevent a distorted/rectangular button. + expect(wrapper.find('.gl-button-text').exists()).toBe(false); - it('error is shown to the user', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - }); + expect(getTooltip()).toBe(expectedTooltip); }); - describe('On a validation error', () => { - const mockErrorMsg = 'Runner not found!'; - const mockErrorMsg2 = 'User not allowed!'; - - beforeEach(async () => { - runnerTogglePausedHandler.mockResolvedValueOnce({ - data: { - runnerUpdate: { - runner: { - id: mockRunner.id, - paused: isPaused, - }, - errors: [mockErrorMsg, mockErrorMsg2], - }, - }, - }); - - await clickAndWait(); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`), - component: 'RunnerPauseButton', - }); - }); - - it('error is shown to the user', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - }); + it('Does not display redundant text for screen readers', () => { + expect(findBtn().attributes('aria-label')).toBe(expectedContent); }); - }); - }); + }, + ); }); - describe('When displaying a compact button for an active runner', () => { - beforeEach(() => { - createComponent({ - props: { - runner: { - paused: false, - }, - compact: true, - }, - mountFn: mountExtended, - }); - }); - - it('Displays no text', () => { - expect(findBtn().text()).toBe(''); + it('Shows loading state', () => { + createComponent({ loading: true }); - // Note: Use <template v-if> to ensure rendering a - // text-less button. Ensure we don't send even empty an - // content slot to prevent a distorted/rectangular button. - expect(wrapper.find('.gl-button-text').exists()).toBe(false); - }); + expect(findBtn().props('loading')).toBe(true); + expect(getTooltip()).toBe(''); + }); - it('Display correctly for screen readers', () => { - expect(findBtn().attributes('aria-label')).toBe(I18N_PAUSE); - expect(getTooltip()).toBe(I18N_PAUSE_TOOLTIP); - }); + it('Triggers action', () => { + const mockOnClick = jest.fn(); - describe('Immediately after the button is clicked', () => { - const setup = async () => { - findBtn().vm.$emit('click'); - await nextTick(); - }; + createComponent({ onClick: mockOnClick }); + findBtn().vm.$emit('click'); - it('The button has a loading state', async () => { - await setup(); + expect(mockOnClick).toHaveBeenCalled(); + }); - expect(findBtn().props('loading')).toBe(true); - }); + it('Emits toggledPaused when done', () => { + createComponent(); - it('The stale tooltip is removed', async () => { - await setup(); + findRunnerPauseAction().vm.$emit('done'); - expect(getTooltip()).toBe(''); - }); - }); + expect(wrapper.emitted('toggledPaused')).toHaveLength(1); }); }); |