diff options
Diffstat (limited to 'spec/frontend/runner')
14 files changed, 401 insertions, 113 deletions
diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js index d121c6be218..8a34cb14d8b 100644 --- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js +++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js @@ -7,17 +7,20 @@ import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/runner/components/runner_header.vue'; -import runnerQuery from '~/runner/graphql/details/runner.query.graphql'; +import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; +import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql'; import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue'; import { captureException } from '~/runner/sentry_utils'; -import { runnerData } from '../mock_data'; +import { runnerFormData } from '../mock_data'; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); -const mockRunnerGraphqlId = runnerData.data.runner.id; +const mockRunner = runnerFormData.data.runner; +const mockRunnerGraphqlId = mockRunner.id; const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; +const mockRunnerPath = `/admin/runners/${mockRunnerId}`; Vue.use(VueApollo); @@ -26,12 +29,14 @@ describe('AdminRunnerEditApp', () => { let mockRunnerQuery; const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerUpdateForm = () => wrapper.findComponent(RunnerUpdateForm); const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { wrapper = mountFn(AdminRunnerEditApp, { - apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]), + apolloProvider: createMockApollo([[runnerFormQuery, mockRunnerQuery]]), propsData: { runnerId: mockRunnerId, + runnerPath: mockRunnerPath, ...props, }, }); @@ -40,7 +45,7 @@ describe('AdminRunnerEditApp', () => { }; beforeEach(() => { - mockRunnerQuery = jest.fn().mockResolvedValue(runnerData); + mockRunnerQuery = jest.fn().mockResolvedValue(runnerFormData); }); afterEach(() => { @@ -68,6 +73,26 @@ describe('AdminRunnerEditApp', () => { expect(findRunnerHeader().text()).toContain(`shared`); }); + it('displays a loading runner form', () => { + createComponentWithApollo(); + + expect(findRunnerUpdateForm().props()).toMatchObject({ + runner: null, + loading: true, + runnerPath: mockRunnerPath, + }); + }); + + it('displays the runner form', async () => { + await createComponentWithApollo(); + + expect(findRunnerUpdateForm().props()).toMatchObject({ + runner: mockRunner, + loading: false, + runnerPath: mockRunnerPath, + }); + }); + describe('When there is an error', () => { beforeEach(async () => { mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js index f994ff24c21..07259ec3538 100644 --- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -3,24 +3,30 @@ import { mount, shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { redirectTo } from '~/lib/utils/url_utility'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/runner/components/runner_header.vue'; import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; +import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; -import runnerQuery from '~/runner/graphql/details/runner.query.graphql'; +import runnerQuery from '~/runner/graphql/show/runner.query.graphql'; import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue'; import { captureException } from '~/runner/sentry_utils'; +import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage'; import { runnerData } 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 = runnerData.data.runner; const mockRunnerGraphqlId = mockRunner.id; const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; +const mockRunnersPath = '/admin/runners'; Vue.use(VueApollo); @@ -29,6 +35,7 @@ describe('AdminRunnerShowApp', () => { let mockRunnerQuery; const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton); const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); @@ -45,6 +52,7 @@ describe('AdminRunnerShowApp', () => { apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]), propsData: { runnerId: mockRunnerId, + runnersPath: mockRunnersPath, ...props, }, }); @@ -75,6 +83,7 @@ describe('AdminRunnerShowApp', () => { it('displays the runner edit and pause buttons', async () => { expect(findRunnerEditButton().exists()).toBe(true); expect(findRunnerPauseButton().exists()).toBe(true); + expect(findRunnerDeleteButton().exists()).toBe(true); }); it('shows basic runner details', async () => { @@ -82,6 +91,9 @@ describe('AdminRunnerShowApp', () => { Last contact Never contacted Version 1.0.0 IP Address 127.0.0.1 + Executor None + Architecture None + Platform darwin Configuration Runs untagged jobs Maximum job timeout None Tags None`.replace(/\s+/g, ' '); @@ -108,6 +120,42 @@ describe('AdminRunnerShowApp', () => { }); }); + describe('when runner cannot be deleted', () => { + beforeEach(async () => { + mockRunnerQueryResult({ + userPermissions: { + deleteRunner: false, + }, + }); + + await createComponent({ + mountFn: mount, + }); + }); + + it('does not display the runner edit and pause buttons', () => { + expect(findRunnerDeleteButton().exists()).toBe(false); + }); + }); + + describe('when runner is deleted', () => { + beforeEach(async () => { + await createComponent({ + mountFn: mount, + }); + }); + + it('redirects to the runner list page', () => { + findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' }); + + expect(saveAlertToLocalStorage).toHaveBeenCalledWith({ + message: 'Runner deleted', + variant: VARIANT_SUCCESS, + }); + expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); + }); + }); + describe('when runner does not have an edit url ', () => { beforeEach(async () => { mockRunnerQueryResult({ diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index 2ef856c90ab..405813be4e3 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -35,6 +35,8 @@ import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, RUNNER_PAGE_SIZE, } from '~/runner/constants'; import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql'; @@ -52,6 +54,7 @@ import { const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRunners = runnersData.data.runners.nodes; +const mockRunnersCount = runnersCountData.data.runners.count; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -124,18 +127,6 @@ describe('AdminRunnersApp', () => { wrapper.destroy(); }); - it('shows total runner counts', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); - - const stats = findRunnerStats().text(); - - expect(stats).toMatch('Online runners 4'); - expect(stats).toMatch('Offline runners 4'); - expect(stats).toMatch('Stale runners 4'); - }); - it('shows the runner tabs with a runner count for each type', async () => { mockRunnersCountQuery.mockImplementation(({ type }) => { let count; @@ -197,6 +188,24 @@ describe('AdminRunnersApp', () => { expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE); }); + it('shows total runner counts', async () => { + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + status: STATUS_ONLINE, + }); + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + status: STATUS_OFFLINE, + }); + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + status: STATUS_STALE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockRunnersCount, + offlineRunnersCount: mockRunnersCount, + staleRunnersCount: mockRunnersCount, + }); + }); + it('shows the runners list', () => { expect(findRunnerList().props('runners')).toEqual(mockRunners); }); @@ -329,13 +338,30 @@ describe('AdminRunnersApp', () => { first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + type: INSTANCE_TYPE, + status: STATUS_ONLINE, + tagList: ['tag1'], + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockRunnersCount, + }); + }); }); describe('when a filter is selected by the user', () => { beforeEach(() => { + mockRunnersCountQuery.mockClear(); + findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, - filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], + filters: [ + { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, + { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } }, + ], sort: CREATED_ASC, }); }); @@ -343,17 +369,45 @@ describe('AdminRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC', + url: 'http://test.host/admin/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC', }); }); it('requests the runners with filters', () => { expect(mockRunnersQuery).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, + tagList: ['tag1'], sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + tagList: ['tag1'], + status: STATUS_ONLINE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockRunnersCount, + }); + }); + + it('skips fetching count results for status that were not in filter', () => { + expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({ + tagList: ['tag1'], + status: STATUS_OFFLINE, + }); + expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({ + tagList: ['tag1'], + status: STATUS_STALE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + offlineRunnersCount: null, + staleRunnersCount: null, + }); + }); }); it('when runners have not loaded, shows a loading state', () => { diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js index 5cd93df9967..81c2788f084 100644 --- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js +++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js @@ -35,6 +35,16 @@ describe('RegistrationDropdown', () => { const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input'); const findTokenResetDropdownItem = () => wrapper.findComponent(RegistrationTokenResetDropdownItem); + const findModalContent = () => + createWrapper(document.body) + .find('[data-testid="runner-instructions-modal"]') + .text() + .replace(/[\n\t\s]+/g, ' '); + + const openModal = async () => { + await findRegistrationInstructionsDropdownItem().trigger('click'); + await waitForPromises(); + }; const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => { wrapper = extendedWrapper( @@ -49,6 +59,25 @@ describe('RegistrationDropdown', () => { ); }; + const createComponentWithModal = () => { + Vue.use(VueApollo); + + 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'} @@ -76,29 +105,10 @@ describe('RegistrationDropdown', () => { }); describe('When the dropdown item is clicked', () => { - Vue.use(VueApollo); - - const requestHandlers = [ - [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)], - [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)], - ]; - - const findModalInBody = () => - createWrapper(document.body).find('[data-testid="runner-instructions-modal"]'); - beforeEach(async () => { - createComponent( - { - // Mock load modal contents from API - apolloProvider: createMockApollo(requestHandlers), - // Use `attachTo` to find the modal - attachTo: document.body, - }, - mount, - ); - - await findRegistrationInstructionsDropdownItem().trigger('click'); - await waitForPromises(); + createComponentWithModal({}, mount); + + await openModal(); }); afterEach(() => { @@ -106,9 +116,7 @@ describe('RegistrationDropdown', () => { }); it('opens the modal with contents', () => { - const modalText = findModalInBody() - .text() - .replace(/[\n\t\s]+/g, ' '); + const modalText = findModalContent(); expect(modalText).toContain('Install a runner'); @@ -153,15 +161,34 @@ describe('RegistrationDropdown', () => { }); }); - it('Updates the token when it gets reset', async () => { + describe('When token is reset', () => { const newToken = 'mock1'; - createComponent({}, mount); - expect(findRegistrationTokenInput().props('value')).not.toBe(newToken); + const resetToken = async () => { + findTokenResetDropdownItem().vm.$emit('tokenReset', newToken); + await nextTick(); + }; + + it('Updates token in input', async () => { + createComponent({}, mount); + + expect(findRegistrationTokenInput().props('value')).not.toBe(newToken); + + await resetToken(); + + expect(findRegistrationToken().props('value')).toBe(newToken); + }); - findTokenResetDropdownItem().vm.$emit('tokenReset', newToken); - await nextTick(); + it('Updates token in modal', async () => { + createComponentWithModal({}, mount); - expect(findRegistrationToken().props('value')).toBe(newToken); + await openModal(); + + expect(findModalContent()).toContain(mockToken); + + await resetToken(); + + expect(findModalContent()).toContain(newToken); + }); }); }); diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js index 3eb257607b4..b11c749d0a7 100644 --- a/spec/frontend/runner/components/runner_delete_button_spec.js +++ b/spec/frontend/runner/components/runner_delete_button_spec.js @@ -118,6 +118,12 @@ describe('RunnerDeleteButton', () => { 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); diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js index 6bf4a52a799..162d21febfd 100644 --- a/spec/frontend/runner/components/runner_details_spec.js +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -77,6 +77,9 @@ describe('RunnerDetails', () => { ${'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'} diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js index 9e40e911448..8ac5685a0dd 100644 --- a/spec/frontend/runner/components/runner_jobs_spec.js +++ b/spec/frontend/runner/components/runner_jobs_spec.js @@ -11,7 +11,7 @@ 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/details/runner_jobs.query.graphql'; +import runnerJobsQuery from '~/runner/graphql/show/runner_jobs.query.graphql'; import { runnerData, runnerJobsData } from '../mock_data'; diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js index 62ebc6539e2..04627e2307b 100644 --- a/spec/frontend/runner/components/runner_projects_spec.js +++ b/spec/frontend/runner/components/runner_projects_spec.js @@ -16,7 +16,7 @@ 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/details/runner_projects.query.graphql'; +import runnerProjectsQuery from '~/runner/graphql/show/runner_projects.query.graphql'; import { runnerData, runnerProjectsData } from '../mock_data'; diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index b071791e39f..3037364d941 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -1,10 +1,11 @@ import Vue, { nextTick } from 'vue'; -import { GlForm } from '@gitlab/ui'; +import { GlForm, GlSkeletonLoader } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; 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, @@ -13,14 +14,18 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED, } from '~/runner/constants'; -import runnerUpdateMutation from '~/runner/graphql/details/runner_update.mutation.graphql'; +import runnerUpdateMutation from '~/runner/graphql/edit/runner_update.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; -import { runnerData } from '../mock_data'; +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 = runnerData.data.runner; +const mockRunner = runnerFormData.data.runner; +const mockRunnerPath = '/admin/runners/1'; Vue.use(VueApollo); @@ -33,8 +38,7 @@ describe('RunnerUpdateForm', () => { const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected'); const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged'); const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked'); - - const findIpInput = () => wrapper.findByTestId('runner-field-ip-address').find('input'); + const findFields = () => wrapper.findAll('[data-testid^="runner-field"'); const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input'); const findMaxJobTimeoutInput = () => @@ -53,7 +57,6 @@ describe('RunnerUpdateForm', () => { : ACCESS_LEVEL_NOT_PROTECTED, runUntagged: findRunUntaggedCheckbox().element.checked, locked: findLockedCheckbox().element?.checked || false, - ipAddress: findIpInput().element.value, maximumTimeout: findMaxJobTimeoutInput().element.value || null, tagList: findTagsInput().element.value.split(',').filter(Boolean), }); @@ -62,6 +65,7 @@ describe('RunnerUpdateForm', () => { wrapper = mountExtended(RunnerUpdateForm, { propsData: { runner: mockRunner, + runnerPath: mockRunnerPath, ...props, }, apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]), @@ -74,12 +78,13 @@ describe('RunnerUpdateForm', () => { input: expect.objectContaining(submittedRunner), }); - expect(createAlert).toHaveBeenLastCalledWith({ - message: expect.stringContaining('saved'), - variant: VARIANT_SUCCESS, - }); - - expect(findSubmitDisabledAttr()).toBeUndefined(); + expect(saveAlertToLocalStorage).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.any(String), + variant: VARIANT_SUCCESS, + }), + ); + expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath); }; beforeEach(() => { @@ -122,27 +127,19 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); // Some read-only fields are not submitted - const { - __typename, - ipAddress, - runnerType, - createdAt, - status, - editAdminUrl, - contactedAt, - userPermissions, - version, - groups, - jobCount, - ...submitted - } = mockRunner; + const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner; expectToHaveSubmittedRunnerContaining(submitted); }); describe('When data is being loaded', () => { beforeEach(() => { - createComponent({ props: { runner: null } }); + createComponent({ props: { loading: true } }); + }); + + it('Form skeleton is shown', () => { + expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); + expect(findFields()).toHaveLength(0); }); it('Form cannot be submitted', () => { @@ -151,11 +148,12 @@ describe('RunnerUpdateForm', () => { it('Form is updated when data loads', async () => { wrapper.setProps({ - runner: mockRunner, + loading: false, }); await nextTick(); + expect(findFields()).not.toHaveLength(0); expect(mockRunner).toMatchObject(getFieldsModel()); }); }); @@ -273,8 +271,11 @@ describe('RunnerUpdateForm', () => { expect(createAlert).toHaveBeenLastCalledWith({ message: mockErrorMsg, }); - expect(captureException).not.toHaveBeenCalled(); expect(findSubmitDisabledAttr()).toBeUndefined(); + + expect(captureException).not.toHaveBeenCalled(); + expect(saveAlertToLocalStorage).not.toHaveBeenCalled(); + expect(redirectTo).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 02348bf737a..52bd51a974b 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -30,7 +30,10 @@ import { PROJECT_TYPE, PARAM_KEY_PAUSED, PARAM_KEY_STATUS, + PARAM_KEY_TAG, STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, RUNNER_PAGE_SIZE, I18N_EDIT, } from '~/runner/constants'; @@ -53,7 +56,7 @@ Vue.use(GlToast); const mockGroupFullPath = 'group1'; const mockRegistrationToken = 'AABBCC'; const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges; -const mockGroupRunnersLimitedCount = mockGroupRunnersEdges.length; +const mockGroupRunnersCount = mockGroupRunnersEdges.length; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -94,7 +97,7 @@ describe('GroupRunnersApp', () => { propsData: { registrationToken: mockRegistrationToken, groupFullPath: mockGroupFullPath, - groupRunnersLimitedCount: mockGroupRunnersLimitedCount, + groupRunnersLimitedCount: mockGroupRunnersCount, ...props, }, provide: { @@ -115,15 +118,24 @@ describe('GroupRunnersApp', () => { }); it('shows total runner counts', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); - - const stats = findRunnerStats().text(); + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + status: STATUS_ONLINE, + }); + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + status: STATUS_OFFLINE, + }); + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + status: STATUS_STALE, + }); - expect(stats).toMatch('Online runners 2'); - expect(stats).toMatch('Offline runners 2'); - expect(stats).toMatch('Stale runners 2'); + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockGroupRunnersCount, + offlineRunnersCount: mockGroupRunnersCount, + staleRunnersCount: mockGroupRunnersCount, + }); }); it('shows the runner tabs with a runner count for each type', async () => { @@ -281,13 +293,28 @@ describe('GroupRunnersApp', () => { first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + type: INSTANCE_TYPE, + status: STATUS_ONLINE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockGroupRunnersCount, + }); + }); }); describe('when a filter is selected by the user', () => { beforeEach(async () => { findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, - filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], + filters: [ + { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, + { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } }, + ], sort: CREATED_ASC, }); @@ -297,7 +324,7 @@ describe('GroupRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC', + url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC', }); }); @@ -305,10 +332,41 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, + tagList: ['tag1'], sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + tagList: ['tag1'], + status: STATUS_ONLINE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockGroupRunnersCount, + }); + }); + + it('skips fetching count results for status that were not in filter', () => { + expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + tagList: ['tag1'], + status: STATUS_OFFLINE, + }); + expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + tagList: ['tag1'], + status: STATUS_STALE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + offlineRunnersCount: null, + staleRunnersCount: null, + }); + }); }); it('when runners have not loaded, shows a loading state', () => { diff --git a/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js b/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js new file mode 100644 index 00000000000..69cda6d6022 --- /dev/null +++ b/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js @@ -0,0 +1,24 @@ +import AccessorUtilities from '~/lib/utils/accessor'; +import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage'; +import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; + +const mockAlert = { message: 'Message!' }; + +describe('saveAlertToLocalStorage', () => { + useLocalStorageSpy(); + + beforeEach(() => { + jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true); + }); + + it('saves message to local storage', () => { + saveAlertToLocalStorage(mockAlert); + + expect(localStorage.setItem).toHaveBeenCalledTimes(1); + expect(localStorage.setItem).toHaveBeenCalledWith( + LOCAL_STORAGE_ALERT_KEY, + JSON.stringify(mockAlert), + ); + }); +}); diff --git a/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js new file mode 100644 index 00000000000..cabbe642dac --- /dev/null +++ b/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js @@ -0,0 +1,40 @@ +import AccessorUtilities from '~/lib/utils/accessor'; +import { showAlertFromLocalStorage } from '~/runner/local_storage_alert/show_alert_from_local_storage'; +import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import { createAlert } from '~/flash'; + +jest.mock('~/flash'); + +describe('showAlertFromLocalStorage', () => { + useLocalStorageSpy(); + + beforeEach(() => { + jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true); + }); + + it('retrieves message from local storage and displays it', async () => { + const mockAlert = { message: 'Message!' }; + + localStorage.getItem.mockReturnValueOnce(JSON.stringify(mockAlert)); + + await showAlertFromLocalStorage(); + + expect(createAlert).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledWith(mockAlert); + + expect(localStorage.removeItem).toHaveBeenCalledTimes(1); + expect(localStorage.removeItem).toHaveBeenCalledWith(LOCAL_STORAGE_ALERT_KEY); + }); + + it.each(['not a json string', null])('does not fail when stored message is %o', async (item) => { + localStorage.getItem.mockReturnValueOnce(item); + + await showAlertFromLocalStorage(); + + expect(createAlert).not.toHaveBeenCalled(); + + expect(localStorage.removeItem).toHaveBeenCalledTimes(1); + expect(localStorage.removeItem).toHaveBeenCalledWith(LOCAL_STORAGE_ALERT_KEY); + }); +}); diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index fbe8926124c..1c2333b552c 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -1,5 +1,14 @@ // Fixtures generated by: spec/frontend/fixtures/runner.rb +// Show runner queries +import runnerData from 'test_fixtures/graphql/runner/show/runner.query.graphql.json'; +import runnerWithGroupData from 'test_fixtures/graphql/runner/show/runner.query.graphql.with_group.json'; +import runnerProjectsData from 'test_fixtures/graphql/runner/show/runner_projects.query.graphql.json'; +import runnerJobsData from 'test_fixtures/graphql/runner/show/runner_jobs.query.graphql.json'; + +// Edit runner queries +import runnerFormData from 'test_fixtures/graphql/runner/edit/runner_form.query.graphql.json'; + // List queries import runnersData from 'test_fixtures/graphql/runner/list/admin_runners.query.graphql.json'; import runnersDataPaginated from 'test_fixtures/graphql/runner/list/admin_runners.query.graphql.paginated.json'; @@ -8,25 +17,20 @@ import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.qu import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json'; import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json'; -// Details queries -import runnerData from 'test_fixtures/graphql/runner/details/runner.query.graphql.json'; -import runnerWithGroupData from 'test_fixtures/graphql/runner/details/runner.query.graphql.with_group.json'; -import runnerProjectsData from 'test_fixtures/graphql/runner/details/runner_projects.query.graphql.json'; -import runnerJobsData from 'test_fixtures/graphql/runner/details/runner_jobs.query.graphql.json'; - // Other mock data export const onlineContactTimeoutSecs = 2 * 60 * 60; export const staleTimeoutSecs = 5259492; // Ruby's `2.months` export { runnersData, - runnersCountData, runnersDataPaginated, + runnersCountData, + groupRunnersData, + groupRunnersDataPaginated, + groupRunnersCountData, runnerData, runnerWithGroupData, runnerProjectsData, runnerJobsData, - groupRunnersData, - groupRunnersCountData, - groupRunnersDataPaginated, + runnerFormData, }; diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js index 7834e76fe48..a3c1458ed26 100644 --- a/spec/frontend/runner/runner_search_utils_spec.js +++ b/spec/frontend/runner/runner_search_utils_spec.js @@ -220,13 +220,11 @@ describe('search_params.js', () => { }); it.each` - query | updatedQuery - ${'status[]=NOT_CONNECTED'} | ${'status[]=NEVER_CONTACTED'} - ${'status[]=NOT_CONNECTED&a=b'} | ${'status[]=NEVER_CONTACTED&a=b'} - ${'status[]=ACTIVE'} | ${'paused[]=false'} - ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'} - ${'status[]=ACTIVE'} | ${'paused[]=false'} - ${'status[]=PAUSED'} | ${'paused[]=true'} + query | updatedQuery + ${'status[]=ACTIVE'} | ${'paused[]=false'} + ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'} + ${'status[]=ACTIVE'} | ${'paused[]=false'} + ${'status[]=PAUSED'} | ${'paused[]=true'} `('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => { const mockUrl = 'http://test.host/admin/runners?'; |