diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-11 21:08:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-11 21:08:37 +0300 |
commit | c511df8a7e79a3df0b03eb774be53651a1aa465d (patch) | |
tree | 486d0b5dc967b610cce89286a7d7849deef8593e | |
parent | e46506bcc32de1af076ec8a5d51d405f827dd986 (diff) |
Add latest changes from gitlab-org/gitlab@master
42 files changed, 1022 insertions, 262 deletions
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue index 510c194f15f..30a178db5cc 100644 --- a/app/assets/javascripts/environments/components/new_environment_folder.vue +++ b/app/assets/javascripts/environments/components/new_environment_folder.vue @@ -19,6 +19,10 @@ export default { type: Object, required: true, }, + scope: { + type: String, + required: true, + }, }, data() { return { visible: false, interval: undefined }; @@ -27,7 +31,7 @@ export default { folder: { query: folderQuery, variables() { - return { environment: this.nestedEnvironment.latest }; + return { environment: this.nestedEnvironment.latest, scope: this.scope }; }, pollInterval() { return this.interval; @@ -52,7 +56,7 @@ export default { return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand; }, count() { - return this.folder?.availableCount ?? 0; + return this.folder?.[`${this.scope}Count`] ?? 0; }, folderClass() { return { 'gl-font-weight-bold': this.visible }; diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue index 80d9b300d3f..f35fabccae7 100644 --- a/app/assets/javascripts/environments/components/new_environment_item.vue +++ b/app/assets/javascripts/environments/components/new_environment_item.vue @@ -302,7 +302,11 @@ export default { class="gl-pl-4" /> </div> - <div v-if="upcomingDeployment" :class="$options.deploymentClasses"> + <div + v-if="upcomingDeployment" + :class="$options.deploymentClasses" + data-testid="upcoming-deployment-content" + > <deployment :deployment="upcomingDeployment" :class="{ 'gl-ml-7': inFolder }" diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue index 67fd6ffd975..8e6457ed918 100644 --- a/app/assets/javascripts/environments/components/new_environments_app.vue +++ b/app/assets/javascripts/environments/components/new_environments_app.vue @@ -16,12 +16,14 @@ import EnvironmentItem from './new_environment_item.vue'; import ConfirmRollbackModal from './confirm_rollback_modal.vue'; import DeleteEnvironmentModal from './delete_environment_modal.vue'; import CanaryUpdateModal from './canary_update_modal.vue'; +import EmptyState from './empty_state.vue'; export default { components: { DeleteEnvironmentModal, CanaryUpdateModal, ConfirmRollbackModal, + EmptyState, EnvironmentFolder, EnableReviewAppModal, EnvironmentItem, @@ -66,7 +68,7 @@ export default { query: environmentToChangeCanaryQuery, }, }, - inject: ['newEnvironmentPath', 'canCreateEnvironment'], + inject: ['newEnvironmentPath', 'canCreateEnvironment', 'helpPagePath'], i18n: { newEnvironmentButtonLabel: s__('Environments|New environment'), reviewAppButtonLabel: s__('Environments|Enable review app'), @@ -103,6 +105,9 @@ export default { environments() { return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? []; }, + hasEnvironments() { + return this.environments.length > 0 || this.folders.length > 0; + }, availableCount() { return this.environmentApp?.availableCount; }, @@ -221,19 +226,23 @@ export default { </template> </gl-tab> </gl-tabs> - <environment-folder - v-for="folder in folders" - :key="folder.name" - class="gl-mb-3" - :nested-environment="folder" - /> - <environment-item - v-for="environment in environments" - :key="environment.name" - class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid" - :environment="environment.latest" - @change="resetPolling" - /> + <template v-if="hasEnvironments"> + <environment-folder + v-for="folder in folders" + :key="folder.name" + class="gl-mb-3" + :scope="scope" + :nested-environment="folder" + /> + <environment-item + v-for="environment in environments" + :key="environment.name" + class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid" + :environment="environment.latest" + @change="resetPolling" + /> + </template> + <empty-state v-else :help-path="helpPagePath" /> <gl-pagination align="center" :total-items="totalItems" diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql index 3292c916b2e..e8c145ee916 100644 --- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql @@ -1,5 +1,5 @@ -query getEnvironmentFolder($environment: NestedLocalEnvironment) { - folder(environment: $environment) @client { +query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String) { + folder(environment: $environment, scope: $scope) @client { availableCount environments stoppedCount diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index 2544fd5273c..a7866c1e778 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -59,8 +59,8 @@ export const resolvers = (endpoint) => ({ }; }); }, - folder(_, { environment: { folderPath } }) { - return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({ + folder(_, { environment: { folderPath }, scope }) { + return axios.get(folderPath, { params: { scope, per_page: 3 } }).then((res) => ({ availableCount: res.data.available_count, environments: res.data.environments.map(mapEnvironment), stoppedCount: res.data.stopped_count, diff --git a/app/assets/javascripts/pipeline_wizard/components/commit.vue b/app/assets/javascripts/pipeline_wizard/components/commit.vue index 518b41c66b1..e68458a494f 100644 --- a/app/assets/javascripts/pipeline_wizard/components/commit.vue +++ b/app/assets/javascripts/pipeline_wizard/components/commit.vue @@ -195,7 +195,7 @@ export default { data-testid="branch_selector_group" label-for="branch" > - <ref-selector id="branch" v-model="branch" data-testid="branch" :project-id="projectPath" /> + <ref-selector id="branch" v-model="branch" :project-id="projectPath" data-testid="branch" /> </gl-form-group> <gl-alert v-if="!!commitError" @@ -206,7 +206,7 @@ export default { > {{ commitError }} </gl-alert> - <step-nav show-back-button v-bind="$props" @back="$emit('go-back')"> + <step-nav show-back-button v-bind="$props" @back="$emit('back')"> <template #after> <gl-button :disabled="isCommitButtonEnabled" diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue new file mode 100644 index 00000000000..b7207576ddc --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue @@ -0,0 +1,185 @@ +<script> +import { GlProgressBar } from '@gitlab/ui'; +import { Document } from 'yaml'; +import { merge } from '~/lib/utils/yaml'; +import { __ } from '~/locale'; +import { isValidStepSeq } from '~/pipeline_wizard/validators'; +import YamlEditor from './editor.vue'; +import WizardStep from './step.vue'; +import CommitStep from './commit.vue'; + +export const i18n = { + stepNofN: __('Step %{currentStep} of %{stepCount}'), + draft: __('Draft: %{filename}'), + overlayMessage: __(`Start inputting changes and we will generate a + YAML-file for you to add to your repository`), +}; + +export default { + name: 'PipelineWizardWrapper', + i18n, + components: { + GlProgressBar, + YamlEditor, + WizardStep, + CommitStep, + }, + props: { + steps: { + type: Object, + required: true, + validator: isValidStepSeq, + }, + projectPath: { + type: String, + required: true, + }, + defaultBranch: { + type: String, + required: true, + }, + filename: { + type: String, + required: true, + }, + }, + data() { + return { + highlightPath: null, + currentStepIndex: 0, + // TODO: In order to support updating existing pipelines, the below + // should contain a parsed version of an existing .gitlab-ci.yml. + // See https://gitlab.com/gitlab-org/gitlab/-/issues/355306 + compiled: new Document({}), + showPlaceholder: true, + pipelineBlob: null, + placeholder: this.getPlaceholder(), + }; + }, + computed: { + currentStepConfig() { + return this.steps.get(this.currentStepIndex); + }, + currentStepInputs() { + return this.currentStepConfig.get('inputs').toJSON(); + }, + currentStepTemplate() { + return this.currentStepConfig.get('template', true); + }, + currentStep() { + return this.currentStepIndex + 1; + }, + stepCount() { + return this.steps.items.length + 1; + }, + progress() { + return Math.ceil((this.currentStep / (this.stepCount + 1)) * 100); + }, + isLastStep() { + return this.currentStep === this.stepCount; + }, + }, + watch: { + isLastStep(value) { + if (value) this.resetHighlight(); + }, + }, + methods: { + resetHighlight() { + this.highlightPath = null; + }, + onUpdate() { + this.showPlaceholder = false; + }, + onEditorUpdate(blob) { + // TODO: In a later iteration, we could add a loopback allowing for + // changes from the editor to flow back into the model + // see https://gitlab.com/gitlab-org/gitlab/-/issues/355312 + this.pipelineBlob = blob; + }, + getPlaceholder() { + const doc = new Document({}); + this.steps.items.forEach((tpl) => { + merge(doc, tpl.get('template').clone()); + }); + return doc; + }, + }, +}; +</script> + +<template> + <div class="row gl-mt-8"> + <main class="col-md-6 gl-pr-8"> + <header class="gl-mb-5"> + <h3 class="text-secondary gl-mt-0" data-testid="step-count"> + {{ sprintf($options.i18n.stepNofN, { currentStep, stepCount }) }} + </h3> + <gl-progress-bar :value="progress" variant="success" /> + </header> + <section class="gl-mb-4"> + <commit-step + v-if="isLastStep" + ref="step" + :default-branch="defaultBranch" + :file-content="pipelineBlob" + :filename="filename" + :project-path="projectPath" + @back="currentStepIndex--" + /> + <wizard-step + v-else + :key="currentStepIndex" + ref="step" + :compiled.sync="compiled" + :has-next-step="currentStepIndex < steps.items.length" + :has-previous-step="currentStepIndex > 0" + :highlight.sync="highlightPath" + :inputs="currentStepInputs" + :template="currentStepTemplate" + @back="currentStepIndex--" + @next="currentStepIndex++" + @update:compiled="onUpdate" + /> + </section> + </main> + <aside class="col-md-6 gl-pt-3"> + <div + class="gl-border-1 gl-border-gray-100 gl-border-solid border-radius-default gl-bg-gray-10" + > + <h6 class="gl-p-2 gl-px-4 text-secondary" data-testid="editor-header"> + {{ sprintf($options.i18n.draft, { filename }) }} + </h6> + <div class="gl-relative gl-overflow-hidden"> + <yaml-editor + :aria-hidden="showPlaceholder" + :doc="showPlaceholder ? placeholder : compiled" + :filename="filename" + :highlight="highlightPath" + class="gl-w-full" + @update:yaml="onEditorUpdate" + /> + <div + v-if="showPlaceholder" + class="gl-absolute gl-top-0 gl-right-0 gl-bottom-0 gl-left-0 gl-filter-blur-1" + data-testid="placeholder-overlay" + > + <div + class="gl-absolute gl-top-0 gl-right-0 gl-bottom-0 gl-left-0 bg-white gl-opacity-5 gl-z-index-2" + ></div> + <div + class="gl-relative gl-h-full gl-display-flex gl-align-items-center gl-justify-content-center gl-z-index-3" + > + <div class="gl-max-w-34"> + <h4 data-testid="filename">{{ filename }}</h4> + <p data-testid="description"> + {{ $options.i18n.overlayMessage }} + </p> + </div> + </div> + </div> + </div> + </div> + </aside> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue new file mode 100644 index 00000000000..7200b4e3782 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue @@ -0,0 +1,65 @@ +<script> +import { parseDocument } from 'yaml'; +import WizardWrapper from './components/wrapper.vue'; + +export default { + name: 'PipelineWizard', + components: { + WizardWrapper, + }, + props: { + template: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + defaultBranch: { + type: String, + required: true, + }, + defaultFilename: { + type: String, + required: false, + default: '.gitlab-ci.yml', + }, + }, + computed: { + parsedTemplate() { + return this.template ? parseDocument(this.template) : null; + }, + title() { + return this.parsedTemplate?.get('title'); + }, + description() { + return this.parsedTemplate?.get('description'); + }, + filename() { + return this.parsedTemplate?.get('filename') || this.defaultFilename; + }, + steps() { + return this.parsedTemplate?.get('steps'); + }, + }, +}; +</script> + +<template> + <div> + <div class="gl-my-8"> + <h2 class="gl-mb-4" data-testid="title">{{ title }}</h2> + <p class="text-tertiary gl-font-lg gl-max-w-80" data-testid="description"> + {{ description }} + </p> + </div> + <wizard-wrapper + v-if="steps" + :default-branch="defaultBranch" + :filename="filename" + :project-path="projectPath" + :steps="steps" + /> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_wizard/validators.js b/app/assets/javascripts/pipeline_wizard/validators.js new file mode 100644 index 00000000000..57cd56b23a5 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/validators.js @@ -0,0 +1,4 @@ +import { isSeq } from 'yaml'; + +export const isValidStepSeq = (v) => + isSeq(v) && v.items.every((s) => s.get('inputs') && s.get('template')); diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 8a4f9c32f9f..d7a5e21e303 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -342,4 +342,27 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709 margin-bottom: $gl-spacing-scale-12 !important; // only need !important for now so that it overrides styles from @gitlab/ui which currently take precedence } } + /* End gitlab-ui#1709 */ + +/* + * The below two styles will be moved to @gitlab/ui by + * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1750 + */ +.gl-max-w-34 { + max-width: 34 * $grid-size; +} + +.gl-max-w-80 { + max-width: 80 * $grid-size; +} + +/* + * The below style will be moved to @gitlab/ui by + * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1751 + */ +.gl-filter-blur-1 { + backdrop-filter: blur(2px); + /* stylelint-disable property-no-vendor-prefix */ + -webkit-backdrop-filter: blur(2px); // still required by Safari +} diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb index 72f967b8beb..4755b88cbea 100644 --- a/app/presenters/search_service_presenter.rb +++ b/app/presenters/search_service_presenter.rb @@ -25,7 +25,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated case scope when 'users' - objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord + objects.eager_load(:status) if objects.respond_to?(:eager_load) # rubocop:disable CodeReuse/ActiveRecord when 'commits' prepare_commits_for_rendering(objects) else diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 77b2fc25c9a..e4b8750b96c 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -1,21 +1,11 @@ - page_title _("Environments") - add_page_specific_style 'page_bundles/environments' -- if Feature.enabled?(:new_environments_table) - #environments-table{ data: { endpoint: project_environments_path(@project, format: :json), - "can-read-environment" => can?(current_user, :read_environment, @project).to_s, - "can-create-environment" => can?(current_user, :create_environment, @project).to_s, - "new-environment-path" => new_project_environment_path(@project), - "help-page-path" => help_page_path("ci/environments/index.md"), - "project-path" => @project.full_path, - "project-id" => @project.id, - "default-branch-name" => @project.default_branch_or_main } } -- else - #environments-list-view{ data: { environments_data: environments_list_data, - "can-read-environment" => can?(current_user, :read_environment, @project).to_s, - "can-create-environment" => can?(current_user, :create_environment, @project).to_s, - "new-environment-path" => new_project_environment_path(@project), - "help-page-path" => help_page_path("ci/environments/index.md"), - "project-path" => @project.full_path, - "project-id" => @project.id, - "default-branch-name" => @project.default_branch_or_main } } +#environments-table{ data: { endpoint: project_environments_path(@project, format: :json), + "can-read-environment" => can?(current_user, :read_environment, @project).to_s, + "can-create-environment" => can?(current_user, :create_environment, @project).to_s, + "new-environment-path" => new_project_environment_path(@project), + "help-page-path" => help_page_path("ci/environments/index.md"), + "project-path" => @project.full_path, + "project-id" => @project.id, + "default-branch-name" => @project.default_branch_or_main } } diff --git a/config/feature_flags/development/new_environments_table.yml b/config/feature_flags/development/new_environments_table.yml deleted file mode 100644 index b97a4d49cd8..00000000000 --- a/config/feature_flags/development/new_environments_table.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: new_environments_table -introduced_by_url: -rollout_issue_url: -milestone: '14.4' -type: development -group: group::release -default_enabled: false diff --git a/config/webpack.config.js b/config/webpack.config.js index 66c02dcc87d..360c5be05d4 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -363,6 +363,10 @@ module.exports = { name: '[name].[contenthash:8].[ext]', }, }, + { + test: /\.(yml|yaml)$/, + loader: 'raw-loader', + }, ], }, diff --git a/db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb b/db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb new file mode 100644 index 00000000000..919e834a783 --- /dev/null +++ b/db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddAsyncIndexCiJobArtifactsProjectIdCreatedAt < Gitlab::Database::Migration[1.0] + INDEX_NAME = 'index_ci_job_artifacts_on_id_project_id_and_created_at' + + def up + prepare_async_index :ci_job_artifacts, [:project_id, :created_at, :id], name: INDEX_NAME + end + + def down + unprepare_async_index_by_name :ci_job_artifacts, INDEX_NAME + end +end diff --git a/db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb b/db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb new file mode 100644 index 00000000000..3c1e6714529 --- /dev/null +++ b/db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemoveDependencyListUsageDataFromRedis < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + Gitlab::Redis::SharedState.with { |r| r.del("DEPENDENCY_LIST_USAGE_COUNTER") } + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20220310095341 b/db/schema_migrations/20220310095341 new file mode 100644 index 00000000000..d52763cce63 --- /dev/null +++ b/db/schema_migrations/20220310095341 @@ -0,0 +1 @@ +56d906eac31954988bd0659eabbc9f1bad1a47dd616fb99e4b90b56b2bf4c6a0
\ No newline at end of file diff --git a/db/schema_migrations/20220310141349 b/db/schema_migrations/20220310141349 new file mode 100644 index 00000000000..d52b2d997a4 --- /dev/null +++ b/db/schema_migrations/20220310141349 @@ -0,0 +1 @@ +39785d4140c7345ddbe62417576381654ce22d505ee5c92a84425f0a3f8e4935
\ No newline at end of file diff --git a/doc/development/database/database_reviewer_guidelines.md b/doc/development/database/database_reviewer_guidelines.md index 8c216d8247f..9d5e4821c9f 100644 --- a/doc/development/database/database_reviewer_guidelines.md +++ b/doc/development/database/database_reviewer_guidelines.md @@ -32,6 +32,13 @@ Team members are encouraged to self-identify as database domain experts, and add projects: gitlab: - reviewer database +``` + +Create the merge request [using the "Database reviewer" template](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/.gitlab/merge_request_templates/Database%20reviewer.md), +adding your expertise your profile YAML file. Assign to a database maintainer or the +[Database Team's Engineering Manager](https://about.gitlab.com/handbook/engineering/development/enablement/database/). + +After the `team.yml` update is merged, the [Reviewer roulette](../code_review.md#reviewer-roulette) may recommend you as a database reviewer. ## Resources for database reviewers diff --git a/doc/development/testing_guide/end_to_end/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md index 05526daa07e..e0f6cbe632d 100644 --- a/doc/development/testing_guide/end_to_end/best_practices.md +++ b/doc/development/testing_guide/end_to_end/best_practices.md @@ -17,11 +17,16 @@ In case custom inflection logic is needed, custom inflectors are added in the [q ## Link a test to its test case Every test should have a corresponding test case in the [GitLab project Test Cases](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases) as well as a results issue in the [Quality Test Cases project](https://gitlab.com/gitlab-org/quality/testcases/-/issues). -It's recommended that you reuse the issue created to plan the test as the results issue. If a test case or results issue does not already exist you -can create them yourself by using this [end-to-end test issue template](https://gitlab.com/gitlab-org/quality/testcases/-/blob/master/.gitlab/issue_templates/End-to-end%20Test.md) to format the issue description. (Note you must copy/paste this for test cases as templates aren't currently available.) Alternatively, you can run the test in a pipeline that has reporting enabled and the test-case reporter will automatically create a new test case and/or results issue and link the results issue to it's corresponding test case. +If a test case issue does not yet exist you can create one yourself. To do so, create a new +issue in the [Test Cases](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases) GitLab project +with a placeholder title. After the test case URL is linked to a test in the code, when the test is +run in a pipeline that has reporting enabled, the `report-results` script automatically updates the +test case and the results issue. +If a results issue does not yet exist, the `report-results` script automatically creates one and +links it to its corresponding test case. -Whether you create a new test case or one is created automatically, you will need to manually add -a `testcase` RSpec metadata tag. In most cases, a single test will be associated with a single test case. +To link a test case to a test in the code, you must manually add a `testcase` RSpec metadata tag. +In most cases, a single test is associated with a single test case. For example: @@ -92,106 +97,7 @@ RSpec.describe 'Create' do end ``` -There would be four associated test cases, two for each shared example, with the following content for the first two: - -[Test 1 Test Case](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347774): - -````markdown -```markdown -Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted -protected branch push and merge when only one user is allowed to merge and push to a protected -branch behaves like only user with access pushes and merges selecte... - -Description: -### Full description - -Create Restricted protected branch push and merge when only one user is allowed to merge and push -to a protected branch behaves like only user with access pushes and merges selected developer user -pushes and merges - -### File path - -./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb - -### DO NOT EDIT BELOW THIS LINE - -Active and historical test results: - -https://gitlab.com/gitlab-org/quality/testcases/-/issues/2177 - -``` -```` - -[Test 1 Results Issue](https://gitlab.com/gitlab-org/quality/testcases/-/issues/2177): - -````markdown -```markdown -Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted -protected branch push and merge when only one user is allowed to merge and push to a protected -branch behaves like only user with access pushes and merges selecte... - -Description: -### Full description - -Create Restricted protected branch push and merge when only one user is allowed to merge and push -to a protected branch behaves like only user with access pushes and merges selected developer user -pushes and merges - -### File path - -./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb - -``` -```` - -[Test 2 Test Case](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347775): - -````markdown -```markdown -Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted -protected branch push and merge when only one user is allowed to merge and push to a protected -branch behaves like only user with access pushes and merges unselec... - -Description: -### Full description - -Create Restricted protected branch push and merge when only one user is allowed to merge and push -to a protected branch behaves like only user with access pushes and merges unselected maintainer -user fails to push - -### File path - -./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb - -### DO NOT EDIT BELOW THIS LINE - -Active and historical test results: - -https://gitlab.com/gitlab-org/quality/testcases/-/issues/2176 - -``` -```` - -[Test 2 Results Issue](https://gitlab.com/gitlab-org/quality/testcases/-/issues/2176): - -````markdown -```markdown -Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted -protected branch push and merge when only one user is allowed to merge and push to a protected -branch behaves like only user with access pushes and merges unselec... - -Description: -### Full description - -Create Restricted protected branch push and merge when only one user is allowed to merge and push -to a protected branch behaves like only user with access pushes and merges unselected maintainer -user fails to push - -### File path - -./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb -``` -```` +We recommend creating four associated test cases, two for each shared example. ## Prefer API over UI diff --git a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md index 45464fb2278..f9b505a8271 100644 --- a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md +++ b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md @@ -42,6 +42,7 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec | `:requires_praefect` | The test requires that the GitLab instance uses [Gitaly Cluster](../../../administration/gitaly/praefect.md) (a.k.a. Praefect) as the repository storage . It's assumed to be used by default but if not the test can be skipped by setting `QA_CAN_TEST_PRAEFECT` to `false`. | | `:runner` | The test depends on and sets up a GitLab Runner instance, typically to run a pipeline. | | `:skip_live_env` | The test is excluded when run against live deployed environments such as Staging, Canary, and Production. | +| `:skip_fips_env` | The test is excluded when run against an environment in FIPS mode. | | `:skip_signup_disabled` | The test uses UI to sign up a new user and is skipped in any environment that does not allow new user registration via the UI. | | `:smoke` | The test belongs to the test suite which verifies basic functionality of a GitLab instance.| | `:smtp` | The test requires a GitLab instance to be configured to use an SMTP server. Tests SMTP notification email delivery from GitLab by using MailHog. | diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml index 1660a9250e3..33c0928db6f 100644 --- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml @@ -52,7 +52,7 @@ rails: # This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk # are supported too: https://github.com/travis-ci/dpl deploy: - type: deploy + stage: deploy environment: production script: - gem install dpl diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c78a131974b..0d6140dee1b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13130,6 +13130,9 @@ msgstr "" msgid "Draft" msgstr "" +msgid "Draft: %{filename}" +msgstr "" + msgid "Drag your designs here or %{linkStart}click to upload%{linkEnd}." msgstr "" @@ -35006,6 +35009,9 @@ msgstr "" msgid "Start free trial" msgstr "" +msgid "Start inputting changes and we will generate a YAML-file for you to add to your repository" +msgstr "" + msgid "Start merge train" msgstr "" @@ -35273,6 +35279,9 @@ msgstr "" msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments." msgstr "" +msgid "Step %{currentStep} of %{stepCount}" +msgstr "" + msgid "Step 1." msgstr "" diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb index 0323448878b..18a77bd5ae3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'SSH key support' do + describe 'SSH key support', :skip_fips_env do # Note: If you run these tests against GDK make sure you've enabled sshd # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb index 2e8c43d6981..b0eb3ac7b37 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'SSH keys support', :smoke do + RSpec.describe 'SSH keys support', :smoke, :skip_fips_env do key_title = "key for ssh tests #{Time.now.to_f}" key = nil diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb index 8f22a28628f..7a0b4674581 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Version control for personal snippets' do + describe 'Version control for personal snippets', :skip_fips_env do let(:new_file) { 'new_snippet_file' } let(:changed_content) { 'changes' } let(:commit_message) { 'Changes to snippets' } diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb index 9a5fe44c927..d269e02e26d 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Version control for project snippets' do + describe 'Version control for project snippets', :skip_fips_env do let(:new_file) { 'new_snippet_file' } let(:changed_content) { 'changes' } let(:commit_message) { 'Changes to snippets' } diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 1055bd98d3c..1661fec03be 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Release' do - describe 'Deploy key creation' do + describe 'Deploy key creation', :skip_fips_env do it 'user adds a deploy key', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348023' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index c86f75e0b16..ff8dc686991 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -4,7 +4,7 @@ require 'digest/sha1' module QA RSpec.describe 'Release', :runner do - describe 'Git clone using a deploy key' do + describe 'Git clone using a deploy key', :skip_fips_env do let(:runner_name) { "qa-runner-#{SecureRandom.hex(4)}" } let(:repository_location) { project.repository_ssh_location } diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index ca9aebf4336..99137018d6b 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -10,7 +10,6 @@ RSpec.describe 'Environments page', :js do let(:role) { :developer } before do - stub_feature_flags(new_environments_table: false) project.add_role(user, role) sign_in(user) end @@ -35,24 +34,18 @@ RSpec.describe 'Environments page', :js do it 'shows "Available" and "Stopped" tab with links' do visit_environments(project) - expect(page).to have_selector('.js-environments-tab-available') - expect(page).to have_content('Available') - expect(page).to have_selector('.js-environments-tab-stopped') - expect(page).to have_content('Stopped') + expect(page).to have_link(_('Available')) + expect(page).to have_link(_('Stopped')) end describe 'with one available environment' do - before do - create(:environment, project: project, state: :available) - end + let!(:environment) { create(:environment, project: project, state: :available) } describe 'in available tab page' do it 'shows one environment' do visit_environments(project, scope: 'available') - expect(page).to have_css('.environments-container') - expect(page.all('.environment-name').length).to eq(1) - expect(page.all('[data-testid="stop-icon"]').length).to eq(1) + expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) end end @@ -77,7 +70,6 @@ RSpec.describe 'Environments page', :js do it 'shows no environments' do visit_environments(project, scope: 'stopped') - expect(page).to have_css('.environments-container') expect(page).to have_content('You don\'t have any environments right now') end end @@ -95,22 +87,18 @@ RSpec.describe 'Environments page', :js do it 'shows one environment without error' do visit_environments(project, scope: 'available') - expect(page).to have_css('.environments-container') - expect(page.all('.environment-name').length).to eq(1) + expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) end end end describe 'with one stopped environment' do - before do - create(:environment, project: project, state: :stopped) - end + let!(:environment) { create(:environment, project: project, state: :stopped) } describe 'in available tab page' do it 'shows no environments' do visit_environments(project, scope: 'available') - expect(page).to have_css('.environments-container') expect(page).to have_content('You don\'t have any environments right now') end end @@ -119,8 +107,7 @@ RSpec.describe 'Environments page', :js do it 'shows one environment' do visit_environments(project, scope: 'stopped') - expect(page).to have_css('.environments-container') - expect(page.all('.environment-name').length).to eq(1) + expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) expect(page.all('[data-testid="stop-icon"]').length).to eq(0) end end @@ -135,8 +122,8 @@ RSpec.describe 'Environments page', :js do it 'does not show environments and counters are set to zero' do expect(page).to have_content('You don\'t have any environments right now') - expect(page.find('.js-environments-tab-available .badge').text).to eq('0') - expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0') + expect(page).to have_link("#{_('Available')} 0") + expect(page).to have_link("#{_('Stopped')} 0") end end @@ -150,21 +137,23 @@ RSpec.describe 'Environments page', :js do context 'when there are no deployments' do before do visit_environments(project) + + page.click_button _('Expand') end it 'shows environments names and counters' do - expect(page).to have_link(environment.name) + expect(page).to have_link(environment.name, href: project_environment_path(project, environment)) - expect(page.find('.js-environments-tab-available .badge').text).to eq('1') - expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0') + expect(page).to have_link("#{_('Available')} 1") + expect(page).to have_link("#{_('Stopped')} 0") end it 'does not show deployments' do - expect(page).to have_content('No deployments yet') + expect(page).to have_content(s_('Environments|There are no deployments for this environment yet. Learn more about setting up deployments.')) end it 'shows stop button when environment is not stoppable' do - expect(page).to have_selector(stop_button_selector) + expect(page).to have_button('Stop') end end @@ -179,8 +168,10 @@ RSpec.describe 'Environments page', :js do it 'shows deployment SHA and internal ID' do visit_environments(project) + page.click_button _('Expand') - expect(page).to have_link(deployment.short_sha) + expect(page).to have_text(deployment.short_sha) + expect(page).to have_link(deployment.commit.full_title) expect(page).to have_content(deployment.iid) end @@ -218,10 +209,6 @@ RSpec.describe 'Environments page', :js do .not_to change { Ci::Pipeline.count } end - it 'shows build name and id' do - expect(page).to have_link("#{build.name} ##{build.id}") - end - it 'shows a stop button' do expect(page).to have_selector(stop_button_selector) end @@ -373,7 +360,8 @@ RSpec.describe 'Environments page', :js do it 'does not show deployments' do visit_environments(project) - expect(page).to have_content('No deployments yet') + page.click_button _('Expand') + expect(page).to have_content(s_('Environments|There are no deployments for this environment yet. Learn more about setting up deployments.')) end end @@ -389,9 +377,10 @@ RSpec.describe 'Environments page', :js do it "renders the upcoming deployment", :aggregate_failures do visit_environments(project) + page.click_button _('Expand') + within(upcoming_deployment_content_selector) do expect(page).to have_content("##{deployment.iid}") - expect(page).to have_selector("a[href=\"#{project_job_path(project, deployment.deployable)}\"]") expect(page).to have_link(href: /#{deployment.user.username}/) end end @@ -413,15 +402,15 @@ RSpec.describe 'Environments page', :js do let(:role) { :developer } it 'developer creates a new environment with a valid name' do - within(".environments-section") { click_link 'New environment' } + click_link 'New environment' fill_in('Name', with: 'production') click_on 'Save' expect(page).to have_content('production') end - it 'developer creates a new environmetn with invalid name' do - within(".environments-section") { click_link 'New environment' } + it 'developer creates a new environment with invalid name' do + click_link 'New environment' fill_in('Name', with: 'name,with,commas') click_on 'Save' @@ -458,20 +447,11 @@ RSpec.describe 'Environments page', :js do expect(page).not_to have_content 'review-2' expect(page).to have_content 'staging 2' - within('.folder-row') do - find('.folder-name', text: 'staging').click - end + page.click_button _('Expand') expect(page).to have_content 'review-1' expect(page).to have_content 'review-2' - within('.ci-table') do - within('[data-qa-selector="environment_item"]', text: 'review-1') do # rubocop:disable QA/SelectorUsage - expect(find('.js-auto-stop').text).not_to be_empty - end - within('[data-qa-selector="environment_item"]', text: 'review-2') do # rubocop:disable QA/SelectorUsage - expect(find('.js-auto-stop').text).not_to be_empty - end - end + expect(page).to have_content 'Auto stop in' end end @@ -494,9 +474,7 @@ RSpec.describe 'Environments page', :js do expect(page).not_to have_content 'review-2' expect(page).to have_content 'staging 2' - within('.folder-row') do - find('.folder-name', text: 'staging').click - end + page.click_button _('Expand') expect(page).to have_content 'review-1' expect(page).to have_content 'review-2' diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js index 32c0cc42a96..26f0659204a 100644 --- a/spec/frontend/environments/graphql/resolvers_spec.js +++ b/spec/frontend/environments/graphql/resolvers_spec.js @@ -124,10 +124,11 @@ describe('~/frontend/environments/graphql/resolvers', () => { }); describe('folder', () => { it('should fetch the folder url passed to it', async () => { - mock.onGet(ENDPOINT, { params: { per_page: 3 } }).reply(200, folder); + mock.onGet(ENDPOINT, { params: { per_page: 3, scope: 'available' } }).reply(200, folder); const environmentFolder = await mockResolvers.Query.folder(null, { environment: { folderPath: ENDPOINT }, + scope: 'available', }); expect(environmentFolder).toEqual(resolvedFolder); diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js index 460263587be..759c7821661 100644 --- a/spec/frontend/environments/new_environment_folder_spec.js +++ b/spec/frontend/environments/new_environment_folder_spec.js @@ -16,8 +16,6 @@ describe('~/environments/components/new_environments_folder.vue', () => { let wrapper; let environmentFolderMock; let nestedEnvironment; - let folderName; - let button; const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') }); @@ -30,7 +28,10 @@ describe('~/environments/components/new_environments_folder.vue', () => { const createWrapper = (propsData, apolloProvider) => mountExtended(EnvironmentsFolder, { apolloProvider, - propsData, + propsData: { + scope: 'available', + ...propsData, + }, stubs: { transition: stubTransition() }, provide: { helpPagePath: '/help' }, }); @@ -39,62 +40,93 @@ describe('~/environments/components/new_environments_folder.vue', () => { environmentFolderMock = jest.fn(); [nestedEnvironment] = resolvedEnvironmentsApp.environments; environmentFolderMock.mockReturnValue(resolvedFolder); - wrapper = createWrapper({ nestedEnvironment }, createApolloProvider()); - - await nextTick(); - await waitForPromises(); - folderName = wrapper.findByText(nestedEnvironment.name); - button = wrapper.findByRole('button', { name: __('Expand') }); }); afterEach(() => { wrapper?.destroy(); }); - it('displays the name of the folder', () => { - expect(folderName.text()).toBe(nestedEnvironment.name); - }); + describe('default', () => { + let folderName; + let button; - describe('collapse', () => { - let icons; - let collapse; + beforeEach(async () => { + wrapper = createWrapper({ nestedEnvironment }, createApolloProvider()); - beforeEach(() => { - collapse = wrapper.findComponent(GlCollapse); - icons = wrapper.findAllComponents(GlIcon); + await nextTick(); + await waitForPromises(); + folderName = wrapper.findByText(nestedEnvironment.name); + button = wrapper.findByRole('button', { name: __('Expand') }); }); - it('is collapsed by default', () => { - const link = findLink(); - - expect(collapse.attributes('visible')).toBeUndefined(); - const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2); - expect(iconNames).toEqual(['angle-right', 'folder-o']); - expect(folderName.classes('gl-font-weight-bold')).toBe(false); - expect(link.exists()).toBe(false); + it('displays the name of the folder', () => { + expect(folderName.text()).toBe(nestedEnvironment.name); }); - it('opens on click', async () => { - await button.trigger('click'); - - const link = findLink(); - - expect(button.attributes('aria-label')).toBe(__('Collapse')); - expect(collapse.attributes('visible')).toBe('visible'); - const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2); - expect(iconNames).toEqual(['angle-down', 'folder-open']); - expect(folderName.classes('gl-font-weight-bold')).toBe(true); - expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath); + describe('collapse', () => { + let icons; + let collapse; + + beforeEach(() => { + collapse = wrapper.findComponent(GlCollapse); + icons = wrapper.findAllComponents(GlIcon); + }); + + it('is collapsed by default', () => { + const link = findLink(); + + expect(collapse.attributes('visible')).toBeUndefined(); + const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2); + expect(iconNames).toEqual(['angle-right', 'folder-o']); + expect(folderName.classes('gl-font-weight-bold')).toBe(false); + expect(link.exists()).toBe(false); + }); + + it('opens on click', async () => { + await button.trigger('click'); + + const link = findLink(); + + expect(button.attributes('aria-label')).toBe(__('Collapse')); + expect(collapse.attributes('visible')).toBe('visible'); + const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2); + expect(iconNames).toEqual(['angle-down', 'folder-open']); + expect(folderName.classes('gl-font-weight-bold')).toBe(true); + expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath); + }); + + it('displays all environments when opened', async () => { + await button.trigger('click'); + + const names = resolvedFolder.environments.map((e) => + expect.stringMatching(e.nameWithoutType), + ); + const environments = wrapper + .findAllComponents(EnvironmentItem) + .wrappers.map((w) => w.text()); + expect(environments).toEqual(expect.arrayContaining(names)); + }); }); + }); - it('displays all environments when opened', async () => { - await button.trigger('click'); - - const names = resolvedFolder.environments.map((e) => - expect.stringMatching(e.nameWithoutType), + it.each(['available', 'stopped'])( + 'with scope=%s, fetches environments with scope', + async (scope) => { + wrapper = createWrapper({ nestedEnvironment, scope }, createApolloProvider()); + + await nextTick(); + await waitForPromises(); + + expect(environmentFolderMock).toHaveBeenCalledTimes(1); + expect(environmentFolderMock).toHaveBeenCalledWith( + {}, + { + environment: nestedEnvironment.latest, + scope, + }, + expect.anything(), + expect.anything(), ); - const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text()); - expect(environments).toEqual(expect.arrayContaining(names)); - }); - }); + }, + ); }); diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js index 2cc1c18d325..d903e1e2d56 100644 --- a/spec/frontend/environments/new_environments_app_spec.js +++ b/spec/frontend/environments/new_environments_app_spec.js @@ -9,6 +9,7 @@ import { sprintf, __, s__ } from '~/locale'; import EnvironmentsApp from '~/environments/components/new_environments_app.vue'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; import EnvironmentsItem from '~/environments/components/new_environment_item.vue'; +import EmptyState from '~/environments/components/empty_state.vue'; import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue'; import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue'; import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data'; @@ -121,6 +122,14 @@ describe('~/environments/components/new_environments_app.vue', () => { expect(text).toContainEqual(expect.stringMatching('production')); }); + it('should show an empty state with no environments', async () => { + await createWrapperWithMocked({ + environmentsApp: { ...resolvedEnvironmentsApp, environments: [] }, + }); + + expect(wrapper.findComponent(EmptyState).exists()).toBe(true); + }); + it('should show a button to create a new environment', async () => { await createWrapperWithMocked({ environmentsApp: resolvedEnvironmentsApp, diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js new file mode 100644 index 00000000000..bd1679baf48 --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js @@ -0,0 +1,250 @@ +import { Document, parseDocument } from 'yaml'; +import { GlProgressBar } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import PipelineWizardWrapper, { i18n } from '~/pipeline_wizard/components/wrapper.vue'; +import WizardStep from '~/pipeline_wizard/components/step.vue'; +import CommitStep from '~/pipeline_wizard/components/commit.vue'; +import YamlEditor from '~/pipeline_wizard/components/editor.vue'; +import { sprintf } from '~/locale'; +import { steps as stepsYaml } from '../mock/yaml'; + +describe('Pipeline Wizard - wrapper.vue', () => { + let wrapper; + const steps = parseDocument(stepsYaml).toJS(); + + const getAsYamlNode = (value) => new Document(value).contents; + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(PipelineWizardWrapper, { + propsData: { + projectPath: '/user/repo', + defaultBranch: 'main', + filename: '.gitlab-ci.yml', + steps: getAsYamlNode(steps), + ...props, + }, + }); + }; + const getEditorContent = () => { + return wrapper.getComponent(YamlEditor).attributes().doc.toString(); + }; + const getStepWrapper = () => wrapper.getComponent(WizardStep); + const getGlProgressBarWrapper = () => wrapper.getComponent(GlProgressBar); + + describe('display', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('shows the steps', () => { + createComponent(); + + expect(getStepWrapper().exists()).toBe(true); + }); + + it('shows the progress bar', () => { + createComponent(); + + const expectedMessage = sprintf(i18n.stepNofN, { + currentStep: 1, + stepCount: 3, + }); + + expect(wrapper.findByTestId('step-count').text()).toBe(expectedMessage); + expect(getGlProgressBarWrapper().exists()).toBe(true); + }); + + it('shows the editor', () => { + createComponent(); + + expect(wrapper.findComponent(YamlEditor).exists()).toBe(true); + }); + + it('shows the editor header with the default filename', () => { + createComponent(); + + const expectedMessage = sprintf(i18n.draft, { + filename: '.gitlab-ci.yml', + }); + + expect(wrapper.findByTestId('editor-header').text()).toBe(expectedMessage); + }); + + it('shows the editor header with a custom filename', async () => { + const filename = 'my-file.yml'; + createComponent({ + filename, + }); + + const expectedMessage = sprintf(i18n.draft, { + filename, + }); + + expect(wrapper.findByTestId('editor-header').text()).toBe(expectedMessage); + }); + }); + + describe('steps', () => { + const totalSteps = steps.length + 1; + + // **Note** on `expectProgressBarValue` + // Why are we expecting 50% here and not 66% or even 100%? + // The reason is mostly a UX thing. + // First, we count the commit step as an extra step, so that would + // be 66% by now (2 of 3). + // But then we add yet another one to the calc, because when we + // arrived on the second step's page, it's not *completed* (which is + // what the progress bar indicates). So in that case we're at 33%. + // Lastly, we want to start out with the progress bar not at zero, + // because UX research indicates that makes a process like this less + // intimidating, so we're always adding one step to the value bar + // (but not to the step counter. Now we're back at 50%. + describe.each` + step | navigationEventChain | expectStepNumber | expectCommitStepShown | expectStepDef | expectProgressBarValue + ${'initial step'} | ${[]} | ${1} | ${false} | ${steps[0]} | ${25} + ${'second step'} | ${['next']} | ${2} | ${false} | ${steps[1]} | ${50} + ${'commit step'} | ${['next', 'next']} | ${3} | ${true} | ${null} | ${75} + ${'stepping back'} | ${['next', 'back']} | ${1} | ${false} | ${steps[0]} | ${25} + ${'clicking next>next>back'} | ${['next', 'next', 'back']} | ${2} | ${false} | ${steps[1]} | ${50} + ${'clicking all the way through and back'} | ${['next', 'next', 'back', 'back']} | ${1} | ${false} | ${steps[0]} | ${25} + `( + '$step', + ({ + navigationEventChain, + expectStepNumber, + expectCommitStepShown, + expectStepDef, + expectProgressBarValue, + }) => { + beforeAll(async () => { + createComponent(); + for (const emittedValue of navigationEventChain) { + wrapper.findComponent({ ref: 'step' }).vm.$emit(emittedValue); + // We have to wait for the next step to be mounted + // before we can emit the next event, so we have to await + // inside the loop. + // eslint-disable-next-line no-await-in-loop + await nextTick(); + } + }); + + afterAll(() => { + wrapper.destroy(); + }); + + if (expectCommitStepShown) { + it('does not show the step wrapper', async () => { + expect(wrapper.findComponent(WizardStep).exists()).toBe(false); + }); + + it('shows the commit step page', () => { + expect(wrapper.findComponent(CommitStep).exists()).toBe(true); + }); + } else { + it('passes the correct step config to the step component', async () => { + expect(getStepWrapper().props('inputs')).toMatchObject(expectStepDef.inputs); + }); + + it('does not show the commit step page', () => { + expect(wrapper.findComponent(CommitStep).exists()).toBe(false); + }); + } + + it('updates the progress bar', () => { + expect(getGlProgressBarWrapper().attributes('value')).toBe(`${expectProgressBarValue}`); + }); + + it('updates the step number', () => { + const expectedMessage = sprintf(i18n.stepNofN, { + currentStep: expectStepNumber, + stepCount: totalSteps, + }); + + expect(wrapper.findByTestId('step-count').text()).toBe(expectedMessage); + }); + }, + ); + }); + + describe('editor overlay', () => { + beforeAll(() => { + createComponent(); + }); + + afterAll(() => { + wrapper.destroy(); + }); + + it('initially shows a placeholder', async () => { + const editorContent = getEditorContent(); + + await nextTick(); + + expect(editorContent).toBe('foo: $FOO\nbar: $BAR\n'); + }); + + it('shows an overlay with help text after setup', () => { + expect(wrapper.findByTestId('placeholder-overlay').exists()).toBe(true); + expect(wrapper.findByTestId('filename').text()).toBe('.gitlab-ci.yml'); + expect(wrapper.findByTestId('description').text()).toBe(i18n.overlayMessage); + }); + + it('does not show overlay when content has changed', async () => { + const newCompiledDoc = new Document({ faa: 'bur' }); + + await getStepWrapper().vm.$emit('update:compiled', newCompiledDoc); + await nextTick(); + + const overlay = wrapper.findByTestId('placeholder-overlay'); + + expect(overlay.exists()).toBe(false); + }); + }); + + describe('editor updates', () => { + beforeAll(() => { + createComponent(); + }); + + afterAll(() => { + wrapper.destroy(); + }); + + it('editor reflects changes', async () => { + const newCompiledDoc = new Document({ faa: 'bur' }); + await getStepWrapper().vm.$emit('update:compiled', newCompiledDoc); + + expect(getEditorContent()).toBe(newCompiledDoc.toString()); + }); + }); + + describe('line highlights', () => { + beforeAll(() => { + createComponent(); + }); + + afterAll(() => { + wrapper.destroy(); + }); + + it('highlight requests by the step get passed on to the editor', async () => { + const highlight = 'foo'; + + await getStepWrapper().vm.$emit('update:highlight', highlight); + + expect(wrapper.getComponent(YamlEditor).props('highlight')).toBe(highlight); + }); + + it('removes the highlight when clicking through to the commit step', async () => { + // Simulate clicking through all steps until the last one + await Promise.all( + steps.map(async () => { + await getStepWrapper().vm.$emit('next'); + await nextTick(); + }), + ); + + expect(wrapper.getComponent(YamlEditor).props('highlight')).toBe(null); + }); + }); +}); diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js index fe4fafb5750..5eaeaa32a8c 100644 --- a/spec/frontend/pipeline_wizard/mock/yaml.js +++ b/spec/frontend/pipeline_wizard/mock/yaml.js @@ -43,3 +43,43 @@ pages: only: - bar `; + +export const steps = ` +- inputs: + - label: foo + target: $FOO + widget: text + template: + foo: $FOO +- inputs: + - label: bar + target: $BAR + widget: text + template: + bar: $BAR +`; + +export const fullTemplate = ` +title: some title +description: some description +filename: foo.yml +steps: + - inputs: + - widget: text + label: foo + target: $BAR + template: + foo: $BAR +`; + +export const fullTemplateWithoutFilename = ` +title: some title +description: some description +steps: + - inputs: + - widget: text + label: foo + target: $BAR + template: + foo: $BAR +`; diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js new file mode 100644 index 00000000000..dd0304518a3 --- /dev/null +++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js @@ -0,0 +1,102 @@ +import { parseDocument } from 'yaml'; +import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue'; +import PipelineWizardWrapper from '~/pipeline_wizard/components/wrapper.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + fullTemplate as template, + fullTemplateWithoutFilename as templateWithoutFilename, +} from './mock/yaml'; + +const projectPath = 'foo/bar'; +const defaultBranch = 'main'; + +describe('PipelineWizard', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(PipelineWizard, { + propsData: { + projectPath, + defaultBranch, + template, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('mounts without error', () => { + const consoleSpy = jest.spyOn(console, 'error'); + + createComponent(); + + expect(consoleSpy).not.toHaveBeenCalled(); + expect(wrapper.exists()).toBe(true); + }); + + it('mounts the wizard wrapper', () => { + createComponent(); + + expect(wrapper.findComponent(PipelineWizardWrapper).exists()).toBe(true); + }); + + it('passes the correct steps prop to the wizard wrapper', () => { + createComponent(); + + expect(wrapper.findComponent(PipelineWizardWrapper).props('steps')).toEqual( + parseDocument(template).get('steps'), + ); + }); + + it('passes all other expected props to the wizard wrapper', () => { + createComponent(); + + expect(wrapper.findComponent(PipelineWizardWrapper).props()).toEqual( + expect.objectContaining({ + defaultBranch, + projectPath, + filename: parseDocument(template).get('filename'), + }), + ); + }); + + it('passes ".gitlab-ci.yml" as default filename to the wizard wrapper', () => { + createComponent({ template: templateWithoutFilename }); + + expect(wrapper.findComponent(PipelineWizardWrapper).attributes('filename')).toBe( + '.gitlab-ci.yml', + ); + }); + + it('allows overriding the defaultFilename with `defaultFilename` prop', () => { + const defaultFilename = 'foobar.yml'; + + createComponent({ + template: templateWithoutFilename, + defaultFilename, + }); + + expect(wrapper.findComponent(PipelineWizardWrapper).attributes('filename')).toBe( + defaultFilename, + ); + }); + + it('displays the title', () => { + createComponent(); + + expect(wrapper.findByTestId('title').text()).toBe( + parseDocument(template).get('title').toString(), + ); + }); + + it('displays the description', () => { + createComponent(); + + expect(wrapper.findByTestId('description').text()).toBe( + parseDocument(template).get('description').toString(), + ); + }); +}); diff --git a/spec/frontend/pipeline_wizard/validators_spec.js b/spec/frontend/pipeline_wizard/validators_spec.js new file mode 100644 index 00000000000..1276c642f30 --- /dev/null +++ b/spec/frontend/pipeline_wizard/validators_spec.js @@ -0,0 +1,22 @@ +import { Document, parseDocument } from 'yaml'; +import { isValidStepSeq } from '~/pipeline_wizard/validators'; +import { steps as stepsYaml } from './mock/yaml'; + +describe('prop validation', () => { + const steps = parseDocument(stepsYaml).toJS(); + const getAsYamlNode = (value) => new Document(value).contents; + + it('allows passing yaml nodes to the steps prop', () => { + const validSteps = getAsYamlNode(steps); + expect(isValidStepSeq(validSteps)).toBe(true); + }); + + it.each` + scenario | stepsValue + ${'not a seq'} | ${{ foo: 'bar' }} + ${'a step missing an input'} | ${[{ template: 'baz: boo' }]} + ${'an empty seq'} | ${[]} + `('throws an error when passing $scenario to the steps prop', ({ stepsValue }) => { + expect(isValidStepSeq(stepsValue)).toBe(false); + }); +}); diff --git a/spec/lib/gitlab/usage_counters/pod_logs_spec.rb b/spec/lib/gitlab/usage_counters/pod_logs_spec.rb new file mode 100644 index 00000000000..1059c519b19 --- /dev/null +++ b/spec/lib/gitlab/usage_counters/pod_logs_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageCounters::PodLogs, :clean_gitlab_redis_shared_state do + it_behaves_like 'a usage counter' +end diff --git a/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb new file mode 100644 index 00000000000..c00685c1397 --- /dev/null +++ b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RemoveDependencyListUsageDataFromRedis, :migration, :clean_gitlab_redis_shared_state do + let(:key) { "DEPENDENCY_LIST_USAGE_COUNTER" } + + describe "#up" do + it 'removes the hash from redis' do + with_redis do |redis| + redis.hincrby(key, 1, 1) + redis.hincrby(key, 2, 1) + end + + expect { migrate! }.to change { with_redis { |r| r.hgetall(key) } }.from({ '1' => '1', '2' => '1' }).to({}) + end + end + + def with_redis(&block) + Gitlab::Redis::SharedState.with(&block) + end +end diff --git a/spec/presenters/search_service_presenter_spec.rb b/spec/presenters/search_service_presenter_spec.rb index 06ece838d8d..af9fee8cfd9 100644 --- a/spec/presenters/search_service_presenter_spec.rb +++ b/spec/presenters/search_service_presenter_spec.rb @@ -4,13 +4,33 @@ require 'spec_helper' RSpec.describe SearchServicePresenter do let(:user) { create(:user) } + let(:search) { '' } let(:search_service) { SearchService.new(user, search: search, scope: scope) } let(:presenter) { described_class.new(search_service, current_user: user) } + describe '#search_objects' do + let(:search_objects) { Kaminari::PaginatableArray.new([]) } + + context 'objects do not respond to eager_load' do + before do + allow(search_service).to receive(:search_objects).and_return(search_objects) + allow(search_objects).to receive(:respond_to?).with(:eager_load).and_return(false) + end + + context 'users scope' do + let(:scope) { 'users' } + + it 'does not eager load anything' do + expect(search_objects).not_to receive(:eager_load) + presenter.search_objects + end + end + end + end + describe '#show_results_status?' do using RSpec::Parameterized::TableSyntax - let(:search) { '' } let(:scope) { nil } before do diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb new file mode 100644 index 00000000000..848437577d7 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a usage counter' do + describe '.increment' do + let(:project_id) { 12 } + + it 'intializes and increments the counter for the project by 1' do + expect do + described_class.increment(project_id) + end.to change { described_class.usage_totals[project_id] }.from(nil).to(1) + end + end + + describe '.usage_totals' do + let(:usage_totals) { described_class.usage_totals } + + context 'when the feature has not been used' do + it 'returns the total counts and counts per project' do + expect(usage_totals.keys).to eq([:total]) + expect(usage_totals[:total]).to eq(0) + end + end + + context 'when the feature has been used in multiple projects' do + let(:project1_id) { 12 } + let(:project2_id) { 16 } + + before do + described_class.increment(project1_id) + described_class.increment(project2_id) + end + + it 'returns the total counts and counts per project' do + expect(usage_totals[project1_id]).to eq(1) + expect(usage_totals[project2_id]).to eq(1) + expect(usage_totals[:total]).to eq(2) + end + end + end +end diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 40c6d400dab..f1ace9878e9 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -126,7 +126,7 @@ RSpec.shared_examples 'namespace traversal scopes' do end context 'with offset and limit' do - subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).offset(1).limit(1).self_and_ancestors } + subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).order(:traversal_ids).offset(1).limit(1).self_and_ancestors } it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } end @@ -185,6 +185,7 @@ RSpec.shared_examples 'namespace traversal scopes' do subject do described_class .where(id: [deep_nested_group_1, deep_nested_group_2]) + .order(:traversal_ids) .limit(1) .offset(1) .self_and_ancestor_ids @@ -240,7 +241,7 @@ RSpec.shared_examples 'namespace traversal scopes' do end context 'with offset and limit' do - subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants } + subject { described_class.where(id: [group_1, group_2]).order(:traversal_ids).offset(1).limit(1).self_and_descendants } it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } end @@ -288,6 +289,7 @@ RSpec.shared_examples 'namespace traversal scopes' do subject do described_class .where(id: [group_1, group_2]) + .order(:traversal_ids) .limit(1) .offset(1) .self_and_descendant_ids |