Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-13 00:10:38 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-13 00:10:38 +0300
commit3b69a04945341516a2ed6a291769c50fe04336df (patch)
tree5910b5f0c80bf98aded05305bbaa7fd30d2742c4
parente4cfc16da343c2008053ee09bb6af7145a6924cb (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/static-analysis.gitlab-ci.yml11
-rw-r--r--app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_interval_description.vue52
-rw-r--r--app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue123
-rw-r--r--app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js32
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/ci_cd/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/show/emoji_menu.js19
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js98
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue3
-rw-r--r--app/assets/javascripts/profile/profile.js24
-rw-r--r--app/assets/javascripts/runner/components/runner_detail.vue9
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue46
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/runner/utils.js11
-rw-r--r--app/assets/javascripts/set_status_modal/constants.js14
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue70
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue22
-rw-r--r--app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue100
-rw-r--r--app/assets/javascripts/set_status_modal/utils.js5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue10
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql10
-rw-r--r--app/controllers/admin/runners_controller.rb10
-rw-r--r--app/controllers/groups/runners_controller.rb6
-rw-r--r--app/controllers/profiles_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb6
-rw-r--r--app/graphql/mutations/ci/runner/update.rb5
-rw-r--r--app/helpers/application_settings_helper.rb8
-rw-r--r--app/models/user_status.rb4
-rw-r--r--app/services/ci/runners/update_runner_service.rb9
-rw-r--r--app/services/system_notes/issuables_service.rb11
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/profiles/show.html.haml41
-rw-r--r--data/deprecations/15-4-cs-docker-variables.yml11
-rw-r--r--doc/ci/runners/configure_runners.md40
-rw-r--r--doc/ci/yaml/artifacts_reports.md3
-rw-r--r--doc/development/backend/ruby_style_guide.md103
-rw-r--r--doc/development/newlines_styleguide.md111
-rw-r--r--doc/development/rake_tasks.md14
-rw-r--r--doc/install/cloud_native/index.md27
-rw-r--r--doc/update/deprecations.md14
-rw-r--r--doc/user/profile/index.md2
-rw-r--r--lib/api/ci/runners.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/github_import/importer/events/changed_reviewer.rb54
-rw-r--r--lib/gitlab/github_import/importer/issue_event_importer.rb2
-rw-r--r--lib/gitlab/github_import/representation/issue_event.rb7
-rw-r--r--lib/gitlab/github_import/user_finder.rb4
-rw-r--r--lib/tasks/rubocop.rake13
-rw-r--r--locale/gitlab.pot60
-rw-r--r--qa/lib/gitlab/page/admin/subscription.rb9
-rw-r--r--rubocop/check_graceful_task.rb83
-rw-r--r--rubocop/cop_todo.rb6
-rw-r--r--rubocop/formatter/graceful_formatter.rb109
-rw-r--r--rubocop/formatter/todo_formatter.rb24
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb4
-rw-r--r--spec/controllers/profiles_controller_spec.rb8
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb20
-rw-r--r--spec/frontend/__helpers__/dl_locator_helper.js13
-rw-r--r--spec/frontend/emoji/components/category_spec.js8
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js10
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js6
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js4
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js2
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js18
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js4
-rw-r--r--spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js10
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js14
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js16
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js10
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js2
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js2
-rw-r--r--spec/frontend/issuable/components/issue_milestone_spec.js2
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js6
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_list_spec.js4
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js10
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js10
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_item_spec.js4
-rw-r--r--spec/frontend/issues/show/components/app_spec.js2
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js2
-rw-r--r--spec/frontend/issues/show/components/fields/title_spec.js2
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js4
-rw-r--r--spec/frontend/issues/show/components/incidents/highlight_bar_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js6
-rw-r--r--spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js8
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js10
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js2
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js2
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js2
-rw-r--r--spec/frontend/labels/components/delete_label_modal_spec.js2
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js2
-rw-r--r--spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js4
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js2
-rw-r--r--spec/frontend/notebook/cells/output/latex_spec.js2
-rw-r--r--spec/frontend/notes/components/note_header_spec.js2
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js10
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js6
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js2
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js2
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js8
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js6
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js115
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js30
-rw-r--r--spec/frontend/runner/utils_spec.js13
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js37
-rw-r--r--spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js156
-rw-r--r--spec/frontend/set_status_modal/utils_spec.js3
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js10
-rw-r--r--spec/frontend/work_items/mock_data.js11
-rw-r--r--spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb101
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/representation/issue_event_spec.rb44
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb12
-rw-r--r--spec/models/ci/runner_spec.rb2
-rw-r--r--spec/models/user_status_spec.rb8
-rw-r--r--spec/rubocop/check_graceful_task_spec.rb125
-rw-r--r--spec/rubocop/cop_todo_spec.rb20
-rw-r--r--spec/rubocop/formatter/graceful_formatter_spec.rb239
-rw-r--r--spec/rubocop/formatter/todo_formatter_spec.rb92
-rw-r--r--spec/services/ci/runners/update_runner_service_spec.rb80
-rw-r--r--spec/support/matchers/abort_matcher.rb19
-rw-r--r--spec/support/rspec.rb1
-rw-r--r--spec/tasks/rubocop_rake_spec.rb38
170 files changed, 2266 insertions, 852 deletions
diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml
index b2cbfedbfbd..cf25a33ed62 100644
--- a/.gitlab/ci/static-analysis.gitlab-ci.yml
+++ b/.gitlab/ci/static-analysis.gitlab-ci.yml
@@ -19,7 +19,10 @@ update-static-analysis-cache:
- .shared:rules:update-cache
stage: prepare
script:
- - run_timed_command "bundle exec rubocop --parallel" # For the moment we only cache `tmp/rubocop_cache` so we don't need to run all the tasks.
+ # Silence cop offenses for rules with "grace period".
+ # This will notify Slack if offenses were silenced.
+ # For the moment we only cache `tmp/rubocop_cache` so we don't need to run all the tasks.
+ - run_timed_command "bundle exec rake rubocop:check:graceful"
static-analysis:
extends:
@@ -121,7 +124,11 @@ rubocop:
- |
# For non-merge request, or when RUN_ALL_RUBOCOP is 'true', run all RuboCop rules
if [ -z "${CI_MERGE_REQUEST_IID}" ] || [ "${RUN_ALL_RUBOCOP}" == "true" ]; then
- run_timed_command "bundle exec rubocop --parallel"
+ # Silence cop offenses for rules with "grace period".
+ # We won't notify Slack if offenses were silenced to avoid frequent messages.
+ # Job `update-static-analysis-cache` takes care of Slack notifications every 2 hours.
+ unset CI_SLACK_WEBHOOK_URL
+ run_timed_command "bundle exec rake rubocop:check:graceful"
else
run_timed_command "bundle exec rubocop --parallel --force-exclusion $(cat ${RSPEC_CHANGED_FILES_PATH})"
fi
diff --git a/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_interval_description.vue b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_interval_description.vue
new file mode 100644
index 00000000000..2f74b44625f
--- /dev/null
+++ b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_interval_description.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ fieldHelpText: s__(
+ 'AdminSettings|If no unit is written, it defaults to seconds. For example, these are all equivalent: %{oneDayInSeconds}, %{oneDayInHoursHumanReadable}, or %{oneDayHumanReadable}. Minimum value is two hours. %{linkStart}Learn more.%{linkEnd}',
+ ),
+ },
+ computed: {
+ helpUrl() {
+ return helpPagePath('ci/runners/configure_runners', {
+ anchor: 'authentication-token-security',
+ });
+ },
+ },
+};
+</script>
+<template>
+ <p>
+ {{ message }}
+ <gl-sprintf :message="$options.i18n.fieldHelpText">
+ <template #oneDayInSeconds>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>86400</code>
+ </template>
+ <template #oneDayInHoursHumanReadable>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>24 hours</code>
+ </template>
+ <template #oneDayHumanReadable>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>1 day</code>
+ </template>
+ <template #link>
+ <gl-link :href="helpUrl" target="_blank">{{ __('Learn more.') }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+</template>
diff --git a/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue
new file mode 100644
index 00000000000..371a26d2664
--- /dev/null
+++ b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue
@@ -0,0 +1,123 @@
+<script>
+import { GlFormGroup } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
+import ExpirationIntervalDescription from './expiration_interval_description.vue';
+
+export default {
+ components: {
+ ChronicDurationInput,
+ ExpirationIntervalDescription,
+ GlFormGroup,
+ },
+ props: {
+ instanceRunnerExpirationInterval: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ groupRunnerExpirationInterval: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ projectRunnerExpirationInterval: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ perInput: {
+ instance: {
+ value: this.instanceRunnerExpirationInterval,
+ valid: null,
+ feedback: '',
+ },
+ group: {
+ value: this.groupRunnerExpirationInterval,
+ valid: null,
+ feedback: '',
+ },
+ project: {
+ value: this.projectRunnerExpirationInterval,
+ valid: null,
+ feedback: '',
+ },
+ },
+ };
+ },
+ methods: {
+ updateValidity(obj, event) {
+ /* eslint-disable no-param-reassign */
+ obj.valid = event.valid;
+ obj.feedback = event.feedback;
+ /* eslint-enable no-param-reassign */
+ },
+ },
+ i18n: {
+ instanceRunnerTitle: s__('AdminSettings|Instance runners expiration'),
+ instanceRunnerDescription: s__(
+ 'AdminSettings|Set the expiration time of authentication tokens of newly registered instance runners. Authentication tokens are automatically reset at these intervals.',
+ ),
+ groupRunnerTitle: s__('AdminSettings|Group runners expiration'),
+ groupRunnerDescription: s__(
+ 'AdminSettings|Set the expiration time of authentication tokens of newly registered group runners.',
+ ),
+ projectRunnerTitle: s__('AdminSettings|Project runners expiration'),
+ projectRunnerDescription: s__(
+ 'AdminSettings|Set the expiration time of authentication tokens of newly registered project runners.',
+ ),
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group
+ :label="$options.i18n.instanceRunnerTitle"
+ :invalid-feedback="perInput.instance.feedback"
+ :state="perInput.instance.valid"
+ >
+ <template #description>
+ <expiration-interval-description :message="$options.i18n.instanceRunnerDescription" />
+ </template>
+ <chronic-duration-input
+ v-model="perInput.instance.value"
+ name="application_setting[runner_token_expiration_interval]"
+ :state="perInput.instance.valid"
+ @valid="updateValidity(perInput.instance, $event)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.groupRunnerTitle"
+ :invalid-feedback="perInput.group.feedback"
+ :state="perInput.group.valid"
+ >
+ <template #description>
+ <expiration-interval-description :message="$options.i18n.groupRunnerDescription" />
+ </template>
+ <chronic-duration-input
+ v-model="perInput.group.value"
+ name="application_setting[group_runner_token_expiration_interval]"
+ :state="perInput.group.valid"
+ @valid="updateValidity(perInput.group, $event)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.projectRunnerTitle"
+ :invalid-feedback="perInput.project.feedback"
+ :state="perInput.project.valid"
+ >
+ <template #description>
+ <expiration-interval-description :message="$options.i18n.projectRunnerDescription" />
+ </template>
+ <chronic-duration-input
+ v-model="perInput.project.value"
+ name="application_setting[project_runner_token_expiration_interval]"
+ :state="perInput.project.valid"
+ @valid="updateValidity(perInput.project, $event)"
+ />
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js b/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js
new file mode 100644
index 00000000000..79d7ff0451a
--- /dev/null
+++ b/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import { parseInterval } from '~/runner/utils';
+import ExpirationIntervals from './components/expiration_intervals.vue';
+
+const initRunnerTokenExpirationIntervals = (selector = '#js-runner-token-expiration-intervals') => {
+ const el = document.querySelector(selector);
+
+ if (!el) {
+ return null;
+ }
+
+ const {
+ instanceRunnerTokenExpirationInterval,
+ groupRunnerTokenExpirationInterval,
+ projectRunnerTokenExpirationInterval,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(ExpirationIntervals, {
+ props: {
+ instanceRunnerExpirationInterval: parseInterval(instanceRunnerTokenExpirationInterval),
+ groupRunnerExpirationInterval: parseInterval(groupRunnerTokenExpirationInterval),
+ projectRunnerExpirationInterval: parseInterval(projectRunnerTokenExpirationInterval),
+ },
+ });
+ },
+ });
+};
+
+export default initRunnerTokenExpirationIntervals;
diff --git a/app/assets/javascripts/pages/admin/application_settings/ci_cd/index.js b/app/assets/javascripts/pages/admin/application_settings/ci_cd/index.js
new file mode 100644
index 00000000000..9b6fba9876e
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/ci_cd/index.js
@@ -0,0 +1,3 @@
+import initRunnerTokenExpirationIntervals from '~/admin/application_settings/runner_token_expiration/index';
+
+initRunnerTokenExpirationIntervals();
diff --git a/app/assets/javascripts/pages/profiles/show/emoji_menu.js b/app/assets/javascripts/pages/profiles/show/emoji_menu.js
deleted file mode 100644
index 286c1f1e929..00000000000
--- a/app/assets/javascripts/pages/profiles/show/emoji_menu.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import '~/commons/bootstrap';
-import { AwardsHandler } from '~/awards_handler';
-
-class EmojiMenu extends AwardsHandler {
- constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback) {
- super(emoji);
-
- this.selectEmojiCallback = selectEmojiCallback;
- this.toggleButtonSelector = toggleButtonSelector;
- this.menuClass = menuClass;
- }
-
- postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
- this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
- callback();
- }
-}
-
-export default EmojiMenu;
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index ccdb9e991ca..96ea7329e6e 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,90 +1,18 @@
import emojiRegex from 'emoji-regex';
-import $ from 'jquery';
-import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import * as Emoji from '~/emoji';
-import createFlash from '~/flash';
import { __ } from '~/locale';
-import EmojiMenu from './emoji_menu';
+import { initSetStatusForm } from '~/profile/profile';
-const defaultStatusEmoji = 'speech_balloon';
-const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
-const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
-const statusEmojiField = document.getElementById('js-status-emoji-field');
-const statusMessageField = document.getElementById('js-status-message-field');
-
-const toggleNoEmojiPlaceholder = (isVisible) => {
- const placeholderElement = document.getElementById('js-no-emoji-placeholder');
- placeholderElement.classList.toggle('hidden', !isVisible);
-};
-
-const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
-const removeStatusEmoji = () => {
- const statusEmoji = findStatusEmoji();
- if (statusEmoji) {
- statusEmoji.remove();
- }
-};
-
-const selectEmojiCallback = (emoji, emojiTag) => {
- statusEmojiField.value = emoji;
- toggleNoEmojiPlaceholder(false);
- removeStatusEmoji();
- // eslint-disable-next-line no-unsanitized/property
- toggleEmojiMenuButton.innerHTML += emojiTag;
-};
-
-const clearEmojiButton = document.getElementById('js-clear-user-status-button');
-clearEmojiButton.addEventListener('click', () => {
- statusEmojiField.value = '';
- statusMessageField.value = '';
- removeStatusEmoji();
- toggleNoEmojiPlaceholder(true);
-});
-
-const emojiAutocomplete = new GfmAutoComplete();
-emojiAutocomplete.setup($(statusMessageField), { emojis: true });
+initSetStatusForm();
const userNameInput = document.getElementById('user_name');
-userNameInput.addEventListener('input', () => {
- const EMOJI_REGEX = emojiRegex();
- if (EMOJI_REGEX.test(userNameInput.value)) {
- // set field to invalid so it gets detected by GlFieldErrors
- userNameInput.setCustomValidity(__('Invalid field'));
- } else {
- userNameInput.setCustomValidity('');
- }
-});
-
-Emoji.initEmojiMap()
- .then(() => {
- const emojiMenu = new EmojiMenu(
- Emoji,
- toggleEmojiMenuButtonSelector,
- 'js-status-emoji-menu',
- selectEmojiCallback,
- );
- emojiMenu.bindEvents();
-
- const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
- statusMessageField.addEventListener('input', () => {
- const hasStatusMessage = statusMessageField.value.trim() !== '';
- const statusEmoji = findStatusEmoji();
- if (hasStatusMessage && statusEmoji) {
- return;
- }
-
- if (hasStatusMessage) {
- toggleNoEmojiPlaceholder(false);
- // eslint-disable-next-line no-unsanitized/property
- toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
- } else if (statusEmoji.dataset.name === defaultStatusEmoji) {
- toggleNoEmojiPlaceholder(true);
- removeStatusEmoji();
- }
- });
- })
- .catch(() =>
- createFlash({
- message: __('Failed to load emoji list.'),
- }),
- );
+if (userNameInput) {
+ userNameInput.addEventListener('input', () => {
+ const EMOJI_REGEX = emojiRegex();
+ if (EMOJI_REGEX.test(userNameInput.value)) {
+ // set field to invalid so it gets detected by GlFieldErrors
+ userNameInput.setCustomValidity(__('Invalid field'));
+ } else {
+ userNameInput.setCustomValidity('');
+ }
+ });
+}
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 31a34ab4fb5..1a05710a13e 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -170,7 +170,7 @@ export default {
ref="mainPipelineContainer"
class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
:class="{
- 'gl-pipeline-min-h gl-py-5 gl-overflow-auto gl-border-t-solid gl-border-t-1 gl-border-gray-100': !isLinkedPipeline,
+ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
}"
>
<linked-graph-wrapper>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 8d764fad0c5..02d0c07ea54 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -82,7 +82,9 @@ export default {
:stage-name="stageName"
/>
- <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
+ <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4 gl-align-self-center">
+ {{ group.size }}
+ </div>
</div>
</button>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 42988e96692..4aec28295bd 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -64,8 +64,7 @@ export default {
},
},
jobClasses: [
- 'gl-py-3',
- 'gl-px-4',
+ 'gl-p-3',
'gl-border-gray-100',
'gl-border-solid',
'gl-border-1',
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 064bcf8e4c4..af5beeb686c 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,11 +1,14 @@
import $ from 'jquery';
+import Vue from 'vue';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseRailsFormFields } from '~/lib/utils/forms';
import { Rails } from '~/lib/utils/rails_ujs';
import TimezoneDropdown, {
formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
export default class Profile {
constructor({ form } = {}) {
@@ -116,3 +119,24 @@ export default class Profile {
}
}
}
+
+export const initSetStatusForm = () => {
+ const el = document.getElementById('js-user-profile-set-status-form');
+
+ if (!el) {
+ return null;
+ }
+
+ const fields = parseRailsFormFields(el);
+
+ return new Vue({
+ el,
+ name: 'UserProfileStatusForm',
+ provide: {
+ fields,
+ },
+ render(h) {
+ return h(UserProfileSetStatusWrapper);
+ },
+ });
+};
diff --git a/app/assets/javascripts/runner/components/runner_detail.vue b/app/assets/javascripts/runner/components/runner_detail.vue
index 584f77b7648..c260670b517 100644
--- a/app/assets/javascripts/runner/components/runner_detail.vue
+++ b/app/assets/javascripts/runner/components/runner_detail.vue
@@ -21,7 +21,8 @@ export default {
props: {
label: {
type: String,
- required: true,
+ default: null,
+ required: false,
},
value: {
type: String,
@@ -39,7 +40,11 @@ export default {
<template>
<div class="gl-display-contents">
- <dt class="gl-mb-5 gl-mr-6 gl-max-w-26">{{ label }}</dt>
+ <dt class="gl-mb-5 gl-mr-6 gl-max-w-26">
+ <template v-if="label || $scopedSlots.label">
+ <slot name="label">{{ label }}</slot>
+ </template>
+ </dt>
<dd class="gl-mb-5">
<template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot>
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index d5222f39b81..79f934764c6 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -1,7 +1,10 @@
<script>
-import { GlIntersperse } from '@gitlab/ui';
+import { GlIntersperse, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue';
@@ -12,6 +15,8 @@ import RunnerTags from './runner_tags.vue';
export default {
components: {
GlIntersperse,
+ GlLink,
+ HelpPopover,
RunnerDetail,
RunnerMaintenanceNoteDetail: () =>
import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
@@ -24,6 +29,7 @@ export default {
RunnerTags,
TimeAgo,
},
+ mixins: [glFeatureFlagMixin()],
props: {
runner: {
type: Object,
@@ -60,6 +66,16 @@ export default {
isProjectRunner() {
return this.runner?.runnerType === PROJECT_TYPE;
},
+ tokenExpirationHelpPopoverOptions() {
+ return {
+ title: s__('Runners|Runner authentication token expiration'),
+ };
+ },
+ tokenExpirationHelpUrl() {
+ return helpPagePath('ci/runners/configure_runners', {
+ anchor: 'authentication-token-security',
+ });
+ },
},
ACCESS_LEVEL_REF_PROTECTED,
};
@@ -101,6 +117,34 @@ export default {
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
+ <runner-detail
+ v-if="glFeatures.enforceRunnerTokenExpiresAt"
+ :empty-value="s__('Runners|Never expires')"
+ >
+ <template #label>
+ {{ s__('Runners|Token expiry') }}
+ <help-popover :options="tokenExpirationHelpPopoverOptions">
+ <p>
+ {{
+ s__(
+ 'Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired.',
+ )
+ }}
+ </p>
+ <p class="gl-mb-0">
+ <gl-link
+ :href="tokenExpirationHelpUrl"
+ target="_blank"
+ class="gl-reset-font-size"
+ >{{ __('Learn more') }}</gl-link
+ >
+ </p>
+ </help-popover>
+ </template>
+ <template v-if="runner.tokenExpiresAt" #value>
+ <time-ago :time="runner.tokenExpiresAt" />
+ </template>
+ </runner-detail>
<runner-detail :label="s__('Runners|Tags')">
<template v-if="tagList.length" #value>
<runner-tags class="gl-vertical-align-middle" :tag-list="tagList" size="sm" />
diff --git a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql
index 499c0156770..b5689ff7687 100644
--- a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -17,6 +17,7 @@ fragment RunnerDetailsShared on CiRunner {
createdAt
status(legacyMode: null)
contactedAt
+ tokenExpiresAt
version
editAdminUrl
userPermissions {
diff --git a/app/assets/javascripts/runner/utils.js b/app/assets/javascripts/runner/utils.js
index cb2917a92fd..1ca0a9e86b5 100644
--- a/app/assets/javascripts/runner/utils.js
+++ b/app/assets/javascripts/runner/utils.js
@@ -70,3 +70,14 @@ export const getPaginationVariables = (pagination, pageSize = 10) => {
// Get the first N items
return { first: pageSize };
};
+
+/**
+ * Turns a server-provided interval integer represented as a string into an
+ * integer that the frontend can use.
+ *
+ * @param {String} interval - String to convert
+ * @returns Parsed integer
+ */
+export const parseInterval = (interval) => {
+ return typeof interval === 'string' ? parseInt(interval, 10) : null;
+};
diff --git a/app/assets/javascripts/set_status_modal/constants.js b/app/assets/javascripts/set_status_modal/constants.js
new file mode 100644
index 00000000000..53e64db1497
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/constants.js
@@ -0,0 +1,14 @@
+import { timeRanges } from '~/vue_shared/constants';
+import { __ } from '~/locale';
+
+export const NEVER_TIME_RANGE = {
+ label: __('Never'),
+ name: 'never',
+};
+
+export const TIME_RANGES_WITH_NEVER = [NEVER_TIME_RANGE, ...timeRanges];
+
+export const AVAILABILITY_STATUS = {
+ BUSY: 'busy',
+ NOT_SET: 'not_set',
+};
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 79af9143861..7f9a30b7ff1 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -9,26 +9,14 @@ import {
GlDropdown,
GlDropdownItem,
GlSprintf,
+ GlFormGroup,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
-import { __, s__ } from '~/locale';
-import { timeRanges } from '~/vue_shared/constants';
-
-export const AVAILABILITY_STATUS = {
- BUSY: 'busy',
- NOT_SET: 'not_set',
-};
-
-const statusTimeRanges = [
- {
- label: __('Never'),
- name: 'never',
- },
- ...timeRanges,
-];
+import { s__ } from '~/locale';
+import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS } from './constants';
export default {
components: {
@@ -40,6 +28,7 @@ export default {
GlDropdown,
GlDropdownItem,
GlSprintf,
+ GlFormGroup,
EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
directives: {
@@ -136,7 +125,8 @@ export default {
this.clearEmoji();
},
},
- statusTimeRanges,
+ TIME_RANGES_WITH_NEVER,
+ AVAILABILITY_STATUS,
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
i18n: {
statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`),
@@ -153,14 +143,11 @@ export default {
<template>
<div>
- <input :value="emoji" class="js-status-emoji-field" type="hidden" name="user[status][emoji]" />
<gl-form-input-group class="gl-mb-5">
<gl-form-input
ref="statusMessageField"
:value="message"
:placeholder="$options.i18n.statusMessagePlaceholder"
- class="js-status-message-field"
- name="user[status][message]"
@keyup="setDefaultEmoji"
@input="$emit('message-input', $event)"
@keyup.enter.prevent
@@ -216,28 +203,29 @@ export default {
</template>
</gl-form-checkbox>
- <div class="form-group">
- <div class="gl-display-flex gl-align-items-baseline">
- <span class="gl-mr-3">{{ $options.i18n.clearStatusAfterDropdownLabel }}</span>
- <gl-dropdown :text="clearStatusAfter.label" data-testid="clear-status-at-dropdown">
- <gl-dropdown-item
- v-for="after in $options.statusTimeRanges"
- :key="after.name"
- :data-testid="after.name"
- @click="$emit('clear-status-after-click', after)"
- >{{ after.label }}</gl-dropdown-item
- >
- </gl-dropdown>
- </div>
- <p
- v-if="currentClearStatusAfter.length"
- class="gl-mt-3 gl-text-gray-400 gl-font-sm"
- data-testid="clear-status-at-message"
+ <gl-form-group :label="$options.i18n.clearStatusAfterDropdownLabel" class="gl-mb-0">
+ <gl-dropdown
+ block
+ :text="clearStatusAfter.label"
+ data-testid="clear-status-at-dropdown"
+ toggle-class="gl-mb-0 gl-form-input-md"
>
- <gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
- <template #date>{{ currentClearStatusAfter }}</template>
- </gl-sprintf>
- </p>
- </div>
+ <gl-dropdown-item
+ v-for="after in $options.TIME_RANGES_WITH_NEVER"
+ :key="after.name"
+ :data-testid="after.name"
+ @click="$emit('clear-status-after-click', after)"
+ >{{ after.label }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+
+ <template v-if="currentClearStatusAfter.length" #description>
+ <span data-testid="clear-status-at-message">
+ <gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
+ <template #date>{{ currentClearStatusAfter }}</template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </gl-form-group>
</div>
</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 3d0c9d76435..80b1cb8c4d5 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -3,28 +3,15 @@ import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitl
import Vue from 'vue';
import createFlash from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
-import { timeRanges } from '~/vue_shared/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isUserBusy } from './utils';
+import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
import SetStatusForm from './set_status_form.vue';
-export const AVAILABILITY_STATUS = {
- BUSY: 'busy',
- NOT_SET: 'not_set',
-};
-
Vue.use(GlToast);
-const statusTimeRanges = [
- {
- label: __('Never'),
- name: 'never',
- },
- ...timeRanges,
-];
-
export default {
components: {
GlModal,
@@ -67,7 +54,7 @@ export default {
message: this.currentMessage,
modalId: 'set-user-status-modal',
availability: isUserBusy(this.currentAvailability),
- clearStatusAfter: statusTimeRanges[0],
+ clearStatusAfter: NEVER_TIME_RANGE,
};
},
mounted() {
@@ -91,7 +78,7 @@ export default {
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
clearStatusAfter:
- clearStatusAfter.label === statusTimeRanges[0].label ? null : clearStatusAfter.shortcut,
+ clearStatusAfter.label === NEVER_TIME_RANGE.label ? null : clearStatusAfter.shortcut,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
@@ -123,7 +110,6 @@ export default {
this.availability = value;
},
},
- statusTimeRanges,
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
actionPrimary: { text: s__('SetStatusModal|Set status') },
actionSecondary: { text: s__('SetStatusModal|Remove status') },
diff --git a/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue b/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue
new file mode 100644
index 00000000000..c709611e13d
--- /dev/null
+++ b/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue
@@ -0,0 +1,100 @@
+<script>
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+import dateFormat from '~/lib/dateformat';
+import SetStatusForm from './set_status_form.vue';
+import { isUserBusy } from './utils';
+import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
+
+export default {
+ components: { SetStatusForm },
+ inject: ['fields'],
+ data() {
+ return {
+ emoji: this.fields.emoji.value,
+ message: this.fields.message.value,
+ availability: isUserBusy(this.fields.availability.value),
+ clearStatusAfter: NEVER_TIME_RANGE,
+ currentClearStatusAfter: this.fields.clearStatusAfter.value,
+ };
+ },
+ computed: {
+ clearStatusAfterInputValue() {
+ return this.clearStatusAfter.label === NEVER_TIME_RANGE.label
+ ? null
+ : this.clearStatusAfter.shortcut;
+ },
+ availabilityInputValue() {
+ return this.availability
+ ? this.$options.AVAILABILITY_STATUS.BUSY
+ : this.$options.AVAILABILITY_STATUS.NOT_SET;
+ },
+ },
+ mounted() {
+ this.$options.formEl = document.querySelector('form.js-edit-user');
+
+ if (!this.$options.formEl) return;
+
+ this.$options.formEl.addEventListener('ajax:success', this.handleFormSuccess);
+ },
+ beforeDestroy() {
+ if (!this.$options.formEl) return;
+
+ this.$options.formEl.removeEventListener('ajax:success', this.handleFormSuccess);
+ },
+ methods: {
+ handleMessageInput(value) {
+ this.message = value;
+ },
+ handleEmojiClick(emoji) {
+ this.emoji = emoji;
+ },
+ handleClearStatusAfterClick(after) {
+ this.clearStatusAfter = after;
+ },
+ handleAvailabilityInput(value) {
+ this.availability = value;
+ },
+ handleFormSuccess() {
+ if (!this.clearStatusAfter?.duration?.seconds) {
+ this.currentClearStatusAfter = '';
+
+ return;
+ }
+
+ const now = new Date();
+ const currentClearStatusAfterDate = new Date(
+ now.getTime() + secondsToMilliseconds(this.clearStatusAfter.duration.seconds),
+ );
+
+ this.currentClearStatusAfter = dateFormat(
+ currentClearStatusAfterDate,
+ "UTC:yyyy-mm-dd HH:MM:ss 'UTC'",
+ );
+ this.clearStatusAfter = NEVER_TIME_RANGE;
+ },
+ },
+ AVAILABILITY_STATUS,
+ formEl: null,
+};
+</script>
+
+<template>
+ <div>
+ <input :value="emoji" type="hidden" :name="fields.emoji.name" />
+ <input :value="message" type="hidden" :name="fields.message.name" />
+ <input :value="availabilityInputValue" type="hidden" :name="fields.availability.name" />
+ <input :value="clearStatusAfterInputValue" type="hidden" :name="fields.clearStatusAfter.name" />
+ <set-status-form
+ default-emoji="speech_balloon"
+ :emoji="emoji"
+ :message="message"
+ :availability="availability"
+ :clear-status-after="clearStatusAfter"
+ :current-clear-status-after="currentClearStatusAfter"
+ @message-input="handleMessageInput"
+ @emoji-click="handleEmojiClick"
+ @clear-status-after-click="handleClearStatusAfterClick"
+ @availability-input="handleAvailabilityInput"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/set_status_modal/utils.js b/app/assets/javascripts/set_status_modal/utils.js
index e17d95adb25..950091195d2 100644
--- a/app/assets/javascripts/set_status_modal/utils.js
+++ b/app/assets/javascripts/set_status_modal/utils.js
@@ -1,7 +1,4 @@
-export const AVAILABILITY_STATUS = {
- BUSY: 'busy',
- NOT_SET: 'not_set',
-};
+import { AVAILABILITY_STATUS } from './constants';
export const isUserBusy = (status = '') =>
Boolean(status.length && status.toLowerCase().trim() === AVAILABILITY_STATUS.BUSY);
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index c18ea3ddb00..c2e4a50fe31 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -8,7 +8,7 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql';
-import updateWorkItemWidgetsMutation from '../graphql/update_work_item_widgets.mutation.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
export default {
@@ -142,9 +142,9 @@ export default {
this.track('updated_description');
const {
- data: { workItemUpdateWidgets },
+ data: { workItemUpdate },
} = await this.$apollo.mutate({
- mutation: updateWorkItemWidgetsMutation,
+ mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItem.id,
@@ -155,8 +155,8 @@ export default {
},
});
- if (workItemUpdateWidgets.errors?.length) {
- throw new Error(workItemUpdateWidgets.errors[0]);
+ if (workItemUpdate.errors?.length) {
+ throw new Error(workItemUpdate.errors[0]);
}
this.isEditing = false;
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql
deleted file mode 100644
index 148b340b439..00000000000
--- a/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "./work_item.fragment.graphql"
-
-mutation workItemUpdateWidgets($input: WorkItemUpdateWidgetsInput!) {
- workItemUpdateWidgets(input: $input) {
- workItem {
- ...WorkItem
- }
- errors
- }
-}
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index e04665e279b..42d3387d27e 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -9,6 +9,10 @@ class Admin::RunnersController < Admin::ApplicationController
push_frontend_feature_flag(:runner_list_stacked_layout_admin)
end
+ before_action only: [:show] do
+ push_frontend_feature_flag(:enforce_runner_token_expires_at)
+ end
+
feature_category :runner
urgency :low
@@ -23,7 +27,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def update
- if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
respond_to do |format|
format.html { redirect_to edit_admin_runner_path(@runner) }
end
@@ -40,7 +44,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def resume
- if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: true).success?
redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
else
redirect_to admin_runners_path, alert: _('Runner was not updated.')
@@ -48,7 +52,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def pause
- if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: false).success?
redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
else
redirect_to admin_runners_path, alert: _('Runner was not updated.')
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 25863632849..3feb6d48fae 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -8,6 +8,10 @@ class Groups::RunnersController < Groups::ApplicationController
push_frontend_feature_flag(:runner_list_stacked_layout, @group)
end
+ before_action only: [:show] do
+ push_frontend_feature_flag(:enforce_runner_token_expires_at)
+ end
+
feature_category :runner
urgency :low
@@ -26,7 +30,7 @@ class Groups::RunnersController < Groups::ApplicationController
end
def update
- if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
redirect_to group_runner_path(@group, @runner), notice: _('Runner was successfully updated.')
else
render 'edit'
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index dd1ac526b89..e3704b77adc 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -137,7 +137,7 @@ class ProfilesController < Profiles::ApplicationController
:pronouns,
:pronunciation,
:validation_password,
- status: [:emoji, :message, :availability]
+ status: [:emoji, :message, :availability, :clear_status_after]
]
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index ba9576795ec..ee12b85b3a4 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -15,7 +15,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def update
- if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
redirect_to project_runner_path(@project, @runner), notice: _('Runner was successfully updated.')
else
render 'edit'
@@ -31,7 +31,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def resume
- if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: true).success?
redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
else
redirect_to project_runners_path(@project), alert: _('Runner was not updated.')
@@ -39,7 +39,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def pause
- if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
+ if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: false).success?
redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
else
redirect_to project_runners_path(@project), alert: _('Runner was not updated.')
diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb
index 1c6cf6989bf..203cd44bc84 100644
--- a/app/graphql/mutations/ci/runner/update.rb
+++ b/app/graphql/mutations/ci/runner/update.rb
@@ -59,9 +59,8 @@ module Mutations
def resolve(id:, **runner_attrs)
runner = authorized_find!(id)
- unless ::Ci::Runners::UpdateRunnerService.new(runner).update(runner_attrs)
- return { runner: nil, errors: runner.errors.full_messages }
- end
+ result = ::Ci::Runners::UpdateRunnerService.new(runner).execute(runner_attrs)
+ return { runner: nil, errors: result.errors } if result.error?
{ runner: runner, errors: [] }
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ba2bb030f19..ddc682bc08a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -450,6 +450,14 @@ module ApplicationSettingsHelper
end
end
+ def runner_token_expiration_interval_attributes
+ {
+ instance_runner_token_expiration_interval: @application_setting.runner_token_expiration_interval,
+ group_runner_token_expiration_interval: @application_setting.group_runner_token_expiration_interval,
+ project_runner_token_expiration_interval: @application_setting.project_runner_token_expiration_interval
+ }
+ end
+
def external_authorization_service_attributes
[
:external_auth_client_cert,
diff --git a/app/models/user_status.rb b/app/models/user_status.rb
index dee976a4497..0c66f465356 100644
--- a/app/models/user_status.rb
+++ b/app/models/user_status.rb
@@ -29,6 +29,10 @@ class UserStatus < ApplicationRecord
cache_markdown_field :message, pipeline: :emoji
+ def clear_status_after
+ clear_status_at
+ end
+
def clear_status_after=(value)
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
end
diff --git a/app/services/ci/runners/update_runner_service.rb b/app/services/ci/runners/update_runner_service.rb
index 6cc080f81c2..bd01f52f396 100644
--- a/app/services/ci/runners/update_runner_service.rb
+++ b/app/services/ci/runners/update_runner_service.rb
@@ -9,11 +9,14 @@ module Ci
@runner = runner
end
- def update(params)
+ def execute(params)
params[:active] = !params.delete(:paused) if params.include?(:paused)
- runner.update(params).tap do |updated|
- runner.tick_runner_queue if updated
+ if runner.update(params)
+ runner.tick_runner_queue
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: runner.errors.full_messages)
end
end
end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 550327cbdc6..7275a05d2ce 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -14,6 +14,13 @@ module SystemNotes
# See also the discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60700#note_612724683
USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE = false
+ def self.issuable_events
+ {
+ review_requested: s_('IssuableEvents|requested review from'),
+ review_request_removed: s_('IssuableEvents|removed review request for')
+ }.freeze
+ end
+
#
# noteable_ref - Referenced noteable object
#
@@ -115,8 +122,8 @@ module SystemNotes
text_parts = []
Gitlab::I18n.with_default_locale do
- text_parts << "requested review from #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
- text_parts << "removed review request for #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
+ text_parts << "#{self.class.issuable_events[:review_requested]} #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
+ text_parts << "#{self.class.issuable_events[:review_request_removed]} #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
end
body = text_parts.join(' and ')
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 9c18f963dd7..05aea2b343d 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -53,6 +53,8 @@
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
+ - if Feature.enabled?(:enforce_runner_token_expires_at)
+ #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index a64968cdcbb..f38d6021b18 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -2,8 +2,6 @@
- page_title s_("Profiles|Edit Profile")
- @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
-- availability = availability_values
-- custom_emoji = @user.status&.customized?
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
.row.js-search-settings-section
@@ -43,39 +41,12 @@
%h4.gl-mt-0= s_("Profiles|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8
- = f.fields_for :status, @user.status do |status_form|
- - emoji_button = render Pajamas::ButtonComponent.new(button_options: { title: s_("Profiles|Add status emoji"),
- class: 'js-toggle-emoji-menu emoji-menu-toggle-button has-tooltip' } ) do
- - if custom_emoji
- = emoji_icon(@user.status.emoji, class: 'gl-mr-0!')
- %span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if custom_emoji) }
- = sprite_icon('slight-smile', css_class: 'award-control-icon-neutral')
- = sprite_icon('smiley', css_class: 'award-control-icon-positive')
- = sprite_icon('smile', css_class: 'award-control-icon-super-positive')
- - reset_message_button = render Pajamas::ButtonComponent.new(icon: 'close',
- button_options: { id: 'js-clear-user-status-button',
- class: 'has-tooltip',
- title: s_("Profiles|Clear status") } )
-
- = status_form.hidden_field :emoji, id: 'js-status-emoji-field'
- .form-group.gl-form-group
- = status_form.label :message, s_("Profiles|Your status")
- .input-group{ role: 'group' }
- .input-group-prepend
- = emoji_button
- = status_form.text_field :message,
- id: 'js-status-message-field',
- class: 'form-control gl-form-input input-lg',
- placeholder: s_("Profiles|What's your status?")
- .input-group-append
- = reset_message_button
- .form-group.gl-form-group
- = status_form.gitlab_ui_checkbox_component :availability,
- s_("Profiles|Busy"),
- help_text: s_('Profiles|An indicator appears next to your name and avatar.'),
- checkbox_options: { data: { testid: "user-availability-checkbox" } },
- checked_value: availability["busy"],
- unchecked_value: availability["not_set"]
+ #js-user-profile-set-status-form
+ = f.fields_for :status, @user.status do |status_form|
+ = status_form.hidden_field :emoji, data: { js_name: 'emoji' }
+ = status_form.hidden_field :message, data: { js_name: 'message' }
+ = status_form.hidden_field :availability, data: { js_name: 'availability' }
+ = status_form.hidden_field :clear_status_after, data: { js_name: 'clearStatusAfter' }
.col-lg-12
%hr
.row.user-time-preferences.js-search-settings-section
diff --git a/data/deprecations/15-4-cs-docker-variables.yml b/data/deprecations/15-4-cs-docker-variables.yml
new file mode 100644
index 00000000000..37f2552ac00
--- /dev/null
+++ b/data/deprecations/15-4-cs-docker-variables.yml
@@ -0,0 +1,11 @@
+- name: "Container Scanning variables that reference Docker"
+ announcement_milestone: "15.4"
+ announcement_date: "2022-09-22"
+ removal_milestone: "16.0"
+ removal_date: "2023-05-22"
+ breaking_change: true
+ reporter: sam.white
+ stage: secure
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371840
+ body: |
+ All Container Scanning variables that are prefixed by `DOCKER_` in variable name are deprecated. This includes the `DOCKER_IMAGE`, `DOCKER_PASSWORD`, `DOCKER_USER`, and `DOCKERFILE_PATH` variables. Support for these variables will be removed in the GitLab 16.0 release. Use the [new variable names](https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-cicd-variables) `CS_IMAGE`, `CS_REGISTRY_PASSWORD`, `CS_REGISTRY_USER`, and `CS_DOCKERFILE_PATH` in place of the deprecated names.
diff --git a/doc/ci/runners/configure_runners.md b/doc/ci/runners/configure_runners.md
index 3efa697bf2f..a42bbc51608 100644
--- a/doc/ci/runners/configure_runners.md
+++ b/doc/ci/runners/configure_runners.md
@@ -912,3 +912,43 @@ To determine which runners need to be upgraded:
- **Outdated - available**: Newer versions are available but upgrading is not critical.
1. Filter the list by status to view which individual runners need to be upgraded.
+
+## Authentication token security
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
+[enable the feature flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`.
+On GitLab.com, this feature is not available.
+
+Each runner has an [authentication token](../../api/runners.md#registration-and-authentication-tokens)
+to connect with the GitLab instance.
+
+To help prevent the token from being compromised, you can have the
+token rotate automatically at specified intervals. When the tokens are rotated,
+they are updated for each runner, regardless of the runner's status (`online` or `offline`).
+
+No manual intervention should be required, and no running jobs should be affected.
+
+If you need to manually update the authentication token, you can run a
+command to [reset the token](https://docs.gitlab.com/runner/commands/#gitlab-runner-reset-token).
+
+### Automatically rotate authentication tokens
+
+You can specify an interval for authentication tokens to rotate.
+This rotation helps ensure the security of the tokens assigned to your runners.
+
+Prerequisites:
+
+- Ensure your runners are using [GitLab Runner 15.3 or later](https://docs.gitlab.com/runner/#gitlab-runner-versions).
+
+To automatically rotate runner authentication tokens:
+
+1. On the top bar, select **Menu > Admin**.
+1. On the left sidebar, select **Settings > CI/CD**.
+1. Expand **Continuous Integration and Deployment**
+1. Set a **Runners expiration** time for runners, leave empty for no expiration.
+1. Select **Save**.
+
+Before the interval expires, runners automatically request a new authentication token.
diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md
index 7f795356c15..56a9da7cb84 100644
--- a/doc/ci/yaml/artifacts_reports.md
+++ b/doc/ci/yaml/artifacts_reports.md
@@ -99,7 +99,8 @@ artifacts:
path: coverage/cobertura-coverage.xml
```
-The collected coverage report is uploaded to GitLab as an artifact.
+The collected coverage report is uploaded to GitLab as an artifact. You can use
+only one report per job.
GitLab can display the results of coverage report in the merge request
[diff annotations](../testing/test_coverage_visualization.md).
diff --git a/doc/development/backend/ruby_style_guide.md b/doc/development/backend/ruby_style_guide.md
index a9fee02a15a..aec4a255dbd 100644
--- a/doc/development/backend/ruby_style_guide.md
+++ b/doc/development/backend/ruby_style_guide.md
@@ -72,3 +72,106 @@ end
Public attributes should only be used if they are accessed outside of the class.
There is not a strong opinion on what strategy is used when attributes are only
accessed internally, as long as there is consistency in related code.
+
+## Newlines style guide
+
+This style guide recommends best practices for newlines in Ruby code.
+
+### Rule: separate code with newlines only to group together related logic
+
+```ruby
+# bad
+def method
+ issue = Issue.new
+
+ issue.save
+
+ render json: issue
+end
+```
+
+```ruby
+# good
+def method
+ issue = Issue.new
+ issue.save
+
+ render json: issue
+end
+```
+
+### Rule: separate code and block with newlines
+
+#### Newline before block
+
+```ruby
+# bad
+def method
+ issue = Issue.new
+ if issue.save
+ render json: issue
+ end
+end
+```
+
+```ruby
+# good
+def method
+ issue = Issue.new
+
+ if issue.save
+ render json: issue
+ end
+end
+```
+
+### Rule: Newline after block
+
+```ruby
+# bad
+def method
+ if issue.save
+ issue.send_email
+ end
+ render json: issue
+end
+```
+
+```ruby
+# good
+def method
+ if issue.save
+ issue.send_email
+ end
+
+ render json: issue
+end
+```
+
+#### Exception: no need for newline when code block starts or ends right inside another code block
+
+```ruby
+# bad
+def method
+
+ if issue
+
+ if issue.valid?
+ issue.save
+ end
+
+ end
+
+end
+```
+
+```ruby
+# good
+def method
+ if issue
+ if issue.valid?
+ issue.save
+ end
+ end
+end
+```
diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md
index 57962129b2f..014affa3e04 100644
--- a/doc/development/newlines_styleguide.md
+++ b/doc/development/newlines_styleguide.md
@@ -1,108 +1,11 @@
---
-stage: none
-group: unassigned
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+redirect_to: 'backend/ruby_style_guide.md#newlines-style-guide'
+remove_date: '2022-12-15'
---
-# Newlines style guide
+This document was moved to [another location](backend/ruby_style_guide.md#newlines-style-guide).
-This style guide recommends best practices for newlines in Ruby code.
-
-## Rule: separate code with newlines only to group together related logic
-
-```ruby
-# bad
-def method
- issue = Issue.new
-
- issue.save
-
- render json: issue
-end
-```
-
-```ruby
-# good
-def method
- issue = Issue.new
- issue.save
-
- render json: issue
-end
-```
-
-## Rule: separate code and block with newlines
-
-### Newline before block
-
-```ruby
-# bad
-def method
- issue = Issue.new
- if issue.save
- render json: issue
- end
-end
-```
-
-```ruby
-# good
-def method
- issue = Issue.new
-
- if issue.save
- render json: issue
- end
-end
-```
-
-## Newline after block
-
-```ruby
-# bad
-def method
- if issue.save
- issue.send_email
- end
- render json: issue
-end
-```
-
-```ruby
-# good
-def method
- if issue.save
- issue.send_email
- end
-
- render json: issue
-end
-```
-
-### Exception: no need for newline when code block starts or ends right inside another code block
-
-```ruby
-# bad
-def method
-
- if issue
-
- if issue.valid?
- issue.save
- end
-
- end
-
-end
-```
-
-```ruby
-# good
-def method
- if issue
- if issue.valid?
- issue.save
- end
- end
-end
-```
+<!-- This redirect file can be deleted after 2022-12-15. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index c13f1195df3..f300904fc19 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -188,6 +188,8 @@ Alternatively you can use the following on each spec run,
bundle exec spring rspec some_spec.rb
```
+## RuboCop tasks
+
## Generate initial RuboCop TODO list
One way to generate the initial list is to run the Rake task `rubocop:todo:generate`:
@@ -209,6 +211,18 @@ Some shells require brackets to be escaped or quoted.
See [Resolving RuboCop exceptions](contributing/style_guides.md#resolving-rubocop-exceptions)
on how to proceed from here.
+### Run RuboCop in graceful mode
+
+You can run RuboCop in "graceful mode". This means all enabled cop rules are
+silenced which have "grace period" activated (via `Details: grace period`).
+
+Run:
+
+```shell
+bundle exec rake 'rubocop:check:graceful'
+bundle exec rake 'rubocop:check:graceful[Gitlab/NamespacedClass]'
+```
+
## Compile Frontend Assets
You shouldn't ever need to compile frontend assets manually in development, but
diff --git a/doc/install/cloud_native/index.md b/doc/install/cloud_native/index.md
index 600eff1250d..d971cd419e9 100644
--- a/doc/install/cloud_native/index.md
+++ b/doc/install/cloud_native/index.md
@@ -1,24 +1,11 @@
---
-stage: Systems
-group: Distribution
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
-comments: false
-description: Install a cloud-native version of GitLab
-type: index
+redirect_to: 'https://docs.gitlab.com/charts/'
+remove_date: '2023-09-09'
---
-# Cloud-native GitLab **(FREE SELF)**
+This document was moved to [another location](https://docs.gitlab.com/charts/).
-A [cloud-native](https://gitlab.com/gitlab-org/build/CNG) version of GitLab is
-available for deployment on Kubernetes, OpenShift, and Kubernetes-compatible
-platforms. The following deployment methods are available:
-
-- [GitLab Helm chart](https://docs.gitlab.com/charts/): A cloud-native version of GitLab
- and all of its components. Use this installation method if your infrastructure is built
- on Kubernetes and you're familiar with how it works. This method of deployment has different
- management, observability, and concepts than traditional deployments.
-- [GitLab Operator](https://docs.gitlab.com/operator/): An installation and management method
- that follows the
- [Kubernetes Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/).
- Use the GitLab Operator to run GitLab in an
- [OpenShift](../openshift_and_gitlab/index.md) or another Kubernetes-compatible platform.
+<!-- This redirect file can be deleted after <2023-09-09>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index bea96b38fd3..ba97c0f615e 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -49,6 +49,20 @@ sole discretion of GitLab Inc.
<div class="deprecation removal-160 breaking-change">
+### Container Scanning variables that reference Docker
+
+Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+All Container Scanning variables that are prefixed by `DOCKER_` in variable name are deprecated. This includes the `DOCKER_IMAGE`, `DOCKER_PASSWORD`, `DOCKER_USER`, and `DOCKERFILE_PATH` variables. Support for these variables will be removed in the GitLab 16.0 release. Use the [new variable names](https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-cicd-variables) `CS_IMAGE`, `CS_REGISTRY_PASSWORD`, `CS_REGISTRY_USER`, and `CS_DOCKERFILE_PATH` in place of the deprecated names.
+
+</div>
+
+<div class="deprecation removal-160 breaking-change">
+
### Non-expiring access tokens
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 4c859d98004..f1f9783878d 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -205,7 +205,7 @@ To set your current status:
1. Select a value from the **Clear status after** dropdown list.
1. Select **Set status**. Alternatively, you can select **Remove status** to remove your user status entirely.
-You can also set your current status by [using the API](../../api/users.md#user-status).
+You can also set your current status from [your user settings](#access-your-user-settings) or by [using the API](../../api/users.md#user-status).
If you select the **Busy** checkbox, remember to clear it when you become available again.
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index cd092408cc5..3903638b410 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -93,7 +93,7 @@ module API
params[:active] = !params.delete(:paused) if params.include?(:paused)
update_service = ::Ci::Runners::UpdateRunnerService.new(runner)
- if update_service.update(declared_params(include_missing: false))
+ if update_service.execute(declared_params(include_missing: false)).success?
present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
diff --git a/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
index a1af53f6bf7..c0ca821ebff 100644
--- a/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
@@ -279,7 +279,7 @@ semgrep-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SERACH_MAX_DEPTH: 20
+ SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
diff --git a/lib/gitlab/github_import/importer/events/changed_reviewer.rb b/lib/gitlab/github_import/importer/events/changed_reviewer.rb
new file mode 100644
index 00000000000..17b1fa4ab45
--- /dev/null
+++ b/lib/gitlab/github_import/importer/events/changed_reviewer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ module Events
+ class ChangedReviewer < BaseImporter
+ def execute(issue_event)
+ requested_reviewer_id = author_id(issue_event, author_key: :requested_reviewer)
+ review_requester_id = author_id(issue_event, author_key: :review_requester)
+
+ note_body = parse_body(issue_event, requested_reviewer_id)
+
+ create_note(issue_event, note_body, review_requester_id)
+ end
+
+ private
+
+ def create_note(issue_event, note_body, review_requester_id)
+ Note.create!(
+ system: true,
+ noteable_type: issuable_type(issue_event),
+ noteable_id: issuable_db_id(issue_event),
+ project: project,
+ author_id: review_requester_id,
+ note: note_body,
+ system_note_metadata: SystemNoteMetadata.new(
+ {
+ action: 'reviewer',
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ }
+ ),
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ )
+ end
+
+ def parse_body(issue_event, requested_reviewer_id)
+ requested_reviewer = User.find(requested_reviewer_id).to_reference
+
+ if issue_event.event == 'review_request_removed'
+ "#{SystemNotes::IssuablesService.issuable_events[:review_request_removed]}" \
+ " #{requested_reviewer}"
+ else
+ "#{SystemNotes::IssuablesService.issuable_events[:review_requested]}" \
+ " #{requested_reviewer}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issue_event_importer.rb b/lib/gitlab/github_import/importer/issue_event_importer.rb
index a5cb71ff6cc..80749aae93c 100644
--- a/lib/gitlab/github_import/importer/issue_event_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_event_importer.rb
@@ -45,6 +45,8 @@ module Gitlab
Gitlab::GithubImport::Importer::Events::CrossReferenced
when 'assigned', 'unassigned'
Gitlab::GithubImport::Importer::Events::ChangedAssignee
+ when 'review_requested', 'review_request_removed'
+ Gitlab::GithubImport::Importer::Events::ChangedReviewer
end
end
end
diff --git a/lib/gitlab/github_import/representation/issue_event.rb b/lib/gitlab/github_import/representation/issue_event.rb
index 3dfebbc7d29..89271a7dcd6 100644
--- a/lib/gitlab/github_import/representation/issue_event.rb
+++ b/lib/gitlab/github_import/representation/issue_event.rb
@@ -10,7 +10,8 @@ module Gitlab
attr_reader :attributes
expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title,
- :milestone_title, :issue, :source, :assignee, :created_at
+ :milestone_title, :issue, :source, :assignee, :review_requester,
+ :requested_reviewer, :created_at
# attributes - A Hash containing the event details. The keys of this
# Hash (and any nested hashes) must be symbols.
@@ -47,6 +48,8 @@ module Gitlab
issue: event.issue&.to_h&.symbolize_keys,
source: event.source,
assignee: user_representation(event.assignee),
+ requested_reviewer: user_representation(event.requested_reviewer),
+ review_requester: user_representation(event.review_requester),
created_at: event.created_at
)
end
@@ -56,6 +59,8 @@ module Gitlab
hash = Representation.symbolize_hash(raw_hash)
hash[:actor] = user_representation(hash[:actor], source: :hash)
hash[:assignee] = user_representation(hash[:assignee], source: :hash)
+ hash[:requested_reviewer] = user_representation(hash[:requested_reviewer], source: :hash)
+ hash[:review_requester] = user_representation(hash[:review_requester], source: :hash)
new(hash)
end
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
index 1805939dfef..1feb0d450f0 100644
--- a/lib/gitlab/github_import/user_finder.rb
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -45,6 +45,10 @@ module Gitlab
object&.actor
when :assignee
object&.assignee
+ when :requested_reviewer
+ object&.requested_reviewer
+ when :review_requester
+ object&.review_requester
else
object&.author
end
diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake
index e993035aa65..0c257585bd0 100644
--- a/lib/tasks/rubocop.rake
+++ b/lib/tasks/rubocop.rake
@@ -6,6 +6,19 @@ unless Rails.env.production?
RuboCop::RakeTask.new
namespace :rubocop do
+ namespace :check do
+ desc 'Run RuboCop check gracefully'
+ task :graceful do |_task, args|
+ require_relative '../../rubocop/check_graceful_task'
+
+ # Don't reveal TODOs in this run.
+ ENV.delete('REVEAL_RUBOCOP_TODO')
+
+ result = RuboCop::CheckGracefulTask.new($stdout).run(args.extras)
+ exit result if result.nonzero?
+ end
+ end
+
namespace :todo do
desc 'Generate RuboCop todos'
task :generate do |_task, args|
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 73243661969..78843c2d371 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2766,9 +2766,15 @@ msgstr ""
msgid "AdminSettings|Git abuse rate limit"
msgstr ""
+msgid "AdminSettings|Group runners expiration"
+msgstr ""
+
msgid "AdminSettings|I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)."
msgstr ""
+msgid "AdminSettings|If no unit is written, it defaults to seconds. For example, these are all equivalent: %{oneDayInSeconds}, %{oneDayInHoursHumanReadable}, or %{oneDayHumanReadable}. Minimum value is two hours. %{linkStart}Learn more.%{linkEnd}"
+msgstr ""
+
msgid "AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories."
msgstr ""
@@ -2781,6 +2787,9 @@ msgstr ""
msgid "AdminSettings|Inactive project deletion"
msgstr ""
+msgid "AdminSettings|Instance runners expiration"
+msgstr ""
+
msgid "AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines"
msgstr ""
@@ -2841,6 +2850,9 @@ msgstr ""
msgid "AdminSettings|Project export"
msgstr ""
+msgid "AdminSettings|Project runners expiration"
+msgstr ""
+
msgid "AdminSettings|Protect CI/CD variables by default"
msgstr ""
@@ -2892,6 +2904,15 @@ msgstr ""
msgid "AdminSettings|Set limit to 0 to disable it."
msgstr ""
+msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered group runners."
+msgstr ""
+
+msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered instance runners. Authentication tokens are automatically reset at these intervals."
+msgstr ""
+
+msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered project runners."
+msgstr ""
+
msgid "AdminSettings|Set the initial name and protections for the default branch of new repositories created in the instance."
msgstr ""
@@ -16080,9 +16101,6 @@ msgstr ""
msgid "Failed to load deploy keys."
msgstr ""
-msgid "Failed to load emoji list."
-msgstr ""
-
msgid "Failed to load error details from Sentry."
msgstr ""
@@ -21928,6 +21946,12 @@ msgstr ""
msgid "Is using seat"
msgstr ""
+msgid "IssuableEvents|removed review request for"
+msgstr ""
+
+msgid "IssuableEvents|requested review from"
+msgstr ""
+
msgid "IssuableStatus|%{wi_type} created %{created_at} by "
msgstr ""
@@ -30166,15 +30190,9 @@ msgstr ""
msgid "Profiles|Add key"
msgstr ""
-msgid "Profiles|Add status emoji"
-msgstr ""
-
msgid "Profiles|An error occurred while updating your username, please try again."
msgstr ""
-msgid "Profiles|An indicator appears next to your name and avatar."
-msgstr ""
-
msgid "Profiles|Avatar cropper"
msgstr ""
@@ -30187,9 +30205,6 @@ msgstr ""
msgid "Profiles|Bio"
msgstr ""
-msgid "Profiles|Busy"
-msgstr ""
-
msgid "Profiles|Change username"
msgstr ""
@@ -30205,9 +30220,6 @@ msgstr ""
msgid "Profiles|City, country"
msgstr ""
-msgid "Profiles|Clear status"
-msgstr ""
-
msgid "Profiles|Commit email"
msgstr ""
@@ -30457,9 +30469,6 @@ msgstr ""
msgid "Profiles|Website url"
msgstr ""
-msgid "Profiles|What's your status?"
-msgstr ""
-
msgid "Profiles|Who you represent or work for."
msgstr ""
@@ -30505,9 +30514,6 @@ msgstr ""
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you."
msgstr ""
-msgid "Profiles|Your status"
-msgstr ""
-
msgid "Profiles|https://website.com"
msgstr ""
@@ -34124,6 +34130,9 @@ msgstr ""
msgid "Runners|Never contacted:"
msgstr ""
+msgid "Runners|Never expires"
+msgstr ""
+
msgid "Runners|New group runners view"
msgstr ""
@@ -34219,6 +34228,12 @@ msgstr ""
msgid "Runners|Runner assigned to project."
msgstr ""
+msgid "Runners|Runner authentication token expiration"
+msgstr ""
+
+msgid "Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired."
+msgstr ""
+
msgid "Runners|Runner cannot be deleted, please contact your administrator"
msgstr ""
@@ -34350,6 +34365,9 @@ msgstr ""
msgid "Runners|To register them, go to the %{link_start}group's Runners page%{link_end}."
msgstr ""
+msgid "Runners|Token expiry"
+msgstr ""
+
msgid "Runners|Up to date"
msgstr ""
diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb
index 1538384f6ed..ef73bad2879 100644
--- a/qa/lib/gitlab/page/admin/subscription.rb
+++ b/qa/lib/gitlab/page/admin/subscription.rb
@@ -10,8 +10,8 @@ module Gitlab
text_field :activation_code
button :activate
label :terms_of_services, text: /I agree that/
- link :remove_license, 'data-testid': 'license-remove-action'
- button :confirm_ok_button
+ button :remove_license
+ button :confirm_remove_license
p :plan
p :started
p :name
@@ -30,6 +30,11 @@ module Gitlab
terms_of_services_element.click # workaround for hidden checkbox
end
+ def remove_license_file
+ remove_license
+ confirm_remove_license
+ end
+
# Checks if a subscription record exists in subscription history table
#
# @param plan [Hash] Name of the plan
diff --git a/rubocop/check_graceful_task.rb b/rubocop/check_graceful_task.rb
new file mode 100644
index 00000000000..7ae74e79e38
--- /dev/null
+++ b/rubocop/check_graceful_task.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative 'formatter/graceful_formatter'
+require_relative '../lib/gitlab/popen'
+
+module RuboCop
+ class CheckGracefulTask
+ def initialize(output)
+ @output = output
+ end
+
+ def run(args)
+ options = %w[
+ --parallel
+ --format RuboCop::Formatter::GracefulFormatter
+ ]
+
+ available_cops = RuboCop::Cop::Registry.global.to_h
+
+ cop_names, paths = args.partition { available_cops.key?(_1) }
+
+ if cop_names.any?
+ list = cop_names.sort.join(',')
+ options.concat ['--only', list]
+ end
+
+ options.concat(paths)
+
+ puts <<~MSG
+ Running RuboCop in graceful mode:
+ rubocop #{options.join(' ')}
+
+ This might take a while...
+ MSG
+
+ status_orig = RuboCop::CLI.new.run(options)
+ status = RuboCop::Formatter::GracefulFormatter.adjusted_exit_status(status_orig)
+
+ # We had to adjust the status which means we have silenced offenses. Notify Slack!
+ notify_slack unless status_orig == status
+
+ status
+ end
+
+ private
+
+ def env_values(*keys)
+ env = ENV.slice(*keys)
+
+ missing_keys = keys - env.keys
+
+ if missing_keys.any?
+ puts "Missing ENV keys: #{missing_keys.join(', ')}"
+ return
+ end
+
+ env.values
+ end
+
+ def notify_slack
+ job_name, job_url, _ = env_values('CI_JOB_NAME', 'CI_JOB_URL', 'CI_SLACK_WEBHOOK_URL')
+
+ unless job_name
+ puts 'Skipping Slack notification.'
+ return
+ end
+
+ channel = 'f_rubocop'
+ message = ":warning: `#{job_name}` passed :green: but contained silenced offenses. See #{job_url}"
+ emoji = 'rubocop'
+ user_name = 'GitLab Bot'
+
+ puts "Notifying Slack ##{channel}."
+
+ _output, result = Gitlab::Popen.popen(['scripts/slack', channel, message, emoji, user_name])
+ puts "Failed to notify Slack channel ##{channel}." if result.nonzero?
+ end
+
+ def puts(...)
+ @output.puts(...)
+ end
+ end
+end
diff --git a/rubocop/cop_todo.rb b/rubocop/cop_todo.rb
index 42e2f9fbe13..a36afc08673 100644
--- a/rubocop/cop_todo.rb
+++ b/rubocop/cop_todo.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
+require_relative 'formatter/graceful_formatter'
+
module RuboCop
class CopTodo
- attr_accessor :previously_disabled
+ attr_accessor :previously_disabled, :grace_period
attr_reader :cop_name, :files, :offense_count
@@ -12,6 +14,7 @@ module RuboCop
@offense_count = 0
@cop_class = self.class.find_cop_by_name(cop_name)
@previously_disabled = false
+ @grace_period = false
end
def record(file, offense_count)
@@ -35,6 +38,7 @@ module RuboCop
yaml << ' Enabled: false'
end
+ yaml << " #{RuboCop::Formatter::GracefulFormatter.grace_period_key_value}" if grace_period
yaml << ' Exclude:'
yaml.concat files.sort.map { |file| " - '#{file}'" }
yaml << ''
diff --git a/rubocop/formatter/graceful_formatter.rb b/rubocop/formatter/graceful_formatter.rb
new file mode 100644
index 00000000000..b5db7c6c192
--- /dev/null
+++ b/rubocop/formatter/graceful_formatter.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'rubocop'
+
+module RuboCop
+ module Formatter
+ class GracefulFormatter < ::RuboCop::Formatter::ProgressFormatter
+ CONFIG_DETAILS_KEY = 'Details'
+ CONFIG_DETAILS_VALUE = 'grace period'
+
+ class << self
+ attr_accessor :active_offenses
+ end
+
+ def started(...)
+ super
+
+ self.class.active_offenses = 0
+
+ @silenced_offenses_for_files = {}
+ @config = RuboCop::ConfigStore.new.for_pwd
+ end
+
+ def file_finished(file, offenses)
+ silenced_offenses, active_offenses = offenses.partition { silenced?(_1) }
+
+ @silenced_offenses_for_files[file] = silenced_offenses if silenced_offenses.any?
+
+ super(file, active_offenses)
+ end
+
+ def finished(inspected_files)
+ # See the note below why are using this ivar in the first place.
+ unless defined?(@total_offense_count)
+ raise <<~MESSAGE
+ RuboCop has changed its internals and the instance variable
+ `@total_offense_count` is no longer defined but we were relying on it.
+
+ Please change the implementation.
+
+ See https://github.com/rubocop/rubocop/blob/65a757b0f/lib/rubocop/formatter/simple_text_formatter.rb#L24
+ MESSAGE
+ end
+
+ super
+
+ # Internally, RuboCop has no notion of "silenced offenses". We cannot
+ # override this meaning in a formatter that's why we track what we
+ # consider to be an active offense.
+ # This is needed for `adjusted_exit_status` method below.
+ self.class.active_offenses = @total_offense_count
+
+ report_silenced_offenses(inspected_files)
+ end
+
+ # We consider this run a success without any active offenses.
+ def self.adjusted_exit_status(status)
+ return status unless status == RuboCop::CLI::STATUS_OFFENSES
+ return RuboCop::CLI::STATUS_SUCCESS if active_offenses == 0
+
+ status
+ end
+
+ def self.grace_period?(cop_name, config)
+ details = config[CONFIG_DETAILS_KEY]
+ return false unless details
+ return true if details == CONFIG_DETAILS_VALUE
+
+ warn "#{cop_name}: Unhandled value #{details.inspect} for `Details` key."
+
+ false
+ end
+
+ def self.grace_period_key_value
+ "#{CONFIG_DETAILS_KEY}: #{CONFIG_DETAILS_VALUE}"
+ end
+
+ private
+
+ def silenced?(offense)
+ cop_config = @config.for_cop(offense.cop_name)
+
+ self.class.grace_period?(offense.cop_name, cop_config)
+ end
+
+ def report_silenced_offenses(inspected_files)
+ return if @silenced_offenses_for_files.empty?
+
+ output.puts
+ output.puts 'Silenced offenses:'
+ output.puts
+
+ @silenced_offenses_for_files.each do |file, offenses|
+ report_file(file, offenses)
+ end
+
+ silenced_offense_count = @silenced_offenses_for_files.values.sum(&:size)
+ silenced_text = colorize("#{silenced_offense_count} offenses", :yellow)
+
+ output.puts
+ output.puts "#{inspected_files.size} files inspected, #{silenced_text} silenced"
+ end
+
+ def report_file_as_mark(_offenses)
+ # Skip progress bar. No dots. No C/Ws.
+ end
+ end
+ end
+end
diff --git a/rubocop/formatter/todo_formatter.rb b/rubocop/formatter/todo_formatter.rb
index 789d0418f96..b1c6d1c1688 100644
--- a/rubocop/formatter/todo_formatter.rb
+++ b/rubocop/formatter/todo_formatter.rb
@@ -6,6 +6,7 @@ require 'yaml'
require_relative '../todo_dir'
require_relative '../cop_todo'
+require_relative '../formatter/graceful_formatter'
module RuboCop
module Formatter
@@ -47,6 +48,8 @@ module RuboCop
def finished(_inspected_files)
@todos.values.sort_by(&:cop_name).each do |todo|
todo.previously_disabled = previously_disabled?(todo)
+ todo.grace_period = grace_period?(todo)
+ validate_todo!(todo)
path = @todo_dir.write(todo.cop_name, todo.to_yaml)
output.puts "Written to #{relative_path(path)}\n"
@@ -79,16 +82,31 @@ module RuboCop
raise "Multiple configurations found for cops:\n#{list}\n"
end
- def previously_disabled?(todo)
+ def config_for(todo)
cop_name = todo.cop_name
- config = @config_old_todo_yml[cop_name] ||
- @config_inspect_todo_dir[cop_name] || {}
+ @config_old_todo_yml[cop_name] || @config_inspect_todo_dir[cop_name] || {}
+ end
+
+ def previously_disabled?(todo)
+ config = config_for(todo)
return false if config.empty?
config['Enabled'] == false
end
+ def grace_period?(todo)
+ config = config_for(todo)
+
+ GracefulFormatter.grace_period?(todo.cop_name, config)
+ end
+
+ def validate_todo!(todo)
+ return unless todo.previously_disabled && todo.grace_period
+
+ raise "#{todo.cop_name}: Cop must be enabled to use `#{GracefulFormatter.grace_period_key_value}`."
+ end
+
def load_config_inspect_todo_dir
@todo_dir.list_inspect.each_with_object({}) do |path, combined|
config = YAML.load_file(path)
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index fea59969400..9e852cb28dd 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe Admin::RunnersController do
context 'with update succeeding' do
before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
- expect(service).to receive(:update).with(anything).and_call_original
+ expect(service).to receive(:execute).with(anything).and_call_original
end
end
@@ -91,7 +91,7 @@ RSpec.describe Admin::RunnersController do
context 'with update failing' do
before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
- expect(service).to receive(:update).with(anything).and_return(false)
+ expect(service).to receive(:execute).with(anything).and_return(ServiceResponse.error(message: 'failure'))
end
end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 89185a8f856..aa92ff6be33 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -82,13 +82,17 @@ RSpec.describe ProfilesController, :request_store do
expect(ldap_user.location).to eq('City, Country')
end
- it 'allows setting a user status' do
+ it 'allows setting a user status', :freeze_time do
sign_in(user)
- put :update, params: { user: { status: { message: 'Working hard!', availability: 'busy' } } }
+ put(
+ :update,
+ params: { user: { status: { message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours' } } }
+ )
expect(user.reload.status.message).to eq('Working hard!')
expect(user.reload.status.availability).to eq('busy')
+ expect(user.reload.status.clear_status_after).to eq(8.hours.from_now)
expect(response).to have_gitlab_http_status(:found)
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 067abb46602..d887a367fcb 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -180,7 +180,7 @@ RSpec.describe 'User edit profile' do
end
it 'adds emoji to user status' do
- emoji = 'biohazard'
+ emoji = 'basketball'
select_emoji(emoji)
submit_settings
@@ -193,7 +193,7 @@ RSpec.describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
- fill_in 'js-status-message-field', with: message
+ fill_in s_("SetStatusModal|What's your status?"), with: message
submit_settings
visit_user
@@ -208,7 +208,7 @@ RSpec.describe 'User edit profile' do
emoji = '8ball'
message = 'Playing outside'
select_emoji(emoji)
- fill_in 'js-status-message-field', with: message
+ fill_in s_("SetStatusModal|What's your status?"), with: message
submit_settings
visit_user
@@ -230,7 +230,7 @@ RSpec.describe 'User edit profile' do
end
visit(profile_path)
- click_button 'js-clear-user-status-button'
+ click_button s_('SetStatusModal|Clear status')
submit_settings
visit_user
@@ -240,9 +240,9 @@ RSpec.describe 'User edit profile' do
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
- fill_in 'js-status-message-field', with: message
+ fill_in s_("SetStatusModal|What's your status?"), with: message
- within('.js-toggle-emoji-menu') do
+ within('.emoji-menu-toggle-button') do
expect(page).to have_emoji('speech_balloon')
end
end
@@ -406,7 +406,7 @@ RSpec.describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
open_user_status_modal
- find('.js-status-message-field').native.send_keys(message)
+ find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal
visit_user
@@ -422,7 +422,7 @@ RSpec.describe 'User edit profile' do
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
- find('.js-status-message-field').native.send_keys(message)
+ find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal
visit_user
@@ -446,7 +446,7 @@ RSpec.describe 'User edit profile' do
open_edit_status_modal
- find('.js-clear-user-status-button').click
+ click_button s_('SetStatusModal|Clear status')
set_user_status_in_modal
visit_user
@@ -491,7 +491,7 @@ RSpec.describe 'User edit profile' do
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
open_user_status_modal
- find('.js-status-message-field').native.send_keys(message)
+ find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
expect(page).to have_emoji('speech_balloon')
end
diff --git a/spec/frontend/__helpers__/dl_locator_helper.js b/spec/frontend/__helpers__/dl_locator_helper.js
index b507dcd599d..591c034be9b 100644
--- a/spec/frontend/__helpers__/dl_locator_helper.js
+++ b/spec/frontend/__helpers__/dl_locator_helper.js
@@ -19,10 +19,13 @@ import { createWrapper, ErrorWrapper } from '@vue/test-utils';
* @returns Wrapper
*/
export const findDd = (dtLabel, wrapper) => {
- const dt = wrapper.findByText(dtLabel).element;
- const dd = dt.nextElementSibling;
- if (dt.tagName === 'DT' && dd.tagName === 'DD') {
- return createWrapper(dd, {});
+ const dtw = wrapper.findByText(dtLabel);
+ if (dtw.exists()) {
+ const dt = dtw.element;
+ const dd = dt.nextElementSibling;
+ if (dt.tagName === 'DT' && dd.tagName === 'DD') {
+ return createWrapper(dd, {});
+ }
}
- return ErrorWrapper(dtLabel);
+ return new ErrorWrapper(dtLabel);
};
diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js
index 82dc0cdc250..b72aa659d56 100644
--- a/spec/frontend/emoji/components/category_spec.js
+++ b/spec/frontend/emoji/components/category_spec.js
@@ -30,19 +30,19 @@ describe('Emoji category component', () => {
// eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ renderGroup: true });
- expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
+ expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('renders group on appear', async () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
- expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
+ expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('emits appear event on appear', async () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index a1b9a1fc7eb..7a714cc1ebc 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -76,8 +76,8 @@ describe('error tracking settings app', () => {
describe('section', () => {
it('renders the form and dropdown', () => {
- expect(wrapper.find(ErrorTrackingForm).exists()).toBe(true);
- expect(wrapper.find(ProjectDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(ErrorTrackingForm).exists()).toBe(true);
+ expect(wrapper.findComponent(ProjectDropdown).exists()).toBe(true);
});
it('renders the Save Changes button', () => {
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index b44af547658..d210f15ea6f 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('shows helper text', () => {
@@ -57,8 +57,8 @@ describe('error tracking settings project dropdown', () => {
});
it('does not contain any dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBe(false);
- expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available');
});
});
@@ -71,11 +71,11 @@ describe('error tracking settings project dropdown', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('contains a number of dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
});
});
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index 897ad5ee2bf..878cf5ed43b 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -6,9 +6,9 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
describe('Recent Searches Dropdown Content', () => {
let wrapper;
- const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' });
+ const findLocalStorageNote = () => wrapper.findComponent({ ref: 'localStorageNote' });
const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' });
- const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' });
+ const findDropdownNote = () => wrapper.findComponent({ ref: 'dropdownNote' });
const createComponent = (props) => {
wrapper = shallowMount(RecentSearchesDropdownContent, {
@@ -94,7 +94,7 @@ describe('Recent Searches Dropdown Content', () => {
});
it('emits requestClearRecentSearches on Clear resent searches button', () => {
- wrapper.find({ ref: 'clearButton' }).trigger('click');
+ wrapper.findComponent({ ref: 'clearButton' }).trigger('click');
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index eef5dc86c1a..85e5e2273a5 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -16,10 +16,10 @@ describe('FrequentItemsListItemComponent', () => {
let trackingSpy;
let store;
- const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
+ const findTitle = () => wrapper.findComponent({ ref: 'frequentItemsItemTitle' });
const findAvatar = () => wrapper.findComponent(ProjectAvatar);
const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
- const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
+ const findNamespace = () => wrapper.findComponent({ ref: 'frequentItemsItemNamespace' });
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar);
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index d0a4cf70f5f..94fc97b82c2 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -23,7 +23,7 @@ describe('FrequentItemsSearchInputComponent', () => {
},
});
- const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index 1c1e1e7ebd4..b896437ecb2 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -42,7 +42,7 @@ describe('Import entities group dropdown component', () => {
createComponent({ namespaces });
namespacesTracker.mockReset();
- wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
await nextTick();
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index cdc508a0033..f97ea046cbe 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -99,7 +99,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not renders loading icon when request is completed', async () => {
@@ -108,7 +108,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
});
@@ -123,7 +123,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
+ expect(wrapper.findComponent(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
});
});
@@ -268,7 +268,7 @@ describe('import table', () => {
});
it('correctly passes pagination info from query', () => {
- expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
+ expect(wrapper.findComponent(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
});
it('renders pagination dropdown', () => {
@@ -293,7 +293,7 @@ describe('import table', () => {
it('updates page when page change is requested', async () => {
const REQUESTED_PAGE = 2;
- wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+ wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@@ -316,7 +316,7 @@ describe('import table', () => {
},
versionValidation: FAKE_VERSION_VALIDATION,
});
- wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+ wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from');
@@ -539,8 +539,8 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlAlert).exists()).toBe(true);
- expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)');
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
});
it('does not renders alert when there are no unavailable features', async () => {
@@ -558,7 +558,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index d3f86672f33..18dc1217fec 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -22,8 +22,8 @@ describe('import target cell', () => {
let wrapper;
let group;
- const findNameInput = () => wrapper.find(GlFormInput);
- const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
+ const findNameInput = () => wrapper.findComponent(GlFormInput);
+ const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const createComponent = (props) => {
wrapper = shallowMount(ImportTargetCell, {
diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
index ea88c361f7b..9eae4ed974e 100644
--- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
@@ -33,12 +33,12 @@ describe('BitbucketStatusTable', () => {
it('renders import table component', () => {
createComponent({ providerTitle: 'Test' });
- expect(wrapper.find(ImportProjectsTable).exists()).toBe(true);
+ expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true);
});
it('passes alert in incompatible-repos-warning slot', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
it('passes actions slot to import project table component', () => {
@@ -46,14 +46,14 @@ describe('BitbucketStatusTable', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
actions: actionsSlotContent,
});
- expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent);
+ expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
});
it('dismisses alert when requested', async () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
- wrapper.find(GlAlert).vm.$emit('dismiss');
+ wrapper.findComponent(GlAlert).vm.$emit('dismiss');
await nextTick();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 140fec3863b..8060fb9af7f 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -33,7 +33,7 @@ describe('ImportProjectsTable', () => {
.findAll(GlButton)
.filter((w) => w.props().variant === 'confirm')
.at(0);
- const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
+ const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
@@ -89,13 +89,13 @@ describe('ImportProjectsTable', () => {
it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders a table with provider repos', () => {
@@ -109,7 +109,7 @@ describe('ImportProjectsTable', () => {
state: { namespaces: [{ fullPath: 'path' }], repositories },
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('table').exists()).toBe(true);
expect(
wrapper
@@ -170,7 +170,7 @@ describe('ImportProjectsTable', () => {
it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } });
- expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
+ expect(wrapper.findComponent(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
@@ -231,11 +231,11 @@ describe('ImportProjectsTable', () => {
});
it('renders intersection observer component', () => {
- expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
});
it('calls fetchRepos when intersection observer appears', async () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 41a005199e1..17a07b1e9f9 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -74,11 +74,13 @@ describe('ProviderRepoTableRow', () => {
});
it('renders empty import status', () => {
- expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
+ expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE);
});
it('renders a group namespace select', () => {
- expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces);
+ expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe(
+ availableNamespaces,
+ );
});
it('renders import button', () => {
@@ -127,11 +129,13 @@ describe('ProviderRepoTableRow', () => {
});
it('renders proper import status', () => {
- expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
+ expect(wrapper.findComponent(ImportStatus).props().status).toBe(
+ repo.importedProject.importStatus,
+ );
});
it('does not renders a namespace select', () => {
- expect(wrapper.find(GlDropdown).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});
it('does not render import button', () => {
@@ -139,7 +143,7 @@ describe('ProviderRepoTableRow', () => {
});
it('passes stats to import status component', () => {
- expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS);
+ expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
});
});
@@ -165,7 +169,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders badge with error', () => {
- expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
+ expect(wrapper.findComponent(GlBadge).text()).toBe('Incompatible project');
});
});
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 356480f931e..5f71fde934f 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -40,15 +40,15 @@ describe('Incidents List', () => {
all: 26,
};
- const findTable = () => wrapper.find(GlTable);
+ const findTable = () => wrapper.findComponent(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr');
- const findAlert = () => wrapper.find(GlAlert);
- const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
- const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
const findIncidentLink = () => wrapper.findByTestId('incident-link');
@@ -179,7 +179,7 @@ describe('Incidents List', () => {
});
it('renders an avatar component when there is an assignee', () => {
- const avatar = findAssignees().at(1).find(GlAvatar);
+ const avatar = findAssignees().at(1).findComponent(GlAvatar);
const { src, label } = avatar.attributes();
const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
index ff40f1fa008..e0ebebf1c7d 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
@@ -20,8 +20,8 @@ describe('IncidentsSettingTabs', () => {
}
});
- const findToggleButton = () => wrapper.find({ ref: 'toggleBtn' });
- const findSectionHeader = () => wrapper.find({ ref: 'sectionHeader' });
+ const findToggleButton = () => wrapper.findComponent({ ref: 'toggleBtn' });
+ const findSectionHeader = () => wrapper.findComponent({ ref: 'sectionHeader' });
const findIntegrationTabs = () => wrapper.findAll(GlTab);
it('renders header text', () => {
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 8ee55928926..e13fd39999b 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -86,7 +86,7 @@ describe('TriggerFields', () => {
expect(checkboxes).toHaveLength(2);
checkboxes.wrappers.forEach((checkbox, index) => {
- const checkBox = checkbox.find(GlFormCheckbox);
+ const checkBox = checkbox.findComponent(GlFormCheckbox);
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
expect(checkbox.find('[type=hidden]').attributes('name')).toBe(
diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
index b4d42d90d99..8b2d13be309 100644
--- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
@@ -53,7 +53,7 @@ afterEach(() => {
describe('ImportProjectMembersModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
- const findIntroText = () => wrapper.find({ ref: 'modalIntro' }).text();
+ const findIntroText = () => wrapper.findComponent({ ref: 'modalIntro' }).text();
const clickImportButton = () => findGlModal().vm.$emit('primary', { preventDefault: jest.fn() });
const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() });
const findFormGroup = () => wrapper.findByTestId('form-group');
diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js
index 9d67f602136..eac53c5f761 100644
--- a/spec/frontend/issuable/components/issue_milestone_spec.js
+++ b/spec/frontend/issuable/components/issue_milestone_spec.js
@@ -144,7 +144,7 @@ describe('IssueMilestoneComponent', () => {
});
it('renders milestone icon', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe('clock');
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
});
it('renders milestone title', () => {
diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
index d6aeacfe07a..bacebbade7f 100644
--- a/spec/frontend/issuable/related_issues/components/issue_token_spec.js
+++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
@@ -31,11 +31,11 @@ describe('IssueToken', () => {
}
});
- const findLink = () => wrapper.find({ ref: 'link' });
- const findReference = () => wrapper.find({ ref: 'reference' });
+ const findLink = () => wrapper.findComponent({ ref: 'link' });
+ const findReference = () => wrapper.findComponent({ ref: 'reference' });
const findReferenceIcon = () => wrapper.find('[data-testid="referenceIcon"]');
const findRemoveBtn = () => wrapper.find('[data-testid="removeBtn"]');
- const findTitle = () => wrapper.find({ ref: 'title' });
+ const findTitle = () => wrapper.findComponent({ ref: 'title' });
describe('with reference supplied', () => {
beforeEach(() => {
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 772cc75a205..1b2935ce5d1 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -153,7 +153,7 @@ describe('RelatedIssuesBlock', () => {
});
it('sets `autoCompleteEpics` to false for add-issuable-form', () => {
- expect(wrapper.find(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
+ expect(wrapper.findComponent(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
});
});
@@ -227,7 +227,7 @@ describe('RelatedIssuesBlock', () => {
},
});
- const iconComponent = wrapper.find(GlIcon);
+ const iconComponent = wrapper.findComponent(GlIcon);
expect(iconComponent.exists()).toBe(true);
expect(iconComponent.props('name')).toBe(icon);
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
index fd623ad9a5f..9bb71ec3dcb 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
@@ -187,7 +187,9 @@ describe('RelatedIssuesList', () => {
});
it('shows due date', () => {
- expect(wrapper.find(IssueDueDate).find('.board-card-info-text').text()).toBe('Nov 22, 2010');
+ expect(wrapper.findComponent(IssueDueDate).find('.board-card-info-text').text()).toBe(
+ 'Nov 22, 2010',
+ );
});
});
});
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index c3f13ca6f9a..b0d3a63a8cf 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -21,7 +21,7 @@ describe('CE IssueCardTimeInfo component', () => {
};
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
- const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title');
+ const findMilestoneTitle = () => findMilestone().findComponent(GlLink).attributes('title');
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
@@ -56,8 +56,8 @@ describe('CE IssueCardTimeInfo component', () => {
const milestone = findMilestone();
expect(milestone.text()).toBe(issue.milestone.title);
- expect(milestone.find(GlIcon).props('name')).toBe('clock');
- expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
+ expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
+ expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath);
});
describe.each`
@@ -84,7 +84,7 @@ describe('CE IssueCardTimeInfo component', () => {
expect(dueDate.text()).toBe('Dec 12, 2020');
expect(dueDate.attributes('title')).toBe('Due date');
- expect(dueDate.find(GlIcon).props('name')).toBe('calendar');
+ expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar');
expect(dueDate.classes()).not.toContain('gl-text-red-500');
});
});
@@ -118,6 +118,6 @@ describe('CE IssueCardTimeInfo component', () => {
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate');
- expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
+ expect(timeEstimate.findComponent(GlIcon).props('name')).toBe('timer');
});
});
diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index 2d773e8bf56..406b1fbc1af 100644
--- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -11,9 +11,9 @@ describe('JiraIssuesImportStatus', () => {
};
let wrapper;
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
- const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
+ const findAlertLabel = () => wrapper.findComponent(GlAlert).findComponent(GlLabel);
const mountComponent = ({
shouldShowFinishedAlert = false,
@@ -49,7 +49,7 @@ describe('JiraIssuesImportStatus', () => {
});
it('does not show an alert', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
@@ -105,12 +105,12 @@ describe('JiraIssuesImportStatus', () => {
shouldShowInProgressAlert: true,
});
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/new/components/title_suggestions_item_spec.js b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
index 5eb30b52de5..eee6804528f 100644
--- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
@@ -105,7 +105,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1');
- expect(count.find(GlIcon).props('name')).toBe('thumb-up');
+ expect(count.findComponent(GlIcon).props('name')).toBe('thumb-up');
});
it('renders notes count', () => {
@@ -114,7 +114,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2');
- expect(count.find(GlIcon).props('name')).toBe('comment');
+ expect(count.findComponent(GlIcon).props('name')).toBe('comment');
});
});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 12f9707da04..3d027e2084c 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -461,7 +461,7 @@ describe('Issuable output', () => {
describe('when title is not in view', () => {
beforeEach(() => {
wrapper.vm.state.titleText = 'Sticky header title';
- wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
});
it('shows with title', () => {
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index d0e33f0b980..61433607a2b 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('Description field component', () => {
let wrapper;
- const findTextarea = () => wrapper.find({ ref: 'textarea' });
+ const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
const mountComponent = (description = 'test') =>
shallowMount(DescriptionField, {
diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js
index de04405d89b..a5fa96d8d64 100644
--- a/spec/frontend/issues/show/components/fields/title_spec.js
+++ b/spec/frontend/issues/show/components/fields/title_spec.js
@@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub';
describe('Title field component', () => {
let wrapper;
- const findInput = () => wrapper.find({ ref: 'input' });
+ const findInput = () => wrapper.findComponent({ ref: 'input' });
beforeEach(() => {
jest.spyOn(eventHub, '$emit');
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 329c4234f30..d7fa1fa41da 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -65,7 +65,7 @@ describe('HeaderActions component', () => {
},
};
- const findToggleIssueStateButton = () => wrapper.find(GlButton);
+ const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
@@ -73,7 +73,7 @@ describe('HeaderActions component', () => {
const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAll(GlDropdownItem);
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index);
diff --git a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
index 155ae703e48..1cfb7d12a91 100644
--- a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
+++ b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
@@ -41,7 +41,7 @@ describe('Highlight Bar', () => {
}
});
- const findLink = () => wrapper.find(GlLink);
+ const findLink = () => wrapper.findComponent(GlLink);
describe('empty state', () => {
beforeEach(() => {
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 8e090645be2..6960eb1416e 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -64,9 +64,9 @@ describe('Incident Tabs component', () => {
const findTabs = () => wrapper.findAll(GlTab);
const findSummaryTab = () => findTabs().at(0);
const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
- const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
- const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
- const findHighlightBarComponent = () => wrapper.find(HighlightBar);
+ const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
+ const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
+ const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
const findTimelineTab = () => wrapper.findComponent(TimelineTab);
describe('empty state', () => {
diff --git a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
index b38d2b60057..d4202f4a6ab 100644
--- a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
@@ -62,8 +62,8 @@ describe('Sentry Error Stack Trace', () => {
describe('loading', () => {
it('should show spinner while loading', () => {
mountComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
});
});
@@ -74,8 +74,8 @@ describe('Sentry Error Stack Trace', () => {
it('should show stacktrace', () => {
mountComponent({ stubs: {} });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index cd8024d4962..022a0f81aaa 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -21,15 +21,15 @@ describe('JiraImportApp', () => {
const setupIllustration = 'setup-illustration.svg';
- const getFormComponent = () => wrapper.find(JiraImportForm);
+ const getFormComponent = () => wrapper.findComponent(JiraImportForm);
- const getProgressComponent = () => wrapper.find(JiraImportProgress);
+ const getProgressComponent = () => wrapper.findComponent(JiraImportProgress);
- const getSetupComponent = () => wrapper.find(JiraImportSetup);
+ const getSetupComponent = () => wrapper.findComponent(JiraImportSetup);
- const getAlert = () => wrapper.find(GlAlert);
+ const getAlert = () => wrapper.findComponent(GlAlert);
- const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const mountComponent = ({
isJiraConfigured = true,
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 41d3cd46d01..783ecec14a2 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -289,7 +289,7 @@ describe('JiraImportForm', () => {
it('updates the user list', () => {
expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1);
- expect(getUserDropdown().find(GlDropdownItem).text()).toContain(
+ expect(getUserDropdown().findComponent(GlDropdownItem).text()).toContain(
'fchopin (Frederic Chopin)',
);
});
diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js
index 04b2a2da622..42356763492 100644
--- a/spec/frontend/jira_import/components/jira_import_progress_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js
@@ -8,7 +8,7 @@ describe('JiraImportProgress', () => {
const importProject = 'JIRAPROJECT';
- const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
+ const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
const getParagraphText = () => wrapper.find('p').text();
diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js
index 320e270b493..0085a2b5572 100644
--- a/spec/frontend/jira_import/components/jira_import_setup_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js
@@ -6,7 +6,7 @@ import { illustration, jiraIntegrationPath } from '../mock_data';
describe('JiraImportSetup', () => {
let wrapper;
- const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
+ const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
beforeEach(() => {
wrapper = shallowMount(JiraImportSetup, {
diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js
index 6204138f885..24a803d3f16 100644
--- a/spec/frontend/labels/components/delete_label_modal_spec.js
+++ b/spec/frontend/labels/components/delete_label_modal_spec.js
@@ -34,7 +34,7 @@ describe('~/labels/components/delete_label_modal', () => {
wrapper.destroy();
});
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
describe('template', () => {
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 9b908e5b6f0..9172876e76f 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,7 +1,7 @@
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';
diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
index 4fdc4024e10..9b5641ef7b3 100644
--- a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
+++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
@@ -49,8 +49,8 @@ describe('Merge Conflict Resolver App', () => {
extendedWrapper(w).findByTestId('interactive-button');
const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button');
const findSideBySideButton = () => wrapper.findByTestId('side-by-side');
- const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines);
- const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines);
+ const findInlineConflictLines = (w = wrapper) => w.findComponent(InlineConflictLines);
+ const findParallelConflictLines = (w = wrapper) => w.findComponent(ParallelConflictLines);
const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message');
it('shows the amount of conflicts', () => {
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index a8e3d13dca0..aee13beacbb 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -96,9 +96,9 @@ describe('Milestone combobox component', () => {
const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findProjectMilestonesSection = () =>
wrapper.find('[data-testid="project-milestones-section"]');
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 6cfbdb16111..048fca846ad 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -25,7 +25,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
};
const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
- const findMenuSections = () => wrapper.find(TopNavMenuSections);
+ const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js
index a7430d8c73f..b9cf39b8c1d 100644
--- a/spec/frontend/nav/components/top_nav_menu_item_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js
@@ -26,7 +26,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const findButtonIcons = () =>
findButton()
.findAllComponents(GlIcon)
diff --git a/spec/frontend/notebook/cells/output/latex_spec.js b/spec/frontend/notebook/cells/output/latex_spec.js
index 848d2069421..ed3b63be50a 100644
--- a/spec/frontend/notebook/cells/output/latex_spec.js
+++ b/spec/frontend/notebook/cells/output/latex_spec.js
@@ -27,7 +27,7 @@ describe('LaTeX output cell', () => {
${1} | ${false}
`('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => {
const wrapper = createComponent(inlineLatex, index);
- const prompt = wrapper.find(Prompt);
+ const prompt = wrapper.findComponent(Prompt);
expect(prompt.props().count).toEqual(count);
expect(prompt.props().showOutput).toEqual(expectation);
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 8befcc496d6..76177229cff 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -3,7 +3,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NoteHeader from '~/notes/components/note_header.vue';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
Vue.use(Vuex);
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index c5d201c3aec..02de67f788d 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -56,7 +56,7 @@ describe('CustomNotificationsModal', () => {
);
}
- const findModalBodyDescription = () => wrapper.find(GlSprintf);
+ const findModalBodyDescription = () => wrapper.findComponent(GlSprintf);
const findAllCheckboxes = () => wrapper.findAll(GlFormCheckbox);
const findCheckboxAt = (index) => findAllCheckboxes().at(index);
@@ -111,7 +111,7 @@ describe('CustomNotificationsModal', () => {
const checkbox = findCheckboxAt(index);
expect(checkbox.text()).toContain(eventName);
expect(checkbox.vm.$attrs.checked).toBe(enabled);
- expect(checkbox.find(GlLoadingIcon).exists()).toBe(loading);
+ expect(checkbox.findComponent(GlLoadingIcon).exists()).toBe(loading);
},
);
});
@@ -142,7 +142,7 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent({ injectedProperties });
- wrapper.find(GlModal).vm.$emit('show');
+ wrapper.findComponent(GlModal).vm.$emit('show');
await waitForPromises();
@@ -159,7 +159,7 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent();
- wrapper.find(GlModal).vm.$emit('show');
+ wrapper.findComponent(GlModal).vm.$emit('show');
expect(wrapper.vm.isLoading).toBe(true);
await waitForPromises();
@@ -176,7 +176,7 @@ describe('CustomNotificationsModal', () => {
mockAxios.onGet('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent();
- wrapper.find(GlModal).vm.$emit('show');
+ wrapper.findComponent(GlModal).vm.$emit('show');
await waitForPromises();
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index 7ca6c2052ae..1f950683630 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -40,12 +40,12 @@ describe('NotificationsDropdown', () => {
});
}
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
const findDropdownItemAt = (index) =>
- findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
- const findNotificationsModal = () => wrapper.find(CustomNotificationsModal);
+ findAllNotificationsDropdownItems().at(index).findComponent(GlDropdownItem);
+ const findNotificationsModal = () => wrapper.findComponent(CustomNotificationsModal);
const clickDropdownItemAt = async (index) => {
const dropdownItem = findDropdownItemAt(index);
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 21145466016..810049220ae 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -63,7 +63,7 @@ describe('operation settings external dashboard component', () => {
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
mountComponent();
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.text()).toBe('Expand');
});
@@ -82,7 +82,7 @@ describe('operation settings external dashboard component', () => {
});
it('renders help page link', () => {
- const link = subHeader.find(GlLink);
+ const link = subHeader.findComponent(GlLink);
expect(link.text()).toBe('Learn more.');
expect(link.attributes().href).toBe(helpPage);
@@ -96,7 +96,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent(false);
- formGroup = wrapper.find(DashboardTimezone).find(GlFormGroup);
+ formGroup = wrapper.findComponent(DashboardTimezone).findComponent(GlFormGroup);
});
it('uses label text', () => {
@@ -117,7 +117,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent();
- select = wrapper.find(DashboardTimezone).find(GlFormSelect);
+ select = wrapper.findComponent(DashboardTimezone).findComponent(GlFormSelect);
});
it('defaults to externalDashboardUrl', () => {
@@ -132,7 +132,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent(false);
- formGroup = wrapper.find(ExternalDashboard).find(GlFormGroup);
+ formGroup = wrapper.findComponent(ExternalDashboard).findComponent(GlFormGroup);
});
it('uses label text', () => {
@@ -153,7 +153,7 @@ describe('operation settings external dashboard component', () => {
beforeEach(() => {
mountComponent();
- input = wrapper.find(ExternalDashboard).find(GlFormInput);
+ input = wrapper.findComponent(ExternalDashboard).findComponent(GlFormInput);
});
it('defaults to externalDashboardUrl', () => {
@@ -167,7 +167,7 @@ describe('operation settings external dashboard component', () => {
});
describe('submit button', () => {
- const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+ const findSubmitButton = () => wrapper.find('.settings-content form').findComponent(GlButton);
const endpointRequest = [
operationsSettingsEndpoint,
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
index ad67128502a..ff11c8843bb 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
@@ -11,8 +11,8 @@ describe('delete_button', () => {
tooltipTitle: 'Bar tooltipTitle',
};
- const findButton = () => wrapper.find(GlButton);
- const findTooltip = () => wrapper.find(GlTooltip);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
const mountComponent = (props) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index 9680e273add..4a026f35822 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -13,8 +13,8 @@ import {
describe('Delete alert', () => {
let wrapper;
- const findAlert = () => wrapper.find(GlAlert);
- const findLink = () => wrapper.find(GlLink);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
const mountComponent = (propsData) => {
wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
index 1a27481a828..ce5ecfe4608 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
@@ -9,7 +9,7 @@ import {
describe('Partial Cleanup alert', () => {
let wrapper;
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findRunLink = () => wrapper.find('[data-testid="run-link"');
const findHelpLink = () => wrapper.find('[data-testid="help-link"');
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
index a11b102d9a6..d83a5099bcd 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
@@ -14,8 +14,8 @@ import {
describe('Status Alert', () => {
let wrapper;
- const findLink = () => wrapper.find(GlLink);
- const findAlert = () => wrapper.find(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findMessage = () => wrapper.find('[data-testid="message"]');
const mountComponent = (propsData) => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index 84f01f10f21..88979ea2b25 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -359,7 +359,7 @@ describe('tags list row', () => {
mountComponent();
await nextTick();
- expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard);
+ expect(finderFunction().findComponent(ClipboardButton).exists()).toBe(clipboard);
});
it('is disabled when the component is disabled', async () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index 61503d0f3bf..535faebdd4e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -16,7 +16,7 @@ describe('cleanup_status', () => {
let wrapper;
const findMainIcon = () => wrapper.findByTestId('main-icon');
- const findMainIconName = () => wrapper.findByTestId('main-icon').find(GlIcon);
+ const findMainIconName = () => wrapper.findByTestId('main-icon').findComponent(GlIcon);
const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
const findPopover = () => wrapper.findComponent(GlPopover);
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index d12933526bc..0b59fe2d8ce 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -233,7 +233,7 @@ describe('Image List Row', () => {
it('contains a tag icon', () => {
mountComponent();
- const icon = findTagsCount().find(GlIcon);
+ const icon = findTagsCount().findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('tag');
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
index e0119954ed4..4d33f75a5fd 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
@@ -9,7 +9,7 @@ describe('Image List', () => {
let wrapper;
const findRow = () => wrapper.findAll(ImageListRow);
- const findPagination = () => wrapper.find(GlKeysetPagination);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const mountComponent = (props) => {
wrapper = shallowMount(Component, {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index a006de9f00c..e6d81d4a28f 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -17,7 +17,7 @@ jest.mock('~/lib/utils/datetime_utility', () => ({
describe('registry_header', () => {
let wrapper;
- const findTitleArea = () => wrapper.find(TitleArea);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 1d161888a4d..ee6470a9df8 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -45,16 +45,16 @@ describe('Details Page', () => {
let wrapper;
let apolloProvider;
- const findDeleteModal = () => wrapper.find(DeleteModal);
- const findPagination = () => wrapper.find(GlKeysetPagination);
- const findTagsLoader = () => wrapper.find(TagsLoader);
- const findTagsList = () => wrapper.find(TagsList);
- const findDeleteAlert = () => wrapper.find(DeleteAlert);
- const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findEmptyState = () => wrapper.find(GlEmptyState);
- const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
- const findStatusAlert = () => wrapper.find(StatusAlert);
- const findDeleteImage = () => wrapper.find(DeleteImage);
+ const findDeleteModal = () => wrapper.findComponent(DeleteModal);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const findTagsList = () => wrapper.findComponent(TagsList);
+ const findDeleteAlert = () => wrapper.findComponent(DeleteAlert);
+ const findDetailsHeader = () => wrapper.findComponent(DetailsHeader);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findPartialCleanupAlert = () => wrapper.findComponent(PartialCleanupAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
+ const findDeleteImage = () => wrapper.findComponent(DeleteImage);
const routeId = 1;
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
index 5f4cb8969bc..add772d27ef 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
@@ -4,7 +4,7 @@ import component from '~/packages_and_registries/container_registry/explorer/pag
describe('List Page', () => {
let wrapper;
- const findRouterView = () => wrapper.find({ ref: 'router-view' });
+ const findRouterView = () => wrapper.findComponent({ ref: 'router-view' });
const mountComponent = () => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
index f6c4d94bb05..b9d6dc2679e 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
@@ -9,9 +9,9 @@ import { defaultConfig, harborArtifactsList } from '../../mock_data';
describe('Harbor artifacts list', () => {
let wrapper;
- const findTagsLoader = () => wrapper.find(TagsLoader);
- const findGlEmptyState = () => wrapper.find(GlEmptyState);
- const findRegistryList = () => wrapper.find(RegistryList);
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findRegistryList = () => wrapper.findComponent(RegistryList);
const findArtifactsListRow = () => wrapper.findAllComponents(ArtifactsListRow);
const mountComponent = ({ propsData, config = defaultConfig }) => {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
index 636f3eeb04a..6602aed0a0c 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
@@ -12,9 +12,9 @@ import {
describe('harbor_list_header', () => {
let wrapper;
- const findTitleArea = () => wrapper.find(TitleArea);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
- const findImagesMetaDataItem = () => wrapper.find(MetadataItem);
+ const findImagesMetaDataItem = () => wrapper.findComponent(MetadataItem);
const mountComponent = async (propsData, slots) => {
wrapper = shallowMount(HarborListHeader, {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
index bd1abc7775d..b62d4e8836b 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
@@ -10,7 +10,7 @@ describe('Harbor List Row', () => {
let wrapper;
const item = harborImagesList[0];
- const findDetailsLink = () => wrapper.find(RouterLink);
+ const findDetailsLink = () => wrapper.findComponent(RouterLink);
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findArtifactsCount = () => wrapper.find('[data-testid="artifacts-count"]');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
@@ -64,7 +64,7 @@ describe('Harbor List Row', () => {
it('contains a package icon', () => {
mountComponent();
- const icon = findArtifactsCount().find(GlIcon);
+ const icon = findArtifactsCount().findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('package');
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
index 15e1130e058..8fd50bea280 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
@@ -24,10 +24,10 @@ jest.mock('~/rest_api', () => ({
describe('Harbor Details Page', () => {
let wrapper;
- const findTagsLoader = () => wrapper.find(TagsLoader);
- const findArtifactsList = () => wrapper.find(ArtifactsList);
- const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findPersistedSearch = () => wrapper.find(PersistedSearch);
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const findArtifactsList = () => wrapper.findComponent(ArtifactsList);
+ const findDetailsHeader = () => wrapper.findComponent(DetailsHeader);
+ const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const waitForHarborDetailRequest = async () => {
await waitForPromises();
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
index 55fc8066f65..942cf9bad2c 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
@@ -4,7 +4,7 @@ import component from '~/packages_and_registries/harbor_registry/pages/index.vue
describe('List Page', () => {
let wrapper;
- const findRouterView = () => wrapper.find({ ref: 'router-view' });
+ const findRouterView = () => wrapper.findComponent({ ref: 'router-view' });
const mountComponent = () => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
index 69c78e64e22..e74375b7705 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
@@ -76,8 +76,8 @@ describe('PackagesApp', () => {
const packageTitle = () => wrapper.findComponent(TerraformTitle);
const emptyState = () => wrapper.findComponent(GlEmptyState);
const deleteButton = () => wrapper.find('.js-delete-button');
- const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' });
- const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' });
+ const findDeleteModal = () => wrapper.findComponent({ ref: 'deleteModal' });
+ const findDeleteFileModal = () => wrapper.findComponent({ ref: 'deleteFileModal' });
const versionsTab = () => wrapper.find('.js-versions-tab > a');
const packagesLoader = () => wrapper.findComponent(PackagesListLoader);
const packagesVersionRows = () => wrapper.findAllComponents(PackageListRow);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
index 95de2f0bb0b..b76d7c2b57b 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
@@ -17,8 +17,8 @@ describe('Package Files', () => {
const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]');
const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]');
const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]');
- const findFirstRowFileIcon = () => findFirstRow().find(FileIcon);
- const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip);
+ const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon);
+ const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip);
const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown);
const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]');
const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
index f10f05f4a0d..c6b5138639e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
@@ -36,8 +36,8 @@ describe('Package History', () => {
});
const findHistoryElement = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findElementLink = (container) => container.find(GlLink);
- const findElementTimeAgo = (container) => container.find(TimeAgoTooltip);
+ const findElementLink = (container) => container.findComponent(GlLink);
+ const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip);
const findTitle = () => wrapper.find('[data-testid="title"]');
const findTimeline = () => wrapper.find('[data-testid="timeline"]');
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index 72d08d5683b..93d013bb458 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -7,8 +7,8 @@ describe('Infrastructure Title', () => {
let wrapper;
let store;
- const findTitleArea = () => wrapper.find(TitleArea);
- const findMetadataItem = () => wrapper.find(MetadataItem);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
+ const findMetadataItem = () => wrapper.findComponent(MetadataItem);
const exampleProps = { helpUrl: 'http://example.gitlab.com/help' };
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index 31616e0b2f5..db1d3f3f633 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -31,9 +31,9 @@ describe('packages_list_app', () => {
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const emptyListHelpUrl = 'helpUrl';
- const findEmptyState = () => wrapper.find(GlEmptyState);
- const findListComponent = () => wrapper.find(PackageList);
- const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findListComponent = () => wrapper.findComponent(PackageList);
+ const findInfrastructureSearch = () => wrapper.findComponent(InfrastructureSearch);
const createStore = ({ filter = [], packageCount = 0 } = {}) => {
store = new Vuex.Store({
@@ -151,7 +151,7 @@ describe('packages_list_app', () => {
describe('empty state', () => {
it('generate the correct empty list link', () => {
- const link = findListComponent().find(GlLink);
+ const link = findListComponent().findComponent(GlLink);
expect(link.attributes('href')).toBe(emptyListHelpUrl);
expect(link.text()).toBe('publish and share your packages');
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index fed82653016..fb5ee4e6884 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -20,11 +20,11 @@ describe('packages_list', () => {
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
- const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
- const findPackageListPagination = () => wrapper.find(GlPagination);
- const findPackageListDeleteModal = () => wrapper.find(GlModal);
- const findEmptySlot = () => wrapper.find(EmptySlotStub);
- const findPackagesListRow = () => wrapper.find(PackagesListRow);
+ const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
+ const findPackageListPagination = () => wrapper.findComponent(GlPagination);
+ const findPackageListDeleteModal = () => wrapper.findComponent(GlModal);
+ const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
+ const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const createStore = (isGroupPage, packages, isLoading) => {
const state = {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js
index abb0d23b6e4..db90bb4c25f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js
@@ -5,7 +5,7 @@ import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_
describe('InfrastructureIconAndName', () => {
let wrapper;
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const mountComponent = () => {
wrapper = shallowMount(InfrastructureIconAndName, {});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index eb1e76377ff..b5a512b8806 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -30,10 +30,10 @@ describe('packages_list_row', () => {
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
const packageCannotDestroy = { ...packageData(), canDestroy: false };
- const findPackageTags = () => wrapper.find(PackageTags);
- const findPackagePath = () => wrapper.find(PackagePath);
+ const findPackageTags = () => wrapper.findComponent(PackageTags);
+ const findPackagePath = () => wrapper.findComponent(PackagePath);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
- const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
+ const findPackageIconAndName = () => wrapper.findComponent(PackageIconAndName);
const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
index 23e5c7330d5..b47515e15c3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -7,8 +7,8 @@ describe('PackageTitle', () => {
let wrapper;
let store;
- const findTitleArea = () => wrapper.find(TitleArea);
- const findMetadataItem = () => wrapper.find(MetadataItem);
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
+ const findMetadataItem = () => wrapper.findComponent(MetadataItem);
const mountComponent = (propsData = { helpUrl: 'foo' }) => {
wrapper = shallowMount(PackageTitle, {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
index d0c111bae2d..e2af06416b4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -6,7 +6,7 @@ import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/consta
describe('packages_filter', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
const mountComponent = ({ attrs, listeners } = {}) => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
index 51fcb1d7300..8b60f31512b 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
@@ -117,9 +117,9 @@ describe('Cleanup image tags project settings', () => {
it('shows the admin part of the alert message', () => {
mountComponent({ ...defaultProvidedValues, isAdmin: true });
- const sprintf = findAlert().find(GlSprintf);
+ const sprintf = findAlert().findComponent(GlSprintf);
expect(sprintf.text()).toBe('administration settings');
- expect(sprintf.find(GlLink).attributes('href')).toBe(
+ expect(sprintf.findComponent(GlLink).attributes('href')).toBe(
defaultProvidedValues.adminSettingsPath,
);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index ca44e77e694..5bd6608a9b8 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -36,7 +36,7 @@ describe('Container Expiration Policy Settings Form', () => {
label: 'docker_container_retention_and_expiration_policies',
};
- const findForm = () => wrapper.find({ ref: 'form-element' });
+ const findForm = () => wrapper.findComponent({ ref: 'form-element' });
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"');
const findSaveButton = () => wrapper.find('[data-testid="save-button"');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index 827cd05390a..35baeaeac61 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -35,7 +35,7 @@ describe('Container expiration policy project settings', () => {
const findDescription = () => wrapper.findByTestId('description');
const findButton = () => wrapper.findByTestId('rules-button');
const findAlert = () => wrapper.findComponent(GlAlert);
- const findSettingsBlock = () => wrapper.find(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMountExtended(component, {
@@ -97,9 +97,9 @@ describe('Container expiration policy project settings', () => {
it('shows the admin part of the alert message', () => {
mountComponent({ ...defaultProvidedValues, isAdmin: true });
- const sprintf = findAlert().find(GlSprintf);
+ const sprintf = findAlert().findComponent(GlSprintf);
expect(sprintf.text()).toBe('administration settings');
- expect(sprintf.find(GlLink).attributes('href')).toBe(
+ expect(sprintf.findComponent(GlLink).attributes('href')).toBe(
defaultProvidedValues.adminSettingsPath,
);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
index 8b99ac6b06c..ae41fdf65e0 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -14,8 +14,8 @@ describe('ExpirationDropdown', () => {
],
};
- const findFormSelect = () => wrapper.find(GlFormSelect);
- const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findFormSelect = () => wrapper.findComponent(GlFormSelect);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findDescription = () => wrapper.find('[data-testid="description"]');
const findOptions = () => wrapper.findAll('[data-testid="option"]');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
index 6b681924fcf..1cea0704154 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
@@ -16,11 +16,11 @@ describe('ExpirationInput', () => {
const tagsRegexHelpPagePath = 'fooPath';
- const findInput = () => wrapper.find(GlFormInput);
- const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findLabel = () => wrapper.find('[data-testid="label"]');
const findDescription = () => wrapper.find('[data-testid="description"]');
- const findDescriptionLink = () => wrapper.find(GlLink);
+ const findDescriptionLink = () => wrapper.findComponent(GlLink);
const mountComponent = (props) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
index 94f7783afe7..653f2a8b40e 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
@@ -11,8 +11,8 @@ describe('ExpirationToggle', () => {
let wrapper;
const value = 'foo';
- const findInput = () => wrapper.find(GlFormInput);
- const findFormGroup = () => wrapper.find(GlFormGroup);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const mountComponent = (propsData) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
index 45039614e49..55a66cebd83 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
@@ -10,7 +10,7 @@ import {
describe('ExpirationToggle', () => {
let wrapper;
- const findToggle = () => wrapper.find(GlToggle);
+ const findToggle = () => wrapper.findComponent(GlToggle);
const findDescription = () => wrapper.find('[data-testid="description"]');
const mountComponent = (propsData) => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
index 86f45d78bae..daf0ee85fdf 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
@@ -39,7 +39,7 @@ describe('Packages Cleanup Policy Settings Form', () => {
label: 'packages_cleanup_policies',
};
- const findForm = () => wrapper.find({ ref: 'form-element' });
+ const findForm = () => wrapper.findComponent({ ref: 'form-element' });
const findSaveButton = () => wrapper.findByTestId('save-button');
const findKeepNDuplicatedPackageFilesDropdown = () =>
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index f576bc79eae..5dd27679708 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -6,8 +6,8 @@ import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/co
describe('Registry Settings app', () => {
let wrapper;
- const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
- const findPackagesCleanupPolicy = () => wrapper.find(PackagesCleanupPolicy);
+ const findContainerExpirationPolicy = () => wrapper.findComponent(ContainerExpirationPolicy);
+ const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
index 22c2d0d3b1a..dfa91fd8740 100644
--- a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
@@ -22,7 +22,7 @@ Vue.use(Vuex);
describe('cli_commands', () => {
let wrapper;
- const findDropdownButton = () => wrapper.find(GlDropdown);
+ const findDropdownButton = () => wrapper.findComponent(GlDropdown);
const findCodeInstruction = () => wrapper.findAll(CodeInstruction);
const mountComponent = () => {
diff --git a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
index d6d1970cb12..a0ff6ca01b5 100644
--- a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
@@ -5,7 +5,7 @@ import PackageIconAndName from '~/packages_and_registries/shared/components/pack
describe('PackageIconAndName', () => {
let wrapper;
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const mountComponent = () => {
wrapper = shallowMount(PackageIconAndName, {
diff --git a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
index 7a8a249cb2a..b393a855a9f 100644
--- a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
+++ b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
@@ -36,7 +36,7 @@ describe('BitbucketServerStatusTable', () => {
it('renders bitbucket status table component', () => {
createComponent();
- expect(wrapper.find(BitbucketStatusTable).exists()).toBe(true);
+ expect(wrapper.findComponent(BitbucketStatusTable).exists()).toBe(true);
});
it('renders Reconfigure button', async () => {
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index a850b1655f7..1790a9c9bf5 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -84,7 +84,7 @@ describe('BulkImportsHistoryApp', () => {
describe('general behavior', () => {
it('renders loading state when loading', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders empty state when no data is available', async () => {
@@ -92,8 +92,8 @@ describe('BulkImportsHistoryApp', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
it('renders table with data when history is available', async () => {
@@ -101,7 +101,7 @@ describe('BulkImportsHistoryApp', () => {
createComponent();
await axios.waitForAll();
- const table = wrapper.find(GlTable);
+ const table = wrapper.findComponent(GlTable);
expect(table.exists()).toBe(true);
// can't use .props() or .attributes() here
expect(table.vm.$attrs.items).toHaveLength(DUMMY_RESPONSE.length);
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
index 4ff3f0361cf..82a3e11186e 100644
--- a/spec/frontend/pages/import/history/components/import_error_details_spec.js
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -41,7 +41,7 @@ describe('ImportErrorDetails', () => {
describe('general behavior', () => {
it('renders loading state when loading', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders import_error if it is available', async () => {
@@ -50,7 +50,7 @@ describe('ImportErrorDetails', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('pre').text()).toBe(FAKE_IMPORT_ERROR);
});
@@ -59,7 +59,7 @@ describe('ImportErrorDetails', () => {
createComponent();
await axios.waitForAll();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('pre').text()).toBe('No additional information provided.');
});
});
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
deleted file mode 100644
index fa6e7e51a60..00000000000
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import $ from 'jquery';
-import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import EmojiMenu from '~/pages/profiles/show/emoji_menu';
-
-describe('EmojiMenu', () => {
- const dummyEmojiTag = '<dummy></tag>';
- const dummyToggleButtonSelector = '.toggle-button-selector';
- const dummyMenuClass = 'dummy-menu-class';
-
- let emojiMenu;
- let dummySelectEmojiCallback;
- let dummyEmojiList;
-
- beforeEach(() => {
- dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback');
- dummyEmojiList = {
- glEmojiTag() {
- return dummyEmojiTag;
- },
- normalizeEmojiName(emoji) {
- return emoji;
- },
- isEmojiNameValid() {
- return true;
- },
- getEmojiCategoryMap() {
- return { dummyCategory: [] };
- },
- };
-
- emojiMenu = new EmojiMenu(
- dummyEmojiList,
- dummyToggleButtonSelector,
- dummyMenuClass,
- dummySelectEmojiCallback,
- );
- });
-
- afterEach(() => {
- emojiMenu.destroy();
- });
-
- describe('addAward', () => {
- const dummyAwardUrl = `${TEST_HOST}/award/url`;
- const dummyEmoji = 'tropical_fish';
- const dummyVotesBlock = () => $('<div />');
-
- it('calls selectEmojiCallback', async () => {
- expect(dummySelectEmojiCallback).not.toHaveBeenCalled();
-
- await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false);
- expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag);
- });
-
- it('does not make an axios request', async () => {
- jest.spyOn(axios, 'request').mockReturnValue();
-
- await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false);
- expect(axios.request).not.toHaveBeenCalled();
- });
- });
-
- describe('bindEvents', () => {
- beforeEach(() => {
- jest.spyOn(emojiMenu, 'registerEventListener').mockReturnValue();
- });
-
- it('binds event listeners to custom toggle button', () => {
- emojiMenu.bindEvents();
-
- expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
- 'one',
- expect.anything(),
- 'mouseenter focus',
- dummyToggleButtonSelector,
- 'mouseenter focus',
- expect.anything(),
- );
-
- expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
- 'on',
- expect.anything(),
- 'click',
- dummyToggleButtonSelector,
- expect.anything(),
- );
- });
-
- it('binds event listeners to custom menu class', () => {
- emojiMenu.bindEvents();
-
- expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
- 'on',
- expect.anything(),
- 'click',
- `.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
- expect.anything(),
- );
- });
- });
-
- describe('createEmojiMenu', () => {
- it('renders the menu with custom menu class', () => {
- const menuElement = () =>
- document.body.querySelector(`.emoji-menu.${dummyMenuClass} .emoji-menu-content`);
-
- expect(menuElement()).toBe(null);
-
- emojiMenu.createEmojiMenu();
-
- expect(menuElement()).not.toBe(null);
- });
- });
-});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 2f452f909ff..8fb59f38ee1 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -58,7 +58,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
it('displays the commit duration chart', () => {
const chart = wrapper.findComponent(GlColumnChart);
- expect(chart.exists()).toBeTruthy();
+ expect(chart.exists()).toBe(true);
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index 552ee29b6f9..f2281223a25 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -25,7 +25,12 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
- const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({
+ props = {},
+ stubs,
+ mountFn = shallowMountExtended,
+ enforceRunnerTokenExpiresAt = false,
+ } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
...props,
@@ -34,6 +39,9 @@ describe('RunnerDetails', () => {
RunnerDetail,
...stubs,
},
+ provide: {
+ glFeatures: { enforceRunnerTokenExpiresAt },
+ },
});
};
@@ -63,6 +71,8 @@ describe('RunnerDetails', () => {
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
+ ${'Token expiry'} | ${{ tokenExpiresAt: mockOneHourAgo }} | ${'1 hour ago'}
+ ${'Token expiry'} | ${{ tokenExpiresAt: null }} | ${'Never expires'}
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
@@ -72,6 +82,7 @@ describe('RunnerDetails', () => {
...runner,
},
},
+ enforceRunnerTokenExpiresAt: true,
stubs: {
GlIntersperse,
GlSprintf,
@@ -124,5 +135,22 @@ describe('RunnerDetails', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
});
+
+ describe('Token expiration field', () => {
+ it.each`
+ case | flag | shown
+ ${'is shown when feature flag is enabled'} | ${true} | ${true}
+ ${'is not shown when feature flag is disabled'} | ${false} | ${false}
+ `('$case', ({ flag, shown }) => {
+ createComponent({
+ props: {
+ runner: mockGroupRunner,
+ },
+ enforceRunnerTokenExpiresAt: flag,
+ });
+
+ expect(findDd('Token expiry', wrapper).exists()).toBe(shown);
+ });
+ });
});
});
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/runner/utils_spec.js
index 1db9815dfd8..33de1345f85 100644
--- a/spec/frontend/runner/utils_spec.js
+++ b/spec/frontend/runner/utils_spec.js
@@ -1,4 +1,4 @@
-import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils';
+import { formatJobCount, tableField, getPaginationVariables, parseInterval } from '~/runner/utils';
describe('~/runner/utils', () => {
describe('formatJobCount', () => {
@@ -66,4 +66,15 @@ describe('~/runner/utils', () => {
expect(getPaginationVariables(pagination, pageSize)).toEqual(variables);
});
});
+
+ describe('parseInterval', () => {
+ it.each`
+ case | argument | returnValue
+ ${'parses integer'} | ${'86400'} | ${86400}
+ ${'returns null for undefined'} | ${undefined} | ${null}
+ ${'returns null for null'} | ${null} | ${null}
+ `('$case', ({ argument, returnValue }) => {
+ expect(parseInterval(argument)).toStrictEqual(returnValue);
+ });
+ });
});
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 4191c44bb99..c5fb590646d 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -1,14 +1,13 @@
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
import createFlash from '~/flash';
import stubChildren from 'helpers/stub_children';
-import SetStatusModalWrapper, {
- AVAILABILITY_STATUS,
-} from '~/set_status_modal/set_status_modal_wrapper.vue';
+import SetStatusModalWrapper from '~/set_status_modal/set_status_modal_wrapper.vue';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
jest.mock('~/flash');
@@ -34,7 +33,7 @@ describe('SetStatusModalWrapper', () => {
};
const createComponent = (props = {}) => {
- return mount(SetStatusModalWrapper, {
+ return mountExtended(SetStatusModalWrapper, {
propsData: {
...defaultProps,
...props,
@@ -53,7 +52,8 @@ describe('SetStatusModalWrapper', () => {
};
const findModal = () => wrapper.find(GlModal);
- const findFormField = (field) => wrapper.find(`[name="user[status][${field}]"]`);
+ const findMessageField = () =>
+ wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
@@ -83,14 +83,8 @@ describe('SetStatusModalWrapper', () => {
return initModal();
});
- it('sets the hidden status emoji field', () => {
- const field = findFormField('emoji');
- expect(field.exists()).toBe(true);
- expect(field.element.value).toBe(defaultEmoji);
- });
-
it('sets the message field', () => {
- const field = findFormField('message');
+ const field = findMessageField();
expect(field.exists()).toBe(true);
expect(field.element.value).toBe(defaultMessage);
});
@@ -135,7 +129,7 @@ describe('SetStatusModalWrapper', () => {
});
it('does not set the message field', () => {
- expect(findFormField('message').element.value).toBe('');
+ expect(findMessageField().element.value).toBe('');
});
it('hides the clear status button', () => {
@@ -143,18 +137,6 @@ describe('SetStatusModalWrapper', () => {
});
});
- describe('with no currentEmoji set', () => {
- beforeEach(async () => {
- await initEmojiMock();
- wrapper = createComponent({ currentEmoji: '' });
- return initModal();
- });
-
- it('does not set the hidden status emoji field', () => {
- expect(findFormField('emoji').element.value).toBe('');
- });
- });
-
describe('with currentClearStatusAfter set', () => {
beforeEach(async () => {
await initEmojiMock();
@@ -184,8 +166,7 @@ describe('SetStatusModalWrapper', () => {
findModal().vm.$emit('secondary');
await nextTick();
- expect(findFormField('message').element.value).toBe('');
- expect(findFormField('emoji').element.value).toBe('');
+ expect(findMessageField().element.value).toBe('');
});
it('clicking "setStatus" submits the user status', async () => {
diff --git a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
new file mode 100644
index 00000000000..eaee0e77311
--- /dev/null
+++ b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
@@ -0,0 +1,156 @@
+import { nextTick } from 'vue';
+import { cloneDeep } from 'lodash';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { resetHTMLFixture } from 'helpers/fixtures';
+import { useFakeDate } from 'helpers/fake_date';
+import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
+import SetStatusForm from '~/set_status_modal/set_status_form.vue';
+import { TIME_RANGES_WITH_NEVER, NEVER_TIME_RANGE } from '~/set_status_modal/constants';
+
+describe('UserProfileSetStatusWrapper', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ fields: {
+ emoji: { name: 'user[status][emoji]', id: 'user_status_emoji', value: '8ball' },
+ message: { name: 'user[status][message]', id: 'user_status_message', value: 'foo bar' },
+ availability: {
+ name: 'user[status][availability]',
+ id: 'user_status_availability',
+ value: 'busy',
+ },
+ clearStatusAfter: {
+ name: 'user[status][clear_status_after]',
+ id: 'user_status_clear_status_after',
+ value: '2022-09-03 03:06:26 UTC',
+ },
+ },
+ };
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = mountExtended(UserProfileSetStatusWrapper, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ });
+ };
+
+ const findInput = (name) => wrapper.find(`[name="${name}"]`);
+ const findSetStatusForm = () => wrapper.findComponent(SetStatusForm);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders `SetStatusForm` component and passes expected props', () => {
+ createComponent();
+
+ expect(cloneDeep(findSetStatusForm().props())).toMatchObject({
+ defaultEmoji: 'speech_balloon',
+ emoji: defaultProvide.fields.emoji.value,
+ message: defaultProvide.fields.message.value,
+ availability: true,
+ clearStatusAfter: NEVER_TIME_RANGE,
+ currentClearStatusAfter: defaultProvide.fields.clearStatusAfter.value,
+ });
+ });
+
+ it.each`
+ input
+ ${'emoji'}
+ ${'message'}
+ ${'availability'}
+ `('renders hidden $input input with value set', ({ input }) => {
+ createComponent();
+
+ expect(findInput(defaultProvide.fields[input].name).attributes('value')).toBe(
+ defaultProvide.fields[input].value,
+ );
+ });
+
+ describe('when clear status after dropdown is set to `Never`', () => {
+ it('renders hidden clear status after input with value unset', () => {
+ createComponent();
+
+ expect(
+ findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value'),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('when clear status after dropdown has a value selected', () => {
+ it('renders hidden clear status after input with value set', async () => {
+ createComponent();
+
+ findSetStatusForm().vm.$emit('clear-status-after-click', TIME_RANGES_WITH_NEVER[1]);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value')).toBe(
+ TIME_RANGES_WITH_NEVER[1].shortcut,
+ );
+ });
+ });
+
+ describe('when emoji is changed', () => {
+ it('updates hidden emoji input value', async () => {
+ createComponent();
+
+ const newEmoji = 'basketball';
+
+ findSetStatusForm().vm.$emit('emoji-click', newEmoji);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.emoji.name).attributes('value')).toBe(newEmoji);
+ });
+ });
+
+ describe('when message is changed', () => {
+ it('updates hidden message input value', async () => {
+ createComponent();
+
+ const newMessage = 'foo bar baz';
+
+ findSetStatusForm().vm.$emit('message-input', newMessage);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.message.name).attributes('value')).toBe(newMessage);
+ });
+ });
+
+ describe('when form is successfully submitted', () => {
+ // 2022-09-02 00:00:00 UTC
+ useFakeDate(2022, 8, 2);
+
+ const form = document.createElement('form');
+ form.classList.add('js-edit-user');
+
+ beforeEach(async () => {
+ document.body.appendChild(form);
+ createComponent();
+
+ const oneDay = TIME_RANGES_WITH_NEVER[4];
+
+ findSetStatusForm().vm.$emit('clear-status-after-click', oneDay);
+
+ await nextTick();
+
+ form.dispatchEvent(new Event('ajax:success'));
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('updates clear status after dropdown to `Never`', () => {
+ expect(findSetStatusForm().props('clearStatusAfter')).toBe(NEVER_TIME_RANGE);
+ });
+
+ it('updates `currentClearStatusAfter` prop', () => {
+ expect(findSetStatusForm().props('currentClearStatusAfter')).toBe('2022-09-03 00:00:00 UTC');
+ });
+ });
+});
diff --git a/spec/frontend/set_status_modal/utils_spec.js b/spec/frontend/set_status_modal/utils_spec.js
index 273f30f8311..1e918b75a98 100644
--- a/spec/frontend/set_status_modal/utils_spec.js
+++ b/spec/frontend/set_status_modal/utils_spec.js
@@ -1,4 +1,5 @@
-import { AVAILABILITY_STATUS, isUserBusy } from '~/set_status_modal/utils';
+import { isUserBusy } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
describe('Set status modal utils', () => {
describe('isUserBusy', () => {
diff --git a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
index 4dbf3d426bb..37c16bc9235 100644
--- a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
+++ b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
const name = 'Administrator';
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index b7ce3e47cef..ce9b7c5f061 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,7 +1,7 @@
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 6ec786507c0..d3165d8dc26 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -10,9 +10,9 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import updateWorkItemWidgetsMutation from '~/work_items/graphql/update_work_item_widgets.mutation.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
- updateWorkItemWidgetsResponse,
+ updateWorkItemMutationResponse,
workItemResponseFactory,
workItemQueryResponse,
} from '../mock_data';
@@ -31,7 +31,7 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
- const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse);
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
@@ -53,7 +53,7 @@ describe('WorkItemDescription', () => {
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemQuery, workItemResponseHandler],
- [updateWorkItemWidgetsMutation, mutationHandler],
+ [updateWorkItemMutation, mutationHandler],
]),
propsData: {
workItemId: id,
@@ -173,7 +173,7 @@ describe('WorkItemDescription', () => {
isEditing: true,
mutationHandler: jest.fn().mockResolvedValue({
data: {
- workItemUpdateWidgets: {
+ workItemUpdate: {
workItem: {},
errors: [error],
},
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 7651ee95b88..d9532910a94 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -227,17 +227,6 @@ export const workItemResponseFactory = ({
},
});
-export const updateWorkItemWidgetsResponse = {
- data: {
- workItemUpdateWidgets: {
- workItem: {
- id: 1234,
- },
- errors: [],
- },
- },
-};
-
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
diff --git a/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb
new file mode 100644
index 00000000000..ff813dd41d9
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/events/changed_reviewer_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedReviewer do
+ subject(:importer) { described_class.new(project, client) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:requested_reviewer) { create(:user) }
+ let_it_be(:review_requester) { create(:user) }
+
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ let(:issue_event) do
+ Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
+ 'id' => 6501124486,
+ 'actor' => { 'id' => 4, 'login' => 'alice' },
+ 'event' => event_type,
+ 'commit_id' => nil,
+ 'created_at' => '2022-04-26 18:30:53 UTC',
+ 'review_requester' => { 'id' => review_requester.id, 'login' => review_requester.username },
+ 'requested_reviewer' => { 'id' => requested_reviewer.id, 'login' => requested_reviewer.username },
+ 'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
+ )
+ end
+
+ let(:note_attrs) do
+ {
+ noteable_id: issuable.id,
+ noteable_type: issuable.class.name,
+ project_id: project.id,
+ author_id: review_requester.id,
+ system: true,
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ }.stringify_keys
+ end
+
+ let(:expected_system_note_metadata_attrs) do
+ {
+ action: 'reviewer',
+ created_at: issue_event.created_at,
+ updated_at: issue_event.created_at
+ }.stringify_keys
+ end
+
+ shared_examples 'create expected notes' do
+ it 'creates expected note' do
+ expect { importer.execute(issue_event) }.to change { issuable.notes.count }
+ .from(0).to(1)
+
+ expect(issuable.notes.last)
+ .to have_attributes(expected_note_attrs)
+ end
+
+ it 'creates expected system note metadata' do
+ expect { importer.execute(issue_event) }.to change(SystemNoteMetadata, :count)
+ .from(0).to(1)
+
+ expect(SystemNoteMetadata.last)
+ .to have_attributes(
+ expected_system_note_metadata_attrs.merge(
+ note_id: Note.last.id
+ )
+ )
+ end
+ end
+
+ shared_examples 'process review_requested & review_request_removed MR events' do
+ context 'when importing a review_requested event' do
+ let(:event_type) { 'review_requested' }
+ let(:expected_note_attrs) { note_attrs.merge(note: "requested review from @#{requested_reviewer.username}") }
+
+ it_behaves_like 'create expected notes'
+ end
+
+ context 'when importing a review_request_removed event' do
+ let(:event_type) { 'review_request_removed' }
+ let(:expected_note_attrs) { note_attrs.merge(note: "removed review request for @#{requested_reviewer.username}") }
+
+ it_behaves_like 'create expected notes'
+ end
+ end
+
+ describe '#execute' do
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
+ allow(finder).to receive(:database_id).and_return(issuable.id)
+ end
+ allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(requested_reviewer.id, requested_reviewer.username)
+ .and_return(requested_reviewer.id)
+ allow(finder).to receive(:find).with(review_requester.id, review_requester.username)
+ .and_return(review_requester.id)
+ end
+ end
+
+ it_behaves_like 'process review_requested & review_request_removed MR events'
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb
index 5db49ee1a75..91121f3c3fc 100644
--- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb
@@ -112,6 +112,20 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab
Gitlab::GithubImport::Importer::Events::ChangedAssignee
end
+ context "when it's review_requested issue event" do
+ let(:event_name) { 'review_requested' }
+
+ it_behaves_like 'triggers specific event importer',
+ Gitlab::GithubImport::Importer::Events::ChangedReviewer
+ end
+
+ context "when it's review_request_removed issue event" do
+ let(:event_name) { 'review_request_removed' }
+
+ it_behaves_like 'triggers specific event importer',
+ Gitlab::GithubImport::Importer::Events::ChangedReviewer
+ end
+
context "when it's unknown issue event" do
let(:event_name) { 'fake' }
diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
index 8d4c1b01e50..2c1af4f8948 100644
--- a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do
let(:parallel) { true }
let(:issue_event) do
struct = Struct.new(
- :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone,
- :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app,
+ :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, :source,
+ :assignee, :assigner, :review_requester, :requested_reviewer, :issue, :created_at, :performed_via_github_app,
keyword_init: true
)
struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC')
diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb
index 2414e46470a..0256858ecf1 100644
--- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_actor) { false }
it 'does not return such info' do
- expect(issue_event.actor).to eq nil
+ expect(issue_event.actor).to be_nil
end
end
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_label) { false }
it 'does not return such info' do
- expect(issue_event.label_title).to eq nil
+ expect(issue_event.label_title).to be_nil
end
end
@@ -72,8 +72,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_rename) { false }
it 'does not return such info' do
- expect(issue_event.old_title).to eq nil
- expect(issue_event.new_title).to eq nil
+ expect(issue_event.old_title).to be_nil
+ expect(issue_event.new_title).to be_nil
end
end
@@ -87,7 +87,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_milestone) { false }
it 'does not return such info' do
- expect(issue_event.milestone_title).to eq nil
+ expect(issue_event.milestone_title).to be_nil
end
end
@@ -104,7 +104,30 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_assignee) { false }
it 'does not return such info' do
- expect(issue_event.assignee).to eq nil
+ expect(issue_event.assignee).to be_nil
+ end
+ end
+
+ context 'when requested_reviewer and review_requester data is present' do
+ it 'includes requested_reviewer and review_requester details' do
+ expect(issue_event.requested_reviewer)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+ expect(issue_event.requested_reviewer.id).to eq(6)
+ expect(issue_event.requested_reviewer.login).to eq('mickey')
+
+ expect(issue_event.review_requester)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+ expect(issue_event.review_requester.id).to eq(7)
+ expect(issue_event.review_requester.login).to eq('minnie')
+ end
+ end
+
+ context 'when requested_reviewer and review_requester data is empty' do
+ let(:with_reviewer) { false }
+
+ it 'does not return such info' do
+ expect(issue_event.requested_reviewer).to be_nil
+ expect(issue_event.review_requester).to be_nil
end
end
@@ -142,7 +165,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:response) do
event_resource = Struct.new(
:id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone,
- :source, :assignee, :issue, :created_at, :performed_via_github_app,
+ :source, :assignee, :requested_reviewer, :review_requester, :issue, :created_at,
+ :performed_via_github_app,
keyword_init: true
)
user_resource = Struct.new(:id, :login, keyword_init: true)
@@ -160,6 +184,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
milestone: with_milestone ? { title: 'milestone title' } : nil,
source: { type: 'issue', id: 123456 },
assignee: with_assignee ? user_resource.new(id: 5, login: 'tom') : nil,
+ requested_reviewer: with_reviewer ? user_resource.new(id: 6, login: 'mickey') : nil,
+ review_requester: with_reviewer ? user_resource.new(id: 7, login: 'minnie') : nil,
issue: { 'number' => 2, 'pull_request' => pull_request },
created_at: '2022-04-26 18:30:53 UTC',
performed_via_github_app: nil
@@ -171,6 +197,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_rename) { true }
let(:with_milestone) { true }
let(:with_assignee) { true }
+ let(:with_reviewer) { true }
let(:pull_request) { nil }
it_behaves_like 'an IssueEvent' do
@@ -196,6 +223,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
'milestone_title' => (with_milestone ? 'milestone title' : nil),
'source' => { 'type' => 'issue', 'id' => 123456 },
'assignee' => (with_assignee ? { 'id' => 5, 'login' => 'tom' } : nil),
+ 'requested_reviewer' => (with_reviewer ? { 'id' => 6, 'login' => 'mickey' } : nil),
+ 'review_requester' => (with_reviewer ? { 'id' => 7, 'login' => 'minnie' } : nil),
'issue' => { 'number' => 2, 'pull_request' => pull_request },
'created_at' => '2022-04-26 18:30:53 UTC',
'performed_via_github_app' => nil
@@ -207,6 +236,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do
let(:with_rename) { true }
let(:with_milestone) { true }
let(:with_assignee) { true }
+ let(:with_reviewer) { true }
let(:pull_request) { nil }
let(:issue_event) { described_class.from_json_hash(hash) }
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index 27426763232..8ebbff31f64 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -67,6 +67,18 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
it_behaves_like 'user ID finder', :assignee
end
+
+ context 'when the author_key parameter is :requested_reviewer' do
+ let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', requested_reviewer: user) }
+
+ it_behaves_like 'user ID finder', :requested_reviewer
+ end
+
+ context 'when the author_key parameter is :review_requester' do
+ let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', review_requester: user) }
+
+ it_behaves_like 'user ID finder', :review_requester
+ end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 70fa942d8b8..cb5c43a5270 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1016,7 +1016,7 @@ RSpec.describe Ci::Runner do
let!(:last_update) { runner.ensure_runner_queue_value }
before do
- Ci::Runners::UpdateRunnerService.new(runner).update(description: 'new runner') # rubocop: disable Rails/SaveBang
+ Ci::Runners::UpdateRunnerService.new(runner).execute(description: 'new runner')
end
it 'sets a new last_update value' do
diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb
index 663df9712ab..289e1ce1856 100644
--- a/spec/models/user_status_spec.rb
+++ b/spec/models/user_status_spec.rb
@@ -18,6 +18,14 @@ RSpec.describe UserStatus do
expect { status.user.destroy! }.to change { described_class.count }.from(1).to(0)
end
+ describe '#clear_status_after' do
+ it 'is an alias of #clear_status_at', :freeze_time do
+ status = build(:user_status, clear_status_at: 8.hours.from_now)
+
+ expect(status.clear_status_after).to be_like_time(8.hours.from_now)
+ end
+ end
+
describe '#clear_status_after=' do
it 'sets clear_status_at' do
status = build(:user_status)
diff --git a/spec/rubocop/check_graceful_task_spec.rb b/spec/rubocop/check_graceful_task_spec.rb
new file mode 100644
index 00000000000..0364820a602
--- /dev/null
+++ b/spec/rubocop/check_graceful_task_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'stringio'
+
+require_relative '../support/helpers/next_instance_of'
+require_relative '../../rubocop/check_graceful_task'
+
+RSpec.describe RuboCop::CheckGracefulTask do
+ include NextInstanceOf
+
+ let(:output) { StringIO.new }
+
+ subject(:task) { described_class.new(output) }
+
+ describe '#run' do
+ let(:status_success) { RuboCop::CLI::STATUS_SUCCESS }
+ let(:status_offenses) { RuboCop::CLI::STATUS_OFFENSES }
+ let(:rubocop_status) { status_success }
+ let(:adjusted_rubocop_status) { rubocop_status }
+
+ subject { task.run(args) }
+
+ before do
+ # Don't notify Slack accidentally.
+ allow(Gitlab::Popen).to receive(:popen).and_raise('Notifications forbidden.')
+ stub_const('ENV', ENV.to_hash.delete_if { |key, _| key.start_with?('CI_') })
+
+ allow_next_instance_of(RuboCop::CLI) do |cli|
+ allow(cli).to receive(:run).and_return(rubocop_status)
+ end
+
+ allow(RuboCop::Formatter::GracefulFormatter)
+ .to receive(:adjusted_exit_status).and_return(adjusted_rubocop_status)
+ end
+
+ shared_examples 'rubocop scan' do |rubocop_args:|
+ it 'invokes a RuboCop scan' do
+ rubocop_options = %w[--parallel --format RuboCop::Formatter::GracefulFormatter]
+ rubocop_options.concat(rubocop_args)
+
+ expect_next_instance_of(RuboCop::CLI) do |cli|
+ expect(cli).to receive(:run).with(rubocop_options).and_return(rubocop_status)
+ end
+
+ subject
+
+ expect(output.string)
+ .to include('Running RuboCop in graceful mode:')
+ .and include("rubocop #{rubocop_options.join(' ')}")
+ .and include('This might take a while...')
+ end
+ end
+
+ context 'without args' do
+ let(:args) { [] }
+
+ it_behaves_like 'rubocop scan', rubocop_args: []
+
+ context 'with adjusted rubocop status' do
+ let(:rubocop_status) { status_offenses }
+ let(:adjusted_rubocop_status) { status_success }
+
+ context 'with sufficient environment variables' do
+ let(:channel) { 'f_rubocop' }
+
+ before do
+ env = {
+ 'CI_SLACK_WEBHOOK_URL' => 'webhook_url',
+ 'CI_JOB_NAME' => 'job_name',
+ 'CI_JOB_URL' => 'job_url'
+ }
+
+ stub_const('ENV', ENV.to_hash.update(env))
+ end
+
+ it 'notifies slack' do
+ popen_args = ['scripts/slack', channel, kind_of(String), 'rubocop', kind_of(String)]
+ popen_result = ['', 0]
+ expect(Gitlab::Popen).to receive(:popen).with(popen_args).and_return(popen_result)
+
+ subject
+
+ expect(output.string).to include("Notifying Slack ##{channel}.")
+ end
+
+ context 'with when notification fails' do
+ it 'prints that notification failed' do
+ popen_result = ['', 1]
+ expect(Gitlab::Popen).to receive(:popen).and_return(popen_result)
+
+ subject
+
+ expect(output.string).to include("Failed to notify Slack channel ##{channel}.")
+ end
+ end
+ end
+
+ context 'with missing environment variables' do
+ it 'skips slack notification' do
+ expect(Gitlab::Popen).not_to receive(:popen)
+
+ subject
+
+ expect(output.string).to include('Skipping Slack notification.')
+ end
+ end
+ end
+ end
+
+ context 'with args' do
+ let(:args) { %w[a.rb Lint/EmptyFile b.rb Lint/Syntax] }
+
+ it_behaves_like 'rubocop scan', rubocop_args: %w[--only Lint/EmptyFile,Lint/Syntax a.rb b.rb]
+
+ it 'does not notify slack' do
+ expect(Gitlab::Popen).not_to receive(:popen)
+
+ subject
+
+ expect(output.string).not_to include('Skipping Slack notification.')
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop_todo_spec.rb b/spec/rubocop/cop_todo_spec.rb
index 490f55a8bb6..3f9c378b303 100644
--- a/spec/rubocop/cop_todo_spec.rb
+++ b/spec/rubocop/cop_todo_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe RuboCop::CopTodo do
cop_name: cop_name,
files: be_empty,
offense_count: 0,
- previously_disabled: false
+ previously_disabled: false,
+ grace_period: false
)
end
end
@@ -102,6 +103,23 @@ RSpec.describe RuboCop::CopTodo do
end
end
+ context 'with grace period' do
+ specify do
+ cop_todo.record('a.rb', 1)
+ cop_todo.record('b.rb', 2)
+ cop_todo.grace_period = true
+
+ expect(yaml).to eq(<<~YAML)
+ ---
+ #{cop_name}:
+ Details: grace period
+ Exclude:
+ - 'a.rb'
+ - 'b.rb'
+ YAML
+ end
+ end
+
context 'with multiple files' do
before do
cop_todo.record('a.rb', 0)
diff --git a/spec/rubocop/formatter/graceful_formatter_spec.rb b/spec/rubocop/formatter/graceful_formatter_spec.rb
new file mode 100644
index 00000000000..0e0c1d52067
--- /dev/null
+++ b/spec/rubocop/formatter/graceful_formatter_spec.rb
@@ -0,0 +1,239 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require 'rubocop/rspec/shared_contexts'
+require 'stringio'
+
+require_relative '../../../rubocop/formatter/graceful_formatter'
+require_relative '../../../rubocop/todo_dir'
+
+RSpec.describe RuboCop::Formatter::GracefulFormatter, :isolated_environment do
+ # Set by :isolated_environment
+ let(:todo_dir) { RuboCop::TodoDir.new("#{Dir.pwd}/.rubocop_todo") }
+ let(:stdout) { StringIO.new }
+
+ subject(:formatter) { described_class.new(stdout) }
+
+ shared_examples 'summary reporting' do |inspected:, offenses: 0, silenced: 0|
+ it "reports summary with #{inspected} inspected, #{offenses} offenses, #{silenced} silenced" do
+ expect(stdout.string)
+ .to match(/Inspecting #{inspected} files/)
+ .and match(/#{inspected} files inspected/)
+
+ if offenses > 0
+ expect(stdout.string).to match(/Offenses:/)
+ expect(stdout.string).to match(/#{offenses} offenses detected/)
+ else
+ expect(stdout.string).not_to match(/Offenses:/)
+ expect(stdout.string).to match(/no offenses detected/)
+ end
+
+ if silenced > 0
+ expect(stdout.string).to match(/Silenced offenses:/)
+ expect(stdout.string).to match(/#{silenced} offenses silenced/)
+ else
+ expect(stdout.string).not_to match(/Silenced offenses:/)
+ expect(stdout.string).not_to match(/offenses silenced/)
+ end
+ end
+ end
+
+ context 'with offenses' do
+ let(:offense1) { fake_offense('Cop1') }
+ let(:offense2) { fake_offense('Cop2') }
+
+ before do
+ FileUtils.touch('.rubocop_todo.yml')
+
+ File.write('.rubocop.yml', <<~YAML)
+ inherit_from:
+ <% Dir.glob('.rubocop_todo/**/*.yml').each do |rubocop_todo_yaml| %>
+ - '<%= rubocop_todo_yaml %>'
+ <% end %>
+ - '.rubocop_todo.yml'
+
+ AllCops:
+ NewCops: enable # Avoiding RuboCop warnings
+ YAML
+
+ # These cops are unknown and would raise an validation error
+ allow(RuboCop::Cop::Registry.global).to receive(:contains_cop_matching?)
+ .and_return(true)
+ end
+
+ context 'with active only' do
+ before do
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [offense1])
+ formatter.file_finished('b.rb', [offense2])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2, offenses: 2
+ end
+
+ context 'with silenced only' do
+ before do
+ todo_dir.write('Cop1', <<~YAML)
+ ---
+ Cop1:
+ Details: grace period
+ YAML
+
+ File.write('.rubocop_todo.yml', <<~YAML)
+ ---
+ Cop2:
+ Details: grace period
+ YAML
+
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [offense1])
+ formatter.file_finished('b.rb', [offense2])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2, silenced: 2
+ end
+
+ context 'with active and silenced' do
+ before do
+ todo_dir.write('Cop1', <<~YAML)
+ ---
+ Cop1:
+ Details: grace period
+ YAML
+
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [offense1, offense2])
+ formatter.file_finished('b.rb', [offense2, offense1, offense1])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2, offenses: 2, silenced: 3
+ end
+ end
+
+ context 'without offenses' do
+ before do
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [])
+ formatter.file_finished('b.rb', [])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 2
+ end
+
+ context 'without files to inspect' do
+ before do
+ formatter.started([])
+ formatter.finished([])
+ end
+
+ it_behaves_like 'summary reporting', inspected: 0
+ end
+
+ context 'with missing @total_offense_count' do
+ it 'raises an error' do
+ formatter.started(%w[a.rb])
+
+ if formatter.instance_variable_defined?(:@total_offense_count)
+ formatter.remove_instance_variable(:@total_offense_count)
+ end
+
+ expect do
+ formatter.finished(%w[a.rb])
+ end.to raise_error(/RuboCop has changed its internals/)
+ end
+ end
+
+ describe '.adjusted_exit_status' do
+ using RSpec::Parameterized::TableSyntax
+
+ success = RuboCop::CLI::STATUS_SUCCESS
+ offenses = RuboCop::CLI::STATUS_OFFENSES
+ error = RuboCop::CLI::STATUS_ERROR
+
+ subject { described_class.adjusted_exit_status(status) }
+
+ where(:active_offenses, :status, :adjusted_status) do
+ 0 | success | success
+ 0 | offenses | success
+ 1 | offenses | offenses
+ 0 | error | error
+ 1 | error | error
+ # impossible cases
+ 1 | success | success
+ end
+
+ with_them do
+ around do |example|
+ described_class.active_offenses = active_offenses
+ example.run
+ ensure
+ described_class.active_offenses = 0
+ end
+
+ it { is_expected.to eq(adjusted_status) }
+ end
+ end
+
+ describe '.grace_period?' do
+ let(:cop_name) { 'Cop/Name' }
+
+ subject { described_class.grace_period?(cop_name, config) }
+
+ context 'with Details in config' do
+ let(:config) { { 'Details' => 'grace period' } }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with unknown value for Details in config' do
+ let(:config) { { 'Details' => 'unknown' } }
+
+ specify do
+ expect { is_expected.to eq(false) }
+ .to output(/#{cop_name}: Unhandled value "unknown" for `Details` key./)
+ .to_stderr
+ end
+ end
+
+ context 'with empty config' do
+ let(:config) { {} }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'without Details in config' do
+ let(:config) { { 'Exclude' => false } }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.grace_period_key_value' do
+ subject { described_class.grace_period_key_value }
+
+ it { is_expected.to eq('Details: grace period') }
+ end
+
+ def fake_offense(cop_name)
+ # rubocop:disable RSpec/VerifiedDoubles
+ double(:offense,
+ cop_name: cop_name,
+ corrected?: false,
+ correctable?: false,
+ severity: double(:severity, name: 'convention', code: :C),
+ line: 5,
+ column: 23,
+ real_column: 23,
+ corrected_with_todo?: false,
+ message: "#{cop_name} message",
+ location: double(:location, source_line: 'line', first_line: 1, last_line: 2),
+ highlighted_area: double(:highlighted_area, begin_pos: 1, size: 2)
+ )
+ # rubocop:enable RSpec/VerifiedDoubles
+ end
+end
diff --git a/spec/rubocop/formatter/todo_formatter_spec.rb b/spec/rubocop/formatter/todo_formatter_spec.rb
index 54b40e5b15f..edd84632409 100644
--- a/spec/rubocop/formatter/todo_formatter_spec.rb
+++ b/spec/rubocop/formatter/todo_formatter_spec.rb
@@ -176,6 +176,98 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
end
end
+ context 'with grace period' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ B/TooManyOffenses:
+ Details: grace period
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ shared_examples 'keeps grace period' do
+ it 'keeps Details: grace period' do
+ run_formatter
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Details: grace period
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'in rubocop_todo/' do
+ before do
+ todo_dir.write('B/TooManyOffenses', yaml)
+ todo_dir.inspect_all
+ end
+
+ it_behaves_like 'keeps grace period'
+ end
+
+ context 'in rubocop_todo.yml' do
+ before do
+ File.write('.rubocop_todo.yml', yaml)
+ end
+
+ it_behaves_like 'keeps grace period'
+ end
+
+ context 'with invalid details value' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ B/TooManyOffenses:
+ Details: something unknown
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ it 'ignores the details and warns' do
+ File.write('.rubocop_todo.yml', yaml)
+
+ expect { run_formatter }
+ .to output(%r{B/TooManyOffenses: Unhandled value "something unknown" for `Details` key.})
+ .to_stderr
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'and previously disabled' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ B/TooManyOffenses:
+ Enabled: false
+ Details: grace period
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ it 'raises an exception' do
+ File.write('.rubocop_todo.yml', yaml)
+
+ expect { run_formatter }
+ .to raise_error(RuntimeError, 'B/TooManyOffenses: Cop must be enabled to use `Details: grace period`.')
+ end
+ end
+ end
+
context 'with cop configuration in both .rubocop_todo/ and .rubocop_todo.yml' do
before do
todo_dir.write('B/TooManyOffenses', <<~YAML)
diff --git a/spec/services/ci/runners/update_runner_service_spec.rb b/spec/services/ci/runners/update_runner_service_spec.rb
index e008fde9982..1f953ac4cbb 100644
--- a/spec/services/ci/runners/update_runner_service_spec.rb
+++ b/spec/services/ci/runners/update_runner_service_spec.rb
@@ -2,69 +2,65 @@
require 'spec_helper'
-RSpec.describe Ci::Runners::UpdateRunnerService do
+RSpec.describe Ci::Runners::UpdateRunnerService, '#execute' do
+ subject(:execute) { described_class.new(runner).execute(params) }
+
let(:runner) { create(:ci_runner) }
- describe '#update' do
- before do
- allow(runner).to receive(:tick_runner_queue)
- end
+ before do
+ allow(runner).to receive(:tick_runner_queue)
+ end
- context 'with description params' do
- let(:params) { { description: 'new runner' } }
+ context 'with description params' do
+ let(:params) { { description: 'new runner' } }
- it 'updates the runner and ticking the queue' do
- expect(update).to be_truthy
+ it 'updates the runner and ticking the queue' do
+ expect(execute).to be_success
- runner.reload
+ runner.reload
- expect(runner).to have_received(:tick_runner_queue)
- expect(runner.description).to eq('new runner')
- end
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.description).to eq('new runner')
end
+ end
- context 'with paused param' do
- let(:params) { { paused: true } }
+ context 'with paused param' do
+ let(:params) { { paused: true } }
- it 'updates the runner and ticking the queue' do
- expect(runner.active).to be_truthy
- expect(update).to be_truthy
+ it 'updates the runner and ticking the queue' do
+ expect(runner.active).to be_truthy
+ expect(execute).to be_success
- runner.reload
+ runner.reload
- expect(runner).to have_received(:tick_runner_queue)
- expect(runner.active).to be_falsey
- end
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.active).to be_falsey
end
+ end
- context 'with cost factor params' do
- let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } }
+ context 'with cost factor params' do
+ let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } }
- it 'updates the runner cost factors' do
- expect(update).to be_truthy
+ it 'updates the runner cost factors' do
+ expect(execute).to be_success
- runner.reload
+ runner.reload
- expect(runner.public_projects_minutes_cost_factor).to eq(1.1)
- expect(runner.private_projects_minutes_cost_factor).to eq(2.2)
- end
+ expect(runner.public_projects_minutes_cost_factor).to eq(1.1)
+ expect(runner.private_projects_minutes_cost_factor).to eq(2.2)
end
+ end
- context 'when params are not valid' do
- let(:params) { { run_untagged: false } }
-
- it 'does not update and give false because it is not valid' do
- expect(update).to be_falsey
+ context 'when params are not valid' do
+ let(:params) { { run_untagged: false } }
- runner.reload
+ it 'does not update and returns error because it is not valid' do
+ expect(execute).to be_error
- expect(runner).not_to have_received(:tick_runner_queue)
- expect(runner.run_untagged).to be_truthy
- end
- end
+ runner.reload
- def update
- described_class.new(runner).update(params) # rubocop: disable Rails/SaveBang
+ expect(runner).not_to have_received(:tick_runner_queue)
+ expect(runner.run_untagged).to be_truthy
end
end
end
diff --git a/spec/support/matchers/abort_matcher.rb b/spec/support/matchers/abort_matcher.rb
index 64fed2ca069..140953cdc42 100644
--- a/spec/support/matchers/abort_matcher.rb
+++ b/spec/support/matchers/abort_matcher.rb
@@ -13,17 +13,16 @@ RSpec::Matchers.define :abort_execution do
captured = @captured_stderr.string.chomp
@actual_exit_code = e.status
break false unless e.status == 1
-
- if @message
- if @message.is_a? String
- @message == captured
- elsif @message.is_a? Regexp
- @message.match?(captured)
- else
- raise ArgumentError, 'with_message must be either a String or a Regular Expression'
- end
+ break true unless @message
+
+ case @message
+ when String
+ @message == captured
+ when Regexp
+ @message.match?(captured)
+ else
+ raise ArgumentError, 'with_message must be either a String or a Regular Expression'
end
-
ensure
$stderr = original_stderr
end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 2fece6bb4ed..a11b720d060 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative "rspec_order"
+require_relative "system_exit_detected"
require_relative "helpers/stub_configuration"
require_relative "helpers/stub_metrics"
require_relative "helpers/stub_object_storage"
diff --git a/spec/tasks/rubocop_rake_spec.rb b/spec/tasks/rubocop_rake_spec.rb
index 901d12df73a..eb360cdff93 100644
--- a/spec/tasks/rubocop_rake_spec.rb
+++ b/spec/tasks/rubocop_rake_spec.rb
@@ -9,11 +9,14 @@ require 'fileutils'
require_relative '../support/silence_stdout'
require_relative '../support/helpers/next_instance_of'
require_relative '../support/helpers/rake_helpers'
+require_relative '../support/matchers/abort_matcher'
require_relative '../../rubocop/formatter/todo_formatter'
require_relative '../../rubocop/todo_dir'
+require_relative '../../rubocop/check_graceful_task'
RSpec.describe 'rubocop rake tasks', :silence_stdout do
include RakeHelpers
+ include NextInstanceOf
before do
stub_const('Rails', double(:rails_env))
@@ -24,6 +27,41 @@ RSpec.describe 'rubocop rake tasks', :silence_stdout do
Rake.application.rake_require 'tasks/rubocop'
end
+ describe 'check:graceful' do
+ let(:options) { %w[file.rb Cop/Name] }
+
+ subject(:run_task) { run_rake_task('rubocop:check:graceful', *options) }
+
+ before do
+ allow_next_instance_of(RuboCop::CheckGracefulTask, $stdout) do |task|
+ allow(task).to receive(:run).with(options).and_return(task_result)
+ end
+ end
+
+ context 'with successful task result' do
+ let(:task_result) { 0 }
+
+ # We cannot use `abort_execution` because it's ignoring exit status `0`.
+ # Rely on SystemExitDetected here.
+ specify { run_task }
+
+ it 'modifies ENV and deletes REVEAL_RUBOCOP_TODO key' do
+ # There's ENV backup in before block.
+ ENV['REVEAL_RUBOCOP_TODO'] = '0' # rubocop:disable RSpec/EnvAssignment
+
+ run_task
+
+ expect(ENV.key?('REVEAL_RUBOCOP_TODO')).to eq(false)
+ end
+ end
+
+ context 'with non-successful task result' do
+ let(:task_result) { 1 }
+
+ specify { expect { run_task }.to abort_execution }
+ end
+ end
+
describe 'todo:generate', :aggregate_failures do
let(:tmp_dir) { Dir.mktmpdir }
let(:rubocop_todo_dir) { File.join(tmp_dir, '.rubocop_todo') }