diff options
Diffstat (limited to 'spec/frontend/runner/components')
19 files changed, 707 insertions, 417 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 5aa3879ac3e..2874bdbe280 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -8,12 +8,11 @@ import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.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 runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql'; +import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; -import { runnersData, runnerData } from '../../mock_data'; +import { runnersData } from '../../mock_data'; const mockRunner = runnersData.data.runners.nodes[0]; -const mockRunnerDetails = runnerData.data.runner; const getRunnersQueryName = getRunnersQuery.definitions[0].name.value; const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value; @@ -27,7 +26,7 @@ jest.mock('~/runner/sentry_utils'); describe('RunnerTypeCell', () => { let wrapper; const runnerDeleteMutationHandler = jest.fn(); - const runnerUpdateMutationHandler = jest.fn(); + const runnerActionsUpdateMutationHandler = jest.fn(); const findEditBtn = () => wrapper.findByTestId('edit-runner'); const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner'); @@ -46,7 +45,7 @@ describe('RunnerTypeCell', () => { localVue, apolloProvider: createMockApollo([ [runnerDeleteMutation, runnerDeleteMutationHandler], - [runnerUpdateMutation, runnerUpdateMutationHandler], + [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler], ]), ...options, }), @@ -62,10 +61,10 @@ describe('RunnerTypeCell', () => { }, }); - runnerUpdateMutationHandler.mockResolvedValue({ + runnerActionsUpdateMutationHandler.mockResolvedValue({ data: { runnerUpdate: { - runner: mockRunnerDetails, + runner: mockRunner, errors: [], }, }, @@ -74,7 +73,7 @@ describe('RunnerTypeCell', () => { afterEach(() => { runnerDeleteMutationHandler.mockReset(); - runnerUpdateMutationHandler.mockReset(); + runnerActionsUpdateMutationHandler.mockReset(); wrapper.destroy(); }); @@ -116,12 +115,12 @@ describe('RunnerTypeCell', () => { describe(`When clicking on the ${icon} button`, () => { it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => { - expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(0); + expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0); await findToggleActiveBtn().vm.$emit('click'); - expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(1); - expect(runnerUpdateMutationHandler).toHaveBeenCalledWith({ + expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1); + expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({ input: { id: mockRunner.id, active: newActiveValue, @@ -145,7 +144,7 @@ describe('RunnerTypeCell', () => { const mockErrorMsg = 'Update error!'; beforeEach(async () => { - runnerUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); + runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); await findToggleActiveBtn().vm.$emit('click'); }); @@ -167,10 +166,10 @@ describe('RunnerTypeCell', () => { const mockErrorMsg2 = 'User not allowed!'; beforeEach(async () => { - runnerUpdateMutationHandler.mockResolvedValue({ + runnerActionsUpdateMutationHandler.mockResolvedValue({ data: { runnerUpdate: { - runner: runnerData.data.runner, + runner: mockRunner, errors: [mockErrorMsg, mockErrorMsg2], }, }, diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js new file mode 100644 index 00000000000..20a1cdf7236 --- /dev/null +++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js @@ -0,0 +1,69 @@ +import { GlBadge } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue'; +import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants'; + +describe('RunnerTypeCell', () => { + let wrapper; + + const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i); + + const createComponent = ({ runner = {} } = {}) => { + wrapper = mount(RunnerStatusCell, { + propsData: { + runner: { + runnerType: INSTANCE_TYPE, + active: true, + status: STATUS_ONLINE, + ...runner, + }, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays online status', () => { + createComponent(); + + expect(wrapper.text()).toMatchInterpolatedText('online'); + expect(findBadgeAt(0).text()).toBe('online'); + }); + + it('Displays offline status', () => { + createComponent({ + runner: { + status: STATUS_OFFLINE, + }, + }); + + expect(wrapper.text()).toMatchInterpolatedText('offline'); + expect(findBadgeAt(0).text()).toBe('offline'); + }); + + it('Displays paused status', () => { + createComponent({ + runner: { + active: false, + status: STATUS_ONLINE, + }, + }); + + expect(wrapper.text()).toMatchInterpolatedText('online paused'); + + expect(findBadgeAt(0).text()).toBe('online'); + expect(findBadgeAt(1).text()).toBe('paused'); + }); + + it('Is empty when data is missing', () => { + createComponent({ + runner: { + status: null, + }, + }); + + expect(wrapper.text()).toBe(''); + }); +}); diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js index 1c9282e0acd..b6d957d27ea 100644 --- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js @@ -1,5 +1,6 @@ -import { mount } from '@vue/test-utils'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue'; +import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; const mockId = '1'; const mockShortSha = '2P6oDVDm'; @@ -8,13 +9,17 @@ const mockDescription = 'runner-1'; describe('RunnerTypeCell', () => { let wrapper; - const createComponent = (options) => { - wrapper = mount(RunnerSummaryCell, { + const findLockIcon = () => wrapper.findByTestId('lock-icon'); + + const createComponent = (runner, options) => { + wrapper = mountExtended(RunnerSummaryCell, { propsData: { runner: { id: `gid://gitlab/Ci::Runner/${mockId}`, shortSha: mockShortSha, description: mockDescription, + runnerType: INSTANCE_TYPE, + ...runner, }, }, ...options, @@ -33,6 +38,23 @@ describe('RunnerTypeCell', () => { expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`); }); + it('Displays the runner type', () => { + expect(wrapper.text()).toContain('shared'); + }); + + it('Does not display the locked icon', () => { + expect(findLockIcon().exists()).toBe(false); + }); + + it('Displays the locked icon for locked runners', () => { + createComponent({ + runnerType: PROJECT_TYPE, + locked: true, + }); + + expect(findLockIcon().exists()).toBe(true); + }); + it('Displays the runner description', () => { expect(wrapper.text()).toContain(mockDescription); }); @@ -40,11 +62,14 @@ describe('RunnerTypeCell', () => { it('Displays a custom slot', () => { const slotContent = 'My custom runner summary'; - createComponent({ - slots: { - 'runner-name': slotContent, + createComponent( + {}, + { + slots: { + 'runner-name': slotContent, + }, }, - }); + ); expect(wrapper.text()).toContain(slotContent); }); diff --git a/spec/frontend/runner/components/cells/runner_type_cell_spec.js b/spec/frontend/runner/components/cells/runner_type_cell_spec.js deleted file mode 100644 index 48958a282fc..00000000000 --- a/spec/frontend/runner/components/cells/runner_type_cell_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import RunnerTypeCell from '~/runner/components/cells/runner_type_cell.vue'; -import { INSTANCE_TYPE } from '~/runner/constants'; - -describe('RunnerTypeCell', () => { - let wrapper; - - const findBadges = () => wrapper.findAllComponents(GlBadge); - - const createComponent = ({ runner = {} } = {}) => { - wrapper = mount(RunnerTypeCell, { - propsData: { - runner: { - runnerType: INSTANCE_TYPE, - active: true, - locked: false, - ...runner, - }, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays the runner type', () => { - createComponent(); - - expect(findBadges()).toHaveLength(1); - expect(findBadges().at(0).text()).toBe('shared'); - }); - - it('Displays locked and paused states', () => { - createComponent({ - runner: { - active: false, - locked: true, - }, - }); - - expect(findBadges()).toHaveLength(3); - expect(findBadges().at(0).text()).toBe('shared'); - expect(findBadges().at(1).text()).toBe('locked'); - expect(findBadges().at(2).text()).toBe('paused'); - }); -}); diff --git a/spec/frontend/runner/components/helpers/masked_value_spec.js b/spec/frontend/runner/components/helpers/masked_value_spec.js deleted file mode 100644 index f87315057ec..00000000000 --- a/spec/frontend/runner/components/helpers/masked_value_spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import MaskedValue from '~/runner/components/helpers/masked_value.vue'; - -const mockSecret = '01234567890'; -const mockMasked = '***********'; - -describe('MaskedValue', () => { - let wrapper; - - const findButton = () => wrapper.findComponent(GlButton); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(MaskedValue, { - propsData: { - value: mockSecret, - ...props, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays masked value by default', () => { - expect(wrapper.text()).toBe(mockMasked); - }); - - describe('When the icon is clicked', () => { - beforeEach(() => { - findButton().vm.$emit('click'); - }); - - it('Displays the actual value', () => { - expect(wrapper.text()).toBe(mockSecret); - expect(wrapper.text()).not.toBe(mockMasked); - }); - - it('When user clicks again, displays masked value', async () => { - await findButton().vm.$emit('click'); - - expect(wrapper.text()).toBe(mockMasked); - expect(wrapper.text()).not.toBe(mockSecret); - }); - }); -}); diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js new file mode 100644 index 00000000000..d18d2bec18e --- /dev/null +++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js @@ -0,0 +1,169 @@ +import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui'; +import { createLocalVue, mount, shallowMount, createWrapper } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; + +import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; +import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue'; + +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; + +import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql'; +import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql'; + +import { + mockGraphqlRunnerPlatforms, + mockGraphqlInstructions, +} from 'jest/vue_shared/components/runner_instructions/mock_data'; + +const mockToken = '0123456789'; +const maskToken = '**********'; + +describe('RegistrationDropdown', () => { + let wrapper; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + + const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem); + const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm); + const findTokenResetDropdownItem = () => + wrapper.findComponent(RegistrationTokenResetDropdownItem); + + const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked'); + + const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => { + wrapper = extendedWrapper( + mountFn(RegistrationDropdown, { + propsData: { + registrationToken: mockToken, + type: INSTANCE_TYPE, + ...props, + }, + ...options, + }), + ); + }; + + it.each` + type | text + ${INSTANCE_TYPE} | ${'Register an instance runner'} + ${GROUP_TYPE} | ${'Register a group runner'} + ${PROJECT_TYPE} | ${'Register a project runner'} + `('Dropdown text for type $type is "$text"', () => { + createComponent({ props: { type: INSTANCE_TYPE } }, mount); + + expect(wrapper.text()).toContain('Register an instance runner'); + }); + + it('Passes attributes to the dropdown component', () => { + createComponent({ attrs: { right: true } }); + + expect(findDropdown().attributes()).toMatchObject({ right: 'true' }); + }); + + describe('Instructions dropdown item', () => { + it('Displays "Show runner" dropdown item', () => { + createComponent(); + + expect(findRegistrationInstructionsDropdownItem().text()).toBe( + 'Show runner installation and registration instructions', + ); + }); + + describe('When the dropdown item is clicked', () => { + const localVue = createLocalVue(); + localVue.use(VueApollo); + + const requestHandlers = [ + [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)], + [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)], + ]; + + const findModalInBody = () => + createWrapper(document.body).find('[data-testid="runner-instructions-modal"]'); + + beforeEach(() => { + createComponent( + { + localVue, + // Mock load modal contents from API + apolloProvider: createMockApollo(requestHandlers), + // Use `attachTo` to find the modal + attachTo: document.body, + }, + mount, + ); + + findRegistrationInstructionsDropdownItem().trigger('click'); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('opens the modal with contents', () => { + const modalText = findModalInBody() + .text() + .replace(/[\n\t\s]+/g, ' '); + + expect(modalText).toContain('Install a runner'); + + // Environment selector + expect(modalText).toContain('Environment'); + expect(modalText).toContain('Linux macOS Windows Docker Kubernetes'); + + // Architecture selector + expect(modalText).toContain('Architecture'); + expect(modalText).toContain('amd64 amd64 386 arm arm64'); + + expect(modalText).toContain('Download and install binary'); + }); + }); + }); + + describe('Registration token', () => { + it('Displays dropdown form for the registration token', () => { + createComponent(); + + expect(findTokenDropdownItem().exists()).toBe(true); + }); + + it('Displays masked value by default', () => { + createComponent({}, mount); + + expect(findTokenDropdownItem().text()).toMatchInterpolatedText( + `Registration token ${maskToken}`, + ); + }); + }); + + describe('Reset token item', () => { + it('Displays registration token reset item', () => { + createComponent(); + + expect(findTokenResetDropdownItem().exists()).toBe(true); + }); + + it.each([INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE])('Set up token reset for %s', (type) => { + createComponent({ props: { type } }); + + expect(findTokenResetDropdownItem().props('type')).toBe(type); + }); + }); + + it('Updates the token when it gets reset', async () => { + createComponent({}, mount); + + const newToken = 'mock1'; + + findTokenResetDropdownItem().vm.$emit('tokenReset', newToken); + findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() }); + await nextTick(); + + expect(findTokenDropdownItem().text()).toMatchInterpolatedText( + `Registration token ${newToken}`, + ); + }); +}); diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js index 8b360b88417..0d002c272b4 100644 --- a/spec/frontend/runner/components/runner_registration_token_reset_spec.js +++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js @@ -1,11 +1,11 @@ -import { GlButton } from '@gitlab/ui'; +import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash, { FLASH_TYPES } from '~/flash'; -import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue'; +import createFlash from '~/flash'; +import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; @@ -15,17 +15,20 @@ jest.mock('~/runner/sentry_utils'); const localVue = createLocalVue(); localVue.use(VueApollo); +localVue.use(GlToast); const mockNewToken = 'NEW_TOKEN'; -describe('RunnerRegistrationTokenReset', () => { +describe('RegistrationTokenResetDropdownItem', () => { let wrapper; let runnersRegistrationTokenResetMutationHandler; + let showToast; - const findButton = () => wrapper.findComponent(GlButton); + const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const createComponent = ({ props, provide = {} } = {}) => { - wrapper = shallowMount(RunnerRegistrationTokenReset, { + wrapper = shallowMount(RegistrationTokenResetDropdownItem, { localVue, provide, propsData: { @@ -36,6 +39,8 @@ describe('RunnerRegistrationTokenReset', () => { [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler], ]), }); + + showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; }; beforeEach(() => { @@ -58,7 +63,7 @@ describe('RunnerRegistrationTokenReset', () => { }); it('Displays reset button', () => { - expect(findButton().exists()).toBe(true); + expect(findDropdownItem().exists()).toBe(true); }); describe('On click and confirmation', () => { @@ -78,7 +83,8 @@ describe('RunnerRegistrationTokenReset', () => { }); window.confirm.mockReturnValueOnce(true); - findButton().vm.$emit('click'); + + findDropdownItem().trigger('click'); await waitForPromises(); }); @@ -95,14 +101,13 @@ describe('RunnerRegistrationTokenReset', () => { }); it('does not show a loading state', () => { - expect(findButton().props('loading')).toBe(false); + expect(findLoadingIcon().exists()).toBe(false); }); it('shows confirmation', () => { - expect(createFlash).toHaveBeenLastCalledWith({ - message: expect.stringContaining('registration token generated'), - type: FLASH_TYPES.SUCCESS, - }); + expect(showToast).toHaveBeenLastCalledWith( + expect.stringContaining('registration token generated'), + ); }); }); }); @@ -110,7 +115,7 @@ describe('RunnerRegistrationTokenReset', () => { describe('On click without confirmation', () => { beforeEach(async () => { window.confirm.mockReturnValueOnce(false); - findButton().vm.$emit('click'); + findDropdownItem().vm.$emit('click'); await waitForPromises(); }); @@ -123,11 +128,11 @@ describe('RunnerRegistrationTokenReset', () => { }); it('does not show a loading state', () => { - expect(findButton().props('loading')).toBe(false); + expect(findLoadingIcon().exists()).toBe(false); }); it('does not shows confirmation', () => { - expect(createFlash).not.toHaveBeenCalled(); + expect(showToast).not.toHaveBeenCalled(); }); }); @@ -138,7 +143,7 @@ describe('RunnerRegistrationTokenReset', () => { runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); window.confirm.mockReturnValueOnce(true); - findButton().vm.$emit('click'); + findDropdownItem().trigger('click'); await waitForPromises(); expect(createFlash).toHaveBeenLastCalledWith({ @@ -164,7 +169,7 @@ describe('RunnerRegistrationTokenReset', () => { }); window.confirm.mockReturnValueOnce(true); - findButton().vm.$emit('click'); + findDropdownItem().trigger('click'); await waitForPromises(); expect(createFlash).toHaveBeenLastCalledWith({ @@ -180,10 +185,10 @@ describe('RunnerRegistrationTokenReset', () => { describe('Immediately after click', () => { it('shows loading state', async () => { window.confirm.mockReturnValue(true); - findButton().vm.$emit('click'); + findDropdownItem().trigger('click'); await nextTick(); - expect(findButton().props('loading')).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js new file mode 100644 index 00000000000..f53ae165344 --- /dev/null +++ b/spec/frontend/runner/components/registration/registration_token_spec.js @@ -0,0 +1,109 @@ +import { nextTick } from 'vue'; +import { GlToast } from '@gitlab/ui'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import RegistrationToken from '~/runner/components/registration/registration_token.vue'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; + +const mockToken = '01234567890'; +const mockMasked = '***********'; + +describe('RegistrationToken', () => { + let wrapper; + let stopPropagation; + let showToast; + + const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked'); + const findCopyButton = () => wrapper.findComponent(ModalCopyButton); + + const vueWithGlToast = () => { + const localVue = createLocalVue(); + localVue.use(GlToast); + return localVue; + }; + + const createComponent = ({ props = {}, withGlToast = true } = {}) => { + const localVue = withGlToast ? vueWithGlToast() : undefined; + + wrapper = extendedWrapper( + shallowMount(RegistrationToken, { + propsData: { + value: mockToken, + ...props, + }, + localVue, + }), + ); + + showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; + }; + + beforeEach(() => { + stopPropagation = jest.fn(); + + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays masked value by default', () => { + expect(wrapper.text()).toBe(mockMasked); + }); + + it('Displays button to reveal token', () => { + expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal'); + }); + + it('Can copy the original token value', () => { + expect(findCopyButton().props('text')).toBe(mockToken); + }); + + describe('When the reveal icon is clicked', () => { + beforeEach(() => { + findToggleMaskButton().vm.$emit('click', { stopPropagation }); + }); + + it('Click event is not propagated', async () => { + expect(stopPropagation).toHaveBeenCalledTimes(1); + }); + + it('Displays the actual value', () => { + expect(wrapper.text()).toBe(mockToken); + }); + + it('Can copy the original token value', () => { + expect(findCopyButton().props('text')).toBe(mockToken); + }); + + it('Displays button to mask token', () => { + expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to hide'); + }); + + it('When user clicks again, displays masked value', async () => { + findToggleMaskButton().vm.$emit('click', { stopPropagation }); + await nextTick(); + + expect(wrapper.text()).toBe(mockMasked); + expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal'); + }); + }); + + describe('When the copy to clipboard button is clicked', () => { + it('shows a copied message', () => { + findCopyButton().vm.$emit('success'); + + expect(showToast).toHaveBeenCalledTimes(1); + expect(showToast).toHaveBeenCalledWith('Registration token copied!'); + }); + + it('does not fail when toast is not defined', () => { + createComponent({ withGlToast: false }); + findCopyButton().vm.$emit('success'); + + // This block also tests for unhandled errors + expect(showToast).toBeNull(); + }); + }); +}); diff --git a/spec/frontend/runner/components/runner_contacted_state_badge_spec.js b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js new file mode 100644 index 00000000000..57a27f39826 --- /dev/null +++ b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js @@ -0,0 +1,86 @@ +import { GlBadge } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerContactedStateBadge from '~/runner/components/runner_contacted_state_badge.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_NOT_CONNECTED } from '~/runner/constants'; + +describe('RunnerTypeBadge', () => { + let wrapper; + + const findBadge = () => wrapper.findComponent(GlBadge); + const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip'); + + const createComponent = ({ runner = {} } = {}) => { + wrapper = shallowMount(RunnerContactedStateBadge, { + propsData: { + runner: { + contactedAt: '2021-01-01T00:00:00Z', + status: STATUS_ONLINE, + ...runner, + }, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + beforeEach(() => { + jest.useFakeTimers('modern'); + }); + + afterEach(() => { + jest.useFakeTimers('legacy'); + + wrapper.destroy(); + }); + + it('renders online state', () => { + jest.setSystemTime(new Date('2021-01-01T00:01:00Z')); + + createComponent(); + + expect(wrapper.text()).toBe('online'); + expect(findBadge().props('variant')).toBe('success'); + expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago'); + }); + + it('renders offline state', () => { + jest.setSystemTime(new Date('2021-01-02T00:00:00Z')); + + createComponent({ + runner: { + status: STATUS_OFFLINE, + }, + }); + + expect(wrapper.text()).toBe('offline'); + expect(findBadge().props('variant')).toBe('muted'); + expect(getTooltip().value).toBe( + 'No recent contact from this runner; last contact was 1 day ago', + ); + }); + + it('renders not connected state', () => { + createComponent({ + runner: { + contactedAt: null, + status: STATUS_NOT_CONNECTED, + }, + }); + + expect(wrapper.text()).toBe('not connected'); + expect(findBadge().props('variant')).toBe('muted'); + expect(getTooltip().value).toMatch('This runner has never connected'); + }); + + it('does not fail when data is missing', () => { + createComponent({ + runner: { + status: null, + }, + }); + + expect(wrapper.text()).toBe(''); + }); +}); diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js index 46948af1f28..9ea0955f2a1 100644 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js @@ -5,13 +5,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_ import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config'; import TagToken from '~/runner/components/search_tokens/tag_token.vue'; import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config'; -import { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config'; -import { - PARAM_KEY_STATUS, - PARAM_KEY_RUNNER_TYPE, - PARAM_KEY_TAG, - STATUS_ACTIVE, -} from '~/runner/constants'; +import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, INSTANCE_TYPE } from '~/runner/constants'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -31,6 +25,11 @@ describe('RunnerList', () => { ]; const mockActiveRunnersCount = 2; + const expectToHaveLastEmittedInput = (value) => { + const inputs = wrapper.emitted('input'); + expect(inputs[inputs.length - 1][0]).toEqual(value); + }; + const createComponent = ({ props = {}, options = {} } = {}) => { wrapper = extendedWrapper( shallowMount(RunnerFilteredSearchBar, { @@ -38,6 +37,7 @@ describe('RunnerList', () => { namespace: 'runners', tokens: [], value: { + runnerType: null, filters: [], sort: mockDefaultSort, }, @@ -86,7 +86,7 @@ describe('RunnerList', () => { it('sets tokens to the filtered search', () => { createComponent({ props: { - tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig], + tokens: [statusTokenConfig, tagTokenConfig], }, }); @@ -97,11 +97,6 @@ describe('RunnerList', () => { options: expect.any(Array), }), expect.objectContaining({ - type: PARAM_KEY_RUNNER_TYPE, - token: BaseToken, - options: expect.any(Array), - }), - expect.objectContaining({ type: PARAM_KEY_TAG, token: TagToken, }), @@ -123,6 +118,7 @@ describe('RunnerList', () => { createComponent({ props: { value: { + runnerType: INSTANCE_TYPE, sort: mockOtherSort, filters: mockFilters, }, @@ -142,30 +138,40 @@ describe('RunnerList', () => { .text(), ).toEqual('Last contact'); }); + + it('when the user sets a filter, the "search" preserves the other filters', () => { + findGlFilteredSearch().vm.$emit('input', mockFilters); + findGlFilteredSearch().vm.$emit('submit'); + + expectToHaveLastEmittedInput({ + runnerType: INSTANCE_TYPE, + filters: mockFilters, + sort: mockOtherSort, + pagination: { page: 1 }, + }); + }); }); it('when the user sets a filter, the "search" is emitted with filters', () => { findGlFilteredSearch().vm.$emit('input', mockFilters); findGlFilteredSearch().vm.$emit('submit'); - expect(wrapper.emitted('input')[0]).toEqual([ - { - filters: mockFilters, - sort: mockDefaultSort, - pagination: { page: 1 }, - }, - ]); + expectToHaveLastEmittedInput({ + runnerType: null, + filters: mockFilters, + sort: mockDefaultSort, + pagination: { page: 1 }, + }); }); it('when the user sets a sorting method, the "search" is emitted with the sort', () => { findSortOptions().at(1).vm.$emit('click'); - expect(wrapper.emitted('input')[0]).toEqual([ - { - filters: [], - sort: mockOtherSort, - pagination: { page: 1 }, - }, - ]); + expectToHaveLastEmittedInput({ + runnerType: null, + filters: [], + sort: mockOtherSort, + pagination: { page: 1 }, + }); }); }); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index e24dffea1eb..986e55a2132 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -1,6 +1,5 @@ import { GlTable, GlSkeletonLoader } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; -import { cloneDeep } from 'lodash'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerList from '~/runner/components/runner_list.vue'; @@ -43,12 +42,10 @@ describe('RunnerList', () => { const headerLabels = findHeaders().wrappers.map((w) => w.text()); expect(headerLabels).toEqual([ - 'Type/State', - 'Runner', + 'Status', + 'Runner ID', 'Version', 'IP Address', - 'Projects', - 'Jobs', 'Tags', 'Last contact', '', // actions has no label @@ -65,7 +62,7 @@ describe('RunnerList', () => { const { id, description, version, ipAddress, shortSha } = mockRunners[0]; // Badges - expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused'); + expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused'); // Runner summary expect(findCell({ fieldKey: 'summary' }).text()).toContain( @@ -76,8 +73,6 @@ describe('RunnerList', () => { // Other fields expect(findCell({ fieldKey: 'version' }).text()).toBe(version); expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress); - expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1'); - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0'); expect(findCell({ fieldKey: 'tagList' }).text()).toBe(''); expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String)); @@ -88,54 +83,6 @@ describe('RunnerList', () => { expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true); }); - describe('Table data formatting', () => { - let mockRunnersCopy; - - beforeEach(() => { - mockRunnersCopy = cloneDeep(mockRunners); - }); - - it('Formats null project counts', () => { - mockRunnersCopy[0].projectCount = null; - - createComponent({ props: { runners: mockRunnersCopy } }, mount); - - expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('n/a'); - }); - - it('Formats 0 project counts', () => { - mockRunnersCopy[0].projectCount = 0; - - createComponent({ props: { runners: mockRunnersCopy } }, mount); - - expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('0'); - }); - - it('Formats big project counts', () => { - mockRunnersCopy[0].projectCount = 1000; - - createComponent({ props: { runners: mockRunnersCopy } }, mount); - - expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1,000'); - }); - - it('Formats job counts', () => { - mockRunnersCopy[0].jobCount = 1000; - - createComponent({ props: { runners: mockRunnersCopy } }, mount); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000'); - }); - - it('Formats big job counts with a plus symbol', () => { - mockRunnersCopy[0].jobCount = 1001; - - createComponent({ props: { runners: mockRunnersCopy } }, mount); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+'); - }); - }); - it('Shows runner identifier', () => { const { id, shortSha } = mockRunners[0]; const numericId = getIdFromGraphQLId(id); diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js deleted file mode 100644 index effef0e7ebf..00000000000 --- a/spec/frontend/runner/components/runner_manual_setup_help_spec.js +++ /dev/null @@ -1,122 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { TEST_HOST } from 'helpers/test_constants'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import MaskedValue from '~/runner/components/helpers/masked_value.vue'; -import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue'; -import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue'; - -const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; -const mockRunnerInstallHelpPage = 'https://docs.gitlab.com/runner/install/'; - -describe('RunnerManualSetupHelp', () => { - let wrapper; - let originalGon; - - const findRunnerInstructions = () => wrapper.findComponent(RunnerInstructions); - const findRunnerRegistrationTokenReset = () => - wrapper.findComponent(RunnerRegistrationTokenReset); - const findClipboardButtons = () => wrapper.findAllComponents(ClipboardButton); - const findRunnerHelpTitle = () => wrapper.findByTestId('runner-help-title'); - const findCoordinatorUrl = () => wrapper.findByTestId('coordinator-url'); - const findRegistrationToken = () => wrapper.findByTestId('registration-token'); - const findRunnerHelpLink = () => wrapper.findByTestId('runner-help-link'); - - const createComponent = ({ props = {} } = {}) => { - wrapper = extendedWrapper( - shallowMount(RunnerManualSetupHelp, { - provide: { - runnerInstallHelpPage: mockRunnerInstallHelpPage, - }, - propsData: { - registrationToken: mockRegistrationToken, - type: INSTANCE_TYPE, - ...props, - }, - stubs: { - MaskedValue, - GlSprintf, - }, - }), - ); - }; - - beforeAll(() => { - originalGon = global.gon; - global.gon = { gitlab_url: TEST_HOST }; - }); - - afterAll(() => { - global.gon = originalGon; - }); - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Title contains the shared runner type', () => { - createComponent({ props: { type: INSTANCE_TYPE } }); - - expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a shared runner manually'); - }); - - it('Title contains the group runner type', () => { - createComponent({ props: { type: GROUP_TYPE } }); - - expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a group runner manually'); - }); - - it('Title contains the specific runner type', () => { - createComponent({ props: { type: PROJECT_TYPE } }); - - expect(findRunnerHelpTitle().text()).toMatchInterpolatedText( - 'Set up a specific runner manually', - ); - }); - - it('Runner Install Page link', () => { - expect(findRunnerHelpLink().attributes('href')).toBe(mockRunnerInstallHelpPage); - }); - - it('Displays the coordinator URL token', () => { - expect(findCoordinatorUrl().text()).toBe(TEST_HOST); - expect(findClipboardButtons().at(0).props('text')).toBe(TEST_HOST); - }); - - it('Displays the runner instructions', () => { - expect(findRunnerInstructions().exists()).toBe(true); - }); - - it('Displays the registration token', async () => { - findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click'); - - await nextTick(); - - expect(findRegistrationToken().text()).toBe(mockRegistrationToken); - expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken); - }); - - it('Displays the runner registration token reset button', () => { - expect(findRunnerRegistrationTokenReset().exists()).toBe(true); - }); - - it('Replaces the runner reset button', async () => { - const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN'; - - findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click'); - findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken); - - await nextTick(); - - expect(findRegistrationToken().text()).toBe(mockNewRegistrationToken); - expect(findClipboardButtons().at(1).props('text')).toBe(mockNewRegistrationToken); - }); -}); diff --git a/spec/frontend/runner/components/runner_state_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js index 8df56d6e3f3..18cfcfae864 100644 --- a/spec/frontend/runner/components/runner_state_paused_badge_spec.js +++ b/spec/frontend/runner/components/runner_paused_badge_spec.js @@ -1,6 +1,6 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import RunnerStatePausedBadge from '~/runner/components/runner_state_paused_badge.vue'; +import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; describe('RunnerTypeBadge', () => { diff --git a/spec/frontend/runner/components/runner_state_locked_badge_spec.js b/spec/frontend/runner/components/runner_state_locked_badge_spec.js deleted file mode 100644 index e92b671f5a1..00000000000 --- a/spec/frontend/runner/components/runner_state_locked_badge_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerStateLockedBadge from '~/runner/components/runner_state_locked_badge.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -describe('RunnerTypeBadge', () => { - let wrapper; - - const findBadge = () => wrapper.findComponent(GlBadge); - const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip'); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(RunnerStateLockedBadge, { - propsData: { - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders locked state', () => { - expect(wrapper.text()).toBe('locked'); - expect(findBadge().props('variant')).toBe('warning'); - }); - - it('renders tooltip', () => { - expect(getTooltip().value).toBeDefined(); - }); - - it('passes arbitrary attributes to the badge', () => { - createComponent({ props: { size: 'sm' } }); - - expect(findBadge().props('size')).toBe('sm'); - }); -}); diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js index dda318f8153..bd05d4b2cfe 100644 --- a/spec/frontend/runner/components/runner_tag_spec.js +++ b/spec/frontend/runner/components/runner_tag_spec.js @@ -1,18 +1,35 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import RunnerTag from '~/runner/components/runner_tag.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +const mockTag = 'tag1'; describe('RunnerTag', () => { let wrapper; const findBadge = () => wrapper.findComponent(GlBadge); + const getTooltipValue = () => getBinding(findBadge().element, 'gl-tooltip').value; + + const setDimensions = ({ scrollWidth, offsetWidth }) => { + jest.spyOn(findBadge().element, 'scrollWidth', 'get').mockReturnValue(scrollWidth); + jest.spyOn(findBadge().element, 'offsetWidth', 'get').mockReturnValue(offsetWidth); + + // Mock trigger resize + getBinding(findBadge().element, 'gl-resize-observer').value(); + }; const createComponent = ({ props = {} } = {}) => { wrapper = shallowMount(RunnerTag, { propsData: { - tag: 'tag1', + tag: mockTag, ...props, }, + directives: { + GlTooltip: createMockDirective(), + GlResizeObserver: createMockDirective(), + }, }); }; @@ -25,21 +42,36 @@ describe('RunnerTag', () => { }); it('Displays tag text', () => { - expect(wrapper.text()).toBe('tag1'); + expect(wrapper.text()).toBe(mockTag); }); it('Displays tags with correct style', () => { expect(findBadge().props()).toMatchObject({ - size: 'md', - variant: 'info', + size: 'sm', + variant: 'neutral', }); }); - it('Displays tags with small size', () => { + it('Displays tags with md size', () => { createComponent({ - props: { size: 'sm' }, + props: { size: 'md' }, }); - expect(findBadge().props('size')).toBe('sm'); + expect(findBadge().props('size')).toBe('md'); }); + + it.each` + case | scrollWidth | offsetWidth | expectedTooltip + ${'overflowing'} | ${110} | ${100} | ${mockTag} + ${'not overflowing'} | ${90} | ${100} | ${''} + ${'almost overflowing'} | ${100} | ${100} | ${''} + `( + 'Sets "$expectedTooltip" as tooltip when $case', + async ({ scrollWidth, offsetWidth, expectedTooltip }) => { + setDimensions({ scrollWidth, offsetWidth }); + await nextTick(); + + expect(getTooltipValue()).toBe(expectedTooltip); + }, + ); }); diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js index b6487ade0d6..da89a659432 100644 --- a/spec/frontend/runner/components/runner_tags_spec.js +++ b/spec/frontend/runner/components/runner_tags_spec.js @@ -33,16 +33,16 @@ describe('RunnerTags', () => { }); it('Displays tags with correct style', () => { - expect(findBadge().props('size')).toBe('md'); - expect(findBadge().props('variant')).toBe('info'); + expect(findBadge().props('size')).toBe('sm'); + expect(findBadge().props('variant')).toBe('neutral'); }); - it('Displays tags with small size', () => { + it('Displays tags with md size', () => { createComponent({ - props: { size: 'sm' }, + props: { size: 'md' }, }); - expect(findBadge().props('size')).toBe('sm'); + expect(findBadge().props('size')).toBe('md'); }); it('Is empty when there are no tags', () => { diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js index e54e499743b..4023c75c9a8 100644 --- a/spec/frontend/runner/components/runner_type_alert_spec.js +++ b/spec/frontend/runner/components/runner_type_alert_spec.js @@ -23,11 +23,11 @@ describe('RunnerTypeAlert', () => { }); describe.each` - type | exampleText | anchor | variant - ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} | ${'success'} - ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} | ${'success'} - ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} | ${'info'} - `('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => { + type | exampleText | anchor + ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} + ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} + ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} + `('When it is an $type level runner', ({ type, exampleText, anchor }) => { beforeEach(() => { createComponent({ props: { type } }); }); @@ -36,8 +36,8 @@ describe('RunnerTypeAlert', () => { expect(wrapper.text()).toMatch(exampleText); }); - it(`Shows a ${variant} variant`, () => { - expect(findAlert().props('variant')).toBe(variant); + it(`Shows an "info" variant`, () => { + expect(findAlert().props('variant')).toBe('info'); }); it(`Links to anchor "${anchor}"`, () => { diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js index fb344e65389..7bb0a2e6e2f 100644 --- a/spec/frontend/runner/components/runner_type_badge_spec.js +++ b/spec/frontend/runner/components/runner_type_badge_spec.js @@ -26,18 +26,18 @@ describe('RunnerTypeBadge', () => { }); describe.each` - type | text | variant - ${INSTANCE_TYPE} | ${'shared'} | ${'success'} - ${GROUP_TYPE} | ${'group'} | ${'success'} - ${PROJECT_TYPE} | ${'specific'} | ${'info'} - `('displays $type runner', ({ type, text, variant }) => { + type | text + ${INSTANCE_TYPE} | ${'shared'} + ${GROUP_TYPE} | ${'group'} + ${PROJECT_TYPE} | ${'specific'} + `('displays $type runner', ({ type, text }) => { beforeEach(() => { createComponent({ props: { type } }); }); - it(`as "${text}" with a ${variant} variant`, () => { + it(`as "${text}" with an "info" variant`, () => { expect(findBadge().text()).toBe(text); - expect(findBadge().props('variant')).toBe(variant); + expect(findBadge().props('variant')).toBe('info'); }); it('with a tooltip', () => { diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js new file mode 100644 index 00000000000..4871d9c470a --- /dev/null +++ b/spec/frontend/runner/components/runner_type_tabs_spec.js @@ -0,0 +1,109 @@ +import { GlTab } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; +import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants'; + +const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }; + +describe('RunnerTypeTabs', () => { + let wrapper; + + const findTabs = () => wrapper.findAll(GlTab); + const findActiveTab = () => + findTabs() + .filter((tab) => tab.attributes('active') === 'true') + .at(0); + + const createComponent = ({ props, ...options } = {}) => { + wrapper = shallowMount(RunnerTypeTabs, { + propsData: { + value: mockSearch, + ...props, + }, + stubs: { + GlTab, + }, + ...options, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Renders options to filter runners', () => { + expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([ + 'All', + 'Instance', + 'Group', + 'Project', + ]); + }); + + it('"All" is selected by default', () => { + expect(findActiveTab().text()).toBe('All'); + }); + + it('Another tab can be preselected by the user', () => { + createComponent({ + props: { + value: { + ...mockSearch, + runnerType: INSTANCE_TYPE, + }, + }, + }); + + expect(findActiveTab().text()).toBe('Instance'); + }); + + describe('When the user selects a tab', () => { + const emittedValue = () => wrapper.emitted('input')[0][0]; + + beforeEach(() => { + findTabs().at(2).vm.$emit('click'); + }); + + it(`Runner type is emitted`, () => { + expect(emittedValue()).toEqual({ + ...mockSearch, + runnerType: GROUP_TYPE, + }); + }); + + it('Runner type is selected', async () => { + const newValue = emittedValue(); + await wrapper.setProps({ value: newValue }); + + expect(findActiveTab().text()).toBe('Group'); + }); + }); + + describe('When using a custom slot', () => { + const mockContent = 'content'; + + beforeEach(() => { + createComponent({ + scopedSlots: { + title: ` + <span> + {{props.tab.title}} ${mockContent} + </span>`, + }, + }); + }); + + it('Renders tabs with additional information', () => { + expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([ + `All ${mockContent}`, + `Instance ${mockContent}`, + `Group ${mockContent}`, + `Project ${mockContent}`, + ]); + }); + }); +}); |