Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/runner/components')
-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
15 files changed, 345 insertions, 88 deletions
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
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();
+ });
+});