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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_manual_todo.yml16
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/alert_management/list.js4
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue38
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue119
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue7
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue90
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue13
-rw-r--r--app/assets/javascripts/performance_bar/constants.js17
-rw-r--r--app/models/commit_status.rb3
-rw-r--r--changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml6
-rw-r--r--doc/ci/yaml/README.md11
-rw-r--r--doc/user/application_security/dast/dast_troubleshooting.md64
-rw-r--r--doc/user/application_security/dast/index.md67
-rw-r--r--doc/user/packages/npm_registry/index.md9
-rw-r--r--lib/peek/views/active_record.rb30
-rw-r--r--locale/gitlab.pot44
-rw-r--r--rubocop/cop/gitlab/delegate_predicate_methods.rb45
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb11
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb922
-rw-r--r--spec/frontend/boards/components/board_add_new_column_form_spec.js32
-rw-r--r--spec/frontend/cycle_analytics/total_time_component_spec.js34
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js237
-rw-r--r--spec/lib/peek/views/active_record_spec.rb27
-rw-r--r--spec/models/commit_status_spec.rb19
-rw-r--r--spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb40
-rw-r--r--vendor/assets/javascripts/jquery.endless-scroll.js17
28 files changed, 1170 insertions, 758 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index affe95ffb9f..a739c3356f3 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -3577,3 +3577,19 @@ Performance/OpenStruct:
- 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
- 'lib/gitlab/testing/request_inspector_middleware.rb'
- 'lib/mattermost/session.rb'
+
+# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/324629
+Gitlab/DelegatePredicateMethods:
+ Exclude:
+ - 'app/models/clusters/cluster.rb'
+ - 'app/models/clusters/platforms/kubernetes.rb'
+ - 'app/models/concerns/ci/metadatable.rb'
+ - 'app/models/concerns/diff_positionable_note.rb'
+ - 'app/models/concerns/resolvable_discussion.rb'
+ - 'app/models/concerns/services/data_fields.rb'
+ - 'app/models/project.rb'
+ - 'ee/app/models/concerns/ee/ci/metadatable.rb'
+ - 'ee/app/models/ee/group.rb'
+ - 'ee/app/models/ee/namespace.rb'
+ - 'ee/app/models/license.rb'
+ - 'lib/gitlab/ci/trace/stream.rb'
diff --git a/Gemfile b/Gemfile
index 522122b13e4..e0cc20cc91b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -198,7 +198,7 @@ gem 'acts-as-taggable-on', '~> 7.0'
gem 'sidekiq', '~> 5.2.7'
gem 'sidekiq-cron', '~> 1.0'
gem 'redis-namespace', '~> 1.7.0'
-gem 'gitlab-sidekiq-fetcher', '0.5.5', require: 'sidekiq-reliable-fetch'
+gem 'gitlab-sidekiq-fetcher', '0.5.6', require: 'sidekiq-reliable-fetch'
# Cron Parser
gem 'fugit', '~> 1.2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5d63cb1cb2d..891ed84c940 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -470,7 +470,7 @@ GEM
gitlab-pry-byebug (3.9.0)
byebug (~> 11.0)
pry (~> 0.13.0)
- gitlab-sidekiq-fetcher (0.5.5)
+ gitlab-sidekiq-fetcher (0.5.6)
sidekiq (~> 5)
gitlab-styles (6.2.0)
rubocop (~> 0.91, >= 0.91.1)
@@ -1431,7 +1431,7 @@ DEPENDENCIES
gitlab-markup (~> 1.7.1)
gitlab-net-dns (~> 0.9.1)
gitlab-pry-byebug
- gitlab-sidekiq-fetcher (= 0.5.5)
+ gitlab-sidekiq-fetcher (= 0.5.6)
gitlab-styles (~> 6.2.0)
gitlab_chronic_duration (~> 0.10.6.2)
gitlab_omniauth-ldap (~> 2.1.1)
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index f5eac26431f..b23f8a8eba4 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
import AlertManagementList from './components/alert_management_list_wrapper.vue';
+import alertsHelpUrlQuery from './graphql/queries/alert_help_url.query.graphql';
Vue.use(VueApollo);
@@ -41,7 +42,8 @@ export default () => {
),
});
- apolloProvider.clients.defaultClient.cache.writeData({
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: alertsHelpUrlQuery,
data: {
alertsHelpUrl,
},
diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue
index 3c7c792b787..a77696b70cc 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column.vue
@@ -1,10 +1,5 @@
<script>
-import {
- GlFormRadio,
- GlFormRadioGroup,
- GlLabel,
- GlTooltipDirective as GlTooltip,
-} from '@gitlab/ui';
+import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { ListType } from '~/boards/constants';
@@ -17,7 +12,6 @@ export default {
BoardAddNewColumnForm,
GlFormRadio,
GlFormRadioGroup,
- GlLabel,
},
directives: {
GlTooltip,
@@ -99,25 +93,25 @@ export default {
<template>
<board-add-new-column-form
:loading="labelsLoading"
- :form-description="__('A label list displays issues with the selected label.')"
- :search-label="__('Select label')"
+ :none-selected="__('Select a label')"
:search-placeholder="__('Search labels')"
:selected-id="selectedId"
@filter-items="filterItems"
@add-list="addList"
>
- <template slot="selected">
- <gl-label
- v-if="selectedLabel"
- v-gl-tooltip
- :title="selectedLabel.title"
- :description="selectedLabel.description"
- :background-color="selectedLabel.color"
- :scoped="showScopedLabels(selectedLabel)"
- />
+ <template #selected>
+ <template v-if="selectedLabel">
+ <span
+ class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
+ :style="{
+ backgroundColor: selectedLabel.color,
+ }"
+ ></span>
+ <div class="gl-text-truncate">{{ selectedLabel.title }}</div>
+ </template>
</template>
- <template slot="items">
+ <template #items>
<gl-form-radio-group
v-if="labels.length > 0"
v-model="selectedId"
@@ -126,11 +120,11 @@ export default {
<label
v-for="label in labels"
:key="label.id"
- class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
+ class="gl-display-flex gl-mb-5 gl-font-weight-normal gl-overflow-break-word"
>
- <gl-form-radio :value="label.id" class="gl-mb-0" />
+ <gl-form-radio :value="label.id" />
<span
- class="dropdown-label-box gl-top-0"
+ class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
:style="{
backgroundColor: label.color,
}"
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index d85343a5390..70ba90bb1d4 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButton, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlFormGroup,
+ GlIcon,
+ GlSearchBoxByType,
+ GlSkeletonLoader,
+} from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
@@ -8,13 +15,16 @@ export default {
add: __('Add to board'),
cancel: __('Cancel'),
newList: __('New list'),
- noneSelected: __('None'),
noResults: __('No matching results'),
+ scope: __('Scope'),
+ scopeDescription: __('Issues must match this scope to appear in this list.'),
selected: __('Selected'),
},
components: {
GlButton,
+ GlDropdown,
GlFormGroup,
+ GlIcon,
GlSearchBoxByType,
GlSkeletonLoader,
},
@@ -23,11 +33,12 @@ export default {
type: Boolean,
required: true,
},
- formDescription: {
+ searchLabel: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
- searchLabel: {
+ noneSelected: {
type: String,
required: true,
},
@@ -46,8 +57,23 @@ export default {
searchValue: '',
};
},
+ watch: {
+ selectedId(val) {
+ if (val) {
+ this.$refs.dropdown.hide(true);
+ }
+ },
+ },
methods: {
...mapActions(['setAddColumnFormVisibility']),
+ setFocus() {
+ this.$refs.searchBox.focusInput();
+ },
+ onHide() {
+ this.searchValue = '';
+ this.$emit('filter-items', '');
+ this.$emit('hide');
+ },
},
};
</script>
@@ -62,51 +88,64 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
>
<h3
- class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ class="gl-font-size-h2 gl-px-5 gl-py-4 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="board-add-column-form-title"
>
{{ $options.i18n.newList }}
</h3>
- <div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden">
- <slot name="select-list-type">
- <div class="gl-mb-5"></div>
- </slot>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-y-auto gl-align-items-flex-start"
+ >
+ <div class="gl-px-5">
+ <h3 class="gl-font-lg gl-mt-5 gl-mb-2">
+ {{ $options.i18n.scope }}
+ </h3>
+ <p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p>
+ </div>
- <p class="gl-px-5">{{ formDescription }}</p>
+ <slot name="select-list-type"></slot>
- <div class="gl-px-5 gl-pb-4">
- <label class="gl-mb-2">{{ $options.i18n.selected }}</label>
- <slot name="selected">
- <div class="gl-text-gray-500">{{ $options.i18n.noneSelected }}</div>
- </slot>
- </div>
+ <gl-form-group class="gl-px-5 lg-mb-3 gl-max-w-full" :label="searchLabel">
+ <gl-dropdown
+ ref="dropdown"
+ class="gl-mb-3 gl-max-w-full"
+ toggle-class="gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate"
+ boundary="viewport"
+ @shown="setFocus"
+ @hide="onHide"
+ >
+ <template #button-content>
+ <slot name="selected">
+ <div>{{ noneSelected }}</div>
+ </slot>
+ <gl-icon class="dropdown-chevron gl-flex-shrink-0" name="chevron-down" />
+ </template>
- <gl-form-group
- class="gl-mx-5 gl-mb-3"
- :label="searchLabel"
- label-for="board-available-column-entities"
- >
- <gl-search-box-by-type
- id="board-available-column-entities"
- v-model="searchValue"
- debounce="250"
- :placeholder="searchPlaceholder"
- @input="$emit('filter-items', $event)"
- />
- </gl-form-group>
+ <template #header>
+ <gl-search-box-by-type
+ ref="searchBox"
+ v-model="searchValue"
+ debounce="250"
+ class="gl-mt-0!"
+ :placeholder="searchPlaceholder"
+ @input="$emit('filter-items', $event)"
+ />
+ </template>
- <div v-if="loading" class="gl-px-5">
- <gl-skeleton-loader :width="500" :height="172">
- <rect width="480" height="20" x="10" y="15" rx="4" />
- <rect width="380" height="20" x="10" y="50" rx="4" />
- <rect width="430" height="20" x="10" y="85" rx="4" />
- </gl-skeleton-loader>
- </div>
+ <div v-if="loading" class="gl-px-5">
+ <gl-skeleton-loader :width="400" :height="172">
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ <rect width="330" height="20" x="10" y="85" rx="4" />
+ </gl-skeleton-loader>
+ </div>
- <slot v-else name="items">
- <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
- </slot>
+ <slot v-else name="items">
+ <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
+ </slot>
+ </gl-dropdown>
+ </gl-form-group>
</div>
<div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index d59fbcc1b31..0534e027c86 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -134,9 +134,10 @@ export default {
e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType;
const cloneActions = {
- label: ['milestone', 'assignee'],
- assignee: ['milestone', 'label'],
- milestone: ['label', 'assignee'],
+ label: ['milestone', 'assignee', 'iteration'],
+ assignee: ['milestone', 'label', 'iteration'],
+ milestone: ['label', 'assignee', 'iteration'],
+ iteration: ['label', 'assignee', 'milestone'],
};
if (toBoardType) {
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 9bf77239a6b..57569340aa5 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,5 +1,8 @@
<script>
-import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import { sortOrders, sortOrderOptions } from '../constants';
import RequestWarning from './request_warning.vue';
export default {
@@ -7,6 +10,7 @@ export default {
RequestWarning,
GlButton,
GlModal,
+ GlSegmentedControl,
},
directives: {
'gl-modal': GlModalDirective,
@@ -39,6 +43,7 @@ export default {
data() {
return {
openedBacktraces: [],
+ sortOrder: sortOrders.DURATION,
};
},
computed: {
@@ -48,13 +53,37 @@ export default {
metricDetails() {
return this.currentRequest.details[this.metric];
},
+ metricDetailsSummary() {
+ return {
+ [s__('Total')]: this.metricDetails.calls,
+ [s__('PerformanceBar|Total duration')]: this.metricDetails.duration,
+ ...(this.metricDetails.summary || {}),
+ };
+ },
metricDetailsLabel() {
- return this.metricDetails.duration
- ? `${this.metricDetails.duration} / ${this.metricDetails.calls}`
- : this.metricDetails.calls;
+ if (this.metricDetails.duration && this.metricDetails.calls) {
+ return `${this.metricDetails.duration} / ${this.metricDetails.calls}`;
+ } else if (this.metricDetails.calls) {
+ return this.metricDetails.calls;
+ }
+
+ return '0';
+ },
+ displaySortOrder() {
+ return (
+ this.metricDetails.details.length !== 0 &&
+ this.metricDetails.details.every((item) => item.start)
+ );
},
detailsList() {
- return this.metricDetails.details;
+ return this.metricDetails.details.map((item, index) => ({ ...item, id: index }));
+ },
+ sortedList() {
+ if (this.sortOrder === sortOrders.CHRONOLOGICAL) {
+ return this.detailsList.slice().sort(this.sortDetailChronologically);
+ }
+
+ return this.detailsList.slice().sort(this.sortDetailByDuration);
},
warnings() {
return this.metricDetails.warnings || [];
@@ -82,7 +111,17 @@ export default {
itemHasOpenedBacktrace(toggledIndex) {
return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0;
},
+ changeSortOrder(order) {
+ this.sortOrder = order;
+ },
+ sortDetailByDuration(a, b) {
+ return a.duration < b.duration ? 1 : -1;
+ },
+ sortDetailChronologically(a, b) {
+ return a.start < b.start ? -1 : 1;
+ },
},
+ sortOrderOptions,
};
</script>
<template>
@@ -93,18 +132,41 @@ export default {
data-qa-selector="detailed_metric_content"
>
<gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
- <span class="gl-text-blue-300 gl-font-weight-bold">{{ metricDetailsLabel }}</span>
+ <span
+ class="gl-text-blue-300 gl-font-weight-bold"
+ data-testid="performance-bar-details-label"
+ >
+ {{ metricDetailsLabel }}
+ </span>
</gl-button>
<gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable>
+ <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ <div class="gl-display-flex gl-align-items-center" data-testid="performance-bar-summary">
+ <div v-for="(value, name) in metricDetailsSummary" :key="name" class="gl-pr-8">
+ <div v-if="value" data-testid="performance-bar-summary-item">
+ <div>{{ name }}</div>
+ <div class="gl-font-size-h1 gl-font-weight-bold">{{ value }}</div>
+ </div>
+ </div>
+ </div>
+ <gl-segmented-control
+ v-if="displaySortOrder"
+ data-testid="performance-bar-sort-order"
+ :options="$options.sortOrderOptions"
+ :checked="sortOrder"
+ @input="changeSortOrder"
+ />
+ </div>
+ <hr />
<table class="table gl-table">
- <template v-if="detailsList.length">
- <tr v-for="(item, index) in detailsList" :key="index">
- <td>
+ <template v-if="sortedList.length">
+ <tr v-for="item in sortedList" :key="item.id">
+ <td data-testid="performance-item-duration">
<span v-if="item.duration">{{
sprintf(__('%{duration}ms'), { duration: item.duration })
}}</span>
</td>
- <td>
+ <td data-testid="performance-item-content">
<div>
<div
v-for="(key, keyIndex) in keys"
@@ -121,12 +183,12 @@ export default {
variant="default"
icon="ellipsis_h"
size="small"
- :selected="itemHasOpenedBacktrace(index)"
+ :selected="itemHasOpenedBacktrace(item.id)"
:aria-label="__('Toggle backtrace')"
- @click="toggleBacktrace(index)"
+ @click="toggleBacktrace(item.id)"
/>
</div>
- <pre v-if="itemHasOpenedBacktrace(index)" class="backtrace-row mt-2">{{
+ <pre v-if="itemHasOpenedBacktrace(item.id)" class="backtrace-row gl-mt-3">{{
item.backtrace
}}</pre>
</div>
@@ -135,7 +197,7 @@ export default {
</template>
<template v-else>
<tr>
- <td>
+ <td data-testid="performance-bar-empty-detail-notice">
{{ sprintf(__('No %{header} for this request.'), { header: header.toLowerCase() }) }}
</td>
</tr>
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 6b446eb6073..4f79d99a49b 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -40,7 +40,7 @@ export default {
metric: 'active-record',
title: 'pg',
header: s__('PerformanceBar|SQL queries'),
- keys: ['sql', 'cached', 'db_role'],
+ keys: ['sql', 'cached', 'transaction', 'db_role'],
},
{
metric: 'bullet',
@@ -69,6 +69,7 @@ export default {
},
{
metric: 'external-http',
+ title: 'external',
header: s__('PerformanceBar|External Http calls'),
keys: ['label', 'code', 'proxy', 'error'],
},
@@ -157,15 +158,17 @@ export default {
class="view"
>
<a class="gl-text-blue-300" :href="currentRequest.details.tracing.tracing_url">{{
- s__('PerformanceBar|trace')
+ s__('PerformanceBar|Trace')
}}</a>
</div>
- <add-request v-on="$listeners" />
<div v-if="currentRequest.details" id="peek-download" class="view">
<a class="gl-text-blue-300" :download="downloadName" :href="downloadPath">{{
s__('PerformanceBar|Download')
}}</a>
</div>
+ <a v-if="statsUrl" class="gl-text-blue-300 view" :href="statsUrl">{{
+ s__('PerformanceBar|Stats')
+ }}</a>
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
@@ -173,9 +176,7 @@ export default {
class="ml-auto"
@change-current-request="changeCurrentRequest"
/>
- <div v-if="statsUrl" id="peek-stats" class="view">
- <a class="gl-text-blue-300" :href="statsUrl">{{ s__('PerformanceBar|Stats') }}</a>
- </div>
+ <add-request v-on="$listeners" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/performance_bar/constants.js b/app/assets/javascripts/performance_bar/constants.js
new file mode 100644
index 00000000000..9659383edd9
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/constants.js
@@ -0,0 +1,17 @@
+import { s__ } from '~/locale';
+
+export const sortOrders = {
+ DURATION: 'duration',
+ CHRONOLOGICAL: 'chronological',
+};
+
+export const sortOrderOptions = [
+ {
+ value: sortOrders.DURATION,
+ text: s__('PerformanceBar|Sort by duration'),
+ },
+ {
+ value: sortOrders.CHRONOLOGICAL,
+ text: s__('PerformanceBar|Sort chronologically'),
+ },
+];
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 524429bf12a..9b08ca3718e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -293,7 +293,8 @@ class CommitStatus < ApplicationRecord
end
def update_older_statuses_retried!
- self.class
+ pipeline
+ .statuses
.latest
.where(name: name)
.where.not(id: id)
diff --git a/changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml b/changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml
new file mode 100644
index 00000000000..6eb2d2fcbe3
--- /dev/null
+++ b/changelogs/unreleased/tor-defect-remove-endless-scroll-jquery-animation.yml
@@ -0,0 +1,6 @@
+---
+title: Remove calls to jQuery animations to fix infinite scrolling on the Repository
+ commits page
+merge_request: 57379
+author:
+type: fixed
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index ec126dc3ee7..5e74fc2027e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1983,6 +1983,10 @@ To disable directed acyclic graphs (DAG), set the limit to `0`.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14311) in GitLab v12.6.
+When a job uses `needs`, it no longer downloads all artifacts from previous stages
+by default, because jobs with `needs` can start before earlier stages complete. With
+`needs` you can only download artifacts from the jobs listed in the `needs:` configuration.
+
Use `artifacts: true` (default) or `artifacts: false` to control when artifacts are
downloaded in jobs that use `needs`.
@@ -3072,6 +3076,13 @@ The artifacts are sent to GitLab after the job finishes. They are
available for download in the GitLab UI if the size is not
larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
+By default, jobs in later stages automatically download all the artifacts created
+by jobs in earlier stages. You can control artifact download behavior in jobs with
+[`dependencies`](#dependencies).
+
+When using the [`needs`](#artifact-downloads-with-needs) keyword, jobs can only download
+artifacts from the jobs defined in the `needs` configuration.
+
Job artifacts are only collected for successful jobs by default, and
artifacts are restored after [caches](#cache).
diff --git a/doc/user/application_security/dast/dast_troubleshooting.md b/doc/user/application_security/dast/dast_troubleshooting.md
new file mode 100644
index 00000000000..b3f853ab2c0
--- /dev/null
+++ b/doc/user/application_security/dast/dast_troubleshooting.md
@@ -0,0 +1,64 @@
+---
+stage: Secure
+group: Dynamic Analysis
+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
+type: reference, howto
+---
+
+# Dynamic Application Security Testing (DAST) Troubleshooting **(ULTIMATE)**
+
+The following troubleshooting scenarios have been collected from customer support cases. If you
+experience a problem not addressed here, or the information here does not fix your problem, create a
+support ticket.
+
+## Running out of memory
+
+By default, ZAProxy, which DAST relies on, is allocated memory that sums to 25%
+of the total memory on the host.
+Since it keeps most of its information in memory during a scan,
+it's possible for DAST to run out of memory while scanning large applications.
+This results in the following error:
+
+```plaintext
+[zap.out] java.lang.OutOfMemoryError: Java heap space
+```
+
+Fortunately, it's straightforward to increase the amount of memory available
+for DAST by using the `DAST_ZAP_CLI_OPTIONS` CI/CD variable:
+
+```yaml
+include:
+ - template: DAST.gitlab-ci.yml
+
+variables:
+ DAST_ZAP_CLI_OPTIONS: "-Xmx3072m"
+```
+
+This example allocates 3072 MB to DAST.
+Change the number after `-Xmx` to the required memory amount.
+
+## DAST job exceeding the job timeout
+
+If your DAST job exceeds the job timeout and you need to reduce the scan duration, we shared some
+tips for optimizing DAST scans in a [blog post](https://about.gitlab.com/blog/2020/08/31/how-to-configure-dast-full-scans-for-complex-web-applications/).
+
+## Getting warning message `gl-dast-report.json: no matching files`
+
+For information on this, see the [general Application Security troubleshooting section](../../../ci/pipelines/job_artifacts.md#error-message-no-files-to-upload).
+
+## Getting error `dast job: chosen stage does not exist` when including DAST CI template
+
+To avoid overwriting stages from other CI files, newer versions of the DAST CI template do not
+define stages. If you recently started using `DAST.latest.gitlab-ci.yml` or upgraded to a new major
+release of GitLab and began receiving this error, you must define a `dast` stage with your other
+stages. Note that you must have a running application for DAST to scan. If your application is set
+up in your pipeline, it must be deployed in a stage _before_ the `dast` stage:
+
+```yaml
+stages:
+ - deploy # DAST needs a running application to scan
+ - dast
+
+include:
+ - template: DAST.latest.gitlab-ci.yml
+```
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 77d906332dd..646e90b30c2 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -1193,70 +1193,3 @@ artifacts, add the following to your `gitlab-ci.yml` file:
dast:
dependencies: []
```
-
-## Troubleshooting
-
-### Running out of memory
-
-By default, ZAProxy, which DAST relies on, is allocated memory that sums to 25%
-of the total memory on the host.
-Since it keeps most of its information in memory during a scan,
-it's possible for DAST to run out of memory while scanning large applications.
-This results in the following error:
-
-```plaintext
-[zap.out] java.lang.OutOfMemoryError: Java heap space
-```
-
-Fortunately, it's straightforward to increase the amount of memory available
-for DAST by using the `DAST_ZAP_CLI_OPTIONS` CI/CD variable:
-
-```yaml
-include:
- - template: DAST.gitlab-ci.yml
-
-variables:
- DAST_ZAP_CLI_OPTIONS: "-Xmx3072m"
-```
-
-Here, DAST is being allocated 3072 MB.
-Change the number after `-Xmx` to the required memory amount.
-
-### DAST job exceeding the job timeout
-
-If your DAST job exceeds the job timeout and you need to reduce the scan duration, we shared some tips for optimizing DAST scans in a [blog post](https://about.gitlab.com/blog/2020/08/31/how-to-configure-dast-full-scans-for-complex-web-applications/).
-
-### Getting warning message `gl-dast-report.json: no matching files`
-
-For information on this, see the [general Application Security troubleshooting section](../../../ci/pipelines/job_artifacts.md#error-message-no-files-to-upload).
-
-### Getting error `dast job: chosen stage does not exist` when including DAST CI template
-
-Newer versions of the DAST CI template do not define stages in order to avoid
-overwriting stages from other CI files. If you've recently started using
-`DAST.latest.gitlab-ci.yml` or upgraded to a new major release of GitLab and
-began receiving this error, you will need to define a `dast` stage with your
-other stages. Please note that you must have a running application for DAST to
-scan. If your application is set up in your pipeline, it must be deployed
- in a stage _before_ the `dast` stage:
-
-```yaml
-stages:
- - deploy # DAST needs a running application to scan
- - dast
-
-include:
- - template: DAST.latest.gitlab-ci.yml
-```
-
-<!-- ## Troubleshooting
-
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
-
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index a44f3286c12..4757e353dd5 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -124,7 +124,7 @@ npm config set @foo:registry https://gitlab.example.com/api/v4/projects/<your_pr
# Add the token for the scoped packages URL. Replace <your_project_id>
# with the project where your package is located.
-npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
+npm config set -- '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
```
- `<your_project_id>` is your project ID, found on the project's home page.
@@ -147,7 +147,7 @@ npm config set @foo:registry https://gitlab.example.com/api/v4/packages/npm/
# Add the token for the scoped packages URL. This will allow you to download
# `@foo/` packages from private projects.
-npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>"
+npm config set -- '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>"
```
- `<your_token>` is your personal access token or deploy token.
@@ -189,8 +189,8 @@ To use the [instance-level](#use-the-gitlab-endpoint-for-npm-packages) npm endpo
To avoid hard-coding the `authToken` value, you may use a variable in its place:
```shell
-npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}"
-npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}"
+npm config set -- '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}"
+npm config set -- '//gitlab.example.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}"
```
Then, you can run `npm publish` either locally or by using GitLab CI/CD.
@@ -482,7 +482,6 @@ NPM_TOKEN=<your_token> npm install
If you get this error, ensure that:
- Your token is not expired and has appropriate permissions.
-- [Your token does not begin with `-`](https://gitlab.com/gitlab-org/gitlab/-/issues/235473).
- A package with the same name or version doesn't already exist within the given scope.
- Your NPM package name does not contain a dot `.`. This is a [known issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/10248)
in GitLab 11.9 and earlier.
diff --git a/lib/peek/views/active_record.rb b/lib/peek/views/active_record.rb
index 523e673e9e1..4040bed50a9 100644
--- a/lib/peek/views/active_record.rb
+++ b/lib/peek/views/active_record.rb
@@ -17,22 +17,32 @@ module Peek
}
}.freeze
- def results
- super.merge(calls: detailed_calls)
- end
-
def self.thresholds
@thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS)
end
+ def results
+ super.merge(summary: summary)
+ end
+
private
- def detailed_calls
- "#{calls} (#{cached_calls} cached)"
+ def summary
+ detail_store.each_with_object({}) do |item, count|
+ count_summary(item, count)
+ end
end
- def cached_calls
- detail_store.count { |item| item[:cached] == 'cached' }
+ def count_summary(item, count)
+ if item[:cached].present?
+ count[item[:cached]] ||= 0
+ count[item[:cached]] += 1
+ end
+
+ if item[:transaction].present?
+ count[item[:transaction]] ||= 0
+ count[item[:transaction]] += 1
+ end
end
def setup_subscribers
@@ -45,10 +55,12 @@ module Peek
def generate_detail(start, finish, data)
{
+ start: start,
duration: finish - start,
sql: data[:sql].strip,
backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller),
- cached: data[:cached] ? 'cached' : ''
+ cached: data[:cached] ? 'Cached' : '',
+ transaction: data[:connection].transaction_open? ? 'In a transaction' : ''
}
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 28c074290f5..9030bfa66b0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1365,9 +1365,6 @@ msgstr ""
msgid "A job artifact is an archive of files and directories saved by a job when it finishes."
msgstr ""
-msgid "A label list displays issues with the selected label."
-msgstr ""
-
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
msgstr ""
@@ -1386,9 +1383,6 @@ msgstr ""
msgid "A merge request hasn't yet been merged"
msgstr ""
-msgid "A milestone list displays issues in the selected milestone."
-msgstr ""
-
msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details"
msgstr ""
@@ -3262,9 +3256,6 @@ msgstr ""
msgid "An application called %{link_to_client} is requesting access to your GitLab account."
msgstr ""
-msgid "An assignee list displays issues assigned to the selected user"
-msgstr ""
-
msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message."
msgstr ""
@@ -3652,9 +3643,6 @@ msgstr ""
msgid "An issue title is required"
msgstr ""
-msgid "An iteration list displays issues in the selected iteration."
-msgstr ""
-
msgid "An unauthenticated user"
msgstr ""
@@ -6446,9 +6434,6 @@ msgstr ""
msgid "ClusterAgents|Configuration"
msgstr ""
-msgid "ClusterAgents|Connect your cluster with the GitLab Agent"
-msgstr ""
-
msgid "ClusterAgents|Created by"
msgstr ""
@@ -6470,6 +6455,9 @@ msgstr ""
msgid "ClusterAgents|Learn how to create an agent access token"
msgstr ""
+msgid "ClusterAgents|Learn more about installing the GitLab Agent"
+msgstr ""
+
msgid "ClusterAgents|Name"
msgstr ""
@@ -17203,6 +17191,9 @@ msgstr ""
msgid "Issues closed"
msgstr ""
+msgid "Issues must match this scope to appear in this list."
+msgstr ""
+
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr ""
@@ -18419,9 +18410,6 @@ msgstr ""
msgid "List the merge requests that must be merged before this one."
msgstr ""
-msgid "List type"
-msgstr ""
-
msgid "List view"
msgstr ""
@@ -22327,10 +22315,19 @@ msgstr ""
msgid "PerformanceBar|SQL queries"
msgstr ""
+msgid "PerformanceBar|Sort by duration"
+msgstr ""
+
+msgid "PerformanceBar|Sort chronologically"
+msgstr ""
+
msgid "PerformanceBar|Stats"
msgstr ""
-msgid "PerformanceBar|trace"
+msgid "PerformanceBar|Total duration"
+msgstr ""
+
+msgid "PerformanceBar|Trace"
msgstr ""
msgid "Permissions"
@@ -27301,6 +27298,9 @@ msgstr ""
msgid "Select a label"
msgstr ""
+msgid "Select a milestone"
+msgstr ""
+
msgid "Select a new namespace"
msgstr ""
@@ -27328,9 +27328,15 @@ msgstr ""
msgid "Select all"
msgstr ""
+msgid "Select an assignee"
+msgstr ""
+
msgid "Select an existing Kubernetes cluster or create a new one."
msgstr ""
+msgid "Select an iteration"
+msgstr ""
+
msgid "Select assignee"
msgstr ""
diff --git a/rubocop/cop/gitlab/delegate_predicate_methods.rb b/rubocop/cop/gitlab/delegate_predicate_methods.rb
new file mode 100644
index 00000000000..43b5184faab
--- /dev/null
+++ b/rubocop/cop/gitlab/delegate_predicate_methods.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Gitlab
+ # This cop looks for delegations to predicate methods with `allow_nil: true` option.
+ # This construct results in three possible results: true, false and nil.
+ # In other words, it does not preserve the strict Boolean nature of predicate method return value.
+ # This cop suggests creating a method to handle `nil` delegator and ensure only Boolean type is returned.
+ #
+ # @example
+ # # bad
+ # delegate :is_foo?, to: :bar, allow_nil: true
+ #
+ # # good
+ # def is_foo?
+ # return false unless bar
+ # bar.is_foo?
+ # end
+ #
+ # def is_foo?
+ # !!bar&.is_foo?
+ # end
+ class DelegatePredicateMethods < RuboCop::Cop::Cop
+ MSG = "Using `delegate` with `allow_nil` on the following predicate methods is discouraged: %s."
+ RESTRICT_ON_SEND = %i[delegate].freeze
+ def_node_matcher :predicate_allow_nil_option, <<~PATTERN
+ (send nil? :delegate
+ (sym $_)*
+ (hash <$(pair (sym :allow_nil) true) ...>)
+ )
+ PATTERN
+
+ def on_send(node)
+ predicate_allow_nil_option(node) do |delegated_methods, _options|
+ offensive_methods = delegated_methods.select { |method| method.end_with?('?') }
+ next if offensive_methods.empty?
+
+ add_offense(node, message: format(MSG, offensive_methods.join(', ')))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index b9945207bb2..5128fc4004e 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -71,10 +71,13 @@ RSpec.describe 'User adds lists', :js do
def select_label(board_new_list_enabled, label)
if board_new_list_enabled
- page.within('.board-add-new-list') do
- find('label', text: label.title).click
- click_button 'Add'
- end
+ click_button 'Select a label'
+
+ find('label', text: label.title).click
+
+ click_button 'Add to board'
+
+ wait_for_all_requests
else
page.within('.dropdown-menu-issues-board-new') do
click_link label.title
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 5a3c2fd6869..ac412d19974 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -7,12 +7,14 @@ RSpec.describe 'GFM autocomplete', :js do
let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
+
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
let_it_be(:project) { create(:project, group: child_group) }
- let_it_be(:label) { create(:label, project: project, title: 'special+') }
- let(:issue) { create(:issue, project: project) }
+ let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
+ let_it_be(:label) { create(:label, project: project, title: 'special+') }
+ let_it_be(:snippet) { create(:project_snippet, project: project, title: 'code snippet') }
before_all do
project.add_maintainer(user)
@@ -21,16 +23,35 @@ RSpec.describe 'GFM autocomplete', :js do
end
describe 'when tribute_autocomplete feature flag is off' do
- before do
- stub_feature_flags(tribute_autocomplete: false)
+ describe 'new issue page' do
+ before do
+ stub_feature_flags(tribute_autocomplete: false)
+
+ sign_in(user)
+ visit new_project_issue_path(project)
+
+ wait_for_requests
+ end
- sign_in(user)
- visit project_issue_path(project, issue)
+ it 'allows quick actions' do
+ fill_in 'Description', with: '/'
- wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ end
end
describe 'issue description' do
+ let(:issue_to_edit) { create(:issue, project: project) }
+
+ before do
+ stub_feature_flags(tribute_autocomplete: false)
+
+ sign_in(user)
+ visit project_issue_path(project, issue_to_edit)
+
+ wait_for_requests
+ end
+
it 'updates with GFM reference' do
click_button 'Edit title and description'
@@ -58,367 +79,355 @@ RSpec.describe 'GFM autocomplete', :js do
end
end
- describe 'triggering autocomplete' do
- it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
- fill_in 'Comment', with: 'testing@'
- expect(page).not_to have_css('.atwho-view')
-
- fill_in 'Comment', with: '@@'
- expect(page).not_to have_css('.atwho-view')
-
- fill_in 'Comment', with: "@#{user.username[0..2]}!"
- expect(page).not_to have_css('.atwho-view')
-
- fill_in 'Comment', with: "hello:#{user.username[0..2]}"
- expect(page).not_to have_css('.atwho-view')
-
- fill_in 'Comment', with: '7:'
- expect(page).not_to have_css('.atwho-view')
-
- fill_in 'Comment', with: 'w:'
- expect(page).not_to have_css('.atwho-view')
+ describe 'issue comment' do
+ before do
+ stub_feature_flags(tribute_autocomplete: false)
- fill_in 'Comment', with: 'Ё:'
- expect(page).not_to have_css('.atwho-view')
+ sign_in(user)
+ visit project_issue_path(project, issue)
- fill_in 'Comment', with: "test\n\n@"
- expect(find_autocomplete_menu).to be_visible
+ wait_for_requests
end
- end
- it 'opens autocomplete menu when field starts with text' do
- fill_in 'Comment', with: '@'
+ describe 'triggering autocomplete' do
+ it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
+ fill_in 'Comment', with: 'testing@'
+ expect(page).not_to have_css('.atwho-view')
- expect(find_autocomplete_menu).to be_visible
- end
+ fill_in 'Comment', with: '@@'
+ expect(page).not_to have_css('.atwho-view')
- it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
- issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
- create(:issue, project: project, title: issue_xss_title)
+ fill_in 'Comment', with: "@#{user.username[0..2]}!"
+ expect(page).not_to have_css('.atwho-view')
- fill_in 'Comment', with: '#'
+ fill_in 'Comment', with: "hello:#{user.username[0..2]}"
+ expect(page).not_to have_css('.atwho-view')
- wait_for_requests
+ fill_in 'Comment', with: '7:'
+ expect(page).not_to have_css('.atwho-view')
- expect(find_autocomplete_menu).to have_text(issue_xss_title)
- end
+ fill_in 'Comment', with: 'w:'
+ expect(page).not_to have_css('.atwho-view')
- it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
- fill_in 'Comment', with: '@ev'
+ fill_in 'Comment', with: 'Ё:'
+ expect(page).not_to have_css('.atwho-view')
- wait_for_requests
+ fill_in 'Comment', with: "test\n\n@"
+ expect(find_autocomplete_menu).to be_visible
+ end
+ end
- expect(find_highlighted_autocomplete_item).to have_text(user_xss.username)
- end
+ it 'opens autocomplete menu when field starts with text' do
+ fill_in 'Comment', with: '@'
- it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
- milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
- create(:milestone, project: project, title: milestone_xss_title)
+ expect(find_autocomplete_menu).to be_visible
+ end
- fill_in 'Comment', with: '%'
+ it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
+ issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
+ create(:issue, project: project, title: issue_xss_title)
- wait_for_requests
+ fill_in 'Comment', with: '#'
- expect(find_autocomplete_menu).to have_text('alert milestone')
- end
+ wait_for_requests
- it 'doesnt select the first item for non-assignee dropdowns' do
- fill_in 'Comment', with: ':'
+ expect(find_autocomplete_menu).to have_text(issue_xss_title)
+ end
- wait_for_requests
+ it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
+ fill_in 'Comment', with: '@ev'
- expect(find_autocomplete_menu).not_to have_css('.cur')
- end
+ wait_for_requests
- it 'selects the first item for assignee dropdowns' do
- fill_in 'Comment', with: '@'
+ expect(find_highlighted_autocomplete_item).to have_text(user_xss.username)
+ end
- wait_for_requests
+ it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
+ milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
+ create(:milestone, project: project, title: milestone_xss_title)
- expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
- end
+ fill_in 'Comment', with: '%'
- it 'includes items for assignee dropdowns with non-ASCII characters in name' do
- fill_in 'Comment', with: "@#{user.name[0...8]}"
+ wait_for_requests
- wait_for_requests
+ expect(find_autocomplete_menu).to have_text('alert milestone')
+ end
- expect(find_autocomplete_menu).to have_text(user.name)
- end
+ it 'doesnt select the first item for non-assignee dropdowns' do
+ fill_in 'Comment', with: ':'
- it 'searches across full name for assignees' do
- fill_in 'Comment', with: '@speciąlsome'
+ wait_for_requests
- wait_for_requests
+ expect(find_autocomplete_menu).not_to have_css('.cur')
+ end
- expect(find_highlighted_autocomplete_item).to have_text(user.name)
- end
+ it 'selects the first item for assignee dropdowns' do
+ fill_in 'Comment', with: '@'
- it 'shows names that start with the query as the top result' do
- fill_in 'Comment', with: '@mar'
+ wait_for_requests
- wait_for_requests
+ expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
+ end
- expect(find_highlighted_autocomplete_item).to have_text(user2.name)
- end
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ fill_in 'Comment', with: "@#{user.name[0...8]}"
- it 'shows usernames that start with the query as the top result' do
- fill_in 'Comment', with: '@msi'
+ wait_for_requests
- wait_for_requests
+ expect(find_autocomplete_menu).to have_text(user.name)
+ end
- expect(find_highlighted_autocomplete_item).to have_text(user2.name)
- end
+ it 'searches across full name for assignees' do
+ fill_in 'Comment', with: '@speciąlsome'
- # Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
- it 'shows username when pasting then pressing Enter' do
- fill_in 'Comment', with: "@#{user.username}\n"
+ wait_for_requests
- expect(find_field('Comment').value).to have_text "@#{user.username}"
- end
+ expect(find_highlighted_autocomplete_item).to have_text(user.name)
+ end
- it 'does not show `@undefined` when pressing `@` then Enter' do
- fill_in 'Comment', with: "@\n"
+ it 'shows names that start with the query as the top result' do
+ fill_in 'Comment', with: '@mar'
- expect(find_field('Comment').value).to have_text '@'
- expect(find_field('Comment').value).not_to have_text '@undefined'
- end
+ wait_for_requests
- it 'selects the first item for non-assignee dropdowns if a query is entered' do
- fill_in 'Comment', with: ':1'
+ expect(find_highlighted_autocomplete_item).to have_text(user2.name)
+ end
- wait_for_requests
+ it 'shows usernames that start with the query as the top result' do
+ fill_in 'Comment', with: '@msi'
- expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
- end
+ wait_for_requests
- context 'if a selected value has special characters' do
- it 'wraps the result in double quotes' do
- fill_in 'Comment', with: "~#{label.title[0]}"
+ expect(find_highlighted_autocomplete_item).to have_text(user2.name)
+ end
- find_highlighted_autocomplete_item.click
+ # Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
+ it 'shows username when pasting then pressing Enter' do
+ fill_in 'Comment', with: "@#{user.username}\n"
- expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
+ expect(find_field('Comment').value).to have_text "@#{user.username}"
end
- it 'doesn\'t wrap for assignee values' do
- fill_in 'Comment', with: "@#{user.username[0]}"
+ it 'does not show `@undefined` when pressing `@` then Enter' do
+ fill_in 'Comment', with: "@\n"
- find_highlighted_autocomplete_item.click
-
- expect(find_field('Comment').value).to have_text("@#{user.username}")
+ expect(find_field('Comment').value).to have_text '@'
+ expect(find_field('Comment').value).not_to have_text '@undefined'
end
- it 'doesn\'t wrap for emoji values' do
- fill_in 'Comment', with: ':cartwheel_'
+ it 'selects the first item for non-assignee dropdowns if a query is entered' do
+ fill_in 'Comment', with: ':1'
- find_highlighted_autocomplete_item.click
+ wait_for_requests
- expect(find_field('Comment').value).to have_text('cartwheel_tone1')
+ expect(find_autocomplete_menu).to have_css('.cur:first-of-type')
end
- it 'triggers autocomplete after selecting a quick action' do
- fill_in 'Comment', with: '/as'
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ fill_in 'Comment', with: "~#{label.title[0]}"
- find_highlighted_autocomplete_item.click
+ find_highlighted_autocomplete_item.click
- expect(find_autocomplete_menu).to have_text(user.username)
- end
+ expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
+ end
- it 'does not limit quick actions autocomplete list to 5' do
- fill_in 'Comment', with: '/'
+ it 'doesn\'t wrap for assignee values' do
+ fill_in 'Comment', with: "@#{user.username[0]}"
- expect(find_autocomplete_menu).to have_css('li', minimum: 6)
- end
- end
+ find_highlighted_autocomplete_item.click
- context 'assignees' do
- let(:issue_assignee) { create(:issue, project: project) }
- let(:unassigned_user) { create(:user) }
+ expect(find_field('Comment').value).to have_text("@#{user.username}")
+ end
- before do
- issue_assignee.update(assignees: [user])
+ it 'doesn\'t wrap for emoji values' do
+ fill_in 'Comment', with: ':cartwheel_'
- project.add_maintainer(unassigned_user)
- end
+ find_highlighted_autocomplete_item.click
- it 'lists users who are currently not assigned to the issue when using /assign' do
- visit project_issue_path(project, issue_assignee)
+ expect(find_field('Comment').value).to have_text('cartwheel_tone1')
+ end
- fill_in 'Comment', with: '/as'
+ it 'triggers autocomplete after selecting a quick action' do
+ fill_in 'Comment', with: '/as'
- find_highlighted_autocomplete_item.click
+ find_highlighted_autocomplete_item.click
- expect(find_autocomplete_menu).not_to have_text(user.username)
- expect(find_autocomplete_menu).to have_text(unassigned_user.username)
- end
+ expect(find_autocomplete_menu).to have_text(user2.username)
+ end
- it 'shows dropdown on new issue form' do
- visit new_project_issue_path(project)
+ it 'does not limit quick actions autocomplete list to 5' do
+ fill_in 'Comment', with: '/'
- fill_in 'Description', with: '/ass'
+ expect(find_autocomplete_menu).to have_css('li', minimum: 6)
+ end
+ end
- find_highlighted_autocomplete_item.click
+ context 'assignees' do
+ it 'lists users who are currently not assigned to the issue when using /assign' do
+ fill_in 'Comment', with: '/as'
- expect(find_autocomplete_menu).to have_text(unassigned_user.username)
- expect(find_autocomplete_menu).to have_text(user.username)
+ find_highlighted_autocomplete_item.click
+
+ expect(find_autocomplete_menu).not_to have_text(user.username)
+ expect(find_autocomplete_menu).to have_text(user2.username)
+ end
end
- end
- context 'labels' do
- it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
- label_xss_title = 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a'
- create(:label, project: project, title: label_xss_title)
+ context 'labels' do
+ it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
+ label_xss_title = 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a'
+ create(:label, project: project, title: label_xss_title)
- fill_in 'Comment', with: '~'
+ fill_in 'Comment', with: '~'
- wait_for_requests
+ wait_for_requests
- expect(find_autocomplete_menu).to have_text('alert label')
- end
+ expect(find_autocomplete_menu).to have_text('alert label')
+ end
- it 'allows colons when autocompleting scoped labels' do
- create(:label, project: project, title: 'scoped:label')
+ it 'allows colons when autocompleting scoped labels' do
+ create(:label, project: project, title: 'scoped:label')
- fill_in 'Comment', with: '~scoped:'
+ fill_in 'Comment', with: '~scoped:'
- wait_for_requests
+ wait_for_requests
- expect(find_autocomplete_menu).to have_text('scoped:label')
- end
+ expect(find_autocomplete_menu).to have_text('scoped:label')
+ end
- it 'allows colons when autocompleting scoped labels with double colons' do
- create(:label, project: project, title: 'scoped::label')
+ it 'allows colons when autocompleting scoped labels with double colons' do
+ create(:label, project: project, title: 'scoped::label')
- fill_in 'Comment', with: '~scoped::'
+ fill_in 'Comment', with: '~scoped::'
- wait_for_requests
+ wait_for_requests
- expect(find_autocomplete_menu).to have_text('scoped::label')
- end
+ expect(find_autocomplete_menu).to have_text('scoped::label')
+ end
- it 'allows spaces when autocompleting multi-word labels' do
- create(:label, project: project, title: 'Accepting merge requests')
+ it 'allows spaces when autocompleting multi-word labels' do
+ create(:label, project: project, title: 'Accepting merge requests')
- fill_in 'Comment', with: '~Accepting merge'
+ fill_in 'Comment', with: '~Accepting merge'
- wait_for_requests
+ wait_for_requests
- expect(find_autocomplete_menu).to have_text('Accepting merge requests')
- end
+ expect(find_autocomplete_menu).to have_text('Accepting merge requests')
+ end
- it 'only autocompletes the latest label' do
- create(:label, project: project, title: 'Accepting merge requests')
- create(:label, project: project, title: 'Accepting job applicants')
+ it 'only autocompletes the latest label' do
+ create(:label, project: project, title: 'Accepting merge requests')
+ create(:label, project: project, title: 'Accepting job applicants')
- fill_in 'Comment', with: '~Accepting merge requests foo bar ~Accepting job'
+ fill_in 'Comment', with: '~Accepting merge requests foo bar ~Accepting job'
- wait_for_requests
+ wait_for_requests
- expect(find_autocomplete_menu).to have_text('Accepting job applicants')
- end
+ expect(find_autocomplete_menu).to have_text('Accepting job applicants')
+ end
- it 'does not autocomplete labels if no tilde is typed' do
- create(:label, project: project, title: 'Accepting merge requests')
+ it 'does not autocomplete labels if no tilde is typed' do
+ create(:label, project: project, title: 'Accepting merge requests')
- fill_in 'Comment', with: 'Accepting merge'
+ fill_in 'Comment', with: 'Accepting merge'
- wait_for_requests
+ wait_for_requests
- expect(page).not_to have_css('.atwho-view')
+ expect(page).not_to have_css('.atwho-view')
+ end
end
- end
- context 'when other notes are destroyed' do
- let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+ context 'when other notes are destroyed' do
+ let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
- # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
- it 'keeps autocomplete key listeners' do
- visit project_issue_path(project, issue)
- note = find_field('Comment')
+ # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
+ it 'keeps autocomplete key listeners' do
+ note = find_field('Comment')
- start_comment_with_emoji(note, '.atwho-view li')
+ start_comment_with_emoji(note, '.atwho-view li')
- start_and_cancel_discussion
+ start_and_cancel_discussion
- note.fill_in(with: '')
- start_comment_with_emoji(note, '.atwho-view li')
- note.native.send_keys(:enter)
+ note.fill_in(with: '')
+ start_comment_with_emoji(note, '.atwho-view li')
+ note.native.send_keys(:enter)
- expect(note.value).to eql('Hello :100: ')
+ expect(note.value).to eql('Hello :100: ')
+ end
end
- end
- shared_examples 'autocomplete suggestions' do
- it 'suggests objects correctly' do
- fill_in 'Comment', with: object.class.reference_prefix
+ shared_examples 'autocomplete suggestions' do
+ it 'suggests objects correctly' do
+ fill_in 'Comment', with: object.class.reference_prefix
- find_autocomplete_menu.find('li').click
+ find_autocomplete_menu.find('li').click
- expect(find_field('Comment').value).to have_text(expected_body)
+ expect(find_field('Comment').value).to have_text(expected_body)
+ end
end
- end
- context 'issues' do
- let(:object) { issue }
- let(:expected_body) { object.to_reference }
+ context 'issues' do
+ let(:object) { issue }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'merge requests' do
- let(:object) { create(:merge_request, source_project: project) }
- let(:expected_body) { object.to_reference }
+ context 'merge requests' do
+ let(:object) { create(:merge_request, source_project: project) }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'project snippets' do
- let!(:object) { create(:project_snippet, project: project, title: 'code snippet') }
- let(:expected_body) { object.to_reference }
+ context 'project snippets' do
+ let!(:object) { snippet }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'label' do
- let!(:object) { label }
- let(:expected_body) { object.title }
+ context 'label' do
+ let!(:object) { label }
+ let(:expected_body) { object.title }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'milestone' do
- let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) }
- let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') }
- let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) }
- let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) }
- let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
+ context 'milestone' do
+ let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) }
+ let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') }
+ let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) }
+ let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) }
+ let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
- before do
- fill_in 'Comment', with: '/milestone %'
+ before do
+ fill_in 'Comment', with: '/milestone %'
- wait_for_requests
- end
+ wait_for_requests
+ end
- it 'shows milestons list in the autocomplete menu' do
- page.within(find_autocomplete_menu) do
- expect(page).to have_selector('li', count: 5)
+ it 'shows milestons list in the autocomplete menu' do
+ page.within(find_autocomplete_menu) do
+ expect(page).to have_selector('li', count: 5)
+ end
end
- end
- it 'shows expired milestone at the bottom of the list' do
- page.within(find_autocomplete_menu) do
- expect(page.find('li:last-child')).to have_content milestone_expired.title
+ it 'shows expired milestone at the bottom of the list' do
+ page.within(find_autocomplete_menu) do
+ expect(page.find('li:last-child')).to have_content milestone_expired.title
+ end
end
- end
- it 'shows milestone due earliest at the top of the list' do
- page.within(find_autocomplete_menu) do
- aggregate_failures do
- expect(page.all('li')[0]).to have_content milestone3.title
- expect(page.all('li')[1]).to have_content milestone2.title
- expect(page.all('li')[2]).to have_content milestone1.title
- expect(page.all('li')[3]).to have_content milestone_no_duedate.title
+ it 'shows milestone due earliest at the top of the list' do
+ page.within(find_autocomplete_menu) do
+ aggregate_failures do
+ expect(page.all('li')[0]).to have_content milestone3.title
+ expect(page.all('li')[1]).to have_content milestone2.title
+ expect(page.all('li')[2]).to have_content milestone1.title
+ expect(page.all('li')[3]).to have_content milestone_no_duedate.title
+ end
end
end
end
@@ -426,16 +435,18 @@ RSpec.describe 'GFM autocomplete', :js do
end
describe 'when tribute_autocomplete feature flag is on' do
- before do
- stub_feature_flags(tribute_autocomplete: true)
+ describe 'issue description' do
+ let(:issue_to_edit) { create(:issue, project: project) }
- sign_in(user)
- visit project_issue_path(project, issue)
+ before do
+ stub_feature_flags(tribute_autocomplete: true)
- wait_for_requests
- end
+ sign_in(user)
+ visit project_issue_path(project, issue_to_edit)
+
+ wait_for_requests
+ end
- describe 'issue description' do
it 'updates with GFM reference' do
click_button 'Edit title and description'
@@ -455,309 +466,306 @@ RSpec.describe 'GFM autocomplete', :js do
end
end
- describe 'triggering autocomplete' do
- it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
- fill_in 'Comment', with: 'testing@'
- expect(page).not_to have_css('.tribute-container')
-
- fill_in 'Comment', with: "hello:#{user.username[0..2]}"
- expect(page).not_to have_css('.tribute-container')
-
- fill_in 'Comment', with: '7:'
- expect(page).not_to have_css('.tribute-container')
-
- fill_in 'Comment', with: 'w:'
- expect(page).not_to have_css('.tribute-container')
+ describe 'issue comment' do
+ before do
+ stub_feature_flags(tribute_autocomplete: true)
- fill_in 'Comment', with: 'Ё:'
- expect(page).not_to have_css('.tribute-container')
+ sign_in(user)
+ visit project_issue_path(project, issue)
- fill_in 'Comment', with: "test\n\n@"
- expect(find_tribute_autocomplete_menu).to be_visible
+ wait_for_requests
end
- end
-
- it 'opens autocomplete menu when field starts with text' do
- fill_in 'Comment', with: '@'
-
- expect(find_tribute_autocomplete_menu).to be_visible
- end
- it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
- issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
- create(:issue, project: project, title: issue_xss_title)
+ describe 'triggering autocomplete' do
+ it 'only opens autocomplete menu when trigger character is after whitespace', :aggregate_failures do
+ fill_in 'Comment', with: 'testing@'
+ expect(page).not_to have_css('.tribute-container')
- fill_in 'Comment', with: '#'
+ fill_in 'Comment', with: "hello:#{user.username[0..2]}"
+ expect(page).not_to have_css('.tribute-container')
- wait_for_requests
+ fill_in 'Comment', with: '7:'
+ expect(page).not_to have_css('.tribute-container')
- expect(find_tribute_autocomplete_menu).to have_text(issue_xss_title)
- end
+ fill_in 'Comment', with: 'w:'
+ expect(page).not_to have_css('.tribute-container')
- it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
- fill_in 'Comment', with: '@ev'
+ fill_in 'Comment', with: 'Ё:'
+ expect(page).not_to have_css('.tribute-container')
- wait_for_requests
+ fill_in 'Comment', with: "test\n\n@"
+ expect(find_tribute_autocomplete_menu).to be_visible
+ end
+ end
- expect(find_tribute_autocomplete_menu).to have_text(user_xss.username)
- end
+ it 'opens autocomplete menu when field starts with text' do
+ fill_in 'Comment', with: '@'
- it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
- milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
- create(:milestone, project: project, title: milestone_xss_title)
+ expect(find_tribute_autocomplete_menu).to be_visible
+ end
- fill_in 'Comment', with: '%'
+ it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
+ issue_xss_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
+ create(:issue, project: project, title: issue_xss_title)
- wait_for_requests
+ fill_in 'Comment', with: '#'
- expect(find_tribute_autocomplete_menu).to have_text('alert milestone')
- end
+ wait_for_requests
- it 'selects the first item for assignee dropdowns' do
- fill_in 'Comment', with: '@'
+ expect(find_tribute_autocomplete_menu).to have_text(issue_xss_title)
+ end
- wait_for_requests
+ it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
+ fill_in 'Comment', with: '@ev'
- expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type')
- end
+ wait_for_requests
- it 'includes items for assignee dropdowns with non-ASCII characters in name' do
- fill_in 'Comment', with: "@#{user.name[0...8]}"
+ expect(find_tribute_autocomplete_menu).to have_text(user_xss.username)
+ end
- wait_for_requests
+ it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
+ milestone_xss_title = 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a'
+ create(:milestone, project: project, title: milestone_xss_title)
- expect(find_tribute_autocomplete_menu).to have_text(user.name)
- end
+ fill_in 'Comment', with: '%'
- it 'selects the first item for non-assignee dropdowns if a query is entered' do
- fill_in 'Comment', with: ':1'
+ wait_for_requests
- wait_for_requests
+ expect(find_tribute_autocomplete_menu).to have_text('alert milestone')
+ end
- expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type')
- end
+ it 'selects the first item for assignee dropdowns' do
+ fill_in 'Comment', with: '@'
- context 'when autocompleting for groups' do
- it 'shows the group when searching for the name of the group' do
- fill_in 'Comment', with: '@mygroup'
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('My group')
+ expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type')
end
- it 'does not show the group when searching for the name of the parent of the group' do
- fill_in 'Comment', with: '@ancestor'
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ fill_in 'Comment', with: "@#{user.name[0...8]}"
- expect(find_tribute_autocomplete_menu).not_to have_text('My group')
+ wait_for_requests
+
+ expect(find_tribute_autocomplete_menu).to have_text(user.name)
end
- end
- context 'if a selected value has special characters' do
- it 'wraps the result in double quotes' do
- fill_in 'Comment', with: "~#{label.title[0]}"
+ it 'selects the first item for non-assignee dropdowns if a query is entered' do
+ fill_in 'Comment', with: ':1'
- find_highlighted_tribute_autocomplete_menu.click
+ wait_for_requests
- expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
+ expect(find_tribute_autocomplete_menu).to have_css('.highlight:first-of-type')
end
- it 'doesn\'t wrap for assignee values' do
- fill_in 'Comment', with: "@#{user.username[0..2]}"
+ context 'when autocompleting for groups' do
+ it 'shows the group when searching for the name of the group' do
+ fill_in 'Comment', with: '@mygroup'
- find_highlighted_tribute_autocomplete_menu.click
+ expect(find_tribute_autocomplete_menu).to have_text('My group')
+ end
+
+ it 'does not show the group when searching for the name of the parent of the group' do
+ fill_in 'Comment', with: '@ancestor'
- expect(find_field('Comment').value).to have_text("@#{user.username}")
+ expect(find_tribute_autocomplete_menu).not_to have_text('My group')
+ end
end
- it 'does not wrap for emoji values' do
- fill_in 'Comment', with: ':cartwheel_'
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ fill_in 'Comment', with: "~#{label.title[0]}"
- find_highlighted_tribute_autocomplete_menu.click
+ find_highlighted_tribute_autocomplete_menu.click
- expect(find_field('Comment').value).to have_text('cartwheel_tone1')
- end
+ expect(find_field('Comment').value).to have_text("~\"#{label.title}\"")
+ end
- it 'autocompletes for quick actions' do
- fill_in 'Comment', with: '/as'
+ it 'doesn\'t wrap for assignee values' do
+ fill_in 'Comment', with: "@#{user.username[0..2]}"
- find_highlighted_tribute_autocomplete_menu.click
+ find_highlighted_tribute_autocomplete_menu.click
- expect(find_field('Comment').value).to have_text('/assign')
- end
- end
+ expect(find_field('Comment').value).to have_text("@#{user.username}")
+ end
- context 'assignees' do
- let(:issue_assignee) { create(:issue, project: project) }
- let(:unassigned_user) { create(:user) }
+ it 'does not wrap for emoji values' do
+ fill_in 'Comment', with: ':cartwheel_'
- before do
- issue_assignee.update(assignees: [user])
+ find_highlighted_tribute_autocomplete_menu.click
- project.add_maintainer(unassigned_user)
- end
+ expect(find_field('Comment').value).to have_text('cartwheel_tone1')
+ end
- it 'lists users who are currently not assigned to the issue when using /assign' do
- visit project_issue_path(project, issue_assignee)
+ it 'autocompletes for quick actions' do
+ fill_in 'Comment', with: '/as'
- note = find_field('Comment')
- note.native.send_keys('/assign ')
- # The `/assign` ajax response might replace the one by `@` below causing a failed test
- # so we need to wait for the `/assign` ajax request to finish first
- wait_for_requests
- note.native.send_keys('@')
- wait_for_requests
+ find_highlighted_tribute_autocomplete_menu.click
- expect(find_tribute_autocomplete_menu).not_to have_text(user.username)
- expect(find_tribute_autocomplete_menu).to have_text(unassigned_user.username)
+ expect(find_field('Comment').value).to have_text('/assign')
+ end
end
- it 'lists users who are currently not assigned to the issue when using /assign on the second line' do
- visit project_issue_path(project, issue_assignee)
+ context 'assignees' do
+ it 'lists users who are currently not assigned to the issue when using /assign' do
+ note = find_field('Comment')
+ note.native.send_keys('/assign ')
+ # The `/assign` ajax response might replace the one by `@` below causing a failed test
+ # so we need to wait for the `/assign` ajax request to finish first
+ wait_for_requests
+ note.native.send_keys('@')
+ wait_for_requests
- note = find_field('Comment')
- note.native.send_keys('/assign @user2')
- note.native.send_keys(:enter)
- note.native.send_keys('/assign ')
- # The `/assign` ajax response might replace the one by `@` below causing a failed test
- # so we need to wait for the `/assign` ajax request to finish first
- wait_for_requests
- note.native.send_keys('@')
- wait_for_requests
+ expect(find_tribute_autocomplete_menu).not_to have_text(user.username)
+ expect(find_tribute_autocomplete_menu).to have_text(user2.username)
+ end
- expect(find_tribute_autocomplete_menu).not_to have_text(user.username)
- expect(find_tribute_autocomplete_menu).to have_text(unassigned_user.username)
+ it 'lists users who are currently not assigned to the issue when using /assign on the second line' do
+ note = find_field('Comment')
+ note.native.send_keys('/assign @user2')
+ note.native.send_keys(:enter)
+ note.native.send_keys('/assign ')
+ # The `/assign` ajax response might replace the one by `@` below causing a failed test
+ # so we need to wait for the `/assign` ajax request to finish first
+ wait_for_requests
+ note.native.send_keys('@')
+ wait_for_requests
+
+ expect(find_tribute_autocomplete_menu).not_to have_text(user.username)
+ expect(find_tribute_autocomplete_menu).to have_text(user2.username)
+ end
end
- end
- context 'labels' do
- it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
- label_xss_title = 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a'
- create(:label, project: project, title: label_xss_title)
+ context 'labels' do
+ it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
+ label_xss_title = 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a'
+ create(:label, project: project, title: label_xss_title)
- fill_in 'Comment', with: '~'
+ fill_in 'Comment', with: '~'
- wait_for_requests
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('alert label')
- end
+ expect(find_tribute_autocomplete_menu).to have_text('alert label')
+ end
- it 'allows colons when autocompleting scoped labels' do
- create(:label, project: project, title: 'scoped:label')
+ it 'allows colons when autocompleting scoped labels' do
+ create(:label, project: project, title: 'scoped:label')
- fill_in 'Comment', with: '~scoped:'
+ fill_in 'Comment', with: '~scoped:'
- wait_for_requests
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('scoped:label')
- end
+ expect(find_tribute_autocomplete_menu).to have_text('scoped:label')
+ end
- it 'allows colons when autocompleting scoped labels with double colons' do
- create(:label, project: project, title: 'scoped::label')
+ it 'allows colons when autocompleting scoped labels with double colons' do
+ create(:label, project: project, title: 'scoped::label')
- fill_in 'Comment', with: '~scoped::'
+ fill_in 'Comment', with: '~scoped::'
- wait_for_requests
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('scoped::label')
- end
+ expect(find_tribute_autocomplete_menu).to have_text('scoped::label')
+ end
- it 'autocompletes multi-word labels' do
- create(:label, project: project, title: 'Accepting merge requests')
+ it 'autocompletes multi-word labels' do
+ create(:label, project: project, title: 'Accepting merge requests')
- fill_in 'Comment', with: '~Acceptingmerge'
+ fill_in 'Comment', with: '~Acceptingmerge'
- wait_for_requests
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('Accepting merge requests')
- end
+ expect(find_tribute_autocomplete_menu).to have_text('Accepting merge requests')
+ end
- it 'only autocompletes the latest label' do
- create(:label, project: project, title: 'documentation')
- create(:label, project: project, title: 'feature')
+ it 'only autocompletes the latest label' do
+ create(:label, project: project, title: 'documentation')
+ create(:label, project: project, title: 'feature')
- fill_in 'Comment', with: '~documentation foo bar ~feat'
- # Invoke autocompletion
- find_field('Comment').native.send_keys(:right)
+ fill_in 'Comment', with: '~documentation foo bar ~feat'
+ # Invoke autocompletion
+ find_field('Comment').native.send_keys(:right)
- wait_for_requests
+ wait_for_requests
- expect(find_tribute_autocomplete_menu).to have_text('feature')
- expect(find_tribute_autocomplete_menu).not_to have_text('documentation')
- end
+ expect(find_tribute_autocomplete_menu).to have_text('feature')
+ expect(find_tribute_autocomplete_menu).not_to have_text('documentation')
+ end
- it 'does not autocomplete labels if no tilde is typed' do
- create(:label, project: project, title: 'documentation')
+ it 'does not autocomplete labels if no tilde is typed' do
+ create(:label, project: project, title: 'documentation')
- fill_in 'Comment', with: 'document'
+ fill_in 'Comment', with: 'document'
- wait_for_requests
+ wait_for_requests
- expect(page).not_to have_css('.tribute-container')
+ expect(page).not_to have_css('.tribute-container')
+ end
end
- end
- context 'when other notes are destroyed' do
- let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+ context 'when other notes are destroyed' do
+ let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
- # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
- it 'keeps autocomplete key listeners' do
- visit project_issue_path(project, issue)
- note = find_field('Comment')
+ # This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
+ it 'keeps autocomplete key listeners' do
+ note = find_field('Comment')
- start_comment_with_emoji(note, '.tribute-container li')
+ start_comment_with_emoji(note, '.tribute-container li')
- start_and_cancel_discussion
+ start_and_cancel_discussion
- note.fill_in(with: '')
- start_comment_with_emoji(note, '.tribute-container li')
- note.native.send_keys(:enter)
+ note.fill_in(with: '')
+ start_comment_with_emoji(note, '.tribute-container li')
+ note.native.send_keys(:enter)
- expect(note.value).to eql('Hello :100: ')
+ expect(note.value).to eql('Hello :100: ')
+ end
end
- end
- shared_examples 'autocomplete suggestions' do
- it 'suggests objects correctly' do
- fill_in 'Comment', with: object.class.reference_prefix
+ shared_examples 'autocomplete suggestions' do
+ it 'suggests objects correctly' do
+ fill_in 'Comment', with: object.class.reference_prefix
- find_tribute_autocomplete_menu.find('li').click
+ find_tribute_autocomplete_menu.find('li').click
- expect(find_field('Comment').value).to have_text(expected_body)
+ expect(find_field('Comment').value).to have_text(expected_body)
+ end
end
- end
- context 'issues' do
- let(:object) { issue }
- let(:expected_body) { object.to_reference }
+ context 'issues' do
+ let(:object) { issue }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'merge requests' do
- let(:object) { create(:merge_request, source_project: project) }
- let(:expected_body) { object.to_reference }
+ context 'merge requests' do
+ let(:object) { create(:merge_request, source_project: project) }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'project snippets' do
- let!(:object) { create(:project_snippet, project: project, title: 'code snippet') }
- let(:expected_body) { object.to_reference }
+ context 'project snippets' do
+ let!(:object) { snippet }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'label' do
- let!(:object) { label }
- let(:expected_body) { object.title }
+ context 'label' do
+ let!(:object) { label }
+ let(:expected_body) { object.title }
- it_behaves_like 'autocomplete suggestions'
- end
+ it_behaves_like 'autocomplete suggestions'
+ end
- context 'milestone' do
- let!(:object) { create(:milestone, project: project) }
- let(:expected_body) { object.to_reference }
+ context 'milestone' do
+ let!(:object) { create(:milestone, project: project) }
+ let(:expected_body) { object.to_reference }
- it_behaves_like 'autocomplete suggestions'
+ it_behaves_like 'autocomplete suggestions'
+ end
end
end
diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js
index 3702f55f17b..3b26ca57d6f 100644
--- a/spec/frontend/boards/components/board_add_new_column_form_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js
@@ -1,6 +1,6 @@
-import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
+import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
@@ -25,7 +25,7 @@ describe('Board card layout', () => {
const mountComponent = ({
loading = false,
- formDescription = '',
+ noneSelected = '',
searchLabel = '',
searchPlaceholder = '',
selectedId,
@@ -34,12 +34,9 @@ describe('Board card layout', () => {
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumnForm, {
- stubs: {
- GlFormGroup: true,
- },
propsData: {
loading,
- formDescription,
+ noneSelected,
searchLabel,
searchPlaceholder,
selectedId,
@@ -51,13 +48,15 @@ describe('Board card layout', () => {
...actions,
},
}),
+ stubs: {
+ GlDropdown,
+ },
}),
);
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
@@ -65,10 +64,13 @@ describe('Board card layout', () => {
const findSearchLabel = () => wrapper.find(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
it('shows form title & search input', () => {
mountComponent();
+ findDropdown().vm.$emit('show');
+
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
expect(findSearchInput().exists()).toBe(true);
});
@@ -86,16 +88,6 @@ describe('Board card layout', () => {
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
- it('sets placeholder and description from props', () => {
- const props = {
- formDescription: 'Some description of a list',
- };
-
- mountComponent(props);
-
- expect(wrapper.html()).toHaveText(props.formDescription);
- });
-
describe('items', () => {
const mountWithItems = (loading) =>
mountComponent({
@@ -151,13 +143,11 @@ describe('Board card layout', () => {
expect(submitButton().props('disabled')).toBe(true);
});
- it('emits add-list event on click', async () => {
+ it('emits add-list event on click', () => {
mountComponent({
selectedId: mockLabelList.label.id,
});
- await nextTick();
-
submitButton().vm.$emit('click');
expect(wrapper.emitted('add-list')).toEqual([[]]);
diff --git a/spec/frontend/cycle_analytics/total_time_component_spec.js b/spec/frontend/cycle_analytics/total_time_component_spec.js
index 0f7f2628aca..e831bc311ed 100644
--- a/spec/frontend/cycle_analytics/total_time_component_spec.js
+++ b/spec/frontend/cycle_analytics/total_time_component_spec.js
@@ -1,58 +1,58 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/cycle_analytics/components/total_time_component.vue';
+import { shallowMount } from '@vue/test-utils';
+import TotalTime from '~/cycle_analytics/components/total_time_component.vue';
describe('Total time component', () => {
- let vm;
- let Component;
+ let wrapper;
- beforeEach(() => {
- Component = Vue.extend(component);
- });
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(TotalTime, {
+ propsData,
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('With data', () => {
it('should render information for days and hours', () => {
- vm = mountComponent(Component, {
+ createComponent({
time: {
days: 3,
hours: 4,
},
});
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('3 days 4 hrs');
+ expect(wrapper.text()).toMatchInterpolatedText('3 days 4 hrs');
});
it('should render information for hours and minutes', () => {
- vm = mountComponent(Component, {
+ createComponent({
time: {
hours: 4,
mins: 35,
},
});
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('4 hrs 35 mins');
+ expect(wrapper.text()).toMatchInterpolatedText('4 hrs 35 mins');
});
it('should render information for seconds', () => {
- vm = mountComponent(Component, {
+ createComponent({
time: {
seconds: 45,
},
});
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('45 s');
+ expect(wrapper.text()).toMatchInterpolatedText('45 s');
});
});
describe('Without data', () => {
it('should render no information', () => {
- vm = mountComponent(Component);
+ createComponent();
- expect(vm.$el.textContent.trim()).toEqual('--');
+ expect(wrapper.text()).toBe('--');
});
});
});
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 6ddd047d549..a58712f2fec 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -1,24 +1,40 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
+import { sortOrders } from '~/performance_bar/constants';
describe('detailedMetric', () => {
let wrapper;
- const createComponent = (props) => {
- wrapper = shallowMount(DetailedMetric, {
- propsData: {
- ...props,
- },
- });
+ const defaultProps = {
+ currentRequest: {},
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ keys: ['feature', 'request'],
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(DetailedMetric, {
+ propsData: { ...defaultProps, ...props },
+ }),
+ );
};
const findAllTraceBlocks = () => wrapper.findAll('pre');
const findTraceBlockAtIndex = (index) => findAllTraceBlocks().at(index);
- const findExpandBacktraceBtns = () => wrapper.findAll('[data-testid="backtrace-expand-btn"]');
+ const findExpandBacktraceBtns = () => wrapper.findAllByTestId('backtrace-expand-btn');
const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index);
+ const findDetailsLabel = () => wrapper.findByTestId('performance-bar-details-label');
+ const findSortOrderSwitcher = () => wrapper.findByTestId('performance-bar-sort-order');
+ const findEmptyDetailNotice = () => wrapper.findByTestId('performance-bar-empty-detail-notice');
+ const findAllDetailDurations = () =>
+ wrapper.findAllByTestId('performance-item-duration').wrappers.map((w) => w.text());
+ const findAllSummaryItems = () =>
+ wrapper.findAllByTestId('performance-bar-summary-item').wrappers.map((w) => w.text());
afterEach(() => {
wrapper.destroy();
@@ -26,13 +42,7 @@ describe('detailedMetric', () => {
describe('when the current request has no details', () => {
beforeEach(() => {
- createComponent({
- currentRequest: {},
- metric: 'gitaly',
- header: 'Gitaly calls',
- details: 'details',
- keys: ['feature', 'request'],
- });
+ createComponent();
});
it('does not render the element', () => {
@@ -42,36 +52,104 @@ describe('detailedMetric', () => {
describe('when the current request has details', () => {
const requestDetails = [
- { duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
{
- duration: '23',
+ duration: 23,
feature: 'rebase_in_progress',
request: '',
backtrace: ['other', 'example'],
},
+ { duration: 100, feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
];
- describe('with a default metric name', () => {
+ describe('with an empty detail', () => {
+ beforeEach(() => {
+ createComponent({
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '0ms',
+ calls: 0,
+ details: [],
+ warnings: [],
+ },
+ },
+ },
+ });
+ });
+
+ it('displays an empty title', () => {
+ expect(findDetailsLabel().text()).toBe('0');
+ });
+
+ it('displays an empty modal', () => {
+ expect(findEmptyDetailNotice().text()).toContain('No gitaly calls for this request');
+ });
+
+ it('does not display sort by switcher', () => {
+ expect(findSortOrderSwitcher().exists()).toBe(false);
+ });
+ });
+
+ describe('when the details have a summary field', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
- calls: '456',
+ calls: 456,
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
+ summary: {
+ 'In controllers': 100,
+ 'In middlewares': 20,
+ },
},
},
},
- metric: 'gitaly',
- header: 'Gitaly calls',
- keys: ['feature', 'request'],
});
});
- it('displays details', () => {
- expect(wrapper.text().replace(/\s+/g, ' ')).toContain('123ms / 456');
+ it('displays a summary section', () => {
+ expect(findAllSummaryItems()).toEqual([
+ 'Total 456',
+ 'Total duration 123ms',
+ 'In controllers 100',
+ 'In middlewares 20',
+ ]);
+ });
+ });
+
+ describe("when the details don't have a start field", () => {
+ beforeEach(() => {
+ createComponent({
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: 456,
+ details: requestDetails,
+ warnings: ['gitaly calls: 456 over 30'],
+ },
+ },
+ },
+ });
+ });
+
+ it('displays details header', () => {
+ expect(findDetailsLabel().text()).toBe('123ms / 456');
+ });
+
+ it('displays a basic summary section', () => {
+ expect(findAllSummaryItems()).toEqual(['Total 456', 'Total duration 123ms']);
+ });
+
+ it('sorts the details by descending duration order', () => {
+ expect(findAllDetailDurations()).toEqual(['100ms', '23ms']);
+ });
+
+ it('does not display sort by switcher', () => {
+ expect(findSortOrderSwitcher().exists()).toBe(false);
});
it('adds a modal with a table of the details', () => {
@@ -119,17 +197,75 @@ describe('detailedMetric', () => {
findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
await nextTick();
expect(findAllTraceBlocks()).toHaveLength(1);
- expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]);
+ expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]);
secondExpandButton.vm.$emit('click');
await nextTick();
expect(findAllTraceBlocks()).toHaveLength(2);
- expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[1].backtrace[0]);
+ expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[0].backtrace[0]);
secondExpandButton.vm.$emit('click');
await nextTick();
expect(findAllTraceBlocks()).toHaveLength(1);
- expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]);
+ expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]);
+ });
+ });
+
+ describe('when the details have a start field', () => {
+ const requestDetailsWithStart = [
+ {
+ start: '2021-03-18 11:41:49.846356 +0700',
+ duration: 23,
+ feature: 'rebase_in_progress',
+ request: '',
+ },
+ {
+ start: '2021-03-18 11:42:11.645711 +0700',
+ duration: 75,
+ feature: 'find_commit',
+ request: 'abcdef',
+ },
+ {
+ start: '2021-03-18 11:42:10.645711 +0700',
+ duration: 100,
+ feature: 'find_commit',
+ request: 'abcdef',
+ },
+ ];
+
+ beforeEach(() => {
+ createComponent({
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: 456,
+ details: requestDetailsWithStart,
+ warnings: ['gitaly calls: 456 over 30'],
+ },
+ },
+ },
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ keys: ['feature', 'request'],
+ });
+ });
+
+ it('sorts the details by descending duration order', () => {
+ expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
+ });
+
+ it('displays sort by switcher', () => {
+ expect(findSortOrderSwitcher().exists()).toBe(true);
+ });
+
+ it('allows switch sorting orders', async () => {
+ findSortOrderSwitcher().vm.$emit('input', sortOrders.CHRONOLOGICAL);
+ await nextTick();
+ expect(findAllDetailDurations()).toEqual(['23ms', '100ms', '75ms']);
+ findSortOrderSwitcher().vm.$emit('input', sortOrders.DURATION);
+ await nextTick();
+ expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
});
});
@@ -145,10 +281,7 @@ describe('detailedMetric', () => {
},
},
},
- metric: 'gitaly',
title: 'custom',
- header: 'Gitaly calls',
- keys: ['feature', 'request'],
});
});
@@ -156,31 +289,39 @@ describe('detailedMetric', () => {
expect(wrapper.text()).toContain('custom');
});
});
- });
- describe('when the details has no duration', () => {
- beforeEach(() => {
- createComponent({
- currentRequest: {
- details: {
- bullet: {
- calls: '456',
- details: [{ notification: 'notification', backtrace: 'backtrace' }],
+ describe('when the details has no duration', () => {
+ beforeEach(() => {
+ createComponent({
+ currentRequest: {
+ details: {
+ bullet: {
+ calls: '456',
+ details: [{ notification: 'notification', backtrace: 'backtrace' }],
+ },
},
},
- },
- metric: 'bullet',
- header: 'Bullet notifications',
- keys: ['notification'],
+ metric: 'bullet',
+ header: 'Bullet notifications',
+ keys: ['notification'],
+ });
+ });
+
+ it('displays calls in the label', () => {
+ expect(findDetailsLabel().text()).toBe('456');
+ });
+
+ it('displays a basic summary section', () => {
+ expect(findAllSummaryItems()).toEqual(['Total 456']);
});
- });
- it('renders only the number of calls', async () => {
- expect(trimText(wrapper.text())).toEqual('456 notification bullet');
+ it('renders only the number of calls', async () => {
+ expect(trimText(wrapper.text())).toContain('notification bullet');
- findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
- await nextTick();
- expect(trimText(wrapper.text())).toEqual('456 notification backtrace bullet');
+ findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
+ await nextTick();
+ expect(trimText(wrapper.text())).toContain('notification backtrace bullet');
+ });
});
});
});
diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb
index dad5a2bf461..9eeeca4de61 100644
--- a/spec/lib/peek/views/active_record_spec.rb
+++ b/spec/lib/peek/views/active_record_spec.rb
@@ -5,14 +5,16 @@ require 'spec_helper'
RSpec.describe Peek::Views::ActiveRecord, :request_store do
subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } }
- let(:connection) { double(:connection) }
+ let(:connection_1) { double(:connection) }
+ let(:connection_2) { double(:connection) }
+ let(:connection_3) { double(:connection) }
let(:event_1) do
{
name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10',
cached: false,
- connection: connection
+ connection: connection_1
}
end
@@ -21,7 +23,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10',
cached: true,
- connection: connection
+ connection: connection_2
}
end
@@ -30,12 +32,15 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL',
sql: 'UPDATE users SET admin = true WHERE id = 10',
cached: false,
- connection: connection
+ connection: connection_3
}
end
before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
+ allow(connection_1).to receive(:transaction_open?).and_return(false)
+ allow(connection_2).to receive(:transaction_open?).and_return(false)
+ allow(connection_3).to receive(:transaction_open?).and_return(true)
end
it 'subscribes and store data into peek views' do
@@ -46,22 +51,32 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
end
expect(subject.results).to match(
- calls: '3 (1 cached)',
+ calls: 3,
+ summary: {
+ "Cached" => 1,
+ "In a transaction" => 1
+ },
duration: '6000.00ms',
warnings: ["active-record duration: 6000.0 over 3000"],
details: contain_exactly(
a_hash_including(
+ start: be_a(Time),
cached: '',
+ transaction: '',
duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10'
),
a_hash_including(
- cached: 'cached',
+ start: be_a(Time),
+ cached: 'Cached',
+ transaction: '',
duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10'
),
a_hash_including(
+ start: be_a(Time),
cached: '',
+ transaction: 'In a transaction',
duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10'
)
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 01da379e001..88496c74e44 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -870,4 +870,23 @@ RSpec.describe CommitStatus do
it { is_expected.to eq(false) }
end
end
+
+ describe '#update_older_statuses_retried!' do
+ let!(:build_old) { create_status(name: 'build') }
+ let!(:build_new) { create_status(name: 'build') }
+ let!(:test) { create_status(name: 'test') }
+ let!(:build_from_other_pipeline) do
+ new_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id)
+ create_status(name: 'build', pipeline: new_pipeline)
+ end
+
+ it "updates 'retried' and 'status' columns of the latest status with the same name in the same pipeline" do
+ build_new.update_older_statuses_retried!
+
+ expect(build_new.reload).to have_attributes(retried: false, processed: false)
+ expect(build_old.reload).to have_attributes(retried: true, processed: true)
+ expect(test.reload).to have_attributes(retried: false, processed: false)
+ expect(build_from_other_pipeline.reload).to have_attributes(retried: false, processed: false)
+ end
+ end
end
diff --git a/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb b/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb
new file mode 100644
index 00000000000..1ceff0dd681
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../../rubocop/cop/gitlab/delegate_predicate_methods'
+
+RSpec.describe RuboCop::Cop::Gitlab::DelegatePredicateMethods do
+ subject(:cop) { described_class.new }
+
+ it 'registers offense for single predicate method with allow_nil:true' do
+ expect_offense(<<~SOURCE)
+ delegate :is_foo?, :do_foo, to: :bar, allow_nil: true
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using `delegate` with `allow_nil` on the following predicate methods is discouraged: is_foo?.
+ SOURCE
+ end
+
+ it 'registers offense for multiple predicate methods with allow_nil:true' do
+ expect_offense(<<~SOURCE)
+ delegate :is_foo?, :is_bar?, to: :bar, allow_nil: true
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using `delegate` with `allow_nil` on the following predicate methods is discouraged: is_foo?, is_bar?.
+ SOURCE
+ end
+
+ it 'registers no offense for non-predicate method with allow_nil:true' do
+ expect_no_offenses(<<~SOURCE)
+ delegate :do_foo, to: :bar, allow_nil: true
+ SOURCE
+ end
+
+ it 'registers no offense with predicate method with allow_nil:false' do
+ expect_no_offenses(<<~SOURCE)
+ delegate :is_foo?, to: :bar, allow_nil: false
+ SOURCE
+ end
+
+ it 'registers no offense with predicate method without allow_nil' do
+ expect_no_offenses(<<~SOURCE)
+ delegate :is_foo?, to: :bar
+ SOURCE
+ end
+end
diff --git a/vendor/assets/javascripts/jquery.endless-scroll.js b/vendor/assets/javascripts/jquery.endless-scroll.js
index f022d9a5d06..fe0fe5b5a8e 100644
--- a/vendor/assets/javascripts/jquery.endless-scroll.js
+++ b/vendor/assets/javascripts/jquery.endless-scroll.js
@@ -19,7 +19,6 @@
* // using some custom options
* $(document).endlessScroll({
* fireOnce: false,
- * fireDelay: false,
* loader: "<div class=\"loading\"><div>",
* callback: function(){
* alert("test");
@@ -30,7 +29,6 @@
*
* bottomPixels integer the number of pixels from the bottom of the page that triggers the event
* fireOnce boolean only fire once until the execution of the current event is completed
- * fireDelay integer delay the subsequent firing, in milliseconds, 0 or false to disable delay
* loader string the HTML to be displayed during loading
* data string|function plain HTML data, can be either a string or a function that returns a string,
* when passed as a function it accepts one argument: fire sequence (the number
@@ -55,7 +53,6 @@
var defaults = {
bottomPixels : 50,
fireOnce : true,
- fireDelay : 150,
loader : "<br />Loading...<br />",
data : "",
insertAfter : "div:last",
@@ -102,21 +99,11 @@
data = typeof options.data == 'function' ? options.data.apply(this, [fireSequence]) : options.data;
if (data !== false) {
- $(options.insertAfter).after("<div id=\"endless_scroll_data\">" + data + "</div>");
- $("#endless_scroll_data").hide().fadeIn(250, function() {$(this).removeAttr("id");});
+ $(options.insertAfter).after("<div>" + data + "</div>");
options.callback.apply(this, [fireSequence]);
- if (options.fireDelay !== false || options.fireDelay !== 0) {
- $("body").after("<div id=\"endless_scroll_marker\"></div>");
- // slight delay for preventing event firing twice
- $("#endless_scroll_marker").fadeTo(options.fireDelay, 1, function() {
- $(this).remove();
- fired = false;
- });
- }
- else
- fired = false;
+ fired = false;
}
$("#endless_scroll_loader").remove();