272 lines
9.7 KiB
JavaScript
272 lines
9.7 KiB
JavaScript
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 { createAlert, VARIANT_SUCCESS } 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 { captureException } from '~/runner/sentry_utils';
|
|
import { runnerData } from '../mock_data';
|
|
|
|
jest.mock('~/flash');
|
|
jest.mock('~/runner/sentry_utils');
|
|
|
|
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 || false,
|
|
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(createAlert).toHaveBeenLastCalledWith({
|
|
message: expect.stringContaining('saved'),
|
|
variant: VARIANT_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, createdAt, status, ...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 | exists | outcome
|
|
${INSTANCE_TYPE} | ${false} | ${'hidden'}
|
|
${GROUP_TYPE} | ${false} | ${'hidden'}
|
|
${PROJECT_TYPE} | ${true} | ${'shown'}
|
|
`(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, exists }) => {
|
|
const runner = { ...mockRunner, runnerType };
|
|
createComponent({ props: { runner } });
|
|
|
|
expect(findLockedCheckbox().exists()).toBe(exists);
|
|
});
|
|
|
|
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'] }}
|
|
${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }}
|
|
`('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 () => {
|
|
const mockErrorMsg = 'Update error!';
|
|
|
|
runnerUpdateHandler.mockRejectedValue(new Error(mockErrorMsg));
|
|
|
|
await submitFormAndWait();
|
|
|
|
expect(createAlert).toHaveBeenLastCalledWith({
|
|
message: `Network error: ${mockErrorMsg}`,
|
|
});
|
|
expect(captureException).toHaveBeenCalledWith({
|
|
component: 'RunnerUpdateForm',
|
|
error: new Error(`Network error: ${mockErrorMsg}`),
|
|
});
|
|
expect(findSubmitDisabledAttr()).toBeUndefined();
|
|
});
|
|
|
|
it('On validation error, error message is shown and it is not sent to sentry', async () => {
|
|
const mockErrorMsg = 'Invalid value!';
|
|
|
|
runnerUpdateHandler.mockResolvedValue({
|
|
data: {
|
|
runnerUpdate: {
|
|
runner: mockRunner,
|
|
errors: [mockErrorMsg],
|
|
},
|
|
},
|
|
});
|
|
|
|
await submitFormAndWait();
|
|
|
|
expect(createAlert).toHaveBeenLastCalledWith({
|
|
message: mockErrorMsg,
|
|
});
|
|
expect(captureException).not.toHaveBeenCalled();
|
|
expect(findSubmitDisabledAttr()).toBeUndefined();
|
|
});
|
|
});
|
|
});
|