diff options
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') } |