Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
129d7ea3db
commit
acc3d48da4
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -32,4 +32,5 @@ export default () => ({
|
|||
sort: PAGINATION_SORT_FIELD_END_EVENT,
|
||||
direction: PAGINATION_SORT_DIRECTION_DESC,
|
||||
},
|
||||
predefinedDateRange: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ query GetSnippetQuery($ids: [SnippetID!]) {
|
|||
webUrl
|
||||
httpUrlToRepo
|
||||
sshUrlToRepo
|
||||
hidden
|
||||
blobs {
|
||||
__typename
|
||||
hasUnretrievableBlobs
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -9,6 +9,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: 28d
|
||||
data_source: database
|
||||
instrumentation_class: CountJiraImportsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: 28d
|
||||
data_source: database
|
||||
instrumentation_class: CountCsvImportsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: CountJiraImportsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ value_type: number
|
|||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: CountCsvImportsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
f71caebfefb4b4fabbb3f9cc059f6f105ea352da11380901b16c687dcca1e6d4
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -141,9 +141,11 @@ describe('Value stream analytics component', () => {
|
|||
namespacePath: groupPath,
|
||||
endDate: createdBefore,
|
||||
hasDateRangeFilter: true,
|
||||
hasPredefinedDateRangesFilter: true,
|
||||
hasProjectFilter: false,
|
||||
selectedProjects: [],
|
||||
startDate: createdAfter,
|
||||
predefinedDateRange: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -261,3 +261,5 @@ export const basePaginationResult = {
|
|||
direction: PAGINATION_SORT_DIRECTION_DESC,
|
||||
page: null,
|
||||
};
|
||||
|
||||
export const predefinedDateRange = 'last_week';
|
||||
|
|
|
|||
|
|
@ -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 } }]}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export const createGQLSnippet = () => ({
|
|||
message: '',
|
||||
},
|
||||
},
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
export const createGQLSnippetsQueryResponse = (snippets) => ({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">'</span>def<span class="idiff right deletion">'</span>})
|
||||
expect(marked_old_line).to eq(%q(abc <span class="idiff left deletion">'</span>def<span class="idiff right deletion">'</span>))
|
||||
expect(marked_old_line).to be_html_safe
|
||||
expect(marked_new_line).to eq(%q{abc <span class="idiff left addition">"</span>def<span class="idiff right addition">"</span>})
|
||||
expect(marked_new_line).to eq(%q(abc <span class="idiff left addition">"</span>def<span class="idiff right addition">"</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"><img src=x onerror=alert(document.domain)></span>})
|
||||
expect(marked_new_line).to be_html_safe
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" http-equiv="refresh"})
|
||||
expect(tags).to include(%q(content="foo" http-equiv="refresh"))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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&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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>" }
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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&</span></a></span>#{Regexp.escape(trailing_punctuation)}))
|
||||
expect(doc.to_html).to match(%r{<span.+><a.+><span.+>\?g\.fm&</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 & references\?</span></a></span>\.\)))
|
||||
expect(doc.to_html).to match(%r{\(<span.+><a.+><span.+>g\.fm & 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue