diff options
Diffstat (limited to 'spec/frontend/runner')
17 files changed, 455 insertions, 220 deletions
diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js index 1a1428e8cb1..ad0bce5c9af 100644 --- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js +++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js @@ -2,12 +2,12 @@ import { createLocalVue, 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 createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue'; +import RunnerHeader from '~/runner/components/runner_header.vue'; import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql'; -import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue'; +import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue'; import { captureException } from '~/runner/sentry_utils'; import { runnerData } from '../mock_data'; @@ -21,14 +21,14 @@ const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; const localVue = createLocalVue(); localVue.use(VueApollo); -describe('RunnerDetailsApp', () => { +describe('AdminRunnerEditApp', () => { let wrapper; let mockRunnerQuery; - const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge); + const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { - wrapper = mountFn(RunnerDetailsApp, { + wrapper = mountFn(AdminRunnerEditApp, { localVue, apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]), propsData: { @@ -40,7 +40,7 @@ describe('RunnerDetailsApp', () => { return waitForPromises(); }; - beforeEach(async () => { + beforeEach(() => { mockRunnerQuery = jest.fn().mockResolvedValue(runnerData); }); @@ -56,15 +56,16 @@ describe('RunnerDetailsApp', () => { }); it('displays the runner id', async () => { - await createComponentWithApollo(); + await createComponentWithApollo({ mountFn: mount }); - expect(wrapper.text()).toContain(`Runner #${mockRunnerId}`); + expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`); }); - it('displays the runner type', async () => { + it('displays the runner type and status', async () => { await createComponentWithApollo({ mountFn: mount }); - expect(findRunnerTypeBadge().text()).toBe('shared'); + expect(findRunnerHeader().text()).toContain(`never contacted`); + expect(findRunnerHeader().text()).toContain(`shared`); }); describe('When there is an error', () => { @@ -73,15 +74,15 @@ describe('RunnerDetailsApp', () => { await createComponentWithApollo(); }); - it('error is reported to sentry', async () => { + it('error is reported to sentry', () => { expect(captureException).toHaveBeenCalledWith({ error: new Error('Network error: Error!'), - component: 'RunnerDetailsApp', + component: 'AdminRunnerEditApp', }); }); - it('error is shown to the user', async () => { - expect(createFlash).toHaveBeenCalled(); + it('error is shown to the user', () => { + expect(createAlert).toHaveBeenCalled(); }); }); }); 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 7015fe809b0..42be691ba4c 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; @@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerList from '~/runner/components/runner_list.vue'; +import RunnerStats from '~/runner/components/stat/runner_stats.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -22,23 +23,21 @@ import { CREATED_DESC, DEFAULT_SORT, INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, RUNNER_PAGE_SIZE, } from '~/runner/constants'; import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql'; +import getRunnersCountQuery from '~/runner/graphql/get_runners_count.query.graphql'; import { captureException } from '~/runner/sentry_utils'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import { runnersData, runnersDataPaginated } from '../mock_data'; +import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; -const mockActiveRunnersCount = '2'; -const mockAllRunnersCount = '6'; -const mockInstanceRunnersCount = '3'; -const mockGroupRunnersCount = '2'; -const mockProjectRunnersCount = '1'; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -53,7 +52,9 @@ localVue.use(VueApollo); describe('AdminRunnersApp', () => { let wrapper; let mockRunnersQuery; + let mockRunnersCountQuery; + const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs); const findRunnerList = () => wrapper.findComponent(RunnerList); @@ -65,27 +66,28 @@ describe('AdminRunnersApp', () => { const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { - const handlers = [[getRunnersQuery, mockRunnersQuery]]; - - wrapper = mountFn(AdminRunnersApp, { - localVue, - apolloProvider: createMockApollo(handlers), - propsData: { - registrationToken: mockRegistrationToken, - activeRunnersCount: mockActiveRunnersCount, - allRunnersCount: mockAllRunnersCount, - instanceRunnersCount: mockInstanceRunnersCount, - groupRunnersCount: mockGroupRunnersCount, - projectRunnersCount: mockProjectRunnersCount, - ...props, - }, - }); + const handlers = [ + [getRunnersQuery, mockRunnersQuery], + [getRunnersCountQuery, mockRunnersCountQuery], + ]; + + wrapper = extendedWrapper( + mountFn(AdminRunnersApp, { + localVue, + apolloProvider: createMockApollo(handlers), + propsData: { + registrationToken: mockRegistrationToken, + ...props, + }, + }), + ); }; beforeEach(async () => { setWindowLocation('/admin/runners'); mockRunnersQuery = jest.fn().mockResolvedValue(runnersData); + mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData); createComponent(); await waitForPromises(); }); @@ -95,13 +97,71 @@ describe('AdminRunnersApp', () => { wrapper.destroy(); }); - it('shows the runner tabs with a runner count', async () => { + it('shows total runner counts', async () => { createComponent({ mountFn: mount }); 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; + 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 Promise.resolve({ data: { runners: { count } } }); + }); + + createComponent({ mountFn: mount }); + await waitForPromises(); + expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( - `All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`, + `All 6 Instance 3 Group 2 Project 1`, + ); + }); + + it('shows the runner tabs with a formatted runner count', async () => { + mockRunnersCountQuery.mockImplementation(({ type }) => { + let count; + switch (type) { + case INSTANCE_TYPE: + count = 3000; + break; + case GROUP_TYPE: + count = 2000; + break; + case PROJECT_TYPE: + count = 1000; + break; + default: + count = 6000; + break; + } + return Promise.resolve({ data: { runners: { count } } }); + }); + + createComponent({ mountFn: mount }); + await waitForPromises(); + + expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( + `All 6,000 Instance 3,000 Group 2,000 Project 1,000`, ); }); @@ -152,12 +212,6 @@ describe('AdminRunnersApp', () => { ]); }); - it('shows the active runner count', () => { - createComponent({ mountFn: mount }); - - expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`)); - }); - describe('when a filter is preselected', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`); @@ -241,7 +295,7 @@ describe('AdminRunnersApp', () => { }); it('error is shown to the user', async () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); it('error is reported to sentry', async () => { diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js index 95c212cb0a9..4233d86c24c 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -4,7 +4,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { captureException } from '~/runner/sentry_utils'; @@ -40,15 +40,17 @@ describe('RunnerTypeCell', () => { const findDeleteBtn = () => wrapper.findByTestId('delete-runner'); const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value; - const createComponent = ({ active = true } = {}, options) => { + const createComponent = (runner = {}, options) => { wrapper = extendedWrapper( shallowMount(RunnerActionCell, { propsData: { runner: { id: mockRunner.id, shortSha: mockRunner.shortSha, - adminUrl: mockRunner.adminUrl, - active, + editAdminUrl: mockRunner.editAdminUrl, + userPermissions: mockRunner.userPermissions, + active: mockRunner.active, + ...runner, }, }, localVue, @@ -101,7 +103,26 @@ describe('RunnerTypeCell', () => { it('Displays the runner edit link with the correct href', () => { createComponent(); - expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl); + expect(findEditBtn().attributes('href')).toBe(mockRunner.editAdminUrl); + }); + + it('Does not render the runner edit link when user cannot update', () => { + createComponent({ + userPermissions: { + ...mockRunner.userPermissions, + updateRunner: false, + }, + }); + + expect(findEditBtn().exists()).toBe(false); + }); + + it('Does not render the runner edit link when editAdminUrl is not provided', () => { + createComponent({ + editAdminUrl: null, + }); + + expect(findEditBtn().exists()).toBe(false); }); }); @@ -179,7 +200,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); }); @@ -208,11 +229,22 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); }); }); }); + + it('Does not render the runner toggle active button when user cannot update', () => { + createComponent({ + userPermissions: { + ...mockRunner.userPermissions, + updateRunner: false, + }, + }); + + expect(findToggleActiveBtn().exists()).toBe(false); + }); }); describe('Delete action', () => { @@ -225,6 +257,10 @@ describe('RunnerTypeCell', () => { ); }); + it('Renders delete button', () => { + expect(findDeleteBtn().exists()).toBe(true); + }); + it('Delete button opens delete modal', () => { const modalId = getBinding(findDeleteBtn().element, 'gl-modal').value; @@ -259,6 +295,18 @@ describe('RunnerTypeCell', () => { }); }); + it('Does not render the runner delete button when user cannot delete', () => { + createComponent({ + userPermissions: { + ...mockRunner.userPermissions, + deleteRunner: false, + }, + }); + + expect(findDeleteBtn().exists()).toBe(false); + expect(findRunnerDeleteModal().exists()).toBe(false); + }); + describe('When delete is clicked', () => { beforeEach(() => { findRunnerDeleteModal().vm.$emit('primary'); @@ -302,7 +350,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); it('toast notification is not shown', () => { @@ -334,7 +382,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); }); }); 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 index 0d002c272b4..e75decddf70 100644 --- 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 @@ -1,14 +1,15 @@ -import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui'; +import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +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/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'); @@ -18,14 +19,18 @@ localVue.use(VueApollo); localVue.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, { @@ -38,6 +43,9 @@ describe('RegistrationTokenResetDropdownItem', () => { apolloProvider: createMockApollo([ [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler], ]), + directives: { + GlModal: createMockDirective(), + }, }); showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; @@ -54,8 +62,6 @@ describe('RegistrationTokenResetDropdownItem', () => { }); createComponent(); - - jest.spyOn(window, 'confirm'); }); afterEach(() => { @@ -66,6 +72,18 @@ describe('RegistrationTokenResetDropdownItem', () => { 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'; @@ -82,9 +100,8 @@ describe('RegistrationTokenResetDropdownItem', () => { props: { type }, }); - window.confirm.mockReturnValueOnce(true); - findDropdownItem().trigger('click'); + clickSubmit(); await waitForPromises(); }); @@ -114,7 +131,6 @@ describe('RegistrationTokenResetDropdownItem', () => { describe('On click without confirmation', () => { beforeEach(async () => { - window.confirm.mockReturnValueOnce(false); findDropdownItem().vm.$emit('click'); await waitForPromises(); }); @@ -142,11 +158,11 @@ describe('RegistrationTokenResetDropdownItem', () => { runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); - window.confirm.mockReturnValueOnce(true); findDropdownItem().trigger('click'); + clickSubmit(); await waitForPromises(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: `Network error: ${mockErrorMsg}`, }); expect(captureException).toHaveBeenCalledWith({ @@ -168,11 +184,11 @@ describe('RegistrationTokenResetDropdownItem', () => { }, }); - window.confirm.mockReturnValueOnce(true); findDropdownItem().trigger('click'); + clickSubmit(); await waitForPromises(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: `${mockErrorMsg} ${mockErrorMsg2}`, }); expect(captureException).toHaveBeenCalledWith({ @@ -184,8 +200,8 @@ describe('RegistrationTokenResetDropdownItem', () => { describe('Immediately after click', () => { it('shows loading state', async () => { - window.confirm.mockReturnValue(true); findDropdownItem().trigger('click'); + clickSubmit(); await nextTick(); expect(findLoadingIcon().exists()).toBe(true); diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js new file mode 100644 index 00000000000..50699df3a44 --- /dev/null +++ b/spec/frontend/runner/components/runner_header_spec.js @@ -0,0 +1,93 @@ +import { GlSprintf } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import { 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 findTimeAgo = () => wrapper.findComponent(TimeAgo); + + const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerHeader, { + propsData: { + runner: { + ...mockRunner, + ...runner, + }, + }, + stubs: { + GlSprintf, + TimeAgo, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('displays the runner status', () => { + createComponent({ + mountFn: mount, + runner: { + status: STATUS_ONLINE, + }, + }); + + expect(findRunnerStatusBadge().text()).toContain(`online`); + }); + + it('displays the runner type', () => { + createComponent({ + mountFn: mount, + runner: { + runnerType: GROUP_TYPE, + }, + }); + + expect(findRunnerTypeBadge().text()).toContain(`group`); + }); + + it('displays the runner id', () => { + createComponent({ + runner: { + id: convertToGraphQLId(TYPE_CI_RUNNER, 99), + }, + }); + + expect(wrapper.text()).toContain(`Runner #99`); + }); + + 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 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); + }); +}); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 5a14fa5a2d5..452430b7237 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -69,7 +69,9 @@ describe('RunnerList', () => { const { id, description, version, ipAddress, shortSha } = mockRunners[0]; // Badges - expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused'); + expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( + 'never contacted paused', + ); // Runner summary expect(findCell({ fieldKey: 'summary' }).text()).toContain( diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js index a19515d6ed2..c470c6bb989 100644 --- a/spec/frontend/runner/components/runner_status_badge_spec.js +++ b/spec/frontend/runner/components/runner_status_badge_spec.js @@ -6,7 +6,6 @@ import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE, - STATUS_NOT_CONNECTED, STATUS_NEVER_CONTACTED, } from '~/runner/constants'; @@ -50,20 +49,7 @@ describe('RunnerTypeBadge', () => { expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago'); }); - it('renders not connected state', () => { - createComponent({ - runner: { - contactedAt: null, - status: STATUS_NOT_CONNECTED, - }, - }); - - expect(wrapper.text()).toBe('not connected'); - expect(findBadge().props('variant')).toBe('muted'); - expect(getTooltip().value).toMatch('This runner has never connected'); - }); - - it('renders never contacted state as not connected, for backwards compatibility', () => { + it('renders never contacted state', () => { createComponent({ runner: { contactedAt: null, @@ -71,9 +57,9 @@ describe('RunnerTypeBadge', () => { }, }); - expect(wrapper.text()).toBe('not connected'); + expect(wrapper.text()).toBe('never contacted'); expect(findBadge().props('variant')).toBe('muted'); - expect(getTooltip().value).toMatch('This runner has never connected'); + expect(getTooltip().value).toMatch('This runner has never contacted'); }); it('renders offline state', () => { diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js deleted file mode 100644 index 4023c75c9a8..00000000000 --- a/spec/frontend/runner/components/runner_type_alert_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { GlAlert, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import RunnerTypeAlert from '~/runner/components/runner_type_alert.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -describe('RunnerTypeAlert', () => { - let wrapper; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findLink = () => wrapper.findComponent(GlLink); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(RunnerTypeAlert, { - propsData: { - type: INSTANCE_TYPE, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe.each` - type | exampleText | anchor - ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} - ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} - ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} - `('When it is an $type level runner', ({ type, exampleText, anchor }) => { - beforeEach(() => { - createComponent({ props: { type } }); - }); - - it('Describes runner type', () => { - expect(wrapper.text()).toMatch(exampleText); - }); - - it(`Shows an "info" variant`, () => { - expect(findAlert().props('variant')).toBe('info'); - }); - - it(`Links to anchor "${anchor}"`, () => { - expect(findLink().attributes('href')).toBe(`/help/ci/runners/runners_scope${anchor}`); - }); - }); - - describe('When runner type is not correct', () => { - it('Does not render content when type is missing', () => { - createComponent({ props: { type: undefined } }); - - expect(wrapper.html()).toBe(''); - }); - - it('Validation fails for an incorrect type', () => { - expect(() => { - createComponent({ props: { type: 'NOT_A_TYPE' } }); - }).toThrow(); - }); - }); -}); diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index 0e0844a785b..ebb2e67d1e2 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash, { FLASH_TYPES } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; import { INSTANCE_TYPE, @@ -79,9 +79,9 @@ describe('RunnerUpdateForm', () => { input: expect.objectContaining(submittedRunner), }); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: expect.stringContaining('saved'), - type: FLASH_TYPES.SUCCESS, + variant: VARIANT_SUCCESS, }); expect(findSubmitDisabledAttr()).toBeUndefined(); @@ -127,7 +127,7 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); // Some fields are not submitted - const { ipAddress, runnerType, ...submitted } = mockRunner; + const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner; expectToHaveSubmittedRunnerContaining(submitted); }); @@ -238,7 +238,7 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: `Network error: ${mockErrorMsg}`, }); expect(captureException).toHaveBeenCalledWith({ @@ -262,7 +262,7 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: mockErrorMsg, }); expect(captureException).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 index 89c06ba2df4..52557ff716d 100644 --- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js +++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue'; @@ -168,8 +168,8 @@ describe('TagToken', () => { }); it('error is shown', async () => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith({ message: expect.any(String) }); + expect(createAlert).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) }); }); }); diff --git a/spec/frontend/runner/components/stat/runner_online_stat_spec.js b/spec/frontend/runner/components/stat/runner_online_stat_spec.js deleted file mode 100644 index 18f865aa22c..00000000000 --- a/spec/frontend/runner/components/stat/runner_online_stat_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { GlSingleStat } from '@gitlab/ui/dist/charts'; -import { shallowMount, mount } from '@vue/test-utils'; -import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue'; - -describe('RunnerOnlineBadge', () => { - let wrapper; - - const findSingleStat = () => wrapper.findComponent(GlSingleStat); - - const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { - wrapper = mountFn(RunnerOnlineBadge, { - propsData: { - value: '99', - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('Uses a success appearance', () => { - createComponent({}, shallowMount); - - expect(findSingleStat().props('variant')).toBe('success'); - }); - - it('Renders a value', () => { - createComponent({}, mount); - - expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`)); - }); -}); diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js new file mode 100644 index 00000000000..68db8621ef0 --- /dev/null +++ b/spec/frontend/runner/components/stat/runner_stats_spec.js @@ -0,0 +1,46 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerStats from '~/runner/components/stat/runner_stats.vue'; +import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; + +describe('RunnerStats', () => { + let wrapper; + + const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i); + + const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerStats, { + propsData: { + onlineRunnersCount: 3, + offlineRunnersCount: 2, + staleRunnersCount: 1, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays all the stats', () => { + createComponent({ mountFn: mount }); + + const stats = wrapper.text(); + + expect(stats).toMatch('Online runners 3'); + expect(stats).toMatch('Offline runners 2'); + expect(stats).toMatch('Stale runners 1'); + }); + + it.each` + i | status + ${0} | ${STATUS_ONLINE} + ${1} | ${STATUS_OFFLINE} + ${2} | ${STATUS_STALE} + `('Displays status types at index $i', ({ i, status }) => { + createComponent(); + + expect(findRunnerStatusStatAt(i).props('status')).toBe(status); + }); +}); diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js new file mode 100644 index 00000000000..3218272eac7 --- /dev/null +++ b/spec/frontend/runner/components/stat/runner_status_stat_spec.js @@ -0,0 +1,67 @@ +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; + +describe('RunnerStatusStat', () => { + let wrapper; + + const findSingleStat = () => wrapper.findComponent(GlSingleStat); + + const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => { + wrapper = mountFn(RunnerStatusStat, { + propsData: { + status: STATUS_ONLINE, + value: 99, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + status | variant | title | badge + ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'} + ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'} + ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'} + `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => { + beforeEach(() => { + createComponent({ props: { status } }, mount); + }); + + it('Renders text', () => { + expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`)); + }); + + it(`Uses variant ${variant}`, () => { + expect(findSingleStat().props('variant')).toBe(variant); + }); + }); + + it('Formats stat number', () => { + createComponent({ props: { value: 1000 } }, mount); + + expect(wrapper.text()).toMatch('Online runners 1,000'); + }); + + it('Shows a null result', () => { + createComponent({ props: { value: null } }, mount); + + expect(wrapper.text()).toMatch('Online runners -'); + }); + + it('Shows an undefined result', () => { + createComponent({ props: { value: undefined } }, mount); + + expect(wrapper.text()).toMatch('Online runners -'); + }); + + it('Shows result for an unknown status', () => { + createComponent({ props: { status: 'UNKNOWN' } }, mount); + + expect(wrapper.text()).toMatch('Runners 99'); + }); +}); 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 4451100de19..034b7848f35 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -6,12 +6,13 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerList from '~/runner/components/runner_list.vue'; +import RunnerStats from '~/runner/components/stat/runner_stats.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -26,10 +27,11 @@ import { RUNNER_PAGE_SIZE, } from '~/runner/constants'; import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql'; +import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql'; import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue'; import { captureException } from '~/runner/sentry_utils'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data'; +import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data'; const localVue = createLocalVue(); localVue.use(VueApollo); @@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('GroupRunnersApp', () => { let wrapper; let mockGroupRunnersQuery; + let mockGroupRunnersCountQuery; + const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); @@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => { const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { - const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]]; + const handlers = [ + [getGroupRunnersQuery, mockGroupRunnersQuery], + [getGroupRunnersCountQuery, mockGroupRunnersCountQuery], + ]; wrapper = mountFn(GroupRunnersApp, { localVue, @@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => { setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`); mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData); + mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData); createComponent(); await waitForPromises(); }); + it('shows total runner counts', async () => { + createComponent({ mountFn: mount }); + + await waitForPromises(); + + const stats = findRunnerStats().text(); + + expect(stats).toMatch('Online runners 2'); + expect(stats).toMatch('Offline runners 2'); + expect(stats).toMatch('Stale runners 2'); + }); + it('shows the runner setup instructions', () => { expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE); @@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => { ); }); - describe('shows the active runner count', () => { - const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`); - - it('with a regular value', () => { - createComponent({ mountFn: mount }); - - expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount)); - }); - - it('at the limit', () => { - createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount }); - - expect(wrapper.text()).toMatch(expectedOnlineCount('1,000')); - }); - - it('over the limit', () => { - createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount }); - - expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+')); - }); - }); - describe('when a filter is preselected', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`); @@ -236,7 +234,7 @@ describe('GroupRunnersApp', () => { }); it('error is shown to the user', async () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); it('error is reported to sentry', async () => { diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index b8d0f1273c7..9c430e205ea 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -2,17 +2,21 @@ // Admin queries import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json'; +import runnersCountData from 'test_fixtures/graphql/runner/get_runners_count.query.graphql.json'; import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json'; import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json'; // Group queries import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json'; +import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json'; import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json'; export { runnerData, + runnersCountData, runnersDataPaginated, runnersData, groupRunnersData, + groupRunnersCountData, groupRunnersDataPaginated, }; diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js index 0fc7917663e..aff1ec882bb 100644 --- a/spec/frontend/runner/runner_search_utils_spec.js +++ b/spec/frontend/runner/runner_search_utils_spec.js @@ -1,6 +1,7 @@ import { RUNNER_PAGE_SIZE } from '~/runner/constants'; import { searchValidator, + updateOutdatedUrl, fromUrlQueryToSearch, fromSearchToUrl, fromSearchToVariables, @@ -190,6 +191,23 @@ describe('search_params.js', () => { }); }); + describe('updateOutdatedUrl', () => { + it('returns null for urls that do not need updating', () => { + expect(updateOutdatedUrl('http://test.host/')).toBe(null); + expect(updateOutdatedUrl('http://test.host/?a=b')).toBe(null); + }); + + it('returns updated url for updating NOT_CONNECTED to NEVER_CONTACTED', () => { + expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED')).toBe( + 'http://test.host/admin/runners?status[]=NEVER_CONTACTED', + ); + + expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED&a=b')).toBe( + 'http://test.host/admin/runners?status[]=NEVER_CONTACTED&a=b', + ); + }); + }); + describe('fromUrlQueryToSearch', () => { examples.forEach(({ name, urlQuery, search }) => { it(`Converts ${name} to a search object`, () => { diff --git a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js b/spec/frontend/runner/runner_update_form_utils_spec.js index 510b4e604ac..a633aee92f7 100644 --- a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js +++ b/spec/frontend/runner/runner_update_form_utils_spec.js @@ -1,8 +1,5 @@ import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; -import { - modelToUpdateMutationVariables, - runnerToModel, -} from '~/runner/runner_details/runner_update_form_utils'; +import { modelToUpdateMutationVariables, runnerToModel } from '~/runner/runner_update_form_utils'; const mockId = 'gid://gitlab/Ci::Runner/1'; const mockDescription = 'Runner Desc.'; @@ -23,7 +20,7 @@ const mockModel = { tagList: 'tag-1, tag-2', }; -describe('~/runner/runner_details/runner_update_form_utils', () => { +describe('~/runner/runner_update_form_utils', () => { describe('runnerToModel', () => { it('collects all model data', () => { expect(runnerToModel(mockRunner)).toEqual(mockModel); |