diff options
Diffstat (limited to 'spec/frontend/ci/runner/components/cells')
6 files changed, 611 insertions, 0 deletions
diff --git a/spec/frontend/ci/runner/components/cells/link_cell_spec.js b/spec/frontend/ci/runner/components/cells/link_cell_spec.js new file mode 100644 index 00000000000..61bb4432c8e --- /dev/null +++ b/spec/frontend/ci/runner/components/cells/link_cell_spec.js @@ -0,0 +1,72 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import LinkCell from '~/ci/runner/components/cells/link_cell.vue'; + +describe('LinkCell', () => { + let wrapper; + + const findGlLink = () => wrapper.findComponent(GlLink); + const findSpan = () => wrapper.find('span'); + + const createComponent = ({ props = {}, ...options } = {}) => { + wrapper = shallowMountExtended(LinkCell, { + propsData: { + ...props, + }, + ...options, + }); + }; + + it('when an href is provided, renders a link', () => { + createComponent({ props: { href: '/url' } }); + expect(findGlLink().exists()).toBe(true); + }); + + it('when an href is not provided, renders no link', () => { + createComponent(); + expect(findGlLink().exists()).toBe(false); + }); + + describe.each` + href | findContent + ${null} | ${findSpan} + ${'/url'} | ${findGlLink} + `('When href is $href', ({ href, findContent }) => { + const content = 'My Text'; + const attrs = { foo: 'bar' }; + const listeners = { + click: jest.fn(), + }; + + beforeEach(() => { + createComponent({ + props: { href }, + slots: { + default: content, + }, + attrs, + listeners, + }); + }); + + afterAll(() => { + listeners.click.mockReset(); + }); + + it('Renders content', () => { + expect(findContent().text()).toBe(content); + }); + + it('Passes attributes', () => { + expect(findContent().attributes()).toMatchObject(attrs); + }); + + it('Passes event listeners', () => { + expect(listeners.click).toHaveBeenCalledTimes(0); + + findContent().vm.$emit('click'); + + expect(listeners.click).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js new file mode 100644 index 00000000000..82e262d1b73 --- /dev/null +++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js @@ -0,0 +1,138 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue'; +import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue'; +import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue'; +import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue'; +import { allRunnersData } from '../../mock_data'; + +const mockRunner = allRunnersData.data.runners.nodes[0]; + +describe('RunnerActionsCell', () => { + let wrapper; + + const findEditBtn = () => wrapper.findComponent(RunnerEditButton); + const findRunnerPauseBtn = () => wrapper.findComponent(RunnerPauseButton); + const findDeleteBtn = () => wrapper.findComponent(RunnerDeleteButton); + + const createComponent = ({ runner = {}, ...props } = {}) => { + wrapper = shallowMountExtended(RunnerActionsCell, { + propsData: { + editUrl: mockRunner.editAdminUrl, + runner: { + id: mockRunner.id, + shortSha: mockRunner.shortSha, + editAdminUrl: mockRunner.editAdminUrl, + userPermissions: mockRunner.userPermissions, + ...runner, + }, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Edit Action', () => { + it('Displays the runner edit link with the correct href', () => { + createComponent(); + + expect(findEditBtn().attributes('href')).toBe(mockRunner.editAdminUrl); + }); + + it('Does not render the runner edit link when user cannot update', () => { + createComponent({ + runner: { + userPermissions: { + ...mockRunner.userPermissions, + updateRunner: false, + }, + }, + }); + + expect(findEditBtn().exists()).toBe(false); + }); + + it('Does not render the runner edit link when editUrl is not provided', () => { + createComponent({ + editUrl: null, + }); + + expect(findEditBtn().exists()).toBe(false); + }); + }); + + describe('Pause action', () => { + it('Renders a compact pause button', () => { + createComponent(); + + expect(findRunnerPauseBtn().props('compact')).toBe(true); + }); + + it('Does not render the runner pause button when user cannot update', () => { + createComponent({ + runner: { + userPermissions: { + ...mockRunner.userPermissions, + updateRunner: false, + }, + }, + }); + + expect(findRunnerPauseBtn().exists()).toBe(false); + }); + }); + + describe('Delete action', () => { + it('Renders a compact delete button', () => { + createComponent(); + + 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' }; + + createComponent(); + + expect(wrapper.emitted('deleted')).toBe(undefined); + + findDeleteBtn().vm.$emit('deleted', value); + + expect(wrapper.emitted('deleted')).toEqual([[value]]); + }); + + it('Does not render the runner delete button when user cannot delete', () => { + createComponent({ + runner: { + userPermissions: { + ...mockRunner.userPermissions, + deleteRunner: false, + }, + }, + }); + + expect(findDeleteBtn().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js new file mode 100644 index 00000000000..3097e43e583 --- /dev/null +++ b/spec/frontend/ci/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 '~/ci/runner/components/cells/runner_owner_cell.vue'; + +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/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/ci/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js new file mode 100644 index 00000000000..4aa354f9b62 --- /dev/null +++ b/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js @@ -0,0 +1,164 @@ +import { __ } from '~/locale'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import RunnerStackedSummaryCell from '~/ci/runner/components/cells/runner_stacked_summary_cell.vue'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import RunnerTags from '~/ci/runner/components/runner_tags.vue'; +import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/ci/runner/constants'; + +import { allRunnersData } from '../../mock_data'; + +const mockRunner = allRunnersData.data.runners.nodes[0]; + +describe('RunnerTypeCell', () => { + let wrapper; + + const findLockIcon = () => wrapper.findByTestId('lock-icon'); + const findRunnerTags = () => wrapper.findComponent(RunnerTags); + const findRunnerSummaryField = (icon) => + wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon) + .wrappers[0]; + + const createComponent = (runner, options) => { + wrapper = mountExtended(RunnerStackedSummaryCell, { + propsData: { + runner: { + ...mockRunner, + ...runner, + }, + }, + stubs: { + RunnerSummaryField, + }, + ...options, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays the runner name as id and short token', () => { + expect(wrapper.text()).toContain( + `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`, + ); + }); + + 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 type', () => { + createComponent({ + runnerType: INSTANCE_TYPE, + locked: true, + }); + + expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE); + }); + + it('Displays the runner version', () => { + expect(wrapper.text()).toContain(mockRunner.version); + }); + + it('Displays the runner description', () => { + expect(wrapper.text()).toContain(mockRunner.description); + }); + + it('Displays last contact', () => { + createComponent({ + contactedAt: '2022-01-02', + }); + + expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02'); + }); + + it('Displays empty last contact', () => { + createComponent({ + contactedAt: null, + }); + + expect(findRunnerSummaryField('clock').findComponent(TimeAgo).exists()).toBe(false); + expect(findRunnerSummaryField('clock').text()).toContain(__('Never')); + }); + + it('Displays ip address', () => { + createComponent({ + ipAddress: '127.0.0.1', + }); + + expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1'); + }); + + it('Displays no ip address', () => { + createComponent({ + ipAddress: null, + }); + + expect(findRunnerSummaryField('disk')).toBeUndefined(); + }); + + it('Displays job count', () => { + expect(findRunnerSummaryField('pipeline').text()).toContain(`${mockRunner.jobCount}`); + }); + + it('Formats large job counts', () => { + createComponent({ + jobCount: 1000, + }); + + expect(findRunnerSummaryField('pipeline').text()).toContain('1,000'); + }); + + it('Formats large job counts with a plus symbol', () => { + createComponent({ + jobCount: 1001, + }); + + expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+'); + }); + + it('Displays created at', () => { + expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe( + mockRunner.createdAt, + ); + }); + + it('Displays tag list', () => { + createComponent({ + tagList: ['shell', 'linux'], + }); + + expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']); + }); + + it('Displays a custom slot', () => { + const slotContent = 'My custom runner name'; + + createComponent( + {}, + { + slots: { + 'runner-name': slotContent, + }, + }, + ); + + expect(wrapper.text()).toContain(slotContent); + }); +}); diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js new file mode 100644 index 00000000000..2fb824a8fa5 --- /dev/null +++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js @@ -0,0 +1,77 @@ +import { mount } from '@vue/test-utils'; +import RunnerStatusCell from '~/ci/runner/components/cells/runner_status_cell.vue'; + +import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue'; +import RunnerPausedBadge from '~/ci/runner/components/runner_paused_badge.vue'; +import { + I18N_PAUSED, + I18N_STATUS_ONLINE, + I18N_STATUS_OFFLINE, + INSTANCE_TYPE, + STATUS_ONLINE, + STATUS_OFFLINE, +} from '~/ci/runner/constants'; + +describe('RunnerStatusCell', () => { + let wrapper; + + const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge); + const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge); + + 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()).toContain(I18N_STATUS_ONLINE); + expect(findStatusBadge().text()).toBe(I18N_STATUS_ONLINE); + }); + + it('Displays offline status', () => { + createComponent({ + runner: { + status: STATUS_OFFLINE, + }, + }); + + expect(wrapper.text()).toMatchInterpolatedText(I18N_STATUS_OFFLINE); + expect(findStatusBadge().text()).toBe(I18N_STATUS_OFFLINE); + }); + + it('Displays paused status', () => { + createComponent({ + runner: { + active: false, + status: STATUS_ONLINE, + }, + }); + + expect(wrapper.text()).toMatchInterpolatedText(`${I18N_STATUS_ONLINE} ${I18N_PAUSED}`); + expect(findPausedBadge().text()).toBe(I18N_PAUSED); + }); + + it('Is empty when data is missing', () => { + createComponent({ + runner: { + status: null, + }, + }); + + expect(wrapper.text()).toBe(''); + }); +}); diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js new file mode 100644 index 00000000000..f536e0dcbcf --- /dev/null +++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js @@ -0,0 +1,49 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +describe('RunnerSummaryField', () => { + let wrapper; + + const findIcon = () => wrapper.findComponent(GlIcon); + const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value; + + const createComponent = ({ props, ...options } = {}) => { + wrapper = shallowMount(RunnerSummaryField, { + propsData: { + icon: '', + tooltip: '', + ...props, + }, + directives: { + GlTooltip: createMockDirective(), + }, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('shows content in slot', () => { + createComponent({ + slots: { default: 'content' }, + }); + + expect(wrapper.text()).toBe('content'); + }); + + it('shows icon', () => { + createComponent({ props: { icon: 'git' } }); + + expect(findIcon().props('name')).toBe('git'); + }); + + it('shows tooltip', () => { + createComponent({ props: { tooltip: 'tooltip' } }); + + expect(getTooltipValue()).toBe('tooltip'); + }); +}); |