Skip to content
Snippets Groups Projects
Commit f279d291 authored by Miguel Rincon's avatar Miguel Rincon
Browse files

Create a Vue form for the runner UI details

This change defines a new form for runners in Vue UI which follows the
design guidelines, this form will later replace the HAML form
to update runner details.
parent b3ebe15e
No related branches found
No related tags found
No related merge requests found
Showing
with 694 additions and 30 deletions
Loading
Loading
@@ -3,7 +3,7 @@ import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
 
const i18n = {
I18N_EDIT: __('Edit'),
Loading
Loading
@@ -76,7 +76,7 @@ export default {
runnerUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: updateRunnerMutation,
mutation: runnerUpdateMutation,
variables: {
input: {
id: this.runner.id,
Loading
Loading
<script>
import { GlAlert, GlLink } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
const ALERT_DATA = {
[INSTANCE_TYPE]: {
title: s__(
'Runners|This runner is available to all groups and projects in your GitLab instance.',
),
message: s__(
'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.',
),
variant: 'success',
anchor: 'shared-runners',
},
[GROUP_TYPE]: {
title: s__('Runners|This runner is available to all projects and subgroups in a group.'),
message: s__(
'Runners|Use Group runners when you want all projects in a group to have access to a set of runners.',
),
variant: 'success',
anchor: 'group-runners',
},
[PROJECT_TYPE]: {
title: s__('Runners|This runner is associated with specific projects.'),
message: s__(
'Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner.',
),
variant: 'info',
anchor: 'specific-runners',
},
};
export default {
components: {
GlAlert,
GlLink,
},
props: {
type: {
type: String,
required: false,
default: null,
validator(type) {
return Boolean(ALERT_DATA[type]);
},
},
},
computed: {
alert() {
return ALERT_DATA[this.type];
},
helpHref() {
return helpPagePath('ci/runners/runners_scope', { anchor: this.alert.anchor });
},
},
};
</script>
<template>
<gl-alert v-if="alert" :variant="alert.variant" :title="alert.title" :dismissible="false">
{{ alert.message }}
<gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
</gl-alert>
</template>
Loading
Loading
@@ -3,7 +3,7 @@ import { GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
 
const badge = {
const BADGE_DATA = {
[INSTANCE_TYPE]: {
variant: 'success',
text: s__('Runners|shared'),
Loading
Loading
@@ -25,21 +25,22 @@ export default {
props: {
type: {
type: String,
required: true,
required: false,
default: null,
validator(type) {
return Boolean(BADGE_DATA[type]);
},
},
},
computed: {
variant() {
return badge[this.type]?.variant;
},
text() {
return badge[this.type]?.text;
badge() {
return BADGE_DATA[this.type];
},
},
};
</script>
<template>
<gl-badge v-if="text" :variant="variant" v-bind="$attrs">
{{ text }}
<gl-badge v-if="badge" :variant="badge.variant" v-bind="$attrs">
{{ badge.text }}
</gl-badge>
</template>
<script>
import {
GlButton,
GlForm,
GlFormCheckbox,
GlFormGroup,
GlFormInputGroup,
GlTooltipDirective,
} from '@gitlab/ui';
import createFlash, { FLASH_TYPES } from '~/flash';
import { __ } from '~/locale';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
import runnerUpdateMutation from '../graphql/runner_update.mutation.graphql';
const runnerToModel = (runner) => {
const {
id,
description,
maximumTimeout,
accessLevel,
active,
locked,
runUntagged,
tagList = [],
} = runner || {};
return {
id,
description,
maximumTimeout,
accessLevel,
active,
locked,
runUntagged,
tagList: tagList.join(', '),
};
};
export default {
components: {
GlButton,
GlForm,
GlFormCheckbox,
GlFormGroup,
GlFormInputGroup,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runner: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
saving: false,
model: runnerToModel(this.runner),
};
},
computed: {
canBeLockedToProject() {
return this.runner?.runnerType === PROJECT_TYPE;
},
readonlyIpAddress() {
return this.runner?.ipAddress;
},
updateMutationInput() {
const { maximumTimeout, tagList } = this.model;
return {
...this.model,
maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null,
tagList: tagList
.split(',')
.map((tag) => tag.trim())
.filter((tag) => Boolean(tag)),
};
},
},
watch: {
runner(newVal, oldVal) {
if (oldVal === null) {
this.model = runnerToModel(newVal);
}
},
},
methods: {
async onSubmit() {
this.saving = true;
try {
const {
data: {
runnerUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: runnerUpdateMutation,
variables: {
input: this.updateMutationInput,
},
});
if (errors?.length) {
this.onError(new Error(errors[0]));
return;
}
this.onSuccess();
} catch (e) {
this.onError(e);
} finally {
this.saving = false;
}
},
onError(error) {
const { message } = error;
createFlash({ message });
},
onSuccess() {
createFlash({ message: __('Changes saved.'), type: FLASH_TYPES.SUCCESS });
this.model = runnerToModel(this.runner);
},
},
ACCESS_LEVEL_NOT_PROTECTED,
ACCESS_LEVEL_REF_PROTECTED,
};
</script>
<template>
<gl-form @submit.prevent="onSubmit">
<gl-form-checkbox
v-model="model.active"
data-testid="runner-field-paused"
:value="false"
:unchecked-value="true"
>
{{ __('Paused') }}
<template #help>
{{ __("Paused runners don't accept new jobs") }}
</template>
</gl-form-checkbox>
<gl-form-checkbox
v-model="model.accessLevel"
data-testid="runner-field-protected"
:value="$options.ACCESS_LEVEL_REF_PROTECTED"
:unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED"
>
{{ __('Protected') }}
<template #help>
{{ __('This runner will only run on pipelines triggered on protected branches') }}
</template>
</gl-form-checkbox>
<gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged">
{{ __('Run untagged jobs') }}
<template #help>
{{ __('Indicates whether this runner can pick jobs without tags') }}
</template>
</gl-form-checkbox>
<gl-form-checkbox
v-model="model.locked"
data-testid="runner-field-locked"
:disabled="!canBeLockedToProject"
>
{{ __('Lock to current projects') }}
<template #help>
{{ __('When a runner is locked, it cannot be assigned to other projects') }}
</template>
</gl-form-checkbox>
<gl-form-group :label="__('IP Address')" data-testid="runner-field-ip-address">
<gl-form-input-group :value="readonlyIpAddress" readonly select-on-click>
<template #append>
<gl-button
v-gl-tooltip.hover
:title="__('Copy IP Address')"
:aria-label="__('Copy IP Address')"
:data-clipboard-text="readonlyIpAddress"
icon="copy-to-clipboard"
class="d-inline-flex"
/>
</template>
</gl-form-input-group>
</gl-form-group>
<gl-form-group :label="__('Description')" data-testid="runner-field-description">
<gl-form-input-group v-model="model.description" />
</gl-form-group>
<gl-form-group
data-testid="runner-field-max-timeout"
:label="__('Maximum job timeout')"
:description="
s__(
'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.',
)
"
>
<gl-form-input-group v-model.number="model.maximumTimeout" type="number" />
</gl-form-group>
<gl-form-group
data-testid="runner-field-tags"
:label="__('Tags')"
:description="
__('You can set up jobs to only use runners with specific tags. Separate tags with commas.')
"
>
<gl-form-input-group v-model="model.tagList" />
</gl-form-group>
<div class="form-actions">
<gl-button
type="submit"
variant="confirm"
class="js-no-auto-disable"
:loading="saving || !runner"
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-form>
</template>
Loading
Loading
@@ -31,6 +31,11 @@ export const STATUS_ONLINE = 'ONLINE';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
 
// CiRunnerAccessLevel
export const ACCESS_LEVEL_NOT_PROTECTED = 'NOT_PROTECTED';
export const ACCESS_LEVEL_REF_PROTECTED = 'REF_PROTECTED';
// CiRunnerSort
 
export const CREATED_DESC = 'CREATED_DESC';
Loading
Loading
#import "~/runner/graphql/runner_details.fragment.graphql"
query getRunner($id: CiRunnerID!) {
runner(id: $id) {
id
runnerType
...RunnerDetails
}
}
fragment RunnerDetails on CiRunner {
id
runnerType
active
accessLevel
runUntagged
locked
ipAddress
description
maximumTimeout
tagList
}
#import "~/runner/graphql/runner_node.fragment.graphql"
#import "~/runner/graphql/runner_details.fragment.graphql"
 
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
runner {
...RunnerNode
...RunnerDetails
}
errors
}
Loading
Loading
<script>
import { convertToGraphQLId } from '~/graphql_shared/utils';
import RunnerTypeAlert from '../components/runner_type_alert.vue';
import RunnerTypeBadge from '../components/runner_type_badge.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
import { I18N_DETAILS_TITLE, RUNNER_ENTITY_TYPE } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
 
export default {
components: {
RunnerTypeAlert,
RunnerTypeBadge,
RunnerUpdateForm,
},
i18n: {
I18N_DETAILS_TITLE,
Loading
Loading
@@ -19,7 +23,7 @@ export default {
},
data() {
return {
runner: {},
runner: null,
};
},
apollo: {
Loading
Loading
@@ -35,9 +39,15 @@ export default {
};
</script>
<template>
<h2 class="page-title">
{{ sprintf($options.i18n.I18N_DETAILS_TITLE, { runner_id: runnerId }) }}
<div>
<h2 class="page-title">
{{ sprintf($options.i18n.I18N_DETAILS_TITLE, { runner_id: runnerId }) }}
 
<runner-type-badge v-if="runner.runnerType" :type="runner.runnerType" />
</h2>
<runner-type-badge v-if="runner" :type="runner.runnerType" />
</h2>
<runner-type-alert v-if="runner" :type="runner.runnerType" />
<runner-update-form :runner="runner" class="gl-my-5" />
</div>
</template>
Loading
Loading
@@ -10,11 +10,9 @@
%h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
= render 'shared/runners/runner_type_badge', runner: @runner
= render 'shared/runners/runner_type_alert', runner: @runner
.gl-mb-6
= render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner), in_gitlab_com_admin_context: Gitlab.com?
= render 'shared/runners/runner_type_alert', runner: @runner
.gl-mb-6
= render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner), in_gitlab_com_admin_context: Gitlab.com?
 
.row
.col-md-6
Loading
Loading
Loading
Loading
@@ -8949,6 +8949,9 @@ msgstr ""
msgid "Copy ID"
msgstr ""
 
msgid "Copy IP Address"
msgstr ""
msgid "Copy KRB5 clone URL"
msgstr ""
 
Loading
Loading
@@ -28159,6 +28162,9 @@ msgstr ""
msgid "Runners|Download latest binary"
msgstr ""
 
msgid "Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project."
msgstr ""
msgid "Runners|IP Address"
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -54,6 +54,10 @@
describe '#show' do
render_views
 
before do
stub_feature_flags(runner_detailed_view_vue_ui: false)
end
let_it_be(:project) { create(:project) }
let_it_be(:project_two) { create(:project) }
 
Loading
Loading
Loading
Loading
@@ -58,7 +58,11 @@
describe GraphQL::Query, type: :request do
get_runner_query_name = 'get_runner.query.graphql'
 
let_it_be(:query) { get_graphql_query_as_string("#{query_path}#{get_runner_query_name}") }
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runner_query_name}", [
'runner/graphql/runner_details.fragment.graphql'
])
end
 
it "#{fixtures_path}#{get_runner_query_name}.json" do
post_graphql(query, current_user: admin, variables: {
Loading
Loading
Loading
Loading
@@ -4,7 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
 
const mockId = '1';
 
Loading
Loading
@@ -101,7 +101,7 @@ describe('RunnerTypeCell', () => {
it(`The apollo mutation to set active to ${newActiveValue} is called`, () => {
expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith({
mutation: updateRunnerMutation,
mutation: runnerUpdateMutation,
variables: {
input: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
Loading
Loading
import { GlAlert, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeAlert from '~/runner/components/runner_type_alert.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
describe('RunnerTypeAlert', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTypeAlert, {
propsData: {
type: INSTANCE_TYPE,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
type | exampleText | anchor | variant
${INSTANCE_TYPE} | ${'Shared runners are available to every project'} | ${'#shared-runners'} | ${'success'}
${GROUP_TYPE} | ${'Use Group runners when you want all projects in a group'} | ${'#group-runners'} | ${'success'}
${PROJECT_TYPE} | ${'You can set up a specific runner to be used by multiple projects'} | ${'#specific-runners'} | ${'info'}
`('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
it('Describes runner type', () => {
expect(wrapper.text()).toMatch(exampleText);
});
it(`Shows a ${variant} variant`, () => {
expect(findAlert().props('variant')).toBe(variant);
});
it(`Links to anchor "${anchor}"`, () => {
expect(findLink().attributes('href')).toBe(`/help/ci/runners/runners_scope${anchor}`);
});
});
describe('When runner type is not correct', () => {
it('Does not render content when type is missing', () => {
createComponent({ props: { type: undefined } });
expect(wrapper.html()).toBe('');
});
it('Validation fails for an incorrect type', () => {
expect(() => {
createComponent({ props: { type: 'NOT_A_TYPE' } });
}).toThrow();
});
});
});
Loading
Loading
@@ -32,8 +32,14 @@ describe('RunnerTypeBadge', () => {
expect(findBadge().props('variant')).toBe(variant);
});
 
it('does not display a badge when type is unknown', () => {
createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
it('validation fails for an incorrect type', () => {
expect(() => {
createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
}).toThrow();
});
it('does not render content when type is missing', () => {
createComponent({ props: { type: undefined } });
 
expect(findBadge().exists()).toBe(false);
});
Loading
Loading
import { GlForm } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash, { FLASH_TYPES } from '~/flash';
import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
ACCESS_LEVEL_REF_PROTECTED,
ACCESS_LEVEL_NOT_PROTECTED,
} from '~/runner/constants';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
import { runnerData } from '../mock_data';
jest.mock('~/flash');
const mockRunner = runnerData.data.runner;
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('RunnerUpdateForm', () => {
let wrapper;
let runnerUpdateHandler;
const findForm = () => wrapper.findComponent(GlForm);
const findPausedCheckbox = () => wrapper.findByTestId('runner-field-paused');
const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected');
const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged');
const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked');
const findIpInput = () => wrapper.findByTestId('runner-field-ip-address').find('input');
const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input');
const findMaxJobTimeoutInput = () =>
wrapper.findByTestId('runner-field-max-timeout').find('input');
const findTagsInput = () => wrapper.findByTestId('runner-field-tags').find('input');
const findSubmit = () => wrapper.find('[type="submit"]');
const findSubmitDisabledAttr = () => findSubmit().attributes('disabled');
const submitForm = () => findForm().trigger('submit');
const submitFormAndWait = () => submitForm().then(waitForPromises);
const getFieldsModel = () => ({
active: !findPausedCheckbox().element.checked,
accessLevel: findProtectedCheckbox().element.checked
? ACCESS_LEVEL_REF_PROTECTED
: ACCESS_LEVEL_NOT_PROTECTED,
runUntagged: findRunUntaggedCheckbox().element.checked,
locked: findLockedCheckbox().element.checked,
ipAddress: findIpInput().element.value,
maximumTimeout: findMaxJobTimeoutInput().element.value || null,
tagList: findTagsInput().element.value.split(',').filter(Boolean),
});
const createComponent = ({ props } = {}) => {
wrapper = extendedWrapper(
mount(RunnerUpdateForm, {
localVue,
propsData: {
runner: mockRunner,
...props,
},
apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]),
}),
);
};
const expectToHaveSubmittedRunnerContaining = (submittedRunner) => {
expect(runnerUpdateHandler).toHaveBeenCalledTimes(1);
expect(runnerUpdateHandler).toHaveBeenCalledWith({
input: expect.objectContaining(submittedRunner),
});
expect(createFlash).toHaveBeenLastCalledWith({
message: expect.stringContaining('saved'),
type: FLASH_TYPES.SUCCESS,
});
expect(findSubmitDisabledAttr()).toBeUndefined();
};
beforeEach(() => {
runnerUpdateHandler = jest.fn().mockImplementation(({ input }) => {
return Promise.resolve({
data: {
runnerUpdate: {
runner: {
...mockRunner,
...input,
},
errors: [],
},
},
});
});
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('Form has a submit button', () => {
expect(findSubmit().exists()).toBe(true);
});
it('Form fields match data', () => {
expect(mockRunner).toMatchObject(getFieldsModel());
});
it('Form prevent multiple submissions', async () => {
await submitForm();
expect(findSubmitDisabledAttr()).toBe('disabled');
});
it('Updates runner with no changes', async () => {
await submitFormAndWait();
// Some fields are not submitted
const { ipAddress, runnerType, ...submitted } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
describe('When data is being loaded', () => {
beforeEach(() => {
createComponent({ props: { runner: null } });
});
it('Form cannot be submitted', () => {
expect(findSubmit().props('loading')).toBe(true);
});
it('Form is updated when data loads', async () => {
wrapper.setProps({
runner: mockRunner,
});
await nextTick();
expect(mockRunner).toMatchObject(getFieldsModel());
});
});
it.each`
runnerType | attrDisabled | outcome
${INSTANCE_TYPE} | ${'disabled'} | ${'disabled'}
${GROUP_TYPE} | ${'disabled'} | ${'disabled'}
${PROJECT_TYPE} | ${undefined} | ${'enabled'}
`(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, attrDisabled }) => {
const runner = { ...mockRunner, runnerType };
createComponent({ props: { runner } });
expect(findLockedCheckbox().attributes('disabled')).toBe(attrDisabled);
});
describe('On submit, runner gets updated', () => {
it.each`
test | initialValue | findCheckbox | checked | submitted
${'pauses'} | ${{ active: true }} | ${findPausedCheckbox} | ${true} | ${{ active: false }}
${'activates'} | ${{ active: false }} | ${findPausedCheckbox} | ${false} | ${{ active: true }}
${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }}
${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }}
${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }}
${'"runs tagged jobs"'} | ${{ runUntagged: false }} | ${findRunUntaggedCheckbox} | ${true} | ${{ runUntagged: true }}
${'locks'} | ${{ runnerType: PROJECT_TYPE, locked: true }} | ${findLockedCheckbox} | ${false} | ${{ locked: false }}
${'unlocks'} | ${{ runnerType: PROJECT_TYPE, locked: false }} | ${findLockedCheckbox} | ${true} | ${{ locked: true }}
`('Checkbox $test runner', async ({ initialValue, findCheckbox, checked, submitted }) => {
const runner = { ...mockRunner, ...initialValue };
createComponent({ props: { runner } });
await findCheckbox().setChecked(checked);
await submitFormAndWait();
expectToHaveSubmittedRunnerContaining({
id: runner.id,
...submitted,
});
});
it.each`
test | initialValue | findInput | value | submitted
${'description'} | ${{ description: 'Desc. 1' }} | ${findDescriptionInput} | ${'Desc. 2'} | ${{ description: 'Desc. 2' }}
${'max timeout'} | ${{ maximumTimeout: 36000 }} | ${findMaxJobTimeoutInput} | ${'40000'} | ${{ maximumTimeout: 40000 }}
${'tags'} | ${{ tagList: ['tag1'] }} | ${findTagsInput} | ${'tag2, tag3'} | ${{ tagList: ['tag2', 'tag3'] }}
`("Field updates runner's $test", async ({ initialValue, findInput, value, submitted }) => {
const runner = { ...mockRunner, ...initialValue };
createComponent({ props: { runner } });
await findInput().setValue(value);
await submitFormAndWait();
expectToHaveSubmittedRunnerContaining({
id: runner.id,
...submitted,
});
});
it.each`
value | submitted
${''} | ${{ tagList: [] }}
${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }}
${'with spaces'} | ${{ tagList: ['with spaces'] }}
${',,,,, commas'} | ${{ tagList: ['commas'] }}
${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }}
${' trimmed , trimmed2 '} | ${{ tagList: ['trimmed', 'trimmed2'] }}
`('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => {
const runner = { ...mockRunner, tagList: ['tag1'] };
createComponent({ props: { runner } });
await findTagsInput().setValue(value);
await submitFormAndWait();
expectToHaveSubmittedRunnerContaining({
id: runner.id,
...submitted,
});
});
});
describe('On error', () => {
beforeEach(() => {
createComponent();
});
it('On network error, error message is shown', async () => {
runnerUpdateHandler.mockRejectedValue(new Error('Something went wrong'));
await submitFormAndWait();
expect(createFlash).toHaveBeenLastCalledWith({
message: 'Network error: Something went wrong',
});
expect(findSubmitDisabledAttr()).toBeUndefined();
});
it('On validation error, error message is shown', async () => {
runnerUpdateHandler.mockResolvedValue({
data: {
runnerUpdate: {
runner: mockRunner,
errors: ['A value is invalid'],
},
},
});
await submitFormAndWait();
expect(createFlash).toHaveBeenLastCalledWith({
message: 'A value is invalid',
});
expect(findSubmitDisabledAttr()).toBeUndefined();
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment