Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e46506bcc3
commit
c511df8a7e
42 changed files with 1026 additions and 266 deletions
|
@ -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 };
|
||||
|
|
|
@ -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 }"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
185
app/assets/javascripts/pipeline_wizard/components/wrapper.vue
Normal file
185
app/assets/javascripts/pipeline_wizard/components/wrapper.vue
Normal file
|
@ -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>
|
65
app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
Normal file
65
app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
Normal file
|
@ -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>
|
4
app/assets/javascripts/pipeline_wizard/validators.js
Normal file
4
app/assets/javascripts/pipeline_wizard/validators.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { isSeq } from 'yaml';
|
||||
|
||||
export const isValidStepSeq = (v) =>
|
||||
isSeq(v) && v.items.every((s) => s.get('inputs') && s.get('template'));
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 } }
|
||||
|
|
|
@ -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
|
|
@ -363,6 +363,10 @@ module.exports = {
|
|||
name: '[name].[contenthash:8].[ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(yml|yaml)$/,
|
||||
loader: 'raw-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
1
db/schema_migrations/20220310095341
Normal file
1
db/schema_migrations/20220310095341
Normal file
|
@ -0,0 +1 @@
|
|||
56d906eac31954988bd0659eabbc9f1bad1a47dd616fb99e4b90b56b2bf4c6a0
|
1
db/schema_migrations/20220310141349
Normal file
1
db/schema_migrations/20220310141349
Normal file
|
@ -0,0 +1 @@
|
|||
39785d4140c7345ddbe62417576381654ce22d505ee5c92a84425f0a3f8e4935
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
|
||||
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
folderName = wrapper.findByText(nestedEnvironment.name);
|
||||
button = wrapper.findByRole('button', { name: __('Expand') });
|
||||
});
|
||||
|
||||
it('displays the name of the folder', () => {
|
||||
expect(folderName.text()).toBe(nestedEnvironment.name);
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapse', () => {
|
||||
let icons;
|
||||
let collapse;
|
||||
it.each(['available', 'stopped'])(
|
||||
'with scope=%s, fetches environments with scope',
|
||||
async (scope) => {
|
||||
wrapper = createWrapper({ nestedEnvironment, scope }, createApolloProvider());
|
||||
|
||||
beforeEach(() => {
|
||||
collapse = wrapper.findComponent(GlCollapse);
|
||||
icons = wrapper.findAllComponents(GlIcon);
|
||||
});
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
|
||||
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),
|
||||
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));
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
250
spec/frontend/pipeline_wizard/components/wrapper_spec.js
Normal file
250
spec/frontend/pipeline_wizard/components/wrapper_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
`;
|
||||
|
|
102
spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
Normal file
102
spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
Normal file
|
@ -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(),
|
||||
);
|
||||
});
|
||||
});
|
22
spec/frontend/pipeline_wizard/validators_spec.js
Normal file
22
spec/frontend/pipeline_wizard/validators_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
7
spec/lib/gitlab/usage_counters/pod_logs_spec.rb
Normal file
7
spec/lib/gitlab/usage_counters/pod_logs_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue