Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-12 06:11:31 +00:00
parent 129d7ea3db
commit acc3d48da4
123 changed files with 1506 additions and 400 deletions

View File

@ -3,70 +3,6 @@
Style/PercentLiteralDelimiters:
Exclude:
- 'metrics_server/metrics_server.rb'
- 'spec/graphql/mutations/alert_management/update_alert_status_spec.rb'
- 'spec/graphql/mutations/ci/runner/update_spec.rb'
- 'spec/graphql/mutations/commits/create_spec.rb'
- 'spec/graphql/resolvers/board_lists_resolver_spec.rb'
- 'spec/graphql/resolvers/container_repository_tags_resolver_spec.rb'
- 'spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb'
- 'spec/graphql/resolvers/projects_resolver_spec.rb'
- 'spec/graphql/types/boards/board_issue_input_type_spec.rb'
- 'spec/graphql/types/design_management/design_collection_copy_state_enum_spec.rb'
- 'spec/graphql/types/issue_type_spec.rb'
- 'spec/helpers/appearances_helper_spec.rb'
- 'spec/helpers/application_settings_helper_spec.rb'
- 'spec/helpers/auth_helper_spec.rb'
- 'spec/helpers/breadcrumbs_helper_spec.rb'
- 'spec/helpers/ci/pipelines_helper_spec.rb'
- 'spec/helpers/clusters_helper_spec.rb'
- 'spec/helpers/diff_helper_spec.rb'
- 'spec/helpers/emails_helper_spec.rb'
- 'spec/helpers/issuables_description_templates_helper_spec.rb'
- 'spec/helpers/issues_helper_spec.rb'
- 'spec/helpers/nav_helper_spec.rb'
- 'spec/helpers/page_layout_helper_spec.rb'
- 'spec/helpers/profiles_helper_spec.rb'
- 'spec/helpers/releases_helper_spec.rb'
- 'spec/helpers/tracking_helper_spec.rb'
- 'spec/initializers/direct_upload_support_spec.rb'
- 'spec/initializers/enumerator_next_patch_spec.rb'
- 'spec/initializers/rack_multipart_patch_spec.rb'
- 'spec/lib/api/ci/helpers/runner_helpers_spec.rb'
- 'spec/lib/api/entities/user_spec.rb'
- 'spec/lib/api/helpers/common_helpers_spec.rb'
- 'spec/lib/backup/files_spec.rb'
- 'spec/lib/backup/manager_spec.rb'
- 'spec/lib/backup/repositories_spec.rb'
- 'spec/lib/banzai/filter/asset_proxy_filter_spec.rb'
- 'spec/lib/banzai/filter/autolink_filter_spec.rb'
- 'spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb'
- 'spec/lib/banzai/filter/image_link_filter_spec.rb'
- 'spec/lib/banzai/filter/references/alert_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/commit_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/design_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/issue_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/label_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/project_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/references/user_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/syntax_highlight_filter_spec.rb'
- 'spec/lib/banzai/filter_array_spec.rb'
- 'spec/lib/banzai/pipeline/description_pipeline_spec.rb'
- 'spec/lib/banzai/pipeline/full_pipeline_spec.rb'
- 'spec/lib/banzai/pipeline/gfm_pipeline_spec.rb'
- 'spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb'
- 'spec/lib/banzai/reference_parser/base_parser_spec.rb'
- 'spec/lib/banzai/reference_parser/issue_parser_spec.rb'
- 'spec/lib/banzai/reference_parser/merge_request_parser_spec.rb'
- 'spec/lib/bitbucket/collection_spec.rb'
- 'spec/lib/bitbucket/representation/repo_spec.rb'
- 'spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
- 'spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb'
- 'spec/lib/gitlab/alert_management/payload/base_spec.rb'
- 'spec/lib/gitlab/asset_proxy_spec.rb'
- 'spec/lib/gitlab/auth/ldap/auth_hash_spec.rb'
@ -186,7 +122,6 @@ Style/PercentLiteralDelimiters:
- 'spec/lib/gitlab/popen_spec.rb'
- 'spec/lib/gitlab/process_management_spec.rb'
- 'spec/lib/gitlab/process_supervisor_spec.rb'
- 'spec/lib/gitlab/prometheus/query_variables_spec.rb'
- 'spec/lib/gitlab/quick_actions/extractor_spec.rb'
- 'spec/lib/gitlab/reference_extractor_spec.rb'
- 'spec/lib/gitlab/repository_cache_adapter_spec.rb'
@ -261,7 +196,6 @@ Style/PercentLiteralDelimiters:
- 'spec/models/diff_viewer/base_spec.rb'
- 'spec/models/environment_spec.rb'
- 'spec/models/group_label_spec.rb'
- 'spec/models/group_spec.rb'
- 'spec/models/instance_configuration_spec.rb'
- 'spec/models/integration_spec.rb'
- 'spec/models/integrations/buildkite_spec.rb'
@ -279,7 +213,6 @@ Style/PercentLiteralDelimiters:
- 'spec/models/project_feature_spec.rb'
- 'spec/models/project_label_spec.rb'
- 'spec/models/project_spec.rb'
- 'spec/models/project_team_spec.rb'
- 'spec/models/projects/topic_spec.rb'
- 'spec/models/prometheus_metric_spec.rb'
- 'spec/models/releases/link_spec.rb'
@ -320,13 +253,11 @@ Style/PercentLiteralDelimiters:
- 'spec/requests/api/graphql/project/terraform/state_spec.rb'
- 'spec/requests/api/graphql/project/terraform/states_spec.rb'
- 'spec/requests/api/internal/base_spec.rb'
- 'spec/requests/api/invitations_spec.rb'
- 'spec/requests/api/issues/get_group_issues_spec.rb'
- 'spec/requests/api/issues/get_project_issues_spec.rb'
- 'spec/requests/api/issues/issues_spec.rb'
- 'spec/requests/api/issues/post_projects_issues_spec.rb'
- 'spec/requests/api/issues/put_projects_issues_spec.rb'
- 'spec/requests/api/members_spec.rb'
- 'spec/requests/api/merge_requests_spec.rb'
- 'spec/requests/api/metadata_spec.rb'
- 'spec/requests/api/project_container_repositories_spec.rb'
@ -377,8 +308,6 @@ Style/PercentLiteralDelimiters:
- 'spec/services/issues/export_csv_service_spec.rb'
- 'spec/services/jira/requests/projects/list_service_spec.rb'
- 'spec/services/lfs/file_transformer_spec.rb'
- 'spec/services/members/create_service_spec.rb'
- 'spec/services/members/invite_service_spec.rb'
- 'spec/services/merge_requests/conflicts/resolve_service_spec.rb'
- 'spec/services/merge_requests/merge_service_spec.rb'
- 'spec/services/merge_requests/pushed_branches_service_spec.rb'
@ -422,7 +351,6 @@ Style/PercentLiteralDelimiters:
- 'spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb'
- 'spec/support/shared_examples/models/application_setting_shared_examples.rb'
- 'spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb'
- 'spec/support/shared_examples/models/member_shared_examples.rb'
- 'spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb'
- 'spec/support/shared_examples/models/wiki_shared_examples.rb'
- 'spec/support/shared_examples/path_extraction_shared_examples.rb'

View File

@ -56,6 +56,7 @@ export default {
'hasNoAccessError',
'groupPath',
'namespace',
'predefinedDateRange',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
isLoaded() {
@ -132,6 +133,7 @@ export default {
'fetchStageData',
'setSelectedStage',
'setDateRange',
'setPredefinedDateRange',
'updateStageTablePagination',
]),
onSetDateRange({ startDate, endDate }) {
@ -170,7 +172,9 @@ export default {
:start-date="createdAfter"
:end-date="createdBefore"
:group-path="groupPath"
:predefined-date-range="predefinedDateRange"
@setDateRange="onSetDateRange"
@setPredefinedDateRange="setPredefinedDateRange"
/>
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
<path-navigation

View File

@ -2,7 +2,17 @@
import { GlTooltipDirective } from '@gitlab/ui';
import DateRange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants';
import {
DATE_RANGE_LIMIT,
DATE_RANGE_CUSTOM_VALUE,
PROJECTS_PER_PAGE,
MAX_DATE_RANGE_TEXT,
DATE_RANGE_LAST_30_DAYS_VALUE,
LAST_30_DAYS,
} from '~/analytics/shared/constants';
import { getCurrentUtcDate, datesMatch } from '~/lib/utils/datetime_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
import FilterBar from './filter_bar.vue';
export default {
@ -11,10 +21,12 @@ export default {
DateRange,
ProjectsDropdownFilter,
FilterBar,
DateRangesDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
selectedProjects: {
type: Array,
@ -31,6 +43,11 @@ export default {
required: false,
default: true,
},
hasPredefinedDateRangesFilter: {
type: Boolean,
required: false,
default: true,
},
namespacePath: {
type: String,
required: true,
@ -49,6 +66,11 @@ export default {
required: false,
default: null,
},
predefinedDateRange: {
type: String,
required: false,
default: null,
},
},
computed: {
projectsQueryParams() {
@ -58,42 +80,104 @@ export default {
};
},
currentDate() {
const now = new Date();
return new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
return getCurrentUtcDate();
},
isDefaultDateRange() {
return datesMatch(this.startDate, LAST_30_DAYS) && datesMatch(this.endDate, this.currentDate);
},
supportsPredefinedDateRanges() {
return this.glFeatures?.vsaPredefinedDateRanges;
},
dateRangeOption() {
const { predefinedDateRange } = this;
if (predefinedDateRange) return predefinedDateRange;
if (!predefinedDateRange && !this.isDefaultDateRange) return DATE_RANGE_CUSTOM_VALUE;
return DATE_RANGE_LAST_30_DAYS_VALUE;
},
isCustomDateRangeSelected() {
return this.dateRangeOption === DATE_RANGE_CUSTOM_VALUE;
},
shouldShowPredefinedDateRanges() {
return this.supportsPredefinedDateRanges && this.hasPredefinedDateRangesFilter;
},
shouldShowDateRangePicker() {
if (this.shouldShowPredefinedDateRanges) {
return this.hasDateRangeFilter && this.isCustomDateRangeSelected;
}
return this.hasDateRangeFilter;
},
maxDateRangeTooltip() {
return this.$options.i18n.maxDateRangeTooltip(this.$options.maxDateRange);
},
shouldShowDateRangeFilters() {
return this.hasDateRangeFilter || this.hasPredefinedDateRangesFilter;
},
shouldShowFilterDropdowns() {
return this.hasProjectFilter || this.shouldShowDateRangeFilters;
},
},
methods: {
onSelectPredefinedDateRange({ value, startDate, endDate }) {
this.$emit('setPredefinedDateRange', value);
this.$emit('setDateRange', { startDate, endDate });
},
onSelectCustomDateRange() {
this.$emit('setPredefinedDateRange', DATE_RANGE_CUSTOM_VALUE);
},
},
multiProjectSelect: true,
maxDateRange: DATE_RANGE_LIMIT,
i18n: {
maxDateRangeTooltip: MAX_DATE_RANGE_TEXT,
},
};
</script>
<template>
<div
class="gl-mt-3 gl-py-2 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
class="gl-mt-3 gl-py-5 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
>
<filter-bar
data-testid="vsa-filter-bar"
class="filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
class="filtered-search-box gl-display-flex gl-border-none"
:namespace-path="namespacePath"
/>
<hr v-if="shouldShowFilterDropdowns" class="gl-my-5" />
<div
v-if="hasDateRangeFilter || hasProjectFilter"
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
v-if="shouldShowFilterDropdowns"
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-gap-5"
>
<div>
<projects-dropdown-filter
v-if="hasProjectFilter"
toggle-classes="gl-max-w-26"
class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
:group-namespace="groupPath"
:query-params="projectsQueryParams"
:multi-select="$options.multiProjectSelect"
:default-projects="selectedProjects"
@selected="$emit('selectProject', $event)"
<projects-dropdown-filter
v-if="hasProjectFilter"
toggle-classes="gl-max-w-26"
class="js-projects-dropdown-filter project-select"
:group-namespace="groupPath"
:query-params="projectsQueryParams"
:multi-select="$options.multiProjectSelect"
:default-projects="selectedProjects"
@selected="$emit('selectProject', $event)"
/>
<div
v-if="shouldShowDateRangeFilters"
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-gap-3"
data-testid="vsa-date-range-filter-container"
>
<date-ranges-dropdown
v-if="shouldShowPredefinedDateRanges"
data-testid="vsa-predefined-date-ranges-dropdown"
:selected="dateRangeOption"
:tooltip="maxDateRangeTooltip"
include-end-date-in-days-selected
:include-custom-date-range-option="hasDateRangeFilter"
@selected="onSelectPredefinedDateRange"
@customDateRangeSelected="onSelectCustomDateRange"
/>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row">
<date-range
v-if="hasDateRangeFilter"
v-if="shouldShowDateRangePicker"
data-testid="vsa-date-range-picker"
:start-date="startDate"
:end-date="endDate"
:max-date="currentDate"

View File

@ -163,6 +163,10 @@ export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore
return dispatch('refetchStageData');
};
export const setPredefinedDateRange = ({ commit }, predefinedDateRange) => {
commit(types.SET_PREDEFINED_DATE_RANGE, predefinedDateRange);
};
export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
if (!stages.length && !stage) {
commit(types.SET_NO_ACCESS_ERROR);

View File

@ -4,6 +4,7 @@ export const SET_LOADING = 'SET_LOADING';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const SET_PREDEFINED_DATE_RANGE = 'SET_PREDEFINED_DATE_RANGE';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_NO_ACCESS_ERROR = 'SET_NO_ACCESS_ERROR';

View File

@ -34,6 +34,9 @@ export default {
state.createdBefore = createdBefore;
state.createdAfter = createdAfter;
},
[types.SET_PREDEFINED_DATE_RANGE](state, predefinedDateRange) {
state.predefinedDateRange = predefinedDateRange;
},
[types.SET_PAGINATION](state, { page, hasNextPage, sort, direction }) {
Vue.set(state, 'pagination', {
page,

View File

@ -32,4 +32,5 @@ export default () => ({
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
predefinedDateRange: null,
});

View File

@ -0,0 +1,131 @@
<script>
import { GlCollapsibleListbox, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { isString } from 'lodash';
import { isValidDate, getDayDifference } from '~/lib/utils/datetime_utility';
import {
DATE_RANGE_CUSTOM_VALUE,
DEFAULT_DATE_RANGE_OPTIONS,
NUMBER_OF_DAYS_SELECTED,
} from '~/analytics/shared/constants';
import { __ } from '~/locale';
export default {
name: 'DateRangesDropdown',
components: {
GlCollapsibleListbox,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
dateRangeOptions: {
type: Array,
required: false,
default: () => DEFAULT_DATE_RANGE_OPTIONS,
validator: (options) =>
options.length &&
options.every(
({ text, value, startDate, endDate }) =>
isString(text) &&
isString(value) &&
isValidDate(startDate) &&
isValidDate(endDate) &&
endDate >= startDate,
),
},
selected: {
type: String,
required: false,
default: '',
},
tooltip: {
type: String,
required: false,
default: '',
},
includeCustomDateRangeOption: {
type: Boolean,
required: false,
default: true,
},
includeEndDateInDaysSelected: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
selectedValue: this.selected || this.dateRangeOptions[0].value,
};
},
computed: {
items() {
const dateRangeOptions = this.dateRangeOptions.map(({ text, value }) => ({ text, value }));
if (!this.includeCustomDateRangeOption) return dateRangeOptions;
return [...dateRangeOptions, this.$options.customDateRangeItem];
},
isCustomDateRangeSelected() {
return this.selectedValue === DATE_RANGE_CUSTOM_VALUE;
},
groupedDateRangeOptionsByValue() {
return this.dateRangeOptions.reduce((acc, { value, startDate, endDate }) => {
acc[value] = { startDate, endDate };
return acc;
}, {});
},
selectedDateRange() {
if (this.isCustomDateRangeSelected) return null;
return this.groupedDateRangeOptionsByValue[this.selectedValue];
},
showDaysSelectedCount() {
return !this.isCustomDateRangeSelected && this.daysSelectedCount;
},
daysSelectedCount() {
const { selectedDateRange } = this;
if (!selectedDateRange) return '';
const { startDate, endDate } = selectedDateRange;
const daysCount = getDayDifference(startDate, endDate);
return this.$options.i18n.daysSelected(
this.includeEndDateInDaysSelected ? daysCount + 1 : daysCount,
);
},
},
methods: {
onSelect(value) {
if (this.isCustomDateRangeSelected) {
this.$emit('customDateRangeSelected');
} else {
this.$emit('selected', { value, ...this.selectedDateRange });
}
},
},
customDateRangeItem: {
text: __('Custom'),
value: DATE_RANGE_CUSTOM_VALUE,
},
i18n: {
daysSelected: NUMBER_OF_DAYS_SELECTED,
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center gl-gap-3">
<gl-collapsible-listbox v-model="selectedValue" :items="items" @select="onSelect" />
<div v-if="showDaysSelectedCount" class="gl-text-gray-500">
<span data-testid="predefined-date-range-days-count">{{ daysSelectedCount }}</span>
<gl-icon v-if="tooltip" v-gl-tooltip class="gl-ml-2" name="information-o" :title="tooltip" />
</div>
</div>
</template>

View File

@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlDaterangePicker } from '@gitlab/ui';
import { n__, __, sprintf } from '~/locale';
import { MAX_DATE_RANGE_TEXT, NUMBER_OF_DAYS_SELECTED } from '~/analytics/shared/constants';
export default {
components: {
@ -46,18 +46,6 @@ export default {
default: false,
},
},
data() {
return {
maxDateRangeTooltip: sprintf(
__(
'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
),
{
maxDateRange: this.maxDateRange,
},
),
};
},
computed: {
dateRange: {
get() {
@ -67,12 +55,19 @@ export default {
this.$emit('change', { startDate, endDate });
},
},
maxDateRangeTooltip() {
return this.$options.i18n.maxDateRangeTooltip(this.maxDateRange);
},
},
methods: {
numberOfDays(daysSelected) {
return n__('1 day selected', '%d days selected', daysSelected);
return this.$options.i18n.daysSelected(daysSelected);
},
},
i18n: {
maxDateRangeTooltip: MAX_DATE_RANGE_TEXT,
daysSelected: NUMBER_OF_DAYS_SELECTED,
},
};
</script>
<template>

View File

@ -1,9 +1,17 @@
import dateFormat, { masks } from '~/lib/dateformat';
import { nDaysBefore, getStartOfDay } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
import {
nDaysBefore,
getStartOfDay,
dayAfter,
getDateInPast,
getCurrentUtcDate,
nWeeksBefore,
} from '~/lib/utils/datetime_utility';
import { s__, __, sprintf, n__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const DATE_RANGE_LIMIT = 180;
export const DEFAULT_DATE_RANGE = 29; // 30 including current date
export const PROJECTS_PER_PAGE = 50;
const { isoDate, mediumDate } = masks;
@ -14,10 +22,63 @@ export const dateFormats = {
month: 'mmmm',
};
const TODAY = getCurrentUtcDate();
const TOMORROW = dayAfter(TODAY, { utc: true });
export const LAST_30_DAYS = getDateInPast(TOMORROW, 30, { utc: true });
const startOfToday = getStartOfDay(new Date(), { utc: true });
const last180Days = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
const lastXDays = __('Last %{days} days');
const lastWeek = nWeeksBefore(TOMORROW, 1, { utc: true });
const last90Days = getDateInPast(TOMORROW, 90, { utc: true });
const last180Days = getDateInPast(TOMORROW, DATE_RANGE_LIMIT, { utc: true });
const mrThroughputStartDate = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
const formatDateParam = (d) => dateFormat(d, dateFormats.isoDate, true);
export const DATE_RANGE_CUSTOM_VALUE = 'custom';
export const DATE_RANGE_LAST_30_DAYS_VALUE = 'last_30_days';
export const DEFAULT_DATE_RANGE_OPTIONS = [
{
text: __('Last week'),
value: 'last_week',
startDate: lastWeek,
endDate: TODAY,
},
{
text: sprintf(lastXDays, { days: 30 }),
value: DATE_RANGE_LAST_30_DAYS_VALUE,
startDate: LAST_30_DAYS,
endDate: TODAY,
},
{
text: sprintf(lastXDays, { days: 90 }),
value: 'last_90_days',
startDate: last90Days,
endDate: TODAY,
},
{
text: sprintf(lastXDays, { days: 180 }),
value: 'last_180_days',
startDate: last180Days,
endDate: TODAY,
},
];
export const MAX_DATE_RANGE_TEXT = (maxDateRange) => {
return sprintf(
__(
'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
),
{
maxDateRange,
},
);
};
export const NUMBER_OF_DAYS_SELECTED = (numDays) => {
return n__('1 day selected', '%d days selected', numDays);
};
export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
export const ISSUES_COMPLETED_TYPE = 'issues_completed';
@ -147,7 +208,7 @@ export const METRIC_TOOLTIPS = {
description: s__('ValueStreamAnalytics|The number of merge requests merged by month.'),
groupLink: '-/analytics/productivity_analytics',
projectLink: `-/analytics/merge_request_analytics?start_date=${formatDateParam(
last180Days,
mrThroughputStartDate,
)}&end_date=${formatDateParam(startOfToday)}`,
docsLink: helpPagePath('user/analytics/merge_request_analytics', {
anchor: 'view-the-number-of-merge-requests-in-a-date-range',

View File

@ -1,28 +1,51 @@
<script>
import { GlSprintf } from '@gitlab/ui';
import { GlIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SnippetDescription from './snippet_description_view.vue';
export default {
components: {
GlIcon,
TimeAgoTooltip,
GlSprintf,
SnippetDescription,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
snippet: {
type: Object,
required: true,
},
},
i18n: {
hiddenTooltip: s__('Snippets|This snippet is hidden because its author has been banned'),
hiddenAriaLabel: __('Hidden'),
},
};
</script>
<template>
<div class="snippet-header limited-header-width">
<h2 class="snippet-title gl-mt-0 mb-3" data-testid="snippet-title-content">
{{ snippet.title }}
</h2>
<div class="gl-display-flex">
<span
v-if="snippet.hidden"
class="gl-bg-orange-50 gl-text-orange-600 gl-h-6 gl-w-6 border-radius-default gl-line-height-24 gl-text-center gl-mr-3 gl-mt-2"
>
<gl-icon
v-gl-tooltip.bottom
name="spam"
:title="$options.i18n.hiddenTooltip"
:aria-label="$options.i18n.hiddenAriaLabel"
/>
</span>
<h2 class="snippet-title gl-mt-0 mb-3" data-testid="snippet-title-content">
{{ snippet.title }}
</h2>
</div>
<snippet-description v-if="snippet.description" :description="snippet.descriptionHtml" />

View File

@ -23,6 +23,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action do
push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
push_licensed_feature(:group_level_analytics_dashboard) if project.licensed_feature_available?(:group_level_analytics_dashboard)
push_frontend_feature_flag(:vsa_predefined_date_ranges, project)
if project.licensed_feature_available?(:cycle_analytics_for_projects)
push_licensed_feature(:cycle_analytics_for_projects)

View File

@ -42,6 +42,7 @@ class SnippetsFinder < UnionFinder
include FinderMethods
include Gitlab::Utils::StrongMemoize
include CreatedAtFilter
include Gitlab::Allowable
attr_reader :current_user, :params
@ -79,6 +80,7 @@ class SnippetsFinder < UnionFinder
snippets = all_snippets
snippets = by_ids(snippets)
snippets = snippets.with_optional_visibility(visibility_from_scope)
snippets = hide_created_by_banned_user(snippets)
end
by_created_at(snippets)
@ -87,7 +89,7 @@ class SnippetsFinder < UnionFinder
def return_all_available_and_permited?
# Currently limited to access_levels `admin` and `auditor`
# See policies/base_policy.rb files for specifics.
params[:all_available] && current_user&.can_read_all_resources?
params[:all_available] && can?(current_user, :read_all_resources)
end
def all_snippets
@ -126,7 +128,7 @@ class SnippetsFinder < UnionFinder
queries = []
queries << personal_snippets unless only_project?
if Ability.allowed?(current_user, :read_cross_project)
if can?(current_user, :read_cross_project)
queries << snippets_of_visible_projects
queries << snippets_of_authorized_projects if current_user
end
@ -207,6 +209,14 @@ class SnippetsFinder < UnionFinder
snippets.id_in(params[:ids])
end
def hide_created_by_banned_user(snippets)
# if admin -> return all snippets, if not-admin -> filter out snippets by banned user
return snippets if can?(current_user, :read_all_resources)
return snippets unless Feature.enabled?(:hide_snippets_of_banned_users)
snippets.without_created_by_banned_user
end
def author
strong_memoize(:author) do
next unless params[:author].present?

View File

@ -13,6 +13,7 @@ query GetSnippetQuery($ids: [SnippetID!]) {
webUrl
httpUrlToRepo
sshUrlToRepo
hidden
blobs {
__typename
hasUnretrievableBlobs

View File

@ -33,6 +33,11 @@ module Types
description: 'Owner of the snippet.',
null: true
field :hidden, GraphQL::Types::Boolean,
description: 'Indicates the snippet is hidden because the author has been banned.',
null: false,
method: :hidden_due_to_author_ban?
field :file_name, GraphQL::Types::String,
description: 'File Name of the snippet.',
null: true

View File

@ -4,6 +4,7 @@
module Ci
module Deployable
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
included do
prepend_mod_with('Ci::Deployable') # rubocop: disable Cop/InjectEnterpriseEditionModule
@ -26,7 +27,7 @@ module Ci
end
# Synchronize Deployment Status
# Please note that the data integirty is not assured because we can't use
# Please note that the data integrity is not assured because we can't use
# a database transaction due to DB decomposition.
after_transition do |job, transition|
next if transition.loopback?
@ -40,13 +41,12 @@ module Ci
end
def outdated_deployment?
strong_memoize(:outdated_deployment) do
deployment_job? &&
project.ci_forward_deployment_enabled? &&
(!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
deployment&.older_than_last_successful_deployment?
end
deployment_job? &&
project.ci_forward_deployment_enabled? &&
(!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
deployment&.older_than_last_successful_deployment?
end
strong_memoize_attr :outdated_deployment?
# Virtual deployment status depending on the environment status.
def deployment_status
@ -114,10 +114,10 @@ module Ci
namespace = options.dig(:environment, :kubernetes, :namespace)
if namespace.present? # rubocop:disable Style/GuardClause
strong_memoize(:expanded_kubernetes_namespace) do
ExpandVariables.expand(namespace, -> { simple_variables })
end
return unless namespace.present?
strong_memoize(:expanded_kubernetes_namespace) do
ExpandVariables.expand(namespace, -> { simple_variables })
end
end
@ -154,12 +154,11 @@ module Ci
end
def environment_status
strong_memoize(:environment_status) do
if has_environment_keyword? && merge_request
EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
end
end
return unless has_environment_keyword? && merge_request
EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
end
strong_memoize_attr :environment_status
def on_stop
options&.dig(:environment, :on_stop)

View File

@ -79,6 +79,10 @@ class Snippet < ApplicationRecord
scope :with_statistics, -> { joins(:statistics) }
scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
scope :without_created_by_banned_user, -> do
where_not_exists(Users::BannedUser.where('snippets.author_id = banned_users.user_id'))
end
attr_mentionable :description
participant :author
@ -365,6 +369,14 @@ class Snippet < ApplicationRecord
def multiple_files?
list_files.size > 1
end
def hidden_due_to_author_ban?
Feature.enabled?(:hide_snippets_of_banned_users) && author_banned?
end
def author_banned?
author.banned?
end
end
Snippet.prepend_mod_with('Snippet')

View File

@ -4,6 +4,7 @@ class PersonalSnippetPolicy < BasePolicy
condition(:public_snippet, scope: :subject) { @subject.public? }
condition(:is_author) { @user && @subject.author == @user }
condition(:internal_snippet, scope: :subject) { @subject.internal? }
condition(:hidden, scope: :subject) { @subject.hidden_due_to_author_ban? }
rule { public_snippet }.policy do
enable :read_snippet
@ -29,5 +30,13 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji
rule { hidden & ~can?(:read_all_resources) }.policy do
prevent :read_snippet
prevent :update_snippet
prevent :admin_snippet
prevent :read_note
prevent :create_note
end
rule { can?(:read_all_resources) }.enable :read_snippet
end

View File

@ -10,6 +10,7 @@ class ProjectSnippetPolicy < BasePolicy
condition(:public_project, scope: :subject) { @subject.project.public? }
condition(:is_author) { @user && @subject.author == @user }
condition(:hidden, scope: :subject) { @subject.hidden_due_to_author_ban? }
# We have to check both project feature visibility and a snippet visibility and take the stricter one
# This will be simplified - check https://gitlab.com/gitlab-org/gitlab-foss/issues/27573
@ -50,6 +51,13 @@ class ProjectSnippetPolicy < BasePolicy
enable :admin_snippet
end
rule { hidden & ~can?(:read_all_resources) }.policy do
prevent :read_snippet
prevent :update_snippet
prevent :admin_snippet
prevent :read_note
end
rule { ~can?(:read_snippet) }.prevent :create_note
end

View File

@ -6,6 +6,9 @@
= link_to gitlab_snippet_path(snippet), class: "title" do
= snippet.title
- if snippet.hidden_due_to_author_ban?
%span{ class: 'has-tooltip gl-bg-orange-50 gl-text-orange-600 border-radius-default gl-p-2', title: s_("Snippets|This snippet is hidden because its author has been banned") }
= sprite_icon('spam', size: '16')
%ul.controls{ data: { testid: 'snippet-file-count-content', qa_snippet_files: snippet.statistics&.file_count } }
%li

View File

@ -0,0 +1,8 @@
---
name: hide_snippets_of_banned_users
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131725
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/425391
milestone: '16.5'
type: development
group: group::anti-abuse
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: vsa_predefined_date_ranges
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131825
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/425317
milestone: '16.5'
type: development
group: group::optimize
default_enabled: false

View File

@ -9,6 +9,7 @@ value_type: number
status: active
time_frame: 28d
data_source: database
instrumentation_class: CountJiraImportsMetric
distribution:
- ce
- ee

View File

@ -9,6 +9,7 @@ value_type: number
status: active
time_frame: 28d
data_source: database
instrumentation_class: CountCsvImportsMetric
distribution:
- ce
- ee

View File

@ -9,6 +9,7 @@ value_type: number
status: active
time_frame: all
data_source: database
instrumentation_class: CountJiraImportsMetric
distribution:
- ce
- ee

View File

@ -9,6 +9,7 @@ value_type: number
status: active
time_frame: all
data_source: database
instrumentation_class: CountCsvImportsMetric
distribution:
- ce
- ee

View File

@ -24,8 +24,8 @@ CREATE TABLE ci_finished_builds
runner_manager_architecture LowCardinality(String) DEFAULT '',
--- Materialized columns
duration Int64 MATERIALIZED age('second', started_at, finished_at),
queueing_duration Int64 MATERIALIZED age('second', queued_at, started_at)
duration Int64 MATERIALIZED age('ms', started_at, finished_at),
queueing_duration Int64 MATERIALIZED age('ms', queued_at, started_at)
--- This table is incomplete, we'll add more fields before starting the data migration
)
ENGINE = ReplacingMergeTree -- Using ReplacingMergeTree just in case we accidentally insert the same data twice

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class SyncForeignKeyForCiStagesPipelineIdBigint < Gitlab::Database::Migration[2.1]
TABLE_NAME = :ci_stages
COLUMN_NAME = :pipeline_id_convert_to_bigint
FK_NAME = :fk_c5ddde695f
def up
validate_foreign_key TABLE_NAME, COLUMN_NAME, name: FK_NAME
end
def down
# Can be safely a no-op if we don't roll back the inconsistent data.
end
end

View File

@ -0,0 +1 @@
f71caebfefb4b4fabbb3f9cc059f6f105ea352da11380901b16c687dcca1e6d4

View File

@ -37484,7 +37484,7 @@ ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_c4b1c1f66e FOREIGN KEY (repository_deleted_event_id) REFERENCES geo_repository_deleted_events(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_stages
ADD CONSTRAINT fk_c5ddde695f FOREIGN KEY (pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_c5ddde695f FOREIGN KEY (pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_c63cbf6c25 FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -262,8 +262,9 @@ Users can also be activated using the [GitLab API](../api/users.md#activate-user
> - Hiding merge requests of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107836) in GitLab 15.8 [with a flag](../administration/feature_flags.md) named `hide_merge_requests_from_banned_users`. Disabled by default.
> - Hiding comments of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112973) in GitLab 15.11 [with a flag](../administration/feature_flags.md) named `hidden_notes`. Disabled by default.
> - Hiding projects of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121488) in GitLab 16.2 [with a flag](../administration/feature_flags.md) named `hide_projects_of_banned_users`. Disabled by default.
> - Hiding snippets of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131725) in GitLab 16.5 [with a flag](../administration/feature_flags.md) named `hide_snippets_of_banned_users`. Disabled by default.
GitLab administrators can ban and unban users. Banned users are blocked, and their projects, issues, merge requests, and comments are hidden.
GitLab administrators can ban and unban users. Banned users are blocked, and their projects, issues, merge requests, snippets, and comments are hidden.
### Ban a user

View File

@ -25042,6 +25042,7 @@ Represents a snippet entry.
| <a id="snippetdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="snippetdiscussions"></a>`discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) |
| <a id="snippetfilename"></a>`fileName` | [`String`](#string) | File Name of the snippet. |
| <a id="snippethidden"></a>`hidden` | [`Boolean!`](#boolean) | Indicates the snippet is hidden because the author has been banned. |
| <a id="snippethttpurltorepo"></a>`httpUrlToRepo` | [`String`](#string) | HTTP URL to the snippet repository. |
| <a id="snippetid"></a>`id` | [`SnippetID!`](#snippetid) | ID of the snippet. |
| <a id="snippetproject"></a>`project` | [`Project`](#project) | Project the snippet is associated with. |

View File

@ -204,6 +204,10 @@ You can change the name of a project environment in your GitLab CI/CD configurat
> - Filtering [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13216) in GitLab 13.3
> - Horizontal stage path [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12196) in 13.0 and [feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/323982) in 13.12
> - Predefined date ranges dropdown list [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/408656/) in GitLab 16.5 [with a flag](../../../administration/feature_flags.md) named `vsa_predefined_date_ranges`. Disabled by default.
FLAG:
On self-managed GitLab, by default the predefined date ranges dropdown list feature is not available. To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `vsa_predefined_date_ranges`. On GitLab.com, this feature is not available. The feature is not ready for production use.
Prerequisites:
@ -219,10 +223,10 @@ To view value stream analytics for your group or project:
1. Select the **Filter results** text box.
1. Select a parameter.
1. Select a value or enter text to refine the results.
1. To adjust the date range:
- In the **From** field, select a start date.
- In the **To** field, select an end date. The charts and list show workflow items created
during the date range.
1. To view metrics in a particular date range, from the dropdown list select a predefined date range or the **Custom** option. With the **Custom** option selected:
- In the **From** field, select a start date.
- In the **To** field, select an end date.
The charts and list display workflow items created during the date range.
1. Optional. Sort results by ascending or descending:
- To sort by most recent or oldest workflow item, select the **Last event** header.
- To sort by most or least amount of time spent in each stage, select the **Duration** header.

View File

@ -8,7 +8,8 @@ module ClickHouse
TYPE_CASTERS = {
'UInt64' => ->(value) { Integer(value) },
"DateTime64(6, 'UTC')" => ->(value) { ActiveSupport::TimeZone['UTC'].parse(value) },
"IntervalSecond" => ->(value) { ActiveSupport::Duration.build(value.to_i) }
"IntervalSecond" => ->(value) { ActiveSupport::Duration.build(value.to_i) },
"IntervalMillisecond" => ->(value) { ActiveSupport::Duration.build(value.to_i / 1000.0) }
}.freeze
def self.format(result)

View File

@ -8,7 +8,8 @@ RSpec.describe ClickHouse::Client::Formatter do
_query = <<~SQL.squish
SELECT toUInt64(1) as uint64,
toDateTime64('2016-06-15 23:00:00', 6, 'UTC') as datetime64_6,
INTERVAL 1 second as interval_second
INTERVAL 1 second as interval_second,
INTERVAL 1 millisecond as interval_millisecond
SQL
response_json = <<~JSON
@ -26,6 +27,10 @@ RSpec.describe ClickHouse::Client::Formatter do
{
"name": "interval_second",
"type": "IntervalSecond"
},
{
"name": "interval_millisecond",
"type": "IntervalMillisecond"
}
],
@ -34,7 +39,8 @@ RSpec.describe ClickHouse::Client::Formatter do
{
"uint64": "1",
"datetime64_6": "2016-06-15 23:00:00.000000",
"interval_second": "1"
"interval_second": "1",
"interval_millisecond": "1"
}
],
@ -56,7 +62,8 @@ RSpec.describe ClickHouse::Client::Formatter do
eq(
[{ "uint64" => 1,
"datetime64_6" => ActiveSupport::TimeZone["UTC"].parse("2016-06-15 23:00:00"),
"interval_second" => 1.second }]
"interval_second" => 1.second,
"interval_millisecond" => 0.001.seconds }]
)
)
end

View File

@ -37,7 +37,7 @@ module Gitlab
request.params.values.any? do |value|
param_has_null_byte?(value)
end
rescue ActionController::BadRequest
rescue ActionController::BadRequest, ActionDispatch::Http::Parameters::ParseError
# If we can't build an ActionDispatch::Request something's wrong
# This would also happen if `#params` contains invalid UTF-8
# in this case we'll return a 400

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class CountCsvImportsMetric < DatabaseMetric
operation :count
relation { ::Issues::CsvImport }
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class CountJiraImportsMetric < DatabaseMetric
operation :count
relation { JiraImportState }
end
end
end
end
end

View File

@ -379,7 +379,6 @@ module Gitlab
bulk_imports: {
gitlab_v1: count(::BulkImport.where(**time_period, source_type: :gitlab))
},
issue_imports: issue_imports(time_period),
group_imports: group_imports(time_period)
}
end
@ -568,15 +567,6 @@ module Gitlab
omniauth_provider_names.reject { |name| name.starts_with?('ldap') }
end
def issue_imports(time_period)
time_frame = metric_time_period(time_period)
{
jira: count(::JiraImportState.where(time_period)), # rubocop: disable CodeReuse/ActiveRecord
fogbugz: add_metric('CountImportedProjectsMetric', time_frame: time_frame, options: { import_type: 'fogbugz' }),
csv: count(::Issues::CsvImport.where(time_period)) # rubocop: disable CodeReuse/ActiveRecord
}
end
def group_imports(time_period)
time_frame = metric_time_period(time_period)
{

View File

@ -14471,6 +14471,9 @@ msgstr ""
msgid "Currently unable to fetch data for this pipeline."
msgstr ""
msgid "Custom"
msgstr ""
msgid "Custom (%{language})"
msgstr ""
@ -44898,6 +44901,9 @@ msgstr ""
msgid "Snippets|Snippets can't contain empty files. Ensure all files have content, or delete them."
msgstr ""
msgid "Snippets|This snippet is hidden because its author has been banned"
msgstr ""
msgid "Snowplow"
msgstr ""

View File

@ -15,6 +15,7 @@ RSpec.describe 'Value Stream Analytics', :js, feature_category: :value_stream_ma
let_it_be(:stage_table_duration_column_header_selector) { '[data-testid="vsa-stage-header-duration"]' }
let_it_be(:metrics_selector) { "[data-testid='vsa-metrics']" }
let_it_be(:metric_value_selector) { "[data-testid='displayValue']" }
let_it_be(:predefined_date_ranges_dropdown_selector) { '[data-testid="vsa-predefined-date-ranges-dropdown"]' }
let(:stage_table) { find(stage_table_selector) }
let(:project) { create(:project, :repository) }
@ -92,6 +93,43 @@ RSpec.describe 'Value Stream Analytics', :js, feature_category: :value_stream_ma
let(:stage_table_events) { stage_table.all(stage_table_event_selector) }
shared_examples 'filters the issues by date' do
it 'can filter the issues by date' do
expect(page).to have_selector(stage_table_event_selector)
set_daterange(from, to)
expect(page).not_to have_selector(stage_table_event_selector)
expect(page).not_to have_selector(stage_table_pagination_selector)
end
end
shared_examples 'filters the metrics by date' do
it 'can filter the metrics by date' do
expect(metrics_values).to match_array(%w[21 2 1])
set_daterange(from, to)
expect(metrics_values).to eq(['-'] * 3)
end
end
shared_examples 'navigates directly to a value stream stream stage with filters applied' do
before do
visit project_cycle_analytics_path(project, created_before: '2019-12-31', created_after: '2019-11-01', stage_id: 'code', milestone_title: milestone.title)
wait_for_requests
end
it 'can navigate directly to a value stream stream stage with filters applied' do
expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Code')
expect(page.find(".js-daterange-picker-from input").value).to eq("2019-11-01")
expect(page.find(".js-daterange-picker-to input").value).to eq("2019-12-31")
filter_bar = page.find(stage_filter_bar)
expect(filter_bar.find(".gl-filtered-search-token-data-content").text).to eq("%#{milestone.title}")
end
end
it 'displays metrics' do
metrics_tiles = page.find(metrics_selector)
@ -121,23 +159,6 @@ RSpec.describe 'Value Stream Analytics', :js, feature_category: :value_stream_ma
expect_merge_request_to_be_present
end
it 'can filter the issues by date' do
expect(page).to have_selector(stage_table_event_selector)
set_daterange(from, to)
expect(page).not_to have_selector(stage_table_event_selector)
expect(page).not_to have_selector(stage_table_pagination_selector)
end
it 'can filter the metrics by date' do
expect(metrics_values).to match_array(%w[21 2 1])
set_daterange(from, to)
expect(metrics_values).to eq(['-'] * 3)
end
it 'can sort records', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338332' do
# NOTE: checking that the string changes should suffice
# depending on the order the tests are run we might run into problems with hard coded strings
@ -163,16 +184,43 @@ RSpec.describe 'Value Stream Analytics', :js, feature_category: :value_stream_ma
expect(page).not_to have_text(original_first_title, exact: true)
end
it 'can navigate directly to a value stream stream stage with filters applied' do
visit project_cycle_analytics_path(project, created_before: '2019-12-31', created_after: '2019-11-01', stage_id: 'code', milestone_title: milestone.title)
wait_for_requests
context 'when the `vsa_predefined_date_ranges` feature flag is enabled' do
before do
visit project_cycle_analytics_path(project)
expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Code')
expect(page.find(".js-daterange-picker-from input").value).to eq("2019-11-01")
expect(page.find(".js-daterange-picker-to input").value).to eq("2019-12-31")
wait_for_requests
end
filter_bar = page.find(stage_filter_bar)
expect(filter_bar.find(".gl-filtered-search-token-data-content").text).to eq("%#{milestone.title}")
it 'shows predefined date ranges dropdown with `Custom` option selected' do
page.within(predefined_date_ranges_dropdown_selector) do
expect(page).to have_button('Custom')
end
end
it_behaves_like 'filters the issues by date'
it_behaves_like 'filters the metrics by date'
it_behaves_like 'navigates directly to a value stream stream stage with filters applied'
end
context 'when the `vsa_predefined_date_ranges` feature flag is disabled' do
before do
stub_feature_flags(vsa_predefined_date_ranges: false)
visit project_cycle_analytics_path(project)
wait_for_requests
end
it 'does not show predefined date ranges dropdown' do
expect(page).not_to have_css(predefined_date_ranges_dropdown_selector)
end
it_behaves_like 'filters the issues by date'
it_behaves_like 'filters the metrics by date'
it_behaves_like 'navigates directly to a value stream stream stage with filters applied'
end
def stage_time_column

View File

@ -112,9 +112,7 @@ RSpec.describe SnippetsFinder do
expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
end
it 'returns all snippets (everything) for an admin when all_available="true" passed in' do
allow(admin).to receive(:can_read_all_resources?).and_return(true)
it 'returns all snippets (everything) for an admin when all_available="true" passed in', :enable_admin_mode do
snippets = described_class.new(admin, author: user, all_available: true).execute
expect(snippets).to contain_exactly(
@ -326,6 +324,50 @@ RSpec.describe SnippetsFinder do
end
end
context 'filtering for snippets authored by banned users', feature_category: :insider_threat do
let_it_be(:banned_user) { create(:user, :banned) }
let_it_be(:banned_public_personal_snippet) { create(:personal_snippet, :public, author: banned_user) }
let_it_be(:banned_public_project_snippet) { create(:project_snippet, :public, project: project, author: banned_user) }
it 'returns banned snippets for admins when in admin mode', :enable_admin_mode do
snippets = described_class.new(
admin,
ids: [banned_public_personal_snippet.id, banned_public_project_snippet.id]
).execute
expect(snippets).to contain_exactly(
banned_public_personal_snippet, banned_public_project_snippet
)
end
it 'does not return banned snippets for non-admin users' do
snippets = described_class.new(
user,
ids: [banned_public_personal_snippet.id, banned_public_project_snippet.id]
).execute
expect(snippets).to be_empty
end
context 'when hide_snippets_of_banned_users feature flag is off' do
before do
stub_feature_flags(hide_snippets_of_banned_users: false)
end
it 'returns banned snippets for non-admin users' do
snippets = described_class.new(
user,
ids: [banned_public_personal_snippet.id, banned_public_project_snippet.id]
).execute
expect(snippets).to contain_exactly(
banned_public_personal_snippet, banned_public_project_snippet
)
end
end
end
context 'when the user cannot read cross project' do
before do
allow(Ability).to receive(:allowed?).and_call_original

View File

@ -141,9 +141,11 @@ describe('Value stream analytics component', () => {
namespacePath: groupPath,
endDate: createdBefore,
hasDateRangeFilter: true,
hasPredefinedDateRangesFilter: true,
hasProjectFilter: false,
selectedProjects: [],
startDate: createdAfter,
predefinedDateRange: null,
});
});

View File

@ -1,20 +1,29 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Daterange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import FilterBar from '~/analytics/cycle_analytics/components/filter_bar.vue';
import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
import {
createdAfter as startDate,
createdBefore as endDate,
currentGroup,
selectedProjects,
} from '../mock_data';
DATE_RANGE_LAST_30_DAYS_VALUE,
DATE_RANGE_CUSTOM_VALUE,
LAST_30_DAYS,
} from '~/analytics/shared/constants';
import { useFakeDate } from 'helpers/fake_date';
import { currentGroup, selectedProjects } from '../mock_data';
const { path } = currentGroup;
const groupPath = `groups/${path}`;
const defaultFeatureFlags = {
vsaPredefinedDateRanges: false,
};
function createComponent(props = {}) {
return shallowMount(ValueStreamFilters, {
const startDate = LAST_30_DAYS;
const endDate = new Date('2019-01-14T00:00:00.000Z');
function createComponent({ props = {}, featureFlags = defaultFeatureFlags } = {}) {
return shallowMountExtended(ValueStreamFilters, {
propsData: {
selectedProjects,
groupPath,
@ -23,15 +32,23 @@ function createComponent(props = {}) {
endDate,
...props,
},
provide: {
glFeatures: {
...featureFlags,
},
},
});
}
describe('ValueStreamFilters', () => {
useFakeDate(2019, 0, 14, 10, 10);
let wrapper;
const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdownFilter);
const findDateRangePicker = () => wrapper.findComponent(Daterange);
const findFilterBar = () => wrapper.findComponent(FilterBar);
const findDateRangesDropdown = () => wrapper.findComponent(DateRangesDropdown);
beforeEach(() => {
wrapper = createComponent();
@ -55,6 +72,10 @@ describe('ValueStreamFilters', () => {
expect(findDateRangePicker().exists()).toBe(true);
});
it('will not render the date ranges dropdown', () => {
expect(findDateRangesDropdown().exists()).toBe(false);
});
it('will emit `selectProject` when a project is selected', () => {
findProjectsDropdown().vm.$emit('selected');
@ -69,21 +90,168 @@ describe('ValueStreamFilters', () => {
describe('hasDateRangeFilter = false', () => {
beforeEach(() => {
wrapper = createComponent({ hasDateRangeFilter: false });
wrapper = createComponent({ props: { hasDateRangeFilter: false } });
});
it('will not render the date range picker', () => {
it('should not render the date range picker', () => {
expect(findDateRangePicker().exists()).toBe(false);
});
});
describe('hasProjectFilter = false', () => {
beforeEach(() => {
wrapper = createComponent({ hasProjectFilter: false });
wrapper = createComponent({ props: { hasProjectFilter: false } });
});
it('will not render the project dropdown', () => {
expect(findProjectsDropdown().exists()).toBe(false);
});
});
describe('`vsaPredefinedDateRanges` feature flag is enabled', () => {
const lastMonthValue = 'lastMonthValue';
const mockDateRange = {
value: lastMonthValue,
startDate: new Date('2023-08-08T00:00:00.000Z'),
endDate: new Date('2023-09-08T00:00:00.000Z'),
};
beforeEach(() => {
wrapper = createComponent({ featureFlags: { vsaPredefinedDateRanges: true } });
});
it('should render date ranges dropdown', () => {
expect(findDateRangesDropdown().exists()).toBe(true);
});
it('should not render date range picker', () => {
expect(findDateRangePicker().exists()).toBe(false);
});
describe('when a date range is selected from the dropdown', () => {
describe('predefined date range option', () => {
beforeEach(async () => {
findDateRangesDropdown().vm.$emit('selected', mockDateRange);
await nextTick();
});
it('should emit `setDateRange` with date range', () => {
const { value, ...dateRange } = mockDateRange;
expect(wrapper.emitted('setDateRange')).toEqual([[dateRange]]);
});
it('should emit `setPredefinedDateRange` with correct value', () => {
expect(wrapper.emitted('setPredefinedDateRange')).toEqual([[lastMonthValue]]);
});
});
describe('custom date range option', () => {
beforeEach(async () => {
findDateRangesDropdown().vm.$emit('customDateRangeSelected');
await nextTick();
});
it('should emit `setPredefinedDateRange` with custom date range value', () => {
expect(wrapper.emitted('setPredefinedDateRange')).toEqual([[DATE_RANGE_CUSTOM_VALUE]]);
});
it('should not emit `setDateRange`', () => {
expect(wrapper.emitted('setDateRange')).toBeUndefined();
});
});
});
describe.each`
predefinedDateRange | shouldRenderDateRangePicker | dateRangeType
${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'custom date range'}
${lastMonthValue} | ${false} | ${'predefined date range'}
`(
'when the `predefinedDateRange` prop is set to a $dateRangeType',
({ predefinedDateRange, shouldRenderDateRangePicker }) => {
beforeEach(() => {
wrapper = createComponent({
props: { predefinedDateRange },
featureFlags: { vsaPredefinedDateRanges: true },
});
});
it("should be passed into the dropdown's `selected` prop", () => {
expect(findDateRangesDropdown().props('selected')).toBe(predefinedDateRange);
});
it(`should ${
shouldRenderDateRangePicker ? '' : 'not'
} render the date range picker`, () => {
expect(findDateRangePicker().exists()).toBe(shouldRenderDateRangePicker);
});
},
);
describe('when the `predefinedDateRange` prop is null', () => {
const laterStartDate = new Date('2018-12-01T00:00:00.000Z');
const earlierStartDate = new Date('2019-01-01T00:00:00.000Z');
const customEndDate = new Date('2019-02-01T00:00:00.000Z');
describe.each`
dateRange | expectedDateRangeOption | shouldRenderDateRangePicker | description
${{ startDate, endDate }} | ${DATE_RANGE_LAST_30_DAYS_VALUE} | ${false} | ${'is default'}
${{ startDate: laterStartDate, endDate }} | ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'has a later start date than the default'}
${{ startDate: earlierStartDate, endDate }} | ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'has an earlier start date than the default'}
${{ startDate, endDate: customEndDate }} | ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'has an end date that is not today'}
`(
'date range $description',
({ dateRange, expectedDateRangeOption, shouldRenderDateRangePicker }) => {
beforeEach(() => {
wrapper = createComponent({
props: { predefinedDateRange: null, ...dateRange },
featureFlags: { vsaPredefinedDateRanges: true },
});
});
it("should set the dropdown's `selected` prop to the correct value", () => {
expect(findDateRangesDropdown().props('selected')).toBe(expectedDateRangeOption);
});
it(`should ${
shouldRenderDateRangePicker ? '' : 'not'
} render the date range picker`, () => {
expect(findDateRangePicker().exists()).toBe(shouldRenderDateRangePicker);
});
},
);
});
describe('hasPredefinedDateRangesFilter = false', () => {
beforeEach(() => {
wrapper = createComponent({
props: { hasPredefinedDateRangesFilter: false },
featureFlags: { vsaPredefinedDateRanges: true },
});
});
it('should not render the date ranges dropdown', () => {
expect(findDateRangesDropdown().exists()).toBe(false);
});
});
describe('hasDateRangeFilter = false', () => {
beforeEach(() => {
wrapper = createComponent({
props: { hasDateRangeFilter: false },
featureFlags: { vsaPredefinedDateRanges: true },
});
});
it('should not render the date range picker', () => {
expect(findDateRangePicker().exists()).toBe(false);
});
it('should remove custom date range option from date ranges dropdown', () => {
expect(findDateRangesDropdown().props('includeCustomDateRangeOption')).toBe(false);
});
});
});
});

View File

@ -261,3 +261,5 @@ export const basePaginationResult = {
direction: PAGINATION_SORT_DIRECTION_DESC,
page: null,
};
export const predefinedDateRange = 'last_week';

View File

@ -14,6 +14,7 @@ import {
initialPaginationState,
reviewEvents,
projectNamespace as namespace,
predefinedDateRange,
} from '../mock_data';
const { path: groupPath } = currentGroup;
@ -32,6 +33,7 @@ const defaultState = {
createdAfter,
createdBefore,
pagination: initialPaginationState,
predefinedDateRange,
};
describe('Project Value Stream Analytics actions', () => {
@ -53,6 +55,7 @@ describe('Project Value Stream Analytics actions', () => {
describe.each`
action | payload | expectedActions | expectedMutations
${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${[{ type: 'refetchStageData' }]} | ${[mockSetDateActionCommit]}
${'setPredefinedDateRange'} | ${{ predefinedDateRange }} | ${[]} | ${[{ type: 'SET_PREDEFINED_DATE_RANGE', payload: { predefinedDateRange } }]}
${'setFilters'} | ${[]} | ${[{ type: 'refetchStageData' }]} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'refetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}

View File

@ -18,6 +18,7 @@ import {
stageCounts,
initialPaginationState as pagination,
projectNamespace as mockNamespace,
predefinedDateRange,
} from '../mock_data';
let state;
@ -94,6 +95,7 @@ describe('Project Value Stream Analytics mutations', () => {
mutation | payload | stateKey | value
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_PREDEFINED_DATE_RANGE} | ${predefinedDateRange} | ${'predefinedDateRange'} | ${predefinedDateRange}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}

View File

@ -0,0 +1,165 @@
import { nextTick } from 'vue';
import { GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('DateRangesDropdown', () => {
let wrapper;
const customDateRangeValue = 'custom';
const lastWeekValue = 'lastWeek';
const last30DaysValue = 'lastMonth';
const mockLastWeek = {
text: 'Last week',
value: lastWeekValue,
startDate: new Date('2023-09-08T00:00:00.000Z'),
endDate: new Date('2023-09-14T00:00:00.000Z'),
};
const mockLast30Days = {
text: 'Last month',
value: last30DaysValue,
startDate: new Date('2023-08-16T00:00:00.000Z'),
endDate: new Date('2023-09-14T00:00:00.000Z'),
};
const mockCustomDateRangeItem = {
text: 'Custom',
value: customDateRangeValue,
};
const mockDateRanges = [mockLastWeek, mockLast30Days];
const mockItems = mockDateRanges.map(({ text, value }) => ({ text, value }));
const mockTooltipText = 'Max date range is 180 days';
const createComponent = ({ props = {}, dateRangeOptions = mockDateRanges } = {}) => {
wrapper = shallowMountExtended(DateRangesDropdown, {
propsData: {
dateRangeOptions,
...props,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findDaysSelectedCount = () => wrapper.findByTestId('predefined-date-range-days-count');
const findHelpIcon = () => wrapper.findComponent(GlIcon);
describe('default state', () => {
beforeEach(() => {
createComponent();
});
it('should pass items to listbox `items` prop in correct order', () => {
expect(findListBox().props('items')).toStrictEqual([...mockItems, mockCustomDateRangeItem]);
});
it('should display first option as selected', () => {
expect(findListBox().props('selected')).toBe(lastWeekValue);
});
it('should not display info icon', () => {
expect(findHelpIcon().exists()).toBe(false);
});
describe.each`
dateRangeValue | dateRangeItem
${lastWeekValue} | ${mockLastWeek}
${last30DaysValue} | ${mockLast30Days}
`('when $dateRangeValue date range is selected', ({ dateRangeValue, dateRangeItem }) => {
beforeEach(async () => {
findListBox().vm.$emit('select', dateRangeValue);
await nextTick();
});
it('should emit `selected` event with value and date range', () => {
const { text, ...dateRangeProps } = dateRangeItem;
expect(wrapper.emitted('selected')).toEqual([[dateRangeProps]]);
});
it('should display days selected indicator', () => {
expect(findDaysSelectedCount().exists()).toBe(true);
});
it('should not emit `customDateRangeSelected` event', () => {
expect(wrapper.emitted('customDateRangeSelected')).toBeUndefined();
});
});
describe('when the custom date range option is selected', () => {
beforeEach(async () => {
findListBox().vm.$emit('select', customDateRangeValue);
await nextTick();
});
it('should emit `customDateRangeSelected` event', () => {
expect(wrapper.emitted('customDateRangeSelected')).toHaveLength(1);
});
it('should hide days selected indicator', () => {
expect(findDaysSelectedCount().exists()).toBe(false);
});
it('should not emit `selected` event', () => {
expect(wrapper.emitted('selected')).toBeUndefined();
});
});
});
describe('when a date range is preselected', () => {
beforeEach(() => {
createComponent({ props: { selected: 'lastMonth' } });
});
it('should display preselected date range as selected in listbox', () => {
expect(findListBox().props('selected')).toBe(last30DaysValue);
});
});
describe('days selected indicator', () => {
it.each`
selected | includeEndDateInDaysSelected | expectedDaysCount
${lastWeekValue} | ${true} | ${7}
${last30DaysValue} | ${true} | ${30}
${lastWeekValue} | ${false} | ${6}
${last30DaysValue} | ${false} | ${29}
`(
'should display correct days selected when includeEndDateInDaysSelected=$includeEndDateInDaysSelected',
({ selected, includeEndDateInDaysSelected, expectedDaysCount }) => {
createComponent({ props: { selected, includeEndDateInDaysSelected } });
expect(wrapper.findByText(`${expectedDaysCount} days selected`).exists()).toBe(true);
},
);
});
describe('when the `tooltip` prop is set', () => {
beforeEach(() => {
createComponent({ props: { tooltip: mockTooltipText } });
});
it('should display info icon with tooltip', () => {
const helpIcon = findHelpIcon();
const tooltip = getBinding(helpIcon.element, 'gl-tooltip');
expect(helpIcon.props('name')).toBe('information-o');
expect(helpIcon.attributes('title')).toBe(mockTooltipText);
expect(tooltip).toBeDefined();
});
});
describe('when `includeCustomDateRangeOption` = false', () => {
beforeEach(() => {
createComponent({ props: { includeCustomDateRangeOption: false } });
});
it('should pass items without custom date range option to listbox `items` prop', () => {
expect(findListBox().props('items')).toEqual(mockItems);
});
});
});

View File

@ -1,5 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
@ -23,14 +24,48 @@ describe('Snippet header component', () => {
propsData: {
...defaultProps,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
}
const findIcon = () => wrapper.findComponent(GlIcon);
const findTooltip = () => getBinding(findIcon().element, 'gl-tooltip');
it('renders itself', () => {
createComponent();
expect(wrapper.find('.snippet-header').exists()).toBe(true);
});
it('does not render spam icon when author is not banned', () => {
createComponent();
expect(findIcon().exists()).toBe(false);
});
it('renders spam icon and tooltip when author is banned', () => {
createComponent({
props: {
snippet: {
...snippet.snippet,
hidden: true,
},
},
});
expect(findIcon().props()).toMatchObject({
ariaLabel: 'Hidden',
name: 'spam',
size: 16,
});
expect(findIcon().attributes('title')).toBe(
'This snippet is hidden because its author has been banned',
);
expect(findTooltip()).toBeDefined();
});
it('renders snippets title and description', () => {
createComponent();

View File

@ -45,6 +45,7 @@ export const createGQLSnippet = () => ({
message: '',
},
},
hidden: false,
});
export const createGQLSnippetsQueryResponse = (snippets) => ({

View File

@ -52,7 +52,7 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
allow(alert).to receive(:save).and_return(false)
allow(alert).to receive(:errors).and_return(
double(full_messages: %w(foo bar), :[] => nil)
double(full_messages: %w[foo bar], :[] => nil)
)
expect(resolve).to eq(
alert: alert,

View File

@ -69,7 +69,7 @@ RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
active: false,
locked: true,
run_untagged: false,
tag_list: %w(tag1 tag2)
tag_list: %w[tag1 tag2]
}
end

View File

@ -67,7 +67,7 @@ RSpec.describe Mutations::Commits::Create do
context 'when service successfully creates a new commit' do
it "returns the ETag path for the commit's pipeline" do
commit_pipeline_path = subject[:commit_pipeline_path]
expect(commit_pipeline_path).to match(%r(pipelines/sha/\w+))
expect(commit_pipeline_path).to match(%r{pipelines/sha/\w+})
end
it 'returns the content of the commit' do

View File

@ -41,7 +41,7 @@ RSpec.describe Resolvers::BoardListsResolver do
lists = resolve_board_lists
expect(lists.count).to eq 3
expect(lists.map(&:list_type)).to eq %w(backlog label closed)
expect(lists.map(&:list_type)).to eq %w[backlog label closed]
end
context 'when another user has list preferences' do

View File

@ -25,7 +25,7 @@ RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
subject { resolver.map(&:name) }
before do
stub_container_registry_tags(repository: repository.path, tags: %w(aaa bab bbb ccc 123), with_manifest: false)
stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false)
end
context 'without sort' do
@ -37,19 +37,19 @@ RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
context "name_asc" do
let(:args) { { sort: :name_asc } }
it { is_expected.to eq(%w(123 aaa bab bbb ccc)) }
it { is_expected.to eq(%w[123 aaa bab bbb ccc]) }
end
context "name_desc" do
let(:args) { { sort: :name_desc } }
it { is_expected.to eq(%w(ccc bbb bab aaa 123)) }
it { is_expected.to eq(%w[ccc bbb bab aaa 123]) }
end
context 'filter by name' do
let(:args) { { sort: :name_desc, name: 'b' } }
it { is_expected.to eq(%w(bbb bab)) }
it { is_expected.to eq(%w[bbb bab]) }
end
end
end

View File

@ -60,9 +60,9 @@ RSpec.describe Resolvers::Projects::JiraProjectsResolver, feature_category: :int
project_ids = jira_projects.map(&:id)
expect(jira_projects.size).to eq 2
expect(project_keys).to eq(%w(EX ABC))
expect(project_names).to eq(%w(Example Alphabetical))
expect(project_ids).to eq(%w(10000 10001))
expect(project_keys).to eq(%w[EX ABC])
expect(project_names).to eq(%w[Example Alphabetical])
expect(project_ids).to eq(%w[10000 10001])
expect(resolver.max_page_size).to eq(2)
end
@ -75,9 +75,9 @@ RSpec.describe Resolvers::Projects::JiraProjectsResolver, feature_category: :int
project_ids = jira_projects.map(&:id)
expect(jira_projects.size).to eq 1
expect(project_keys).to eq(%w(ABC))
expect(project_names).to eq(%w(Alphabetical))
expect(project_ids).to eq(%w(10001))
expect(project_keys).to eq(%w[ABC])
expect(project_names).to eq(%w[Alphabetical])
expect(project_ids).to eq(%w[10001])
expect(resolver.max_page_size).to eq(1)
end
end

View File

@ -10,7 +10,7 @@ RSpec.describe Resolvers::ProjectsResolver do
let_it_be(:group) { create(:group, name: 'public-group') }
let_it_be(:private_group) { create(:group, name: 'private-group') }
let_it_be(:project) { create(:project, :public, topic_list: %w(ruby javascript)) }
let_it_be(:project) { create(:project, :public, topic_list: %w[ruby javascript]) }
let_it_be(:other_project) { create(:project, :public) }
let_it_be(:group_project) { create(:project, :public, group: group) }
let_it_be(:private_project) { create(:project, :private) }
@ -68,7 +68,7 @@ RSpec.describe Resolvers::ProjectsResolver do
end
context 'when topics filter is provided' do
let(:filters) { { topics: %w(ruby) } }
let(:filters) { { topics: %w[ruby] } }
it 'returns matching project' do
is_expected.to contain_exactly(project)
@ -148,7 +148,7 @@ RSpec.describe Resolvers::ProjectsResolver do
end
context 'when topics filter is provided' do
let(:filters) { { topics: %w(ruby) } }
let(:filters) { { topics: %w[ruby] } }
it 'returns matching project' do
is_expected.to contain_exactly(project)

View File

@ -6,8 +6,8 @@ RSpec.describe GitlabSchema.types['BoardIssueInput'] do
it { expect(described_class.graphql_name).to eq('BoardIssueInput') }
it 'has specific fields' do
allowed_args = %w(labelName milestoneTitle assigneeUsername authorUsername
releaseTag myReactionEmoji not search assigneeWildcardId confidential)
allowed_args = %w[labelName milestoneTitle assigneeUsername authorUsername
releaseTag myReactionEmoji not search assigneeWildcardId confidential]
expect(described_class.arguments.keys).to include(*allowed_args)
expect(described_class.arguments['not'].type).to eq(Types::Boards::NegatedBoardIssueInputType)

View File

@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['DesignCollectionCopyState'] do
it { expect(described_class.graphql_name).to eq('DesignCollectionCopyState') }
it 'exposes the correct event states' do
expect(described_class.values.keys).to match_array(%w(READY IN_PROGRESS ERROR))
expect(described_class.values.keys).to match_array(%w[READY IN_PROGRESS ERROR])
end
end

View File

@ -32,7 +32,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
let_it_be(:now) { Time.now.change(usec: 0) }
let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: now) }
let(:count_path) { %w(data project issues count) }
let(:count_path) { %w[data project issues count] }
let(:page_size) { 3 }
let(:query) do
<<~GRAPHQL
@ -81,8 +81,8 @@ RSpec.describe GitlabSchema.types['Issue'] do
end
context 'count' do
let(:end_cursor) { %w(data project issues pageInfo endCursor) }
let(:issues_edges) { %w(data project issues edges) }
let(:end_cursor) { %w[data project issues pageInfo endCursor] }
let(:issues_edges) { %w[data project issues edges] }
it 'returns total count' do
expect(subject.dig(*count_path)).to eq(issues.count)

View File

@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
let_it_be(:user) { create(:user) }
it 'has the correct fields' do
expected_fields = [:id, :title, :project, :author,
expected_fields = [:id, :title, :project, :author, :hidden,
:file_name, :description,
:visibility_level, :created_at, :updated_at,
:web_url, :raw_url, :ssh_url_to_repo, :http_url_to_repo,

View File

@ -152,7 +152,7 @@ RSpec.describe AppearancesHelper do
let!(:appearance) { create(:appearance, :with_logo) }
it 'returns a path' do
expect(helper.brand_image).to match(%r(img .* data-src="/uploads/-/system/appearance/.*png))
expect(helper.brand_image).to match(%r{img .* data-src="/uploads/-/system/appearance/.*png})
end
context 'when there is no associated upload' do
@ -163,14 +163,14 @@ RSpec.describe AppearancesHelper do
end
it 'falls back to using the original path' do
expect(helper.brand_image).to match(%r(img .* data-src="/uploads/-/system/appearance/.*png))
expect(helper.brand_image).to match(%r{img .* data-src="/uploads/-/system/appearance/.*png})
end
end
end
context 'when there is no logo' do
it 'returns path of GitLab logo' do
expect(helper.brand_image).to match(%r(img .* data-src="#{gitlab_logo}))
expect(helper.brand_image).to match(%r{img .* data-src="#{gitlab_logo}})
end
end
@ -178,13 +178,13 @@ RSpec.describe AppearancesHelper do
let!(:appearance) { create(:appearance, title: 'My title') }
it 'returns the title' do
expect(helper.brand_image).to match(%r(img alt="My title"))
expect(helper.brand_image).to match(%r{img alt="My title"})
end
end
context 'when there is no title' do
it 'returns the default title' do
expect(helper.brand_image).to match(%r(img alt="GitLab))
expect(helper.brand_image).to match(%r{img alt="GitLab})
end
end
end
@ -194,7 +194,7 @@ RSpec.describe AppearancesHelper do
let!(:appearance) { create(:appearance, :with_logo) }
it 'returns path of custom logo' do
expect(helper.brand_image_path).to match(%r(/uploads/-/system/appearance/.*/dk.png))
expect(helper.brand_image_path).to match(%r{/uploads/-/system/appearance/.*/dk.png})
end
end

View File

@ -47,7 +47,7 @@ RSpec.describe ApplicationSettingsHelper do
describe '.visible_attributes' do
it 'contains tracking parameters' do
expect(helper.visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id))
expect(helper.visible_attributes).to include(*%i[snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id])
end
it 'contains :deactivate_dormant_users' do
@ -60,16 +60,16 @@ RSpec.describe ApplicationSettingsHelper do
it 'contains rate limit parameters' do
expect(helper.visible_attributes).to include(
*%i(
*%i[
issues_create_limit notes_create_limit project_export_limit
project_download_export_limit project_export_limit project_import_limit
raw_blob_request_limit group_export_limit group_download_export_limit
group_import_limit users_get_by_id_limit search_rate_limit search_rate_limit_unauthenticated
))
])
end
it 'contains GitLab for Slack app parameters' do
params = %i(slack_app_enabled slack_app_id slack_app_secret slack_app_signing_secret slack_app_verification_token)
params = %i[slack_app_enabled slack_app_id slack_app_secret slack_app_signing_secret slack_app_verification_token]
expect(helper.visible_attributes).to include(*params)
end
@ -306,7 +306,7 @@ RSpec.describe ApplicationSettingsHelper do
describe '#sidekiq_job_limiter_modes_for_select' do
subject { helper.sidekiq_job_limiter_modes_for_select }
it { is_expected.to eq([%w(Track track), %w(Compress compress)]) }
it { is_expected.to eq([%w[Track track], %w[Compress compress]]) }
end
describe '#instance_clusters_enabled?', :request_store do

View File

@ -35,12 +35,12 @@ RSpec.describe AuthHelper do
describe "form_based_providers" do
it 'includes LDAP providers' do
allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
expect(helper.form_based_providers).to eq %i(ldapmain)
expect(helper.form_based_providers).to eq %i[ldapmain]
end
it 'includes crowd provider' do
allow(helper).to receive(:auth_providers) { [:twitter, :crowd] }
expect(helper.form_based_providers).to eq %i(crowd)
expect(helper.form_based_providers).to eq %i[crowd]
end
end
@ -101,15 +101,15 @@ RSpec.describe AuthHelper do
describe 'popular_enabled_button_based_providers' do
it 'returns the intersection set of popular & enabled providers', :aggregate_failures do
allow(helper).to receive(:enabled_button_based_providers) { %w(twitter github google_oauth2) }
allow(helper).to receive(:enabled_button_based_providers) { %w[twitter github google_oauth2] }
expect(helper.popular_enabled_button_based_providers).to eq(%w(github google_oauth2))
expect(helper.popular_enabled_button_based_providers).to eq(%w[github google_oauth2])
allow(helper).to receive(:enabled_button_based_providers) { %w(google_oauth2 bitbucket) }
allow(helper).to receive(:enabled_button_based_providers) { %w[google_oauth2 bitbucket] }
expect(helper.popular_enabled_button_based_providers).to eq(%w(google_oauth2))
expect(helper.popular_enabled_button_based_providers).to eq(%w[google_oauth2])
allow(helper).to receive(:enabled_button_based_providers) { %w(bitbucket) }
allow(helper).to receive(:enabled_button_based_providers) { %w[bitbucket] }
expect(helper.popular_enabled_button_based_providers).to be_empty
end
@ -129,7 +129,7 @@ RSpec.describe AuthHelper do
context 'all the button based providers are disabled via application_setting' do
it 'returns false' do
stub_application_setting(
disabled_oauth_sign_in_sources: %w(github twitter)
disabled_oauth_sign_in_sources: %w[github twitter]
)
expect(helper.button_based_providers_enabled?).to be false

View File

@ -54,8 +54,8 @@ RSpec.describe BreadcrumbsHelper do
describe '#schema_breadcrumb_json' do
let(:elements) do
[
%w(element1 http://test.host/link1),
%w(element2 http://test.host/link2)
%w[element1 http://test.host/link1],
%w[element2 http://test.host/link2]
]
end
@ -89,8 +89,8 @@ RSpec.describe BreadcrumbsHelper do
context 'when extra breadcrumb element is added' do
let(:extra_elements) do
[
%w(extra_element1 http://test.host/extra_link1),
%w(extra_element2 http://test.host/extra_link2)
%w[extra_element1 http://test.host/extra_link1],
%w[extra_element2 http://test.host/extra_link2]
]
end

View File

@ -110,7 +110,7 @@ RSpec.describe Ci::PipelinesHelper do
before do
allow(helper).to receive(:current_user).and_return(user)
project.add_developer(user)
create(:project_setting, project: project, target_platforms: %w(ios))
create(:project_setting, project: project, target_platforms: %w[ios])
end
describe 'the `registration_token` attribute' do

View File

@ -48,9 +48,9 @@ RSpec.describe ClustersHelper do
end
it 'generates svg image data', :aggregate_failures do
expect(subject.dig(:img_tags, :aws, :path)).to match(%r(/illustrations/logos/amazon_eks|svg))
expect(subject.dig(:img_tags, :default, :path)).to match(%r(/illustrations/logos/kubernetes|svg))
expect(subject.dig(:img_tags, :gcp, :path)).to match(%r(/illustrations/logos/google_gke|svg))
expect(subject.dig(:img_tags, :aws, :path)).to match(%r{/illustrations/logos/amazon_eks|svg})
expect(subject.dig(:img_tags, :default, :path)).to match(%r{/illustrations/logos/kubernetes|svg})
expect(subject.dig(:img_tags, :gcp, :path)).to match(%r{/illustrations/logos/google_gke|svg})
expect(subject.dig(:img_tags, :aws, :text)).to eq('Amazon EKS')
expect(subject.dig(:img_tags, :default, :text)).to eq('Kubernetes Cluster')
@ -62,8 +62,8 @@ RSpec.describe ClustersHelper do
end
it 'displays empty image path' do
expect(subject[:clusters_empty_state_image]).to match(%r(/illustrations/empty-state/empty-state-clusters|svg))
expect(subject[:empty_state_image]).to match(%r(/illustrations/empty-state/empty-state-agents|svg))
expect(subject[:clusters_empty_state_image]).to match(%r{/illustrations/empty-state/empty-state-clusters|svg})
expect(subject[:empty_state_image]).to match(%r{/illustrations/empty-state/empty-state-agents|svg})
end
it 'displays add cluster using certificate path' do

View File

@ -196,26 +196,26 @@ RSpec.describe DiffHelper, feature_category: :code_review_workflow do
end
describe "#mark_inline_diffs" do
let(:old_line) { %{abc 'def'} }
let(:new_line) { %{abc "def"} }
let(:old_line) { %(abc 'def') }
let(:new_line) { %(abc "def") }
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{abc <span class="idiff left deletion">&#39;</span>def<span class="idiff right deletion">&#39;</span>})
expect(marked_old_line).to eq(%q(abc <span class="idiff left deletion">&#39;</span>def<span class="idiff right deletion">&#39;</span>))
expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{abc <span class="idiff left addition">&quot;</span>def<span class="idiff right addition">&quot;</span>})
expect(marked_new_line).to eq(%q(abc <span class="idiff left addition">&quot;</span>def<span class="idiff right addition">&quot;</span>))
expect(marked_new_line).to be_html_safe
end
context 'when given HTML' do
it 'sanitizes it' do
old_line = %{test.txt}
old_line = %(test.txt)
new_line = %{<img src=x onerror=alert(document.domain)>}
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{<span class="idiff left right deletion">test.txt</span>})
expect(marked_old_line).to eq(%q(<span class="idiff left right deletion">test.txt</span>))
expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{<span class="idiff left right addition">&lt;img src=x onerror=alert(document.domain)&gt;</span>})
expect(marked_new_line).to be_html_safe

View File

@ -237,7 +237,7 @@ RSpec.describe EmailsHelper do
it 'returns the brand header logo' do
expect(header_logo).to eq(
%{<img style="height: 50px" src="/uploads/-/system/appearance/header_logo/#{appearance.id}/dk.png" />}
%(<img style="height: 50px" src="/uploads/-/system/appearance/header_logo/#{appearance.id}/dk.png" />)
)
end
@ -326,8 +326,8 @@ RSpec.describe EmailsHelper do
create :appearance, header_message: 'Foo', footer_message: 'Bar', email_header_and_footer_enabled: true
aggregate_failures do
expect(html_header_message).to eq(%{<div class="header-message" style=""><p>Foo</p></div>})
expect(html_footer_message).to eq(%{<div class="footer-message" style=""><p>Bar</p></div>})
expect(html_header_message).to eq(%(<div class="header-message" style=""><p>Foo</p></div>))
expect(html_footer_message).to eq(%(<div class="footer-message" style=""><p>Bar</p></div>))
expect(text_header_message).to eq('Foo')
expect(text_footer_message).to eq('Bar')
end

View File

@ -85,7 +85,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
describe '#selected_template_name' do
let(:template_names) { %w(another_issue_template custom_issue_template) }
let(:template_names) { %w[another_issue_template custom_issue_template] }
context 'when no issuable_template parameter is provided' do
it 'does not select a template' do
@ -118,7 +118,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
describe '#default_template_name' do
context 'when a default template is available' do
let(:template_names) { %w(another_issue_template deFault) }
let(:template_names) { %w[another_issue_template deFault] }
it 'returns the default template' do
issue = build(:issue)
@ -140,7 +140,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
context 'when there is no default template' do
let(:template_names) { %w(another_issue_template) }
let(:template_names) { %w[another_issue_template] }
it 'returns nil' do
expect(helper.default_template_name(template_names, build(:issue))).to be_nil

View File

@ -78,7 +78,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
describe 'awards_sort' do
it 'sorts a hash so thumbsup and thumbsdown are always on top' do
data = { 'thumbsdown' => 'some value', 'lifter' => 'some value', 'thumbsup' => 'some value' }
expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter))
expect(awards_sort(data).keys).to eq(%w[thumbsup thumbsdown lifter])
end
end

View File

@ -115,13 +115,13 @@ RSpec.describe NavHelper, feature_category: :navigation do
describe '#page_has_markdown?' do
using RSpec::Parameterized::TableSyntax
where path: %w(
where path: %w[
projects/merge_requests#show
projects/merge_requests/conflicts#show
issues#show
milestones#show
issues#designs
)
]
with_them do
before do

View File

@ -55,7 +55,7 @@ RSpec.describe PageLayoutHelper do
expect(helper.page_image).to match_asset_path 'assets/twitter_card.jpg'
end
%w(project user group).each do |type|
%w[project user group].each do |type|
context "with @#{type} assigned" do
let(:object) { build(type, trait) }
let(:trait) { :with_avatar }
@ -116,11 +116,11 @@ RSpec.describe PageLayoutHelper do
it 'escapes content' do
allow(helper).to receive(:page_card_attributes)
.and_return(foo: %q{foo" http-equiv="refresh}.html_safe)
.and_return(foo: %q(foo" http-equiv="refresh).html_safe)
tags = helper.page_card_meta_tags
expect(tags).to include(%q{content="foo&quot; http-equiv=&quot;refresh"})
expect(tags).to include(%q(content="foo&quot; http-equiv=&quot;refresh"))
end
end

View File

@ -106,9 +106,9 @@ RSpec.describe ProfilesHelper do
using RSpec::Parameterized::TableSyntax
where(:stacking, :breakpoint, :expected) do
nil | nil | %w(gl-mb-3 gl-display-inline-block middle-dot-divider)
true | nil | %w(gl-mb-3 middle-dot-divider-sm gl-display-block gl-sm-display-inline-block)
nil | :sm | %w(gl-mb-3 gl-display-inline-block middle-dot-divider-sm)
nil | nil | %w[gl-mb-3 gl-display-inline-block middle-dot-divider]
true | nil | %w[gl-mb-3 middle-dot-divider-sm gl-display-block gl-sm-display-inline-block]
nil | :sm | %w[gl-mb-3 gl-display-inline-block middle-dot-divider-sm]
end
with_them do

View File

@ -59,7 +59,7 @@ RSpec.describe ReleasesHelper do
describe '#data_for_edit_release_page' do
it 'has the needed data to display the "edit release" page' do
keys = %i(project_id
keys = %i[project_id
group_id
group_milestones_available
project_path
@ -72,7 +72,7 @@ RSpec.describe ReleasesHelper do
new_milestone_path
upcoming_release_docs_path
edit_release_docs_path
delete_release_docs_path)
delete_release_docs_path]
expect(helper.data_for_edit_release_page.keys).to match_array(keys)
end
@ -80,7 +80,7 @@ RSpec.describe ReleasesHelper do
describe '#data_for_new_release_page' do
it 'has the needed data to display the "new release" page' do
keys = %i(project_id
keys = %i[project_id
group_id
group_milestones_available
project_path
@ -93,7 +93,7 @@ RSpec.describe ReleasesHelper do
new_milestone_path
default_branch
upcoming_release_docs_path
edit_release_docs_path)
edit_release_docs_path]
expect(helper.data_for_new_release_page.keys).to match_array(keys)
end
@ -101,9 +101,9 @@ RSpec.describe ReleasesHelper do
describe '#data_for_show_page' do
it 'has the needed data to display the individual "release" page' do
keys = %i(project_id
keys = %i[project_id
project_path
tag_name)
tag_name]
expect(helper.data_for_show_page.keys).to match_array(keys)
end

View File

@ -6,7 +6,7 @@ RSpec.describe TrackingHelper do
describe '#tracking_attrs' do
using RSpec::Parameterized::TableSyntax
let(:input) { %w(a b c) }
let(:input) { %w[a b c] }
let(:result) { { data: { track_label: 'a', track_action: 'b', track_property: 'c' } } }
before do

View File

@ -8,7 +8,7 @@ RSpec.describe 'Direct upload support' do
end
where(:config_name) do
%w(artifacts lfs uploads)
%w[artifacts lfs uploads]
end
with_them do

View File

@ -33,15 +33,15 @@ RSpec.describe 'Enumerator#next patch fix' do
end
def have_been_raised_by_next_and_not_fixed_up
contain_unique_method_calls_in_order %w(call_enum_method)
contain_unique_method_calls_in_order %w[call_enum_method]
end
def have_been_raised_by_enum_object_and_fixed_up
contain_unique_method_calls_in_order %w(make_error call_enum_method)
contain_unique_method_calls_in_order %w[make_error call_enum_method]
end
def have_been_raised_by_nested_next_and_fixed_up
contain_unique_method_calls_in_order %w(call_nested_next call_enum_method)
contain_unique_method_calls_in_order %w[call_nested_next call_enum_method]
end
methods = [

View File

@ -37,7 +37,7 @@ EOF
expect(described_class).not_to receive(:log_multipart_warning)
params = described_class.parse_multipart(env)
expect(params.keys).to include(*%w(reply fileupload))
expect(params.keys).to include(*%w[reply fileupload])
end
end
@ -56,7 +56,7 @@ EOF
})
params = described_class.parse_multipart(env)
expect(params.keys).to include(*%w(reply fileupload))
expect(params.keys).to include(*%w[reply fileupload])
end
end

View File

@ -62,7 +62,7 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do
it 'extracts the runner details', :aggregate_failures do
expect(details.keys).to match_array(
%w(system_id name version revision platform architecture executor config ip_address)
%w[system_id name version revision platform architecture executor config ip_address]
)
expect(details['system_id']).to eq(system_id)
expect(details['name']).to eq(name)

View File

@ -29,7 +29,7 @@ RSpec.describe API::Entities::User do
allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, user).and_return(can_read_user_profile)
end
%i(followers following is_followed).each do |relationship|
%i[followers following is_followed].each do |relationship|
shared_examples 'does not expose relationship' do
it "does not expose #{relationship}" do
expect(subject).not_to include(relationship)

View File

@ -44,7 +44,7 @@ RSpec.describe API::Helpers::CommonHelpers do
get '/test?array=&array_of_strings=test,me&array_of_ints=1,2'
expect(json_response['array']).to eq([])
expect(json_response['array_of_strings']).to eq(%w(test me))
expect(json_response['array_of_strings']).to eq(%w[test me])
expect(json_response['array_of_ints']).to eq([1, 2])
end
end

View File

@ -68,7 +68,7 @@ RSpec.describe Backup::Files, feature_category: :backup_restore do
it 'calls tar command with unlink' do
expect(subject).to receive(:tar).and_return('blabla-tar')
expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(blabla-tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args)
expect(subject).to receive(:run_pipeline!).with([%w[gzip -cd], %w[blabla-tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -]], any_args)
expect(subject).to receive(:pipeline_succeeded?).and_return(true)
subject.restore('registry.tar.gz', 'backup_id')
end
@ -124,7 +124,7 @@ RSpec.describe Backup::Files, feature_category: :backup_restore do
it 'excludes tmp dirs from archive' do
expect(subject).to receive(:tar).and_return('blabla-tar')
expect(subject).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found --exclude=./@pages.tmp -C /var/gitlab-pages -cf - .), 'gzip -c -1'], any_args)
expect(subject).to receive(:run_pipeline!).with([%w[blabla-tar --exclude=lost+found --exclude=./@pages.tmp -C /var/gitlab-pages -cf - .], 'gzip -c -1'], any_args)
subject.dump('registry.tar.gz', 'backup_id')
end
@ -146,7 +146,7 @@ RSpec.describe Backup::Files, feature_category: :backup_restore do
it 'excludes tmp dirs from rsync' do
expect(Gitlab::Popen).to receive(:popen)
.with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.with(%w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup])
.and_return(['', 0])
subject.dump('registry.tar.gz', 'backup_id')
@ -154,7 +154,7 @@ RSpec.describe Backup::Files, feature_category: :backup_restore do
it 'retries if rsync fails due to vanishing files' do
expect(Gitlab::Popen).to receive(:popen)
.with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.with(%w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup])
.and_return(['rsync failed', 24], ['', 0])
expect do
@ -164,7 +164,7 @@ RSpec.describe Backup::Files, feature_category: :backup_restore do
it 'raises an error and outputs an error message if rsync failed' do
allow(Gitlab::Popen).to receive(:popen)
.with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.with(%w[rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup])
.and_return(['rsync failed', 1])
expect do

View File

@ -156,7 +156,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
describe '#create' do
let(:incremental_env) { 'false' }
let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz task2.tar.gz} }
let(:expected_backup_contents) { %w[backup_information.yml task1.tar.gz task2.tar.gz] }
let(:backup_time) { Time.zone.parse('2019-1-1') }
let(:backup_id) { "1546300800_2019_01_01_#{Gitlab::VERSION}" }
let(:full_backup_id) { backup_id }
@ -223,7 +223,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
context 'when SKIP env is set' do
let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} }
let(:expected_backup_contents) { %w[backup_information.yml task1.tar.gz] }
before do
stub_env('SKIP', 'task2')
@ -237,7 +237,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
context 'when the destination is optional' do
let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz} }
let(:expected_backup_contents) { %w[backup_information.yml task1.tar.gz] }
let(:definitions) do
{
'task1' => Backup::Manager::TaskDefinition.new(task: task1, destination_path: 'task1.tar.gz'),
@ -1015,7 +1015,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
context 'when BACKUP variable is set to a correct file' do
let(:tar_cmdline) { %w{tar -xf 1451606400_2016_01_01_1.2.3_gitlab_backup.tar} }
let(:tar_cmdline) { %w[tar -xf 1451606400_2016_01_01_1.2.3_gitlab_backup.tar] }
let(:backup_id) { "1451606400_2016_01_01_1.2.3" }
before do

View File

@ -85,7 +85,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do
end
describe 'storages' do
let(:storages) { %w{default} }
let(:storages) { %w[default] }
let_it_be(:project) { create(:project_with_design, :repository) }
@ -280,7 +280,7 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do
end
context 'storages' do
let(:storages) { %w{default} }
let(:storages) { %w[default] }
before do
stub_storage_settings('test_second_storage' => {

View File

@ -28,14 +28,14 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
stub_application_setting(asset_proxy_enabled: true)
stub_application_setting(asset_proxy_secret_key: 'shared-secret')
stub_application_setting(asset_proxy_url: 'https://assets.example.com')
stub_application_setting(asset_proxy_allowlist: %w(gitlab.com *.mydomain.com))
stub_application_setting(asset_proxy_allowlist: %w[gitlab.com *.mydomain.com])
described_class.initialize_settings
expect(Gitlab.config.asset_proxy.enabled).to be_truthy
expect(Gitlab.config.asset_proxy.secret_key).to eq 'shared-secret'
expect(Gitlab.config.asset_proxy.url).to eq 'https://assets.example.com'
expect(Gitlab.config.asset_proxy.allowlist).to eq %w(gitlab.com *.mydomain.com)
expect(Gitlab.config.asset_proxy.allowlist).to eq %w[gitlab.com *.mydomain.com]
expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i)
end
@ -52,12 +52,12 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
it 'supports deprecated whitelist settings' do
stub_application_setting(asset_proxy_enabled: true)
stub_application_setting(asset_proxy_whitelist: %w(foo.com bar.com))
stub_application_setting(asset_proxy_whitelist: %w[foo.com bar.com])
stub_application_setting(asset_proxy_allowlist: [])
described_class.initialize_settings
expect(Gitlab.config.asset_proxy.allowlist).to eq %w(foo.com bar.com)
expect(Gitlab.config.asset_proxy.allowlist).to eq %w[foo.com bar.com]
end
end
@ -66,7 +66,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
stub_asset_proxy_setting(enabled: true)
stub_asset_proxy_setting(secret_key: 'shared-secret')
stub_asset_proxy_setting(url: 'https://assets.example.com')
stub_asset_proxy_setting(allowlist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host}))
stub_asset_proxy_setting(allowlist: %W[gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host}])
stub_asset_proxy_setting(domain_regexp: described_class.compile_allowlist(Gitlab.config.asset_proxy.allowlist))
@context = described_class.transform_context({})
end

View File

@ -178,7 +178,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning
it 'does not double-encode HTML entities' do
encoded_link = "#{link}?foo=bar&amp;baz=quux"
expected_encoded_link = %{<a href="#{encoded_link}">#{encoded_link}</a>}
expected_encoded_link = %(<a href="#{encoded_link}">#{encoded_link}</a>)
actual = unescape(filter(encoded_link).to_html)
expect(actual).to eq(Rinku.auto_link(encoded_link))

View File

@ -14,13 +14,13 @@ RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter, feature_categ
subject { filter(exp).to_html }
context 'allows `a` elements' do
let(:exp) { %q{<a href="/">Link</a>} }
let(:exp) { %q(<a href="/">Link</a>) }
it { is_expected.to eq(exp) }
end
context 'allows `br` elements' do
let(:exp) { %q{Hello<br>World} }
let(:exp) { %q(Hello<br>World) }
it { is_expected.to eq(exp) }
end
@ -29,21 +29,21 @@ RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter, feature_categ
let(:allowed_style) { 'color: red; border: blue; background: green; padding: 10px; margin: 10px; text-decoration: underline;' }
context 'allows specific properties' do
let(:exp) { %{<a href="#" style="#{allowed_style}">Stylish Link</a>} }
let(:exp) { %(<a href="#" style="#{allowed_style}">Stylish Link</a>) }
it { is_expected.to eq(exp) }
end
it 'disallows other properties in `style` attribute on `a` elements' do
style = [allowed_style, 'position: fixed'].join(';')
doc = filter(%{<a href="#" style="#{style}">Stylish Link</a>})
doc = filter(%(<a href="#" style="#{style}">Stylish Link</a>))
expect(doc.at_css('a')['style']).to eq(allowed_style)
end
end
context 'allows `class` on `a` elements' do
let(:exp) { %q{<a href="#" class="btn">Button Link</a>} }
let(:exp) { %q(<a href="#" class="btn">Button Link</a>) }
it { is_expected.to eq(exp) }
end

View File

@ -9,8 +9,8 @@ RSpec.describe Banzai::Filter::ImageLinkFilter, feature_category: :team_planning
let(:context) { {} }
def image(path, alt: nil, data_src: nil)
alt_tag = alt ? %{alt="#{alt}"} : ""
data_src_tag = data_src ? %{data-src="#{data_src}"} : ""
alt_tag = alt ? %(alt="#{alt}") : ""
data_src_tag = data_src ? %(data-src="#{data_src}") : ""
%(<img src="#{path}" #{alt_tag} #{data_src_tag} />)
end

View File

@ -13,7 +13,7 @@ RSpec.describe Banzai::Filter::References::AlertReferenceFilter, feature_categor
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Alert #{reference}</#{elem}>"
@ -47,7 +47,7 @@ RSpec.describe Banzai::Filter::References::AlertReferenceFilter, feature_categor
end
it 'escapes the title attribute' do
allow(alert).to receive(:title).and_return(%{"></a>whatever<a title="})
allow(alert).to receive(:title).and_return(%("></a>whatever<a title="))
doc = reference_filter("Alert #{reference}")
expect(doc.text).to eq "Alert #{reference}"
@ -79,7 +79,7 @@ RSpec.describe Banzai::Filter::References::AlertReferenceFilter, feature_categor
doc = reference_filter("Alert #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq urls.details_project_alert_management_url(project, alert.iid, only_path: true)
end
end

View File

@ -16,7 +16,7 @@ RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter, feature_c
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
@ -96,7 +96,7 @@ RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter, feature_c
doc = reference_filter("See #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq urls.project_compare_url(project, from: commit1.id, to: commit2.id, only_path: true)
end
end

View File

@ -12,7 +12,7 @@ RSpec.describe Banzai::Filter::References::CommitReferenceFilter, feature_catego
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
@ -61,7 +61,7 @@ RSpec.describe Banzai::Filter::References::CommitReferenceFilter, feature_catego
it 'escapes the title attribute' do
allow_next_instance_of(Commit) do |instance|
allow(instance).to receive(:title).and_return(%{"></a>whatever<a title="})
allow(instance).to receive(:title).and_return(%("></a>whatever<a title="))
end
doc = reference_filter("See #{reference}")
@ -93,7 +93,7 @@ RSpec.describe Banzai::Filter::References::CommitReferenceFilter, feature_catego
doc = reference_filter("See #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq urls.project_commit_url(project, reference, only_path: true)
end

View File

@ -77,7 +77,7 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter, feature_catego
end
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
context "wrapped in a <#{elem}/>" do
let(:input_text) { "<#{elem}>Design #{url_for_design(design)}</#{elem}>" }

View File

@ -14,7 +14,7 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter, feature
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
@ -59,7 +59,7 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter, feature
it 'escapes the title attribute' do
allow(project.external_issue_tracker).to receive(:title)
.and_return(%{"></a>whatever<a title="})
.and_return(%("></a>whatever<a title="))
doc = filter("Issue #{reference}")
expect(doc.text).to eq "Issue #{reference}"

View File

@ -13,7 +13,7 @@ RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter, feature_c
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Feature Flag #{reference}</#{elem}>"
@ -47,7 +47,7 @@ RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter, feature_c
end
it 'escapes the title attribute' do
allow(feature_flag).to receive(:name).and_return(%{"></a>whatever<a title="})
allow(feature_flag).to receive(:name).and_return(%("></a>whatever<a title="))
doc = reference_filter("Feature Flag #{reference}")
expect(doc.text).to eq "Feature Flag #{reference}"
@ -79,7 +79,7 @@ RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter, feature_c
doc = reference_filter("Feature Flag #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true)
end
end

View File

@ -27,7 +27,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
@ -77,7 +77,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
end
it 'escapes the title attribute' do
issue.update_attribute(:title, %{"></a>whatever<a title="})
issue.update_attribute(:title, %("></a>whatever<a title="))
doc = reference_filter("Issue #{written_reference}")
expect(doc.text).to eq "Issue #{reference}"
@ -128,7 +128,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
it 'does not escape the data-original attribute' do
inner_html = 'element <code>node</code> inside'
doc = reference_filter(%{<a href="#{written_reference}">#{inner_html}</a>})
doc = reference_filter(%(<a href="#{written_reference}">#{inner_html}</a>))
expect(doc.children.first.attr('data-original')).to eq inner_html
end
@ -163,7 +163,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
doc = reference_filter("Issue #{written_reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq issue_path
end
@ -381,7 +381,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
end
context 'cross-project reference in link href' do
let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
let(:reference_link) { %(<a href="#{reference}">Reference</a>) }
let(:reference) { issue.to_reference(project) }
let(:issue) { create(:issue, project: project2) }
let(:project2) { create(:project, :public, namespace: namespace) }
@ -412,7 +412,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
end
context 'cross-project URL in link href' do
let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
let(:reference_link) { %(<a href="#{reference}">Reference</a>) }
let(:reference) { (issue_url + "#note_123").to_s }
let(:issue) { create(:issue, project: project2) }
let(:project2) { create(:project, :public, namespace: namespace) }
@ -519,7 +519,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
it 'links to a valid reference for cross-reference in link href' do
reference = (issue_url + "#note_123").to_s
reference_link = %{<a href="#{reference}">Reference</a>}
reference_link = %(<a href="#{reference}">Reference</a>)
doc = reference_filter("See #{reference_link}", context)
@ -530,7 +530,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
it 'links to a valid reference for issue reference in the link href' do
reference = issue.to_reference(group)
reference_link = %{<a href="#{reference}">Reference</a>}
reference_link = %(<a href="#{reference}">Reference</a>)
doc = reference_filter("See #{reference_link}", context)
link = doc.css('a').first

View File

@ -19,7 +19,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Label #{reference}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
@ -64,14 +64,14 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
doc = reference_filter("Label #{reference}")
link = doc.css('a').first.attr('href')
expect(link).to match %r(https?://)
expect(link).to match %r{https?://}
end
it 'does not include protocol when :only_path true' do
doc = reference_filter("Label #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
end
it 'links to issue list when :label_url_method is not present' do
@ -118,7 +118,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)})
end
it 'ignores invalid label IDs' do
@ -142,7 +142,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}).")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\)\.))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{label.name}</span></a></span>\)\.})
end
it 'ignores invalid label names' do
@ -166,7 +166,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}).")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\)\.))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{label.name}</span></a></span>\)\.})
end
it 'ignores invalid label names' do
@ -191,7 +191,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'does not include trailing punctuation', :aggregate_failures do
['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation|
doc = filter("Label #{reference}#{trailing_punctuation}")
expect(doc.to_html).to match(%r(<span.+><a.+><span.+>\?g\.fm&amp;</span></a></span>#{Regexp.escape(trailing_punctuation)}))
expect(doc.to_html).to match(%r{<span.+><a.+><span.+>\?g\.fm&amp;</span></a></span>#{Regexp.escape(trailing_punctuation)}})
end
end
@ -217,7 +217,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)})
end
it 'ignores invalid label names' do
@ -241,7 +241,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)})
end
it 'ignores invalid label names' do
@ -265,7 +265,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>g\.fm &amp; references\?</span></a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>g\.fm &amp; references\?</span></a></span>\.\)})
end
it 'ignores invalid label names' do
@ -344,7 +344,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
end
describe 'referencing a label in a link href' do
let(:reference) { %{<a href="#{label.to_reference}">Label</a>} }
let(:reference) { %(<a href="#{label.to_reference}">Label</a>) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@ -355,7 +355,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+>Label</a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+>Label</a></span>\.\)})
end
it 'includes a data-project attribute' do
@ -393,7 +393,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{group_label.name}</span></a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{group_label.name}</span></a></span>\.\)})
end
it 'ignores invalid label names' do
@ -416,7 +416,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{group_label.name}</span></a></span>\.\)))
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>#{group_label.name}</span></a></span>\.\)})
end
it 'ignores invalid label names' do

View File

@ -12,7 +12,7 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter, feature_
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
@ -83,7 +83,7 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter, feature_
end
it 'escapes the title attribute' do
merge.update_attribute(:title, %{"></a>whatever<a title="})
merge.update_attribute(:title, %("></a>whatever<a title="))
doc = reference_filter("Merge #{reference}")
expect(doc.text).to eq "Merge #{reference}"
@ -141,7 +141,7 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter, feature_
doc = reference_filter("Merge #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq urls.project_merge_request_url(project, merge, only_path: true)
end
end

View File

@ -16,7 +16,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
end
shared_examples 'reference parsing' do
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>milestone #{reference}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
@ -49,7 +49,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
doc = reference_filter("Milestone #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).not_to match %r{https?://}
expect(link).to eq urls.milestone_path(milestone)
end
end
@ -63,7 +63,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'links with adjacent text' do
doc = reference_filter("Milestone (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+>#{milestone.reference_link_text}</a>\.\)))
expect(doc.to_html).to match(%r{\(<a.+>#{milestone.reference_link_text}</a>\.\)})
end
it 'ignores invalid milestone IIDs' do
@ -89,12 +89,12 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'links with adjacent text' do
doc = reference_filter("Milestone (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+>#{milestone.reference_link_text}</a>\.\)))
expect(doc.to_html).to match(%r{\(<a.+>#{milestone.reference_link_text}</a>\.\)})
end
it 'links with adjacent html tags' do
doc = reference_filter("Milestone <p>#{reference}</p>.")
expect(doc.to_html).to match(%r(<p><a.+>#{milestone.reference_link_text}</a></p>))
expect(doc.to_html).to match(%r{<p><a.+>#{milestone.reference_link_text}</a></p>})
end
it 'ignores invalid milestone names' do
@ -120,7 +120,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'links with adjacent text' do
doc = reference_filter("Milestone (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+>#{milestone.reference_link_text}</a>\.\)))
expect(doc.to_html).to match(%r{\(<a.+>#{milestone.reference_link_text}</a>\.\)})
end
it 'ignores invalid milestone names' do
@ -132,7 +132,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
shared_examples 'referencing a milestone in a link href' do
let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
let(:link_reference) { %{<a href="#{unquoted_reference}">Milestone</a>} }
let(:link_reference) { %(<a href="#{unquoted_reference}">Milestone</a>) }
before do
milestone.update!(name: 'gfm')
@ -146,7 +146,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'links with adjacent text' do
doc = reference_filter("Milestone (#{link_reference}.)")
expect(doc.to_html).to match(%r(\(<a.+>Milestone</a>\.\)))
expect(doc.to_html).to match(%r{\(<a.+>Milestone</a>\.\)})
end
it 'includes a data-project attribute' do
@ -169,7 +169,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
shared_examples 'linking to a milestone as the entire link' do
let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
let(:link) { urls.milestone_url(milestone) }
let(:link_reference) { %{<a href="#{link}">#{link}</a>} }
let(:link_reference) { %(<a href="#{link}">#{link}</a>) }
it 'replaces the link text with the milestone reference' do
doc = reference_filter("See #{link}")
@ -220,7 +220,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'escapes the name attribute' do
allow_next_instance_of(Milestone) do |instance|
allow(instance).to receive(:title).and_return(%{"></a>whatever<a title="})
allow(instance).to receive(:title).and_return(%("></a>whatever<a title="))
end
doc = reference_filter("See #{reference}")
@ -257,7 +257,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'escapes the name attribute' do
allow_next_instance_of(Milestone) do |instance|
allow(instance).to receive(:title).and_return(%{"></a>whatever<a title="})
allow(instance).to receive(:title).and_return(%("></a>whatever<a title="))
end
doc = reference_filter("See #{reference}")
@ -294,7 +294,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
it 'escapes the name attribute' do
allow_next_instance_of(Milestone) do |instance|
allow(instance).to receive(:title).and_return(%{"></a>whatever<a title="})
allow(instance).to receive(:title).and_return(%("></a>whatever<a title="))
end
doc = reference_filter("See #{reference}")

View File

@ -47,7 +47,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter, feature_categ
expect(doc.css('a').first.attr('href')).to eq urls.project_url(subject)
end
%w(pre code a style).each do |elem|
%w[pre code a style].each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Hey #{CGI.escapeHTML(reference)}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp

Some files were not shown because too many files have changed in this diff Show More