summaryrefslogtreecommitdiff
path: root/spec/frontend/runner
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/runner')
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js111
-rw-r--r--spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap3
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js22
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js6
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js17
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js75
-rw-r--r--spec/frontend/runner/components/runner_assigned_item_spec.js3
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_spec.js103
-rw-r--r--spec/frontend/runner/components/runner_delete_button_spec.js64
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js6
-rw-r--r--spec/frontend/runner/components/runner_jobs_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js65
-rw-r--r--spec/frontend/runner/components/runner_pause_button_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_status_badge_spec.js25
-rw-r--r--spec/frontend/runner/components/runner_status_popover_spec.js36
-rw-r--r--spec/frontend/runner/graphql/local_state_spec.js72
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js56
-rw-r--r--spec/frontend/runner/mock_data.js4
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js40
-rw-r--r--spec/frontend/runner/utils_spec.js4
21 files changed, 589 insertions, 131 deletions
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 cdaec0a3a8b..2ef856c90ab 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -13,6 +13,7 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
+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';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
@@ -30,9 +31,10 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
- STATUS_ACTIVE,
+ STATUS_ONLINE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql';
@@ -40,9 +42,16 @@ import adminRunnersCountQuery from '~/runner/graphql/list/admin_runners_count.qu
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
+import {
+ runnersData,
+ runnersCountData,
+ runnersDataPaginated,
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+} from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+const mockRunners = runnersData.data.runners.nodes;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -58,6 +67,8 @@ describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
let mockRunnersCountQuery;
+ let cacheConfig;
+ let localMutations;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
@@ -69,18 +80,32 @@ describe('AdminRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({
+ props = {},
+ mountFn = shallowMountExtended,
+ provide,
+ ...options
+ } = {}) => {
+ ({ cacheConfig, localMutations } = createLocalState());
+
const handlers = [
[adminRunnersQuery, mockRunnersQuery],
[adminRunnersCountQuery, mockRunnersCountQuery],
];
wrapper = mountFn(AdminRunnersApp, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
...props,
},
+ provide: {
+ localMutations,
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ ...provide,
+ },
+ ...options,
});
};
@@ -173,7 +198,7 @@ describe('AdminRunnersApp', () => {
});
it('shows the runners list', () => {
- expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
+ expect(findRunnerList().props('runners')).toEqual(mockRunners);
});
it('runner item links to the runner admin page', async () => {
@@ -181,7 +206,7 @@ describe('AdminRunnersApp', () => {
await waitForPromises();
- const { id, shortSha } = runnersData.data.runners.nodes[0];
+ const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
@@ -197,7 +222,7 @@ describe('AdminRunnersApp', () => {
const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell);
- const runner = runnersData.data.runners.nodes[0];
+ const runner = mockRunners[0];
expect(runnerActions.props()).toEqual({
runner,
@@ -219,6 +244,10 @@ describe('AdminRunnersApp', () => {
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
+ type: PARAM_KEY_PAUSED,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
@@ -232,12 +261,13 @@ describe('AdminRunnersApp', () => {
describe('Single runner row', () => {
let showToast;
- const mockRunner = runnersData.data.runners.nodes[0];
- const { id: graphqlId, shortSha } = mockRunner;
+ const { id: graphqlId, shortSha } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockRunnersQuery.mockClear();
+ mockRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -252,12 +282,18 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
- expect(mockRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
- expect(mockRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES);
+
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
@@ -266,7 +302,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
- setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
+ setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
createComponent();
await waitForPromises();
@@ -276,7 +312,7 @@ describe('AdminRunnersApp', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [
- { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
@@ -286,7 +322,7 @@ describe('AdminRunnersApp', () => {
it('requests the runners with filter parameters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
type: INSTANCE_TYPE,
tagList: ['tag1'],
sort: DEFAULT_SORT,
@@ -299,7 +335,7 @@ describe('AdminRunnersApp', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
});
@@ -307,13 +343,13 @@ describe('AdminRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/admin/runners?status[]=ACTIVE&sort=CREATED_ASC',
+ url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
@@ -325,6 +361,41 @@ describe('AdminRunnersApp', () => {
expect(findRunnerList().props('loading')).toBe(true);
});
+ describe('when bulk delete is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { adminRunnersBulkDelete: true },
+ },
+ });
+ });
+
+ it('runner list is checkable', () => {
+ expect(findRunnerList().props('checkable')).toBe(true);
+ });
+
+ it('responds to checked items by updating the local cache', () => {
+ const setRunnerCheckedMock = jest
+ .spyOn(localMutations, 'setRunnerChecked')
+ .mockImplementation(() => {});
+
+ const runner = mockRunners[0];
+
+ expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
+
+ findRunnerList().vm.$emit('checked', {
+ runner,
+ isChecked: true,
+ });
+
+ expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
+ expect(setRunnerCheckedMock).toHaveBeenCalledWith({
+ runner,
+ isChecked: true,
+ });
+ });
+ });
+
describe('when no runners are found', () => {
beforeEach(async () => {
mockRunnersQuery = jest.fn().mockResolvedValue({
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
new file mode 100644
index 00000000000..80a04401760
--- /dev/null
+++ b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RunnerStatusPopover renders complete text 1`] = `"Never contacted: Runner has never contacted GitLab (when you register a runner, use gitlab-runner run to bring it online) Online: Runner has contacted GitLab within the last 2 hours Offline: Runner has not contacted GitLab in more than 2 hours Stale: Runner has not contacted GitLab in more than 2 months"`;
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 0d579106860..7a949cb6505 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -92,6 +92,24 @@ describe('RunnerActionsCell', () => {
expect(findDeleteBtn().props('compact')).toBe(true);
});
+ it('Passes runner data to delete button', () => {
+ createComponent({
+ runner: mockRunner,
+ });
+
+ expect(findDeleteBtn().props('runner')).toEqual(mockRunner);
+ });
+
+ it('Emits toggledPaused events', () => {
+ createComponent();
+
+ expect(wrapper.emitted('toggledPaused')).toBe(undefined);
+
+ findRunnerPauseBtn().vm.$emit('toggledPaused');
+
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
+
it('Emits delete events', () => {
const value = { name: 'Runner' };
@@ -104,7 +122,7 @@ describe('RunnerActionsCell', () => {
expect(wrapper.emitted('deleted')).toEqual([[value]]);
});
- it('Does not render the runner delete button when user cannot delete', () => {
+ it('Renders the runner delete disabled button when user cannot delete', () => {
createComponent({
runner: {
userPermissions: {
@@ -114,7 +132,7 @@ describe('RunnerActionsCell', () => {
},
});
- expect(findDeleteBtn().exists()).toBe(false);
+ expect(findDeleteBtn().props('disabled')).toBe(true);
});
});
});
diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
index b6d957d27ea..b2e8c5a3ad9 100644
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -5,6 +5,7 @@ import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
const mockDescription = 'runner-1';
+const mockIpAddress = '0.0.0.0';
describe('RunnerTypeCell', () => {
let wrapper;
@@ -18,6 +19,7 @@ describe('RunnerTypeCell', () => {
id: `gid://gitlab/Ci::Runner/${mockId}`,
shortSha: mockShortSha,
description: mockDescription,
+ ipAddress: mockIpAddress,
runnerType: INSTANCE_TYPE,
...runner,
},
@@ -59,6 +61,10 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(mockDescription);
});
+ it('Displays the runner ip address', () => {
+ expect(wrapper.text()).toContain(mockIpAddress);
+ });
+
it('Displays a custom slot', () => {
const slotContent = 'My custom runner summary';
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
index da8ef7c3af0..5cd93df9967 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -8,6 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
+import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
@@ -30,11 +31,11 @@ describe('RegistrationDropdown', () => {
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
+ const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
+ const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
- const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
-
const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(RegistrationDropdown, {
@@ -134,9 +135,7 @@ describe('RegistrationDropdown', () => {
it('Displays masked value by default', () => {
createComponent({}, mount);
- expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
- `Registration token ${maskToken}`,
- );
+ expect(findRegistrationTokenInput().element.value).toBe(maskToken);
});
});
@@ -155,16 +154,14 @@ describe('RegistrationDropdown', () => {
});
it('Updates the token when it gets reset', async () => {
+ const newToken = 'mock1';
createComponent({}, mount);
- const newToken = 'mock1';
+ expect(findRegistrationTokenInput().props('value')).not.toBe(newToken);
findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
- findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() });
await nextTick();
- expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
- `Registration token ${newToken}`,
- );
+ expect(findRegistrationToken().props('value')).toBe(newToken);
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
index 6b9708cc525..cb42c7c8493 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -1,20 +1,17 @@
-import { nextTick } from 'vue';
import { GlToast } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
const mockToken = '01234567890';
const mockMasked = '***********';
describe('RegistrationToken', () => {
let wrapper;
- let stopPropagation;
let showToast;
- const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
- const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
+ const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
const vueWithGlToast = () => {
const localVue = createLocalVue();
@@ -22,10 +19,14 @@ describe('RegistrationToken', () => {
return localVue;
};
- const createComponent = ({ props = {}, withGlToast = true } = {}) => {
+ const createComponent = ({
+ props = {},
+ withGlToast = true,
+ mountFn = shallowMountExtended,
+ } = {}) => {
const localVue = withGlToast ? vueWithGlToast() : undefined;
- wrapper = shallowMountExtended(RegistrationToken, {
+ wrapper = mountFn(RegistrationToken, {
propsData: {
value: mockToken,
...props,
@@ -36,61 +37,33 @@ describe('RegistrationToken', () => {
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
- beforeEach(() => {
- stopPropagation = jest.fn();
-
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('Displays masked value by default', () => {
- expect(wrapper.text()).toBe(mockMasked);
- });
+ it('Displays value and copy button', () => {
+ createComponent();
- it('Displays button to reveal token', () => {
- expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
+ expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
+ 'Copy registration token',
+ );
});
- it('Can copy the original token value', () => {
- expect(findCopyButton().props('text')).toBe(mockToken);
+ // Component integration test to ensure secure masking
+ it('Displays masked value by default', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(wrapper.find('input').element.value).toBe(mockMasked);
});
- describe('When the reveal icon is clicked', () => {
+ describe('When the copy to clipboard button is clicked', () => {
beforeEach(() => {
- findToggleMaskButton().vm.$emit('click', { stopPropagation });
- });
-
- it('Click event is not propagated', async () => {
- expect(stopPropagation).toHaveBeenCalledTimes(1);
+ createComponent();
});
- it('Displays the actual value', () => {
- expect(wrapper.text()).toBe(mockToken);
- });
-
- it('Can copy the original token value', () => {
- expect(findCopyButton().props('text')).toBe(mockToken);
- });
-
- it('Displays button to mask token', () => {
- expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to hide');
- });
-
- it('When user clicks again, displays masked value', async () => {
- findToggleMaskButton().vm.$emit('click', { stopPropagation });
- await nextTick();
-
- expect(wrapper.text()).toBe(mockMasked);
- expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
- });
- });
-
- describe('When the copy to clipboard button is clicked', () => {
it('shows a copied message', () => {
- findCopyButton().vm.$emit('success');
+ findInputCopyToggleVisibility().vm.$emit('copy');
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
@@ -98,7 +71,7 @@ describe('RegistrationToken', () => {
it('does not fail when toast is not defined', () => {
createComponent({ withGlToast: false });
- findCopyButton().vm.$emit('success');
+ findInputCopyToggleVisibility().vm.$emit('copy');
// This block also tests for unhandled errors
expect(showToast).toBeNull();
diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js
index c6156c16d4a..1ff6983fbe7 100644
--- a/spec/frontend/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/runner/components/runner_assigned_item_spec.js
@@ -1,6 +1,7 @@
import { GlAvatar } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
const mockHref = '/group/project';
const mockName = 'Project';
@@ -40,7 +41,7 @@ describe('RunnerAssignedItem', () => {
alt: mockName,
entityName: mockName,
src: mockAvatarUrl,
- shape: 'rect',
+ shape: AVATAR_SHAPE_OPTION_RECT,
size: 48,
});
});
diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js
new file mode 100644
index 00000000000..f5b56396cf1
--- /dev/null
+++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import { GlSprintf } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createLocalState } from '~/runner/graphql/list/local_state';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+describe('RunnerBulkDelete', () => {
+ let wrapper;
+ let mockState;
+ let mockCheckedRunnerIds;
+
+ const findClearBtn = () => wrapper.findByTestId('clear-btn');
+ const findDeleteBtn = () => wrapper.findByTestId('delete-btn');
+
+ const createComponent = () => {
+ const { cacheConfig, localMutations } = mockState;
+
+ wrapper = shallowMountExtended(RunnerBulkDelete, {
+ apolloProvider: createMockApollo(undefined, undefined, cacheConfig),
+ provide: {
+ localMutations,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockState = createLocalState();
+
+ jest
+ .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
+ .mockImplementation(() => mockCheckedRunnerIds);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('When no runners are checked', () => {
+ beforeEach(async () => {
+ mockCheckedRunnerIds = [];
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('shows no contents', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe.each`
+ count | ids | text
+ ${1} | ${['gid:Runner/1']} | ${'1 runner'}
+ ${2} | ${['gid:Runner/1', 'gid:Runner/2']} | ${'2 runners'}
+ `('When $count runner(s) are checked', ({ count, ids, text }) => {
+ beforeEach(() => {
+ mockCheckedRunnerIds = ids;
+
+ createComponent();
+
+ jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
+ });
+
+ it(`shows "${text}"`, () => {
+ expect(wrapper.text()).toContain(text);
+ });
+
+ it('clears selection', () => {
+ expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(0);
+
+ findClearBtn().vm.$emit('click');
+
+ expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows confirmation modal', () => {
+ expect(confirmAction).toHaveBeenCalledTimes(0);
+
+ findDeleteBtn().vm.$emit('click');
+
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+
+ const [, confirmOptions] = confirmAction.mock.calls[0];
+ const { title, modalHtmlMessage, primaryBtnText } = confirmOptions;
+
+ expect(title).toMatch(text);
+ expect(primaryBtnText).toMatch(text);
+ expect(modalHtmlMessage).toMatch(`<strong>${count}</strong>`);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js
index 81c870f23cf..3eb257607b4 100644
--- a/spec/frontend/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/runner/components/runner_delete_button_spec.js
@@ -9,7 +9,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
-import { I18N_DELETE_RUNNER } from '~/runner/constants';
+import {
+ I18N_DELETE_RUNNER,
+ I18N_DELETE_DISABLED_MANY_PROJECTS,
+ I18N_DELETE_DISABLED_UNKNOWN_REASON,
+} from '~/runner/constants';
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
@@ -25,26 +29,32 @@ jest.mock('~/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
+ let apolloProvider;
+ let apolloCache;
let runnerDeleteHandler;
- const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const getModal = () => getBinding(wrapper.element, 'gl-modal').value;
const findBtn = () => wrapper.findComponent(GlButton);
const findModal = () => wrapper.findComponent(RunnerDeleteModal);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+ const getModal = () => getBinding(findBtn().element, 'gl-modal').value;
+
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const { runner, ...propsData } = props;
wrapper = mountFn(RunnerDeleteButton, {
propsData: {
runner: {
+ // We need typename so that cache.identify works
+ // eslint-disable-next-line no-underscore-dangle
+ __typename: mockRunner.__typename,
id: mockRunner.id,
shortSha: mockRunner.shortSha,
...runner,
},
...propsData,
},
- apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]),
+ apolloProvider,
directives: {
GlTooltip: createMockDirective(),
GlModal: createMockDirective(),
@@ -67,6 +77,11 @@ describe('RunnerDeleteButton', () => {
},
});
});
+ apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]);
+ apolloCache = apolloProvider.defaultClient.cache;
+
+ jest.spyOn(apolloCache, 'evict');
+ jest.spyOn(apolloCache, 'gc');
createComponent();
});
@@ -88,6 +103,10 @@ describe('RunnerDeleteButton', () => {
expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`);
});
+ it('Does not have tabindex when button is enabled', () => {
+ expect(wrapper.attributes('tabindex')).toBeUndefined();
+ });
+
it('Displays a modal when clicked', () => {
const modalId = `delete-runner-modal-${mockRunnerId}`;
@@ -140,6 +159,13 @@ describe('RunnerDeleteButton', () => {
expect(deleted[0][0].message).toMatch(`#${mockRunnerId}`);
expect(deleted[0][0].message).toMatch(`${mockRunner.shortSha}`);
});
+
+ it('evicts runner from apollo cache', () => {
+ expect(apolloCache.evict).toHaveBeenCalledWith({
+ id: apolloCache.identify(mockRunner),
+ });
+ expect(apolloCache.gc).toHaveBeenCalled();
+ });
});
describe('When update fails', () => {
@@ -190,6 +216,11 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
+
+ it('does not evict runner from apollo cache', () => {
+ expect(apolloCache.evict).not.toHaveBeenCalled();
+ expect(apolloCache.gc).not.toHaveBeenCalled();
+ });
});
});
@@ -230,4 +261,29 @@ describe('RunnerDeleteButton', () => {
});
});
});
+
+ describe.each`
+ reason | runner | tooltip
+ ${'runner belongs to more than 1 project'} | ${{ projectCount: 2 }} | ${I18N_DELETE_DISABLED_MANY_PROJECTS}
+ ${'unknown reason'} | ${{}} | ${I18N_DELETE_DISABLED_UNKNOWN_REASON}
+ `('When button is disabled because $reason', ({ runner, tooltip }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ disabled: true,
+ runner,
+ },
+ });
+ });
+
+ it('Displays a disabled delete button', () => {
+ expect(findBtn().props('disabled')).toBe(true);
+ });
+
+ it(`Tooltip "${tooltip}" is shown`, () => {
+ // tabindex is required for a11y
+ expect(wrapper.attributes('tabindex')).toBe('0');
+ expect(getTooltip()).toBe(tooltip);
+ });
+ });
});
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 fda96e5918e..b1b436e5443 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -4,7 +4,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_
import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
-import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, INSTANCE_TYPE } from '~/runner/constants';
+import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, INSTANCE_TYPE } from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -18,7 +18,7 @@ describe('RunnerList', () => {
const mockDefaultSort = 'CREATED_DESC';
const mockOtherSort = 'CONTACTED_DESC';
const mockFilters = [
- { type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
];
@@ -113,7 +113,7 @@ describe('RunnerList', () => {
});
it('filter values are shown', () => {
- expect(findGlFilteredSearch().props('value')).toEqual(mockFilters);
+ expect(findGlFilteredSearch().props('value')).toMatchObject(mockFilters);
});
it('sort option is selected', () => {
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
index 9abb2861005..9e40e911448 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index a0f42738d2c..872394430ae 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -6,7 +6,8 @@ import {
} from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
-import { runnersData } from '../mock_data';
+import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import { runnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
const mockRunners = runnersData.data.runners.nodes;
const mockActiveRunnersCount = mockRunners.length;
@@ -28,26 +29,38 @@ describe('RunnerList', () => {
activeRunnersCount: mockActiveRunnersCount,
...props,
},
+ provide: {
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ },
...options,
});
};
- beforeEach(() => {
- createComponent({}, mountExtended);
- });
-
afterEach(() => {
wrapper.destroy();
});
it('Displays headers', () => {
+ createComponent(
+ {
+ stubs: {
+ RunnerStatusPopover: {
+ template: '<div/>',
+ },
+ },
+ },
+ mountExtended,
+ );
+
const headerLabels = findHeaders().wrappers.map((w) => w.text());
+ expect(findHeaders().at(0).findComponent(RunnerStatusPopover).exists()).toBe(true);
+
expect(headerLabels).toEqual([
'Status',
'Runner',
'Version',
- 'IP',
'Jobs',
'Tags',
'Last contact',
@@ -56,19 +69,23 @@ describe('RunnerList', () => {
});
it('Sets runner id as a row key', () => {
- createComponent({});
+ createComponent();
expect(findTable().attributes('primary-key')).toBe('id');
});
it('Displays a list of runners', () => {
+ createComponent({}, mountExtended);
+
expect(findRows()).toHaveLength(4);
expect(findSkeletonLoader().exists()).toBe(false);
});
it('Displays details of a runner', () => {
- const { id, description, version, ipAddress, shortSha } = mockRunners[0];
+ const { id, description, version, shortSha } = mockRunners[0];
+
+ createComponent({}, mountExtended);
// Badges
expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText(
@@ -83,7 +100,6 @@ describe('RunnerList', () => {
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
- expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -92,6 +108,35 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true);
});
+ describe('When the list is checkable', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ props: {
+ checkable: true,
+ },
+ },
+ mountExtended,
+ );
+ });
+
+ it('Displays a checkbox field', () => {
+ expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
+ });
+
+ it('Emits a checked event', () => {
+ const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
+
+ checkbox.setChecked();
+
+ expect(wrapper.emitted('checked')).toHaveLength(1);
+ expect(wrapper.emitted('checked')[0][0]).toEqual({
+ isChecked: true,
+ runner: mockRunners[0],
+ });
+ });
+ });
+
describe('Scoped cell slots', () => {
it('Render #runner-name slot in "summary" cell', () => {
createComponent(
@@ -156,6 +201,8 @@ describe('RunnerList', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
+ createComponent({}, mountExtended);
+
expect(findCell({ fieldKey: 'summary' }).text()).toContain(`#${numericId} (${shortSha})`);
});
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js
index 3d9df03977e..9ebb30b6ed7 100644
--- a/spec/frontend/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/runner/components/runner_pause_button_spec.js
@@ -146,6 +146,10 @@ describe('RunnerPauseButton', () => {
it('The button does not have a loading state', () => {
expect(findBtn().props('loading')).toBe(false);
});
+
+ it('The button emits toggledPaused', () => {
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
});
describe('When update fails', () => {
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
index 96de8d11bca..62ebc6539e2 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js
index c470c6bb989..bb833bd7d5a 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/runner/components/runner_status_badge_spec.js
@@ -7,6 +7,8 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NEVER_CONTACTED,
+ I18N_NEVER_CONTACTED_TOOLTIP,
+ I18N_STALE_NEVER_CONTACTED_TOOLTIP,
} from '~/runner/constants';
describe('RunnerTypeBadge', () => {
@@ -59,7 +61,7 @@ describe('RunnerTypeBadge', () => {
expect(wrapper.text()).toBe('never contacted');
expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toMatch('This runner has never contacted');
+ expect(getTooltip().value).toBe(I18N_NEVER_CONTACTED_TOOLTIP);
});
it('renders offline state', () => {
@@ -72,9 +74,7 @@ describe('RunnerTypeBadge', () => {
expect(wrapper.text()).toBe('offline');
expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toBe(
- 'No recent contact from this runner; last contact was 1 day ago',
- );
+ expect(getTooltip().value).toBe('Runner is offline; last contact was 1 day ago');
});
it('renders stale state', () => {
@@ -87,7 +87,20 @@ describe('RunnerTypeBadge', () => {
expect(wrapper.text()).toBe('stale');
expect(findBadge().props('variant')).toBe('warning');
- expect(getTooltip().value).toBe('No contact from this runner in over 3 months');
+ expect(getTooltip().value).toBe('Runner is stale; last contact was 1 year ago');
+ });
+
+ it('renders stale state with no contact time', () => {
+ createComponent({
+ runner: {
+ contactedAt: null,
+ status: STATUS_STALE,
+ },
+ });
+
+ expect(wrapper.text()).toBe('stale');
+ expect(findBadge().props('variant')).toBe('warning');
+ expect(getTooltip().value).toBe(I18N_STALE_NEVER_CONTACTED_TOOLTIP);
});
describe('does not fail when data is missing', () => {
@@ -100,7 +113,7 @@ describe('RunnerTypeBadge', () => {
});
expect(wrapper.text()).toBe('online');
- expect(getTooltip().value).toBe('Runner is online; last contact was n/a');
+ expect(getTooltip().value).toBe('Runner is online; last contact was never');
});
it('status is missing', () => {
diff --git a/spec/frontend/runner/components/runner_status_popover_spec.js b/spec/frontend/runner/components/runner_status_popover_spec.js
new file mode 100644
index 00000000000..789283d1245
--- /dev/null
+++ b/spec/frontend/runner/components/runner_status_popover_spec.js
@@ -0,0 +1,36 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import { onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
+
+describe('RunnerStatusPopover', () => {
+ let wrapper;
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = shallowMountExtended(RunnerStatusPopover, {
+ provide: {
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ ...provide,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+
+ it('renders popoover', () => {
+ createComponent();
+
+ expect(findHelpPopover().exists()).toBe(true);
+ });
+
+ it('renders complete text', () => {
+ createComponent();
+
+ expect(findHelpPopover().text()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/runner/graphql/local_state_spec.js
new file mode 100644
index 00000000000..5c4302e4aa2
--- /dev/null
+++ b/spec/frontend/runner/graphql/local_state_spec.js
@@ -0,0 +1,72 @@
+import createApolloClient from '~/lib/graphql';
+import { createLocalState } from '~/runner/graphql/list/local_state';
+import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
+
+describe('~/runner/graphql/list/local_state', () => {
+ let localState;
+ let apolloClient;
+
+ const createSubject = () => {
+ if (apolloClient) {
+ throw new Error('test subject already exists!');
+ }
+
+ localState = createLocalState();
+
+ const { cacheConfig, typeDefs } = localState;
+
+ apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
+ };
+
+ const queryCheckedRunnerIds = () => {
+ const { checkedRunnerIds } = apolloClient.readQuery({
+ query: getCheckedRunnerIdsQuery,
+ });
+ return checkedRunnerIds;
+ };
+
+ beforeEach(() => {
+ createSubject();
+ });
+
+ afterEach(() => {
+ localState = null;
+ apolloClient = null;
+ });
+
+ describe('default', () => {
+ it('has empty checked list', () => {
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+ });
+
+ describe.each`
+ inputs | expected
+ ${[['a', true], ['b', true], ['b', true]]} | ${['a', 'b']}
+ ${[['a', true], ['b', true], ['a', false]]} | ${['b']}
+ ${[['c', true], ['b', true], ['a', true], ['d', false]]} | ${['c', 'b', 'a']}
+ `('setRunnerChecked', ({ inputs, expected }) => {
+ beforeEach(() => {
+ inputs.forEach(([id, isChecked]) => {
+ localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
+ });
+ });
+ it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
+ expect(queryCheckedRunnerIds()).toEqual(expected);
+ });
+ });
+
+ describe('clearChecked', () => {
+ it('clears all checked items', () => {
+ ['a', 'b', 'c'].forEach((id) => {
+ localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
+ });
+
+ expect(queryCheckedRunnerIds()).toEqual(['a', 'b', 'c']);
+
+ localState.localMutations.clearChecked();
+
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+ });
+});
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 70e303e8626..02348bf737a 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -28,8 +28,9 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
- STATUS_ACTIVE,
+ STATUS_ONLINE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
@@ -38,7 +39,13 @@ import getGroupRunnersCountQuery from '~/runner/graphql/list/group_runners_count
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, groupRunnersCountData } from '../mock_data';
+import {
+ groupRunnersData,
+ groupRunnersDataPaginated,
+ groupRunnersCountData,
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+} from '../mock_data';
Vue.use(VueApollo);
Vue.use(GlToast);
@@ -90,6 +97,10 @@ describe('GroupRunnersApp', () => {
groupRunnersLimitedCount: mockGroupRunnersLimitedCount,
...props,
},
+ provide: {
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ },
});
};
@@ -178,13 +189,16 @@ describe('GroupRunnersApp', () => {
const tokens = findFilteredSearch().props('tokens');
- expect(tokens).toHaveLength(1);
- expect(tokens[0]).toEqual(
+ expect(tokens).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_PAUSED,
+ options: expect.any(Array),
+ }),
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
- );
+ ]);
});
describe('Single runner row', () => {
@@ -193,9 +207,11 @@ describe('GroupRunnersApp', () => {
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockGroupRunnersQuery.mockClear();
+ mockGroupRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -219,12 +235,20 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is deleted, data is refetched and a toast is shown', async () => {
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
+
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(
+ COUNT_QUERIES + FILTERED_COUNT_QUERIES,
+ );
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
@@ -233,7 +257,7 @@ describe('GroupRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
- setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
+ setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`);
createComponent();
await waitForPromises();
@@ -242,7 +266,7 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
- filters: [{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
@@ -251,7 +275,7 @@ describe('GroupRunnersApp', () => {
it('requests the runners with filter parameters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
type: INSTANCE_TYPE,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
@@ -263,7 +287,7 @@ describe('GroupRunnersApp', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
@@ -273,14 +297,14 @@ describe('GroupRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/groups/group1/-/runners?status[]=ACTIVE&sort=CREATED_ASC',
+ url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 49c25039719..fbe8926124c 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -14,6 +14,10 @@ import runnerWithGroupData from 'test_fixtures/graphql/runner/details/runner.que
import runnerProjectsData from 'test_fixtures/graphql/runner/details/runner_projects.query.graphql.json';
import runnerJobsData from 'test_fixtures/graphql/runner/details/runner_jobs.query.graphql.json';
+// Other mock data
+export const onlineContactTimeoutSecs = 2 * 60 * 60;
+export const staleTimeoutSecs = 5259492; // Ruby's `2.months`
+
export {
runnersData,
runnersCountData,
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index aff1ec882bb..7834e76fe48 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -181,6 +181,28 @@ describe('search_params.js', () => {
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', () => {
@@ -197,14 +219,18 @@ describe('search_params.js', () => {
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',
- );
+ it.each`
+ query | updatedQuery
+ ${'status[]=NOT_CONNECTED'} | ${'status[]=NEVER_CONTACTED'}
+ ${'status[]=NOT_CONNECTED&a=b'} | ${'status[]=NEVER_CONTACTED&a=b'}
+ ${'status[]=ACTIVE'} | ${'paused[]=false'}
+ ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
+ ${'status[]=ACTIVE'} | ${'paused[]=false'}
+ ${'status[]=PAUSED'} | ${'paused[]=true'}
+ `('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => {
+ const mockUrl = 'http://test.host/admin/runners?';
- expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED&a=b')).toBe(
- 'http://test.host/admin/runners?status[]=NEVER_CONTACTED&a=b',
- );
+ expect(updateOutdatedUrl(`${mockUrl}${query}`)).toBe(`${mockUrl}${updatedQuery}`);
});
});
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/runner/utils_spec.js
index 3fa9784ecdf..1db9815dfd8 100644
--- a/spec/frontend/runner/utils_spec.js
+++ b/spec/frontend/runner/utils_spec.js
@@ -44,6 +44,10 @@ describe('~/runner/utils', () => {
thClass: expect.arrayContaining(mockClasses),
});
});
+
+ it('a field with custom options', () => {
+ expect(tableField({ foo: 'bar' })).toMatchObject({ foo: 'bar' });
+ });
});
describe('getPaginationVariables', () => {