diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 18:40:28 +0300 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/frontend/runner | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/frontend/runner')
16 files changed, 957 insertions, 574 deletions
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 28e7d192938..433be5d5027 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 @@ -9,6 +9,7 @@ import { redirectTo } from '~/lib/utils/url_utility'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/runner/components/runner_header.vue'; +import RunnerDetails from '~/runner/components/runner_details.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'; @@ -37,6 +38,7 @@ describe('AdminRunnerShowApp', () => { let mockRunnerQuery; const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerDetails = () => wrapper.findComponent(RunnerDetails); const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton); const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); @@ -179,12 +181,32 @@ describe('AdminRunnerShowApp', () => { }); }); + describe('When loading', () => { + beforeEach(() => { + mockRunnerQueryResult(); + + createComponent(); + }); + + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + + it('does not show runner jobs', () => { + expect(findRunnersJobs().exists()).toBe(false); + }); + }); + describe('When there is an error', () => { beforeEach(async () => { mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); await createComponent(); }); + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + it('error is reported to sentry', () => { expect(captureException).toHaveBeenCalledWith({ error: new Error('Error!'), @@ -201,13 +223,6 @@ describe('AdminRunnerShowApp', () => { const stubs = { GlTab, GlTabs, - RunnerDetails: { - template: ` - <div> - <slot name="jobs-tab"></slot> - </div> - `, - }, }; it('without a runner, shows no jobs', () => { 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 3d25ad075de..aa1aa723491 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -10,9 +10,11 @@ import { } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; +import { s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; +import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config'; import { createLocalState } from '~/runner/graphql/list/local_state'; import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; @@ -20,6 +22,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_ import RunnerList from '~/runner/components/runner_list.vue'; import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; +import RunnerCount from '~/runner/components/stat/runner_count.vue'; import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -30,8 +33,6 @@ import { CREATED_DESC, DEFAULT_SORT, INSTANCE_TYPE, - GROUP_TYPE, - PROJECT_TYPE, PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG, @@ -40,15 +41,14 @@ import { STATUS_STALE, RUNNER_PAGE_SIZE, } from '~/runner/constants'; -import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql'; -import adminRunnersCountQuery from '~/runner/graphql/list/admin_runners_count.query.graphql'; +import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql'; +import allRunnersCountQuery from '~/runner/graphql/list/all_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, + allRunnersData, runnersCountData, - runnersDataPaginated, + allRunnersDataPaginated, onlineContactTimeoutSecs, staleTimeoutSecs, emptyStateSvgPath, @@ -56,9 +56,12 @@ import { } from '../mock_data'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; -const mockRunners = runnersData.data.runners.nodes; +const mockRunners = allRunnersData.data.runners.nodes; const mockRunnersCount = runnersCountData.data.runners.count; +const mockRunnersHandler = jest.fn(); +const mockRunnersCountHandler = jest.fn(); + jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility', () => ({ @@ -71,8 +74,6 @@ Vue.use(GlToast); describe('AdminRunnersApp', () => { let wrapper; - let mockRunnersQuery; - let mockRunnersCountQuery; let cacheConfig; let localMutations; @@ -85,7 +86,6 @@ describe('AdminRunnersApp', () => { const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page'); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); - const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const createComponent = ({ props = {}, @@ -96,8 +96,8 @@ describe('AdminRunnersApp', () => { ({ cacheConfig, localMutations } = createLocalState()); const handlers = [ - [adminRunnersQuery, mockRunnersQuery], - [adminRunnersCountQuery, mockRunnersCountQuery], + [allRunnersQuery, mockRunnersHandler], + [allRunnersCountQuery, mockRunnersCountHandler], ]; wrapper = mountFn(AdminRunnersApp, { @@ -116,110 +116,62 @@ describe('AdminRunnersApp', () => { }, ...options, }); - }; - beforeEach(async () => { - setWindowLocation('/admin/runners'); + return waitForPromises(); + }; - mockRunnersQuery = jest.fn().mockResolvedValue(runnersData); - mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData); - createComponent(); - await waitForPromises(); + beforeEach(() => { + mockRunnersHandler.mockResolvedValue(allRunnersData); + mockRunnersCountHandler.mockResolvedValue(runnersCountData); }); afterEach(() => { - mockRunnersQuery.mockReset(); - mockRunnersCountQuery.mockReset(); + mockRunnersHandler.mockReset(); + mockRunnersCountHandler.mockReset(); wrapper.destroy(); }); 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: mountExtended }); - await waitForPromises(); - - expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( - `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: mountExtended }); - await waitForPromises(); + await createComponent({ mountFn: mountExtended }); expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( - `All 6,000 Instance 3,000 Group 2,000 Project 1,000`, + `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`, ); }); it('shows the runner setup instructions', () => { + createComponent(); + expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); 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, - }); + await createComponent({ mountFn: mountExtended }); - expect(findRunnerStats().props()).toMatchObject({ - onlineRunnersCount: mockRunnersCount, - offlineRunnersCount: mockRunnersCount, - staleRunnersCount: mockRunnersCount, - }); + expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_ONLINE }); + expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_OFFLINE }); + expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_STALE }); + + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Online runners')} ${mockRunnersCount}`, + ); + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Offline runners')} ${mockRunnersCount}`, + ); + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Stale runners')} ${mockRunnersCount}`, + ); }); - it('shows the runners list', () => { + it('shows the runners list', async () => { + await createComponent(); + expect(findRunnerList().props('runners')).toEqual(mockRunners); }); it('runner item links to the runner admin page', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); + await createComponent({ mountFn: mountExtended }); const { id, shortSha } = mockRunners[0]; const numericId = getIdFromGraphQLId(id); @@ -231,12 +183,9 @@ describe('AdminRunnersApp', () => { }); it('renders runner actions for each runner', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); + await createComponent({ mountFn: mountExtended }); const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell); - const runner = mockRunners[0]; expect(runnerActions.props()).toEqual({ @@ -245,8 +194,10 @@ describe('AdminRunnersApp', () => { }); }); - it('requests the runners with no filters', () => { - expect(mockRunnersQuery).toHaveBeenLastCalledWith({ + it('requests the runners with no filters', async () => { + await createComponent(); + + expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: undefined, type: undefined, sort: DEFAULT_SORT, @@ -255,9 +206,9 @@ describe('AdminRunnersApp', () => { }); it('sets tokens in the filtered search', () => { - createComponent({ mountFn: mountExtended }); + createComponent(); - expect(findFilteredSearch().props('tokens')).toEqual([ + expect(findRunnerFilteredSearchBar().props('tokens')).toEqual([ expect.objectContaining({ type: PARAM_KEY_PAUSED, options: expect.any(Array), @@ -270,6 +221,7 @@ describe('AdminRunnersApp', () => { type: PARAM_KEY_TAG, recentSuggestionsStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`, }), + upgradeStatusTokenConfig, ]); }); @@ -282,12 +234,10 @@ describe('AdminRunnersApp', () => { const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs beforeEach(async () => { - mockRunnersCountQuery.mockClear(); + mockRunnersCountHandler.mockClear(); - createComponent({ mountFn: mountExtended }); + await createComponent({ mountFn: mountExtended }); showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); - - await waitForPromises(); }); it('Links to the runner page', async () => { @@ -298,12 +248,11 @@ describe('AdminRunnersApp', () => { }); it('When runner is paused or unpaused, some data is refetched', async () => { - expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES); + expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES); findRunnerActionsCell().vm.$emit('toggledPaused'); - expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES); - + expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES); expect(showToast).toHaveBeenCalledTimes(0); }); @@ -319,8 +268,12 @@ describe('AdminRunnersApp', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`); - createComponent(); - await waitForPromises(); + await createComponent({ + stubs: { + RunnerStats, + RunnerCount, + }, + }); }); it('sets the filters in the search bar', () => { @@ -336,7 +289,7 @@ describe('AdminRunnersApp', () => { }); it('requests the runners with filter parameters', () => { - expect(mockRunnersQuery).toHaveBeenLastCalledWith({ + expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, type: INSTANCE_TYPE, tagList: ['tag1'], @@ -346,21 +299,22 @@ describe('AdminRunnersApp', () => { }); it('fetches count results for requested status', () => { - expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + expect(mockRunnersCountHandler).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(); + createComponent({ + stubs: { + RunnerStats, + RunnerCount, + }, + }); findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, @@ -375,12 +329,12 @@ describe('AdminRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: 'http://test.host/admin/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC', + url: expect.stringContaining('?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC'), }); }); it('requests the runners with filters', () => { - expect(mockRunnersQuery).toHaveBeenLastCalledWith({ + expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, tagList: ['tag1'], sort: CREATED_ASC, @@ -389,30 +343,10 @@ describe('AdminRunnersApp', () => { }); it('fetches count results for requested status', () => { - expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + expect(mockRunnersCountHandler).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, - }); }); }); @@ -458,14 +392,13 @@ describe('AdminRunnersApp', () => { describe('when no runners are found', () => { beforeEach(async () => { - mockRunnersQuery = jest.fn().mockResolvedValue({ + mockRunnersHandler.mockResolvedValue({ data: { runners: { nodes: [] }, }, }); - createComponent(); - await waitForPromises(); + await createComponent(); }); it('shows an empty state', () => { @@ -490,9 +423,8 @@ describe('AdminRunnersApp', () => { describe('when runners query fails', () => { beforeEach(async () => { - mockRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!')); - createComponent(); - await waitForPromises(); + mockRunnersHandler.mockRejectedValue(new Error('Error!')); + await createComponent(); }); it('error is shown to the user', async () => { @@ -509,19 +441,18 @@ describe('AdminRunnersApp', () => { describe('Pagination', () => { beforeEach(async () => { - mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated); + mockRunnersHandler.mockResolvedValue(allRunnersDataPaginated); - createComponent({ mountFn: mountExtended }); - await waitForPromises(); + await createComponent({ mountFn: mountExtended }); }); it('navigates to the next page', async () => { await findRunnerPaginationNext().trigger('click'); - expect(mockRunnersQuery).toHaveBeenLastCalledWith({ + expect(mockRunnersHandler).toHaveBeenLastCalledWith({ sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, - after: runnersDataPaginated.data.runners.pageInfo.endCursor, + after: allRunnersDataPaginated.data.runners.pageInfo.endCursor, }); }); }); 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 7a949cb6505..ffd6f126627 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -4,9 +4,9 @@ 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 { runnersData } from '../../mock_data'; +import { allRunnersData } from '../../mock_data'; -const mockRunner = runnersData.data.runners.nodes[0]; +const mockRunner = allRunnersData.data.runners.nodes[0]; describe('RunnerActionsCell', () => { let wrapper; diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js index b11c749d0a7..52fe803c536 100644 --- a/spec/frontend/runner/components/runner_delete_button_spec.js +++ b/spec/frontend/runner/components/runner_delete_button_spec.js @@ -17,9 +17,9 @@ import { import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue'; -import { runnersData } from '../mock_data'; +import { allRunnersData } from '../mock_data'; -const mockRunner = runnersData.data.runners.nodes[0]; +const mockRunner = allRunnersData.data.runners.nodes[0]; const mockRunnerId = getIdFromGraphQLId(mockRunner.id); Vue.use(VueApollo); diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js index 9e0f7014750..552ee29b6f9 100644 --- a/spec/frontend/runner/components/runner_details_spec.js +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -25,12 +25,7 @@ describe('RunnerDetails', () => { const findDetailGroups = () => wrapper.findComponent(RunnerGroups); - const createComponent = ({ - props = {}, - stubs, - mountFn = shallowMountExtended, - ...options - } = {}) => { + const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerDetails, { propsData: { ...props, @@ -39,7 +34,6 @@ describe('RunnerDetails', () => { RunnerDetail, ...stubs, }, - ...options, }); }; @@ -47,16 +41,6 @@ describe('RunnerDetails', () => { wrapper.destroy(); }); - it('when no runner is present, no contents are shown', () => { - createComponent({ - props: { - runner: null, - }, - }); - - expect(wrapper.text()).toBe(''); - }); - describe('Details tab', () => { describe.each` field | runner | expectedValue @@ -141,18 +125,4 @@ describe('RunnerDetails', () => { }); }); }); - - describe('Jobs tab slot', () => { - it('shows job tab slot', () => { - const JOBS_TAB = '<div>Jobs Tab</div>'; - - createComponent({ - slots: { - 'jobs-tab': JOBS_TAB, - }, - }); - - expect(wrapper.html()).toContain(JOBS_TAB); - }); - }); }); diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js index b1b436e5443..83fb1764c6d 100644 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js @@ -89,6 +89,16 @@ describe('RunnerList', () => { ]); }); + 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' } } }); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 872394430ae..eca4bbc3490 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -7,9 +7,9 @@ import { import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerList from '~/runner/components/runner_list.vue'; import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue'; -import { runnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; +import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; -const mockRunners = runnersData.data.runners.nodes; +const mockRunners = allRunnersData.data.runners.nodes; const mockActiveRunnersCount = mockRunners.length; describe('RunnerList', () => { diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js index 9ebb30b6ed7..61476007571 100644 --- a/spec/frontend/runner/components/runner_pause_button_spec.js +++ b/spec/frontend/runner/components/runner_pause_button_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { GlButton } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -16,9 +16,9 @@ import { } from '~/runner/constants'; import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; -import { runnersData } from '../mock_data'; +import { allRunnersData } from '../mock_data'; -const mockRunner = runnersData.data.runners.nodes[0]; +const mockRunner = allRunnersData.data.runners.nodes[0]; Vue.use(VueApollo); @@ -115,15 +115,20 @@ describe('RunnerPauseButton', () => { }); describe(`Immediately after the ${icon} button is clicked`, () => { - beforeEach(async () => { + 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(''); }); }); @@ -237,15 +242,20 @@ describe('RunnerPauseButton', () => { }); describe('Immediately after the button is clicked', () => { - beforeEach(async () => { + 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_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js index 9da5d842d8f..22d2a9e60f7 100644 --- a/spec/frontend/runner/components/runner_type_tabs_spec.js +++ b/spec/frontend/runner/components/runner_type_tabs_spec.js @@ -1,10 +1,30 @@ 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 } from '~/runner/constants'; const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }; +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; @@ -13,33 +33,94 @@ describe('RunnerTypeTabs', () => { findTabs() .filter((tab) => tab.attributes('active') === 'true') .at(0); - const getTabsTitles = () => findTabs().wrappers.map((tab) => tab.text()); + const getTabsTitles = () => findTabs().wrappers.map((tab) => tab.text().replace(/\s+/g, ' ')); - const createComponent = ({ props, ...options } = {}) => { + const createComponent = ({ props, stubs, ...options } = {}) => { wrapper = shallowMount(RunnerTypeTabs, { propsData: { value: mockSearch, + countScope: INSTANCE_TYPE, + countVariables: {}, ...props, }, stubs: { GlTab, + ...stubs, }, ...options, }); }; - beforeEach(() => { - createComponent(); - }); - 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.find(RunnerCount).props()).toEqual({ + scope: INSTANCE_TYPE, + skip: false, + variables: expect.objectContaining(mockVariables), + }); + }); + }); + it('Renders fewer options to filter runners', () => { createComponent({ props: { @@ -51,6 +132,8 @@ describe('RunnerTypeTabs', () => { }); it('"All" is selected by default', () => { + createComponent(); + expect(findActiveTab().text()).toBe('All'); }); @@ -71,6 +154,7 @@ describe('RunnerTypeTabs', () => { const emittedValue = () => wrapper.emitted('input')[0][0]; beforeEach(() => { + createComponent(); findTabs().at(2).vm.$emit('click'); }); @@ -89,27 +173,30 @@ describe('RunnerTypeTabs', () => { }); }); - describe('When using a custom slot', () => { - const mockContent = 'content'; - - beforeEach(() => { - createComponent({ - scopedSlots: { - title: ` - <span> - {{props.tab.title}} ${mockContent} - </span>`, - }, + 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('Renders tabs with additional information', () => { - expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([ - `All ${mockContent}`, - `Instance ${mockContent}`, - `Group ${mockContent}`, - `Project ${mockContent}`, - ]); + it('refetch is called for each count', () => { + expect(mockRefetch).toHaveBeenCalledTimes(4); + }); }); }); }); 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 52557ff716d..22f0561ca5f 100644 --- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js +++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js @@ -134,8 +134,6 @@ describe('TagToken', () => { describe('when the users filters suggestions', () => { beforeEach(async () => { findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm }); - - jest.runAllTimers(); }); it('requests filtered tags suggestions', async () => { @@ -145,6 +143,7 @@ describe('TagToken', () => { }); it('shows the loading icon', async () => { + findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm }); await nextTick(); expect(findGlLoadingIcon().exists()).toBe(true); diff --git a/spec/frontend/runner/components/stat/runner_count_spec.js b/spec/frontend/runner/components/stat/runner_count_spec.js new file mode 100644 index 00000000000..89b51b1b4a7 --- /dev/null +++ b/spec/frontend/runner/components/stat/runner_count_spec.js @@ -0,0 +1,148 @@ +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 '~/runner/graphql/list/all_runners_count.query.graphql'; +import groupRunnersCountQuery from '~/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_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js index 68db8621ef0..f1ba6403dfb 100644 --- a/spec/frontend/runner/components/stat/runner_stats_spec.js +++ b/spec/frontend/runner/components/stat/runner_stats_spec.js @@ -1,21 +1,24 @@ import { shallowMount, mount } from '@vue/test-utils'; +import { s__ } from '~/locale'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; +import RunnerCount from '~/runner/components/stat/runner_count.vue'; import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue'; -import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; +import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; describe('RunnerStats', () => { let wrapper; + const findRunnerCountAt = (i) => wrapper.findAllComponents(RunnerCount).at(i); const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i); - const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { + const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => { wrapper = mountFn(RunnerStats, { propsData: { - onlineRunnersCount: 3, - offlineRunnersCount: 2, - staleRunnersCount: 1, + scope: INSTANCE_TYPE, + variables: {}, ...props, }, + ...options, }); }; @@ -24,13 +27,46 @@ describe('RunnerStats', () => { }); it('Displays all the stats', () => { - createComponent({ mountFn: mount }); + 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).toMatch(`${s__('Runners|Online runners')} 3`); + expect(text).toMatch(`${s__('Runners|Offline runners')} 2`); + expect(text).toMatch(`${s__('Runners|Stale runners')} 1`); + }); - const stats = wrapper.text(); + it('Displays counts for filtered searches', () => { + createComponent({ props: { variables: { paused: true } } }); - expect(stats).toMatch('Online runners 3'); - expect(stats).toMatch('Offline runners 2'); - expect(stats).toMatch('Stale runners 1'); + expect(findRunnerCountAt(0).props('variables').paused).toBe(true); + expect(findRunnerCountAt(1).props('variables').paused).toBe(true); + expect(findRunnerCountAt(2).props('variables').paused).toBe(true); + }); + + it('Skips overlapping statuses', () => { + createComponent({ props: { variables: { status: STATUS_ONLINE } } }); + + expect(findRunnerCountAt(0).props('skip')).toBe(false); + expect(findRunnerCountAt(1).props('skip')).toBe(true); + expect(findRunnerCountAt(2).props('skip')).toBe(true); }); it.each` @@ -38,9 +74,10 @@ describe('RunnerStats', () => { ${0} | ${STATUS_ONLINE} ${1} | ${STATUS_OFFLINE} ${2} | ${STATUS_STALE} - `('Displays status types at index $i', ({ i, status }) => { - createComponent(); + `('Displays status $status at index $i', ({ i, status }) => { + createComponent({ mountFn: mount }); + expect(findRunnerCountAt(i).props('variables').status).toBe(status); expect(findRunnerStatusStatAt(i).props('status')).toBe(status); }); }); diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js new file mode 100644 index 00000000000..2065874c288 --- /dev/null +++ b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js @@ -0,0 +1,213 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +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 RunnerDetails from '~/runner/components/runner_details.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/show/runner.query.graphql'; +import GroupRunnerShowApp from '~/runner/group_runner_show/group_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 = '/groups/group1/-/runners'; +const mockEditGroupRunnerPath = `/groups/group1/-/runners/${mockRunnerId}/edit`; + +Vue.use(VueApollo); + +describe('GroupRunnerShowApp', () => { + let wrapper; + let mockRunnerQuery; + + const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerDetails = () => wrapper.findComponent(RunnerDetails); + const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton); + const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); + const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); + + const mockRunnerQueryResult = (runner = {}) => { + mockRunnerQuery = jest.fn().mockResolvedValue({ + data: { + runner: { ...mockRunner, ...runner }, + }, + }); + }; + + const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { + wrapper = mountFn(GroupRunnerShowApp, { + apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]), + propsData: { + runnerId: mockRunnerId, + runnersPath: mockRunnersPath, + editGroupRunnerPath: mockEditGroupRunnerPath, + ...props, + }, + ...options, + }); + + return waitForPromises(); + }; + + afterEach(() => { + mockRunnerQuery.mockReset(); + wrapper.destroy(); + }); + + describe('When showing runner details', () => { + beforeEach(async () => { + mockRunnerQueryResult(); + + await createComponent({ mountFn: mountExtended }); + }); + + it('expect GraphQL ID to be requested', async () => { + expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId }); + }); + + it('displays the header', async () => { + expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`); + }); + + it('displays edit, pause, delete buttons', async () => { + expect(findRunnerEditButton().exists()).toBe(true); + expect(findRunnerPauseButton().exists()).toBe(true); + expect(findRunnerDeleteButton().exists()).toBe(true); + }); + + it('shows basic runner details', () => { + const expected = `Description Instance runner + 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, ' '); + + expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected); + }); + + it('renders runner details component', () => { + expect(findRunnerDetails().props('runner')).toEqual(mockRunner); + }); + + describe('when runner cannot be updated', () => { + beforeEach(async () => { + mockRunnerQueryResult({ + userPermissions: { + ...mockRunner.userPermissions, + updateRunner: false, + }, + }); + + await createComponent({ + mountFn: mountExtended, + }); + }); + + it('does not display edit and pause buttons', () => { + expect(findRunnerEditButton().exists()).toBe(false); + expect(findRunnerPauseButton().exists()).toBe(false); + }); + + it('displays delete button', () => { + expect(findRunnerDeleteButton().exists()).toBe(true); + }); + }); + + describe('when runner cannot be deleted', () => { + beforeEach(async () => { + mockRunnerQueryResult({ + userPermissions: { + ...mockRunner.userPermissions, + deleteRunner: false, + }, + }); + + await createComponent({ + mountFn: mountExtended, + }); + }); + + it('does not display delete button', () => { + expect(findRunnerDeleteButton().exists()).toBe(false); + }); + + it('displays edit and pause buttons', () => { + expect(findRunnerEditButton().exists()).toBe(true); + expect(findRunnerPauseButton().exists()).toBe(true); + }); + }); + + describe('when runner is deleted', () => { + beforeEach(async () => { + await createComponent({ + mountFn: mountExtended, + }); + }); + + 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 loading', () => { + beforeEach(() => { + mockRunnerQueryResult(); + + createComponent(); + }); + + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + }); + + describe('When there is an error', () => { + beforeEach(async () => { + mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); + await createComponent(); + }); + + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + + it('error is reported to sentry', () => { + expect(captureException).toHaveBeenCalledWith({ + error: new Error('Error!'), + component: 'GroupRunnerShowApp', + }); + }); + + it('error is shown to the user', () => { + expect(createAlert).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 eb9f85a7d0f..9c42b0d6865 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -10,6 +10,7 @@ import { } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; +import { s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; @@ -18,6 +19,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_ import RunnerList from '~/runner/components/runner_list.vue'; import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; +import RunnerCount from '~/runner/components/stat/runner_count.vue'; import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; @@ -28,7 +30,6 @@ import { DEFAULT_SORT, INSTANCE_TYPE, GROUP_TYPE, - PROJECT_TYPE, PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG, @@ -38,11 +39,10 @@ import { RUNNER_PAGE_SIZE, I18N_EDIT, } from '~/runner/constants'; -import getGroupRunnersQuery from '~/runner/graphql/list/group_runners.query.graphql'; -import getGroupRunnersCountQuery from '~/runner/graphql/list/group_runners_count.query.graphql'; +import groupRunnersQuery from '~/runner/graphql/list/group_runners.query.graphql'; +import groupRunnersCountQuery from '~/runner/graphql/list/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, @@ -61,6 +61,9 @@ const mockRegistrationToken = 'AABBCC'; const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges; const mockGroupRunnersCount = mockGroupRunnersEdges.length; +const mockGroupRunnersHandler = jest.fn(); +const mockGroupRunnersCountHandler = jest.fn(); + jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); jest.mock('~/lib/utils/url_utility', () => ({ @@ -70,8 +73,6 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('GroupRunnersApp', () => { let wrapper; - let mockGroupRunnersQuery; - let mockGroupRunnersCountQuery; const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell); @@ -83,17 +84,11 @@ describe('GroupRunnersApp', () => { const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page'); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); - const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); - - const mockCountQueryResult = (count) => - Promise.resolve({ - data: { group: { id: groupRunnersCountData.data.group.id, runners: { count } } }, - }); - const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { + const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { const handlers = [ - [getGroupRunnersQuery, mockGroupRunnersQuery], - [getGroupRunnersCountQuery, mockGroupRunnersCountQuery], + [groupRunnersQuery, mockGroupRunnersHandler], + [groupRunnersCountQuery, mockGroupRunnersCountHandler], ]; wrapper = mountFn(GroupRunnersApp, { @@ -110,90 +105,76 @@ describe('GroupRunnersApp', () => { emptyStateSvgPath, emptyStateFilteredSvgPath, }, + ...options, }); + + return waitForPromises(); }; beforeEach(async () => { - setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`); + mockGroupRunnersHandler.mockResolvedValue(groupRunnersData); + mockGroupRunnersCountHandler.mockResolvedValue(groupRunnersCountData); + }); - mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData); - mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData); + afterEach(() => { + mockGroupRunnersHandler.mockReset(); + mockGroupRunnersCountHandler.mockReset(); + wrapper.destroy(); + }); + + it('shows the runner tabs with a runner count for each type', async () => { + await createComponent({ mountFn: mountExtended }); + expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( + `All ${mockGroupRunnersCount} Group ${mockGroupRunnersCount} Project ${mockGroupRunnersCount}`, + ); + }); + + it('shows the runner setup instructions', () => { createComponent(); - await waitForPromises(); + + expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); + expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE); }); it('shows total runner counts', async () => { - expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ - groupFullPath: mockGroupFullPath, + await createComponent({ mountFn: mountExtended }); + + expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_ONLINE, - }); - expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ groupFullPath: mockGroupFullPath, - status: STATUS_OFFLINE, }); - expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ + status: STATUS_OFFLINE, groupFullPath: mockGroupFullPath, - status: STATUS_STALE, - }); - - expect(findRunnerStats().props()).toMatchObject({ - onlineRunnersCount: mockGroupRunnersCount, - offlineRunnersCount: mockGroupRunnersCount, - staleRunnersCount: mockGroupRunnersCount, - }); - }); - - it('shows the runner tabs with a runner count for each type', async () => { - mockGroupRunnersCountQuery.mockImplementation(({ type }) => { - switch (type) { - case GROUP_TYPE: - return mockCountQueryResult(2); - case PROJECT_TYPE: - return mockCountQueryResult(1); - default: - return mockCountQueryResult(4); - } }); - - createComponent({ mountFn: mountExtended }); - await waitForPromises(); - - expect(findRunnerTypeTabs().text()).toMatchInterpolatedText('All 4 Group 2 Project 1'); - }); - - it('shows the runner tabs with a formatted runner count', async () => { - mockGroupRunnersCountQuery.mockImplementation(({ type }) => { - switch (type) { - case GROUP_TYPE: - return mockCountQueryResult(2000); - case PROJECT_TYPE: - return mockCountQueryResult(1000); - default: - return mockCountQueryResult(3000); - } + expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ + status: STATUS_STALE, + groupFullPath: mockGroupFullPath, }); - createComponent({ mountFn: mountExtended }); - await waitForPromises(); - - expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( - 'All 3,000 Group 2,000 Project 1,000', + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Online runners')} ${mockGroupRunnersCount}`, + ); + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Offline runners')} ${mockGroupRunnersCount}`, + ); + expect(findRunnerStats().text()).toContain( + `${s__('Runners|Stale runners')} ${mockGroupRunnersCount}`, ); }); - it('shows the runner setup instructions', () => { - expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); - expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE); - }); + it('shows the runners list', async () => { + await createComponent(); - it('shows the runners list', () => { const runners = findRunnerList().props('runners'); expect(runners).toEqual(mockGroupRunnersEdges.map(({ node }) => node)); }); - it('requests the runners with group path and no other filters', () => { - expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({ + it('requests the runners with group path and no other filters', async () => { + await createComponent(); + + expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: undefined, type: undefined, @@ -203,9 +184,9 @@ describe('GroupRunnersApp', () => { }); it('sets tokens in the filtered search', () => { - createComponent({ mountFn: mountExtended }); + createComponent(); - const tokens = findFilteredSearch().props('tokens'); + const tokens = findRunnerFilteredSearchBar().props('tokens'); expect(tokens).toEqual([ expect.objectContaining({ @@ -229,12 +210,8 @@ describe('GroupRunnersApp', () => { const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs beforeEach(async () => { - mockGroupRunnersCountQuery.mockClear(); - - createComponent({ mountFn: mountExtended }); + await createComponent({ mountFn: mountExtended }); showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); - - await waitForPromises(); }); it('view link is displayed correctly', () => { @@ -254,11 +231,11 @@ describe('GroupRunnersApp', () => { }); it('When runner is paused or unpaused, some data is refetched', async () => { - expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES); + expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES); findRunnerActionsCell().vm.$emit('toggledPaused'); - expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes( + expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes( COUNT_QUERIES + FILTERED_COUNT_QUERIES, ); @@ -277,8 +254,12 @@ describe('GroupRunnersApp', () => { beforeEach(async () => { setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`); - createComponent(); - await waitForPromises(); + await createComponent({ + stubs: { + RunnerStats, + RunnerCount, + }, + }); }); it('sets the filters in the search bar', () => { @@ -291,7 +272,7 @@ describe('GroupRunnersApp', () => { }); it('requests the runners with filter parameters', () => { - expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({ + expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, type: INSTANCE_TYPE, @@ -301,20 +282,23 @@ describe('GroupRunnersApp', () => { }); it('fetches count results for requested status', () => { - expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + expect(mockGroupRunnersCountHandler).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 () => { + createComponent({ + stubs: { + RunnerStats, + RunnerCount, + }, + }); + findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, filters: [ @@ -330,12 +314,12 @@ describe('GroupRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC', + url: expect.stringContaining('?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC'), }); }); it('requests the runners with filters', () => { - expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({ + expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, tagList: ['tag1'], @@ -345,33 +329,11 @@ describe('GroupRunnersApp', () => { }); it('fetches count results for requested status', () => { - expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + expect(mockGroupRunnersCountHandler).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, - }); }); }); @@ -382,7 +344,7 @@ describe('GroupRunnersApp', () => { describe('when no runners are found', () => { beforeEach(async () => { - mockGroupRunnersQuery = jest.fn().mockResolvedValue({ + mockGroupRunnersHandler.mockResolvedValue({ data: { group: { id: '1', @@ -390,8 +352,7 @@ describe('GroupRunnersApp', () => { }, }, }); - createComponent(); - await waitForPromises(); + await createComponent(); }); it('shows an empty state', async () => { @@ -401,9 +362,8 @@ describe('GroupRunnersApp', () => { describe('when runners query fails', () => { beforeEach(async () => { - mockGroupRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!')); - createComponent(); - await waitForPromises(); + mockGroupRunnersHandler.mockRejectedValue(new Error('Error!')); + await createComponent(); }); it('error is shown to the user', async () => { @@ -420,16 +380,15 @@ describe('GroupRunnersApp', () => { describe('Pagination', () => { beforeEach(async () => { - mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated); + mockGroupRunnersHandler.mockResolvedValue(groupRunnersDataPaginated); - createComponent({ mountFn: mountExtended }); - await waitForPromises(); + await createComponent({ mountFn: mountExtended }); }); it('navigates to the next page', async () => { await findRunnerPaginationNext().trigger('click'); - expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({ + expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index 3368fc21544..e5472ace817 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -10,14 +10,216 @@ import runnerJobsData from 'test_fixtures/graphql/runner/show/runner_jobs.query. 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'; -import runnersCountData from 'test_fixtures/graphql/runner/list/admin_runners_count.query.graphql.json'; +import allRunnersData from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.json'; +import allRunnersDataPaginated from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.paginated.json'; +import runnersCountData from 'test_fixtures/graphql/runner/list/all_runners_count.query.graphql.json'; import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.json'; 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'; +import { RUNNER_PAGE_SIZE } from '~/runner/constants'; + // Other mock data + +// Mock searches and their corresponding urls +export const mockSearchExamples = [ + { + name: 'a default query', + urlQuery: '', + search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }, + graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + isDefault: true, + }, + { + name: 'a single status', + urlQuery: '?status[]=ACTIVE', + search: { + runnerType: null, + filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'a single term text search', + urlQuery: '?search=something', + search: { + runnerType: null, + filters: [ + { + type: 'filtered-search-term', + value: { data: 'something' }, + }, + ], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'a two terms text search', + urlQuery: '?search=something+else', + search: { + runnerType: null, + filters: [ + { + type: 'filtered-search-term', + value: { data: 'something' }, + }, + { + type: 'filtered-search-term', + value: { data: 'else' }, + }, + ], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'single instance type', + urlQuery: '?runner_type[]=INSTANCE_TYPE', + search: { + runnerType: 'INSTANCE_TYPE', + filters: [], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'multiple runner status', + urlQuery: '?status[]=ACTIVE&status[]=PAUSED', + search: { + runnerType: null, + filters: [ + { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, + { type: 'status', value: { data: 'PAUSED', operator: '=' } }, + ], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'multiple status, a single instance type and a non default sort', + urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC', + search: { + runnerType: 'INSTANCE_TYPE', + filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], + pagination: { page: 1 }, + sort: 'CREATED_ASC', + }, + graphqlVariables: { + status: 'ACTIVE', + type: 'INSTANCE_TYPE', + sort: 'CREATED_ASC', + first: RUNNER_PAGE_SIZE, + }, + }, + { + name: 'a tag', + urlQuery: '?tag[]=tag-1', + search: { + runnerType: null, + filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { + tagList: ['tag-1'], + first: 20, + sort: 'CREATED_DESC', + }, + }, + { + name: 'two tags', + urlQuery: '?tag[]=tag-1&tag[]=tag-2', + search: { + runnerType: null, + filters: [ + { type: 'tag', value: { data: 'tag-1', operator: '=' } }, + { type: 'tag', value: { data: 'tag-2', operator: '=' } }, + ], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { + tagList: ['tag-1', 'tag-2'], + first: 20, + sort: 'CREATED_DESC', + }, + }, + { + name: 'the next page', + urlQuery: '?page=2&after=AFTER_CURSOR', + search: { + runnerType: null, + filters: [], + pagination: { page: 2, after: 'AFTER_CURSOR' }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'the previous page', + urlQuery: '?page=2&before=BEFORE_CURSOR', + search: { + runnerType: null, + filters: [], + pagination: { page: 2, before: 'BEFORE_CURSOR' }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE }, + }, + { + name: 'the next page filtered by a status, an instance type, tags and a non default sort', + urlQuery: + '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR', + search: { + runnerType: 'INSTANCE_TYPE', + filters: [ + { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, + { type: 'tag', value: { data: 'tag-1', operator: '=' } }, + { type: 'tag', value: { data: 'tag-2', operator: '=' } }, + ], + pagination: { page: 2, after: 'AFTER_CURSOR' }, + sort: 'CREATED_ASC', + }, + graphqlVariables: { + status: 'ACTIVE', + type: 'INSTANCE_TYPE', + tagList: ['tag-1', 'tag-2'], + sort: 'CREATED_ASC', + after: 'AFTER_CURSOR', + first: RUNNER_PAGE_SIZE, + }, + }, + { + name: 'paused runners', + urlQuery: '?paused[]=true', + search: { + runnerType: null, + filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, + { + name: 'active runners', + urlQuery: '?paused[]=false', + search: { + runnerType: null, + filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }], + pagination: { page: 1 }, + sort: 'CREATED_DESC', + }, + graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + }, +]; + export const onlineContactTimeoutSecs = 2 * 60 * 60; export const staleTimeoutSecs = 7889238; // Ruby's `3.months` @@ -25,8 +227,8 @@ export const emptyStateSvgPath = 'emptyStateSvgPath.svg'; export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg'; export { - runnersData, - runnersDataPaginated, + allRunnersData, + allRunnersDataPaginated, runnersCountData, groupRunnersData, groupRunnersDataPaginated, diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js index 1f102f86b2a..6f954143ab1 100644 --- a/spec/frontend/runner/runner_search_utils_spec.js +++ b/spec/frontend/runner/runner_search_utils_spec.js @@ -1,4 +1,3 @@ -import { RUNNER_PAGE_SIZE } from '~/runner/constants'; import { searchValidator, updateOutdatedUrl, @@ -6,209 +5,12 @@ import { fromSearchToUrl, fromSearchToVariables, isSearchFiltered, -} from '~/runner/runner_search_utils'; +} from 'ee_else_ce/runner/runner_search_utils'; +import { mockSearchExamples } from './mock_data'; describe('search_params.js', () => { - const examples = [ - { - name: 'a default query', - urlQuery: '', - search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }, - graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - isDefault: true, - }, - { - name: 'a single status', - urlQuery: '?status[]=ACTIVE', - search: { - runnerType: null, - filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'a single term text search', - urlQuery: '?search=something', - search: { - runnerType: null, - filters: [ - { - type: 'filtered-search-term', - value: { data: 'something' }, - }, - ], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'a two terms text search', - urlQuery: '?search=something+else', - search: { - runnerType: null, - filters: [ - { - type: 'filtered-search-term', - value: { data: 'something' }, - }, - { - type: 'filtered-search-term', - value: { data: 'else' }, - }, - ], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'single instance type', - urlQuery: '?runner_type[]=INSTANCE_TYPE', - search: { - runnerType: 'INSTANCE_TYPE', - filters: [], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'multiple runner status', - urlQuery: '?status[]=ACTIVE&status[]=PAUSED', - search: { - runnerType: null, - filters: [ - { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, - { type: 'status', value: { data: 'PAUSED', operator: '=' } }, - ], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'multiple status, a single instance type and a non default sort', - urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC', - search: { - runnerType: 'INSTANCE_TYPE', - filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], - pagination: { page: 1 }, - sort: 'CREATED_ASC', - }, - graphqlVariables: { - status: 'ACTIVE', - type: 'INSTANCE_TYPE', - sort: 'CREATED_ASC', - first: RUNNER_PAGE_SIZE, - }, - }, - { - name: 'a tag', - urlQuery: '?tag[]=tag-1', - search: { - runnerType: null, - filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { - tagList: ['tag-1'], - first: 20, - sort: 'CREATED_DESC', - }, - }, - { - name: 'two tags', - urlQuery: '?tag[]=tag-1&tag[]=tag-2', - search: { - runnerType: null, - filters: [ - { type: 'tag', value: { data: 'tag-1', operator: '=' } }, - { type: 'tag', value: { data: 'tag-2', operator: '=' } }, - ], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { - tagList: ['tag-1', 'tag-2'], - first: 20, - sort: 'CREATED_DESC', - }, - }, - { - name: 'the next page', - urlQuery: '?page=2&after=AFTER_CURSOR', - search: { - runnerType: null, - filters: [], - pagination: { page: 2, after: 'AFTER_CURSOR' }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'the previous page', - urlQuery: '?page=2&before=BEFORE_CURSOR', - search: { - runnerType: null, - filters: [], - pagination: { page: 2, before: 'BEFORE_CURSOR' }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE }, - }, - { - name: 'the next page filtered by a status, an instance type, tags and a non default sort', - urlQuery: - '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR', - search: { - runnerType: 'INSTANCE_TYPE', - filters: [ - { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, - { type: 'tag', value: { data: 'tag-1', operator: '=' } }, - { type: 'tag', value: { data: 'tag-2', operator: '=' } }, - ], - pagination: { page: 2, after: 'AFTER_CURSOR' }, - sort: 'CREATED_ASC', - }, - graphqlVariables: { - status: 'ACTIVE', - type: 'INSTANCE_TYPE', - tagList: ['tag-1', 'tag-2'], - sort: 'CREATED_ASC', - after: 'AFTER_CURSOR', - first: RUNNER_PAGE_SIZE, - }, - }, - { - name: 'paused runners', - urlQuery: '?paused[]=true', - search: { - runnerType: null, - filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - { - name: 'active runners', - urlQuery: '?paused[]=false', - search: { - runnerType: null, - filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }], - pagination: { page: 1 }, - sort: 'CREATED_DESC', - }, - graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, - }, - ]; - describe('searchValidator', () => { - examples.forEach(({ name, search }) => { + mockSearchExamples.forEach(({ name, search }) => { it(`Validates ${name} as a search object`, () => { expect(searchValidator(search)).toBe(true); }); @@ -235,7 +37,7 @@ describe('search_params.js', () => { }); describe('fromUrlQueryToSearch', () => { - examples.forEach(({ name, urlQuery, search }) => { + mockSearchExamples.forEach(({ name, urlQuery, search }) => { it(`Converts ${name} to a search object`, () => { expect(fromUrlQueryToSearch(urlQuery)).toEqual(search); }); @@ -268,7 +70,7 @@ describe('search_params.js', () => { }); describe('fromSearchToUrl', () => { - examples.forEach(({ name, urlQuery, search }) => { + mockSearchExamples.forEach(({ name, urlQuery, search }) => { it(`Converts ${name} to a url`, () => { expect(fromSearchToUrl(search)).toBe(`http://test.host/${urlQuery}`); }); @@ -295,7 +97,7 @@ describe('search_params.js', () => { }); describe('fromSearchToVariables', () => { - examples.forEach(({ name, graphqlVariables, search }) => { + mockSearchExamples.forEach(({ name, graphqlVariables, search }) => { it(`Converts ${name} to a GraphQL query variables object`, () => { expect(fromSearchToVariables(search)).toEqual(graphqlVariables); }); @@ -335,7 +137,7 @@ describe('search_params.js', () => { }); describe('isSearchFiltered', () => { - examples.forEach(({ name, search, isDefault }) => { + mockSearchExamples.forEach(({ name, search, isDefault }) => { it(`Given ${name}, evaluates to ${isDefault ? 'not ' : ''}filtered`, () => { expect(isSearchFiltered(search)).toBe(!isDefault); }); |