diff options
Diffstat (limited to 'spec/frontend/runner/components')
15 files changed, 408 insertions, 143 deletions
diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/runner/components/cells/link_cell_spec.js index a59a0eaa5d8..46ab1adb6b6 100644 --- a/spec/frontend/runner/components/cells/link_cell_spec.js +++ b/spec/frontend/runner/components/cells/link_cell_spec.js @@ -5,7 +5,7 @@ import LinkCell from '~/runner/components/cells/link_cell.vue'; describe('LinkCell', () => { let wrapper; - const findGlLink = () => wrapper.find(GlLink); + const findGlLink = () => wrapper.findComponent(GlLink); const findSpan = () => wrapper.find('span'); const createComponent = ({ props = {}, ...options } = {}) => { 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 ffd6f126627..58974d4f85f 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -122,7 +122,7 @@ describe('RunnerActionsCell', () => { expect(wrapper.emitted('deleted')).toEqual([[value]]); }); - it('Renders the runner delete disabled button when user cannot delete', () => { + it('Does not render the runner delete button when user cannot delete', () => { createComponent({ runner: { userPermissions: { @@ -132,7 +132,7 @@ describe('RunnerActionsCell', () => { }, }); - expect(findDeleteBtn().props('disabled')).toBe(true); + expect(findDeleteBtn().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/runner/components/cells/runner_owner_cell_spec.js new file mode 100644 index 00000000000..e9965d8855d --- /dev/null +++ b/spec/frontend/runner/components/cells/runner_owner_cell_spec.js @@ -0,0 +1,111 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import RunnerOwnerCell from '~/runner/components/cells/runner_owner_cell.vue'; + +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; + +describe('RunnerOwnerCell', () => { + let wrapper; + + const findLink = () => wrapper.findComponent(GlLink); + const getLinkTooltip = () => getBinding(findLink().element, 'gl-tooltip').value; + + const createComponent = ({ runner } = {}) => { + wrapper = shallowMount(RunnerOwnerCell, { + directives: { + GlTooltip: createMockDirective(), + }, + propsData: { + runner, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('When its an instance runner', () => { + beforeEach(() => { + createComponent({ + runner: { + runnerType: INSTANCE_TYPE, + }, + }); + }); + + it('shows an administrator label', () => { + expect(findLink().exists()).toBe(false); + expect(wrapper.text()).toBe(s__('Runners|Administrator')); + }); + }); + + describe('When its a group runner', () => { + const mockName = 'Group 2'; + const mockFullName = 'Group 1 / Group 2'; + const mockWebUrl = '/group-1/group-2'; + + beforeEach(() => { + createComponent({ + runner: { + runnerType: GROUP_TYPE, + groups: { + nodes: [ + { + name: mockName, + fullName: mockFullName, + webUrl: mockWebUrl, + }, + ], + }, + }, + }); + }); + + it('Displays a group link', () => { + expect(findLink().attributes('href')).toBe(mockWebUrl); + expect(wrapper.text()).toBe(mockName); + expect(getLinkTooltip()).toBe(mockFullName); + }); + }); + + describe('When its a project runner', () => { + const mockName = 'Project 1'; + const mockNameWithNamespace = 'Group 1 / Project 1'; + const mockWebUrl = '/group-1/project-1'; + + beforeEach(() => { + createComponent({ + runner: { + runnerType: PROJECT_TYPE, + ownerProject: { + name: mockName, + nameWithNamespace: mockNameWithNamespace, + webUrl: mockWebUrl, + }, + }, + }); + }); + + it('Displays a project link', () => { + expect(findLink().attributes('href')).toBe(mockWebUrl); + expect(wrapper.text()).toBe(mockName); + expect(getLinkTooltip()).toBe(mockNameWithNamespace); + }); + }); + + describe('When its an empty runner', () => { + beforeEach(() => { + createComponent({ + runner: {}, + }); + }); + + it('shows no label', () => { + expect(wrapper.text()).toBe(''); + }); + }); +}); diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js index 21ec9f61f37..e7cadefc140 100644 --- a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js @@ -85,7 +85,7 @@ describe('RunnerTypeCell', () => { contactedAt: '2022-01-02', }); - expect(findRunnerSummaryField('clock').find(TimeAgo).props('time')).toBe('2022-01-02'); + expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02'); }); it('Displays empty last contact', () => { @@ -93,7 +93,7 @@ describe('RunnerTypeCell', () => { contactedAt: null, }); - expect(findRunnerSummaryField('clock').find(TimeAgo).exists()).toBe(false); + expect(findRunnerSummaryField('clock').findComponent(TimeAgo).exists()).toBe(false); expect(findRunnerSummaryField('clock').text()).toContain(__('Never')); }); @@ -134,7 +134,7 @@ describe('RunnerTypeCell', () => { }); it('Displays created at', () => { - expect(findRunnerSummaryField('calendar').find(TimeAgo).props('time')).toBe( + expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe( mockRunner.createdAt, ); }); diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js index 0ac89e82314..424a4e61ccd 100644 --- a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js +++ b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js @@ -5,11 +5,21 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createLocalState } from '~/runner/graphql/list/local_state'; -import { allRunnersData } from '../mock_data'; Vue.use(VueApollo); -jest.mock('~/flash'); +const makeRunner = (id, deleteRunner = true) => ({ + id, + userPermissions: { deleteRunner }, +}); + +// Multi-select checkbox possible states: +const stateToAttrs = { + unchecked: { disabled: undefined, checked: undefined, indeterminate: undefined }, + checked: { disabled: undefined, checked: 'true', indeterminate: undefined }, + indeterminate: { disabled: undefined, checked: undefined, indeterminate: 'true' }, + disabled: { disabled: 'true', checked: undefined, indeterminate: undefined }, +}; describe('RunnerBulkDeleteCheckbox', () => { let wrapper; @@ -18,12 +28,14 @@ describe('RunnerBulkDeleteCheckbox', () => { const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); - const mockRunners = allRunnersData.data.runners.nodes; - const mockIds = allRunnersData.data.runners.nodes.map(({ id }) => id); - const mockId = mockIds[0]; - const mockIdAnotherPage = 'RUNNER_IN_ANOTHER_PAGE_ID'; + const expectCheckboxToBe = (state) => { + const expected = stateToAttrs[state]; + expect(findCheckbox().attributes('disabled')).toBe(expected.disabled); + expect(findCheckbox().attributes('checked')).toBe(expected.checked); + expect(findCheckbox().attributes('indeterminate')).toBe(expected.indeterminate); + }; - const createComponent = ({ props = {} } = {}) => { + const createComponent = ({ runners = [] } = {}) => { const { cacheConfig, localMutations } = mockState; const apolloProvider = createMockApollo(undefined, undefined, cacheConfig); @@ -33,8 +45,7 @@ describe('RunnerBulkDeleteCheckbox', () => { localMutations, }, propsData: { - runners: mockRunners, - ...props, + runners, }, }); }; @@ -49,31 +60,61 @@ describe('RunnerBulkDeleteCheckbox', () => { jest.spyOn(mockState.localMutations, 'setRunnersChecked'); }); - describe.each` - case | is | checkedRunnerIds | disabled | checked | indeterminate - ${'no runners'} | ${'unchecked'} | ${[]} | ${undefined} | ${undefined} | ${undefined} - ${'no runners in this page'} | ${'unchecked'} | ${[mockIdAnotherPage]} | ${undefined} | ${undefined} | ${undefined} - ${'all runners'} | ${'checked'} | ${mockIds} | ${undefined} | ${'true'} | ${undefined} - ${'some runners'} | ${'indeterminate'} | ${[mockId]} | ${undefined} | ${undefined} | ${'true'} - ${'all plus other runners'} | ${'checked'} | ${[...mockIds, mockIdAnotherPage]} | ${undefined} | ${'true'} | ${undefined} - `('When $case are checked', ({ is, checkedRunnerIds, disabled, checked, indeterminate }) => { - beforeEach(async () => { + describe('when all runners can be deleted', () => { + const mockIds = ['1', '2', '3']; + const mockIdAnotherPage = '4'; + const mockRunners = mockIds.map((id) => makeRunner(id)); + + it.each` + case | checkedRunnerIds | state + ${'no runners'} | ${[]} | ${'unchecked'} + ${'no runners in this page'} | ${[mockIdAnotherPage]} | ${'unchecked'} + ${'all runners'} | ${mockIds} | ${'checked'} + ${'some runners'} | ${[mockIds[0]]} | ${'indeterminate'} + ${'all plus other runners'} | ${[...mockIds, mockIdAnotherPage]} | ${'checked'} + `('if $case are checked, checkbox is $state', ({ checkedRunnerIds, state }) => { mockCheckedRunnerIds = checkedRunnerIds; - createComponent(); + createComponent({ runners: mockRunners }); + expectCheckboxToBe(state); }); + }); + + describe('when some runners cannot be deleted', () => { + it('all allowed runners are selected, checkbox is checked', () => { + mockCheckedRunnerIds = ['a', 'b', 'c']; + createComponent({ + runners: [makeRunner('a'), makeRunner('b'), makeRunner('c', false)], + }); - it(`is ${is}`, () => { - expect(findCheckbox().attributes('disabled')).toBe(disabled); - expect(findCheckbox().attributes('checked')).toBe(checked); - expect(findCheckbox().attributes('indeterminate')).toBe(indeterminate); + expectCheckboxToBe('checked'); + }); + + it('some allowed runners are selected, checkbox is indeterminate', () => { + mockCheckedRunnerIds = ['a', 'b']; + createComponent({ + runners: [makeRunner('a'), makeRunner('b'), makeRunner('c')], + }); + + expectCheckboxToBe('indeterminate'); + }); + + it('no allowed runners are selected, checkbox is disabled', () => { + mockCheckedRunnerIds = ['a', 'b']; + createComponent({ + runners: [makeRunner('a', false), makeRunner('b', false)], + }); + + expectCheckboxToBe('disabled'); }); }); describe('When user selects', () => { + const mockRunners = [makeRunner('1'), makeRunner('2')]; + beforeEach(() => { - mockCheckedRunnerIds = mockIds; - createComponent(); + mockCheckedRunnerIds = ['1', '2']; + createComponent({ runners: mockRunners }); }); it.each([[true], [false]])('sets checked to %s', (checked) => { @@ -89,13 +130,11 @@ describe('RunnerBulkDeleteCheckbox', () => { describe('When runners are loading', () => { beforeEach(() => { - createComponent({ props: { runners: [] } }); + createComponent(); }); - it(`is disabled`, () => { - expect(findCheckbox().attributes('disabled')).toBe('true'); - expect(findCheckbox().attributes('checked')).toBe(undefined); - expect(findCheckbox().attributes('indeterminate')).toBe(undefined); + it('is disabled', () => { + expectCheckboxToBe('disabled'); }); }); }); diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js index 52fe803c536..c8fb7a69379 100644 --- a/spec/frontend/runner/components/runner_delete_button_spec.js +++ b/spec/frontend/runner/components/runner_delete_button_spec.js @@ -9,11 +9,7 @@ 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, - I18N_DELETE_DISABLED_MANY_PROJECTS, - I18N_DELETE_DISABLED_UNKNOWN_REASON, -} from '~/runner/constants'; +import { I18N_DELETE_RUNNER } from '~/runner/constants'; import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue'; @@ -267,29 +263,4 @@ 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_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js index f2281223a25..e6cc936e260 100644 --- a/spec/frontend/runner/components/runner_details_spec.js +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -25,12 +25,7 @@ describe('RunnerDetails', () => { const findDetailGroups = () => wrapper.findComponent(RunnerGroups); - const createComponent = ({ - props = {}, - stubs, - mountFn = shallowMountExtended, - enforceRunnerTokenExpiresAt = false, - } = {}) => { + const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerDetails, { propsData: { ...props, @@ -39,9 +34,6 @@ describe('RunnerDetails', () => { RunnerDetail, ...stubs, }, - provide: { - glFeatures: { enforceRunnerTokenExpiresAt }, - }, }); }; @@ -82,7 +74,6 @@ describe('RunnerDetails', () => { ...runner, }, }, - enforceRunnerTokenExpiresAt: true, stubs: { GlIntersperse, GlSprintf, @@ -135,22 +126,5 @@ describe('RunnerDetails', () => { expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner); }); }); - - describe('Token expiration field', () => { - it.each` - case | flag | shown - ${'is shown when feature flag is enabled'} | ${true} | ${true} - ${'is not shown when feature flag is disabled'} | ${false} | ${false} - `('$case', ({ flag, shown }) => { - createComponent({ - props: { - runner: mockGroupRunner, - }, - enforceRunnerTokenExpiresAt: flag, - }); - - expect(findDd('Token expiry', wrapper).exists()).toBe(shown); - }); - }); }); }); 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 e35bec3aa38..c92e19f9263 100644 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js @@ -4,10 +4,26 @@ 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_ONLINE, INSTANCE_TYPE } from '~/runner/constants'; +import { + PARAM_KEY_STATUS, + PARAM_KEY_TAG, + STATUS_ONLINE, + INSTANCE_TYPE, + DEFAULT_MEMBERSHIP, + DEFAULT_SORT, + CONTACTED_DESC, +} 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'; +const mockSearch = { + runnerType: null, + membership: DEFAULT_MEMBERSHIP, + filters: [], + pagination: { page: 1 }, + sort: DEFAULT_SORT, +}; + describe('RunnerList', () => { let wrapper; @@ -15,8 +31,7 @@ describe('RunnerList', () => { const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem); - const mockDefaultSort = 'CREATED_DESC'; - const mockOtherSort = 'CONTACTED_DESC'; + const mockOtherSort = CONTACTED_DESC; const mockFilters = [ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, { type: 'filtered-search-term', value: { data: '' } }, @@ -32,11 +47,7 @@ describe('RunnerList', () => { propsData: { namespace: 'runners', tokens: [], - value: { - runnerType: null, - filters: [], - sort: mockDefaultSort, - }, + value: mockSearch, ...props, }, stubs: { @@ -115,6 +126,7 @@ describe('RunnerList', () => { props: { value: { runnerType: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, sort: mockOtherSort, filters: mockFilters, }, @@ -141,6 +153,7 @@ describe('RunnerList', () => { expectToHaveLastEmittedInput({ runnerType: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, filters: mockFilters, sort: mockOtherSort, pagination: {}, @@ -154,8 +167,9 @@ describe('RunnerList', () => { expectToHaveLastEmittedInput({ runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: mockFilters, - sort: mockDefaultSort, + sort: DEFAULT_SORT, pagination: {}, }); }); @@ -165,6 +179,7 @@ describe('RunnerList', () => { expectToHaveLastEmittedInput({ runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [], sort: mockOtherSort, pagination: {}, diff --git a/spec/frontend/runner/components/runner_list_empty_state_spec.js b/spec/frontend/runner/components/runner_list_empty_state_spec.js index 59cff863106..038162b889e 100644 --- a/spec/frontend/runner/components/runner_list_empty_state_spec.js +++ b/spec/frontend/runner/components/runner_list_empty_state_spec.js @@ -8,6 +8,7 @@ import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vu const mockSvgPath = 'mock-svg-path.svg'; const mockFilteredSvgPath = 'mock-filtered-svg-path.svg'; +const mockRegistrationToken = 'REGISTRATION_TOKEN'; describe('RunnerListEmptyState', () => { let wrapper; @@ -21,6 +22,7 @@ describe('RunnerListEmptyState', () => { propsData: { svgPath: mockSvgPath, filteredSvgPath: mockFilteredSvgPath, + registrationToken: mockRegistrationToken, ...props, }, directives: { @@ -35,27 +37,52 @@ describe('RunnerListEmptyState', () => { }; describe('when search is not filtered', () => { - beforeEach(() => { - createComponent(); - }); + const title = s__('Runners|Get started with runners'); - it('renders an illustration', () => { - expect(findEmptyState().props('svgPath')).toBe(mockSvgPath); - }); + describe('when there is a registration token', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders an illustration', () => { + expect(findEmptyState().props('svgPath')).toBe(mockSvgPath); + }); + + it('displays "no results" text with instructions', () => { + const desc = s__( + 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', + ); - it('displays "no results" text', () => { - const title = s__('Runners|Get started with runners'); - const desc = s__( - 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', - ); + expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); + }); - expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); + it('opens a runner registration instructions modal with a link', () => { + const { value } = getBinding(findLink().element, 'gl-modal'); + + expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + }); }); - it('opens a runner registration instructions modal with a link', () => { - const { value } = getBinding(findLink().element, 'gl-modal'); + describe('when there is no registration token', () => { + beforeEach(() => { + createComponent({ props: { registrationToken: null } }); + }); + + it('renders an illustration', () => { + expect(findEmptyState().props('svgPath')).toBe(mockSvgPath); + }); + + it('displays "no results" text', () => { + const desc = s__( + 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.', + ); + + expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); + }); - expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + it('has no registration instructions link', () => { + expect(findLink().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 54a9e713721..a31990f8f7e 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -1,12 +1,19 @@ import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; import { extendedWrapper, shallowMountExtended, mountExtended, } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { createLocalState } from '~/runner/graphql/list/local_state'; + import RunnerList from '~/runner/components/runner_list.vue'; -import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue'; +import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; +import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue'; + import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants'; import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; @@ -15,6 +22,8 @@ const mockActiveRunnersCount = mockRunners.length; describe('RunnerList', () => { let wrapper; + let cacheConfig; + let localMutations; const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findTable = () => wrapper.findComponent(GlTableLite); @@ -22,18 +31,24 @@ describe('RunnerList', () => { const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]'); const findCell = ({ row = 0, fieldKey }) => extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`)); + const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete); + const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox); const createComponent = ( { props = {}, provide = {}, ...options } = {}, mountFn = shallowMountExtended, ) => { + ({ cacheConfig, localMutations } = createLocalState()); + wrapper = mountFn(RunnerList, { + apolloProvider: createMockApollo([], {}, cacheConfig), propsData: { runners: mockRunners, activeRunnersCount: mockActiveRunnersCount, ...props, }, provide: { + localMutations, onlineContactTimeoutSecs, staleTimeoutSecs, ...provide, @@ -50,7 +65,7 @@ describe('RunnerList', () => { createComponent( { stubs: { - RunnerStatusPopover: { + HelpPopover: { template: '<div/>', }, }, @@ -60,11 +75,13 @@ describe('RunnerList', () => { const headerLabels = findHeaders().wrappers.map((w) => w.text()); - expect(findHeaders().at(0).findComponent(RunnerStatusPopover).exists()).toBe(true); + expect(findHeaders().at(0).findComponent(HelpPopover).exists()).toBe(true); + expect(findHeaders().at(2).findComponent(HelpPopover).exists()).toBe(true); expect(headerLabels).toEqual([ - 'Status', - 'Runner', + s__('Runners|Status'), + s__('Runners|Runner'), + s__('Runners|Owner'), '', // actions has no label ]); }); @@ -123,21 +140,40 @@ describe('RunnerList', () => { ); }); + it('runner bulk delete is available', () => { + expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners); + }); + + it('runner bulk delete checkbox is available', () => { + expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners); + }); + it('Displays a checkbox field', () => { expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true); }); - it('Emits a checked event', async () => { - const checkbox = findCell({ fieldKey: 'checkbox' }).find('input'); + it('Sets a runner as checked', async () => { + const runner = mockRunners[0]; + const setRunnerCheckedMock = jest + .spyOn(localMutations, 'setRunnerChecked') + .mockImplementation(() => {}); + const checkbox = findCell({ fieldKey: 'checkbox' }).find('input'); await checkbox.setChecked(); - expect(wrapper.emitted('checked')).toHaveLength(1); - expect(wrapper.emitted('checked')[0][0]).toEqual({ + expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1); + expect(setRunnerCheckedMock).toHaveBeenCalledWith({ + runner, isChecked: true, - runner: mockRunners[0], }); }); + + it('Emits a deleted event', async () => { + const event = { message: 'Deleted!' }; + findRunnerBulkDelete().vm.$emit('deleted', event); + + expect(wrapper.emitted('deleted')).toEqual([[event]]); + }); }); describe('Scoped cell slots', () => { diff --git a/spec/frontend/runner/components/runner_membership_toggle_spec.js b/spec/frontend/runner/components/runner_membership_toggle_spec.js new file mode 100644 index 00000000000..1a7ae22618a --- /dev/null +++ b/spec/frontend/runner/components/runner_membership_toggle_spec.js @@ -0,0 +1,57 @@ +import { GlToggle } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue'; +import { + I18N_SHOW_ONLY_INHERITED, + MEMBERSHIP_DESCENDANTS, + MEMBERSHIP_ALL_AVAILABLE, +} from '~/runner/constants'; + +describe('RunnerMembershipToggle', () => { + let wrapper; + + const findToggle = () => wrapper.findComponent(GlToggle); + + const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerMembershipToggle, { + propsData: props, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays text', () => { + createComponent({ mountFn: mount }); + + expect(wrapper.text()).toBe(I18N_SHOW_ONLY_INHERITED); + }); + + it.each` + membershipValue | toggleValue + ${MEMBERSHIP_DESCENDANTS} | ${true} + ${MEMBERSHIP_ALL_AVAILABLE} | ${false} + `( + 'Displays a membership of $membershipValue as enabled=$toggleValue', + ({ membershipValue, toggleValue }) => { + createComponent({ props: { value: membershipValue } }); + + expect(findToggle().props('value')).toBe(toggleValue); + }, + ); + + it.each` + changeEvt | membershipValue + ${true} | ${MEMBERSHIP_DESCENDANTS} + ${false} | ${MEMBERSHIP_ALL_AVAILABLE} + `( + 'Emits $changeEvt when value is changed to $membershipValue', + ({ changeEvt, membershipValue }) => { + createComponent(); + findToggle().vm.$emit('change', changeEvt); + + expect(wrapper.emitted('input')).toStrictEqual([[membershipValue]]); + }, + ); +}); diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js index 1a8aced9292..d1f04f0ee37 100644 --- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js +++ b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js @@ -29,6 +29,8 @@ describe('RunnerStackedLayoutBanner', () => { }); it('Does not display a banner when dismissed', async () => { + createComponent(); + findLocalStorageSync().vm.$emit('input', true); await nextTick(); diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js index 45ab8684332..dde35533bc3 100644 --- a/spec/frontend/runner/components/runner_type_tabs_spec.js +++ b/spec/frontend/runner/components/runner_type_tabs_spec.js @@ -2,9 +2,21 @@ import { GlTab } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerCount from '~/runner/components/stat/runner_count.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }; +import { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + DEFAULT_MEMBERSHIP, + DEFAULT_SORT, +} from '~/runner/constants'; + +const mockSearch = { + runnerType: null, + membership: DEFAULT_MEMBERSHIP, + filters: [], + pagination: { page: 1 }, + sort: DEFAULT_SORT, +}; const mockCount = (type, multiplier = 1) => { let count; @@ -113,7 +125,7 @@ describe('RunnerTypeTabs', () => { }); findTabs().wrappers.forEach((tab) => { - expect(tab.find(RunnerCount).props()).toEqual({ + expect(tab.findComponent(RunnerCount).props()).toEqual({ scope: INSTANCE_TYPE, skip: false, variables: expect.objectContaining(mockVariables), diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index 7b67a89f989..e12736216a0 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -145,7 +145,7 @@ describe('RunnerUpdateForm', () => { }); it('Form skeleton is shown', () => { - expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); expect(findFields()).toHaveLength(0); }); diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js index 22f0561ca5f..a7363eb11cd 100644 --- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js +++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js @@ -77,7 +77,7 @@ describe('TagToken', () => { const findToken = () => wrapper.findComponent(GlToken); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - beforeEach(async () => { + beforeEach(() => { mock = new MockAdapter(axios); mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags); @@ -86,9 +86,6 @@ describe('TagToken', () => { .reply(200, mockTagsFiltered); getRecentlyUsedSuggestions.mockReturnValue([]); - - createComponent(); - await waitForPromises(); }); afterEach(() => { @@ -97,11 +94,17 @@ describe('TagToken', () => { }); describe('when the tags token is displayed', () => { + beforeEach(() => { + createComponent(); + }); + it('requests tags suggestions', () => { expect(mock.history.get[0].params).toEqual({ search: '' }); }); - it('displays tags suggestions', () => { + it('displays tags suggestions', async () => { + await waitForPromises(); + mockTags.forEach(({ name }, i) => { expect(findGlFilteredSearchSuggestions().at(i).text()).toBe(name); }); @@ -132,13 +135,13 @@ describe('TagToken', () => { }); describe('when the users filters suggestions', () => { - beforeEach(async () => { + beforeEach(() => { + createComponent(); + findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm }); }); - it('requests filtered tags suggestions', async () => { - await waitForPromises(); - + it('requests filtered tags suggestions', () => { expect(mock.history.get[1].params).toEqual({ search: mockSearchTerm }); }); @@ -166,7 +169,7 @@ describe('TagToken', () => { await waitForPromises(); }); - it('error is shown', async () => { + it('error is shown', () => { expect(createAlert).toHaveBeenCalledTimes(1); expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) }); }); @@ -180,8 +183,26 @@ describe('TagToken', () => { await waitForPromises(); }); - it('selected tag is displayed', async () => { + it('selected tag is displayed', () => { expect(findToken().exists()).toBe(true); }); }); + + describe('when suggestions are disabled', () => { + beforeEach(async () => { + createComponent({ + config: { + ...mockTagTokenConfig, + suggestionsDisabled: true, + }, + }); + + await waitForPromises(); + }); + + it('displays no suggestions', () => { + expect(findGlFilteredSearchSuggestions()).toHaveLength(0); + expect(mock.history.get).toHaveLength(0); + }); + }); }); |