Commit 399182b2 authored by Miguel Rincon's avatar Miguel Rincon Committed by Jose Ivan Vargas

Add basic runner details in admin view runner

This change adds basic details for the read-only admin page for runners
as a first MCV of the page.

This page is behind a feature flag, so no changelog is included.
parent 4f1601c7
import { initAdminRunnerShow } from '~/runner/admin_runner_show';
initAdminRunnerShow();
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import RunnerEditButton from '../components/runner_edit_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import { I18N_FETCH_ERROR } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
import { captureException } from '../sentry_utils';
export default {
name: 'AdminRunnerShowApp',
components: {
RunnerEditButton,
RunnerHeader,
RunnerDetails,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runnerId: {
type: String,
required: true,
},
},
data() {
return {
runner: null,
};
},
apollo: {
runner: {
query: getRunnerQuery,
variables() {
return {
id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
};
},
error(error) {
createAlert({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
},
computed: {
canUpdate() {
return this.runner.userPermissions?.updateRunner;
},
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
},
};
</script>
<template>
<div>
<runner-header v-if="runner" :runner="runner">
<template #actions>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
</template>
</runner-header>
<runner-details :runner="runner" />
</div>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AdminRunnerShowApp from './admin_runner_show_app.vue';
Vue.use(VueApollo);
export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
const el = document.querySelector(selector);
if (!el) {
return null;
}
const { runnerId } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
render(h) {
return h(AdminRunnerShowApp, {
props: {
runnerId,
},
});
},
});
};
......@@ -6,9 +6,9 @@ import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphq
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerEditButton from '../runner_edit_button.vue';
import RunnerDeleteModal from '../runner_delete_modal.vue';
const I18N_EDIT = __('Edit');
const I18N_PAUSE = __('Pause');
const I18N_RESUME = __('Resume');
const I18N_DELETE = s__('Runners|Delete runner');
......@@ -19,6 +19,7 @@ export default {
components: {
GlButton,
GlButtonGroup,
RunnerEditButton,
RunnerDeleteModal,
},
directives: {
......@@ -147,7 +148,6 @@ export default {
captureException({ error, component: this.$options.name });
},
},
I18N_EDIT,
I18N_DELETE,
};
</script>
......@@ -161,14 +161,7 @@ export default {
See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
-->
<gl-button
v-if="canUpdate && runner.editAdminUrl"
v-gl-tooltip.hover.viewport="$options.I18N_EDIT"
:href="runner.editAdminUrl"
:aria-label="$options.I18N_EDIT"
icon="pencil"
data-testid="edit-runner"
/>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
<gl-button
v-if="canUpdate"
v-gl-tooltip.hover.viewport="toggleActiveTitle"
......
<script>
import { __ } from '~/locale';
/**
* Usage:
*
* With a `value` prop:
*
* <runner-detail label="Field Name" :value="value" />
*
* Or a `value` slot:
*
* <runner-detail label="Field Name">
* <template #value>
* <strong>{{ value }}</strong>
* </template>
* </runner-detail>
*
*/
export default {
props: {
label: {
type: String,
required: true,
},
value: {
type: String,
default: null,
required: false,
},
emptyValue: {
type: String,
default: __('None'),
required: false,
},
},
};
</script>
<template>
<div class="gl-display-flex gl-pb-4">
<dt class="gl-mr-2">{{ label }}</dt>
<dd class="gl-mb-0">
<template v-if="value || $slots.value">
<slot name="value">{{ value }}</slot>
</template>
<span v-else class="gl-text-gray-500">{{ emptyValue }}</span>
</dd>
</div>
</template>
<script>
import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED } from '../constants';
import RunnerDetail from './runner_detail.vue';
import RunnerTags from './runner_tags.vue';
export default {
components: {
GlTabs,
GlTab,
GlIntersperse,
RunnerDetail,
RunnerTags,
TimeAgo,
},
props: {
runner: {
type: Object,
required: false,
default: null,
},
},
computed: {
maximumTimeout() {
const { maximumTimeout } = this.runner;
if (typeof maximumTimeout !== 'number') {
return null;
}
return timeIntervalInWords(maximumTimeout);
},
configTextProtected() {
if (this.runner.accessLevel === ACCESS_LEVEL_REF_PROTECTED) {
return s__('Runners|Protected');
}
return null;
},
configTextUntagged() {
if (this.runner.runUntagged) {
return s__('Runners|Runs untagged jobs');
}
return null;
},
},
ACCESS_LEVEL_REF_PROTECTED,
};
</script>
<template>
<gl-tabs>
<gl-tab>
<template #title>{{ s__('Runners|Details') }}</template>
<div v-if="runner" class="gl-py-4">
<dl>
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
<runner-detail
:label="s__('Runners|Last contact')"
:empty-value="s__('Runners|Never contacted')"
>
<template #value>
<time-ago v-if="runner.contactedAt" :time="runner.contactedAt" />
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Version')" :value="runner.version" />
<runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
<runner-detail :label="s__('Runners|Configuration')">
<template #value>
<gl-intersperse v-if="configTextProtected || configTextUntagged">
<span v-if="configTextProtected">{{ configTextProtected }}</span>
<span v-if="configTextUntagged">{{ configTextUntagged }}</span>
</gl-intersperse>
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
<runner-detail :label="s__('Runners|Tags')">
<template #value>
<runner-tags
v-if="runner.tagList && runner.tagList.length"
class="gl-vertical-align-middle"
:tag-list="runner.tagList"
size="sm"
/>
</template>
</runner-detail>
</dl>
</div>
</gl-tab>
</gl-tabs>
</template>
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const I18N_EDIT = __('Edit');
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
I18N_EDIT,
};
</script>
<template>
<gl-button
v-gl-tooltip="$options.I18N_EDIT"
v-bind="$attrs"
:aria-label="$options.I18N_EDIT"
icon="pencil"
v-on="$listeners"
/>
</template>
<script>
import { GlSprintf } from '@gitlab/ui';
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { I18N_DETAILS_TITLE } from '../constants';
import { I18N_DETAILS_TITLE, I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
import RunnerTypeBadge from './runner_type_badge.vue';
import RunnerStatusBadge from './runner_status_badge.vue';
export default {
components: {
GlIcon,
GlSprintf,
TimeAgo,
RunnerTypeBadge,
RunnerStatusBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runner: {
type: Object,
......@@ -29,24 +33,36 @@ export default {
return sprintf(I18N_DETAILS_TITLE, { runner_id: id });
},
},
I18N_LOCKED_RUNNER_DESCRIPTION,
};
</script>
<template>
<div class="gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<runner-status-badge :runner="runner" />
<runner-type-badge v-if="runner" :type="runner.runnerType" />
<template v-if="runner.createdAt">
<gl-sprintf :message="__('%{runner} created %{timeago}')">
<template #runner>
<strong>{{ heading }}</strong>
</template>
<template #timeago>
<time-ago :time="runner.createdAt" />
</template>
</gl-sprintf>
</template>
<template v-else>
<strong>{{ heading }}</strong>
</template>
<div
class="gl-display-flex gl-align-items-center gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
<div>
<runner-status-badge :runner="runner" />
<runner-type-badge v-if="runner" :type="runner.runnerType" />
<template v-if="runner.createdAt">
<gl-sprintf :message="__('%{runner} created %{timeago}')">
<template #runner>
<strong>{{ heading }}</strong>
<gl-icon
v-if="runner.locked"
v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
name="lock"
:aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
/>
</template>
<template #timeago>
<time-ago :time="runner.createdAt" />
</template>
</gl-sprintf>
</template>
<template v-else>
<strong>{{ heading }}</strong>
</template>
</div>
<div class="gl-ml-auto"><slot name="actions"></slot></div>
</div>
</template>
......@@ -20,7 +20,7 @@ export default {
};
</script>
<template>
<div>
<span>
<runner-tag
v-for="tag in tagList"
:key="tag"
......@@ -28,5 +28,5 @@ export default {
:tag="tag"
:size="size"
/>
</div>
</span>
</template>
......@@ -11,4 +11,11 @@ fragment RunnerDetailsShared on CiRunner {
tagList
createdAt
status(legacyMode: null)
contactedAt
version
editAdminUrl
userPermissions {
updateRunner
deleteRunner
}
}
......@@ -5,4 +5,4 @@
- page_title title
- add_to_breadcrumbs _('Runners'), admin_runners_path
-# Empty view in development behind feature flag runner_read_only_admin_view
#js-admin-runner-show{ data: {runner_id: @runner.id} }
......@@ -30738,6 +30738,9 @@ msgstr ""
msgid "Runners|Command to register runner"
msgstr ""
msgid "Runners|Configuration"
msgstr ""
msgid "Runners|Copy instructions"
msgstr ""
......@@ -30756,6 +30759,9 @@ msgstr ""
msgid "Runners|Description"
msgstr ""
msgid "Runners|Details"
msgstr ""
msgid "Runners|Download and install binary"
msgstr ""
......@@ -30906,6 +30912,9 @@ msgstr ""
msgid "Runners|Runners in this group: %{groupRunnersCount}"
msgstr ""
msgid "Runners|Runs untagged jobs"
msgstr ""
msgid "Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner."
msgstr ""
......
......@@ -476,6 +476,42 @@ RSpec.describe "Admin Runners" do
end
end
describe "Runner show page", :js do
let(:runner) do
create(
:ci_runner,
description: 'runner-foo',
version: '14.0',
ip_address: '127.0.0.1',
tag_list: %w(tag1 tag2)
)
end
before do
visit admin_runner_path(runner)
end
describe 'runner show page breadcrumbs' do
it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
expect(page.find('h2')).to have_link("##{runner.id} (#{runner.short_sha})")
end
end
end
it 'shows runner details' do
aggregate_failures do
expect(page).to have_content 'Description runner-foo'
expect(page).to have_content 'Last contact Never contacted'
expect(page).to have_content 'Version 14.0'
expect(page).to have_content 'IP Address 127.0.0.1'
expect(page).to have_content 'Configuration Runs untagged jobs'
expect(page).to have_content 'Maximum job timeout None'
expect(page).to have_content 'Tags tag1 tag2'
end
end
end
describe "Runner edit page" do
let(:runner) { create(:ci_runner) }
......@@ -487,7 +523,7 @@ RSpec.describe "Admin Runners" do
wait_for_requests
end
describe 'runner page breadcrumbs' do
describe 'runner edit page breadcrumbs' do
it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
......
......@@ -55,10 +55,11 @@ describe('AdminRunnerEditApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
it('displays the runner id', async () => {
it('displays the runner id and creation date', async () => {
await createComponentWithApollo({ mountFn: mount });
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`);
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
expect(findRunnerHeader().text()).toContain('created');
});
it('displays the runner type and status', async () => {
......
import Vue from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerDetails from '~/runner/components/runner_details.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
import { captureException } from '~/runner/sentry_utils';
import { runnerData } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
const mockRunnerGraphqlId = runnerData.data.runner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
Vue.use(VueApollo);
describe('AdminRunnerShowApp', () => {
let wrapper;
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(AdminRunnerShowApp, {
apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
...props,
},
});
return waitForPromises();
};
beforeEach(() => {
mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
});
afterEach(() => {
mockRunnerQuery.mockReset();
wrapper.destroy();
});
describe('When showing runner details', () => {
beforeEach(async () => {
await createComponent({ mountFn: mount });
});
it('expect GraphQL ID to be requested', async () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
it('displays the runner header', async () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
it('shows basic runner details', async () => {
const expected = `Details
Description Instance runner
Last contact Never contacted
Version 1.0.0
IP Address 127.0.0.1
Configuration Runs untagged jobs
Maximum job timeout None
Tags None`.replace(/\s+/g, ' ');
expect(findRunnerDetails().text()).toMatchInterpolatedText(expected);
});
});
describe('When there is an error', () => {
beforeEach(async () => {
mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!'));
await createComponent();
});
it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Network error: Error!'),
component: 'AdminRunnerShowApp',
});
});
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalled();
});
});
});
......@@ -9,6 +9,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { captureException } from '~/runner/sentry_utils';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
......@@ -33,7 +34,7 @@ describe('RunnerTypeCell', () => {
const runnerDeleteMutationHandler = jest.fn();
const runnerActionsUpdateMutationHandler = jest.fn();
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findEditBtn = () => wrapper.findComponent(RunnerEditButton);
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal);
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
......
import { GlSprintf, GlIntersperse } from '@gitlab/ui';
import { createWrapper, ErrorWrapper } from '@vue/test-utils';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { useFakeDate } from 'helpers/fake_date';
import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue';
import { runnerData } from '../mock_data';
const mockRunner = runnerData.data.runner;
describe('RunnerDetails', () => {
let wrapper;
const mockNow = '2021-01-15T12:00:00Z';
const mockOneHourAgo = '2021-01-15T11:00:00Z';
useFakeDate(mockNow);
/**
* Find the definition (<dd>) that corresponds to this term (<dt>)
* @param {string} dtLabel - Label for this value
* @returns Wrapper
*/
const findDd = (dtLabel) => {
const dt = wrapper.findByText(dtLabel).element;
const dd = dt.nextElementSibling;
if (dt.tagName === 'DT' && dd.tagName === 'DD') {
return createWrapper(dd, {});
}
return ErrorWrapper(dtLabel);
};
const createComponent = ({ runner = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
runner: {
...mockRunner,
...runner,
},
},
stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
RunnerDetail,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
field | runner | expectedValue
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
${'Version'} | ${{ version: null }} | ${'None'}
${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
runner,
});
});
it(`displays expected value "${expectedValue}"`, () => {
expect(findDd(field).text()).toBe(expectedValue);
});
});
describe('"Tags" field', () => {
it('displays expected value "tag-1 tag-2"', () => {
createComponent({
runner: { tagList: ['tag-1', 'tag-2'] },
mountFn: mountExtended,
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
});
it('displays "None" when runner has no tags', () => {
createComponent({
runner: { tagList: [] },
mountFn: mountExtended,
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
});
});
});
import { shallowMount, mount } from '@vue/test-utils';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerEditButton', () => {
let wrapper;
const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value;
const createComponent = ({ attrs = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(RunnerEditButton, {
attrs,
directives: {
GlTooltip: createMockDirective(),
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('Displays Edit text', () => {
expect(wrapper.attributes('aria-label')).toBe('Edit');
});
it('Displays Edit tooltip', () => {
expect(getTooltipValue()).toBe('Edit');
});
it('Renders a link and adds an href attribute', () => {
createComponent({ attrs: { href: '/edit' }, mountFn: mount });
expect(wrapper.element.tagName).toBe('A');
expect(wrapper.attributes('href')).toBe('/edit');
});
});
import { GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
......@@ -18,9 +18,10 @@ describe('RunnerHeader', () => {
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findRunnerLockedIcon = () => wrapper.findByTestId('lock-icon');
const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => {
const createComponent = ({ runner = {}, options = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerHeader, {
propsData: {
runner: {
......@@ -32,6 +33,7 @@ describe('RunnerHeader', () => {
GlSprintf,
TimeAgo,
},
...options,
});
};
......@@ -41,24 +43,24 @@ describe('RunnerHeader', () => {
it('displays the runner status', () => {
createComponent({
mountFn: mount,
mountFn: mountExtended,
runner: {
status: STATUS_ONLINE,
},
});
expect(findRunnerStatusBadge().text()).toContain(`online`);
expect(findRunnerStatusBadge().text()).toContain('online');
});
it('displays the runner type', () => {
createComponent({
mountFn: mount,
mountFn: mountExtended,
runner: {
runnerType: GROUP_TYPE,
},
});
expect(findRunnerTypeBadge().text()).toContain(`group`);
expect(findRunnerTypeBadge().text()).toContain('group');
});
it('displays the runner id', () => {
......@@ -68,7 +70,18 @@ describe('RunnerHeader', () => {
},
});
expect(wrapper.text()).toContain(`Runner #99`);
expect(wrapper.text()).toContain('Runner #99');
});
it('displays the runner locked icon', () => {
createComponent({
runner: {
locked: true,
},
mountFn: mountExtended,
});
expect(findRunnerLockedIcon().exists()).toBe(true);
});
it('displays the runner creation time', () => {
......@@ -78,7 +91,7 @@ describe('RunnerHeader', () => {
expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
});
it('does not display runner creation time if createdAt missing', () => {
it('does not display runner creation time if "createdAt" is missing', () => {
createComponent({
runner: {
id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
......@@ -86,8 +99,21 @@ describe('RunnerHeader', () => {
},
});
expect(wrapper.text()).toContain(`Runner #99`);
expect(wrapper.text()).toContain('Runner #99');
expect(wrapper.text()).not.toMatch(/created .+/);
expect(findTimeAgo().exists()).toBe(false);
});
it('displays actions in a slot', () => {
createComponent({
options: {
slots: {
actions: '<div data-testid="actions-content">My Actions</div>',
},
mountFn: mountExtended,
},
});
expect(wrapper.findByTestId('actions-content').text()).toBe('My Actions');
});
});
......@@ -6,6 +6,7 @@ import {
} from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import { runnersData } from '../mock_data';
const mockRunners = runnersData.data.runners.nodes;
......@@ -90,7 +91,7 @@ describe('RunnerList', () => {
// Actions
const actions = findCell({ fieldKey: 'actions' });
expect(actions.findByTestId('edit-runner').exists()).toBe(true);
expect(actions.findComponent(RunnerEditButton).exists()).toBe(true);
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
......
......@@ -121,8 +121,18 @@ describe('RunnerUpdateForm', () => {
it('Updates runner with no changes', async () => {
await submitFormAndWait();
// Some fields are not submitted
const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner;
// Some read-only fields are not submitted
const {
ipAddress,
runnerType,
createdAt,
status,
editAdminUrl,
contactedAt,
userPermissions,
version,
...submitted
} = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment