diff options
Diffstat (limited to 'spec/frontend/runner/components')
41 files changed, 0 insertions, 5154 deletions
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap deleted file mode 100644 index b27a1adf01b..00000000000 --- a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RunnerStatusPopover renders complete text 1`] = `"Never contacted: Runner has never contacted GitLab (when you register a runner, use gitlab-runner run to bring it online) Online: Runner has contacted GitLab within the last 2 hours Offline: Runner has not contacted GitLab in more than 2 hours Stale: Runner has not contacted GitLab in more than 3 months"`; diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/runner/components/cells/link_cell_spec.js deleted file mode 100644 index 46ab1adb6b6..00000000000 --- a/spec/frontend/runner/components/cells/link_cell_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import LinkCell from '~/runner/components/cells/link_cell.vue'; - -describe('LinkCell', () => { - let wrapper; - - const findGlLink = () => wrapper.findComponent(GlLink); - const findSpan = () => wrapper.find('span'); - - const createComponent = ({ props = {}, ...options } = {}) => { - wrapper = shallowMountExtended(LinkCell, { - propsData: { - ...props, - }, - ...options, - }); - }; - - it('when an href is provided, renders a link', () => { - createComponent({ props: { href: '/url' } }); - expect(findGlLink().exists()).toBe(true); - }); - - it('when an href is not provided, renders no link', () => { - createComponent(); - expect(findGlLink().exists()).toBe(false); - }); - - describe.each` - href | findContent - ${null} | ${findSpan} - ${'/url'} | ${findGlLink} - `('When href is $href', ({ href, findContent }) => { - const content = 'My Text'; - const attrs = { foo: 'bar' }; - const listeners = { - click: jest.fn(), - }; - - beforeEach(() => { - createComponent({ - props: { href }, - slots: { - default: content, - }, - attrs, - listeners, - }); - }); - - afterAll(() => { - listeners.click.mockReset(); - }); - - it('Renders content', () => { - expect(findContent().text()).toBe(content); - }); - - it('Passes attributes', () => { - expect(findContent().attributes()).toMatchObject(attrs); - }); - - it('Passes event listeners', () => { - expect(listeners.click).toHaveBeenCalledTimes(0); - - findContent().vm.$emit('click'); - - expect(listeners.click).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js deleted file mode 100644 index 58974d4f85f..00000000000 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ /dev/null @@ -1,138 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue'; -import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; -import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; -import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; -import { allRunnersData } from '../../mock_data'; - -const mockRunner = allRunnersData.data.runners.nodes[0]; - -describe('RunnerActionsCell', () => { - let wrapper; - - const findEditBtn = () => wrapper.findComponent(RunnerEditButton); - const findRunnerPauseBtn = () => wrapper.findComponent(RunnerPauseButton); - const findDeleteBtn = () => wrapper.findComponent(RunnerDeleteButton); - - const createComponent = ({ runner = {}, ...props } = {}) => { - wrapper = shallowMountExtended(RunnerActionsCell, { - propsData: { - editUrl: mockRunner.editAdminUrl, - runner: { - id: mockRunner.id, - shortSha: mockRunner.shortSha, - editAdminUrl: mockRunner.editAdminUrl, - userPermissions: mockRunner.userPermissions, - ...runner, - }, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('Edit Action', () => { - it('Displays the runner edit link with the correct href', () => { - createComponent(); - - expect(findEditBtn().attributes('href')).toBe(mockRunner.editAdminUrl); - }); - - it('Does not render the runner edit link when user cannot update', () => { - createComponent({ - runner: { - userPermissions: { - ...mockRunner.userPermissions, - updateRunner: false, - }, - }, - }); - - expect(findEditBtn().exists()).toBe(false); - }); - - it('Does not render the runner edit link when editUrl is not provided', () => { - createComponent({ - editUrl: null, - }); - - expect(findEditBtn().exists()).toBe(false); - }); - }); - - describe('Pause action', () => { - it('Renders a compact pause button', () => { - createComponent(); - - expect(findRunnerPauseBtn().props('compact')).toBe(true); - }); - - it('Does not render the runner pause button when user cannot update', () => { - createComponent({ - runner: { - userPermissions: { - ...mockRunner.userPermissions, - updateRunner: false, - }, - }, - }); - - expect(findRunnerPauseBtn().exists()).toBe(false); - }); - }); - - describe('Delete action', () => { - it('Renders a compact delete button', () => { - createComponent(); - - expect(findDeleteBtn().props('compact')).toBe(true); - }); - - it('Passes runner data to delete button', () => { - createComponent({ - runner: mockRunner, - }); - - expect(findDeleteBtn().props('runner')).toEqual(mockRunner); - }); - - it('Emits toggledPaused events', () => { - createComponent(); - - expect(wrapper.emitted('toggledPaused')).toBe(undefined); - - findRunnerPauseBtn().vm.$emit('toggledPaused'); - - expect(wrapper.emitted('toggledPaused')).toHaveLength(1); - }); - - it('Emits delete events', () => { - const value = { name: 'Runner' }; - - createComponent(); - - expect(wrapper.emitted('deleted')).toBe(undefined); - - findDeleteBtn().vm.$emit('deleted', value); - - expect(wrapper.emitted('deleted')).toEqual([[value]]); - }); - - it('Does not render the runner delete button when user cannot delete', () => { - createComponent({ - runner: { - userPermissions: { - ...mockRunner.userPermissions, - deleteRunner: false, - }, - }, - }); - - expect(findDeleteBtn().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/runner/components/cells/runner_owner_cell_spec.js deleted file mode 100644 index e9965d8855d..00000000000 --- a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js +++ /dev/null @@ -1,111 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlLink } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -import RunnerOwnerCell from '~/runner/components/cells/runner_owner_cell.vue'; - -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -describe('RunnerOwnerCell', () => { - let wrapper; - - const findLink = () => wrapper.findComponent(GlLink); - const getLinkTooltip = () => getBinding(findLink().element, 'gl-tooltip').value; - - const createComponent = ({ runner } = {}) => { - wrapper = shallowMount(RunnerOwnerCell, { - directives: { - GlTooltip: createMockDirective(), - }, - propsData: { - runner, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('When its an instance runner', () => { - beforeEach(() => { - createComponent({ - runner: { - runnerType: INSTANCE_TYPE, - }, - }); - }); - - it('shows an administrator label', () => { - expect(findLink().exists()).toBe(false); - expect(wrapper.text()).toBe(s__('Runners|Administrator')); - }); - }); - - describe('When its a group runner', () => { - const mockName = 'Group 2'; - const mockFullName = 'Group 1 / Group 2'; - const mockWebUrl = '/group-1/group-2'; - - beforeEach(() => { - createComponent({ - runner: { - runnerType: GROUP_TYPE, - groups: { - nodes: [ - { - name: mockName, - fullName: mockFullName, - webUrl: mockWebUrl, - }, - ], - }, - }, - }); - }); - - it('Displays a group link', () => { - expect(findLink().attributes('href')).toBe(mockWebUrl); - expect(wrapper.text()).toBe(mockName); - expect(getLinkTooltip()).toBe(mockFullName); - }); - }); - - describe('When its a project runner', () => { - const mockName = 'Project 1'; - const mockNameWithNamespace = 'Group 1 / Project 1'; - const mockWebUrl = '/group-1/project-1'; - - beforeEach(() => { - createComponent({ - runner: { - runnerType: PROJECT_TYPE, - ownerProject: { - name: mockName, - nameWithNamespace: mockNameWithNamespace, - webUrl: mockWebUrl, - }, - }, - }); - }); - - it('Displays a project link', () => { - expect(findLink().attributes('href')).toBe(mockWebUrl); - expect(wrapper.text()).toBe(mockName); - expect(getLinkTooltip()).toBe(mockNameWithNamespace); - }); - }); - - describe('When its an empty runner', () => { - beforeEach(() => { - createComponent({ - runner: {}, - }); - }); - - it('shows no label', () => { - expect(wrapper.text()).toBe(''); - }); - }); -}); diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js deleted file mode 100644 index e7cadefc140..00000000000 --- a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js +++ /dev/null @@ -1,164 +0,0 @@ -import { __ } from '~/locale'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerStackedSummaryCell from '~/runner/components/cells/runner_stacked_summary_cell.vue'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import RunnerTags from '~/runner/components/runner_tags.vue'; -import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; - -import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -import { allRunnersData } from '../../mock_data'; - -const mockRunner = allRunnersData.data.runners.nodes[0]; - -describe('RunnerTypeCell', () => { - let wrapper; - - const findLockIcon = () => wrapper.findByTestId('lock-icon'); - const findRunnerTags = () => wrapper.findComponent(RunnerTags); - const findRunnerSummaryField = (icon) => - wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon) - .wrappers[0]; - - const createComponent = (runner, options) => { - wrapper = mountExtended(RunnerStackedSummaryCell, { - propsData: { - runner: { - ...mockRunner, - ...runner, - }, - }, - stubs: { - RunnerSummaryField, - }, - ...options, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays the runner name as id and short token', () => { - expect(wrapper.text()).toContain( - `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`, - ); - }); - - 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 type', () => { - createComponent({ - runnerType: INSTANCE_TYPE, - locked: true, - }); - - expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE); - }); - - it('Displays the runner version', () => { - expect(wrapper.text()).toContain(mockRunner.version); - }); - - it('Displays the runner description', () => { - expect(wrapper.text()).toContain(mockRunner.description); - }); - - it('Displays last contact', () => { - createComponent({ - contactedAt: '2022-01-02', - }); - - expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02'); - }); - - it('Displays empty last contact', () => { - createComponent({ - contactedAt: null, - }); - - expect(findRunnerSummaryField('clock').findComponent(TimeAgo).exists()).toBe(false); - expect(findRunnerSummaryField('clock').text()).toContain(__('Never')); - }); - - it('Displays ip address', () => { - createComponent({ - ipAddress: '127.0.0.1', - }); - - expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1'); - }); - - it('Displays no ip address', () => { - createComponent({ - ipAddress: null, - }); - - expect(findRunnerSummaryField('disk')).toBeUndefined(); - }); - - it('Displays job count', () => { - expect(findRunnerSummaryField('pipeline').text()).toContain(`${mockRunner.jobCount}`); - }); - - it('Formats large job counts', () => { - createComponent({ - jobCount: 1000, - }); - - expect(findRunnerSummaryField('pipeline').text()).toContain('1,000'); - }); - - it('Formats large job counts with a plus symbol', () => { - createComponent({ - jobCount: 1001, - }); - - expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+'); - }); - - it('Displays created at', () => { - expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe( - mockRunner.createdAt, - ); - }); - - it('Displays tag list', () => { - createComponent({ - tagList: ['shell', 'linux'], - }); - - expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']); - }); - - it('Displays a custom slot', () => { - const slotContent = 'My custom runner name'; - - createComponent( - {}, - { - slots: { - 'runner-name': slotContent, - }, - }, - ); - - expect(wrapper.text()).toContain(slotContent); - }); -}); diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js deleted file mode 100644 index 1d4e3762c91..00000000000 --- a/spec/frontend/runner/components/cells/runner_status_cell_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { mount } from '@vue/test-utils'; -import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue'; - -import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue'; -import RunnerPausedBadge from '~/runner/components/runner_paused_badge.vue'; -import { - I18N_PAUSED, - I18N_STATUS_ONLINE, - I18N_STATUS_OFFLINE, - INSTANCE_TYPE, - STATUS_ONLINE, - STATUS_OFFLINE, -} from '~/runner/constants'; - -describe('RunnerStatusCell', () => { - let wrapper; - - const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge); - const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge); - - 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()).toContain(I18N_STATUS_ONLINE); - expect(findStatusBadge().text()).toBe(I18N_STATUS_ONLINE); - }); - - it('Displays offline status', () => { - createComponent({ - runner: { - status: STATUS_OFFLINE, - }, - }); - - expect(wrapper.text()).toMatchInterpolatedText(I18N_STATUS_OFFLINE); - expect(findStatusBadge().text()).toBe(I18N_STATUS_OFFLINE); - }); - - it('Displays paused status', () => { - createComponent({ - runner: { - active: false, - status: STATUS_ONLINE, - }, - }); - - expect(wrapper.text()).toMatchInterpolatedText(`${I18N_STATUS_ONLINE} ${I18N_PAUSED}`); - expect(findPausedBadge().text()).toBe(I18N_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_field_spec.js b/spec/frontend/runner/components/cells/runner_summary_field_spec.js deleted file mode 100644 index b49addf112f..00000000000 --- a/spec/frontend/runner/components/cells/runner_summary_field_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -describe('RunnerSummaryField', () => { - let wrapper; - - const findIcon = () => wrapper.findComponent(GlIcon); - const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value; - - const createComponent = ({ props, ...options } = {}) => { - wrapper = shallowMount(RunnerSummaryField, { - propsData: { - icon: '', - tooltip: '', - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - ...options, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('shows content in slot', () => { - createComponent({ - slots: { default: 'content' }, - }); - - expect(wrapper.text()).toBe('content'); - }); - - it('shows icon', () => { - createComponent({ props: { icon: 'git' } }); - - expect(findIcon().props('name')).toBe('git'); - }); - - it('shows tooltip', () => { - createComponent({ props: { tooltip: 'tooltip' } }); - - expect(getTooltipValue()).toBe('tooltip'); - }); -}); diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js deleted file mode 100644 index d3f38bc1d26..00000000000 --- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js +++ /dev/null @@ -1,198 +0,0 @@ -import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui'; -import { mount, shallowMount, createWrapper } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; - -import VueApollo from 'vue-apollo'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; - -import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; -import RegistrationToken from '~/runner/components/registration/registration_token.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 = '**********'; - -Vue.use(VueApollo); - -describe('RegistrationDropdown', () => { - let wrapper; - - const findDropdown = () => wrapper.findComponent(GlDropdown); - - const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem); - const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm); - const findRegistrationToken = () => wrapper.findComponent(RegistrationToken); - const findRegistrationTokenInput = () => - wrapper.findByLabelText(RegistrationToken.i18n.registrationToken); - const findTokenResetDropdownItem = () => - wrapper.findComponent(RegistrationTokenResetDropdownItem); - const findModal = () => wrapper.findComponent(GlModal); - const findModalContent = () => - createWrapper(document.body) - .find('[data-testid="runner-instructions-modal"]') - .text() - .replace(/[\n\t\s]+/g, ' '); - - const openModal = async () => { - await findRegistrationInstructionsDropdownItem().trigger('click'); - findModal().vm.$emit('shown'); - - await waitForPromises(); - }; - - const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => { - wrapper = extendedWrapper( - mountFn(RegistrationDropdown, { - propsData: { - registrationToken: mockToken, - type: INSTANCE_TYPE, - ...props, - }, - ...options, - }), - ); - }; - - const createComponentWithModal = () => { - const requestHandlers = [ - [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)], - [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)], - ]; - - createComponent( - { - // Mock load modal contents from API - apolloProvider: createMockApollo(requestHandlers), - // Use `attachTo` to find the modal - attachTo: document.body, - }, - mount, - ); - }; - - 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', () => { - beforeEach(async () => { - createComponentWithModal({}, mount); - - await openModal(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('opens the modal with contents', () => { - const modalText = findModalContent(); - - 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(findRegistrationTokenInput().element.value).toBe(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); - }); - }); - - describe('When token is reset', () => { - const newToken = 'mock1'; - - const resetToken = async () => { - findTokenResetDropdownItem().vm.$emit('tokenReset', newToken); - await nextTick(); - }; - - it('Updates token input', async () => { - createComponent({}, mount); - - expect(findRegistrationToken().props('value')).not.toBe(newToken); - - await resetToken(); - - expect(findRegistrationToken().props('value')).toBe(newToken); - }); - - it('Updates token in modal', async () => { - createComponentWithModal({}, mount); - - await openModal(); - - expect(findModalContent()).toContain(mockToken); - - await resetToken(); - - expect(findModalContent()).toContain(newToken); - }); - }); -}); diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js deleted file mode 100644 index 2510aaf0334..00000000000 --- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js +++ /dev/null @@ -1,209 +0,0 @@ -import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; - -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } 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/list/runners_registration_token_reset.mutation.graphql'; -import { captureException } from '~/runner/sentry_utils'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); - -Vue.use(VueApollo); -Vue.use(GlToast); - -const mockNewToken = 'NEW_TOKEN'; -const modalID = 'token-reset-modal'; - -describe('RegistrationTokenResetDropdownItem', () => { - let wrapper; - let runnersRegistrationTokenResetMutationHandler; - let showToast; - - const mockEvent = { preventDefault: jest.fn() }; - const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findModal = () => wrapper.findComponent(GlModal); - const clickSubmit = () => findModal().vm.$emit('primary', mockEvent); - - const createComponent = ({ props, provide = {} } = {}) => { - wrapper = shallowMount(RegistrationTokenResetDropdownItem, { - provide, - propsData: { - type: INSTANCE_TYPE, - ...props, - }, - apolloProvider: createMockApollo([ - [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler], - ]), - directives: { - GlModal: createMockDirective(), - }, - }); - - showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; - }; - - beforeEach(() => { - runnersRegistrationTokenResetMutationHandler = jest.fn().mockResolvedValue({ - data: { - runnersRegistrationTokenReset: { - token: mockNewToken, - errors: [], - }, - }, - }); - - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays reset button', () => { - expect(findDropdownItem().exists()).toBe(true); - }); - - describe('modal directive integration', () => { - it('has the correct ID on the dropdown', () => { - const binding = getBinding(findDropdownItem().element, 'gl-modal'); - - expect(binding.value).toBe(modalID); - }); - - it('has the correct ID on the modal', () => { - expect(findModal().props('modalId')).toBe(modalID); - }); - }); - - describe('On click and confirmation', () => { - const mockGroupId = '11'; - const mockProjectId = '22'; - - describe.each` - type | provide | expectedInput - ${INSTANCE_TYPE} | ${{}} | ${{ type: INSTANCE_TYPE }} - ${GROUP_TYPE} | ${{ groupId: mockGroupId }} | ${{ type: GROUP_TYPE, id: `gid://gitlab/Group/${mockGroupId}` }} - ${PROJECT_TYPE} | ${{ projectId: mockProjectId }} | ${{ type: PROJECT_TYPE, id: `gid://gitlab/Project/${mockProjectId}` }} - `('Resets token of type $type', ({ type, provide, expectedInput }) => { - beforeEach(async () => { - createComponent({ - provide, - props: { type }, - }); - - findDropdownItem().trigger('click'); - clickSubmit(); - await waitForPromises(); - }); - - it('resets token', () => { - expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledTimes(1); - expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledWith({ - input: expectedInput, - }); - }); - - it('emits result', () => { - expect(wrapper.emitted('tokenReset')).toHaveLength(1); - expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]); - }); - - it('does not show a loading state', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('shows confirmation', () => { - expect(showToast).toHaveBeenLastCalledWith( - expect.stringContaining('registration token generated'), - ); - }); - }); - }); - - describe('On click without confirmation', () => { - beforeEach(async () => { - findDropdownItem().vm.$emit('click'); - await waitForPromises(); - }); - - it('does not reset token', () => { - expect(runnersRegistrationTokenResetMutationHandler).not.toHaveBeenCalled(); - }); - - it('does not emit any result', () => { - expect(wrapper.emitted('tokenReset')).toBeUndefined(); - }); - - it('does not show a loading state', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('does not shows confirmation', () => { - expect(showToast).not.toHaveBeenCalled(); - }); - }); - - describe('On error', () => { - it('On network error, error message is shown', async () => { - const mockErrorMsg = 'Token reset failed!'; - - runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - - findDropdownItem().trigger('click'); - clickSubmit(); - await waitForPromises(); - - expect(createAlert).toHaveBeenLastCalledWith({ - message: mockErrorMsg, - }); - expect(captureException).toHaveBeenCalledWith({ - error: new Error(mockErrorMsg), - component: 'RunnerRegistrationTokenReset', - }); - }); - - it('On validation error, error message is shown', async () => { - const mockErrorMsg = 'User not allowed!'; - const mockErrorMsg2 = 'Type is not valid!'; - - runnersRegistrationTokenResetMutationHandler.mockResolvedValue({ - data: { - runnersRegistrationTokenReset: { - token: null, - errors: [mockErrorMsg, mockErrorMsg2], - }, - }, - }); - - findDropdownItem().trigger('click'); - clickSubmit(); - await waitForPromises(); - - expect(createAlert).toHaveBeenLastCalledWith({ - message: `${mockErrorMsg} ${mockErrorMsg2}`, - }); - expect(captureException).toHaveBeenCalledWith({ - error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`), - component: 'RunnerRegistrationTokenReset', - }); - }); - }); - - describe('Immediately after click', () => { - it('shows loading state', async () => { - findDropdownItem().trigger('click'); - clickSubmit(); - await nextTick(); - - 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 deleted file mode 100644 index 19344a68f79..00000000000 --- a/spec/frontend/runner/components/registration/registration_token_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { GlToast } from '@gitlab/ui'; -import Vue from 'vue'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RegistrationToken from '~/runner/components/registration/registration_token.vue'; -import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; - -const mockToken = '01234567890'; -const mockMasked = '***********'; - -describe('RegistrationToken', () => { - let wrapper; - let showToast; - - Vue.use(GlToast); - - const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility); - - const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RegistrationToken, { - propsData: { - value: mockToken, - inputId: 'token-value', - ...props, - }, - }); - - showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays value and copy button', () => { - createComponent(); - - expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken); - expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe( - 'Copy registration token', - ); - }); - - // Component integration test to ensure secure masking - it('Displays masked value by default', () => { - createComponent({ mountFn: mountExtended }); - - expect(wrapper.find('input').element.value).toBe(mockMasked); - }); - - describe('When the copy to clipboard button is clicked', () => { - beforeEach(() => { - createComponent(); - }); - - it('shows a copied message', () => { - findInputCopyToggleVisibility().vm.$emit('copy'); - - expect(showToast).toHaveBeenCalledTimes(1); - expect(showToast).toHaveBeenCalledWith('Registration token copied!'); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js deleted file mode 100644 index cc09046c000..00000000000 --- a/spec/frontend/runner/components/runner_assigned_item_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import { GlAvatar, GlBadge } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue'; -import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; - -const mockHref = '/group/project'; -const mockName = 'Project'; -const mockDescription = 'Project description'; -const mockFullName = 'Group / Project'; -const mockAvatarUrl = '/avatar.png'; - -describe('RunnerAssignedItem', () => { - let wrapper; - - const findAvatar = () => wrapper.findByTestId('item-avatar'); - const findBadge = () => wrapper.findComponent(GlBadge); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMountExtended(RunnerAssignedItem, { - propsData: { - href: mockHref, - name: mockName, - fullName: mockFullName, - avatarUrl: mockAvatarUrl, - description: mockDescription, - ...props, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Shows an avatar', () => { - const avatar = findAvatar(); - - expect(avatar.attributes('href')).toBe(mockHref); - expect(avatar.findComponent(GlAvatar).props()).toMatchObject({ - alt: mockName, - entityName: mockName, - src: mockAvatarUrl, - shape: AVATAR_SHAPE_OPTION_RECT, - size: 48, - }); - }); - - it('Shows an item link', () => { - const groupFullName = wrapper.findByText(mockFullName); - - expect(groupFullName.attributes('href')).toBe(mockHref); - }); - - it('Shows description', () => { - expect(wrapper.text()).toContain(mockDescription); - }); - - it('Shows owner badge', () => { - createComponent({ props: { isOwner: true } }); - - expect(findBadge().text()).toBe(s__('Runner|Owner')); - }); -}); diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js deleted file mode 100644 index 424a4e61ccd..00000000000 --- a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import Vue from 'vue'; -import { GlFormCheckbox } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { createLocalState } from '~/runner/graphql/list/local_state'; - -Vue.use(VueApollo); - -const makeRunner = (id, deleteRunner = true) => ({ - id, - userPermissions: { deleteRunner }, -}); - -// Multi-select checkbox possible states: -const stateToAttrs = { - unchecked: { disabled: undefined, checked: undefined, indeterminate: undefined }, - checked: { disabled: undefined, checked: 'true', indeterminate: undefined }, - indeterminate: { disabled: undefined, checked: undefined, indeterminate: 'true' }, - disabled: { disabled: 'true', checked: undefined, indeterminate: undefined }, -}; - -describe('RunnerBulkDeleteCheckbox', () => { - let wrapper; - let mockState; - let mockCheckedRunnerIds; - - const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); - - const expectCheckboxToBe = (state) => { - const expected = stateToAttrs[state]; - expect(findCheckbox().attributes('disabled')).toBe(expected.disabled); - expect(findCheckbox().attributes('checked')).toBe(expected.checked); - expect(findCheckbox().attributes('indeterminate')).toBe(expected.indeterminate); - }; - - const createComponent = ({ runners = [] } = {}) => { - const { cacheConfig, localMutations } = mockState; - const apolloProvider = createMockApollo(undefined, undefined, cacheConfig); - - wrapper = shallowMountExtended(RunnerBulkDeleteCheckbox, { - apolloProvider, - provide: { - localMutations, - }, - propsData: { - runners, - }, - }); - }; - - beforeEach(() => { - mockState = createLocalState(); - - jest - .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds') - .mockImplementation(() => mockCheckedRunnerIds); - - jest.spyOn(mockState.localMutations, 'setRunnersChecked'); - }); - - describe('when all runners can be deleted', () => { - const mockIds = ['1', '2', '3']; - const mockIdAnotherPage = '4'; - const mockRunners = mockIds.map((id) => makeRunner(id)); - - it.each` - case | checkedRunnerIds | state - ${'no runners'} | ${[]} | ${'unchecked'} - ${'no runners in this page'} | ${[mockIdAnotherPage]} | ${'unchecked'} - ${'all runners'} | ${mockIds} | ${'checked'} - ${'some runners'} | ${[mockIds[0]]} | ${'indeterminate'} - ${'all plus other runners'} | ${[...mockIds, mockIdAnotherPage]} | ${'checked'} - `('if $case are checked, checkbox is $state', ({ checkedRunnerIds, state }) => { - mockCheckedRunnerIds = checkedRunnerIds; - - createComponent({ runners: mockRunners }); - expectCheckboxToBe(state); - }); - }); - - describe('when some runners cannot be deleted', () => { - it('all allowed runners are selected, checkbox is checked', () => { - mockCheckedRunnerIds = ['a', 'b', 'c']; - createComponent({ - runners: [makeRunner('a'), makeRunner('b'), makeRunner('c', false)], - }); - - expectCheckboxToBe('checked'); - }); - - it('some allowed runners are selected, checkbox is indeterminate', () => { - mockCheckedRunnerIds = ['a', 'b']; - createComponent({ - runners: [makeRunner('a'), makeRunner('b'), makeRunner('c')], - }); - - expectCheckboxToBe('indeterminate'); - }); - - it('no allowed runners are selected, checkbox is disabled', () => { - mockCheckedRunnerIds = ['a', 'b']; - createComponent({ - runners: [makeRunner('a', false), makeRunner('b', false)], - }); - - expectCheckboxToBe('disabled'); - }); - }); - - describe('When user selects', () => { - const mockRunners = [makeRunner('1'), makeRunner('2')]; - - beforeEach(() => { - mockCheckedRunnerIds = ['1', '2']; - createComponent({ runners: mockRunners }); - }); - - it.each([[true], [false]])('sets checked to %s', (checked) => { - findCheckbox().vm.$emit('change', checked); - - expect(mockState.localMutations.setRunnersChecked).toHaveBeenCalledTimes(1); - expect(mockState.localMutations.setRunnersChecked).toHaveBeenCalledWith({ - isChecked: checked, - runners: mockRunners, - }); - }); - }); - - describe('When runners are loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('is disabled', () => { - expectCheckboxToBe('disabled'); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js deleted file mode 100644 index 6df918c684f..00000000000 --- a/spec/frontend/runner/components/runner_bulk_delete_spec.js +++ /dev/null @@ -1,243 +0,0 @@ -import Vue from 'vue'; -import { GlModal, GlSprintf } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import { createAlert } from '~/flash'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { s__ } from '~/locale'; -import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import BulkRunnerDeleteMutation from '~/runner/graphql/list/bulk_runner_delete.mutation.graphql'; -import { createLocalState } from '~/runner/graphql/list/local_state'; -import waitForPromises from 'helpers/wait_for_promises'; -import { allRunnersData } from '../mock_data'; - -Vue.use(VueApollo); - -jest.mock('~/flash'); - -describe('RunnerBulkDelete', () => { - let wrapper; - let apolloCache; - let mockState; - let mockCheckedRunnerIds; - - const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection')); - const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected')); - const findModal = () => wrapper.findComponent(GlModal); - - const mockRunners = allRunnersData.data.runners.nodes; - const mockId1 = allRunnersData.data.runners.nodes[0].id; - const mockId2 = allRunnersData.data.runners.nodes[1].id; - - const bulkRunnerDeleteHandler = jest.fn(); - - const createComponent = () => { - const { cacheConfig, localMutations } = mockState; - const apolloProvider = createMockApollo( - [[BulkRunnerDeleteMutation, bulkRunnerDeleteHandler]], - undefined, - cacheConfig, - ); - - wrapper = shallowMountExtended(RunnerBulkDelete, { - apolloProvider, - provide: { - localMutations, - }, - propsData: { - runners: mockRunners, - }, - directives: { - GlTooltip: createMockDirective(), - }, - stubs: { - GlSprintf, - GlModal, - }, - }); - - apolloCache = apolloProvider.defaultClient.cache; - jest.spyOn(apolloCache, 'evict'); - jest.spyOn(apolloCache, 'gc'); - }; - - beforeEach(() => { - mockState = createLocalState(); - - jest - .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds') - .mockImplementation(() => mockCheckedRunnerIds); - }); - - afterEach(() => { - bulkRunnerDeleteHandler.mockReset(); - wrapper.destroy(); - }); - - describe('When no runners are checked', () => { - beforeEach(async () => { - mockCheckedRunnerIds = []; - - createComponent(); - - await waitForPromises(); - }); - - it('shows no contents', () => { - expect(wrapper.html()).toBe(''); - }); - }); - - describe.each` - count | ids | text - ${1} | ${[mockId1]} | ${'1 runner'} - ${2} | ${[mockId1, mockId2]} | ${'2 runners'} - `('When $count runner(s) are checked', ({ ids, text }) => { - beforeEach(() => { - mockCheckedRunnerIds = ids; - - createComponent(); - - jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {}); - }); - - it(`shows "${text}"`, () => { - expect(wrapper.text()).toContain(text); - }); - - it('clears selection', () => { - expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(0); - - findClearBtn().vm.$emit('click'); - - expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(1); - }); - - it('shows confirmation modal', () => { - const modalId = getBinding(findDeleteBtn().element, 'gl-modal'); - - expect(findModal().props('modal-id')).toBe(modalId); - expect(findModal().text()).toContain(text); - }); - }); - - describe('when runners are deleted', () => { - let evt; - let mockHideModal; - - beforeEach(() => { - mockCheckedRunnerIds = [mockId1, mockId2]; - - createComponent(); - - jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {}); - mockHideModal = jest.spyOn(findModal().vm, 'hide'); - }); - - describe('when deletion is successful', () => { - beforeEach(() => { - bulkRunnerDeleteHandler.mockResolvedValue({ - data: { - bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] }, - }, - }); - - evt = { - preventDefault: jest.fn(), - }; - findModal().vm.$emit('primary', evt); - }); - - it('has loading state', async () => { - expect(findModal().props('actionPrimary').attributes.loading).toBe(true); - expect(findModal().props('actionCancel').attributes.loading).toBe(true); - - await waitForPromises(); - - expect(findModal().props('actionPrimary').attributes.loading).toBe(false); - expect(findModal().props('actionCancel').attributes.loading).toBe(false); - }); - - it('modal is not prevented from closing', () => { - expect(evt.preventDefault).toHaveBeenCalledTimes(1); - }); - - it('mutation is called', async () => { - expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({ - input: { ids: mockCheckedRunnerIds }, - }); - }); - - it('user interface is updated', async () => { - const { evict, gc } = apolloCache; - - expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length); - expect(evict).toHaveBeenCalledWith({ - id: expect.stringContaining(mockCheckedRunnerIds[0]), - }); - expect(evict).toHaveBeenCalledWith({ - id: expect.stringContaining(mockCheckedRunnerIds[1]), - }); - - expect(gc).toHaveBeenCalledTimes(1); - }); - - it('modal is hidden', () => { - expect(mockHideModal).toHaveBeenCalledTimes(1); - }); - }); - - describe('when deletion fails', () => { - beforeEach(() => { - bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!')); - - evt = { - preventDefault: jest.fn(), - }; - findModal().vm.$emit('primary', evt); - }); - - it('has loading state', async () => { - expect(findModal().props('actionPrimary').attributes.loading).toBe(true); - expect(findModal().props('actionCancel').attributes.loading).toBe(true); - - await waitForPromises(); - - expect(findModal().props('actionPrimary').attributes.loading).toBe(false); - expect(findModal().props('actionCancel').attributes.loading).toBe(false); - }); - - it('modal is not prevented from closing', () => { - expect(evt.preventDefault).toHaveBeenCalledTimes(1); - }); - - it('mutation is called', () => { - expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({ - input: { ids: mockCheckedRunnerIds }, - }); - }); - - it('user interface is not updated', async () => { - await waitForPromises(); - - const { evict, gc } = apolloCache; - - expect(evict).not.toHaveBeenCalled(); - expect(gc).not.toHaveBeenCalled(); - expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled(); - }); - - it('alert is called', async () => { - await waitForPromises(); - - expect(createAlert).toHaveBeenCalled(); - expect(createAlert).toHaveBeenCalledWith({ - message: expect.any(String), - captureError: true, - error: expect.any(Error), - }); - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js deleted file mode 100644 index c8fb7a69379..00000000000 --- a/spec/frontend/runner/components/runner_delete_button_spec.js +++ /dev/null @@ -1,266 +0,0 @@ -import Vue from 'vue'; -import { GlButton } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; -import runnerDeleteMutation from '~/runner/graphql/shared/runner_delete.mutation.graphql'; -import waitForPromises from 'helpers/wait_for_promises'; -import { captureException } from '~/runner/sentry_utils'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { createAlert } from '~/flash'; -import { I18N_DELETE_RUNNER } from '~/runner/constants'; - -import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; -import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue'; -import { allRunnersData } from '../mock_data'; - -const mockRunner = allRunnersData.data.runners.nodes[0]; -const mockRunnerId = getIdFromGraphQLId(mockRunner.id); - -Vue.use(VueApollo); - -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); - -describe('RunnerDeleteButton', () => { - let wrapper; - let apolloProvider; - let apolloCache; - let runnerDeleteHandler; - - const findBtn = () => wrapper.findComponent(GlButton); - const findModal = () => wrapper.findComponent(RunnerDeleteModal); - - const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value; - const getModal = () => getBinding(findBtn().element, 'gl-modal').value; - - const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { - const { runner, ...propsData } = props; - - wrapper = mountFn(RunnerDeleteButton, { - propsData: { - runner: { - // We need typename so that cache.identify works - // eslint-disable-next-line no-underscore-dangle - __typename: mockRunner.__typename, - id: mockRunner.id, - shortSha: mockRunner.shortSha, - ...runner, - }, - ...propsData, - }, - apolloProvider, - directives: { - GlTooltip: createMockDirective(), - GlModal: createMockDirective(), - }, - }); - }; - - const clickOkAndWait = async () => { - findModal().vm.$emit('primary'); - await waitForPromises(); - }; - - beforeEach(() => { - runnerDeleteHandler = jest.fn().mockImplementation(() => { - return Promise.resolve({ - data: { - runnerDelete: { - errors: [], - }, - }, - }); - }); - apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]); - apolloCache = apolloProvider.defaultClient.cache; - - jest.spyOn(apolloCache, 'evict'); - jest.spyOn(apolloCache, 'gc'); - - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays a delete button without an icon', () => { - expect(findBtn().props()).toMatchObject({ - loading: false, - icon: '', - }); - expect(findBtn().classes('btn-icon')).toBe(false); - expect(findBtn().text()).toBe(I18N_DELETE_RUNNER); - }); - - it('Displays a modal with the runner name', () => { - expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`); - }); - - it('Does not have tabindex when button is enabled', () => { - expect(wrapper.attributes('tabindex')).toBeUndefined(); - }); - - it('Displays a modal when clicked', () => { - const modalId = `delete-runner-modal-${mockRunnerId}`; - - expect(getModal()).toBe(modalId); - expect(findModal().attributes('modal-id')).toBe(modalId); - }); - - it('Does not display redundant text for screen readers', () => { - expect(findBtn().attributes('aria-label')).toBe(undefined); - }); - - it('Passes other attributes to the button', () => { - createComponent({ props: { category: 'secondary' } }); - - expect(findBtn().props('category')).toBe('secondary'); - }); - - describe(`Before the delete button is clicked`, () => { - it('The mutation has not been called', () => { - expect(runnerDeleteHandler).toHaveBeenCalledTimes(0); - }); - }); - - describe('Immediately after the delete button is clicked', () => { - beforeEach(async () => { - findModal().vm.$emit('primary'); - }); - - it('The button has a loading state', async () => { - expect(findBtn().props('loading')).toBe(true); - }); - - it('The stale tooltip is removed', async () => { - expect(getTooltip()).toBe(''); - }); - }); - - describe('After clicking on the delete button', () => { - beforeEach(async () => { - await clickOkAndWait(); - }); - - it('The mutation to delete is called', () => { - expect(runnerDeleteHandler).toHaveBeenCalledTimes(1); - expect(runnerDeleteHandler).toHaveBeenCalledWith({ - input: { - id: mockRunner.id, - }, - }); - }); - - it('The user can be notified with an event', () => { - const deleted = wrapper.emitted('deleted'); - - expect(deleted).toHaveLength(1); - expect(deleted[0][0].message).toMatch(`#${mockRunnerId}`); - expect(deleted[0][0].message).toMatch(`${mockRunner.shortSha}`); - }); - - it('evicts runner from apollo cache', () => { - expect(apolloCache.evict).toHaveBeenCalledWith({ - id: apolloCache.identify(mockRunner), - }); - expect(apolloCache.gc).toHaveBeenCalled(); - }); - }); - - describe('When update fails', () => { - describe('On a network error', () => { - const mockErrorMsg = 'Update error!'; - - beforeEach(async () => { - runnerDeleteHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - - await clickOkAndWait(); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(mockErrorMsg), - component: 'RunnerDeleteButton', - }); - }); - - it('error is shown to the user', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - }); - }); - - describe('On a validation error', () => { - const mockErrorMsg = 'Runner not found!'; - const mockErrorMsg2 = 'User not allowed!'; - - beforeEach(async () => { - runnerDeleteHandler.mockResolvedValueOnce({ - data: { - runnerDelete: { - errors: [mockErrorMsg, mockErrorMsg2], - }, - }, - }); - - await clickOkAndWait(); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`), - component: 'RunnerDeleteButton', - }); - }); - - it('error is shown to the user', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - }); - - it('does not evict runner from apollo cache', () => { - expect(apolloCache.evict).not.toHaveBeenCalled(); - expect(apolloCache.gc).not.toHaveBeenCalled(); - }); - }); - }); - - describe('When displaying a compact button for an active runner', () => { - beforeEach(() => { - createComponent({ - props: { - runner: { - active: true, - }, - compact: true, - }, - mountFn: mountExtended, - }); - }); - - it('Displays no text', () => { - expect(findBtn().text()).toBe(''); - expect(findBtn().classes('btn-icon')).toBe(true); - }); - - it('Display correctly for screen readers', () => { - expect(findBtn().attributes('aria-label')).toBe(I18N_DELETE_RUNNER); - expect(getTooltip()).toBe(I18N_DELETE_RUNNER); - }); - - describe('Immediately after the button is clicked', () => { - beforeEach(async () => { - findModal().vm.$emit('primary'); - }); - - it('The button has a loading state', async () => { - expect(findBtn().props('loading')).toBe(true); - }); - - it('The stale tooltip is removed', async () => { - expect(getTooltip()).toBe(''); - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_delete_modal_spec.js b/spec/frontend/runner/components/runner_delete_modal_spec.js deleted file mode 100644 index 3e5b634d815..00000000000 --- a/spec/frontend/runner/components/runner_delete_modal_spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue'; - -describe('RunnerDeleteModal', () => { - let wrapper; - - const findGlModal = () => wrapper.findComponent(GlModal); - - const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { - wrapper = mountFn(RunnerDeleteModal, { - attachTo: document.body, - propsData: { - runnerName: '#99 (AABBCCDD)', - ...props, - }, - attrs: { - modalId: 'delete-runner-modal-99', - }, - }); - }; - - it('Displays title', () => { - createComponent(); - - expect(findGlModal().props('title')).toBe('Delete runner #99 (AABBCCDD)?'); - }); - - it('Displays buttons', () => { - createComponent(); - - expect(findGlModal().props('actionPrimary')).toMatchObject({ text: 'Delete runner' }); - expect(findGlModal().props('actionCancel')).toMatchObject({ text: 'Cancel' }); - }); - - it('Displays contents', () => { - createComponent(); - - expect(findGlModal().html()).toContain( - 'The runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?', - ); - }); - - describe('When modal is confirmed by the user', () => { - let hideModalSpy; - - beforeEach(() => { - createComponent({}, mount); - hideModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide').mockImplementation(() => {}); - }); - - it('Modal gets hidden', () => { - expect(hideModalSpy).toHaveBeenCalledTimes(0); - - findGlModal().vm.$emit('primary'); - - expect(hideModalSpy).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js deleted file mode 100644 index e6cc936e260..00000000000 --- a/spec/frontend/runner/components/runner_details_spec.js +++ /dev/null @@ -1,130 +0,0 @@ -import { GlSprintf, GlIntersperse } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import { useFakeDate } from 'helpers/fake_date'; -import { findDd } from 'helpers/dl_locator_helper'; -import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; - -import RunnerDetails from '~/runner/components/runner_details.vue'; -import RunnerDetail from '~/runner/components/runner_detail.vue'; -import RunnerGroups from '~/runner/components/runner_groups.vue'; -import RunnerTags from '~/runner/components/runner_tags.vue'; -import RunnerTag from '~/runner/components/runner_tag.vue'; - -import { runnerData, runnerWithGroupData } from '../mock_data'; - -const mockRunner = runnerData.data.runner; -const mockGroupRunner = runnerWithGroupData.data.runner; - -describe('RunnerDetails', () => { - let wrapper; - const mockNow = '2021-01-15T12:00:00Z'; - const mockOneHourAgo = '2021-01-15T11:00:00Z'; - - useFakeDate(mockNow); - - const findDetailGroups = () => wrapper.findComponent(RunnerGroups); - - const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RunnerDetails, { - propsData: { - ...props, - }, - stubs: { - RunnerDetail, - ...stubs, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('Details tab', () => { - describe.each` - field | runner | expectedValue - ${'Description'} | ${{ description: 'My runner' }} | ${'My runner'} - ${'Description'} | ${{ description: null }} | ${'None'} - ${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'} - ${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'} - ${'Version'} | ${{ version: '12.3' }} | ${'12.3'} - ${'Version'} | ${{ version: null }} | ${'None'} - ${'Executor'} | ${{ executorName: 'shell' }} | ${'shell'} - ${'Architecture'} | ${{ architectureName: 'amd64' }} | ${'amd64'} - ${'Platform'} | ${{ platformName: 'darwin' }} | ${'darwin'} - ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'} - ${'IP Address'} | ${{ ipAddress: null }} | ${'None'} - ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'} - ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'} - ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'} - ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'} - ${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'} - ${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'} - ${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'} - ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'} - ${'Token expiry'} | ${{ tokenExpiresAt: mockOneHourAgo }} | ${'1 hour ago'} - ${'Token expiry'} | ${{ tokenExpiresAt: null }} | ${'Never expires'} - `('"$field" field', ({ field, runner, expectedValue }) => { - beforeEach(() => { - createComponent({ - props: { - runner: { - ...mockRunner, - ...runner, - }, - }, - stubs: { - GlIntersperse, - GlSprintf, - TimeAgo, - }, - }); - }); - - it(`displays expected value "${expectedValue}"`, () => { - expect(findDd(field, wrapper).text()).toBe(expectedValue); - }); - }); - - describe('"Tags" field', () => { - const stubs = { RunnerTags, RunnerTag }; - - it('displays expected value "tag-1 tag-2"', () => { - createComponent({ - props: { - runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] }, - }, - stubs, - }); - - expect(findDd('Tags', wrapper).text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2'); - }); - - it('displays "None" when runner has no tags', () => { - createComponent({ - props: { - runner: { ...mockRunner, tagList: [] }, - }, - stubs, - }); - - expect(findDd('Tags', wrapper).text().replace(/\s+/g, ' ')).toBe('None'); - }); - }); - - describe('Group runners', () => { - beforeEach(() => { - createComponent({ - props: { - runner: mockGroupRunner, - }, - }); - }); - - it('Shows a group runner details', () => { - expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner); - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_edit_button_spec.js b/spec/frontend/runner/components/runner_edit_button_spec.js deleted file mode 100644 index 428c1ef07e9..00000000000 --- a/spec/frontend/runner/components/runner_edit_button_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -describe('RunnerEditButton', () => { - let wrapper; - - const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value; - - const createComponent = ({ attrs = {}, mountFn = shallowMount } = {}) => { - wrapper = mountFn(RunnerEditButton, { - attrs, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays Edit text', () => { - expect(wrapper.attributes('aria-label')).toBe('Edit'); - }); - - it('Displays Edit tooltip', () => { - expect(getTooltipValue()).toBe('Edit'); - }); - - it('Renders a link and adds an href attribute', () => { - createComponent({ attrs: { href: '/edit' }, mountFn: mount }); - - expect(wrapper.element.tagName).toBe('A'); - expect(wrapper.attributes('href')).toBe('/edit'); - }); -}); diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js deleted file mode 100644 index c92e19f9263..00000000000 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ /dev/null @@ -1,188 +0,0 @@ -import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; -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 { - PARAM_KEY_STATUS, - PARAM_KEY_TAG, - STATUS_ONLINE, - INSTANCE_TYPE, - DEFAULT_MEMBERSHIP, - DEFAULT_SORT, - CONTACTED_DESC, -} 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'; - -const mockSearch = { - runnerType: null, - membership: DEFAULT_MEMBERSHIP, - filters: [], - pagination: { page: 1 }, - sort: DEFAULT_SORT, -}; - -describe('RunnerList', () => { - let wrapper; - - const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); - const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); - const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem); - - const mockOtherSort = CONTACTED_DESC; - const mockFilters = [ - { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, - { type: 'filtered-search-term', value: { data: '' } }, - ]; - - const expectToHaveLastEmittedInput = (value) => { - const inputs = wrapper.emitted('input'); - expect(inputs[inputs.length - 1][0]).toEqual(value); - }; - - const createComponent = ({ props = {}, options = {} } = {}) => { - wrapper = shallowMountExtended(RunnerFilteredSearchBar, { - propsData: { - namespace: 'runners', - tokens: [], - value: mockSearch, - ...props, - }, - stubs: { - FilteredSearch, - GlFilteredSearch, - GlDropdown, - GlDropdownItem, - }, - ...options, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('binds a namespace to the filtered search', () => { - expect(findFilteredSearch().props('namespace')).toBe('runners'); - }); - - it('sets sorting options', () => { - const SORT_OPTIONS_COUNT = 2; - - expect(findSortOptions()).toHaveLength(SORT_OPTIONS_COUNT); - expect(findSortOptions().at(0).text()).toBe('Created date'); - expect(findSortOptions().at(1).text()).toBe('Last contact'); - }); - - it('sets tokens to the filtered search', () => { - createComponent({ - props: { - tokens: [statusTokenConfig, tagTokenConfig], - }, - }); - - expect(findFilteredSearch().props('tokens')).toEqual([ - expect.objectContaining({ - type: PARAM_KEY_STATUS, - token: BaseToken, - options: expect.any(Array), - }), - expect.objectContaining({ - type: PARAM_KEY_TAG, - token: TagToken, - }), - ]); - }); - - it('can be configured with null or undefined tokens, which are ignored', () => { - createComponent({ - props: { - tokens: [statusTokenConfig, null, undefined], - }, - }); - - expect(findFilteredSearch().props('tokens')).toEqual([statusTokenConfig]); - }); - - it('fails validation for v-model with the wrong shape', () => { - expect(() => { - createComponent({ props: { value: { filters: 'wrong_filters', sort: 'sort' } } }); - }).toThrow('Invalid prop: custom validator check failed'); - - expect(() => { - createComponent({ props: { value: { sort: 'sort' } } }); - }).toThrow('Invalid prop: custom validator check failed'); - }); - - describe('when a search is preselected', () => { - beforeEach(() => { - createComponent({ - props: { - value: { - runnerType: INSTANCE_TYPE, - membership: DEFAULT_MEMBERSHIP, - sort: mockOtherSort, - filters: mockFilters, - }, - }, - }); - }); - - it('filter values are shown', () => { - expect(findGlFilteredSearch().props('value')).toMatchObject(mockFilters); - }); - - it('sort option is selected', () => { - expect( - findSortOptions() - .filter((w) => w.props('isChecked')) - .at(0) - .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, - membership: DEFAULT_MEMBERSHIP, - filters: mockFilters, - sort: mockOtherSort, - pagination: {}, - }); - }); - }); - - it('when the user sets a filter, the "search" is emitted with filters', () => { - findGlFilteredSearch().vm.$emit('input', mockFilters); - findGlFilteredSearch().vm.$emit('submit'); - - expectToHaveLastEmittedInput({ - runnerType: null, - membership: DEFAULT_MEMBERSHIP, - filters: mockFilters, - sort: DEFAULT_SORT, - pagination: {}, - }); - }); - - it('when the user sets a sorting method, the "search" is emitted with the sort', () => { - findSortOptions().at(1).vm.$emit('click'); - - expectToHaveLastEmittedInput({ - runnerType: null, - membership: DEFAULT_MEMBERSHIP, - filters: [], - sort: mockOtherSort, - pagination: {}, - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_groups_spec.js b/spec/frontend/runner/components/runner_groups_spec.js deleted file mode 100644 index b83733b9972..00000000000 --- a/spec/frontend/runner/components/runner_groups_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -import RunnerGroups from '~/runner/components/runner_groups.vue'; -import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue'; - -import { runnerData, runnerWithGroupData } from '../mock_data'; - -const mockInstanceRunner = runnerData.data.runner; -const mockGroupRunner = runnerWithGroupData.data.runner; -const mockGroup = mockGroupRunner.groups.nodes[0]; - -describe('RunnerGroups', () => { - let wrapper; - - const findHeading = () => wrapper.find('h3'); - const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem); - - const createComponent = ({ runner = mockGroupRunner, mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RunnerGroups, { - propsData: { - runner, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Shows a heading', () => { - createComponent(); - - expect(findHeading().text()).toBe('Assigned Group'); - }); - - describe('When there is a group runner', () => { - beforeEach(() => { - createComponent(); - }); - - it('Shows a project', () => { - createComponent(); - - const item = findRunnerAssignedItems().at(0); - const { webUrl, name, fullName, avatarUrl } = mockGroup; - - expect(item.props()).toMatchObject({ - href: webUrl, - name, - fullName, - avatarUrl, - }); - }); - }); - - describe('When there are no groups', () => { - beforeEach(() => { - createComponent({ - runner: mockInstanceRunner, - }); - }); - - it('Shows a "None" label', () => { - expect(wrapper.findByText('None').exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js deleted file mode 100644 index 701d39108cb..00000000000 --- a/spec/frontend/runner/components/runner_header_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { I18N_STATUS_ONLINE, I18N_GROUP_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants'; -import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; - -import RunnerHeader from '~/runner/components/runner_header.vue'; -import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue'; -import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue'; - -import { runnerData } from '../mock_data'; - -const mockRunner = runnerData.data.runner; - -describe('RunnerHeader', () => { - let wrapper; - - const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge); - const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge); - const findRunnerLockedIcon = () => wrapper.findByTestId('lock-icon'); - const findTimeAgo = () => wrapper.findComponent(TimeAgo); - - const createComponent = ({ runner = {}, options = {}, mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RunnerHeader, { - propsData: { - runner: { - ...mockRunner, - ...runner, - }, - }, - stubs: { - GlSprintf, - TimeAgo, - }, - ...options, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('displays the runner status', () => { - createComponent({ - mountFn: mountExtended, - runner: { - status: STATUS_ONLINE, - }, - }); - - expect(findRunnerStatusBadge().text()).toContain(I18N_STATUS_ONLINE); - }); - - it('displays the runner type', () => { - createComponent({ - mountFn: mountExtended, - runner: { - runnerType: GROUP_TYPE, - }, - }); - - expect(findRunnerTypeBadge().text()).toContain(I18N_GROUP_TYPE); - }); - - it('displays the runner id', () => { - createComponent({ - runner: { - id: convertToGraphQLId(TYPE_CI_RUNNER, 99), - }, - }); - - expect(wrapper.text()).toContain('Runner #99'); - }); - - it('displays the runner locked icon', () => { - createComponent({ - runner: { - locked: true, - }, - mountFn: mountExtended, - }); - - expect(findRunnerLockedIcon().exists()).toBe(true); - }); - - it('displays the runner creation time', () => { - createComponent(); - - expect(wrapper.text()).toMatch(/created .+/); - expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt); - }); - - it('does not display runner creation time if "createdAt" is missing', () => { - createComponent({ - runner: { - id: convertToGraphQLId(TYPE_CI_RUNNER, 99), - createdAt: null, - }, - }); - - expect(wrapper.text()).toContain('Runner #99'); - expect(wrapper.text()).not.toMatch(/created .+/); - expect(findTimeAgo().exists()).toBe(false); - }); - - it('displays actions in a slot', () => { - createComponent({ - options: { - slots: { - actions: '<div data-testid="actions-content">My Actions</div>', - }, - mountFn: mountExtended, - }, - }); - - expect(wrapper.findByTestId('actions-content').text()).toBe('My Actions'); - }); -}); diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js deleted file mode 100644 index 4d38afb25ee..00000000000 --- a/spec/frontend/runner/components/runner_jobs_spec.js +++ /dev/null @@ -1,155 +0,0 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; -import RunnerJobs from '~/runner/components/runner_jobs.vue'; -import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue'; -import RunnerPagination from '~/runner/components/runner_pagination.vue'; -import { captureException } from '~/runner/sentry_utils'; -import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants'; - -import runnerJobsQuery from '~/runner/graphql/show/runner_jobs.query.graphql'; - -import { runnerData, runnerJobsData } from '../mock_data'; - -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); - -const mockRunner = runnerData.data.runner; -const mockRunnerWithJobs = runnerJobsData.data.runner; -const mockJobs = mockRunnerWithJobs.jobs.nodes; - -Vue.use(VueApollo); - -describe('RunnerJobs', () => { - let wrapper; - let mockRunnerJobsQuery; - - const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader); - const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable); - const findRunnerPagination = () => wrapper.findComponent(RunnerPagination); - - const createComponent = ({ mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RunnerJobs, { - apolloProvider: createMockApollo([[runnerJobsQuery, mockRunnerJobsQuery]]), - propsData: { - runner: mockRunner, - }, - }); - }; - - beforeEach(() => { - mockRunnerJobsQuery = jest.fn(); - }); - - afterEach(() => { - mockRunnerJobsQuery.mockReset(); - wrapper.destroy(); - }); - - it('Requests runner jobs', async () => { - createComponent(); - - await waitForPromises(); - - expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(1); - expect(mockRunnerJobsQuery).toHaveBeenCalledWith({ - id: mockRunner.id, - first: RUNNER_DETAILS_JOBS_PAGE_SIZE, - }); - }); - - describe('When there are jobs assigned', () => { - beforeEach(async () => { - mockRunnerJobsQuery.mockResolvedValueOnce(runnerJobsData); - - createComponent(); - await waitForPromises(); - }); - - it('Shows jobs', () => { - const jobs = findRunnerJobsTable().props('jobs'); - - expect(jobs).toEqual(mockJobs); - }); - - describe('When "Next" page is clicked', () => { - beforeEach(async () => { - findRunnerPagination().vm.$emit('input', { page: 2, after: 'AFTER_CURSOR' }); - - await waitForPromises(); - }); - - it('A new page is requested', () => { - expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(2); - expect(mockRunnerJobsQuery).toHaveBeenLastCalledWith({ - id: mockRunner.id, - first: RUNNER_DETAILS_JOBS_PAGE_SIZE, - after: 'AFTER_CURSOR', - }); - }); - }); - }); - - describe('When loading', () => { - it('shows loading indicator and no other content', () => { - createComponent(); - - expect(findGlSkeletonLoading().exists()).toBe(true); - expect(findRunnerJobsTable().exists()).toBe(false); - expect(findRunnerPagination().attributes('disabled')).toBe('true'); - }); - }); - - describe('When there are no jobs', () => { - beforeEach(async () => { - mockRunnerJobsQuery.mockResolvedValueOnce({ - data: { - runner: { - id: mockRunner.id, - projectCount: 0, - jobs: { - nodes: [], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - }, - }, - }, - }); - - createComponent(); - await waitForPromises(); - }); - - it('Shows a "None" label', () => { - expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND); - }); - }); - - describe('When an error occurs', () => { - beforeEach(async () => { - mockRunnerJobsQuery.mockRejectedValue(new Error('Error!')); - - createComponent(); - await waitForPromises(); - }); - - it('shows an error', () => { - expect(createAlert).toHaveBeenCalled(); - }); - - it('reports an error', () => { - expect(captureException).toHaveBeenCalledWith({ - component: 'RunnerJobs', - error: expect.any(Error), - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_jobs_table_spec.js b/spec/frontend/runner/components/runner_jobs_table_spec.js deleted file mode 100644 index 5f4905ad2a8..00000000000 --- a/spec/frontend/runner/components/runner_jobs_table_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import { GlTableLite } from '@gitlab/ui'; -import { - extendedWrapper, - shallowMountExtended, - mountExtended, -} from 'helpers/vue_test_utils_helper'; -import { __, s__ } from '~/locale'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue'; -import { useFakeDate } from 'helpers/fake_date'; -import { runnerJobsData } from '../mock_data'; - -const mockJobs = runnerJobsData.data.runner.jobs.nodes; - -describe('RunnerJobsTable', () => { - let wrapper; - const mockNow = '2021-01-15T12:00:00Z'; - const mockOneHourAgo = '2021-01-15T11:00:00Z'; - - useFakeDate(mockNow); - - const findTable = () => wrapper.findComponent(GlTableLite); - const findHeaders = () => wrapper.findAll('th'); - const findRows = () => wrapper.findAll('[data-testid^="job-row-"]'); - const findCell = ({ field }) => - extendedWrapper(findRows().at(0).find(`[data-testid="td-${field}"]`)); - - const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => { - wrapper = mountFn(RunnerJobsTable, { - propsData: { - jobs: mockJobs, - ...props, - }, - stubs: { - GlTableLite, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Sets job id as a row key', () => { - createComponent(); - - expect(findTable().attributes('primarykey')).toBe('id'); - }); - - describe('Table data', () => { - beforeEach(() => { - createComponent({}, mountExtended); - }); - - it('Displays headers', () => { - const headerLabels = findHeaders().wrappers.map((w) => w.text()); - - expect(headerLabels).toEqual([ - s__('Job|Status'), - __('Job'), - __('Project'), - __('Commit'), - s__('Job|Finished at'), - s__('Runners|Tags'), - ]); - }); - - it('Displays a list of jobs', () => { - expect(findRows()).toHaveLength(1); - }); - - it('Displays details of a job', () => { - const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0]; - - expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text); - - expect(findCell({ field: 'job' }).text()).toContain(`#${getIdFromGraphQLId(id)}`); - expect(findCell({ field: 'job' }).find('a').attributes('href')).toBe( - detailedStatus.detailsPath, - ); - - expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name); - expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe( - pipeline.project.webUrl, - ); - - expect(findCell({ field: 'commit' }).text()).toBe(shortSha); - expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath); - }); - }); - - describe('Table data formatting', () => { - let mockJobsCopy; - - beforeEach(() => { - mockJobsCopy = [ - { - ...mockJobs[0], - }, - ]; - }); - - it('Formats finishedAt time', () => { - mockJobsCopy[0].finishedAt = mockOneHourAgo; - - createComponent({ props: { jobs: mockJobsCopy } }, mountExtended); - - expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago'); - }); - - it('Formats tags', () => { - mockJobsCopy[0].tags = ['tag-1', 'tag-2']; - - createComponent({ props: { jobs: mockJobsCopy } }, mountExtended); - - expect(findCell({ field: 'tags' }).text()).toMatchInterpolatedText('tag-1 tag-2'); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_list_empty_state_spec.js b/spec/frontend/runner/components/runner_list_empty_state_spec.js deleted file mode 100644 index 038162b889e..00000000000 --- a/spec/frontend/runner/components/runner_list_empty_state_spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; - -import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue'; - -const mockSvgPath = 'mock-svg-path.svg'; -const mockFilteredSvgPath = 'mock-filtered-svg-path.svg'; -const mockRegistrationToken = 'REGISTRATION_TOKEN'; - -describe('RunnerListEmptyState', () => { - let wrapper; - - const findEmptyState = () => wrapper.findComponent(GlEmptyState); - const findLink = () => wrapper.findComponent(GlLink); - const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal); - - const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RunnerListEmptyState, { - propsData: { - svgPath: mockSvgPath, - filteredSvgPath: mockFilteredSvgPath, - registrationToken: mockRegistrationToken, - ...props, - }, - directives: { - GlModal: createMockDirective(), - }, - stubs: { - GlEmptyState, - GlSprintf, - GlLink, - }, - }); - }; - - describe('when search is not filtered', () => { - const title = s__('Runners|Get started with runners'); - - describe('when there is a registration token', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders an illustration', () => { - expect(findEmptyState().props('svgPath')).toBe(mockSvgPath); - }); - - it('displays "no results" text with instructions', () => { - const desc = s__( - 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', - ); - - expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); - }); - - it('opens a runner registration instructions modal with a link', () => { - const { value } = getBinding(findLink().element, 'gl-modal'); - - expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); - }); - }); - - describe('when there is no registration token', () => { - beforeEach(() => { - createComponent({ props: { registrationToken: null } }); - }); - - it('renders an illustration', () => { - expect(findEmptyState().props('svgPath')).toBe(mockSvgPath); - }); - - it('displays "no results" text', () => { - const desc = s__( - 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.', - ); - - expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); - }); - - it('has no registration instructions link', () => { - expect(findLink().exists()).toBe(false); - }); - }); - }); - - describe('when search is filtered', () => { - beforeEach(() => { - createComponent({ props: { isSearchFiltered: true } }); - }); - - it('renders a "filtered search" illustration', () => { - expect(findEmptyState().props('svgPath')).toBe(mockFilteredSvgPath); - }); - - it('displays "no filtered results" text', () => { - expect(findEmptyState().text()).toContain(s__('Runners|No results found')); - expect(findEmptyState().text()).toContain(s__('Runners|Edit your search and try again')); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js deleted file mode 100644 index a31990f8f7e..00000000000 --- a/spec/frontend/runner/components/runner_list_spec.js +++ /dev/null @@ -1,231 +0,0 @@ -import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import { - extendedWrapper, - shallowMountExtended, - mountExtended, -} from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { s__ } from '~/locale'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { createLocalState } from '~/runner/graphql/list/local_state'; - -import RunnerList from '~/runner/components/runner_list.vue'; -import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; -import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue'; - -import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants'; -import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; - -const mockRunners = allRunnersData.data.runners.nodes; -const mockActiveRunnersCount = mockRunners.length; - -describe('RunnerList', () => { - let wrapper; - let cacheConfig; - let localMutations; - - const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - const findTable = () => wrapper.findComponent(GlTableLite); - const findHeaders = () => wrapper.findAll('th'); - const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]'); - const findCell = ({ row = 0, fieldKey }) => - extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`)); - const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete); - const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox); - - const createComponent = ( - { props = {}, provide = {}, ...options } = {}, - mountFn = shallowMountExtended, - ) => { - ({ cacheConfig, localMutations } = createLocalState()); - - wrapper = mountFn(RunnerList, { - apolloProvider: createMockApollo([], {}, cacheConfig), - propsData: { - runners: mockRunners, - activeRunnersCount: mockActiveRunnersCount, - ...props, - }, - provide: { - localMutations, - onlineContactTimeoutSecs, - staleTimeoutSecs, - ...provide, - }, - ...options, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays headers', () => { - createComponent( - { - stubs: { - HelpPopover: { - template: '<div/>', - }, - }, - }, - mountExtended, - ); - - const headerLabels = findHeaders().wrappers.map((w) => w.text()); - - expect(findHeaders().at(0).findComponent(HelpPopover).exists()).toBe(true); - expect(findHeaders().at(2).findComponent(HelpPopover).exists()).toBe(true); - - expect(headerLabels).toEqual([ - s__('Runners|Status'), - s__('Runners|Runner'), - s__('Runners|Owner'), - '', // actions has no label - ]); - }); - - it('Sets runner id as a row key', () => { - createComponent(); - - expect(findTable().attributes('primary-key')).toBe('id'); - }); - - it('Displays a list of runners', () => { - createComponent({}, mountExtended); - - expect(findRows()).toHaveLength(4); - - expect(findSkeletonLoader().exists()).toBe(false); - }); - - it('Displays details of a runner', () => { - createComponent({}, mountExtended); - - const { id, description, version, shortSha } = mockRunners[0]; - const numericId = getIdFromGraphQLId(id); - - // Badges - expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( - I18N_STATUS_NEVER_CONTACTED, - ); - - // Runner summary - const summary = findCell({ fieldKey: 'summary' }).text(); - - expect(summary).toContain(`#${numericId} (${shortSha})`); - expect(summary).toContain(I18N_PROJECT_TYPE); - - expect(summary).toContain(version); - expect(summary).toContain(description); - - expect(summary).toContain('Last contact'); - expect(summary).toContain('0'); // job count - expect(summary).toContain('Created'); - - // Actions - expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); - }); - - describe('When the list is checkable', () => { - beforeEach(() => { - createComponent( - { - props: { - checkable: true, - }, - }, - mountExtended, - ); - }); - - it('runner bulk delete is available', () => { - expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners); - }); - - it('runner bulk delete checkbox is available', () => { - expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners); - }); - - it('Displays a checkbox field', () => { - expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true); - }); - - it('Sets a runner as checked', async () => { - const runner = mockRunners[0]; - const setRunnerCheckedMock = jest - .spyOn(localMutations, 'setRunnerChecked') - .mockImplementation(() => {}); - - const checkbox = findCell({ fieldKey: 'checkbox' }).find('input'); - await checkbox.setChecked(); - - expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1); - expect(setRunnerCheckedMock).toHaveBeenCalledWith({ - runner, - isChecked: true, - }); - }); - - it('Emits a deleted event', async () => { - const event = { message: 'Deleted!' }; - findRunnerBulkDelete().vm.$emit('deleted', event); - - expect(wrapper.emitted('deleted')).toEqual([[event]]); - }); - }); - - describe('Scoped cell slots', () => { - it('Render #runner-name slot in "summary" cell', () => { - createComponent( - { - scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` }, - }, - mountExtended, - ); - - expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`); - }); - - it('Render #runner-actions-cell slot in "actions" cell', () => { - createComponent( - { - scopedSlots: { 'runner-actions-cell': ({ runner }) => `Actions: ${runner.id}` }, - }, - mountExtended, - ); - - expect(findCell({ fieldKey: 'actions' }).text()).toBe(`Actions: ${mockRunners[0].id}`); - }); - }); - - it('Shows runner identifier', () => { - const { id, shortSha } = mockRunners[0]; - const numericId = getIdFromGraphQLId(id); - - createComponent({}, mountExtended); - - expect(findCell({ fieldKey: 'summary' }).text()).toContain(`#${numericId} (${shortSha})`); - }); - - describe('When data is loading', () => { - it('shows a busy state', () => { - createComponent({ props: { runners: [], loading: true } }); - - expect(findTable().classes('gl-opacity-6')).toBe(true); - }); - - it('when there are no runners, shows an skeleton loader', () => { - createComponent({ props: { runners: [], loading: true } }, mountExtended); - - expect(findSkeletonLoader().exists()).toBe(true); - }); - - it('when there are runners, shows a busy indicator skeleton loader', () => { - createComponent({ props: { loading: true } }, mountExtended); - - expect(findSkeletonLoader().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_membership_toggle_spec.js b/spec/frontend/runner/components/runner_membership_toggle_spec.js deleted file mode 100644 index 1a7ae22618a..00000000000 --- a/spec/frontend/runner/components/runner_membership_toggle_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { GlToggle } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; -import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue'; -import { - I18N_SHOW_ONLY_INHERITED, - MEMBERSHIP_DESCENDANTS, - MEMBERSHIP_ALL_AVAILABLE, -} from '~/runner/constants'; - -describe('RunnerMembershipToggle', () => { - let wrapper; - - const findToggle = () => wrapper.findComponent(GlToggle); - - const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { - wrapper = mountFn(RunnerMembershipToggle, { - propsData: props, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays text', () => { - createComponent({ mountFn: mount }); - - expect(wrapper.text()).toBe(I18N_SHOW_ONLY_INHERITED); - }); - - it.each` - membershipValue | toggleValue - ${MEMBERSHIP_DESCENDANTS} | ${true} - ${MEMBERSHIP_ALL_AVAILABLE} | ${false} - `( - 'Displays a membership of $membershipValue as enabled=$toggleValue', - ({ membershipValue, toggleValue }) => { - createComponent({ props: { value: membershipValue } }); - - expect(findToggle().props('value')).toBe(toggleValue); - }, - ); - - it.each` - changeEvt | membershipValue - ${true} | ${MEMBERSHIP_DESCENDANTS} - ${false} | ${MEMBERSHIP_ALL_AVAILABLE} - `( - 'Emits $changeEvt when value is changed to $membershipValue', - ({ changeEvt, membershipValue }) => { - createComponent(); - findToggle().vm.$emit('change', changeEvt); - - expect(wrapper.emitted('input')).toStrictEqual([[membershipValue]]); - }, - ); -}); diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/runner/components/runner_pagination_spec.js deleted file mode 100644 index 499cc59250d..00000000000 --- a/spec/frontend/runner/components/runner_pagination_spec.js +++ /dev/null @@ -1,115 +0,0 @@ -import { GlKeysetPagination } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerPagination from '~/runner/components/runner_pagination.vue'; - -const mockStartCursor = 'START_CURSOR'; -const mockEndCursor = 'END_CURSOR'; - -describe('RunnerPagination', () => { - let wrapper; - - const findPagination = () => wrapper.findComponent(GlKeysetPagination); - - const createComponent = (propsData = {}) => { - wrapper = shallowMount(RunnerPagination, { - propsData, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('When in between pages', () => { - const mockPageInfo = { - startCursor: mockStartCursor, - endCursor: mockEndCursor, - hasPreviousPage: true, - hasNextPage: true, - }; - - beforeEach(() => { - createComponent({ - pageInfo: mockPageInfo, - }); - }); - - it('Contains the current page information', () => { - expect(findPagination().props()).toMatchObject(mockPageInfo); - }); - - it('Goes to the prev page', () => { - findPagination().vm.$emit('prev'); - - expect(wrapper.emitted('input')[0]).toEqual([ - { - before: mockStartCursor, - }, - ]); - }); - - it('Goes to the next page', () => { - findPagination().vm.$emit('next'); - - expect(wrapper.emitted('input')[0]).toEqual([ - { - after: mockEndCursor, - }, - ]); - }); - }); - - describe.each` - page | hasPreviousPage | hasNextPage - ${'first'} | ${false} | ${true} - ${'last'} | ${true} | ${false} - `('When on the $page page', ({ page, hasPreviousPage, hasNextPage }) => { - const mockPageInfo = { - startCursor: mockStartCursor, - endCursor: mockEndCursor, - hasPreviousPage, - hasNextPage, - }; - - beforeEach(() => { - createComponent({ - pageInfo: mockPageInfo, - }); - }); - - it(`Contains the ${page} page information`, () => { - expect(findPagination().props()).toMatchObject(mockPageInfo); - }); - }); - - describe('When no other pages', () => { - beforeEach(() => { - createComponent({ - pageInfo: { - hasPreviousPage: false, - hasNextPage: false, - }, - }); - }); - - it('is not shown', () => { - expect(findPagination().exists()).toBe(false); - }); - }); - - describe('When adding more attributes', () => { - beforeEach(() => { - createComponent({ - pageInfo: { - hasPreviousPage: true, - hasNextPage: false, - }, - disabled: true, - }); - }); - - it('attributes are passed', () => { - expect(findPagination().props('disabled')).toBe(true); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js deleted file mode 100644 index 61476007571..00000000000 --- a/spec/frontend/runner/components/runner_pause_button_spec.js +++ /dev/null @@ -1,263 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import { GlButton } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; -import runnerToggleActiveMutation from '~/runner/graphql/shared/runner_toggle_active.mutation.graphql'; -import waitForPromises from 'helpers/wait_for_promises'; -import { captureException } from '~/runner/sentry_utils'; -import { createAlert } from '~/flash'; -import { - I18N_PAUSE, - I18N_PAUSE_TOOLTIP, - I18N_RESUME, - I18N_RESUME_TOOLTIP, -} from '~/runner/constants'; - -import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; -import { allRunnersData } from '../mock_data'; - -const mockRunner = allRunnersData.data.runners.nodes[0]; - -Vue.use(VueApollo); - -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); - -describe('RunnerPauseButton', () => { - let wrapper; - let runnerToggleActiveHandler; - - const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value; - const findBtn = () => wrapper.findComponent(GlButton); - - const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { - const { runner, ...propsData } = props; - - wrapper = mountFn(RunnerPauseButton, { - propsData: { - runner: { - id: mockRunner.id, - active: mockRunner.active, - ...runner, - }, - ...propsData, - }, - apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]), - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - const clickAndWait = async () => { - findBtn().vm.$emit('click'); - await waitForPromises(); - }; - - beforeEach(() => { - runnerToggleActiveHandler = jest.fn().mockImplementation(({ input }) => { - return Promise.resolve({ - data: { - runnerUpdate: { - runner: { - id: input.id, - active: input.active, - }, - errors: [], - }, - }, - }); - }); - - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('Pause/Resume action', () => { - describe.each` - runnerState | icon | content | tooltip | isActive | newActiveValue - ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${false} | ${true} - ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${true} | ${false} - `('When the runner is $runnerState', ({ icon, content, tooltip, isActive, newActiveValue }) => { - beforeEach(() => { - createComponent({ - props: { - runner: { - active: isActive, - }, - }, - }); - }); - - 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(runnerToggleActiveHandler).toHaveBeenCalledTimes(0); - }); - }); - - 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('The stale tooltip is removed', async () => { - await setup(); - - expect(getTooltip()).toBe(''); - }); - }); - - describe(`After clicking on the ${icon} button`, () => { - beforeEach(async () => { - await clickAndWait(); - }); - - it(`The mutation to that sets active to ${newActiveValue} is called`, async () => { - expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1); - expect(runnerToggleActiveHandler).toHaveBeenCalledWith({ - input: { - id: mockRunner.id, - active: newActiveValue, - }, - }); - }); - - it('The button does not have a loading state', () => { - expect(findBtn().props('loading')).toBe(false); - }); - - 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 () => { - runnerToggleActiveHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - - await clickAndWait(); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error(mockErrorMsg), - component: 'RunnerPauseButton', - }); - }); - - it('error is shown to the user', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - }); - }); - - describe('On a validation error', () => { - const mockErrorMsg = 'Runner not found!'; - const mockErrorMsg2 = 'User not allowed!'; - - beforeEach(async () => { - runnerToggleActiveHandler.mockResolvedValueOnce({ - data: { - runnerUpdate: { - runner: { - id: mockRunner.id, - active: isActive, - }, - 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); - }); - }); - }); - }); - }); - - describe('When displaying a compact button for an active runner', () => { - beforeEach(() => { - createComponent({ - props: { - runner: { - active: true, - }, - compact: true, - }, - mountFn: mountExtended, - }); - }); - - it('Displays no text', () => { - 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('Display correctly for screen readers', () => { - expect(findBtn().attributes('aria-label')).toBe(I18N_PAUSE); - expect(getTooltip()).toBe(I18N_PAUSE_TOOLTIP); - }); - - describe('Immediately after the 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('The stale tooltip is removed', async () => { - await setup(); - - expect(getTooltip()).toBe(''); - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js deleted file mode 100644 index c1c7351aab2..00000000000 --- a/spec/frontend/runner/components/runner_paused_badge_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { I18N_PAUSED } from '~/runner/constants'; - -describe('RunnerTypeBadge', () => { - let wrapper; - - const findBadge = () => wrapper.findComponent(GlBadge); - const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip'); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(RunnerStatePausedBadge, { - propsData: { - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders paused state', () => { - expect(wrapper.text()).toBe(I18N_PAUSED); - 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_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js deleted file mode 100644 index eca042cae86..00000000000 --- a/spec/frontend/runner/components/runner_projects_spec.js +++ /dev/null @@ -1,251 +0,0 @@ -import { GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; -import { sprintf } from '~/locale'; -import { - I18N_ASSIGNED_PROJECTS, - I18N_CLEAR_FILTER_PROJECTS, - I18N_FILTER_PROJECTS, - I18N_NO_PROJECTS_FOUND, - RUNNER_DETAILS_PROJECTS_PAGE_SIZE, -} from '~/runner/constants'; -import RunnerProjects from '~/runner/components/runner_projects.vue'; -import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue'; -import RunnerPagination from '~/runner/components/runner_pagination.vue'; -import { captureException } from '~/runner/sentry_utils'; - -import runnerProjectsQuery from '~/runner/graphql/show/runner_projects.query.graphql'; - -import { runnerData, runnerProjectsData } from '../mock_data'; - -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); - -const mockRunner = runnerData.data.runner; -const mockRunnerWithProjects = runnerProjectsData.data.runner; -const mockProjects = mockRunnerWithProjects.projects.nodes; - -Vue.use(VueApollo); - -describe('RunnerProjects', () => { - let wrapper; - let mockRunnerProjectsQuery; - - const findHeading = () => wrapper.find('h3'); - const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader); - const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); - const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem); - const findRunnerPagination = () => wrapper.findComponent(RunnerPagination); - - const createComponent = ({ mountFn = shallowMountExtended } = {}) => { - wrapper = mountFn(RunnerProjects, { - apolloProvider: createMockApollo([[runnerProjectsQuery, mockRunnerProjectsQuery]]), - propsData: { - runner: mockRunner, - }, - }); - }; - - beforeEach(() => { - mockRunnerProjectsQuery = jest.fn(); - }); - - afterEach(() => { - mockRunnerProjectsQuery.mockReset(); - wrapper.destroy(); - }); - - it('Requests runner projects', async () => { - createComponent(); - - await waitForPromises(); - - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); - expect(mockRunnerProjectsQuery).toHaveBeenCalledWith({ - id: mockRunner.id, - search: '', - first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, - }); - }); - - it('Shows a filter box', () => { - createComponent(); - - expect(findGlSearchBoxByType().attributes()).toMatchObject({ - clearbuttontitle: I18N_CLEAR_FILTER_PROJECTS, - debounce: '500', - placeholder: I18N_FILTER_PROJECTS, - }); - }); - - describe('When there are projects assigned', () => { - beforeEach(async () => { - mockRunnerProjectsQuery.mockResolvedValueOnce(runnerProjectsData); - - createComponent(); - await waitForPromises(); - }); - - it('Shows a heading', async () => { - const expected = sprintf(I18N_ASSIGNED_PROJECTS, { projectCount: mockProjects.length }); - - expect(findHeading().text()).toBe(expected); - }); - - it('Shows projects', () => { - expect(findRunnerAssignedItems().length).toBe(mockProjects.length); - }); - - it('Shows a project', () => { - const item = findRunnerAssignedItems().at(0); - const { webUrl, name, nameWithNamespace, avatarUrl } = mockProjects[0]; - - expect(item.props()).toMatchObject({ - href: webUrl, - name, - fullName: nameWithNamespace, - avatarUrl, - isOwner: true, // first project is always owner - }); - }); - - describe('When "Next" page is clicked', () => { - beforeEach(async () => { - findRunnerPagination().vm.$emit('input', { page: 3, after: 'AFTER_CURSOR' }); - - await waitForPromises(); - }); - - it('A new page is requested', () => { - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2); - expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ - id: mockRunner.id, - search: '', - first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, - after: 'AFTER_CURSOR', - }); - }); - - it('When "Prev" page is clicked, the previous page is requested', async () => { - findRunnerPagination().vm.$emit('input', { page: 2, before: 'BEFORE_CURSOR' }); - - await waitForPromises(); - - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3); - expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ - id: mockRunner.id, - search: '', - last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, - before: 'BEFORE_CURSOR', - }); - }); - - it('When user filters after paginating, the first page is requested', async () => { - findGlSearchBoxByType().vm.$emit('input', 'my search'); - await waitForPromises(); - - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3); - expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ - id: mockRunner.id, - search: 'my search', - first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, - }); - }); - }); - - describe('When user filters', () => { - it('Filtered results are requested', async () => { - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); - - findGlSearchBoxByType().vm.$emit('input', 'my search'); - await waitForPromises(); - - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2); - expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ - id: mockRunner.id, - search: 'my search', - first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, - }); - }); - - it('Filtered results are not requested for short searches', async () => { - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); - - findGlSearchBoxByType().vm.$emit('input', 'm'); - await waitForPromises(); - - findGlSearchBoxByType().vm.$emit('input', 'my'); - await waitForPromises(); - - expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('When loading', () => { - it('shows loading indicator and no other content', () => { - createComponent(); - - expect(findGlSkeletonLoading().exists()).toBe(true); - - expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(false); - expect(findRunnerAssignedItems().length).toBe(0); - - expect(findRunnerPagination().attributes('disabled')).toBe('true'); - expect(findGlSearchBoxByType().props('isLoading')).toBe(true); - }); - }); - - describe('When there are no projects', () => { - beforeEach(async () => { - mockRunnerProjectsQuery.mockResolvedValueOnce({ - data: { - runner: { - id: mockRunner.id, - projectCount: 0, - projects: { - nodes: [], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - endCursor: '', - }, - }, - }, - }, - }); - - createComponent(); - await waitForPromises(); - }); - - it('Shows a "None" label', () => { - expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(true); - }); - }); - - describe('When an error occurs', () => { - beforeEach(async () => { - mockRunnerProjectsQuery.mockRejectedValue(new Error('Error!')); - - createComponent(); - await waitForPromises(); - }); - - it('shows an error', () => { - expect(createAlert).toHaveBeenCalled(); - }); - - it('reports an error', () => { - expect(captureException).toHaveBeenCalledWith({ - component: 'RunnerProjects', - error: expect.any(Error), - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js deleted file mode 100644 index d1f04f0ee37..00000000000 --- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import { nextTick } from 'vue'; -import { GlBanner } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; - -describe('RunnerStackedLayoutBanner', () => { - let wrapper; - - const findBanner = () => wrapper.findComponent(GlBanner); - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - - const createComponent = ({ ...options } = {}, mountFn = shallowMount) => { - wrapper = mountFn(RunnerStackedLayoutBanner, { - ...options, - }); - }; - - it('Displays a banner', () => { - createComponent(); - - expect(findBanner().props()).toMatchObject({ - svgPath: expect.stringContaining('data:image/svg+xml;utf8,'), - title: expect.any(String), - buttonText: expect.any(String), - buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'), - }); - expect(findLocalStorageSync().exists()).toBe(true); - }); - - it('Does not display a banner when dismissed', async () => { - createComponent(); - - findLocalStorageSync().vm.$emit('input', true); - - await nextTick(); - - expect(findBanner().exists()).toBe(false); - expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal - }); -}); diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js deleted file mode 100644 index 9ab6378304f..00000000000 --- a/spec/frontend/runner/components/runner_status_badge_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { - I18N_STATUS_ONLINE, - I18N_STATUS_NEVER_CONTACTED, - I18N_STATUS_OFFLINE, - I18N_STATUS_STALE, - I18N_NEVER_CONTACTED_TOOLTIP, - I18N_STALE_NEVER_CONTACTED_TOOLTIP, - STATUS_ONLINE, - STATUS_OFFLINE, - STATUS_STALE, - STATUS_NEVER_CONTACTED, -} from '~/runner/constants'; - -describe('RunnerTypeBadge', () => { - let wrapper; - - const findBadge = () => wrapper.findComponent(GlBadge); - const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip'); - - const createComponent = (props = {}) => { - wrapper = shallowMount(RunnerStatusBadge, { - propsData: { - runner: { - contactedAt: '2020-12-31T23:59:00Z', - status: STATUS_ONLINE, - }, - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - beforeEach(() => { - jest.useFakeTimers('modern'); - jest.setSystemTime(new Date('2021-01-01T00:00:00Z')); - }); - - afterEach(() => { - jest.useFakeTimers('legacy'); - - wrapper.destroy(); - }); - - it('renders online state', () => { - createComponent(); - - expect(wrapper.text()).toBe(I18N_STATUS_ONLINE); - expect(findBadge().props('variant')).toBe('success'); - expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago'); - }); - - it('renders never contacted state', () => { - createComponent({ - runner: { - contactedAt: null, - status: STATUS_NEVER_CONTACTED, - }, - }); - - expect(wrapper.text()).toBe(I18N_STATUS_NEVER_CONTACTED); - expect(findBadge().props('variant')).toBe('muted'); - expect(getTooltip().value).toBe(I18N_NEVER_CONTACTED_TOOLTIP); - }); - - it('renders offline state', () => { - createComponent({ - runner: { - contactedAt: '2020-12-31T00:00:00Z', - status: STATUS_OFFLINE, - }, - }); - - expect(wrapper.text()).toBe(I18N_STATUS_OFFLINE); - expect(findBadge().props('variant')).toBe('muted'); - expect(getTooltip().value).toBe('Runner is offline; last contact was 1 day ago'); - }); - - it('renders stale state', () => { - createComponent({ - runner: { - contactedAt: '2020-01-01T00:00:00Z', - status: STATUS_STALE, - }, - }); - - expect(wrapper.text()).toBe(I18N_STATUS_STALE); - expect(findBadge().props('variant')).toBe('warning'); - expect(getTooltip().value).toBe('Runner is stale; last contact was 1 year ago'); - }); - - it('renders stale state with no contact time', () => { - createComponent({ - runner: { - contactedAt: null, - status: STATUS_STALE, - }, - }); - - expect(wrapper.text()).toBe(I18N_STATUS_STALE); - expect(findBadge().props('variant')).toBe('warning'); - expect(getTooltip().value).toBe(I18N_STALE_NEVER_CONTACTED_TOOLTIP); - }); - - describe('does not fail when data is missing', () => { - it('contacted_at is missing', () => { - createComponent({ - runner: { - contactedAt: null, - status: STATUS_ONLINE, - }, - }); - - expect(wrapper.text()).toBe(I18N_STATUS_ONLINE); - expect(getTooltip().value).toBe('Runner is online; last contact was never'); - }); - - it('status is missing', () => { - createComponent({ - runner: { - status: null, - }, - }); - - expect(wrapper.text()).toBe(''); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_status_popover_spec.js b/spec/frontend/runner/components/runner_status_popover_spec.js deleted file mode 100644 index 789283d1245..00000000000 --- a/spec/frontend/runner/components/runner_status_popover_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import { onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; - -describe('RunnerStatusPopover', () => { - let wrapper; - - const createComponent = ({ provide = {} } = {}) => { - wrapper = shallowMountExtended(RunnerStatusPopover, { - provide: { - onlineContactTimeoutSecs, - staleTimeoutSecs, - ...provide, - }, - stubs: { - GlSprintf, - }, - }); - }; - - const findHelpPopover = () => wrapper.findComponent(HelpPopover); - - it('renders popoover', () => { - createComponent(); - - expect(findHelpPopover().exists()).toBe(true); - }); - - it('renders complete text', () => { - createComponent(); - - expect(findHelpPopover().text()).toMatchSnapshot(); - }); -}); diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js deleted file mode 100644 index 391c17f81cb..00000000000 --- a/spec/frontend/runner/components/runner_tag_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; - -import { RUNNER_TAG_BADGE_VARIANT } from '~/runner/constants'; -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: mockTag, - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - GlResizeObserver: createMockDirective(), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays tag text', () => { - expect(wrapper.text()).toBe(mockTag); - }); - - it('Displays tags with correct style', () => { - expect(findBadge().props()).toMatchObject({ - size: 'sm', - variant: RUNNER_TAG_BADGE_VARIANT, - }); - }); - - it('Displays tags with md size', () => { - createComponent({ - props: { size: 'md' }, - }); - - 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 deleted file mode 100644 index c6bfabdb18a..00000000000 --- a/spec/frontend/runner/components/runner_tags_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import RunnerTags from '~/runner/components/runner_tags.vue'; - -describe('RunnerTags', () => { - let wrapper; - - const findBadge = () => wrapper.findComponent(GlBadge); - const findBadgesAt = (i = 0) => wrapper.findAllComponents(GlBadge).at(i); - - const createComponent = ({ props = {} } = {}) => { - wrapper = mount(RunnerTags, { - propsData: { - tagList: ['tag1', 'tag2'], - ...props, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays tags text', () => { - expect(wrapper.text()).toMatchInterpolatedText('tag1 tag2'); - - expect(findBadgesAt(0).text()).toBe('tag1'); - expect(findBadgesAt(1).text()).toBe('tag2'); - }); - - it('Displays tags with correct style', () => { - expect(findBadge().props('size')).toBe('sm'); - }); - - it('Displays tags with md size', () => { - createComponent({ - props: { size: 'md' }, - }); - - expect(findBadge().props('size')).toBe('md'); - }); - - it('Is empty when there are no tags', () => { - createComponent({ - props: { tagList: null }, - }); - - expect(wrapper.html()).toEqual(''); - }); -}); diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js deleted file mode 100644 index fe922fb9d18..00000000000 --- a/spec/frontend/runner/components/runner_type_badge_spec.js +++ /dev/null @@ -1,66 +0,0 @@ -import { GlBadge } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { - INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, - I18N_INSTANCE_TYPE, - I18N_GROUP_TYPE, - I18N_PROJECT_TYPE, -} from '~/runner/constants'; - -describe('RunnerTypeBadge', () => { - let wrapper; - - const findBadge = () => wrapper.findComponent(GlBadge); - const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip'); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(RunnerTypeBadge, { - propsData: { - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe.each` - type | text - ${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE} - ${GROUP_TYPE} | ${I18N_GROUP_TYPE} - ${PROJECT_TYPE} | ${I18N_PROJECT_TYPE} - `('displays $type runner', ({ type, text }) => { - beforeEach(() => { - createComponent({ props: { type } }); - }); - - it(`as "${text}" with an "info" variant`, () => { - expect(findBadge().text()).toBe(text); - expect(findBadge().props('variant')).toBe('muted'); - }); - - it('with a tooltip', () => { - expect(getTooltip().value).toBeDefined(); - }); - }); - - it('validation fails for an incorrect type', () => { - expect(() => { - createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } }); - }).toThrow(); - }); - - it('does not render content when type is missing', () => { - createComponent({ props: { type: undefined } }); - - expect(findBadge().exists()).toBe(false); - }); -}); diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js deleted file mode 100644 index dde35533bc3..00000000000 --- a/spec/frontend/runner/components/runner_type_tabs_spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import { GlTab } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; -import RunnerCount from '~/runner/components/stat/runner_count.vue'; -import { - INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, - DEFAULT_MEMBERSHIP, - DEFAULT_SORT, -} from '~/runner/constants'; - -const mockSearch = { - runnerType: null, - membership: DEFAULT_MEMBERSHIP, - filters: [], - pagination: { page: 1 }, - sort: DEFAULT_SORT, -}; - -const mockCount = (type, multiplier = 1) => { - let count; - switch (type) { - case INSTANCE_TYPE: - count = 3; - break; - case GROUP_TYPE: - count = 2; - break; - case PROJECT_TYPE: - count = 1; - break; - default: - count = 6; - break; - } - return count * multiplier; -}; - -describe('RunnerTypeTabs', () => { - let wrapper; - - const findTabs = () => wrapper.findAllComponents(GlTab); - const findActiveTab = () => - findTabs() - .filter((tab) => tab.attributes('active') === 'true') - .at(0); - const getTabsTitles = () => findTabs().wrappers.map((tab) => tab.text().replace(/\s+/g, ' ')); - - const createComponent = ({ props, stubs, ...options } = {}) => { - wrapper = shallowMount(RunnerTypeTabs, { - propsData: { - value: mockSearch, - countScope: INSTANCE_TYPE, - countVariables: {}, - ...props, - }, - stubs: { - GlTab, - ...stubs, - }, - ...options, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Renders all options to filter runners by default', () => { - createComponent(); - - expect(getTabsTitles()).toEqual(['All', 'Instance', 'Group', 'Project']); - }); - - it('Shows count when receiving a number', () => { - createComponent({ - stubs: { - RunnerCount: { - props: ['variables'], - render() { - return this.$scopedSlots.default({ - count: mockCount(this.variables.type), - }); - }, - }, - }, - }); - - expect(getTabsTitles()).toEqual([`All 6`, `Instance 3`, `Group 2`, `Project 1`]); - }); - - it('Shows formatted count when receiving a large number', () => { - createComponent({ - stubs: { - RunnerCount: { - props: ['variables'], - render() { - return this.$scopedSlots.default({ - count: mockCount(this.variables.type, 1000), - }); - }, - }, - }, - }); - - expect(getTabsTitles()).toEqual([ - `All 6,000`, - `Instance 3,000`, - `Group 2,000`, - `Project 1,000`, - ]); - }); - - it('Renders a count next to each tab', () => { - const mockVariables = { - paused: true, - status: 'ONLINE', - }; - - createComponent({ - props: { - countVariables: mockVariables, - }, - }); - - findTabs().wrappers.forEach((tab) => { - expect(tab.findComponent(RunnerCount).props()).toEqual({ - scope: INSTANCE_TYPE, - skip: false, - variables: expect.objectContaining(mockVariables), - }); - }); - }); - - it('Renders fewer options to filter runners', () => { - createComponent({ - props: { - runnerTypes: [GROUP_TYPE, PROJECT_TYPE], - }, - }); - - expect(getTabsTitles()).toEqual(['All', 'Group', 'Project']); - }); - - it('"All" is selected by default', () => { - createComponent(); - - 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(() => { - createComponent(); - 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('Component API', () => { - describe('When .refetch() is called', () => { - let mockRefetch; - - beforeEach(() => { - mockRefetch = jest.fn(); - - createComponent({ - stubs: { - RunnerCount: { - methods: { - refetch: mockRefetch, - }, - render() {}, - }, - }, - }); - - wrapper.vm.refetch(); - }); - - it('refetch is called for each count', () => { - expect(mockRefetch).toHaveBeenCalledTimes(4); - }); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js deleted file mode 100644 index e12736216a0..00000000000 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ /dev/null @@ -1,288 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import { GlForm, GlSkeletonLoader } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import { __ } from '~/locale'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; -import { redirectTo } from '~/lib/utils/url_utility'; -import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; -import { - INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, - ACCESS_LEVEL_REF_PROTECTED, - ACCESS_LEVEL_NOT_PROTECTED, -} from '~/runner/constants'; -import runnerUpdateMutation from '~/runner/graphql/edit/runner_update.mutation.graphql'; -import { captureException } from '~/runner/sentry_utils'; -import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage'; -import { runnerFormData } from '../mock_data'; - -jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage'); -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); -jest.mock('~/lib/utils/url_utility'); - -const mockRunner = runnerFormData.data.runner; -const mockRunnerPath = '/admin/runners/1'; - -Vue.use(VueApollo); - -describe('RunnerUpdateForm', () => { - let wrapper; - let runnerUpdateHandler; - - const findForm = () => wrapper.findComponent(GlForm); - const findPausedCheckbox = () => wrapper.findByTestId('runner-field-paused'); - const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected'); - const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged'); - const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked'); - const findFields = () => wrapper.findAll('[data-testid^="runner-field"'); - - const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input'); - const findMaxJobTimeoutInput = () => - wrapper.findByTestId('runner-field-max-timeout').find('input'); - const findTagsInput = () => wrapper.findByTestId('runner-field-tags').find('input'); - - const findSubmit = () => wrapper.find('[type="submit"]'); - const findSubmitDisabledAttr = () => findSubmit().attributes('disabled'); - const findCancelBtn = () => wrapper.findByRole('link', { name: __('Cancel') }); - const submitForm = () => findForm().trigger('submit'); - const submitFormAndWait = () => submitForm().then(waitForPromises); - - const getFieldsModel = () => ({ - active: !findPausedCheckbox().element.checked, - accessLevel: findProtectedCheckbox().element.checked - ? ACCESS_LEVEL_REF_PROTECTED - : ACCESS_LEVEL_NOT_PROTECTED, - runUntagged: findRunUntaggedCheckbox().element.checked, - locked: findLockedCheckbox().element?.checked || false, - maximumTimeout: findMaxJobTimeoutInput().element.value || null, - tagList: findTagsInput().element.value.split(',').filter(Boolean), - }); - - const createComponent = ({ props } = {}) => { - wrapper = mountExtended(RunnerUpdateForm, { - propsData: { - runner: mockRunner, - runnerPath: mockRunnerPath, - ...props, - }, - apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]), - }); - }; - - const expectToHaveSubmittedRunnerContaining = (submittedRunner) => { - expect(runnerUpdateHandler).toHaveBeenCalledTimes(1); - expect(runnerUpdateHandler).toHaveBeenCalledWith({ - input: expect.objectContaining(submittedRunner), - }); - - expect(saveAlertToLocalStorage).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.any(String), - variant: VARIANT_SUCCESS, - }), - ); - expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath); - }; - - beforeEach(() => { - runnerUpdateHandler = jest.fn().mockImplementation(({ input }) => { - return Promise.resolve({ - data: { - runnerUpdate: { - runner: { - ...mockRunner, - ...input, - }, - errors: [], - }, - }, - }); - }); - - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Form has a submit button', () => { - expect(findSubmit().exists()).toBe(true); - }); - - it('Form fields match data', () => { - expect(mockRunner).toMatchObject(getFieldsModel()); - }); - - it('Form shows a cancel button', () => { - expect(runnerUpdateHandler).not.toHaveBeenCalled(); - expect(findCancelBtn().attributes('href')).toBe(mockRunnerPath); - }); - - it('Form prevent multiple submissions', async () => { - await submitForm(); - - expect(findSubmitDisabledAttr()).toBe('disabled'); - }); - - it('Updates runner with no changes', async () => { - await submitFormAndWait(); - - // Some read-only fields are not submitted - const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner; - - expectToHaveSubmittedRunnerContaining(submitted); - }); - - describe('When data is being loaded', () => { - beforeEach(() => { - createComponent({ props: { loading: true } }); - }); - - it('Form skeleton is shown', () => { - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); - expect(findFields()).toHaveLength(0); - }); - - it('Form cannot be submitted', () => { - expect(findSubmit().props('loading')).toBe(true); - }); - - it('Form is updated when data loads', async () => { - wrapper.setProps({ - loading: false, - }); - - await nextTick(); - - expect(findFields()).not.toHaveLength(0); - expect(mockRunner).toMatchObject(getFieldsModel()); - }); - }); - - it.each` - runnerType | exists | outcome - ${INSTANCE_TYPE} | ${false} | ${'hidden'} - ${GROUP_TYPE} | ${false} | ${'hidden'} - ${PROJECT_TYPE} | ${true} | ${'shown'} - `(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, exists }) => { - const runner = { ...mockRunner, runnerType }; - createComponent({ props: { runner } }); - - expect(findLockedCheckbox().exists()).toBe(exists); - }); - - describe('On submit, runner gets updated', () => { - it.each` - test | initialValue | findCheckbox | checked | submitted - ${'pauses'} | ${{ active: true }} | ${findPausedCheckbox} | ${true} | ${{ active: false }} - ${'activates'} | ${{ active: false }} | ${findPausedCheckbox} | ${false} | ${{ active: true }} - ${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} - ${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} - ${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }} - ${'"runs tagged jobs"'} | ${{ runUntagged: false }} | ${findRunUntaggedCheckbox} | ${true} | ${{ runUntagged: true }} - ${'locks'} | ${{ runnerType: PROJECT_TYPE, locked: true }} | ${findLockedCheckbox} | ${false} | ${{ locked: false }} - ${'unlocks'} | ${{ runnerType: PROJECT_TYPE, locked: false }} | ${findLockedCheckbox} | ${true} | ${{ locked: true }} - `('Checkbox $test runner', async ({ initialValue, findCheckbox, checked, submitted }) => { - const runner = { ...mockRunner, ...initialValue }; - createComponent({ props: { runner } }); - - await findCheckbox().setChecked(checked); - await submitFormAndWait(); - - expectToHaveSubmittedRunnerContaining({ - id: runner.id, - ...submitted, - }); - }); - - it.each` - test | initialValue | findInput | value | submitted - ${'description'} | ${{ description: 'Desc. 1' }} | ${findDescriptionInput} | ${'Desc. 2'} | ${{ description: 'Desc. 2' }} - ${'max timeout'} | ${{ maximumTimeout: 36000 }} | ${findMaxJobTimeoutInput} | ${'40000'} | ${{ maximumTimeout: 40000 }} - ${'tags'} | ${{ tagList: ['tag1'] }} | ${findTagsInput} | ${'tag2, tag3'} | ${{ tagList: ['tag2', 'tag3'] }} - `("Field updates runner's $test", async ({ initialValue, findInput, value, submitted }) => { - const runner = { ...mockRunner, ...initialValue }; - createComponent({ props: { runner } }); - - await findInput().setValue(value); - await submitFormAndWait(); - - expectToHaveSubmittedRunnerContaining({ - id: runner.id, - ...submitted, - }); - }); - - it.each` - value | submitted - ${''} | ${{ tagList: [] }} - ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }} - ${'with spaces'} | ${{ tagList: ['with spaces'] }} - ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }} - `('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => { - const runner = { ...mockRunner, tagList: ['tag1'] }; - createComponent({ props: { runner } }); - - await findTagsInput().setValue(value); - await submitFormAndWait(); - - expectToHaveSubmittedRunnerContaining({ - id: runner.id, - ...submitted, - }); - }); - }); - - describe('On error', () => { - beforeEach(() => { - createComponent(); - }); - - it('On network error, error message is shown', async () => { - const mockErrorMsg = 'Update error!'; - - runnerUpdateHandler.mockRejectedValue(new Error(mockErrorMsg)); - - await submitFormAndWait(); - - expect(createAlert).toHaveBeenLastCalledWith({ - message: mockErrorMsg, - }); - expect(captureException).toHaveBeenCalledWith({ - component: 'RunnerUpdateForm', - error: new Error(mockErrorMsg), - }); - expect(findSubmitDisabledAttr()).toBeUndefined(); - }); - - it('On validation error, error message is shown and it is not sent to sentry', async () => { - const mockErrorMsg = 'Invalid value!'; - - runnerUpdateHandler.mockResolvedValue({ - data: { - runnerUpdate: { - runner: mockRunner, - errors: [mockErrorMsg], - }, - }, - }); - - await submitFormAndWait(); - - expect(createAlert).toHaveBeenLastCalledWith({ - message: mockErrorMsg, - }); - expect(findSubmitDisabledAttr()).toBeUndefined(); - - expect(captureException).not.toHaveBeenCalled(); - expect(saveAlertToLocalStorage).not.toHaveBeenCalled(); - expect(redirectTo).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js deleted file mode 100644 index a7363eb11cd..00000000000 --- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js +++ /dev/null @@ -1,208 +0,0 @@ -import { GlFilteredSearchSuggestion, GlLoadingIcon, GlToken } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; - -import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; -import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; - -jest.mock('~/flash'); - -jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ - ...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'), - getRecentlyUsedSuggestions: jest.fn(), -})); - -const mockStorageKey = 'stored-recent-tags'; - -const mockTags = [ - { id: 1, name: 'linux' }, - { id: 2, name: 'windows' }, - { id: 3, name: 'mac' }, -]; - -const mockTagsFiltered = [mockTags[0]]; - -const mockSearchTerm = mockTags[0].name; - -const GlFilteredSearchTokenStub = { - template: `<div> - <slot name="view-token"></slot> - <slot name="suggestions"></slot> - </div>`, -}; - -const mockTagTokenConfig = { - icon: 'tag', - title: 'Tags', - type: 'tag', - token: TagToken, - recentSuggestionsStorageKey: mockStorageKey, - operators: OPERATOR_IS_ONLY, -}; - -describe('TagToken', () => { - let mock; - let wrapper; - - const createComponent = (props = {}) => { - wrapper = mount(TagToken, { - propsData: { - config: mockTagTokenConfig, - value: { data: '' }, - active: false, - ...props, - }, - provide: { - portalName: 'fake target', - alignSuggestions: function fakeAlignSuggestions() {}, - filteredSearchSuggestionListInstance: { - register: jest.fn(), - unregister: jest.fn(), - }, - }, - stubs: { - GlFilteredSearchToken: GlFilteredSearchTokenStub, - }, - }); - }; - - const findGlFilteredSearchSuggestions = () => - wrapper.findAllComponents(GlFilteredSearchSuggestion); - const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchTokenStub); - const findToken = () => wrapper.findComponent(GlToken); - const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - - beforeEach(() => { - mock = new MockAdapter(axios); - - mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags); - mock - .onGet(TAG_SUGGESTIONS_PATH, { params: { search: mockSearchTerm } }) - .reply(200, mockTagsFiltered); - - getRecentlyUsedSuggestions.mockReturnValue([]); - }); - - afterEach(() => { - getRecentlyUsedSuggestions.mockReset(); - wrapper.destroy(); - }); - - describe('when the tags token is displayed', () => { - beforeEach(() => { - createComponent(); - }); - - it('requests tags suggestions', () => { - expect(mock.history.get[0].params).toEqual({ search: '' }); - }); - - it('displays tags suggestions', async () => { - await waitForPromises(); - - mockTags.forEach(({ name }, i) => { - expect(findGlFilteredSearchSuggestions().at(i).text()).toBe(name); - }); - }); - }); - - describe('when suggestions are stored', () => { - const storedSuggestions = [{ id: 4, value: 'docker', text: 'docker' }]; - - beforeEach(async () => { - getRecentlyUsedSuggestions.mockReturnValue(storedSuggestions); - - createComponent(); - await waitForPromises(); - }); - - it('suggestions are loaded from a correct key', () => { - expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey); - }); - - it('displays stored tags suggestions', () => { - expect(findGlFilteredSearchSuggestions()).toHaveLength( - mockTags.length + storedSuggestions.length, - ); - - expect(findGlFilteredSearchSuggestions().at(0).text()).toBe(storedSuggestions[0].text); - }); - }); - - describe('when the users filters suggestions', () => { - beforeEach(() => { - createComponent(); - - findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm }); - }); - - it('requests filtered tags suggestions', () => { - expect(mock.history.get[1].params).toEqual({ search: mockSearchTerm }); - }); - - it('shows the loading icon', async () => { - findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm }); - await nextTick(); - - expect(findGlLoadingIcon().exists()).toBe(true); - }); - - it('displays filtered tags suggestions', async () => { - await waitForPromises(); - - expect(findGlFilteredSearchSuggestions()).toHaveLength(mockTagsFiltered.length); - - expect(findGlFilteredSearchSuggestions().at(0).text()).toBe(mockTagsFiltered[0].name); - }); - }); - - describe('when suggestions cannot be loaded', () => { - beforeEach(async () => { - mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(500); - - createComponent(); - await waitForPromises(); - }); - - it('error is shown', () => { - expect(createAlert).toHaveBeenCalledTimes(1); - expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) }); - }); - }); - - describe('when the user selects a value', () => { - beforeEach(async () => { - createComponent({ value: { data: mockTags[0].name } }); - findGlFilteredSearchToken().vm.$emit('select'); - - await waitForPromises(); - }); - - it('selected tag is displayed', () => { - expect(findToken().exists()).toBe(true); - }); - }); - - describe('when suggestions are disabled', () => { - beforeEach(async () => { - createComponent({ - config: { - ...mockTagTokenConfig, - suggestionsDisabled: true, - }, - }); - - await waitForPromises(); - }); - - it('displays no suggestions', () => { - expect(findGlFilteredSearchSuggestions()).toHaveLength(0); - expect(mock.history.get).toHaveLength(0); - }); - }); -}); diff --git a/spec/frontend/runner/components/stat/runner_count_spec.js b/spec/frontend/runner/components/stat/runner_count_spec.js deleted file mode 100644 index 2a6a745099f..00000000000 --- a/spec/frontend/runner/components/stat/runner_count_spec.js +++ /dev/null @@ -1,148 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import { shallowMount } from '@vue/test-utils'; -import RunnerCount from '~/runner/components/stat/runner_count.vue'; -import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { captureException } from '~/runner/sentry_utils'; - -import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql'; -import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql'; - -import { runnersCountData, groupRunnersCountData } from '../../mock_data'; - -jest.mock('~/runner/sentry_utils'); - -Vue.use(VueApollo); - -describe('RunnerCount', () => { - let wrapper; - let mockRunnersCountHandler; - let mockGroupRunnersCountHandler; - - const createComponent = ({ props = {}, ...options } = {}) => { - const handlers = [ - [allRunnersCountQuery, mockRunnersCountHandler], - [groupRunnersCountQuery, mockGroupRunnersCountHandler], - ]; - - wrapper = shallowMount(RunnerCount, { - apolloProvider: createMockApollo(handlers), - propsData: { - ...props, - }, - scopedSlots: { - default: '<strong>{{props.count}}</strong>', - }, - ...options, - }); - - return waitForPromises(); - }; - - beforeEach(() => { - mockRunnersCountHandler = jest.fn().mockResolvedValue(runnersCountData); - mockGroupRunnersCountHandler = jest.fn().mockResolvedValue(groupRunnersCountData); - }); - - describe('in admin scope', () => { - const mockVariables = { status: 'ONLINE' }; - - beforeEach(async () => { - await createComponent({ props: { scope: INSTANCE_TYPE } }); - }); - - it('fetches data from admin query', () => { - expect(mockRunnersCountHandler).toHaveBeenCalledTimes(1); - expect(mockRunnersCountHandler).toHaveBeenCalledWith({}); - }); - - it('fetches data with filters', async () => { - await createComponent({ props: { scope: INSTANCE_TYPE, variables: mockVariables } }); - - expect(mockRunnersCountHandler).toHaveBeenCalledTimes(2); - expect(mockRunnersCountHandler).toHaveBeenCalledWith(mockVariables); - - expect(wrapper.html()).toBe(`<strong>${runnersCountData.data.runners.count}</strong>`); - }); - - it('does not fetch from the group query', async () => { - expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled(); - }); - - describe('when this query is skipped after data was loaded', () => { - beforeEach(async () => { - wrapper.setProps({ skip: true }); - - await nextTick(); - }); - - it('clears current data', () => { - expect(wrapper.html()).toBe('<strong></strong>'); - }); - }); - }); - - describe('when skipping query', () => { - beforeEach(async () => { - await createComponent({ props: { scope: INSTANCE_TYPE, skip: true } }); - }); - - it('does not fetch data', async () => { - expect(mockRunnersCountHandler).not.toHaveBeenCalled(); - expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled(); - - expect(wrapper.html()).toBe('<strong></strong>'); - }); - }); - - describe('when runners query fails', () => { - const mockError = new Error('error!'); - - beforeEach(async () => { - mockRunnersCountHandler.mockRejectedValue(mockError); - - await createComponent({ props: { scope: INSTANCE_TYPE } }); - }); - - it('data is not shown and error is reported', async () => { - expect(wrapper.html()).toBe('<strong></strong>'); - - expect(captureException).toHaveBeenCalledWith({ - component: 'RunnerCount', - error: mockError, - }); - }); - }); - - describe('in group scope', () => { - beforeEach(async () => { - await createComponent({ props: { scope: GROUP_TYPE } }); - }); - - it('fetches data from the group query', async () => { - expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(1); - expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({}); - - expect(wrapper.html()).toBe( - `<strong>${groupRunnersCountData.data.group.runners.count}</strong>`, - ); - }); - - it('does not fetch from the group query', () => { - expect(mockRunnersCountHandler).not.toHaveBeenCalled(); - }); - }); - - describe('when .refetch() is called', () => { - beforeEach(async () => { - await createComponent({ props: { scope: INSTANCE_TYPE } }); - wrapper.vm.refetch(); - }); - - it('data is not shown and error is reported', async () => { - expect(mockRunnersCountHandler).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/spec/frontend/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/runner/components/stat/runner_single_stat_spec.js deleted file mode 100644 index 964a6a6ff71..00000000000 --- a/spec/frontend/runner/components/stat/runner_single_stat_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { GlSingleStat } from '@gitlab/ui/dist/charts'; -import { shallowMount } from '@vue/test-utils'; -import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue'; -import RunnerCount from '~/runner/components/stat/runner_count.vue'; -import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants'; - -describe('RunnerStats', () => { - let wrapper; - - const findRunnerCount = () => wrapper.findComponent(RunnerCount); - const findGlSingleStat = () => wrapper.findComponent(GlSingleStat); - - const createComponent = ({ props = {}, count, mountFn = shallowMount, ...options } = {}) => { - wrapper = mountFn(RunnerSingleStat, { - propsData: { - scope: INSTANCE_TYPE, - title: 'My title', - variables: {}, - ...props, - }, - stubs: { - RunnerCount: { - props: ['scope', 'variables', 'skip'], - render() { - return this.$scopedSlots.default({ - count, - }); - }, - }, - }, - ...options, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it.each` - case | count | value - ${'number'} | ${99} | ${'99'} - ${'long number'} | ${1000} | ${'1,000'} - ${'empty number'} | ${null} | ${'-'} - `('formats $case', ({ count, value }) => { - createComponent({ count }); - - expect(findGlSingleStat().props('value')).toBe(value); - }); - - it('Passes runner count props', () => { - const props = { - scope: GROUP_TYPE, - variables: { paused: true }, - skip: true, - }; - - createComponent({ props }); - - expect(findRunnerCount().props()).toEqual(props); - }); -}); diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js deleted file mode 100644 index 4afbe453903..00000000000 --- a/spec/frontend/runner/components/stat/runner_stats_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import RunnerStats from '~/runner/components/stat/runner_stats.vue'; -import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue'; -import { - I18N_STATUS_ONLINE, - I18N_STATUS_OFFLINE, - I18N_STATUS_STALE, - INSTANCE_TYPE, - STATUS_ONLINE, - STATUS_OFFLINE, - STATUS_STALE, -} from '~/runner/constants'; - -describe('RunnerStats', () => { - let wrapper; - - const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat); - - const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => { - wrapper = mountFn(RunnerStats, { - propsData: { - scope: INSTANCE_TYPE, - variables: {}, - ...props, - }, - ...options, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays all the stats', () => { - const mockCounts = { - [STATUS_ONLINE]: 3, - [STATUS_OFFLINE]: 2, - [STATUS_STALE]: 1, - }; - - createComponent({ - mountFn: mount, - stubs: { - RunnerCount: { - props: ['variables'], - render() { - return this.$scopedSlots.default({ - count: mockCounts[this.variables.status], - }); - }, - }, - }, - }); - - const text = wrapper.text(); - expect(text).toContain(`${I18N_STATUS_ONLINE} 3`); - expect(text).toContain(`${I18N_STATUS_OFFLINE} 2`); - expect(text).toContain(`${I18N_STATUS_STALE} 1`); - }); - - it('Skips query for other stats', () => { - createComponent({ - props: { - variables: { status: STATUS_ONLINE }, - }, - }); - - expect(findSingleStats().at(0).props('skip')).toBe(false); - expect(findSingleStats().at(1).props('skip')).toBe(true); - expect(findSingleStats().at(2).props('skip')).toBe(true); - }); - - it('Displays all counts for filtered searches', () => { - const mockVariables = { paused: true }; - createComponent({ props: { variables: mockVariables } }); - - findSingleStats().wrappers.forEach((stat) => { - expect(stat.props('variables')).toMatchObject(mockVariables); - }); - }); -}); |