diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-01 09:09:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-01 09:09:29 +0300 |
commit | 2db9c1eee29dbd6e2592627bca55fa084f36d6d5 (patch) | |
tree | 8f71fe6e7abb62e6da66efaf773563109a4681f6 | |
parent | 4223ed2e83401f32f60d9de549d9c33cb376757a (diff) |
Add latest changes from gitlab-org/gitlab@master
53 files changed, 662 insertions, 354 deletions
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue index 6ce934dbaea..8282f1d910a 100644 --- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue +++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue @@ -68,6 +68,7 @@ export default { v-gl-modal.deploy-freeze-modal icon="pencil" data-testid="edit-deploy-freeze" + :aria-label="__('Edit deploy freeze')" @click="setFreezePeriod(item)" /> </template> diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue index 8abf1529f3c..b84fe45b77e 100644 --- a/app/assets/javascripts/design_management/components/toolbar/index.vue +++ b/app/assets/javascripts/design_management/components/toolbar/index.vue @@ -1,13 +1,16 @@ <script> import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql'; -import { __, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { DESIGNS_ROUTE_NAME } from '../../router/constants'; import DeleteButton from '../delete_button.vue'; import DesignNavigation from './design_navigation.vue'; export default { + i18n: { + downloadButtonLabel: s__('DesignManagement|Download design'), + }, components: { GlButton, GlIcon, @@ -119,7 +122,8 @@ export default { v-gl-tooltip.bottom :href="image" icon="download" - :title="s__('DesignManagement|Download design')" + :title="$options.i18n.downloadButtonLabel" + :aria-label="$options.i18n.downloadButtonLabel" /> <delete-button v-if="isLatestVersion && canDeleteDesign" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 1f50b3a38a6..27d9343ebf6 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -49,6 +49,7 @@ export default { mixins: [glFeatureFlagsMixin()], i18n: { ...DIFF_FILE_HEADER, + compareButtonLabel: s__('Compare submodule commit revisions'), }, props: { discussionPath: { @@ -192,6 +193,9 @@ export default { isReviewable() { return reviewable(this.diffFile); }, + externalUrlLabel() { + return sprintf(__('View on %{url}'), { url: this.diffFile.formatted_external_url }); + }, }, methods: { ...mapActions('diffs', [ @@ -352,7 +356,8 @@ export default { ref="externalLink" v-gl-tooltip.hover :href="diffFile.external_url" - :title="`View on ${diffFile.formatted_external_url}`" + :title="externalUrlLabel" + :aria-label="externalUrlLabel" target="_blank" data-track-event="click_toggle_external_button" data-track-label="diff_toggle_external_button" @@ -444,7 +449,8 @@ export default { v-gl-tooltip.hover v-safe-html="submoduleDiffCompareLinkText" class="submodule-compare" - :title="s__('Compare submodule commit revisions')" + :title="$options.i18n.compareButtonLabel" + :aria-label="$options.i18n.compareButtonLabel" :href="diffFile.submodule_compare.url" /> </div> diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue index 222815407ea..9220077af71 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue @@ -7,6 +7,8 @@ import { labelForStrategy } from '../utils'; export default { i18n: { + deleteLabel: __('Delete'), + editLabel: __('Edit'), toggleLabel: __('Feature flag status'), }, components: { @@ -215,19 +217,21 @@ export default { <div class="table-action-buttons btn-group"> <template v-if="featureFlag.edit_path"> <gl-button - v-gl-tooltip.hover.bottom="__('Edit')" + v-gl-tooltip.hover.bottom="$options.i18n.editLabel" class="js-feature-flag-edit-button" icon="pencil" + :aria-label="$options.i18n.editLabel" :href="featureFlag.edit_path" /> </template> <template v-if="featureFlag.destroy_path"> <gl-button - v-gl-tooltip.hover.bottom="__('Delete')" + v-gl-tooltip.hover.bottom="$options.i18n.deleteLabel" class="js-feature-flag-delete-button" variant="danger" icon="remove" :disabled="!canDeleteFlag(featureFlag)" + :aria-label="$options.i18n.deleteLabel" @click="setDeleteModalData(featureFlag)" /> </template> diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue index 7e3d3bc5203..67ddceaf080 100644 --- a/app/assets/javascripts/feature_flags/components/form.vue +++ b/app/assets/javascripts/feature_flags/components/form.vue @@ -30,6 +30,7 @@ import Strategy from './strategy.vue'; export default { i18n: { + removeLabel: s__('FeatureFlags|Remove'), statusLabel: s__('FeatureFlags|Status'), }, components: { @@ -507,7 +508,8 @@ export default { <gl-button v-if="!isAllEnvironment(scope.environmentScope) && canUpdateScope(scope)" v-gl-tooltip - :title="s__('FeatureFlags|Remove')" + :title="$options.i18n.removeLabel" + :aria-label="$options.i18n.removeLabel" class="js-delete-scope btn-transparent pr-3 pl-3" icon="clear" data-testid="feature-flag-delete" diff --git a/app/assets/javascripts/feature_flags/components/strategy.vue b/app/assets/javascripts/feature_flags/components/strategy.vue index 170f120b036..3f515dcdf18 100644 --- a/app/assets/javascripts/feature_flags/components/strategy.vue +++ b/app/assets/javascripts/feature_flags/components/strategy.vue @@ -165,6 +165,7 @@ export default { data-testid="delete-strategy-button" variant="danger" icon="remove" + :aria-label="__('Delete')" @click="$emit('delete')" /> </div> diff --git a/app/assets/javascripts/feature_flags/components/user_lists_table.vue b/app/assets/javascripts/feature_flags/components/user_lists_table.vue index 0bfd18f992c..a77aa663a63 100644 --- a/app/assets/javascripts/feature_flags/components/user_lists_table.vue +++ b/app/assets/javascripts/feature_flags/components/user_lists_table.vue @@ -100,6 +100,7 @@ export default { category="secondary" variant="danger" icon="remove" + :aria-label="$options.modal.actionPrimary.text" data-testid="delete-user-list" @click="confirmDeleteList(list)" /> diff --git a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue index bbf4160ce35..14474d44e10 100644 --- a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue +++ b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue @@ -13,6 +13,10 @@ import CsvExportModal from './csv_export_modal.vue'; import CsvImportModal from './csv_import_modal.vue'; export default { + i18n: { + exportAsCsvButtonText: __('Export as CSV'), + importIssuesText: __('Import issues'), + }, name: 'CsvImportExportButtons', components: { GlButtonGroup, @@ -57,16 +61,15 @@ export default { return `${this.issuableType}-import-modal`; }, importButtonText() { - return this.showLabel ? this.$options.importIssuesText : null; + return this.showLabel ? this.$options.i18n.importIssuesText : null; }, importButtonTooltipText() { - return this.showLabel ? null : this.$options.importIssuesText; + return this.showLabel ? null : this.$options.i18n.importIssuesText; }, importButtonIcon() { return this.showLabel ? null : 'import'; }, }, - importIssuesText: __('Import issues'), }; </script> @@ -75,9 +78,10 @@ export default { <gl-button-group> <gl-button v-if="showExportButton" - v-gl-tooltip.hover="__('Export as CSV')" + v-gl-tooltip.hover="$options.i18n.exportAsCsvButtonText" v-gl-modal="exportModalId" icon="export" + :aria-label="$options.i18n.exportAsCsvButtonText" data-qa-selector="export_as_csv_button" data-testid="export-csv-button" /> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index ce4a85b35b7..ea50a11bed6 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,9 +1,15 @@ <script> import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { __, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; export default { + i18n: { + eraseLogButtonLabel: s__('Job|Erase job log'), + scrollToBottomButtonLabel: s__('Job|Scroll to bottom'), + scrollToTopButtonLabel: s__('Job|Scroll to top'), + showRawButtonLabel: s__('Job|Show complete raw'), + }, components: { GlLink, GlButton, @@ -82,7 +88,8 @@ export default { <gl-button v-if="rawPath" v-gl-tooltip.body - :title="s__('Job|Show complete raw')" + :title="$options.i18n.showRawButtonLabel" + :aria-label="$options.i18n.showRawButtonLabel" :href="rawPath" data-testid="job-raw-link-controller" icon="doc-text" @@ -91,7 +98,8 @@ export default { <gl-button v-if="erasePath" v-gl-tooltip.body - :title="s__('Job|Erase job log')" + :title="$options.i18n.eraseLogButtonLabel" + :aria-label="$options.i18n.eraseLogButtonLabel" :href="erasePath" :data-confirm="__('Are you sure you want to erase this build?')" class="gl-ml-3" @@ -102,23 +110,25 @@ export default { <!-- eo links --> <!-- scroll buttons --> - <div v-gl-tooltip :title="s__('Job|Scroll to top')" class="gl-ml-3"> + <div v-gl-tooltip :title="$options.i18n.scrollToTopButtonLabel" class="gl-ml-3"> <gl-button :disabled="isScrollTopDisabled" class="btn-scroll" data-testid="job-controller-scroll-top" icon="scroll_up" + :aria-label="$options.i18n.scrollToTopButtonLabel" @click="handleScrollToTop" /> </div> - <div v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="gl-ml-3"> + <div v-gl-tooltip :title="$options.i18n.scrollToBottomButtonLabel" class="gl-ml-3"> <gl-button :disabled="isScrollBottomDisabled" class="js-scroll-bottom btn-scroll" data-testid="job-controller-scroll-bottom" icon="scroll_down" :class="{ animate: isScrollingDown }" + :aria-label="$options.i18n.scrollToBottomButtonLabel" @click="handleScrollToBottom" /> </div> diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue index 847339e814a..e5f0206bb8b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue @@ -10,6 +10,7 @@ import { GlTooltipDirective, } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; +import { s__ } from '~/locale'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import { timeRanges } from '~/vue_shared/constants'; import DashboardPanel from './dashboard_panel.vue'; @@ -24,6 +25,9 @@ metrics: `; export default { + i18n: { + refreshButtonLabel: s__('Metrics|Refresh Prometheus data'), + }, components: { GlCard, GlForm, @@ -191,7 +195,8 @@ export default { v-gl-tooltip data-testid="previewRefreshButton" icon="retry" - :title="s__('Metrics|Refresh Prometheus data')" + :title="$options.i18n.refreshButtonLabel" + :aria-label="$options.i18n.refreshButtonLabel" @click="onRefresh" /> <dashboard-panel :graph-data="panelPreviewGraphData" /> diff --git a/app/assets/javascripts/projects/commit/components/commit_comments_button.vue b/app/assets/javascripts/projects/commit/components/commit_comments_button.vue new file mode 100644 index 00000000000..67b5e1e512c --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/commit_comments_button.vue @@ -0,0 +1,42 @@ +<script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { n__ } from '~/locale'; + +export default { + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlButton, + }, + props: { + commentsCount: { + type: Number, + required: true, + }, + }, + computed: { + tooltipText() { + return n__('%d comment on this commit', '%d comments on this commit', this.commentsCount); + }, + showCommentButton() { + return this.commentsCount > 0; + }, + }, +}; +</script> + +<template> + <span + v-if="showCommentButton" + v-gl-tooltip + class="gl-display-none gl-sm-display-inline-block" + tabindex="0" + :title="tooltipText" + data-testid="comment-button-wrapper" + > + <gl-button icon="comment" class="gl-mr-3" disabled> + {{ commentsCount }} + </gl-button> + </span> +</template> diff --git a/app/assets/javascripts/projects/commit/index.js b/app/assets/javascripts/projects/commit/index.js index 4eb51d566c5..d8d30c4332c 100644 --- a/app/assets/javascripts/projects/commit/index.js +++ b/app/assets/javascripts/projects/commit/index.js @@ -1,9 +1,11 @@ import initCherryPickCommitModal from './init_cherry_pick_commit_modal'; +import initCommitCommentsButton from './init_commit_comments_button'; import initCommitOptionsDropdown from './init_commit_options_dropdown'; import initRevertCommitModal from './init_revert_commit_modal'; export default () => { initRevertCommitModal(); initCherryPickCommitModal(); + initCommitCommentsButton(); initCommitOptionsDropdown(); }; diff --git a/app/assets/javascripts/projects/commit/init_commit_comments_button.js b/app/assets/javascripts/projects/commit/init_commit_comments_button.js new file mode 100644 index 00000000000..d70f7cb65f3 --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_commit_comments_button.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import CommitCommentsButton from './components/commit_comments_button.vue'; + +export default function initCommitCommentsButton() { + const el = document.querySelector('#js-commit-comments-button'); + + if (!el) { + return false; + } + + const { commentsCount } = el.dataset; + + return new Vue({ + el, + render: (createElement) => + createElement(CommitCommentsButton, { props: { commentsCount: Number(commentsCount) } }), + }); +} diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 50972a8c32c..149909d263e 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -28,6 +28,7 @@ import { import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; +import { __ } from '~/locale'; import initMRPopovers from '~/mr_popover/'; import noteHeader from '~/notes/components/note_header.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -37,6 +38,9 @@ import TimelineEntryItem from './timeline_entry_item.vue'; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; export default { + i18n: { + deleteButtonLabel: __('Remove description history'), + }, name: 'SystemNote', components: { GlIcon, @@ -139,7 +143,8 @@ export default { <gl-button v-if="displayDeleteButton" v-gl-tooltip - :title="__('Remove description history')" + :title="$options.i18n.deleteButtonLabel" + :aria-label="$options.i18n.deleteButtonLabel" variant="default" category="tertiary" icon="remove" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index 1d3bd312b09..320e2048f1c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -164,6 +164,7 @@ export default { variant="link" icon="close" class="gl-mr-2 gl-w-auto! gl-p-2!" + :aria-label="__('Close')" @click.prevent="handleDropdownCloseClick" /> </div> diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index ef7e3efdc68..ae4b0807fc5 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -5,32 +5,29 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues") -- if group_issues_count(state: 'all') == 0 - = render 'shared/empty_states/issues', project_select_button: true -- else - .top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls - = render 'shared/issuable/feed_buttons' +.top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + = render 'shared/issuable/feed_buttons' - - if @can_bulk_update - = render_if_exists 'shared/issuable/bulk_update_button', type: :issues + - if @can_bulk_update + = render_if_exists 'shared/issuable/bulk_update_button', type: :issues - = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true - = render 'shared/issuable/search_bar', type: :issues += render 'shared/issuable/search_bar', type: :issues - - if @can_bulk_update - = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues +- if @can_bulk_update + = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues - - if Feature.enabled?(:vue_issuables_list, @group) - - if use_startup_call? - - add_page_startup_api_call(api_v4_groups_issues_path(id: @group.id, params: startup_call_params)) - .js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)), - 'can-bulk-edit': @can_bulk_update.to_json, - 'empty-state-meta': { svg_path: image_path('illustrations/issues.svg') }, - 'sort-key': @sort, - type: 'issues', - 'scoped-labels-available': scoped_labels_available?(@group).to_json } } - - else - = render 'shared/issues' +- if Feature.enabled?(:vue_issuables_list, @group) && @issues.to_a.any? + - if use_startup_call? + - add_page_startup_api_call(api_v4_groups_issues_path(id: @group.id, params: startup_call_params)) + .js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)), + 'can-bulk-edit': @can_bulk_update.to_json, + 'empty-state-meta': { svg_path: image_path('illustrations/issues.svg') }, + 'sort-key': @sort, + type: 'issues', + 'scoped-labels-available': scoped_labels_available?(@group).to_json } } +- else + = render 'shared/issues', project_select_button: true diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2ab8af2ce80..1b28136e82c 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -18,10 +18,7 @@ = commit_committer_link(@commit, avatar: true, size: 24) #{time_ago_with_tooltip(@commit.committed_date)} - - if defined?(@notes_count) && @notes_count > 0 - %span.btn.gl-button.btn-default.disabled.gl-button.btn-icon.d-none.d-sm-inline.gl-mr-3.has-tooltip{ title: n_("%d comment on this commit", "%d comments on this commit", @notes_count) % @notes_count } - = sprite_icon('comment') - = @notes_count + #js-commit-comments-button{ data: { comments_count: @notes_count.to_i } } = link_to _('Browse files'), project_tree_path(@project, @commit), class: "btn gl-button btn-default gl-mr-3 gl-xs-w-full gl-xs-mb-3" #js-commit-options-dropdown{ data: commit_options_dropdown_data(@project, @commit) } diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 57575f89803..eb12e9d463c 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -3,4 +3,5 @@ = render partial: 'projects/issues/issue', collection: @issues = paginate @issues, theme: "gitlab" - else - = render 'shared/empty_states/issues' + - project_select_button = local_assigns.fetch(:project_select_button, false) + = render 'shared/empty_states/issues', project_select_button: project_select_button diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index f52bf1551f4..f99aea61c17 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -50,16 +50,16 @@ .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end - if @project # if in milestones list on project level - if can_admin_group_milestones? - %button.js-promote-project-milestone-button.btn.gl-button.btn-default-tertiary.btn-sm.btn-grouped.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'), + %button.js-promote-project-milestone-button.btn.gl-button.btn-icon.btn-default-tertiary.btn-sm.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'), disabled: true, type: 'button', data: { url: promote_project_milestone_path(milestone.project, milestone), milestone_title: milestone.title, group_name: @project.group.name } } - = sprite_icon('level-up', size: 14) + = sprite_icon('level-up', size: 14, css_class: 'gl-button-icon gl-icon') - if can?(current_user, :admin_milestone, milestone) - if milestone.closed? - = link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn gl-button btn-sm btn-grouped" + = link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn gl-button btn-sm gl-ml-3" - else - = link_to s_('Milestones|Close Milestone'), milestone_path(milestone, milestone: { state_event: :close }), method: :put, class: "btn gl-button btn-warning-secondary btn-sm btn-grouped btn-close" + = link_to s_('Milestones|Close Milestone'), milestone_path(milestone, milestone: { state_event: :close }), method: :put, class: "btn gl-button btn-default btn-default-secondary btn-sm gl-ml-3" diff --git a/changelogs/unreleased/324803-covert-has-tooltip-on-commit-page-to-pajamas.yml b/changelogs/unreleased/324803-covert-has-tooltip-on-commit-page-to-pajamas.yml new file mode 100644 index 00000000000..8f7747d2784 --- /dev/null +++ b/changelogs/unreleased/324803-covert-has-tooltip-on-commit-page-to-pajamas.yml @@ -0,0 +1,5 @@ +--- +title: Covert has-tooltip on commit page to pajamas +merge_request: 57858 +author: +type: fixed diff --git a/changelogs/unreleased/cngo-add-aria-labels-to-icon-only-buttons.yml b/changelogs/unreleased/cngo-add-aria-labels-to-icon-only-buttons.yml new file mode 100644 index 00000000000..98d9b9fbd2d --- /dev/null +++ b/changelogs/unreleased/cngo-add-aria-labels-to-icon-only-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Add aria labels to icon-only buttons +merge_request: 57610 +author: +type: fixed diff --git a/changelogs/unreleased/milestone-list-deprecated-button.yml b/changelogs/unreleased/milestone-list-deprecated-button.yml new file mode 100644 index 00000000000..634f35ab0f8 --- /dev/null +++ b/changelogs/unreleased/milestone-list-deprecated-button.yml @@ -0,0 +1,5 @@ +--- +title: Replace deprecated Close Milestone button on list view +merge_request: 57871 +author: +type: changed diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md index 5f84e1cc44a..3e1518447d6 100644 --- a/doc/ci/variables/where_variables_can_be_used.md +++ b/doc/ci/variables/where_variables_can_be_used.md @@ -35,7 +35,7 @@ There are two places defined variables can be used. On the: | `cache:key` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `artifacts:name` | yes | Runner | The variable expansion is made by GitLab Runner's shell environment | | `script`, `before_script`, `after_script` | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) | -| `only:variables:[]`, `except:variables:[]` | no | n/a | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | +| `only:variables:[]`, `except:variables:[]`, `rules:if` | no | n/a | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | ### `config.toml` file diff --git a/doc/development/fe_guide/design_anti_patterns.md b/doc/development/fe_guide/design_anti_patterns.md index d230e413879..ee4fceff927 100644 --- a/doc/development/fe_guide/design_anti_patterns.md +++ b/doc/development/fe_guide/design_anti_patterns.md @@ -30,7 +30,7 @@ const createStore = () => new Vuex.Store({ // Notice that we are forcing all references to this module to use the same single instance of the store. // We are also creating the store at import-time and there is nothing which can automatically dispose of it. // -// As an alternative, we should simply export the `createStore` and let the client manage the +// As an alternative, we should export the `createStore` and let the client manage the // lifecycle and instance of the store. export default createStore(); ``` @@ -129,7 +129,7 @@ Here are some ills that Singletons often produce: This is because of the limitations of languages like Java where everything has to be wrapped in a class. In JavaScript we have things like object and function literals where we can solve -many problems with a module that simply exports utility functions. +many problems with a module that exports utility functions. ### When could the Singleton pattern be actually appropriate?** diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index d2a2e7ae463..8c1420a3630 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -254,7 +254,7 @@ end **Creating a new table with a foreign key:** -We can simply wrap the `create_table` method with `with_lock_retries`: +We can wrap the `create_table` method with `with_lock_retries`: ```ruby def up @@ -715,7 +715,7 @@ the `DROP TABLE` statement is likely to stall concurrent traffic until it fails Table **has no records** (feature was never in use) and **no foreign keys**: -- Simply use the `drop_table` method in your migration. +- Use the `drop_table` method in your migration. ```ruby def change diff --git a/doc/development/polymorphic_associations.md b/doc/development/polymorphic_associations.md index cabd2e3fb41..b71e66c8671 100644 --- a/doc/development/polymorphic_associations.md +++ b/doc/development/polymorphic_associations.md @@ -74,7 +74,7 @@ different columns set) in the same table. ## The Solution -Fortunately there is a very simple solution to these problems: simply use a +Fortunately there is a very simple solution to these problems: use a separate table for every type you would otherwise store in the same table. Using a separate table allows you to use everything a database may provide to ensure consistency and query data efficiently, without any additional application logic diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index f28828c2e4e..6f56e60f619 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -223,4 +223,4 @@ When importing, GitLab would execute the following command, passing the `import_ git clone file://git:/tmp/lol ``` -Git would simply ignore the `git:` part, interpret the path as `file:///tmp/lol`, and import the repository into the new project. This action could potentially give the attacker access to any repository in the system, whether private or not. +Git ignores the `git:` part, interpret the path as `file:///tmp/lol`, and imports the repository into the new project. This action could potentially give the attacker access to any repository in the system, whether private or not. diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index cff199c8b1d..151194e87c1 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -551,7 +551,7 @@ does not account for weights. As we are [moving towards using `sidekiq-cluster` in Free](https://gitlab.com/gitlab-org/gitlab/-/issues/34396), newly-added -workers do not need to have weights specified. They can simply use the +workers do not need to have weights specified. They can use the default weight, which is 1. ## Worker context diff --git a/doc/development/sql.md b/doc/development/sql.md index 1053abf5c02..a98645cfcae 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -313,9 +313,9 @@ Project.from("(#{union.to_sql}) projects") ## Ordering by Creation Date -When ordering records based on the time they were created you can simply order +When ordering records based on the time they were created, you can order by the `id` column instead of ordering by `created_at`. Because IDs are always -unique and incremented in the order that rows are created this will produce the +unique and incremented in the order that rows are created, doing so will produce the exact same results. This also means there's no need to add an index on `created_at` to ensure consistent performance as `id` is already indexed by default. diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md index 6ac2f8dfa7b..c03b744a908 100644 --- a/doc/development/testing_guide/end_to_end/index.md +++ b/doc/development/testing_guide/end_to_end/index.md @@ -159,9 +159,9 @@ See [Review Apps](../review_apps.md) for more details about Review Apps. ## How do I run the tests? If you are not [testing code in a merge request](#testing-code-in-merge-requests), -there are two main options for running the tests. If you simply want to run -the existing tests against a live GitLab instance or against a pre-built Docker image -you can use the [GitLab QA orchestrator](https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md). See also [examples +there are two main options for running the tests. If you want to run +the existing tests against a live GitLab instance or against a pre-built Docker image, +use the [GitLab QA orchestrator](https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md). See also [examples of the test scenarios you can run via the orchestrator](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#examples). On the other hand, if you would like to run against a local development GitLab diff --git a/doc/development/testing_guide/end_to_end/style_guide.md b/doc/development/testing_guide/end_to_end/style_guide.md index f9c13d5dd67..22ddae6e836 100644 --- a/doc/development/testing_guide/end_to_end/style_guide.md +++ b/doc/development/testing_guide/end_to_end/style_guide.md @@ -122,7 +122,7 @@ avoid confusion or make the code more readable. For example, if a page object is named `New`, it could be confusing to name the block argument `new` because that is used to instantiate objects, so `new_page` would be acceptable. -We chose not to simply use `page` because that would shadow the +We chose not to use `page` because that would shadow the Capybara DSL, potentially leading to confusion and bugs. ### Examples diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 5a3144f76f1..78f8e320269 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -755,7 +755,7 @@ If a manual mock is needed for a CE module, place it in `spec/frontend/mocks/ce` any behavior, only provides a nice es6 compatible wrapper. - [`__mocks__/monaco-editor/index.js`](https://gitlab.com/gitlab-org/gitlab/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js) - This mock is helpful because the Monaco package is completely incompatible in a Jest environment. In fact, webpack requires a special loader to make it work. This mock - simply makes this package consumable by Jest. + makes this package consumable by Jest. ### Keep mocks light diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md index 3f596e89e29..e0176c190d6 100644 --- a/doc/development/understanding_explain_plans.md +++ b/doc/development/understanding_explain_plans.md @@ -280,7 +280,7 @@ FROM users WHERE twitter != ''; ``` -This query simply counts the number of users that have a Twitter profile set. +This query counts the number of users that have a Twitter profile set. Let's run this using `EXPLAIN (ANALYZE, BUFFERS)`: ```sql @@ -388,7 +388,7 @@ we created the index: CREATE INDEX CONCURRENTLY twitter_test ON users (twitter); ``` -We simply told PostgreSQL to index all possible values of the `twitter` column, +We told PostgreSQL to index all possible values of the `twitter` column, even empty strings. Our query in turn uses `WHERE twitter != ''`. This means that the index does improve things, as we don't need to do a sequential scan, but we may still encounter empty strings. This means PostgreSQL _has_ to apply a diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index e126abccf62..a6c3afceeea 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -679,7 +679,7 @@ Sidekiq processes](../administration/operations/extra_sidekiq_processes.md). Whenever a change or deletion is made to an indexed GitLab object (a merge request description is changed, a file is deleted from the master branch in a repository, a project is deleted, etc), a document in the index is deleted. However, since these are "soft" deletes, the overall number of "deleted documents", and therefore wasted space, increases. Elasticsearch does intelligent merging of segments in order to remove these deleted documents. However, depending on the amount and type of activity in your GitLab installation, it's possible to see as much as 50% wasted space in the index. -In general, we recommend simply letting Elasticsearch merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene's defaults as-is and not fret too much about when deletes are reclaimed."_ +In general, we recommend letting Elasticsearch merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene's defaults as-is and not fret too much about when deletes are reclaimed."_ However, some larger installations may wish to tune the merge policy settings: @@ -719,8 +719,7 @@ data). The use of Elasticsearch in GitLab is only ever as a secondary data store. This means that all of the data stored in Elasticsearch can always be derived again from other data sources, specifically PostgreSQL and Gitaly. Therefore, if -the Elasticsearch data store is ever corrupted for whatever reason, you can -simply reindex everything from scratch. +the Elasticsearch data store is ever corrupted for whatever reason, you can reindex everything from scratch. ## Troubleshooting @@ -916,7 +915,7 @@ In GitLab 13.9, a change was made where [binary file names are being indexed](ht ### Last resort to recreate an index There may be cases where somehow data never got indexed and it's not in the -queue, or the index is somehow in a state where migrations just simply cannot +queue, or the index is somehow in a state where migrations just cannot proceed. It is always best to try to troubleshoot the root cause of the problem using the above [troubleshooting](#troubleshooting) steps. diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md index a6173548042..18e5eaeef43 100644 --- a/doc/operations/error_tracking.md +++ b/doc/operations/error_tracking.md @@ -77,7 +77,7 @@ You can take action on Sentry Errors from within the GitLab UI. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39665) in GitLab 12.7. -From within the [Error Details](#error-details) page you can ignore a Sentry error by simply clicking the **Ignore** button near the top of the page. +From within the [Error Details](#error-details) page you can ignore a Sentry error by clicking the **Ignore** button near the top of the page. Ignoring an error prevents it from appearing in the [Error Tracking List](#error-tracking-list), and silences notifications that were set up within Sentry. diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md index 76fc9bc92b0..eba471882f1 100644 --- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md +++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md @@ -379,7 +379,7 @@ it also provides a clear timeline and development structure. ![Use revert to keep branch flowing](img/revert.png) -If you want to revert changes introduced in certain `commit-id` you can simply +If you want to revert changes introduced in certain `commit-id`, you can revert that `commit-id` (swap additions and deletions) in newly created commit: You can do this with diff --git a/doc/user/admin_area/activating_deactivating_users.md b/doc/user/admin_area/activating_deactivating_users.md index 1bca1751d2e..144ee2dbf98 100644 --- a/doc/user/admin_area/activating_deactivating_users.md +++ b/doc/user/admin_area/activating_deactivating_users.md @@ -66,4 +66,4 @@ Activating a user changes the user's state to active and consumes a [seat](../../subscriptions/self_managed/index.md#billable-users). NOTE: -A deactivated user can also activate their account themselves by simply logging back in via the UI. +A deactivated user can also activate their account themselves by logging back in via the UI. diff --git a/doc/user/application_security/api_fuzzing/index.md b/doc/user/application_security/api_fuzzing/index.md index 3675c36c5be..39aa6464a1e 100644 --- a/doc/user/application_security/api_fuzzing/index.md +++ b/doc/user/application_security/api_fuzzing/index.md @@ -994,7 +994,7 @@ False positives can be handled in two ways: Checks perform testing of a specific type and can be turned on and off for specific configuration profiles. The provided [configuration files](#configuration-files) define several profiles that you can use. The profile definition in the configuration file lists all the checks that are active -during a scan. To turn off a specific check, simply remove it from the profile definition in the +during a scan. To turn off a specific check, remove it from the profile definition in the configuration file. The profiles are defined in the `Profiles` section of the configuration file. Example profile definition: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ea91a3b6f55..72f5c58d8ed 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33715,6 +33715,9 @@ msgstr "" msgid "View merge request" msgstr "" +msgid "View on %{url}" +msgstr "" + msgid "View open merge request" msgstr "" @@ -578,7 +578,9 @@ module QA autoload :LoopRunner, 'qa/specs/loop_runner' module Helpers + autoload :ContextSelector, 'qa/specs/helpers/context_selector' autoload :Quarantine, 'qa/specs/helpers/quarantine' + autoload :RSpec, 'qa/specs/helpers/rspec' end end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 7aa45204513..0e384a99fcf 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -2,6 +2,7 @@ require 'gitlab/qa' require 'uri' +require 'active_support/core_ext/object/blank' module QA module Runtime @@ -24,48 +25,6 @@ module QA SUPPORTED_FEATURES end - def context_matches?(*options) - return false unless Runtime::Scenario.attributes[:gitlab_address] - - opts = {} - opts[:domain] = '.+' - opts[:tld] = '.com' - - uri = URI(Runtime::Scenario.gitlab_address) - - options.each do |option| - opts[:domain] = 'gitlab' if option == :production - - if option.is_a?(Hash) && !option[:pipeline].nil? && !ci_project_name.nil? - return pipeline_matches?(option[:pipeline]) - - elsif option.is_a?(Hash) && !option[:subdomain].nil? - opts.merge!(option) - - opts[:subdomain] = case option[:subdomain] - when Array - "(#{option[:subdomain].join("|")})." - when Regexp - option[:subdomain] - else - "(#{option[:subdomain]})." - end - end - end - - uri.host.match?(/^#{opts[:subdomain]}#{opts[:domain]}#{opts[:tld]}$/) - end - - alias_method :dot_com?, :context_matches? - - def pipeline_matches?(pipeline_to_run_in) - Array(pipeline_to_run_in).any? { |pipeline| pipeline.to_s.casecmp?(pipeline_from_project_name) } - end - - def pipeline_from_project_name - ci_project_name.to_s.start_with?('gitlab-qa') ? Runtime::Env.default_branch : ci_project_name - end - def additional_repository_storage ENV['QA_ADDITIONAL_REPOSITORY_STORAGE'] end @@ -82,6 +41,10 @@ module QA ENV['CI_JOB_URL'] end + def ci_job_name + ENV['CI_JOB_NAME'] + end + def ci_project_name ENV['CI_PROJECT_NAME'] end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 91495247906..59effc4c184 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -181,7 +181,7 @@ module QA end def verify_storage_move(source_storage, destination_storage, repo_type: :project) - return if QA::Runtime::Env.dot_com? + return if Specs::Helpers::ContextSelector.dot_com? repo_path = verify_storage_move_from_gitaly(source_storage[:name], repo_type: repo_type) diff --git a/qa/qa/specs/helpers/context_selector.rb b/qa/qa/specs/helpers/context_selector.rb new file mode 100644 index 00000000000..4313f7c34dd --- /dev/null +++ b/qa/qa/specs/helpers/context_selector.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'rspec/core' + +module QA + module Specs + module Helpers + module ContextSelector + extend self + + def configure_rspec + ::RSpec.configure do |config| + config.before do |example| + if example.metadata.key?(:only) + skip('Test is not compatible with this environment or pipeline') unless ContextSelector.context_matches?(example.metadata[:only]) + elsif example.metadata.key?(:exclude) + skip('Test is excluded in this job') if ContextSelector.exclude?(example.metadata[:exclude]) + end + end + end + end + + def exclude?(*options) + return false unless Runtime::Env.ci_job_name.present? + + context_matches?(*options) + end + + def context_matches?(*options) + return false unless Runtime::Scenario.attributes[:gitlab_address] + + opts = {} + opts[:domain] = '.+' + opts[:tld] = '.com' + + uri = URI(Runtime::Scenario.gitlab_address) + + options.each do |option| + opts[:domain] = 'gitlab' if option == :production + + next unless option.is_a?(Hash) + + if option[:pipeline].present? && Runtime::Env.ci_project_name.present? + return pipeline_matches?(option[:pipeline]) + + elsif option[:job].present? + return job_matches?(option[:job]) + + elsif option[:subdomain].present? + opts.merge!(option) + + opts[:subdomain] = case option[:subdomain] + when Array + "(#{option[:subdomain].join("|")})." + when Regexp + option[:subdomain] + else + "(#{option[:subdomain]})." + end + end + end + + uri.host.match?(/^#{opts[:subdomain]}#{opts[:domain]}#{opts[:tld]}$/) + end + + alias_method :dot_com?, :context_matches? + + def job_matches?(job_patterns) + Array(job_patterns).any? do |job| + pattern = job.is_a?(Regexp) ? job : Regexp.new(job) + pattern = Regexp.new(pattern.source, pattern.options | Regexp::IGNORECASE) + pattern =~ Runtime::Env.ci_job_name + end + end + + def pipeline_matches?(pipeline_to_run_in) + Array(pipeline_to_run_in).any? { |pipeline| pipeline.to_s.casecmp?(pipeline_from_project_name(Runtime::Env.ci_project_name)) } + end + + def pipeline_from_project_name(project_name) + project_name.to_s.start_with?('gitlab-qa') ? Runtime::Env.default_branch : project_name + end + end + end + end +end diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb index ee318ac498d..15b4ed8336b 100644 --- a/qa/qa/specs/helpers/quarantine.rb +++ b/qa/qa/specs/helpers/quarantine.rb @@ -6,22 +6,18 @@ module QA module Specs module Helpers module Quarantine - include RSpec::Core::Pending + include ::RSpec::Core::Pending extend self def configure_rspec - RSpec.configure do |config| + ::RSpec.configure do |config| config.before(:context, :quarantine) do Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class) end config.before do |example| Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example) - - if example.metadata.key?(:only) - skip('Test is not compatible with this environment or pipeline') unless Runtime::Env.context_matches?(example.metadata[:only]) - end end end end @@ -55,7 +51,7 @@ module QA if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only) # If the :quarantine hash contains :only, we respect that. # For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging. - return unless Runtime::Env.context_matches?(quarantine_tag[:only]) + return unless ContextSelector.context_matches?(quarantine_tag[:only]) end skip(quarantine_message(quarantine_tag)) diff --git a/qa/qa/specs/helpers/rspec.rb b/qa/qa/specs/helpers/rspec.rb new file mode 100644 index 00000000000..f49e556b0d9 --- /dev/null +++ b/qa/qa/specs/helpers/rspec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rspec/core' + +module QA + module Specs + module Helpers + module RSpec + # We need a reporter for internal tests that's different from the reporter for + # external tests otherwise the results will be mixed up. We don't care about + # most reporting, but we do want to know if a test fails + class RaiseOnFailuresReporter < ::RSpec::Core::NullReporter + def self.example_failed(example) + raise example.exception + end + end + + # We use an example group wrapper to prevent the state of internal tests + # expanding into the global state + # See: https://github.com/rspec/rspec-core/issues/2603 + def describe_successfully(*args, &describe_body) + example_group = RSpec.describe(*args, &describe_body) + ran_successfully = example_group.run RaiseOnFailuresReporter + expect(ran_successfully).to eq true + example_group + end + end + end + end +end diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index 04eaa02bda5..ff690962db8 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -44,7 +44,7 @@ module QA tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled? - tags_for_rspec.push(%w[--tag ~skip_live_env]) if QA::Runtime::Env.dot_com? + tags_for_rspec.push(%w[--tag ~skip_live_env]) if QA::Specs::Helpers::ContextSelector.dot_com? QA::Runtime::Env.supported_features.each_key do |key| tags_for_rspec.push(%W[--tag ~requires_#{key}]) unless QA::Runtime::Env.can_test? key diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 5a98721466f..8218ab428b0 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -341,56 +341,4 @@ RSpec.describe QA::Runtime::Env do end end end - - describe '.context_matches?' do - it 'returns true when url has .com' do - QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") - - expect(described_class.dot_com?).to be_truthy - end - - it 'returns false when url does not have .com' do - QA::Runtime::Scenario.define(:gitlab_address, "https://gitlab.test") - - expect(described_class.dot_com?).to be_falsey - end - - context 'with arguments' do - it 'returns true when :subdomain is set' do - QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") - - expect(described_class.dot_com?(subdomain: :staging)).to be_truthy - end - - it 'matches multiple subdomains' do - QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") - - expect(described_class.context_matches?(subdomain: [:release, :staging])).to be_truthy - expect(described_class.context_matches?(:production, subdomain: [:release, :staging])).to be_truthy - end - - it 'matches :production' do - QA::Runtime::Scenario.define(:gitlab_address, "https://gitlab.com/") - - expect(described_class.context_matches?(:production)).to be_truthy - end - - it 'doesnt match with mismatching switches' do - QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.test') - - aggregate_failures do - expect(described_class.context_matches?(tld: '.net')).to be_falsey - expect(described_class.context_matches?(:production)).to be_falsey - expect(described_class.context_matches?(subdomain: [:staging])).to be_falsey - expect(described_class.context_matches?(domain: 'example')).to be_falsey - end - end - end - - it 'returns false for mismatching' do - QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") - - expect(described_class.context_matches?(:production)).to be_falsey - end - end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 16d86ef6ed2..631ebf65893 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -22,6 +22,7 @@ RSpec.configure do |config| config.include ::Matchers QA::Specs::Helpers::Quarantine.configure_rspec + QA::Specs::Helpers::ContextSelector.configure_rspec config.before do |example| QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb new file mode 100644 index 00000000000..16b6c6601b1 --- /dev/null +++ b/qa/spec/specs/helpers/context_selector_spec.rb @@ -0,0 +1,294 @@ +# frozen_string_literal: true + +require 'rspec/core/sandbox' + +RSpec.configure do |c| + c.around do |ex| + RSpec::Core::Sandbox.sandboxed do |config| + # If there is an example-within-an-example, we want to make sure the inner example + # does not get a reference to the outer example (the real spec) if it calls + # something like `pending` + config.before(:context) { RSpec.current_example = nil } + + config.color_mode = :off + + ex.run + end + end +end + +RSpec.describe QA::Specs::Helpers::ContextSelector do + include Helpers::StubENV + include QA::Specs::Helpers::RSpec + + before do + QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') + described_class.configure_rspec + end + + describe '.context_matches?' do + it 'returns true when url has .com' do + QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") + + expect(described_class.dot_com?).to be_truthy + end + + it 'returns false when url does not have .com' do + QA::Runtime::Scenario.define(:gitlab_address, "https://gitlab.test") + + expect(described_class.dot_com?).to be_falsey + end + + context 'with arguments' do + it 'returns true when :subdomain is set' do + QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") + + expect(described_class.dot_com?(subdomain: :staging)).to be_truthy + end + + it 'matches multiple subdomains' do + QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") + + expect(described_class.context_matches?(subdomain: [:release, :staging])).to be_truthy + expect(described_class.context_matches?(:production, subdomain: [:release, :staging])).to be_truthy + end + + it 'matches :production' do + QA::Runtime::Scenario.define(:gitlab_address, "https://gitlab.com/") + + expect(described_class.context_matches?(:production)).to be_truthy + end + + it 'doesnt match with mismatching switches' do + QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.test') + + aggregate_failures do + expect(described_class.context_matches?(tld: '.net')).to be_falsey + expect(described_class.context_matches?(:production)).to be_falsey + expect(described_class.context_matches?(subdomain: [:staging])).to be_falsey + expect(described_class.context_matches?(domain: 'example')).to be_falsey + end + end + end + + it 'returns false for mismatching' do + QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") + + expect(described_class.context_matches?(:production)).to be_falsey + end + end + + describe 'description and context blocks' do + context 'with environment set' do + it 'can apply to contexts or descriptions' do + group = describe_successfully 'Runs in staging', only: { subdomain: :staging } do + it('runs in staging') {} + end + + expect(group.examples[0].execution_result.status).to eq(:passed) + end + end + + context 'with different environment set' do + before do + QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com') + described_class.configure_rspec + end + + it 'does not run against production' do + group = describe_successfully 'Runs in staging', :something, only: { subdomain: :staging } do + it('runs in staging') {} + end + + expect(group.examples[0].execution_result.status).to eq(:pending) + end + end + end + + it 'runs only in staging' do + group = describe_successfully do + it('runs in staging', only: { subdomain: :staging }) {} + it('doesnt run in staging', only: :production) {} + it('runs in staging also', only: { subdomain: %i[release staging] }) {} + it('runs in any env') {} + end + + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:pending) + expect(group.examples[2].execution_result.status).to eq(:passed) + expect(group.examples[3].execution_result.status).to eq(:passed) + end + + context 'custom env' do + before do + QA::Runtime::Scenario.define(:gitlab_address, 'https://release.gitlab.net') + end + + it 'runs on a custom environment' do + group = describe_successfully do + it('runs on release gitlab net', only: { tld: '.net', subdomain: :release, domain: 'gitlab' }) {} + it('does not run on release', only: :production) {} + end + + expect(group.examples.first.execution_result.status).to eq(:passed) + expect(group.examples.last.execution_result.status).to eq(:pending) + end + end + + context 'production' do + before do + QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com/') + end + + it 'runs on production' do + group = describe_successfully do + it('runs on prod', only: :production) {} + it('does not run in prod', only: { subdomain: :staging }) {} + it('runs in prod and staging', only: { subdomain: /(staging.)?/, domain: 'gitlab' }) {} + end + + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:pending) + expect(group.examples[2].execution_result.status).to eq(:passed) + end + end + + it 'outputs a message for invalid environments' do + group = describe_successfully do + it('will skip', only: :production) {} + end + + expect(group.examples.first.execution_result.pending_message).to match(/[Tt]est.*not compatible.*environment/) + end + + context 'with pipeline constraints' do + context 'without CI_PROJECT_NAME set' do + before do + stub_env('CI_PROJECT_NAME', nil) + described_class.configure_rspec + end + + it 'runs on any pipeline' do + group = describe_successfully do + it('runs given a single named pipeline', only: { pipeline: :nightly }) {} + it('runs given an array of pipelines', only: { pipeline: [:canary, :not_nightly] }) {} + end + + aggregate_failures do + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:passed) + end + end + end + + context 'when a pipeline triggered from the default branch runs in gitlab-qa' do + before do + stub_env('CI_PROJECT_NAME', 'gitlab-qa') + described_class.configure_rspec + end + + it 'runs on default branch pipelines' do + group = describe_successfully do + it('runs on master pipeline given a single pipeline', only: { pipeline: :master }) {} + it('runs in master given an array of pipelines', only: { pipeline: [:canary, :master] }) {} + it('does not run in non-default pipelines', only: { pipeline: [:nightly, :not_nightly, :not_master] }) {} + end + + aggregate_failures do + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:passed) + expect(group.examples[2].execution_result.status).to eq(:pending) + end + end + end + + context 'with CI_PROJECT_NAME set' do + before do + stub_env('CI_PROJECT_NAME', 'NIGHTLY') + described_class.configure_rspec + end + + it 'runs on designated pipeline' do + group = describe_successfully do + it('runs on nightly', only: { pipeline: :nightly }) {} + it('does not run in not_nightly', only: { pipeline: :not_nightly }) {} + it('runs on nightly given an array', only: { pipeline: [:canary, :nightly] }) {} + it('does not run in not_nightly given an array', only: { pipeline: [:not_nightly, :canary] }) {} + end + + aggregate_failures do + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:pending) + expect(group.examples[2].execution_result.status).to eq(:passed) + expect(group.examples[3].execution_result.status).to eq(:pending) + end + end + end + end + + context 'when excluding contexts' do + context 'with job constraints' do + context 'without CI_JOB_NAME set' do + before do + stub_env('CI_JOB_NAME', nil) + described_class.configure_rspec + end + + it 'runs in any job' do + group = describe_successfully do + it('runs given a single named job', exclude: { job: 'ee:instance-image' }) {} + it('runs given a single regex pattern', exclude: { job: '.*:instance-image' }) {} + it('runs given an array of jobs', exclude: { job: %w[ee:instance-image qa-schedules-browser_ui-3_create] }) {} + it('runs given an array of regex patterns', exclude: { job: %w[ee:.* qa-schedules-browser_ui.*] }) {} + it('runs given a mix of strings and regex patterns', exclude: { job: %w[ee:instance-image qa-schedules-browser_ui.*] }) {} + end + + aggregate_failures do + group.examples.each do |example| + expect(example.execution_result.status).to eq(:passed) + end + end + end + end + + context 'with CI_JOB_NAME set' do + before do + stub_env('CI_JOB_NAME', 'ee:instance-image') + described_class.configure_rspec + end + + it 'does not run in the specified job' do + group = describe_successfully do + it('skips given a single named job', exclude: { job: 'ee:instance-image' }) {} + it('skips given a single regex pattern', exclude: { job: '.*:instance-image' }) {} + it('skips given an array of jobs', exclude: { job: %w[ee:instance-image qa-schedules-browser_ui-3_create] }) {} + it('skips given an array of regex patterns', exclude: { job: %w[ee:.* qa-schedules-browser_ui.*] }) {} + it('skips given a mix of strings and regex patterns', exclude: { job: %w[ee:instance-image qa-schedules-browser_ui.*] }) {} + end + + aggregate_failures do + group.examples.each do |example| + expect(example.execution_result.status).to eq(:pending) + end + end + end + + it 'runs in jobs that do not match' do + group = describe_successfully do + it('runs given a single named job', exclude: { job: 'ce:instance-image' }) {} + it('runs given a single regex pattern', exclude: { job: '.*:instance-image-quarantine' }) {} + it('runs given an array of jobs', exclude: { job: %w[ce:instance-image qa-schedules-browser_ui-3_create] }) {} + it('runs given an array of regex patterns', exclude: { job: %w[ce:.* qa-schedules-browser_ui.*] }) {} + it('runs given a mix of strings and regex patterns', exclude: { job: %w[ce:instance-image qa-schedules-browser_ui.*] }) {} + end + + aggregate_failures do + group.examples.each do |example| + expect(example.execution_result.status).to eq(:passed) + end + end + end + end + end + end +end diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index 694c320ce3d..45754a09b17 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -2,25 +2,6 @@ require 'rspec/core/sandbox' -# We need a reporter for internal tests that's different from the reporter for -# external tests otherwise the results will be mixed up. We don't care about -# most reporting, but we do want to know if a test fails -class RaiseOnFailuresReporter < RSpec::Core::NullReporter - def self.example_failed(example) - raise example.exception - end -end - -# We use an example group wrapper to prevent the state of internal tests -# expanding into the global state -# See: https://github.com/rspec/rspec-core/issues/2603 -def describe_successfully(*args, &describe_body) - example_group = RSpec.describe(*args, &describe_body) - ran_successfully = example_group.run RaiseOnFailuresReporter - expect(ran_successfully).to eq true - example_group -end - RSpec.configure do |c| c.around do |ex| RSpec::Core::Sandbox.sandboxed do |config| @@ -38,6 +19,7 @@ end RSpec.describe QA::Specs::Helpers::Quarantine do include Helpers::StubENV + include QA::Specs::Helpers::RSpec describe '.skip_or_run_quarantined_contexts' do context 'with no tag focused' do @@ -336,159 +318,4 @@ RSpec.describe QA::Specs::Helpers::Quarantine do end end end - - describe 'running against specific environments or pipelines' do - before do - QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') - described_class.configure_rspec - end - - describe 'description and context blocks' do - context 'with environment set' do - it 'can apply to contexts or descriptions' do - group = describe_successfully 'Runs in staging', only: { subdomain: :staging } do - it('runs in staging') {} - end - - expect(group.examples[0].execution_result.status).to eq(:passed) - end - end - - context 'with different environment set' do - before do - QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com') - described_class.configure_rspec - end - - it 'does not run against production' do - group = describe_successfully 'Runs in staging', :something, only: { subdomain: :staging } do - it('runs in staging') {} - end - - expect(group.examples[0].execution_result.status).to eq(:pending) - end - end - end - - it 'runs only in staging' do - group = describe_successfully do - it('runs in staging', only: { subdomain: :staging }) {} - it('doesnt run in staging', only: :production) {} - it('runs in staging also', only: { subdomain: %i[release staging] }) {} - it('runs in any env') {} - end - - expect(group.examples[0].execution_result.status).to eq(:passed) - expect(group.examples[1].execution_result.status).to eq(:pending) - expect(group.examples[2].execution_result.status).to eq(:passed) - expect(group.examples[3].execution_result.status).to eq(:passed) - end - - context 'custom env' do - before do - QA::Runtime::Scenario.define(:gitlab_address, 'https://release.gitlab.net') - end - - it 'runs on a custom environment' do - group = describe_successfully do - it('runs on release gitlab net', only: { tld: '.net', subdomain: :release, domain: 'gitlab' }) {} - it('does not run on release', only: :production) {} - end - - expect(group.examples.first.execution_result.status).to eq(:passed) - expect(group.examples.last.execution_result.status).to eq(:pending) - end - end - - context 'production' do - before do - QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com/') - end - - it 'runs on production' do - group = describe_successfully do - it('runs on prod', only: :production) {} - it('does not run in prod', only: { subdomain: :staging }) {} - it('runs in prod and staging', only: { subdomain: /(staging.)?/, domain: 'gitlab' }) {} - end - - expect(group.examples[0].execution_result.status).to eq(:passed) - expect(group.examples[1].execution_result.status).to eq(:pending) - expect(group.examples[2].execution_result.status).to eq(:passed) - end - end - - it 'outputs a message for invalid environments' do - group = describe_successfully do - it('will skip', only: :production) {} - end - - expect(group.examples.first.execution_result.pending_message).to match(/[Tt]est.*not compatible.*environment/) - end - - context 'with pipeline constraints' do - context 'without CI_PROJECT_NAME set' do - before do - stub_env('CI_PROJECT_NAME', nil) - described_class.configure_rspec - end - - it 'runs on any pipeline' do - group = describe_successfully do - it('runs given a single named pipeline', only: { pipeline: :nightly }) {} - it('runs given an array of pipelines', only: { pipeline: [:canary, :not_nightly] }) {} - end - - aggregate_failures do - expect(group.examples[0].execution_result.status).to eq(:passed) - expect(group.examples[1].execution_result.status).to eq(:passed) - end - end - end - - context 'when a pipeline triggered from the default branch runs in gitlab-qa' do - before do - stub_env('CI_PROJECT_NAME', 'gitlab-qa') - described_class.configure_rspec - end - - it 'runs on default branch pipelines' do - group = describe_successfully do - it('runs on master pipeline given a single pipeline', only: { pipeline: :master }) {} - it('runs in master given an array of pipelines', only: { pipeline: [:canary, :master] }) {} - it('does not run in non-default pipelines', only: { pipeline: [:nightly, :not_nightly, :not_master] }) {} - end - - aggregate_failures do - expect(group.examples[0].execution_result.status).to eq(:passed) - expect(group.examples[1].execution_result.status).to eq(:passed) - expect(group.examples[2].execution_result.status).to eq(:pending) - end - end - end - - context 'with CI_PROJECT_NAME set' do - before do - stub_env('CI_PROJECT_NAME', 'NIGHTLY') - described_class.configure_rspec - end - - it 'runs on designated pipeline' do - group = describe_successfully do - it('runs on nightly', only: { pipeline: :nightly }) {} - it('does not run in not_nightly', only: { pipeline: :not_nightly }) {} - it('runs on nightly given an array', only: { pipeline: [:canary, :nightly] }) {} - it('does not run in not_nightly given an array', only: { pipeline: [:not_nightly, :canary] }) {} - end - - aggregate_failures do - expect(group.examples[0].execution_result.status).to eq(:passed) - expect(group.examples[1].execution_result.status).to eq(:pending) - expect(group.examples[2].execution_result.status).to eq(:passed) - expect(group.examples[3].execution_result.status).to eq(:pending) - end - end - end - end - end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 8ecd2beba68..b0d2f90145f 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -108,7 +108,7 @@ RSpec.describe 'Group issues page' do it 'shows projects only with issues feature enabled', :js do find('.empty-state .js-lazy-loaded') - find('.new-project-item-link').click + find('.empty-state .new-project-item-link').click page.within('.select2-results') do expect(page).to have_content(project.full_name) diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap index e2ad4c68bea..6dfd57906d8 100644 --- a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap @@ -41,6 +41,7 @@ exports[`Design management toolbar component renders design and updated data 1`] /> <gl-button-stub + aria-label="Download design" buttontextclasses="" category="primary" href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d" diff --git a/spec/frontend/projects/commit/components/commit_comments_button_spec.js b/spec/frontend/projects/commit/components/commit_comments_button_spec.js new file mode 100644 index 00000000000..873270c5be1 --- /dev/null +++ b/spec/frontend/projects/commit/components/commit_comments_button_spec.js @@ -0,0 +1,42 @@ +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import CommitCommentsButton from '~/projects/commit/components/commit_comments_button.vue'; + +describe('CommitCommentsButton', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = extendedWrapper( + shallowMount(CommitCommentsButton, { + propsData: { + commentsCount: 1, + ...props, + }, + }), + ); + }; + + const tooltip = () => wrapper.findByTestId('comment-button-wrapper'); + + describe('Comment Button', () => { + it('has proper tooltip and button attributes for 1 comment', () => { + createComponent(); + + expect(tooltip().attributes('title')).toBe('1 comment on this commit'); + expect(tooltip().text()).toBe('1'); + }); + + it('has proper tooltip and button attributes for multiple comments', () => { + createComponent({ commentsCount: 2 }); + + expect(tooltip().attributes('title')).toBe('2 comments on this commit'); + expect(tooltip().text()).toBe('2'); + }); + + it('does not show when there are no comments', () => { + createComponent({ commentsCount: 0 }); + + expect(tooltip().exists()).toBe(false); + }); + }); +}); |