summaryrefslogtreecommitdiff
path: root/spec/frontend/runner/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/runner/components')
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js27
-rw-r--r--spec/frontend/runner/components/cells/runner_status_cell_spec.js69
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js39
-rw-r--r--spec/frontend/runner/components/cells/runner_type_cell_spec.js48
-rw-r--r--spec/frontend/runner/components/helpers/masked_value_spec.js51
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js169
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js (renamed from spec/frontend/runner/components/runner_registration_token_reset_spec.js)45
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js109
-rw-r--r--spec/frontend/runner/components/runner_contacted_state_badge_spec.js86
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js60
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js59
-rw-r--r--spec/frontend/runner/components/runner_manual_setup_help_spec.js122
-rw-r--r--spec/frontend/runner/components/runner_paused_badge_spec.js (renamed from spec/frontend/runner/components/runner_state_paused_badge_spec.js)2
-rw-r--r--spec/frontend/runner/components/runner_state_locked_badge_spec.js45
-rw-r--r--spec/frontend/runner/components/runner_tag_spec.js46
-rw-r--r--spec/frontend/runner/components/runner_tags_spec.js10
-rw-r--r--spec/frontend/runner/components/runner_type_alert_spec.js14
-rw-r--r--spec/frontend/runner/components/runner_type_badge_spec.js14
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js109
19 files changed, 707 insertions, 417 deletions
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 5aa3879ac3e..2874bdbe280 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -8,12 +8,11 @@ import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue'
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
-import { runnersData, runnerData } from '../../mock_data';
+import { runnersData } from '../../mock_data';
const mockRunner = runnersData.data.runners.nodes[0];
-const mockRunnerDetails = runnerData.data.runner;
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
@@ -27,7 +26,7 @@ jest.mock('~/runner/sentry_utils');
describe('RunnerTypeCell', () => {
let wrapper;
const runnerDeleteMutationHandler = jest.fn();
- const runnerUpdateMutationHandler = jest.fn();
+ const runnerActionsUpdateMutationHandler = jest.fn();
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
@@ -46,7 +45,7 @@ describe('RunnerTypeCell', () => {
localVue,
apolloProvider: createMockApollo([
[runnerDeleteMutation, runnerDeleteMutationHandler],
- [runnerUpdateMutation, runnerUpdateMutationHandler],
+ [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
]),
...options,
}),
@@ -62,10 +61,10 @@ describe('RunnerTypeCell', () => {
},
});
- runnerUpdateMutationHandler.mockResolvedValue({
+ runnerActionsUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: mockRunnerDetails,
+ runner: mockRunner,
errors: [],
},
},
@@ -74,7 +73,7 @@ describe('RunnerTypeCell', () => {
afterEach(() => {
runnerDeleteMutationHandler.mockReset();
- runnerUpdateMutationHandler.mockReset();
+ runnerActionsUpdateMutationHandler.mockReset();
wrapper.destroy();
});
@@ -116,12 +115,12 @@ describe('RunnerTypeCell', () => {
describe(`When clicking on the ${icon} button`, () => {
it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
- expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(0);
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
await findToggleActiveBtn().vm.$emit('click');
- expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(1);
- expect(runnerUpdateMutationHandler).toHaveBeenCalledWith({
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
input: {
id: mockRunner.id,
active: newActiveValue,
@@ -145,7 +144,7 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg = 'Update error!';
beforeEach(async () => {
- runnerUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+ runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
await findToggleActiveBtn().vm.$emit('click');
});
@@ -167,10 +166,10 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg2 = 'User not allowed!';
beforeEach(async () => {
- runnerUpdateMutationHandler.mockResolvedValue({
+ runnerActionsUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: runnerData.data.runner,
+ runner: mockRunner,
errors: [mockErrorMsg, mockErrorMsg2],
},
},
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
new file mode 100644
index 00000000000..20a1cdf7236
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
@@ -0,0 +1,69 @@
+import { GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
+import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+
+ const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = mount(RunnerStatusCell, {
+ propsData: {
+ runner: {
+ runnerType: INSTANCE_TYPE,
+ active: true,
+ status: STATUS_ONLINE,
+ ...runner,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays online status', () => {
+ createComponent();
+
+ expect(wrapper.text()).toMatchInterpolatedText('online');
+ expect(findBadgeAt(0).text()).toBe('online');
+ });
+
+ it('Displays offline status', () => {
+ createComponent({
+ runner: {
+ status: STATUS_OFFLINE,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText('offline');
+ expect(findBadgeAt(0).text()).toBe('offline');
+ });
+
+ it('Displays paused status', () => {
+ createComponent({
+ runner: {
+ active: false,
+ status: STATUS_ONLINE,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText('online paused');
+
+ expect(findBadgeAt(0).text()).toBe('online');
+ expect(findBadgeAt(1).text()).toBe('paused');
+ });
+
+ it('Is empty when data is missing', () => {
+ createComponent({
+ runner: {
+ status: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+});
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 1c9282e0acd..b6d957d27ea 100644
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -1,5 +1,6 @@
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
+import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
@@ -8,13 +9,17 @@ const mockDescription = 'runner-1';
describe('RunnerTypeCell', () => {
let wrapper;
- const createComponent = (options) => {
- wrapper = mount(RunnerSummaryCell, {
+ const findLockIcon = () => wrapper.findByTestId('lock-icon');
+
+ const createComponent = (runner, options) => {
+ wrapper = mountExtended(RunnerSummaryCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
shortSha: mockShortSha,
description: mockDescription,
+ runnerType: INSTANCE_TYPE,
+ ...runner,
},
},
...options,
@@ -33,6 +38,23 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
});
+ it('Displays the runner type', () => {
+ expect(wrapper.text()).toContain('shared');
+ });
+
+ it('Does not display the locked icon', () => {
+ expect(findLockIcon().exists()).toBe(false);
+ });
+
+ it('Displays the locked icon for locked runners', () => {
+ createComponent({
+ runnerType: PROJECT_TYPE,
+ locked: true,
+ });
+
+ expect(findLockIcon().exists()).toBe(true);
+ });
+
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockDescription);
});
@@ -40,11 +62,14 @@ describe('RunnerTypeCell', () => {
it('Displays a custom slot', () => {
const slotContent = 'My custom runner summary';
- createComponent({
- slots: {
- 'runner-name': slotContent,
+ createComponent(
+ {},
+ {
+ slots: {
+ 'runner-name': slotContent,
+ },
},
- });
+ );
expect(wrapper.text()).toContain(slotContent);
});
diff --git a/spec/frontend/runner/components/cells/runner_type_cell_spec.js b/spec/frontend/runner/components/cells/runner_type_cell_spec.js
deleted file mode 100644
index 48958a282fc..00000000000
--- a/spec/frontend/runner/components/cells/runner_type_cell_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import RunnerTypeCell from '~/runner/components/cells/runner_type_cell.vue';
-import { INSTANCE_TYPE } from '~/runner/constants';
-
-describe('RunnerTypeCell', () => {
- let wrapper;
-
- const findBadges = () => wrapper.findAllComponents(GlBadge);
-
- const createComponent = ({ runner = {} } = {}) => {
- wrapper = mount(RunnerTypeCell, {
- propsData: {
- runner: {
- runnerType: INSTANCE_TYPE,
- active: true,
- locked: false,
- ...runner,
- },
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays the runner type', () => {
- createComponent();
-
- expect(findBadges()).toHaveLength(1);
- expect(findBadges().at(0).text()).toBe('shared');
- });
-
- it('Displays locked and paused states', () => {
- createComponent({
- runner: {
- active: false,
- locked: true,
- },
- });
-
- expect(findBadges()).toHaveLength(3);
- expect(findBadges().at(0).text()).toBe('shared');
- expect(findBadges().at(1).text()).toBe('locked');
- expect(findBadges().at(2).text()).toBe('paused');
- });
-});
diff --git a/spec/frontend/runner/components/helpers/masked_value_spec.js b/spec/frontend/runner/components/helpers/masked_value_spec.js
deleted file mode 100644
index f87315057ec..00000000000
--- a/spec/frontend/runner/components/helpers/masked_value_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-
-const mockSecret = '01234567890';
-const mockMasked = '***********';
-
-describe('MaskedValue', () => {
- let wrapper;
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(MaskedValue, {
- propsData: {
- value: mockSecret,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays masked value by default', () => {
- expect(wrapper.text()).toBe(mockMasked);
- });
-
- describe('When the icon is clicked', () => {
- beforeEach(() => {
- findButton().vm.$emit('click');
- });
-
- it('Displays the actual value', () => {
- expect(wrapper.text()).toBe(mockSecret);
- expect(wrapper.text()).not.toBe(mockMasked);
- });
-
- it('When user clicks again, displays masked value', async () => {
- await findButton().vm.$emit('click');
-
- expect(wrapper.text()).toBe(mockMasked);
- expect(wrapper.text()).not.toBe(mockSecret);
- });
- });
-});
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
new file mode 100644
index 00000000000..d18d2bec18e
--- /dev/null
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -0,0 +1,169 @@
+import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount, createWrapper } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
+import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
+
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+
+import {
+ mockGraphqlRunnerPlatforms,
+ mockGraphqlInstructions,
+} from 'jest/vue_shared/components/runner_instructions/mock_data';
+
+const mockToken = '0123456789';
+const maskToken = '**********';
+
+describe('RegistrationDropdown', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
+ const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
+ const findTokenResetDropdownItem = () =>
+ wrapper.findComponent(RegistrationTokenResetDropdownItem);
+
+ const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
+
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
+ wrapper = extendedWrapper(
+ mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ }),
+ );
+ };
+
+ it.each`
+ type | text
+ ${INSTANCE_TYPE} | ${'Register an instance runner'}
+ ${GROUP_TYPE} | ${'Register a group runner'}
+ ${PROJECT_TYPE} | ${'Register a project runner'}
+ `('Dropdown text for type $type is "$text"', () => {
+ createComponent({ props: { type: INSTANCE_TYPE } }, mount);
+
+ expect(wrapper.text()).toContain('Register an instance runner');
+ });
+
+ it('Passes attributes to the dropdown component', () => {
+ createComponent({ attrs: { right: true } });
+
+ expect(findDropdown().attributes()).toMatchObject({ right: 'true' });
+ });
+
+ describe('Instructions dropdown item', () => {
+ it('Displays "Show runner" dropdown item', () => {
+ createComponent();
+
+ expect(findRegistrationInstructionsDropdownItem().text()).toBe(
+ 'Show runner installation and registration instructions',
+ );
+ });
+
+ describe('When the dropdown item is clicked', () => {
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ const findModalInBody = () =>
+ createWrapper(document.body).find('[data-testid="runner-instructions-modal"]');
+
+ beforeEach(() => {
+ createComponent(
+ {
+ localVue,
+ // Mock load modal contents from API
+ apolloProvider: createMockApollo(requestHandlers),
+ // Use `attachTo` to find the modal
+ attachTo: document.body,
+ },
+ mount,
+ );
+
+ findRegistrationInstructionsDropdownItem().trigger('click');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('opens the modal with contents', () => {
+ const modalText = findModalInBody()
+ .text()
+ .replace(/[\n\t\s]+/g, ' ');
+
+ expect(modalText).toContain('Install a runner');
+
+ // Environment selector
+ expect(modalText).toContain('Environment');
+ expect(modalText).toContain('Linux macOS Windows Docker Kubernetes');
+
+ // Architecture selector
+ expect(modalText).toContain('Architecture');
+ expect(modalText).toContain('amd64 amd64 386 arm arm64');
+
+ expect(modalText).toContain('Download and install binary');
+ });
+ });
+ });
+
+ describe('Registration token', () => {
+ it('Displays dropdown form for the registration token', () => {
+ createComponent();
+
+ expect(findTokenDropdownItem().exists()).toBe(true);
+ });
+
+ it('Displays masked value by default', () => {
+ createComponent({}, mount);
+
+ expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
+ `Registration token ${maskToken}`,
+ );
+ });
+ });
+
+ describe('Reset token item', () => {
+ it('Displays registration token reset item', () => {
+ createComponent();
+
+ expect(findTokenResetDropdownItem().exists()).toBe(true);
+ });
+
+ it.each([INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE])('Set up token reset for %s', (type) => {
+ createComponent({ props: { type } });
+
+ expect(findTokenResetDropdownItem().props('type')).toBe(type);
+ });
+ });
+
+ it('Updates the token when it gets reset', async () => {
+ createComponent({}, mount);
+
+ const newToken = 'mock1';
+
+ findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
+ findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() });
+ await nextTick();
+
+ expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
+ `Registration token ${newToken}`,
+ );
+ });
+});
diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 8b360b88417..0d002c272b4 100644
--- a/spec/frontend/runner/components/runner_registration_token_reset_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,11 +1,11 @@
-import { GlButton } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash, { FLASH_TYPES } from '~/flash';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
+import createFlash from '~/flash';
+import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
@@ -15,17 +15,20 @@ jest.mock('~/runner/sentry_utils');
const localVue = createLocalVue();
localVue.use(VueApollo);
+localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
-describe('RunnerRegistrationTokenReset', () => {
+describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
+ let showToast;
- const findButton = () => wrapper.findComponent(GlButton);
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = ({ props, provide = {} } = {}) => {
- wrapper = shallowMount(RunnerRegistrationTokenReset, {
+ wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
localVue,
provide,
propsData: {
@@ -36,6 +39,8 @@ describe('RunnerRegistrationTokenReset', () => {
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
});
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
beforeEach(() => {
@@ -58,7 +63,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('Displays reset button', () => {
- expect(findButton().exists()).toBe(true);
+ expect(findDropdownItem().exists()).toBe(true);
});
describe('On click and confirmation', () => {
@@ -78,7 +83,8 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+
+ findDropdownItem().trigger('click');
await waitForPromises();
});
@@ -95,14 +101,13 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('shows confirmation', () => {
- expect(createFlash).toHaveBeenLastCalledWith({
- message: expect.stringContaining('registration token generated'),
- type: FLASH_TYPES.SUCCESS,
- });
+ expect(showToast).toHaveBeenLastCalledWith(
+ expect.stringContaining('registration token generated'),
+ );
});
});
});
@@ -110,7 +115,7 @@ describe('RunnerRegistrationTokenReset', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
window.confirm.mockReturnValueOnce(false);
- findButton().vm.$emit('click');
+ findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -123,11 +128,11 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('does not shows confirmation', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(showToast).not.toHaveBeenCalled();
});
});
@@ -138,7 +143,7 @@ describe('RunnerRegistrationTokenReset', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -164,7 +169,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -180,10 +185,10 @@ describe('RunnerRegistrationTokenReset', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
window.confirm.mockReturnValue(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await nextTick();
- expect(findButton().props('loading')).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
new file mode 100644
index 00000000000..f53ae165344
--- /dev/null
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -0,0 +1,109 @@
+import { nextTick } from 'vue';
+import { GlToast } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { extendedWrapper } 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';
+
+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 vueWithGlToast = () => {
+ const localVue = createLocalVue();
+ localVue.use(GlToast);
+ return localVue;
+ };
+
+ const createComponent = ({ props = {}, withGlToast = true } = {}) => {
+ const localVue = withGlToast ? vueWithGlToast() : undefined;
+
+ wrapper = extendedWrapper(
+ shallowMount(RegistrationToken, {
+ propsData: {
+ value: mockToken,
+ ...props,
+ },
+ localVue,
+ }),
+ );
+
+ 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 button to reveal token', () => {
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ });
+
+ it('Can copy the original token value', () => {
+ expect(findCopyButton().props('text')).toBe(mockToken);
+ });
+
+ describe('When the reveal icon is clicked', () => {
+ beforeEach(() => {
+ findToggleMaskButton().vm.$emit('click', { stopPropagation });
+ });
+
+ it('Click event is not propagated', async () => {
+ expect(stopPropagation).toHaveBeenCalledTimes(1);
+ });
+
+ 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');
+
+ expect(showToast).toHaveBeenCalledTimes(1);
+ expect(showToast).toHaveBeenCalledWith('Registration token copied!');
+ });
+
+ it('does not fail when toast is not defined', () => {
+ createComponent({ withGlToast: false });
+ findCopyButton().vm.$emit('success');
+
+ // This block also tests for unhandled errors
+ expect(showToast).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_contacted_state_badge_spec.js b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js
new file mode 100644
index 00000000000..57a27f39826
--- /dev/null
+++ b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js
@@ -0,0 +1,86 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerContactedStateBadge from '~/runner/components/runner_contacted_state_badge.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_NOT_CONNECTED } from '~/runner/constants';
+
+describe('RunnerTypeBadge', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = shallowMount(RunnerContactedStateBadge, {
+ propsData: {
+ runner: {
+ contactedAt: '2021-01-01T00:00:00Z',
+ status: STATUS_ONLINE,
+ ...runner,
+ },
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.useFakeTimers('modern');
+ });
+
+ afterEach(() => {
+ jest.useFakeTimers('legacy');
+
+ wrapper.destroy();
+ });
+
+ it('renders online state', () => {
+ jest.setSystemTime(new Date('2021-01-01T00:01:00Z'));
+
+ createComponent();
+
+ expect(wrapper.text()).toBe('online');
+ expect(findBadge().props('variant')).toBe('success');
+ expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
+ });
+
+ it('renders offline state', () => {
+ jest.setSystemTime(new Date('2021-01-02T00:00:00Z'));
+
+ createComponent({
+ runner: {
+ status: STATUS_OFFLINE,
+ },
+ });
+
+ 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',
+ );
+ });
+
+ it('renders not connected state', () => {
+ createComponent({
+ runner: {
+ contactedAt: null,
+ status: STATUS_NOT_CONNECTED,
+ },
+ });
+
+ expect(wrapper.text()).toBe('not connected');
+ expect(findBadge().props('variant')).toBe('muted');
+ expect(getTooltip().value).toMatch('This runner has never connected');
+ });
+
+ it('does not fail when data is missing', () => {
+ createComponent({
+ runner: {
+ status: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+});
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 46948af1f28..9ea0955f2a1 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -5,13 +5,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 { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config';
-import {
- PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
- PARAM_KEY_TAG,
- STATUS_ACTIVE,
-} from '~/runner/constants';
+import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, 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';
@@ -31,6 +25,11 @@ describe('RunnerList', () => {
];
const mockActiveRunnersCount = 2;
+ const expectToHaveLastEmittedInput = (value) => {
+ const inputs = wrapper.emitted('input');
+ expect(inputs[inputs.length - 1][0]).toEqual(value);
+ };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(RunnerFilteredSearchBar, {
@@ -38,6 +37,7 @@ describe('RunnerList', () => {
namespace: 'runners',
tokens: [],
value: {
+ runnerType: null,
filters: [],
sort: mockDefaultSort,
},
@@ -86,7 +86,7 @@ describe('RunnerList', () => {
it('sets tokens to the filtered search', () => {
createComponent({
props: {
- tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig],
+ tokens: [statusTokenConfig, tagTokenConfig],
},
});
@@ -97,11 +97,6 @@ describe('RunnerList', () => {
options: expect.any(Array),
}),
expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- options: expect.any(Array),
- }),
- expect.objectContaining({
type: PARAM_KEY_TAG,
token: TagToken,
}),
@@ -123,6 +118,7 @@ describe('RunnerList', () => {
createComponent({
props: {
value: {
+ runnerType: INSTANCE_TYPE,
sort: mockOtherSort,
filters: mockFilters,
},
@@ -142,30 +138,40 @@ describe('RunnerList', () => {
.text(),
).toEqual('Last contact');
});
+
+ it('when the user sets a filter, the "search" preserves the other filters', () => {
+ findGlFilteredSearch().vm.$emit('input', mockFilters);
+ findGlFilteredSearch().vm.$emit('submit');
+
+ expectToHaveLastEmittedInput({
+ runnerType: INSTANCE_TYPE,
+ filters: mockFilters,
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ });
+ });
});
it('when the user sets a filter, the "search" is emitted with filters', () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- filters: mockFilters,
- sort: mockDefaultSort,
- pagination: { page: 1 },
- },
- ]);
+ expectToHaveLastEmittedInput({
+ runnerType: null,
+ filters: mockFilters,
+ sort: mockDefaultSort,
+ pagination: { page: 1 },
+ });
});
it('when the user sets a sorting method, the "search" is emitted with the sort', () => {
findSortOptions().at(1).vm.$emit('click');
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- filters: [],
- sort: mockOtherSort,
- pagination: { page: 1 },
- },
- ]);
+ expectToHaveLastEmittedInput({
+ runnerType: null,
+ filters: [],
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ });
});
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index e24dffea1eb..986e55a2132 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,6 +1,5 @@
import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { cloneDeep } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
@@ -43,12 +42,10 @@ describe('RunnerList', () => {
const headerLabels = findHeaders().wrappers.map((w) => w.text());
expect(headerLabels).toEqual([
- 'Type/State',
- 'Runner',
+ 'Status',
+ 'Runner ID',
'Version',
'IP Address',
- 'Projects',
- 'Jobs',
'Tags',
'Last contact',
'', // actions has no label
@@ -65,7 +62,7 @@ describe('RunnerList', () => {
const { id, description, version, ipAddress, shortSha } = mockRunners[0];
// Badges
- expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused');
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
@@ -76,8 +73,6 @@ describe('RunnerList', () => {
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1');
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -88,54 +83,6 @@ describe('RunnerList', () => {
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
- describe('Table data formatting', () => {
- let mockRunnersCopy;
-
- beforeEach(() => {
- mockRunnersCopy = cloneDeep(mockRunners);
- });
-
- it('Formats null project counts', () => {
- mockRunnersCopy[0].projectCount = null;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('n/a');
- });
-
- it('Formats 0 project counts', () => {
- mockRunnersCopy[0].projectCount = 0;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('0');
- });
-
- it('Formats big project counts', () => {
- mockRunnersCopy[0].projectCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1,000');
- });
-
- it('Formats job counts', () => {
- mockRunnersCopy[0].jobCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
- });
-
- it('Formats big job counts with a plus symbol', () => {
- mockRunnersCopy[0].jobCount = 1001;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
- });
- });
-
it('Shows runner identifier', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
deleted file mode 100644
index effef0e7ebf..00000000000
--- a/spec/frontend/runner/components/runner_manual_setup_help_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-
-const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockRunnerInstallHelpPage = 'https://docs.gitlab.com/runner/install/';
-
-describe('RunnerManualSetupHelp', () => {
- let wrapper;
- let originalGon;
-
- const findRunnerInstructions = () => wrapper.findComponent(RunnerInstructions);
- const findRunnerRegistrationTokenReset = () =>
- wrapper.findComponent(RunnerRegistrationTokenReset);
- const findClipboardButtons = () => wrapper.findAllComponents(ClipboardButton);
- const findRunnerHelpTitle = () => wrapper.findByTestId('runner-help-title');
- const findCoordinatorUrl = () => wrapper.findByTestId('coordinator-url');
- const findRegistrationToken = () => wrapper.findByTestId('registration-token');
- const findRunnerHelpLink = () => wrapper.findByTestId('runner-help-link');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = extendedWrapper(
- shallowMount(RunnerManualSetupHelp, {
- provide: {
- runnerInstallHelpPage: mockRunnerInstallHelpPage,
- },
- propsData: {
- registrationToken: mockRegistrationToken,
- type: INSTANCE_TYPE,
- ...props,
- },
- stubs: {
- MaskedValue,
- GlSprintf,
- },
- }),
- );
- };
-
- beforeAll(() => {
- originalGon = global.gon;
- global.gon = { gitlab_url: TEST_HOST };
- });
-
- afterAll(() => {
- global.gon = originalGon;
- });
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Title contains the shared runner type', () => {
- createComponent({ props: { type: INSTANCE_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a shared runner manually');
- });
-
- it('Title contains the group runner type', () => {
- createComponent({ props: { type: GROUP_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a group runner manually');
- });
-
- it('Title contains the specific runner type', () => {
- createComponent({ props: { type: PROJECT_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText(
- 'Set up a specific runner manually',
- );
- });
-
- it('Runner Install Page link', () => {
- expect(findRunnerHelpLink().attributes('href')).toBe(mockRunnerInstallHelpPage);
- });
-
- it('Displays the coordinator URL token', () => {
- expect(findCoordinatorUrl().text()).toBe(TEST_HOST);
- expect(findClipboardButtons().at(0).props('text')).toBe(TEST_HOST);
- });
-
- it('Displays the runner instructions', () => {
- expect(findRunnerInstructions().exists()).toBe(true);
- });
-
- it('Displays the registration token', async () => {
- findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
-
- await nextTick();
-
- expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
- expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
- });
-
- it('Displays the runner registration token reset button', () => {
- expect(findRunnerRegistrationTokenReset().exists()).toBe(true);
- });
-
- it('Replaces the runner reset button', async () => {
- const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN';
-
- findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
- findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken);
-
- await nextTick();
-
- expect(findRegistrationToken().text()).toBe(mockNewRegistrationToken);
- expect(findClipboardButtons().at(1).props('text')).toBe(mockNewRegistrationToken);
- });
-});
diff --git a/spec/frontend/runner/components/runner_state_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js
index 8df56d6e3f3..18cfcfae864 100644
--- a/spec/frontend/runner/components/runner_state_paused_badge_spec.js
+++ b/spec/frontend/runner/components/runner_paused_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatePausedBadge from '~/runner/components/runner_state_paused_badge.vue';
+import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerTypeBadge', () => {
diff --git a/spec/frontend/runner/components/runner_state_locked_badge_spec.js b/spec/frontend/runner/components/runner_state_locked_badge_spec.js
deleted file mode 100644
index e92b671f5a1..00000000000
--- a/spec/frontend/runner/components/runner_state_locked_badge_spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerStateLockedBadge from '~/runner/components/runner_state_locked_badge.vue';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
-describe('RunnerTypeBadge', () => {
- let wrapper;
-
- const findBadge = () => wrapper.findComponent(GlBadge);
- const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(RunnerStateLockedBadge, {
- propsData: {
- ...props,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders locked state', () => {
- expect(wrapper.text()).toBe('locked');
- expect(findBadge().props('variant')).toBe('warning');
- });
-
- it('renders tooltip', () => {
- expect(getTooltip().value).toBeDefined();
- });
-
- it('passes arbitrary attributes to the badge', () => {
- createComponent({ props: { size: 'sm' } });
-
- expect(findBadge().props('size')).toBe('sm');
- });
-});
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js
index dda318f8153..bd05d4b2cfe 100644
--- a/spec/frontend/runner/components/runner_tag_spec.js
+++ b/spec/frontend/runner/components/runner_tag_spec.js
@@ -1,18 +1,35 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+const mockTag = 'tag1';
describe('RunnerTag', () => {
let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltipValue = () => getBinding(findBadge().element, 'gl-tooltip').value;
+
+ const setDimensions = ({ scrollWidth, offsetWidth }) => {
+ jest.spyOn(findBadge().element, 'scrollWidth', 'get').mockReturnValue(scrollWidth);
+ jest.spyOn(findBadge().element, 'offsetWidth', 'get').mockReturnValue(offsetWidth);
+
+ // Mock trigger resize
+ getBinding(findBadge().element, 'gl-resize-observer').value();
+ };
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTag, {
propsData: {
- tag: 'tag1',
+ tag: mockTag,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ GlResizeObserver: createMockDirective(),
+ },
});
};
@@ -25,21 +42,36 @@ describe('RunnerTag', () => {
});
it('Displays tag text', () => {
- expect(wrapper.text()).toBe('tag1');
+ expect(wrapper.text()).toBe(mockTag);
});
it('Displays tags with correct style', () => {
expect(findBadge().props()).toMatchObject({
- size: 'md',
- variant: 'info',
+ size: 'sm',
+ variant: 'neutral',
});
});
- it('Displays tags with small size', () => {
+ it('Displays tags with md size', () => {
createComponent({
- props: { size: 'sm' },
+ props: { size: 'md' },
});
- expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('size')).toBe('md');
});
+
+ it.each`
+ case | scrollWidth | offsetWidth | expectedTooltip
+ ${'overflowing'} | ${110} | ${100} | ${mockTag}
+ ${'not overflowing'} | ${90} | ${100} | ${''}
+ ${'almost overflowing'} | ${100} | ${100} | ${''}
+ `(
+ 'Sets "$expectedTooltip" as tooltip when $case',
+ async ({ scrollWidth, offsetWidth, expectedTooltip }) => {
+ setDimensions({ scrollWidth, offsetWidth });
+ await nextTick();
+
+ expect(getTooltipValue()).toBe(expectedTooltip);
+ },
+ );
});
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js
index b6487ade0d6..da89a659432 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/runner/components/runner_tags_spec.js
@@ -33,16 +33,16 @@ describe('RunnerTags', () => {
});
it('Displays tags with correct style', () => {
- expect(findBadge().props('size')).toBe('md');
- expect(findBadge().props('variant')).toBe('info');
+ expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('variant')).toBe('neutral');
});
- it('Displays tags with small size', () => {
+ it('Displays tags with md size', () => {
createComponent({
- props: { size: 'sm' },
+ props: { size: 'md' },
});
- expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('size')).toBe('md');
});
it('Is empty when there are no tags', () => {
diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js
index e54e499743b..4023c75c9a8 100644
--- a/spec/frontend/runner/components/runner_type_alert_spec.js
+++ b/spec/frontend/runner/components/runner_type_alert_spec.js
@@ -23,11 +23,11 @@ describe('RunnerTypeAlert', () => {
});
describe.each`
- type | exampleText | anchor | variant
- ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} | ${'success'}
- ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} | ${'success'}
- ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} | ${'info'}
- `('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => {
+ type | exampleText | anchor
+ ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
+ ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
+ ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
+ `('When it is an $type level runner', ({ type, exampleText, anchor }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
@@ -36,8 +36,8 @@ describe('RunnerTypeAlert', () => {
expect(wrapper.text()).toMatch(exampleText);
});
- it(`Shows a ${variant} variant`, () => {
- expect(findAlert().props('variant')).toBe(variant);
+ it(`Shows an "info" variant`, () => {
+ expect(findAlert().props('variant')).toBe('info');
});
it(`Links to anchor "${anchor}"`, () => {
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js
index fb344e65389..7bb0a2e6e2f 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/runner/components/runner_type_badge_spec.js
@@ -26,18 +26,18 @@ describe('RunnerTypeBadge', () => {
});
describe.each`
- type | text | variant
- ${INSTANCE_TYPE} | ${'shared'} | ${'success'}
- ${GROUP_TYPE} | ${'group'} | ${'success'}
- ${PROJECT_TYPE} | ${'specific'} | ${'info'}
- `('displays $type runner', ({ type, text, variant }) => {
+ type | text
+ ${INSTANCE_TYPE} | ${'shared'}
+ ${GROUP_TYPE} | ${'group'}
+ ${PROJECT_TYPE} | ${'specific'}
+ `('displays $type runner', ({ type, text }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
- it(`as "${text}" with a ${variant} variant`, () => {
+ it(`as "${text}" with an "info" variant`, () => {
expect(findBadge().text()).toBe(text);
- expect(findBadge().props('variant')).toBe(variant);
+ expect(findBadge().props('variant')).toBe('info');
});
it('with a tooltip', () => {
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
new file mode 100644
index 00000000000..4871d9c470a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -0,0 +1,109 @@
+import { GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+
+const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
+
+describe('RunnerTypeTabs', () => {
+ let wrapper;
+
+ const findTabs = () => wrapper.findAll(GlTab);
+ const findActiveTab = () =>
+ findTabs()
+ .filter((tab) => tab.attributes('active') === 'true')
+ .at(0);
+
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = shallowMount(RunnerTypeTabs, {
+ propsData: {
+ value: mockSearch,
+ ...props,
+ },
+ stubs: {
+ GlTab,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Renders options to filter runners', () => {
+ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
+ 'All',
+ 'Instance',
+ 'Group',
+ 'Project',
+ ]);
+ });
+
+ it('"All" is selected by default', () => {
+ expect(findActiveTab().text()).toBe('All');
+ });
+
+ it('Another tab can be preselected by the user', () => {
+ createComponent({
+ props: {
+ value: {
+ ...mockSearch,
+ runnerType: INSTANCE_TYPE,
+ },
+ },
+ });
+
+ expect(findActiveTab().text()).toBe('Instance');
+ });
+
+ describe('When the user selects a tab', () => {
+ const emittedValue = () => wrapper.emitted('input')[0][0];
+
+ beforeEach(() => {
+ findTabs().at(2).vm.$emit('click');
+ });
+
+ it(`Runner type is emitted`, () => {
+ expect(emittedValue()).toEqual({
+ ...mockSearch,
+ runnerType: GROUP_TYPE,
+ });
+ });
+
+ it('Runner type is selected', async () => {
+ const newValue = emittedValue();
+ await wrapper.setProps({ value: newValue });
+
+ expect(findActiveTab().text()).toBe('Group');
+ });
+ });
+
+ describe('When using a custom slot', () => {
+ const mockContent = 'content';
+
+ beforeEach(() => {
+ createComponent({
+ scopedSlots: {
+ title: `
+ <span>
+ {{props.tab.title}} ${mockContent}
+ </span>`,
+ },
+ });
+ });
+
+ it('Renders tabs with additional information', () => {
+ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
+ `All ${mockContent}`,
+ `Instance ${mockContent}`,
+ `Group ${mockContent}`,
+ `Project ${mockContent}`,
+ ]);
+ });
+ });
+});