diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-10 09:07:16 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-10 09:07:16 +0300 |
commit | 32c68185530d784ce557342e69d96af2f43b0dcd (patch) | |
tree | e5372123e2d9bd24a9c8106f1961ff0b57ef834c | |
parent | 1e5e685f4fa6745023da8691edc272e39804a50a (diff) |
Add latest changes from gitlab-org/gitlab@master
16 files changed, 115 insertions, 1240 deletions
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index 21c50acbf5d..f67519712cc 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -758,7 +758,17 @@ "type": "object", "properties": { "value": { - "type": "string" + "type": "string", + "markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#prefill-variables-in-manual-pipelines)" + }, + "options": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#configure-a-list-of-selectable-values-for-a-prefilled-variable)" }, "description": { "type": "string", @@ -1922,4 +1932,4 @@ "additionalProperties": false } } -} +}
\ No newline at end of file diff --git a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue deleted file mode 100644 index c3d46d1954a..00000000000 --- a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue +++ /dev/null @@ -1,490 +0,0 @@ -<script> -import { - GlAlert, - GlIcon, - GlButton, - GlDropdown, - GlDropdownItem, - GlForm, - GlFormGroup, - GlFormInput, - GlFormTextarea, - GlLink, - GlSprintf, - GlLoadingIcon, -} from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; -import { uniqueId } from 'lodash'; -import Vue from 'vue'; -import SafeHtml from '~/vue_shared/directives/safe_html'; -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 filterVariables from '../utils/filter_variables'; -import RefsDropdown from './refs_dropdown.vue'; - -const i18n = { - variablesDescription: s__( - 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.', - ), - defaultError: __('Something went wrong on our end. Please try again.'), - refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'), - submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'), - warningTitle: __('The form contains the following warning:'), - maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'), - removeVariableLabel: s__('CiVariables|Remove variable'), -}; - -export default { - typeOptions: { - [VARIABLE_TYPE]: __('Variable'), - [FILE_TYPE]: __('File'), - }, - i18n, - formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0', - // this height value is used inline on the textarea to match the input field height - // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used - textAreaStyle: { height: '32px' }, - components: { - GlAlert, - GlIcon, - GlButton, - GlDropdown, - GlDropdownItem, - GlForm, - GlFormGroup, - GlFormInput, - GlFormTextarea, - GlLink, - GlSprintf, - GlLoadingIcon, - RefsDropdown, - CcValidationRequiredAlert: () => - import('ee_component/billings/components/cc_validation_required_alert.vue'), - }, - directives: { SafeHtml }, - props: { - pipelinesPath: { - type: String, - required: true, - }, - configVariablesPath: { - type: String, - required: true, - }, - defaultBranch: { - type: String, - required: true, - }, - projectId: { - type: String, - required: true, - }, - settingsLink: { - type: String, - required: true, - }, - fileParams: { - type: Object, - required: false, - default: () => ({}), - }, - refParam: { - type: String, - required: false, - default: '', - }, - variableParams: { - type: Object, - required: false, - default: () => ({}), - }, - maxWarnings: { - type: Number, - required: true, - }, - }, - data() { - return { - refValue: { - shortName: this.refParam, - }, - form: {}, - errorTitle: null, - error: null, - warnings: [], - totalWarnings: 0, - isWarningDismissed: false, - isLoading: false, - submitted: false, - ccAlertDismissed: false, - }; - }, - computed: { - overMaxWarningsLimit() { - return this.totalWarnings > this.maxWarnings; - }, - warningsSummary() { - return n__('%d warning found:', '%d warnings found:', this.warnings.length); - }, - summaryMessage() { - return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary; - }, - shouldShowWarning() { - return this.warnings.length > 0 && !this.isWarningDismissed; - }, - refShortName() { - return this.refValue.shortName; - }, - refFullName() { - return this.refValue.fullName; - }, - variables() { - return this.form[this.refFullName]?.variables ?? []; - }, - descriptions() { - return this.form[this.refFullName]?.descriptions ?? {}; - }, - ccRequiredError() { - 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]; - - const lastVar = variables[variables.length - 1]; - if (lastVar?.key === '' && lastVar?.value === '') { - return; - } - - variables.push({ - uniqueId: uniqueId(`var-${refValue}`), - variable_type: VARIABLE_TYPE, - key: '', - value: '', - }); - }, - setVariable(refValue, type, key, value) { - const { variables } = this.form[refValue]; - - const variable = variables.find((v) => v.key === key); - if (variable) { - variable.type = type; - variable.value = value; - } else { - variables.push({ - uniqueId: uniqueId(`var-${refValue}`), - key, - value, - variable_type: type, - }); - } - }, - setVariableType(key, type) { - const { variables } = this.form[this.refFullName]; - const variable = variables.find((v) => v.key === key); - variable.variable_type = type; - }, - setVariableParams(refValue, type, paramsObj) { - Object.entries(paramsObj).forEach(([key, value]) => { - this.setVariable(refValue, type, key, value); - }); - }, - 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() { - this.submitted = true; - this.ccAlertDismissed = false; - - return axios - .post(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; - - const { - errors = [], - warnings = [], - total_warnings: totalWarnings = 0, - } = err.response.data; - const [error] = errors; - - this.reportError({ - title: i18n.submitErrorTitle, - error, - warnings, - totalWarnings, - }); - }); - }, - onRefsLoadingError(error) { - this.reportError({ title: i18n.refsLoadingErrorTitle }); - - Sentry.captureException(error); - }, - reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) { - this.errorTitle = title; - this.error = error; - this.warnings = warnings; - this.totalWarnings = totalWarnings; - }, - dismissError() { - this.ccAlertDismissed = true; - this.error = null; - }, - }, -}; -</script> - -<template> - <gl-form @submit.prevent="createPipeline"> - <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" /> - <gl-alert - v-else-if="error" - :title="errorTitle" - :dismissible="false" - variant="danger" - class="gl-mb-4" - data-testid="run-pipeline-error-alert" - > - <span v-safe-html="error"></span> - </gl-alert> - <gl-alert - v-if="shouldShowWarning" - :title="$options.i18n.warningTitle" - variant="warning" - class="gl-mb-4" - data-testid="run-pipeline-warning-alert" - @dismiss="isWarningDismissed = true" - > - <details> - <summary> - <gl-sprintf :message="summaryMessage"> - <template #total> - {{ totalWarnings }} - </template> - <template #warningsDisplayed> - {{ maxWarnings }} - </template> - </gl-sprintf> - </summary> - <p - v-for="(warning, index) in warnings" - :key="`warning-${index}`" - data-testid="run-pipeline-warning" - > - {{ warning }} - </p> - </details> - </gl-alert> - <gl-form-group :label="s__('Pipeline|Run for branch name or tag')"> - <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" /> - </gl-form-group> - - <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" /> - - <gl-form-group v-else :label="s__('Pipeline|Variables')"> - <div - v-for="(variable, index) in variables" - :key="variable.uniqueId" - class="gl-mb-3 gl-pb-2" - data-testid="ci-variable-row" - data-qa-selector="ci_variable_row_container" - > - <div - class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row" - > - <gl-dropdown - :text="$options.typeOptions[variable.variable_type]" - :class="$options.formElementClasses" - data-testid="pipeline-form-ci-variable-type" - > - <gl-dropdown-item - v-for="type in Object.keys($options.typeOptions)" - :key="type" - @click="setVariableType(variable.key, type)" - > - {{ $options.typeOptions[type] }} - </gl-dropdown-item> - </gl-dropdown> - <gl-form-input - v-model="variable.key" - :placeholder="s__('CiVariables|Input variable key')" - :class="$options.formElementClasses" - data-testid="pipeline-form-ci-variable-key" - data-qa-selector="ci_variable_key_field" - @change="addEmptyVariable(refFullName)" - /> - <gl-form-textarea - v-model="variable.value" - :placeholder="s__('CiVariables|Input variable value')" - class="gl-mb-3" - :style="$options.textAreaStyle" - :no-resize="false" - data-testid="pipeline-form-ci-variable-value" - data-qa-selector="ci_variable_value_field" - /> - - <template v-if="variables.length > 1"> - <gl-button - v-if="canRemove(index)" - class="gl-md-ml-3 gl-mb-3" - data-testid="remove-ci-variable-row" - variant="danger" - category="secondary" - :aria-label="$options.i18n.removeVariableLabel" - @click="removeVariable(index)" - > - <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" /> - <span class="gl-md-display-none">{{ $options.i18n.removeVariableLabel }}</span> - </gl-button> - <gl-button - v-else - class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden" - icon="clear" - :aria-label="$options.i18n.removeVariableLabel" - /> - </template> - </div> - <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3"> - {{ descriptions[variable.key] }} - </div> - </div> - - <template #description - ><gl-sprintf :message="$options.i18n.variablesDescription"> - <template #link="{ content }"> - <gl-link :href="settingsLink">{{ content }}</gl-link> - </template> - </gl-sprintf></template - > - </gl-form-group> - <div class="gl-pt-5 gl-display-flex"> - <gl-button - type="submit" - category="primary" - variant="confirm" - class="js-no-auto-disable gl-mr-3" - data-qa-selector="run_pipeline_button" - data-testid="run_pipeline_button" - :disabled="submitted" - >{{ s__('Pipeline|Run pipeline') }}</gl-button - > - <gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button> - </div> - </gl-form> -</template> diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js index 60b4c93d1d5..71c76aeab36 100644 --- a/app/assets/javascripts/pipeline_new/index.js +++ b/app/assets/javascripts/pipeline_new/index.js @@ -1,53 +1,9 @@ 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 { - // provide/inject - projectRefsEndpoint, - - // props - configVariablesPath, - defaultBranch, - fileParam, - maxWarnings, - pipelinesPath, - projectId, - refParam, - settingsLink, - varParam, - } = el.dataset; - - const variableParams = JSON.parse(varParam); - const fileParams = JSON.parse(fileParam); - - return new Vue({ - el, - provide: { - projectRefsEndpoint, - }, - render(createElement) { - return createElement(LegacyPipelineNewForm, { - props: { - configVariablesPath, - defaultBranch, - fileParams, - maxWarnings: Number(maxWarnings), - pipelinesPath, - projectId, - refParam, - settingsLink, - variableParams, - }, - }); - }, - }); -}; - const mountPipelineNewForm = (el) => { const { // provide/inject @@ -101,9 +57,5 @@ const mountPipelineNewForm = (el) => { export default () => { const el = document.getElementById('js-new-pipeline'); - if (gon.features?.runPipelineGraphql) { - mountPipelineNewForm(el); - } else { - mountLegacyPipelineNewForm(el); - } + mountPipelineNewForm(el); }; diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 7d1a75ae449..ea8c556dc49 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -26,7 +26,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action do push_frontend_feature_flag(:pipeline_tabs_vue, @project) - push_frontend_feature_flag(:run_pipeline_graphql, @project) end # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 diff --git a/config/feature_flags/development/run_pipeline_graphql.yml b/config/feature_flags/development/run_pipeline_graphql.yml deleted file mode 100644 index 78d8afbbee5..00000000000 --- a/config/feature_flags/development/run_pipeline_graphql.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: run_pipeline_graphql -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96633 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372310 -milestone: '15.4' -type: development -group: group::pipeline authoring -default_enabled: false diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index 2126997e25f..e013f9475d8 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -192,11 +192,7 @@ In this example: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. Disabled by default. > - The `options` keyword was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105502) in GitLab 15.7. - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, -ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. -The feature is not ready for production use. +> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106038) in GitLab 15.7. Feature flag `run_pipeline_graphql` removed. You can define an array of CI/CD variable values the user can select from when running a pipeline manually. These values are in a dropdown list in the **Run pipeline** page. Add the list of diff --git a/qa/qa/page/project/pipeline/new.rb b/qa/qa/page/project/pipeline/new.rb index f56f69fc077..1d85d072e34 100644 --- a/qa/qa/page/project/pipeline/new.rb +++ b/qa/qa/page/project/pipeline/new.rb @@ -5,15 +5,6 @@ module QA module Project module Pipeline class New < QA::Page::Base - # TODO: Replace legacy form with the new form when run_pipeline_graphql is enabled by default - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310 - view 'app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue' do - element :run_pipeline_button, required: true - element :ci_variable_row_container - element :ci_variable_key_field - element :ci_variable_value_field - end - view 'app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue' do element :run_pipeline_button, required: true element :ci_variable_row_container diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb index 4a0a8be3659..5dda8b04805 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb @@ -2,10 +2,7 @@ module QA RSpec.describe 'Verify', :runner do - describe 'Pipeline with customizable variable', feature_flag: { - name: :run_pipeline_graphql, - scope: :project - } do + describe 'Pipeline with customizable variable' do let(:executor) { "qa-runner-#{Time.now.to_i}" } let(:pipeline_job_name) { 'customizable-variable' } let(:variable_custom_value) { 'Custom Foo' } @@ -48,74 +45,45 @@ module QA end end - shared_examples 'pipeline with custom variable' do - before do - Flow::Login.sign_in + before do + Flow::Login.sign_in - project.visit! - Page::Project::Menu.perform(&:click_ci_cd_pipelines) - Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + project.visit! + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) - # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again. - # TODO: Investigate alternatives to deal with cache implementation - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233 - page.refresh - end - - after do - runner&.remove_via_api! - end - - it 'manually creates a pipeline and uses the defined custom variable value' do - Page::Project::Pipeline::New.perform do |new| - new.configure_variable(value: variable_custom_value) - new.click_run_pipeline_button - end - - Page::Project::Pipeline::Show.perform do |show| - Support::Waiter.wait_until { show.passed? } - end - - job = Resource::Job.fabricate_via_api! do |job| - job.id = project.job_by_name(pipeline_job_name)[:id] - job.name = pipeline_job_name - job.project = project - end - - job.visit! + # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again. + # TODO: Investigate alternatives to deal with cache implementation + # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233 + page.refresh + end - Page::Project::Job::Show.perform do |show| - expect(show.output).to have_content(variable_custom_value) - end - end + after do + runner&.remove_via_api! end - # TODO: Clean up tests when run_pipeline_graphql is enabled - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310 - context( - 'with feature flag disabled', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/361814' - ) do - before do - Runtime::Feature.disable(:run_pipeline_graphql, project: project) + it 'manually creates a pipeline and uses the defined custom variable value', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378975' do + Page::Project::Pipeline::New.perform do |new| + new.configure_variable(value: variable_custom_value) + new.click_run_pipeline_button end - it_behaves_like 'pipeline with custom variable' - end - - context( - 'with feature flag enabled', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378975' - ) do - before do - Runtime::Feature.enable(:run_pipeline_graphql, project: project) + Page::Project::Pipeline::Show.perform do |show| + Support::Waiter.wait_until { show.passed? } end - after do - Runtime::Feature.disable(:run_pipeline_graphql, project: project) + job = Resource::Job.fabricate_via_api! do |job| + job.id = project.job_by_name(pipeline_job_name)[:id] + job.name = pipeline_job_name + job.project = project end - it_behaves_like 'pipeline with custom variable' + job.visit! + + Page::Project::Job::Show.perform do |show| + expect(show.output).to have_content(variable_custom_value) + end end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb index c5112ebedc6..1878292015e 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb @@ -2,10 +2,7 @@ module QA RSpec.describe 'Verify' do - describe 'Pipeline with prefill variables', feature_flag: { - name: :run_pipeline_graphql, - scope: :global - } do + describe 'Pipeline with prefill variables' do let(:prefill_variable_description1) { Faker::Lorem.sentence } let(:prefill_variable_value1) { Faker::Lorem.word } let(:prefill_variable_value5) { Faker::Lorem.word } @@ -50,93 +47,51 @@ module QA end end - shared_examples 'pre-filled variables form' do |testcase| - it 'shows only variables with description as prefill variables on the run pipeline page', testcase: testcase do - Page::Project::Pipeline::New.perform do |new| - aggregate_failures do - expect(new).to have_field('Input variable key', with: 'TEST1') - expect(new).to have_field('Input variable value', with: prefill_variable_value1) - expect(new).to have_content(prefill_variable_description1) + before do + Flow::Login.sign_in + project.visit! - expect(new).to have_field('Input variable key', with: 'TEST2') - expect(new).to have_field('Input variable value', with: '') - expect(new).to have_content(prefill_variable_description2) + # Navigate to Run Pipeline page + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) - expect(new).not_to have_field('Input variable key', with: 'TEST3') - expect(new).not_to have_field('Input variable key', with: 'TEST4') - - # Legacy and GQL app will render the variable field differently - expect(new).to have_field('Input variable key', with: 'TEST5') - expect(new).to have_content(prefill_variable_description5) - end - end - end + # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again. + # TODO: Investigate alternatives to deal with cache implementation + # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233 + page.refresh end - # TODO: Clean up tests when run_pipeline_graphql is enabled - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310 - context 'with feature flag disabled' do - before do - Flow::Login.sign_in - project.visit! + it 'shows only variables with description as prefill variables on the run pipeline page', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378977' do + Page::Project::Pipeline::New.perform do |new| + aggregate_failures do + expect(new).to have_field('Input variable key', with: 'TEST1') + expect(new).to have_field('Input variable value', with: prefill_variable_value1) + expect(new).to have_content(prefill_variable_description1) - # Navigate to Run Pipeline page - Page::Project::Menu.perform(&:click_ci_cd_pipelines) - Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + expect(new).to have_field('Input variable key', with: 'TEST2') + expect(new).to have_field('Input variable value', with: '') + expect(new).to have_content(prefill_variable_description2) - # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again. - # TODO: Investigate alternatives to deal with cache implementation - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233 - page.refresh - end + expect(new).not_to have_field('Input variable key', with: 'TEST3') + expect(new).not_to have_field('Input variable key', with: 'TEST4') - it_behaves_like 'pre-filled variables form', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/371204' - - it 'does not prefill dropdown variables but renders them as input fields', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/383819' do - Page::Project::Pipeline::New.perform do |new| - expect(new.has_variable_dropdown?).to be(false) - expect(new).to have_field('Input variable value', with: 'FOO') + expect(new).to have_field('Input variable key', with: 'TEST5') + expect(new).to have_content(prefill_variable_description5) end end end - context 'with feature flag enabled' do - before do - Runtime::Feature.enable(:run_pipeline_graphql) - sleep 60 - - Flow::Login.sign_in - project.visit! - - # Navigate to Run Pipeline page - Page::Project::Menu.perform(&:click_ci_cd_pipelines) - Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) - - # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again. - # TODO: Investigate alternatives to deal with cache implementation - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233 - page.refresh - end - - after do - Runtime::Feature.disable(:run_pipeline_graphql) - sleep 60 - end - - it_behaves_like 'pre-filled variables form', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378977' - - it 'shows dropdown for variables with description, value, and options defined', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/383820' do - Page::Project::Pipeline::New.perform do |new| - aggregate_failures do - expect(new.variable_dropdown).to have_text('FOO') + it 'shows dropdown for variables with description, value, and options defined', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/383820' do + Page::Project::Pipeline::New.perform do |new| + aggregate_failures do + expect(new.variable_dropdown).to have_text('FOO') - new.click_variable_dropdown + new.click_variable_dropdown - expect(new.variable_dropdown_item_with_index(0)).to have_text(prefill_variable_value5) - expect(new.variable_dropdown_item_with_index(1)).to have_text('FOO') - end + expect(new.variable_dropdown_item_with_index(0)).to have_text(prefill_variable_value5) + expect(new.variable_dropdown_item_with_index(1)).to have_text('FOO') end end end diff --git a/spec/features/populate_new_pipeline_vars_with_params_spec.rb b/spec/features/populate_new_pipeline_vars_with_params_spec.rb index 0943b018693..a83b5a81a41 100644 --- a/spec/features/populate_new_pipeline_vars_with_params_spec.rb +++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb @@ -7,42 +7,24 @@ RSpec.describe "Populate new pipeline CI variables with url params", :js, featur let(:project) { create(:project) } let(:page_path) { new_project_pipeline_path(project) } - shared_examples 'form pre-filled with URL params' do - before do - sign_in(user) - project.add_maintainer(user) + before do + sign_in(user) + project.add_maintainer(user) - visit "#{page_path}?var[key1]=value1&file_var[key2]=value2" - end - - it "var[key1]=value1 populates env_var variable correctly" do - page.within(all("[data-testid='ci-variable-row']")[0]) do - expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1') - expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1') - end - end - - it "file_var[key2]=value2 populates file variable correctly" do - page.within(all("[data-testid='ci-variable-row']")[1]) do - expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2') - expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2') - end - end + visit "#{page_path}?var[key1]=value1&file_var[key2]=value2" end - context 'when feature flag is disabled' do - before do - stub_feature_flags(run_pipeline_graphql: false) + it "var[key1]=value1 populates env_var variable correctly" do + page.within(all("[data-testid='ci-variable-row']")[0]) do + expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1') + expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1') end - - it_behaves_like 'form pre-filled with URL params' end - context 'when feature flag is enabled' do - before do - stub_feature_flags(run_pipeline_graphql: true) + it "file_var[key2]=value2 populates file variable correctly" do + page.within(all("[data-testid='ci-variable-row']")[1]) do + expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2') + expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2') end - - it_behaves_like 'form pre-filled with URL params' end end diff --git a/spec/features/projects/pipelines/legacy_pipelines_spec.rb b/spec/features/projects/pipelines/legacy_pipelines_spec.rb index 0da5fc81da0..b6ae4144709 100644 --- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb +++ b/spec/features/projects/pipelines/legacy_pipelines_spec.rb @@ -673,7 +673,6 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do let(:project) { create(:project, :repository) } before do - stub_feature_flags(run_pipeline_graphql: false) visit new_project_pipeline_path(project) end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 14151ad0a6a..3bdabd672c7 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -663,7 +663,19 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do describe 'POST /:project/-/pipelines' do let(:project) { create(:project, :repository) } - shared_examples 'run pipeline form with gitlab-ci.yml' do + before do + visit new_project_pipeline_path(project) + end + + context 'for valid commit', :js do + before do + click_button project.default_branch + wait_for_requests + + find('p', text: 'master').click + wait_for_requests + end + context 'with gitlab-ci.yml', :js do before do stub_ci_pipeline_to_return_yaml_file @@ -697,9 +709,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do end end end - end - shared_examples 'run pipeline form without gitlab-ci.yml' do context 'without gitlab-ci.yml' do before do click_on 'Run pipeline' @@ -719,52 +729,6 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do end end end - - # Run Pipeline form with REST endpoints - # TODO: Clean up tests when run_pipeline_graphql is enabled - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310 - context 'with feature flag disabled' do - before do - stub_feature_flags(run_pipeline_graphql: false) - visit new_project_pipeline_path(project) - end - - context 'for valid commit', :js do - before do - click_button project.default_branch - wait_for_requests - - find('p', text: 'master').click - wait_for_requests - end - - it_behaves_like 'run pipeline form with gitlab-ci.yml' - - it_behaves_like 'run pipeline form without gitlab-ci.yml' - end - end - - # Run Pipeline form with GraphQL - context 'with feature flag enabled' do - before do - stub_feature_flags(run_pipeline_graphql: true) - visit new_project_pipeline_path(project) - end - - context 'for valid commit', :js do - before do - click_button project.default_branch - wait_for_requests - - find('p', text: 'master').click - wait_for_requests - end - - it_behaves_like 'run pipeline form with gitlab-ci.yml' - - it_behaves_like 'run pipeline form without gitlab-ci.yml' - end - end end describe 'Reset runner caches' do diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index a06f81e4d1c..b8b91eb8167 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -45,6 +45,7 @@ import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_p import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml'; import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml'; import TriggerNegative from './yaml_tests/negative_tests/trigger.yml'; +import VariablesInvalidOptionsYaml from './yaml_tests/negative_tests/variables/invalid_options.yml'; import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variables/invalid_syntax_desc.yml'; import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml'; import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml'; @@ -113,6 +114,7 @@ describe('negative tests', () => { IncludeNegativeYaml, JobWhenNegativeYaml, RulesNegativeYaml, + VariablesInvalidOptionsYaml, VariablesInvalidSyntaxDescYaml, VariablesWrongSyntaxUsageExpand, ProjectPathIncludeEmptyYaml, diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml new file mode 100644 index 00000000000..aac4c4e456d --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml @@ -0,0 +1,4 @@ +variables: + INVALID_OPTIONS: + value: "staging" + options: "staging" # must be an array diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml index 53d020c432f..5c91de9be70 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml @@ -4,11 +4,18 @@ variables: FOO: value: "BAR" description: "A single value variable" - DEPLOY_ENVIRONMENT: + VAR_WITH_DESCRIPTION: description: "A multi-value variable" RAW_VAR: value: "Hello $FOO" expand: false + VAR_WITH_OPTIONS: + value: "staging" + options: + - "production" + - "staging" + - "canary" + description: "The deployment target. Set to 'production' by default." rspec: script: rspec diff --git a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js deleted file mode 100644 index 512b152f106..00000000000 --- a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js +++ /dev/null @@ -1,456 +0,0 @@ -import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue'; -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 LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue'; -import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue'; -import { - mockQueryParams, - mockPostParams, - mockProjectId, - mockError, - mockRefs, - mockCreditCardValidationRequiredError, -} from '../mock_data'; - -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 newPipelinePostResponse = { id: 1 }; -const defaultBranch = 'main'; - -describe('Pipeline New Form', () => { - let wrapper; - let mock; - 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 findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf); - const findWarnings = () => wrapper.findAll('[data-testid="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) => { - // Select a branch in the dropdown - findRefsDropdown().vm.$emit('input', { - shortName: branch, - fullName: `refs/heads/${branch}`, - }); - }; - - const createComponent = (props = {}, method = shallowMount) => { - wrapper = method(LegacyPipelineNewForm, { - provide: { - projectRefsEndpoint, - }, - propsData: { - projectId: mockProjectId, - pipelinesPath, - configVariablesPath, - defaultBranch, - refParam: defaultBranch, - settingsLink: '', - maxWarnings: 25, - ...props, - }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {}); - mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs); - - dummySubmitEvent = { - preventDefault: jest.fn(), - }; - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - - mock.restore(); - }); - - describe('Form', () => { - beforeEach(async () => { - createComponent(mockQueryParams, mount); - - mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); - - 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(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' }); - expect(findVariableRows()).toHaveLength(3); - }); - - it('displays a variable from provided query params', () => { - expect(findKeyInputs().at(0).element.value).toBe('test_var'); - expect(findValueInputs().at(0).element.value).toBe('test_var_val'); - }); - - 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'); - }); - - it('does not display remove icon for last row', () => { - expect(findRemoveIcons()).toHaveLength(2); - }); - - it('removes ci variable row on remove icon button click', async () => { - findRemoveIcons().at(1).trigger('click'); - - await nextTick(); - - expect(findVariableRows()).toHaveLength(2); - }); - - it('creates blank variable on input change event', async () => { - const input = findKeyInputs().at(2); - input.element.value = 'test_var_2'; - input.trigger('change'); - - await nextTick(); - - expect(findVariableRows()).toHaveLength(4); - expect(findKeyInputs().at(3).element.value).toBe(''); - expect(findValueInputs().at(3).element.value).toBe(''); - }); - }); - - describe('Pipeline creation', () => { - beforeEach(async () => { - mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); - - await waitForPromises(); - }); - - it('does not submit the native HTML form', async () => { - createComponent(); - - findForm().vm.$emit('submit', dummySubmitEvent); - - expect(dummySubmitEvent.preventDefault).toHaveBeenCalled(); - }); - - it('disables the submit button immediately after submitting', async () => { - createComponent(); - - expect(findSubmitButton().props('disabled')).toBe(false); - - findForm().vm.$emit('submit', dummySubmitEvent); - await waitForPromises(); - - expect(findSubmitButton().props('disabled')).toBe(true); - }); - - it('creates pipeline with full ref and variables', async () => { - createComponent(); - - findForm().vm.$emit('submit', dummySubmitEvent); - await waitForPromises(); - - expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`); - expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); - }); - - it('creates a pipeline with short ref and variables from the query params', async () => { - createComponent(mockQueryParams); - - await waitForPromises(); - - findForm().vm.$emit('submit', dummySubmitEvent); - - await waitForPromises(); - - expect(getFormPostParams()).toEqual(mockPostParams); - expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); - }); - }); - - describe('When the ref has been changed', () => { - beforeEach(async () => { - createComponent({}, mount); - - 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'); - - await nextTick(); - - selectBranch('branch-1'); - - await waitForPromises(); - - const branchOneInput = findKeyInputs().at(0); - branchOneInput.element.value = 'deploy_var'; - branchOneInput.trigger('change'); - - await nextTick(); - - selectBranch('main'); - - await waitForPromises(); - - expect(findKeyInputs().at(0).element.value).toBe('build_var'); - expect(findVariableRows().length).toBe(2); - - selectBranch('branch-1'); - - await waitForPromises(); - - expect(findKeyInputs().at(0).element.value).toBe('deploy_var'); - expect(findVariableRows().length).toBe(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, - }, - }); - - expect(findLoadingIcon().exists()).toBe(true); - - await waitForPromises(); - - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('multi-line strings are added to the value field without removing line breaks', async () => { - createComponent(mockQueryParams, mount); - - mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { - [mockYmlKey]: { - value: mockYmlMultiLineValue, - description: mockYmlDesc, - }, - }); - - await waitForPromises(); - - expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue); - }); - - describe('with description', () => { - beforeEach(async () => { - createComponent(mockQueryParams, mount); - - mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { - [mockYmlKey]: { - value: mockYmlValue, - description: mockYmlDesc, - }, - }); - - await waitForPromises(); - }); - - it('displays all the variables', async () => { - expect(findVariableRows()).toHaveLength(4); - }); - - it('displays a variable from yml', () => { - expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey); - expect(findValueInputs().at(0).element.value).toBe(mockYmlValue); - }); - - 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'); - }); - - it('adds a description to the first variable from yml', () => { - expect(findVariableRows().at(0).text()).toContain(mockYmlDesc); - }); - - it('removes the description when a variable key changes', async () => { - findKeyInputs().at(0).element.value = 'yml_var_modified'; - findKeyInputs().at(0).trigger('change'); - - await nextTick(); - - expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc); - }); - }); - - 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: '', - }, - }); - - await waitForPromises(); - }); - - it('displays all the variables', async () => { - expect(findVariableRows()).toHaveLength(3); - }); - }); - }); - - describe('Form errors and warnings', () => { - beforeEach(() => { - createComponent(); - }); - - describe('when the refs cannot be loaded', () => { - beforeEach(() => { - mock - .onGet(projectRefsEndpoint, { params: { search: '' } }) - .reply(httpStatusCodes.INTERNAL_SERVER_ERROR); - - findRefsDropdown().vm.$emit('loadingError'); - }); - - it('shows both an error alert', () => { - expect(findErrorAlert().exists()).toBe(true); - expect(findWarningAlert().exists()).toBe(false); - }); - }); - - describe('when the error response can be handled', () => { - beforeEach(async () => { - mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError); - - findForm().vm.$emit('submit', dummySubmitEvent); - - await waitForPromises(); - }); - - it('shows both error and warning', () => { - expect(findErrorAlert().exists()).toBe(true); - expect(findWarningAlert().exists()).toBe(true); - }); - - it('shows the correct error', () => { - expect(findErrorAlert().text()).toBe(mockError.errors[0]); - }); - - it('shows the correct warning title', () => { - const { length } = mockError.warnings; - - expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`); - }); - - it('shows the correct amount of warnings', () => { - expect(findWarnings()).toHaveLength(mockError.warnings.length); - }); - - it('re-enables the submit button', () => { - expect(findSubmitButton().props('disabled')).toBe(false); - }); - - it('does not show the credit card validation required alert', () => { - expect(findCCAlert().exists()).toBe(false); - }); - - describe('when the error response is credit card validation required', () => { - beforeEach(async () => { - mock - .onPost(pipelinesPath) - .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError); - - window.gon = { - subscriptions_url: TEST_HOST, - payment_form_url: TEST_HOST, - }; - - findForm().vm.$emit('submit', dummySubmitEvent); - - await waitForPromises(); - }); - - it('shows credit card validation required alert', () => { - expect(findErrorAlert().exists()).toBe(false); - expect(findCCAlert().exists()).toBe(true); - }); - - it('clears error and hides the alert on dismiss', async () => { - expect(findCCAlert().exists()).toBe(true); - expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]); - - findCCAlert().vm.$emit('dismiss'); - - await nextTick(); - - expect(findCCAlert().exists()).toBe(false); - expect(wrapper.vm.$data.error).toBe(null); - }); - }); - }); - - describe('when the error response cannot be handled', () => { - beforeEach(async () => { - mock - .onPost(pipelinesPath) - .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong'); - - findForm().vm.$emit('submit', dummySubmitEvent); - - await waitForPromises(); - }); - - it('re-enables the submit button', () => { - expect(findSubmitButton().props('disabled')).toBe(false); - }); - }); - }); -}); |