Skip to content
Snippets Groups Projects
Commit 15a639d9 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas :palm_tree:
Browse files

Merge branch 'run-pipeline-graphql' into 'master'

Pre-fill Run Pipeline form with predefined variables

See merge request !96542



Merged-by: Jose Ivan Vargas's avatarJose Ivan Vargas <jvargas@gitlab.com>
Approved-by: Jose Ivan Vargas's avatarJose Ivan Vargas <jvargas@gitlab.com>
Co-authored-by: default avatarmgandres <mandres@gitlab.com>
parents d0729870 297b1595
No related branches found
No related tags found
1 merge request!96542Pre-fill Run Pipeline form with predefined variables
Pipeline #666144271 passed
......@@ -17,17 +17,11 @@ import {
import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
import {
VARIABLE_TYPE,
FILE_TYPE,
CONFIG_VARIABLES_TIMEOUT,
CC_VALIDATION_REQUIRED_ERROR,
} from '../constants';
import { VARIABLE_TYPE, FILE_TYPE, CC_VALIDATION_REQUIRED_ERROR } from '../constants';
import createPipelineMutation from '../graphql/mutations/create_pipeline.mutation.graphql';
import ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql';
import filterVariables from '../utils/filter_variables';
import RefsDropdown from './refs_dropdown.vue';
......@@ -76,10 +70,6 @@ export default {
type: String,
required: true,
},
configVariablesPath: {
type: String,
required: true,
},
defaultBranch: {
type: String,
required: true,
......@@ -97,6 +87,10 @@ export default {
required: false,
default: () => ({}),
},
projectPath: {
type: String,
required: true,
},
refParam: {
type: String,
required: false,
......@@ -116,19 +110,77 @@ export default {
return {
refValue: {
shortName: this.refParam,
// this is needed until we add support for ref type in url query strings
// ensure default branch is called with full ref on load
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
},
form: {},
errorTitle: null,
error: null,
predefinedValueOptions: {},
warnings: [],
totalWarnings: 0,
isWarningDismissed: false,
isLoading: false,
submitted: false,
ccAlertDismissed: false,
};
},
apollo: {
ciConfigVariables: {
query: ciConfigVariablesQuery,
// Skip when variables already cached in `form`
skip() {
return Object.keys(this.form).includes(this.refFullName);
},
variables() {
return {
fullPath: this.projectPath,
ref: this.refQueryParam,
};
},
update({ project }) {
return project?.ciConfigVariables || [];
},
result({ data }) {
const predefinedVars = data?.project?.ciConfigVariables || [];
const params = {};
const descriptions = {};
predefinedVars.forEach(({ description, key, value, valueOptions }) => {
if (description) {
params[key] = value;
descriptions[key] = description;
this.predefinedValueOptions[key] = valueOptions;
}
});
Vue.set(this.form, this.refFullName, { descriptions, variables: [] });
// Add default variables from yml
this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
// Add/update variables, e.g. from query string
if (this.variableParams) {
this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
}
// Adds empty var at the end of the form
this.addEmptyVariable(this.refFullName);
},
error(error) {
Sentry.captureException(error);
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.ciConfigVariables.loading;
},
overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings;
},
......@@ -147,6 +199,9 @@ export default {
refFullName() {
return this.refValue.fullName;
},
refQueryParam() {
return this.refFullName || this.refShortName;
},
variables() {
return this.form[this.refFullName]?.variables ?? [];
},
......@@ -157,21 +212,6 @@ export default {
return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
},
},
watch: {
refValue() {
this.loadConfigVariablesForm();
},
},
created() {
// this is needed until we add support for ref type in url query strings
// ensure default branch is called with full ref on load
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
if (this.refValue.shortName === this.defaultBranch) {
this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
}
this.loadConfigVariablesForm();
},
methods: {
addEmptyVariable(refValue) {
const { variables } = this.form[refValue];
......@@ -204,132 +244,57 @@ export default {
});
}
},
setVariableType(key, type) {
setVariableAttribute(key, attribute, value) {
const { variables } = this.form[this.refFullName];
const variable = variables.find((v) => v.key === key);
variable.variable_type = type;
variable[attribute] = value;
},
setVariableParams(refValue, type, paramsObj) {
Object.entries(paramsObj).forEach(([key, value]) => {
this.setVariable(refValue, type, key, value);
});
},
shouldShowValuesDropdown(key) {
return this.predefinedValueOptions[key]?.length > 1;
},
removeVariable(index) {
this.variables.splice(index, 1);
},
canRemove(index) {
return index < this.variables.length - 1;
},
loadConfigVariablesForm() {
// Skip when variables already cached in `form`
if (this.form[this.refFullName]) {
return;
}
this.fetchConfigVariables(this.refFullName || this.refShortName)
.then(({ descriptions, params }) => {
Vue.set(this.form, this.refFullName, {
variables: [],
descriptions,
});
// Add default variables from yml
this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
})
.catch(() => {
Vue.set(this.form, this.refFullName, {
variables: [],
descriptions: {},
});
})
.finally(() => {
// Add/update variables, e.g. from query string
if (this.variableParams) {
this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
}
// Adds empty var at the end of the form
this.addEmptyVariable(this.refFullName);
});
},
fetchConfigVariables(refValue) {
this.isLoading = true;
return backOff((next, stop) => {
axios
.get(this.configVariablesPath, {
params: {
sha: refValue,
},
})
.then(({ data, status }) => {
if (status === httpStatusCodes.NO_CONTENT) {
next();
} else {
this.isLoading = false;
stop(data);
}
})
.catch((error) => {
stop(error);
});
}, CONFIG_VARIABLES_TIMEOUT)
.then((data) => {
const params = {};
const descriptions = {};
Object.entries(data).forEach(([key, { value, description }]) => {
if (description) {
params[key] = value;
descriptions[key] = description;
}
});
return { params, descriptions };
})
.catch((error) => {
this.isLoading = false;
Sentry.captureException(error);
return { params: {}, descriptions: {} };
});
},
createPipeline() {
async createPipeline() {
this.submitted = true;
this.ccAlertDismissed = false;
return axios
.post(this.pipelinesPath, {
const { data } = await this.$apollo.mutate({
mutation: createPipelineMutation,
variables: {
endpoint: this.pipelinesPath,
// send shortName as fall back for query params
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
ref: this.refValue.fullName || this.refShortName,
variables_attributes: filterVariables(this.variables),
})
.then(({ data }) => {
redirectTo(`${this.pipelinesPath}/${data.id}`);
})
.catch((err) => {
// always re-enable submit button
this.submitted = false;
ref: this.refQueryParam,
variablesAttributes: filterVariables(this.variables),
},
});
const {
errors = [],
warnings = [],
total_warnings: totalWarnings = 0,
} = err.response.data;
const [error] = errors;
const { id, errors, totalWarnings, warnings } = data.createPipeline;
this.reportError({
title: i18n.submitErrorTitle,
error,
warnings,
totalWarnings,
});
});
if (id) {
redirectTo(`${this.pipelinesPath}/${id}`);
return;
}
// always re-enable submit button
this.submitted = false;
const [error] = errors;
this.reportError({
title: i18n.submitErrorTitle,
error,
warnings,
totalWarnings,
});
},
onRefsLoadingError(error) {
this.reportError({ title: i18n.refsLoadingErrorTitle });
......@@ -416,7 +381,7 @@ export default {
<gl-dropdown-item
v-for="type in Object.keys($options.typeOptions)"
:key="type"
@click="setVariableType(variable.key, type)"
@click="setVariableAttribute(variable.key, 'variable_type', type)"
>
{{ $options.typeOptions[type] }}
</gl-dropdown-item>
......@@ -429,7 +394,24 @@ export default {
data-qa-selector="ci_variable_key_field"
@change="addEmptyVariable(refFullName)"
/>
<gl-dropdown
v-if="shouldShowValuesDropdown(variable.key)"
:text="variable.value"
:class="$options.formElementClasses"
class="gl-flex-grow-1 gl-mr-0!"
data-testid="pipeline-form-ci-variable-value-dropdown"
>
<gl-dropdown-item
v-for="value in predefinedValueOptions[variable.key]"
:key="value"
data-testid="pipeline-form-ci-variable-value-dropdown-items"
@click="setVariableAttribute(variable.key, 'value', value)"
>
{{ value }}
</gl-dropdown-item>
</gl-dropdown>
<gl-form-textarea
v-else
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3"
......
mutation createPipeline($endpoint: String, $ref: String, $variablesAttributes: Array) {
createPipeline(endpoint: $endpoint, ref: $ref, variablesAttributes: $variablesAttributes)
@client {
id
errors
totalWarnings
warnings
}
}
query ciConfigVariables($fullPath: ID!, $ref: String!) {
project(fullPath: $fullPath) {
id
ciConfigVariables(sha: $ref) {
description
key
value
valueOptions
}
}
}
import axios from '~/lib/utils/axios_utils';
export const resolvers = {
Mutation: {
createPipeline: (_, { endpoint, ref, variablesAttributes }) => {
return axios
.post(endpoint, { ref, variables_attributes: variablesAttributes })
.then((response) => {
const { id } = response.data;
return {
id,
errors: [],
totalWarnings: 0,
warnings: [],
};
})
.catch((err) => {
const { errors = [], totalWarnings = 0, warnings = [] } = err.response.data;
return {
id: null,
errors,
totalWarnings,
warnings,
};
});
},
},
};
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
import { resolvers } from './graphql/resolvers';
const mountLegacyPipelineNewForm = (el) => {
const {
......@@ -51,12 +54,12 @@ const mountPipelineNewForm = (el) => {
projectRefsEndpoint,
// props
configVariablesPath,
defaultBranch,
fileParam,
maxWarnings,
pipelinesPath,
projectId,
projectPath,
refParam,
settingsLink,
varParam,
......@@ -65,22 +68,27 @@ const mountPipelineNewForm = (el) => {
const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam);
// TODO: add apolloProvider
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers),
});
return new Vue({
el,
apolloProvider,
provide: {
projectRefsEndpoint,
},
render(createElement) {
return createElement(PipelineNewForm, {
props: {
configVariablesPath,
defaultBranch,
fileParams,
maxWarnings: Number(maxWarnings),
pipelinesPath,
projectId,
projectPath,
refParam,
settingsLink,
variableParams,
......
......@@ -12,6 +12,7 @@
ref_param: params[:ref] || @project.default_branch,
var_param: params[:var].to_json,
file_param: params[:file_var].to_json,
project_path: @project.full_path,
project_refs_endpoint: refs_project_path(@project, sort: 'updated_desc'),
settings_link: project_settings_ci_cd_path(@project),
max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlForm, GlDropdownItem, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import ciConfigVariablesQuery from '~/pipeline_new/graphql/queries/ci_config_variables.graphql';
import { resolvers } from '~/pipeline_new/graphql/resolvers';
import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
import {
mockCreditCardValidationRequiredError,
mockCiConfigVariablesResponse,
mockCiConfigVariablesResponseWithoutDesc,
mockEmptyCiConfigVariablesResponse,
mockError,
mockQueryParams,
mockPostParams,
mockProjectId,
mockError,
mockRefs,
mockCreditCardValidationRequiredError,
mockYamlVariables,
} from '../mock_data';
Vue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines';
const configVariablesPath = '/root/project/-/pipelines/config_variables';
const projectPath = '/root/project/-/pipelines/config_variables';
const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'main';
describe('Pipeline New Form', () => {
let wrapper;
let mock;
let mockApollo;
let mockCiConfigVariables;
let dummySubmitEvent;
const findForm = () => wrapper.findComponent(GlForm);
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
const findSubmitButton = () => wrapper.findByTestId('run_pipeline_button');
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
const findVariableTypes = () => wrapper.findAllByTestId('pipeline-form-ci-variable-type');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
const findValueDropdowns = () =>
wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert');
const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert');
const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
const selectBranch = (branch) => {
const selectBranch = async (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
shortName: branch,
fullName: `refs/heads/${branch}`,
});
await waitForPromises();
};
const changeKeyInputValue = async (keyInputIndex, value) => {
const input = findKeyInputs().at(keyInputIndex);
input.element.value = value;
input.trigger('change');
await nextTick();
};
const createComponent = (props = {}, method = shallowMount) => {
const createComponentWithApollo = ({ method = shallowMountExtended, props = {} } = {}) => {
const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
mockApollo = createMockApollo(handlers, resolvers);
wrapper = method(PipelineNewForm, {
apolloProvider: mockApollo,
provide: {
projectRefsEndpoint,
},
propsData: {
projectId: mockProjectId,
pipelinesPath,
configVariablesPath,
projectPath,
defaultBranch,
refParam: defaultBranch,
settingsLink: '',
......@@ -78,7 +107,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
mockCiConfigVariables = jest.fn();
mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
dummySubmitEvent = {
......@@ -87,24 +116,20 @@ describe('Pipeline New Form', () => {
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
wrapper.destroy();
});
describe('Form', () => {
beforeEach(async () => {
createComponent(mockQueryParams, mount);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
await waitForPromises();
});
it('displays the correct values for the provided query params', async () => {
expect(findDropdowns().at(0).props('text')).toBe('Variable');
expect(findDropdowns().at(1).props('text')).toBe('File');
expect(findVariableTypes().at(0).props('text')).toBe('Variable');
expect(findVariableTypes().at(1).props('text')).toBe('File');
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
expect(findVariableRows()).toHaveLength(3);
});
......@@ -117,7 +142,7 @@ describe('Pipeline New Form', () => {
it('displays an empty variable for the user to fill out', async () => {
expect(findKeyInputs().at(2).element.value).toBe('');
expect(findValueInputs().at(2).element.value).toBe('');
expect(findDropdowns().at(2).props('text')).toBe('Variable');
expect(findVariableTypes().at(2).props('text')).toBe('Variable');
});
it('does not display remove icon for last row', () => {
......@@ -147,13 +172,12 @@ describe('Pipeline New Form', () => {
describe('Pipeline creation', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
await waitForPromises();
});
it('does not submit the native HTML form', async () => {
createComponent();
createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
......@@ -161,7 +185,7 @@ describe('Pipeline New Form', () => {
});
it('disables the submit button immediately after submitting', async () => {
createComponent();
createComponentWithApollo();
expect(findSubmitButton().props('disabled')).toBe(false);
......@@ -172,7 +196,7 @@ describe('Pipeline New Form', () => {
});
it('creates pipeline with full ref and variables', async () => {
createComponent();
createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
await waitForPromises();
......@@ -182,7 +206,7 @@ describe('Pipeline New Form', () => {
});
it('creates a pipeline with short ref and variables from the query params', async () => {
createComponent(mockQueryParams);
createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
......@@ -197,64 +221,51 @@ describe('Pipeline New Form', () => {
describe('When the ref has been changed', () => {
beforeEach(async () => {
createComponent({}, mount);
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
it('variables persist between ref changes', async () => {
selectBranch('main');
await waitForPromises();
const mainInput = findKeyInputs().at(0);
mainInput.element.value = 'build_var';
mainInput.trigger('change');
it('variables persist between ref changes', async () => {
await selectBranch('main');
await changeKeyInputValue(0, 'build_var');
await nextTick();
await selectBranch('branch-1');
await changeKeyInputValue(0, 'deploy_var');
selectBranch('branch-1');
await selectBranch('main');
await waitForPromises();
expect(findKeyInputs().at(0).element.value).toBe('build_var');
expect(findVariableRows().length).toBe(2);
const branchOneInput = findKeyInputs().at(0);
branchOneInput.element.value = 'deploy_var';
branchOneInput.trigger('change');
await selectBranch('branch-1');
await nextTick();
expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
expect(findVariableRows().length).toBe(2);
});
selectBranch('main');
it('skips query call when form variables are already cached', async () => {
await selectBranch('main');
await changeKeyInputValue(0, 'build_var');
await waitForPromises();
expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
expect(findKeyInputs().at(0).element.value).toBe('build_var');
expect(findVariableRows().length).toBe(2);
await selectBranch('branch-1');
selectBranch('branch-1');
expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
await waitForPromises();
// no additional call since `main` form values have been cached
await selectBranch('main');
expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
expect(findVariableRows().length).toBe(2);
expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
});
});
describe('when yml defines a variable', () => {
const mockYmlKey = 'yml_var';
const mockYmlValue = 'yml_var_val';
const mockYmlMultiLineValue = `A value
with multiple
lines`;
const mockYmlDesc = 'A var from yml.';
it('loading icon is shown when content is requested and hidden when received', async () => {
createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: {
value: mockYmlValue,
description: mockYmlDesc,
},
});
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
expect(findLoadingIcon().exists()).toBe(true);
......@@ -263,51 +274,62 @@ describe('Pipeline New Form', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('multi-line strings are added to the value field without removing line breaks', async () => {
createComponent(mockQueryParams, mount);
describe('with different predefined values', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
it('multi-line strings are added to the value field without removing line breaks', () => {
expect(findValueInputs().at(1).element.value).toBe(mockYamlVariables[1].value);
});
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: {
value: mockYmlMultiLineValue,
description: mockYmlDesc,
},
it('multiple predefined values are rendered as a dropdown', () => {
const dropdown = findValueDropdowns().at(0);
const dropdownItems = findValueDropdownItems(dropdown);
const { valueOptions } = mockYamlVariables[2];
expect(dropdownItems.at(0).text()).toBe(valueOptions[0]);
expect(dropdownItems.at(1).text()).toBe(valueOptions[1]);
expect(dropdownItems.at(2).text()).toBe(valueOptions[2]);
});
await waitForPromises();
it('variables with multiple predefined values sets the first option as the default', () => {
const dropdown = findValueDropdowns().at(0);
const { valueOptions } = mockYamlVariables[2];
expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
expect(dropdown.props('text')).toBe(valueOptions[0]);
});
});
describe('with description', () => {
beforeEach(async () => {
createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: {
value: mockYmlValue,
description: mockYmlDesc,
},
});
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
await waitForPromises();
});
it('displays all the variables', async () => {
expect(findVariableRows()).toHaveLength(4);
expect(findVariableRows()).toHaveLength(6);
});
it('displays a variable from yml', () => {
expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
expect(findKeyInputs().at(0).element.value).toBe(mockYamlVariables[0].key);
expect(findValueInputs().at(0).element.value).toBe(mockYamlVariables[0].value);
});
it('displays a variable from provided query params', () => {
expect(findKeyInputs().at(1).element.value).toBe('test_var');
expect(findValueInputs().at(1).element.value).toBe('test_var_val');
expect(findKeyInputs().at(3).element.value).toBe(
Object.keys(mockQueryParams.variableParams)[0],
);
expect(findValueInputs().at(3).element.value).toBe(
Object.values(mockQueryParams.fileParams)[0],
);
});
it('adds a description to the first variable from yml', () => {
expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
expect(findVariableRows().at(0).text()).toContain(mockYamlVariables[0].description);
});
it('removes the description when a variable key changes', async () => {
......@@ -316,39 +338,27 @@ describe('Pipeline New Form', () => {
await nextTick();
expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
expect(findVariableRows().at(0).text()).not.toContain(mockYamlVariables[0].description);
});
});
describe('without description', () => {
beforeEach(async () => {
createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: {
value: mockYmlValue,
description: null,
},
yml_var2: {
value: 'yml_var2_val',
},
yml_var3: {
description: '',
},
});
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponseWithoutDesc);
createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
it('displays all the variables', async () => {
expect(findVariableRows()).toHaveLength(3);
it('displays variables with description only', async () => {
expect(findVariableRows()).toHaveLength(2); // extra empty variable is added at the end
});
});
});
describe('Form errors and warnings', () => {
beforeEach(() => {
createComponent();
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
createComponentWithApollo();
});
describe('when the refs cannot be loaded', () => {
......
......@@ -65,3 +65,62 @@ export const mockVariables = [
},
{ uniqueId: 'var-refs/heads/main4', variable_type: 'env_var', key: '', value: '' },
];
export const mockYamlVariables = [
{
description: 'This is a variable with a value.',
key: 'VAR_WITH_VALUE',
value: 'test_value',
valueOptions: null,
},
{
description: 'This is a variable with a multi-line value.',
key: 'VAR_WITH_MULTILINE',
value: `this is
a multiline value`,
valueOptions: null,
},
{
description: 'This is a variable with predefined values.',
key: 'VAR_WITH_OPTIONS',
value: 'development',
valueOptions: ['development', 'staging', 'production'],
},
];
export const mockYamlVariablesWithoutDesc = [
{
description: 'This is a variable with a value.',
key: 'VAR_WITH_VALUE',
value: 'test_value',
valueOptions: null,
},
{
description: null,
key: 'VAR_WITH_MULTILINE',
value: `this is
a multiline value`,
valueOptions: null,
},
{
description: null,
key: 'VAR_WITH_OPTIONS',
value: 'development',
valueOptions: ['development', 'staging', 'production'],
},
];
export const mockCiConfigVariablesQueryResponse = (ciConfigVariables) => ({
data: {
project: {
id: 1,
ciConfigVariables,
},
},
});
export const mockCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(mockYamlVariables);
export const mockEmptyCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse([]);
export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQueryResponse(
mockYamlVariablesWithoutDesc,
);
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