Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-22 21:07:01 +00:00
parent 9db4bab965
commit dbdf272ee1
77 changed files with 1642 additions and 1109 deletions

View File

@ -82,7 +82,6 @@ Lint/AmbiguousRegexpLiteral:
- 'spec/services/loose_foreign_keys/cleaner_service_spec.rb'
- 'spec/services/snippets/repository_validation_service_spec.rb'
- 'spec/services/system_notes/merge_requests_service_spec.rb'
- 'spec/support/shared_examples/features/content_editor_shared_examples.rb'
- 'spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb'
- 'spec/support_specs/database/multiple_databases_helpers_spec.rb'
- 'spec/tasks/gitlab/gitaly_rake_spec.rb'

View File

@ -52,7 +52,7 @@ gem 'sprockets', '~> 3.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'view_component', '~> 3.8.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Supported DBs
gem 'pg', '~> 1.5.4' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'pg', '~> 1.5.5' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'neighbor', '~> 0.2.3' # rubocop:todo Gemfile/MissingFeatureCategory
@ -277,7 +277,7 @@ gem 're2', '2.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Misc
gem 'semver_dialects', '~> 1.6.1', feature_category: :static_application_security_testing
gem 'semver_dialects', '~> 1.6.2', feature_category: :static_application_security_testing
gem 'version_sorter', '~> 2.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'csv_builder', path: 'gems/csv_builder' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -460,7 +460,10 @@
{"name":"parslet","version":"1.8.2","platform":"ruby","checksum":"08d1ab3721cd3f175bfbee8788b2ddff71f92038f2d69bd65454c22bb9fbd98a"},
{"name":"pastel","version":"0.8.0","platform":"ruby","checksum":"481da9fb7d2f6e6b1a08faf11fa10363172dc40fd47848f096ae21209f805a75"},
{"name":"peek","version":"1.1.0","platform":"ruby","checksum":"d6501ead8cde46d8d8ed0d59eb6f0ba713d0a41c11a2c4a81447b2dce37b3ecc"},
{"name":"pg","version":"1.5.4","platform":"ruby","checksum":"04f7b247151c639a0b955d8e5a9a41541343f4640aa3c2bdf749a872c339d25d"},
{"name":"pg","version":"1.5.5","platform":"ruby","checksum":"7e4baa3395619424fe0e82d0b0489e54d3015c6ee5896dd007b3bce6d7d49b68"},
{"name":"pg","version":"1.5.5","platform":"x64-mingw-ucrt","checksum":"1adad3a4b4631e3676891639bab5aed68ac7c6a379d8314b768f74e6bdf0375e"},
{"name":"pg","version":"1.5.5","platform":"x64-mingw32","checksum":"98b1480a04e3f8aca9c7fc06dec5662117cec540e5c5058cb3a0812e8261adcc"},
{"name":"pg","version":"1.5.5","platform":"x86-mingw32","checksum":"4fd1e309c5d227ecb1704fc2b3f1168b13748a8d8b0eb7c09d834b28069b9433"},
{"name":"pg_query","version":"5.1.0","platform":"ruby","checksum":"b7f7f47c864f08ccbed46a8244906fb6ee77ee344fd27250717963928c93145d"},
{"name":"plist","version":"3.7.0","platform":"ruby","checksum":"703ca90a7cb00e8263edd03da2266627f6741d280c910abbbac07c95ffb2f073"},
{"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"},
@ -593,7 +596,7 @@
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
{"name":"selenium-webdriver","version":"4.18.1","platform":"ruby","checksum":"abe8daa474c9fa8b94b6462a0cdbe093140218f876e58e3911bb064a60d45eab"},
{"name":"semver_dialects","version":"1.6.1","platform":"ruby","checksum":"6e5f592d8958480ea9dab80cbbbef0eea1032d373968a15b07786eae3f5e558e"},
{"name":"semver_dialects","version":"1.6.2","platform":"ruby","checksum":"ac05ed56aa386292e7e0e8f648f57fdfbc0db78dbadfec0b1dc9b5ec1a8d7234"},
{"name":"sentry-rails","version":"5.10.0","platform":"ruby","checksum":"99aa2fac136c26942eb1897c65de65dac88ad43ac5eb183ff20711287a137ebd"},
{"name":"sentry-raven","version":"3.1.2","platform":"ruby","checksum":"103d3b122958810d34898ce2e705bcf549ddb9d855a70ce9a3970ee2484f364a"},
{"name":"sentry-ruby","version":"5.10.0","platform":"ruby","checksum":"115c24c0aee1309210f3a2988fb118e2bec1f11609feeda90e694388b1183619"},

View File

@ -1270,7 +1270,7 @@ GEM
tty-color (~> 0.5)
peek (1.1.0)
railties (>= 4.0.0)
pg (1.5.4)
pg (1.5.5)
pg_query (5.1.0)
google-protobuf (>= 3.22.3)
plist (3.7.0)
@ -1560,7 +1560,7 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semver_dialects (1.6.1)
semver_dialects (1.6.2)
deb_version (~> 1.0.1)
pastel (~> 0.8.0)
thor (~> 1.3)
@ -2047,7 +2047,7 @@ DEPENDENCIES
parser (~> 3.3, >= 3.3.0.2)
parslet (~> 1.8)
peek (~> 1.1)
pg (~> 1.5.4)
pg (~> 1.5.5)
pg_query (~> 5.1.0)
png_quantizator (~> 0.2.1)
premailer-rails (~> 1.10.3)
@ -2100,7 +2100,7 @@ DEPENDENCIES
sd_notify (~> 0.1.0)
seed-fu (~> 2.3.7)
selenium-webdriver (~> 4.18, >= 4.18.1)
semver_dialects (~> 1.6.1)
semver_dialects (~> 1.6.2)
sentry-rails (~> 5.10.0)
sentry-raven (~> 3.1)
sentry-ruby (~> 5.10.0)

View File

@ -1540,6 +1540,24 @@
}
}
]
},
"exit_codes": {
"markdownDescription": "Either a single or array of exit codes to trigger job retry on. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retryexit_codes).",
"oneOf": [
{
"description": "Retry when the job exit code is included in the array's values.",
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "integer"
}
},
{
"description": "Retry when the job exit code is equal to.",
"type": "integer"
}
]
}
}
}

View File

@ -66,6 +66,9 @@ export const organizationProjects = [
userPermissions: {
removeProject: true,
},
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Project/7',
@ -92,6 +95,9 @@ export const organizationProjects = [
userPermissions: {
removeProject: true,
},
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Project/6',
@ -118,6 +124,9 @@ export const organizationProjects = [
userPermissions: {
removeProject: true,
},
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Project/5',
@ -144,6 +153,9 @@ export const organizationProjects = [
userPermissions: {
removeProject: true,
},
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Project/1',
@ -170,6 +182,9 @@ export const organizationProjects = [
userPermissions: {
removeProject: false,
},
maxAccessLevel: {
integerValue: 30,
},
},
];
@ -186,6 +201,9 @@ export const organizationGroups = [
projectsCount: 3,
groupMembersCount: 2,
visibility: 'public',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/33',
@ -199,6 +217,9 @@ export const organizationGroups = [
projectsCount: 3,
groupMembersCount: 1,
visibility: 'private',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/24',
@ -212,6 +233,9 @@ export const organizationGroups = [
projectsCount: 1,
groupMembersCount: 2,
visibility: 'internal',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/27',
@ -225,6 +249,9 @@ export const organizationGroups = [
projectsCount: 2,
groupMembersCount: 3,
visibility: 'public',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/31',
@ -237,6 +264,9 @@ export const organizationGroups = [
projectsCount: 3,
groupMembersCount: 10,
visibility: 'private',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/22',
@ -250,6 +280,9 @@ export const organizationGroups = [
projectsCount: 3,
groupMembersCount: 40,
visibility: 'internal',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/35',
@ -263,6 +296,9 @@ export const organizationGroups = [
projectsCount: 30,
groupMembersCount: 100,
visibility: 'public',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/73',
@ -275,6 +311,9 @@ export const organizationGroups = [
projectsCount: 1,
groupMembersCount: 1,
visibility: 'private',
maxAccessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/74',
@ -289,6 +328,9 @@ export const organizationGroups = [
projectsCount: 4,
groupMembersCount: 4,
visibility: 'internal',
maxAccessLevel: {
integerValue: 30,
},
},
];

View File

@ -31,6 +31,9 @@ query getOrganizationGroups(
projectsCount
groupMembersCount
visibility
maxAccessLevel {
integerValue
}
}
pageInfo {
...PageInfo

View File

@ -34,6 +34,9 @@ query getOrganizationProjects(
userPermissions {
removeProject
}
maxAccessLevel {
integerValue
}
}
pageInfo {
...PageInfo

View File

@ -22,6 +22,7 @@ export const formatProjects = (projects) =>
forkingAccessLevel,
webUrl,
userPermissions,
maxAccessLevel: accessLevel,
...project
}) => ({
...project,
@ -32,6 +33,7 @@ export const formatProjects = (projects) =>
forkingAccessLevel: forkingAccessLevel.stringValue,
webUrl,
isForked: false,
accessLevel,
editPath: `${webUrl}/edit`,
availableActions: availableProjectActions(userPermissions),
actionLoadingStates: {
@ -41,11 +43,12 @@ export const formatProjects = (projects) =>
);
export const formatGroups = (groups) =>
groups.map(({ id, webUrl, parent, ...group }) => ({
groups.map(({ id, webUrl, parent, maxAccessLevel: accessLevel, ...group }) => ({
...group,
id: getIdFromGraphQLId(id),
webUrl,
parent: parent?.id || null,
accessLevel,
editPath: `${webUrl}/-/edit`,
availableActions: [ACTION_EDIT, ACTION_DELETE],
}));

View File

@ -5,6 +5,7 @@ import { mapActions, mapState } from 'vuex';
import { createAlert, VARIANT_INFO } from '~/alert';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
@ -34,11 +35,9 @@ export default {
computed: {
...mapState({
emptyListIllustration: (state) => state.config.emptyListIllustration,
emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
filter: (state) => state.filter,
isGroupPage: (state) => state.config.isGroupPage,
selectedType: (state) => state.selectedType,
packageHelpUrl: (state) => state.config.packageHelpUrl,
packagesCount: (state) => state.pagination?.total,
}),
emptySearch() {
@ -92,12 +91,13 @@ export default {
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
},
terraformRegistryHelpUrl: helpPagePath('user/packages/terraform_module_registry/index'),
};
</script>
<template>
<div>
<infrastructure-title :help-url="packageHelpUrl" :count="packagesCount" />
<infrastructure-title :help-url="$options.terraformRegistryHelpUrl" :count="packagesCount" />
<infrastructure-search v-if="packagesCount > 0" @update="requestPackagesList" />
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
@ -107,7 +107,9 @@ export default {
<gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
<gl-sprintf v-else :message="noResultsText">
<template #noPackagesLink="{ content }">
<gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
<gl-link :href="$options.terraformRegistryHelpUrl" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</template>

View File

@ -9,7 +9,6 @@ export default () => ({
* resourceId: String,
* pageType: String,
* emptyListIllustration: String,
* emptyListHelpUrl: String,
* comingSoon: { projectPath: String, suggestedContributions : String } | null;
* }
*/

View File

@ -1,23 +1,26 @@
<script>
import { nextTick } from 'vue';
import { GlForm, GlButton } from '@gitlab/ui';
import { GlForm, GlButton, GlFormGroup } from '@gitlab/ui';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import { isUserBusy, computedClearStatusAfterValue } from '~/set_status_modal/utils';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import { i18n, statusI18n } from '../constants';
import { i18n, statusI18n, timezoneI18n } from '../constants';
import UserAvatar from './user_avatar.vue';
export default {
components: {
UserAvatar,
GlForm,
GlFormGroup,
GlButton,
SettingsBlock,
SetStatusForm,
TimezoneDropdown,
},
inject: [
'currentEmoji',
@ -25,6 +28,8 @@ export default {
'currentAvailability',
'defaultEmoji',
'currentClearStatusAfter',
'timezones',
'userTimezone',
],
props: {
profilePath: {
@ -46,6 +51,7 @@ export default {
availability: isUserBusy(this.currentAvailability),
clearStatusAfter: null,
},
timezone: this.userTimezone,
};
},
computed: {
@ -82,6 +88,8 @@ export default {
formData.append('user[avatar]', this.avatarBlob, 'avatar.png');
}
formData.append('user[timezone]', this.timezone);
try {
const { data } = await axios.put(this.profilePath, formData);
@ -127,10 +135,14 @@ export default {
onAvailabilityInput(value) {
this.status.availability = value;
},
onTimezoneInput(selectedTimezone) {
this.timezone = selectedTimezone.identifier || '';
},
},
i18n: {
...i18n,
...statusI18n,
...timezoneI18n,
},
};
</script>
@ -156,6 +168,13 @@ export default {
/>
</div>
</settings-block>
<settings-block class="js-search-settings-section">
<template #title>{{ $options.i18n.setTimezoneTitle }}</template>
<template #description>{{ $options.i18n.setTimezoneDescription }}</template>
<gl-form-group :label="__('Timezone')" class="gl-md-form-input-lg">
<timezone-dropdown :value="timezone" :timezone-data="timezones" @input="onTimezoneInput" />
</gl-form-group>
</settings-block>
<!-- TODO: to implement profile editing form fields -->
<!-- It will be implemented in the upcoming MRs -->
<!-- Related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/389918 -->

View File

@ -28,6 +28,11 @@ export const statusI18n = {
),
};
export const timezoneI18n = {
setTimezoneTitle: s__('Profiles|Time settings'),
setTimezoneDescription: s__('Profiles|Set your local time zone.'),
};
export const i18n = {
updateProfileSettings: s__('Profiles|Update profile settings'),
cancel: __('Cancel'),

View File

@ -15,6 +15,8 @@ export const initProfileEdit = () => {
currentAvailability,
defaultEmoji,
currentClearStatusAfter,
timezones,
userTimezone,
...provides
} = mountEl.dataset;
@ -31,6 +33,8 @@ export const initProfileEdit = () => {
hasAvatar: parseBoolean(provides.hasAvatar),
gravatarEnabled: parseBoolean(provides.gravatarEnabled),
gravatarLink: JSON.parse(provides.gravatarLink),
timezones: JSON.parse(timezones),
userTimezone,
},
render(createElement) {
return createElement(ProfileEditApp, {

View File

@ -184,7 +184,7 @@ export default {
<template #prepend>
<emoji-picker
dropdown-class="gl-h-full"
toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none! gl-h-7!"
:right="false"
@click="handleEmojiClick"
>

View File

@ -3,7 +3,7 @@ import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText, GlBadge }
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
@ -66,7 +66,7 @@ export default {
return ACCESS_LEVEL_LABELS[this.accessLevel];
},
shouldShowAccessLevel() {
return this.accessLevel !== undefined;
return this.accessLevel !== undefined && this.accessLevel !== ACCESS_LEVEL_NO_ACCESS_INTEGER;
},
groupIconName() {
return this.group.parent ? 'subgroup' : 'group';
@ -138,9 +138,13 @@ export default {
/>
</div>
<div class="gl-px-2">
<gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
accessLevelLabel
}}</gl-badge>
<gl-badge
v-if="shouldShowAccessLevel"
size="sm"
class="gl-display-block"
data-testid="access-level-badge"
>{{ accessLevelLabel }}</gl-badge
>
</div>
</div>
</div>

View File

@ -12,7 +12,7 @@ import {
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
import { FEATURABLE_ENABLED } from '~/featurable/constants';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
@ -74,8 +74,8 @@ export default {
* issuesAccessLevel: string;
* forkingAccessLevel: string;
* openIssuesCount: number;
* permissions: {
* projectAccess: { accessLevel: 50 };
* maxAccessLevel: {
* integerValue: number;
* };
* descriptionHtml: string;
* updatedAt: string;
@ -111,13 +111,13 @@ export default {
return PROJECT_VISIBILITY_TYPE[this.visibility];
},
accessLevel() {
return this.project.permissions?.projectAccess?.accessLevel;
return this.project.accessLevel?.integerValue;
},
accessLevelLabel() {
return ACCESS_LEVEL_LABELS[this.accessLevel];
},
shouldShowAccessLevel() {
return this.accessLevel !== undefined;
return this.accessLevel !== undefined && this.accessLevel !== ACCESS_LEVEL_NO_ACCESS_INTEGER;
},
starsHref() {
return `${this.project.webUrl}/-/starrers`;
@ -254,9 +254,13 @@ export default {
/>
</div>
<div class="gl-px-2">
<gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
accessLevelLabel
}}</gl-badge>
<gl-badge
v-if="shouldShowAccessLevel"
size="sm"
class="gl-display-block"
data-testid="access-level-badge"
>{{ accessLevelLabel }}</gl-badge
>
</div>
</div>
</div>

View File

@ -67,28 +67,6 @@ class IssuesFinder < IssuableFinder
super.with_projects_matching_search_data
end
override :use_full_text_search?
def use_full_text_search?
return false if include_namespace_level_work_items?
super
end
override :by_parent
def by_parent(items)
return super unless include_namespace_level_work_items?
relations = [group_namespaces, project_namespaces].compact
namespaces = if relations.one?
relations.first
else
Namespace.from_union(relations)
end
items.in_namespaces(namespaces)
end
def group_namespaces
return if params[:project_id] || params[:projects]
@ -146,10 +124,6 @@ class IssuesFinder < IssuableFinder
items.without_issue_type(issue_type_params)
end
def include_namespace_level_work_items?
params.group? && Feature.enabled?(:namespace_level_work_items, params.group)
end
end
IssuesFinder.prepend_mod_with('IssuesFinder')

View File

@ -39,5 +39,31 @@ module WorkItems
rescue NameError
nil
end
override :use_full_text_search?
def use_full_text_search?
return false if include_namespace_level_work_items?
super
end
override :by_parent
def by_parent(items)
return super unless include_namespace_level_work_items?
relations = [group_namespaces, project_namespaces].compact
namespaces = if relations.one?
relations.first
else
Namespace.from_union(relations)
end
items.in_namespaces(namespaces)
end
def include_namespace_level_work_items?
params.group? && Feature.enabled?(:namespace_level_work_items, params.group)
end
end
end

View File

@ -81,6 +81,8 @@ module ProfilesHelper
brand_profile_image_guidelines: current_appearance&.profile_image_guidelines? ? brand_profile_image_guidelines : '',
cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css'),
user_path: user_path(current_user),
timezones: timezone_data_with_unique_identifiers.to_json,
user_timezone: user.timezone,
**user_status_properties(user)
}
end

View File

@ -59,8 +59,7 @@ module Ci
return true unless accessed_project.ci_inbound_job_token_scope_enabled?
inbound_linked_as_accessible?(accessed_project) ||
(::Feature.enabled?(:ci_job_token_groups_allowlist, accessed_project) &&
group_linked_as_accessible?(accessed_project))
group_linked_as_accessible?(accessed_project)
end
# We don't check the inbound allowlist here. That is because

View File

@ -1,9 +1,5 @@
- page_title _("Terraform Module Registry")
.row
.col-12
#js-vue-packages-list{ data: { resource_id: @group.id,
page_type: 'groups',
empty_list_help_url: help_page_path('user/infrastructure/index'),
empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg'),
package_help_url: help_page_path('user/infrastructure/index') } }
#js-vue-packages-list{ data: { resource_id: @group.id,
page_type: 'groups',
empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg') } }

View File

@ -1,9 +1,5 @@
- page_title _("Terraform Module Registry")
.row
.col-12
#js-vue-packages-list{ data: { resource_id: @project.id,
page_type: 'project',
empty_list_help_url: help_page_path('user/infrastructure/index'),
empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg'),
package_help_url: help_page_path('user/infrastructure/index') } }
#js-vue-packages-list{ data: { resource_id: @project.id,
page_type: 'project',
empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg') } }

View File

@ -1,8 +0,0 @@
---
name: ci_job_token_groups_allowlist
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142441
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/439611
milestone: '16.9'
type: development
group: group::pipeline security
default_enabled: false

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Documentation for the REST API for Git commits in GitLab."
---
# Commits API

View File

@ -6694,6 +6694,37 @@ Input type: `PromoteToEpicInput`
| <a id="mutationpromotetoepicerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationpromotetoepicissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
### `Mutation.provisionGoogleCloudRunner`
Provisions a runner in Google Cloud.
NOTE:
**Introduced** in 16.10.
**Status**: Experiment.
Input type: `ProvisionGoogleCloudRunnerInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprovisiongooglecloudrunnerclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprovisiongooglecloudrunnerdryrun"></a>`dryRun` | [`Boolean`](#boolean) | If true, returns the Terraform script without executing it. Defaults to false. True is currently not supported. |
| <a id="mutationprovisiongooglecloudrunnerprojectpath"></a>`projectPath` | [`ID!`](#id) | Project to create the runner in. |
| <a id="mutationprovisiongooglecloudrunnerprovisioningmachinetype"></a>`provisioningMachineType` | [`String!`](#string) | Name of the machine type to use for provisioning the runner. |
| <a id="mutationprovisiongooglecloudrunnerprovisioningprojectid"></a>`provisioningProjectId` | [`String!`](#string) | Identifier of the project where the runner is provisioned. |
| <a id="mutationprovisiongooglecloudrunnerprovisioningregion"></a>`provisioningRegion` | [`String!`](#string) | Name of the region to provision the runner in. |
| <a id="mutationprovisiongooglecloudrunnerprovisioningzone"></a>`provisioningZone` | [`String!`](#string) | Name of the zone to provision the runner in. |
| <a id="mutationprovisiongooglecloudrunnerrunnertoken"></a>`runnerToken` | [`String`](#string) | Authentication token of the runner. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprovisiongooglecloudrunnerclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprovisiongooglecloudrunnererrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationprovisiongooglecloudrunnerprovisioningsteps"></a>`provisioningSteps` | [`[CiRunnerCloudProvisioningStep!]`](#cirunnercloudprovisioningstep) | Steps used to provision the runner. |
### `Mutation.refreshStandardsAdherenceChecks`
Input type: `RefreshStandardsAdherenceChecksInput`
@ -16482,6 +16513,18 @@ Region used for runner cloud provisioning.
| <a id="cirunnercloudprovisioningregiondescription"></a>`description` | [`String`](#string) | Description of the region. |
| <a id="cirunnercloudprovisioningregionname"></a>`name` | [`String`](#string) | Name of the region. |
### `CiRunnerCloudProvisioningStep`
Step used to provision the runner to Google Cloud.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnercloudprovisioningstepinstructions"></a>`instructions` | [`String`](#string) | Instructions to provision the runner. |
| <a id="cirunnercloudprovisioningsteplanguageidentifier"></a>`languageIdentifier` | [`String`](#string) | Identifier of the language used for the instructions field. This identifier can be any of the identifiers specified in the [list of supported languages and lexers](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers). |
| <a id="cirunnercloudprovisioningsteptitle"></a>`title` | [`String`](#string) | Title of the step. |
### `CiRunnerCloudProvisioningZone`
Zone used for runner cloud provisioning.

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
description: "Documentation for the REST API for managing Git repository files in GitLab."
---
# Repository files API

View File

@ -3793,7 +3793,7 @@ If not defined, defaults to `0` and jobs do not retry.
When a job fails, the job is processed up to two more times, until it succeeds or
reaches the maximum number of retries.
By default, all failure types cause the job to be retried. Use [`retry:when`](#retrywhen)
By default, all failure types cause the job to be retried. Use [`retry:when`](#retrywhen) or [`retry:exit_codes`](#retryexit_codes)
to select which failures to retry on.
**Keyword type**: Job keyword. You can use it only as part of a job or in the
@ -3809,8 +3809,20 @@ to select which failures to retry on.
test:
script: rspec
retry: 2
test_advanced:
script:
- echo "Run a script that results in exit code 137."
- exit 137
retry:
max: 2
when: runner_system_failure
exit_codes: 137
```
`test_advanced` will be retried up to 2 times if the exit code is `137` or if it had
a runner system failure.
#### `retry:when`
Use `retry:when` with `retry:max` to retry jobs for only specific failure cases.
@ -3872,6 +3884,48 @@ test:
- stuck_or_timeout_failure
```
#### `retry:exit_codes`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/430037) in GitLab 16.10 [with a flag](../../administration/feature_flags.md) named `ci_retry_on_exit_codes`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
an administrator can [enable the feature flag](../../administration/feature_flags.md) named `ci_retry_on_exit_codes`.
Use `retry:exit_codes` with `retry:max` to retry jobs for only specific failure cases.
`retry:max` is the maximum number of retries, like [`retry`](#retry), and can be
`0`, `1`, or `2`.
**Keyword type**: Job keyword. You can use it only as part of a job or in the
[`default` section](#default).
**Possible inputs**:
- A single exit code.
- An array of exit codes.
**Example of `retry:exit_codes`**:
```yaml
test_job_1:
script:
- echo "Run a script that results in exit code 1. This job isn't retried."
- exit 1
retry:
max: 2
exit_codes: 137
test_job_2:
script:
- echo "Run a script that results in exit code 137. This job will be retried."
- exit 137
retry:
max: 1
exit_codes:
- 255
- 137
```
**Related topics**:
You can specify the number of [retry attempts for certain stages of job execution](../runners/configure_runners.md#job-stages-attempts)

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Use Code Owners to define experts for your code base, and set review requirements based on file type or location."
---
# Code Owners

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
description: "To ensure all changes are reviewed, configure optional or required approvals for merge requests in your project."
---
# Merge request approvals

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Understand and configure the commit squashing options available in GitLab."
---
# Squash and merge

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Use repository mirroring to push or pull the contents of a Git repository into another repository."
---
# Repository mirroring

View File

@ -7,9 +7,9 @@ module Backup
attr_reader :progress, :remote_storage, :options
def initialize(progress, definitions: nil)
def initialize(progress, backup_tasks: nil)
@progress = progress
@definitions = definitions
@backup_tasks = backup_tasks
@options = Backup::Options.new
@metadata = Backup::Metadata.new(manifest_filepath)
@options.extract_from_env! # preserve existing behavior
@ -29,28 +29,28 @@ module Backup
puts_time "Backup #{backup_id} is done."
end
def run_create_task(task_name)
# @param [Gitlab::Backup::Tasks::Task] task
def run_create_task(task)
build_backup_information
definition = definitions[task_name]
destination_dir = backup_path.join(definition.destination_path)
destination_dir = backup_path.join(task.destination_path)
unless definition.enabled?
puts_time "Dumping #{definition.human_name} ... ".color(:blue) + "[DISABLED]".color(:cyan)
unless task.enabled?
puts_time "Dumping #{task.human_name} ... ".color(:blue) + "[DISABLED]".color(:cyan)
return
end
if options.skip_task?(task_name)
puts_time "Dumping #{definition.human_name} ... ".color(:blue) + "[SKIPPED]".color(:cyan)
if options.skip_task?(task.id)
puts_time "Dumping #{task.human_name} ... ".color(:blue) + "[SKIPPED]".color(:cyan)
return
end
puts_time "Dumping #{definition.human_name} ... ".color(:blue)
definition.target.dump(destination_dir, backup_id)
puts_time "Dumping #{definition.human_name} ... ".color(:blue) + "done".color(:green)
puts_time "Dumping #{task.human_name} ... ".color(:blue)
task.target.dump(destination_dir, backup_id)
puts_time "Dumping #{task.human_name} ... ".color(:blue) + "done".color(:green)
rescue Backup::DatabaseBackupError, Backup::FileBackupError => e
puts_time "Dumping #{definition.human_name} failed: #{e.message}".color(:red)
puts_time "Dumping #{task.human_name} failed: #{e.message}".color(:red)
end
def restore
@ -62,29 +62,29 @@ module Backup
puts_time "Restore task is done."
end
def run_restore_task(task_name)
# @param [Gitlab::Backup::Tasks::Task] task
def run_restore_task(task)
read_backup_information
definition = definitions[task_name]
destination_dir = backup_path.join(definition.destination_path)
destination_dir = backup_path.join(task.destination_path)
unless definition.enabled?
puts_time "Restoring #{definition.human_name} ... ".color(:blue) + "[DISABLED]".color(:cyan)
unless task.enabled?
puts_time "Restoring #{task.human_name} ... ".color(:blue) + "[DISABLED]".color(:cyan)
return
end
puts_time "Restoring #{definition.human_name} ... ".color(:blue)
puts_time "Restoring #{task.human_name} ... ".color(:blue)
warning = definition.target.pre_restore_warning
warning = task.target.pre_restore_warning
if warning.present?
puts_time warning.color(:red)
Gitlab::TaskHelpers.ask_to_continue
end
definition.target.restore(destination_dir, backup_id)
puts_time "Restoring #{definition.human_name} ... ".color(:blue) + "done".color(:green)
task.target.restore(destination_dir, backup_id)
puts_time "Restoring #{task.human_name} ... ".color(:blue) + "done".color(:green)
warning = definition.target.post_restore_warning
warning = task.target.post_restore_warning
if warning.present?
puts_time warning.color(:red)
Gitlab::TaskHelpers.ask_to_continue
@ -95,13 +95,24 @@ module Backup
exit 1
end
# Finds a task by id
#
# @param [String] task_id
# @return [Backup::Tasks::Task]
def find_task(task_id)
backup_tasks[task_id].tap do |task|
raise ArgumentError, "Cannot find task with name: #{task_id}" unless task
end
end
private
def definitions
@definitions ||= {
# @return [Hash<String, Backup::Tasks::Task>]
def backup_tasks
@backup_tasks ||= {
Backup::Tasks::Database.id => Backup::Tasks::Database.new(progress: progress, options: options),
Backup::Tasks::Repositories.id => Backup::Tasks::Repositories.new(progress: progress, options: options,
server_side: backup_information[:repositories_server_side]),
server_side_callable: -> { backup_information[:repositories_server_side] }),
Backup::Tasks::Uploads.id => Backup::Tasks::Uploads.new(progress: progress, options: options),
Backup::Tasks::Builds.id => Backup::Tasks::Builds.new(progress: progress, options: options),
Backup::Tasks::Artifacts.id => Backup::Tasks::Artifacts.new(progress: progress, options: options),
@ -123,9 +134,7 @@ module Backup
build_backup_information
definitions.each_key do |task_name|
run_create_task(task_name)
end
backup_tasks.each_value { |task| run_create_task(task) }
write_backup_information
@ -144,9 +153,9 @@ module Backup
read_backup_information
verify_backup_version
definitions.each do |task_name, definition|
if !options.skip_task?(task_name) && definition.enabled?
run_restore_task(task_name)
backup_tasks.each_value do |task|
if !options.skip_task?(task.id) && task.enabled?
run_restore_task(task)
end
end
@ -246,8 +255,8 @@ module Backup
puts_time "Deleting tar staging files ... ".color(:blue)
remove_backup_path(MANIFEST_NAME)
definitions.each do |_, definition|
remove_backup_path(definition.cleanup_path || definition.destination_path)
backup_tasks.each_value do |task|
remove_backup_path(task.cleanup_path || task.destination_path)
end
puts_time "Deleting tar staging files ... ".color(:blue) + 'done'.color(:green)
@ -409,11 +418,11 @@ module Backup
end
def backup_contents
[MANIFEST_NAME] + definitions.reject do |name, definition|
options.skip_task?(name) || # task skipped via CLI option
!definition.enabled? || # task disabled via definition/configuration
(definition.destination_optional && !File.exist?(backup_path.join(definition.destination_path)))
end.values.map(&:destination_path)
[MANIFEST_NAME] + backup_tasks.values.reject do |task|
options.skip_task?(task.id) || # task skipped via CLI option
!task.enabled? || # task disabled via code/configuration
(task.destination_optional && !File.exist?(backup_path.join(task.destination_path)))
end.map(&:destination_path)
end
def tar_file

View File

@ -3,12 +3,12 @@
module Backup
module Tasks
class Repositories < Task
attr_reader :server_side
attr_reader :server_side_callable
def self.id = 'repositories'
def initialize(progress:, options:, server_side:)
@server_side = server_side
def initialize(progress:, options:, server_side_callable:)
@server_side_callable = server_side_callable
super(progress: progress, options: options)
end
@ -24,7 +24,7 @@ module Backup
incremental: options.incremental?,
max_parallelism: options.max_parallelism,
storage_parallelism: options.max_storage_parallelism,
server_side: server_side
server_side: server_side_callable.call
)
::Backup::Targets::Repositories.new(progress,

View File

@ -16,8 +16,8 @@ module Backup
end
# Key string that identifies the task
def key
raise NotImplementedError
def id
self.class.id
end
# Name of the task used for logging.

View File

@ -22,15 +22,21 @@ module Tasks
end
end
def self.create_task(task)
def self.create_task(task_id)
lock_backup do
::Backup::Manager.new(backup_progress).run_create_task(task)
backup_manager = ::Backup::Manager.new(backup_progress)
task = backup_manager.find_task(task_id)
backup_manager.run_create_task(task)
end
end
def self.restore_task(task)
def self.restore_task(task_id)
lock_backup do
::Backup::Manager.new(backup_progress).run_restore_task(task)
backup_manager = ::Backup::Manager.new(backup_progress)
task = backup_manager.find_task(task_id)
backup_manager.run_restore_task(task)
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :requires_admin, product_group: :pipeline_execution do
RSpec.describe 'Verify', :requires_admin, :reliable, product_group: :pipeline_execution do
describe 'When user is blocked' do
let!(:admin_api_client) { Runtime::API::Client.as_admin }
let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_security do
RSpec.describe 'Verify', :runner, :reliable, product_group: :pipeline_security do
describe "Unlocking job artifacts across pipelines" do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) { create(:project, name: 'unlock-job-artifacts-project') }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_authoring do
RSpec.describe 'Verify', :runner, :reliable, product_group: :pipeline_authoring do
describe 'Trigger matrix' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) { create(:project, name: 'project-with-pipeline') }

View File

@ -33,9 +33,7 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
end
end
# do not test quick actions here since guest users don't have permission
# to execute all quick actions
it_behaves_like 'edits content using the content editor', { with_quick_actions: false }
it_behaves_like 'rich text editor - common'
it "adds comment with code block" do
code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)"

View File

@ -318,7 +318,13 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
visit(new_project_issue_path(project))
end
it_behaves_like 'edits content using the content editor'
it_behaves_like 'rich text editor - autocomplete'
it_behaves_like 'rich text editor - code blocks'
it_behaves_like 'rich text editor - common'
it_behaves_like 'rich text editor - copy/paste'
it_behaves_like 'rich text editor - links'
it_behaves_like 'rich text editor - media'
it_behaves_like 'rich text editor - selection'
end
context "when signed in as user with special characters in their name" do

View File

@ -26,7 +26,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
visit edit_project_issue_path(project, issue)
end
it_behaves_like 'edits content using the content editor'
it_behaves_like 'rich text editor - common'
it "previews content", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391757' do
form = first(".gfm-form")

View File

@ -33,9 +33,7 @@ RSpec.describe 'User comments on a merge request', :js, feature_category: :code_
end
end
context 'with content editor' do
it_behaves_like 'edits content using the content editor'
end
it_behaves_like 'rich text editor - common'
it 'replys to a new comment' do
page.within('.js-main-target-form') do

View File

@ -109,5 +109,5 @@ RSpec.describe 'User edits a merge request', :js, feature_category: :code_review
end
end
it_behaves_like 'edits content using the content editor'
it_behaves_like 'rich text editor - common'
end

View File

@ -21,6 +21,29 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
expect(page).to have_button 'Merge'
end
end
context 'when an active pipeline running' do
let!(:pipeline) do
create(
:ci_empty_pipeline,
project: project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch,
status: :running,
head_pipeline_of: merge_request
)
end
it 'allows MR to be merged' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_button 'Set to auto-merge'
end
end
end
end
context 'when project has CI enabled' do

View File

@ -32,6 +32,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
stub_licensed_features(merge_request_approvers: true) if Gitlab.ee?
# rubocop:enable RSpec/AvoidConditionalStatements
project.update!(only_allow_merge_if_pipeline_succeeds: true)
stub_application_setting(auto_devops_enabled: false)
stub_ci_pipeline_yaml_file(YAML.dump(config))
project.add_maintainer(user)
@ -381,10 +382,6 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
context 'when the parent project enables pipeline must succeed' do
before do
project.update!(only_allow_merge_if_pipeline_succeeds: true)
end
it 'shows Set to auto-merge button' do
visit project_merge_request_path(project, merge_request)

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe IssuesFinder, feature_category: :team_planning do
include_context 'IssuesFinder context'
include_context 'Issues or WorkItems Finder context', :issue
it_behaves_like 'issues or work items finder', :issue, 'IssuesFinder#execute context'
it_behaves_like 'issues or work items finder', :issue, '{Issues|WorkItems}Finder#execute context'
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::WorkItemsFinder, feature_category: :team_planning do
include_context 'Issues or WorkItems Finder context', :work_item
it_behaves_like 'issues or work items finder', :work_item, '{Issues|WorkItems}Finder#execute context'
context 'when group parameter is present' do
include_context '{Issues|WorkItems}Finder#execute context', :work_item
let_it_be(:group_level_item) { create(:work_item, :group_level, namespace: group, author: user) }
let_it_be(:group_level_confidential_item) do
create(:work_item, :confidential, :group_level, namespace: group, author: user2)
end
let(:params) { { group_id: group } }
let(:scope) { 'all' }
it 'returns group level work items' do
expect(items).to contain_exactly(item1, item5, group_level_item)
end
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'does not return group level work items' do
expect(items).to contain_exactly(item1, item5)
end
end
context 'when user has access to confidential items' do
before do
group.add_reporter(user)
end
it 'includes confidential group-level items' do
expect(items).to contain_exactly(item1, item5, group_level_item, group_level_confidential_item)
end
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'only returns project-level items' do
expect(items).to contain_exactly(item1, item5)
end
end
end
end
end

View File

@ -43,6 +43,7 @@ import WorkflowAutoCancelOnNewCommitYaml from './yaml_tests/positive_tests/workf
import WorkflowRulesAutoCancelOnJobFailureYaml from './yaml_tests/positive_tests/workflow/rules/auto_cancel/on_job_failure.yml';
import WorkflowRulesAutoCancelOnNewCommitYaml from './yaml_tests/positive_tests/workflow/rules/auto_cancel/on_new_commit.yml';
import StagesYaml from './yaml_tests/positive_tests/stages.yml';
import RetryYaml from './yaml_tests/positive_tests/retry.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
@ -74,6 +75,7 @@ import WorkflowAutoCancelOnNewCommitNegativeYaml from './yaml_tests/negative_tes
import WorkflowRulesAutoCancelOnJobFailureNegativeYaml from './yaml_tests/negative_tests/workflow/rules/auto_cancel/on_job_failure.yml';
import WorkflowRulesAutoCancelOnNewCommitNegativeYaml from './yaml_tests/negative_tests/workflow/rules/auto_cancel/on_new_commit.yml';
import StagesNegativeYaml from './yaml_tests/negative_tests/stages.yml';
import RetryNegativeYaml from './yaml_tests/negative_tests/retry.yml';
const ajv = new Ajv({
strictTypes: false,
@ -122,6 +124,7 @@ describe('positive tests', () => {
WorkflowRulesAutoCancelOnJobFailureYaml,
WorkflowRulesAutoCancelOnNewCommitYaml,
StagesYaml,
RetryYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
@ -172,6 +175,7 @@ describe('negative tests', () => {
WorkflowRulesAutoCancelOnJobFailureNegativeYaml,
WorkflowRulesAutoCancelOnNewCommitNegativeYaml,
StagesNegativeYaml,
RetryNegativeYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a

View File

@ -0,0 +1,60 @@
# invalid retry
invalid_job_with_retry_int:
stage: "test"
script: "rspec"
retry: -1
invalid_job_with_retry_type:
stage: "test"
script: "rspec"
retry: "2"
invalid_job_with_retry_object_type:
stage: "test"
script: "rspec"
retry:
max: 2
unknown: 2
# invalid retry:when
invalid_job_with_retry_single_when_reason:
stage: "test"
script: "rspec"
retry:
max: 2
when: "gitlab-ci-retry-object-unknown-when"
invalid_job_with_retry_when_reason:
stage: "test"
script: "rspec"
retry:
max: 2
when:
- "api_failure"
- "gitlab-ci-retry-object-unknown-when"
# invalid retry:exit_codes
invalid_job_with_retry_single_exit_codes_type:
stage: "test"
script: "rspec"
retry:
max: 2
exit_codes: "137"
invalid_job_with_retry_exit_codes_type:
stage: "test"
script: "rspec"
retry:
max: 2
exit_codes:
- 137
- "1"
invalid_job_with_retry_exit_codes_duplicate:
stage: "test"
script: "rspec"
retry:
max: 2
exit_codes:
- 137
- 137

View File

@ -0,0 +1,94 @@
# valid retry
valid_job_with_retry_int:
stage: "test"
script: "rspec"
retry: 2
valid_job_with_retry_object_max:
stage: "test"
script: "rspec"
retry:
max: 2
valid_job_with_retry_object_when:
stage: "test"
script: "rspec"
retry:
when: "runner_system_failure"
valid_job_with_retry_object_exit_codes:
stage: "test"
script: "rspec"
retry:
exit_codes: 137
valid_job_with_retry_object_all_properties:
stage: "test"
script: "rspec"
retry:
max: 1
when: "runner_system_failure"
exit_codes: 137
# valid retry:when
valid_job_with_retry_single_when:
stage: "test"
script: "rspec"
retry:
max: 2
when: "runner_system_failure"
valid_job_with_retry_multiple_when:
stage: "test"
script: "rspec"
retry:
max: 2
when:
- "runner_system_failure"
- "stuck_or_timeout_failure"
valid_job_with_retry_all_when:
stage: "test"
script: "rspec"
retry:
max: 2
when:
- "always"
- "unknown_failure"
- "script_failure"
- "api_failure"
- "stuck_or_timeout_failure"
- "runner_system_failure"
- "runner_unsupported"
- "stale_schedule"
- "job_execution_timeout"
- "archived_failure"
- "unmet_prerequisites"
- "scheduler_failure"
- "data_integrity_failure"
valid_job_with_retry_duplicate_when:
stage: "test"
script: "rspec"
retry:
max: 2
when:
- "runner_system_failure"
- "runner_system_failure"
# valid retry:exit_codes
valid_job_with_retry_single_exit_codes:
stage: "test"
script: "rspec"
retry:
max: 2
exit_codes: 137
valid_job_with_retry_multiple_exit_codes:
stage: "test"
script: "rspec"
retry:
max: 2
exit_codes:
- 137
- 255

View File

@ -15,6 +15,9 @@ describe('formatProjects', () => {
mergeRequestsAccessLevel: firstMockProject.mergeRequestsAccessLevel.stringValue,
issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue,
forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue,
accessLevel: {
integerValue: 30,
},
availableActions: [ACTION_EDIT, ACTION_DELETE],
actionLoadingStates: {
[ACTION_DELETE]: false,
@ -55,6 +58,9 @@ describe('formatGroups', () => {
id: getIdFromGraphQLId(firstMockGroup.id),
parent: null,
editPath: `${firstFormattedGroup.webUrl}/-/edit`,
accessLevel: {
integerValue: 30,
},
availableActions: [ACTION_EDIT, ACTION_DELETE],
});
expect(formattedGroups.length).toBe(organizationGroups.length);

View File

@ -4,7 +4,7 @@ exports[`packages_list_app renders 1`] = `
<div>
<infrastructure-title-stub
count="1"
helpurl="foo"
helpurl="/help/user/packages/terraform_module_registry/index"
/>
<infrastructure-search-stub />
<div>
@ -36,7 +36,7 @@ exports[`packages_list_app renders 1`] = `
Learn how to
<b-link-stub
class="gl-link"
href="helpUrl"
href="/help/user/packages/terraform_module_registry/index"
target="_blank"
>
publish and share your packages

View File

@ -4,6 +4,7 @@ import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { createAlert, VARIANT_INFO } from '~/alert';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
@ -29,7 +30,6 @@ describe('packages_list_app', () => {
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findListComponent = () => wrapper.findComponent(PackageList);
const findInfrastructureSearch = () => wrapper.findComponent(InfrastructureSearch);
@ -41,8 +41,6 @@ describe('packages_list_app', () => {
config: {
resourceId: 'project_id',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl,
packageHelpUrl: 'foo',
isGroupPage,
},
filter,
@ -150,7 +148,9 @@ describe('packages_list_app', () => {
it('generate the correct empty list link', () => {
const link = findListComponent().findComponent(GlLink);
expect(link.attributes('href')).toBe(emptyListHelpUrl);
expect(link.attributes('href')).toBe(
helpPagePath('user/packages/terraform_module_registry/index'),
);
expect(link.text()).toBe('publish and share your packages');
});

View File

@ -17,7 +17,6 @@ describe('Mutations Registry Store', () => {
pageType: 'groups',
userCanDelete: '',
emptyListIllustration: 'foo',
emptyListHelpUrl: 'baz',
};
const expectedState = {

View File

@ -1,5 +1,6 @@
import { GlButton, GlForm } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import mockTimezones from 'test_fixtures/timezones/full.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -7,6 +8,7 @@ import axios from '~/lib/utils/axios_utils';
import ProfileEditApp from '~/profile/edit/components/profile_edit_app.vue';
import UserAvatar from '~/profile/edit/components/user_avatar.vue';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/alert';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import { timeRanges } from '~/vue_shared/constants';
@ -23,6 +25,8 @@ const defaultProvide = {
currentAvailability: AVAILABILITY_STATUS.NOT_SET,
defaultEmoji: 'speech_balloon',
currentClearStatusAfter: oneMinute.shortcut,
timezones: mockTimezones,
userTimezone: '',
};
describe('Profile Edit App', () => {
@ -57,6 +61,7 @@ describe('Profile Edit App', () => {
const findButtons = () => wrapper.findAllComponents(GlButton);
const findAvatar = () => wrapper.findComponent(UserAvatar);
const findSetStatusForm = () => wrapper.findComponent(SetStatusForm);
const findTimezoneDropdown = () => wrapper.findComponent(TimezoneDropdown);
const submitForm = () => findForm().vm.$emit('submit', new Event('submit'));
const setAvatar = () => findAvatar().vm.$emit('blob-change', mockAvatarFile);
const setStatus = () => {
@ -67,6 +72,11 @@ describe('Profile Edit App', () => {
setStatusForm.vm.$emit('clear-status-after-click', oneHour);
setStatusForm.vm.$emit('availability-input', true);
};
const setTimezone = (index = 0) => {
const timezoneForm = findTimezoneDropdown();
timezoneForm.vm.$emit('input', mockTimezones[index]);
};
it('renders the form for users to interact with', () => {
const form = findForm();
@ -89,6 +99,13 @@ describe('Profile Edit App', () => {
});
});
it('renders `TimezoneForm` component and passes correct props', () => {
expect(findTimezoneDropdown().props()).toMatchObject({
timezoneData: mockTimezones,
value: '',
});
});
describe('when form submit request is successful', () => {
it('shows success alert', async () => {
mockAxios.onPut(stubbedProfilePath).reply(200, {
@ -134,6 +151,25 @@ describe('Profile Edit App', () => {
expect(axiosRequestData.get('user[status][availability]')).toBe(AVAILABILITY_STATUS.BUSY);
});
it('contains changes from timezone form', async () => {
mockAxios.onPut(stubbedProfilePath).reply(200, {
message: successMessage,
});
const selectedTimezoneIndex = 2;
setTimezone(selectedTimezoneIndex);
submitForm();
await waitForPromises();
const axiosRequestData = mockAxios.history.put[0].data;
expect(findTimezoneDropdown().props('value')).toBe(
mockTimezones[selectedTimezoneIndex].identifier,
);
expect(axiosRequestData.get('user[timezone]')).toBe(
mockTimezones[selectedTimezoneIndex].identifier,
);
});
describe('when clear status after has not been changed', () => {
it('does not include it in the API request', async () => {
mockAxios.onPut(stubbedProfilePath).reply(200, {

View File

@ -7,7 +7,7 @@ import {
VISIBILITY_LEVEL_INTERNAL_STRING,
GROUP_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import DangerConfirmModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
@ -34,6 +34,7 @@ describe('GroupsListItem', () => {
const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
const findListActions = () => wrapper.findComponent(ListActions);
const findConfirmationModal = () => wrapper.findComponent(DangerConfirmModal);
const findAccessLevelBadge = () => wrapper.findByTestId('access-level-badge');
it('renders group avatar', () => {
createComponent();
@ -108,7 +109,7 @@ describe('GroupsListItem', () => {
});
});
it('renders access role badge', () => {
it('renders access level badge', () => {
createComponent();
expect(findAvatarLabeled().findComponent(GlBadge).text()).toBe(
@ -116,6 +117,33 @@ describe('GroupsListItem', () => {
);
});
describe('when access level is not available', () => {
const { accessLevel, ...groupWithoutAccessLevel } = group;
beforeEach(() => {
createComponent({
propsData: { group: groupWithoutAccessLevel },
});
});
it('does not render level role badge', () => {
expect(findAccessLevelBadge().exists()).toBe(false);
});
});
describe('when access level is `No access`', () => {
beforeEach(() => {
createComponent({
propsData: {
group: { ...group, accessLevel: { integerValue: ACCESS_LEVEL_NO_ACCESS_INTEGER } },
},
});
});
it('does not render level role badge', () => {
expect(findAccessLevelBadge().exists()).toBe(false);
});
});
describe('when group has a description', () => {
it('renders description', () => {
const descriptionHtml = '<p>Foo bar</p>';

View File

@ -12,7 +12,7 @@ import {
VISIBILITY_LEVEL_PRIVATE_STRING,
PROJECT_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { ACCESS_LEVEL_LABELS, ACCESS_LEVEL_NO_ACCESS_INTEGER } from '~/access_level/constants';
import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
@ -22,9 +22,16 @@ jest.mock('lodash/uniqueId');
describe('ProjectsListItem', () => {
let wrapper;
const [project] = convertObjectPropsToCamelCase(projects, { deep: true });
const [{ permissions, ...project }] = convertObjectPropsToCamelCase(projects, { deep: true });
const defaultPropsData = { project };
const defaultPropsData = {
project: {
...project,
accessLevel: {
integerValue: permissions.projectAccess.accessLevel,
},
},
};
const createComponent = ({ propsData = {} } = {}) => {
wrapper = mountExtended(ProjectsListItem, {
@ -45,6 +52,7 @@ describe('ProjectsListItem', () => {
const findProjectDescription = () => wrapper.findByTestId('project-description');
const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
const findListActions = () => wrapper.findComponent(ListActions);
const findAccessLevelBadge = () => wrapper.findByTestId('access-level-badge');
beforeEach(() => {
uniqueId.mockImplementation(jest.requireActual('lodash/uniqueId'));
@ -90,14 +98,40 @@ describe('ProjectsListItem', () => {
});
});
it('renders access role badge', () => {
it('renders access level badge', () => {
createComponent();
expect(findAvatarLabeled().findComponent(GlBadge).text()).toBe(
ACCESS_LEVEL_LABELS[project.permissions.projectAccess.accessLevel],
expect(findAccessLevelBadge().text()).toBe(
ACCESS_LEVEL_LABELS[defaultPropsData.project.accessLevel.integerValue],
);
});
describe('when access level is not available', () => {
beforeEach(() => {
createComponent({
propsData: { project },
});
});
it('does not render access level badge', () => {
expect(findAccessLevelBadge().exists()).toBe(false);
});
});
describe('when access level is `No access`', () => {
beforeEach(() => {
createComponent({
propsData: {
project: { ...project, accessLevel: { integerValue: ACCESS_LEVEL_NO_ACCESS_INTEGER } },
},
});
});
it('does not render access level badge', () => {
expect(findAccessLevelBadge().exists()).toBe(false);
});
});
describe('if project is archived', () => {
beforeEach(() => {
createComponent({

View File

@ -126,13 +126,14 @@ RSpec.describe ProfilesHelper do
describe '#user_profile_data' do
let(:time) { 3.hours.ago }
let(:timezone) { 'Europe/London' }
let(:user) do
build_stubbed(:user, status: UserStatus.new(
message: 'Some message',
emoji: 'basketball',
availability: 'busy',
clear_status_at: time
))
), timezone: timezone)
end
before do
@ -156,6 +157,8 @@ RSpec.describe ProfilesHelper do
expect(data[:current_availability]).to eq('busy')
expect(data[:current_clear_status_after]).to eq(time.to_fs(:iso8601))
expect(data[:default_emoji]).to eq(UserStatus::DEFAULT_EMOJI)
expect(data[:timezones]).to eq(helper.timezone_data_with_unique_identifiers.to_json)
expect(data[:user_timezone]).to eq(timezone)
end
end

View File

@ -6,10 +6,10 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
include StubENV
let(:progress) { StringIO.new }
let(:definitions) { nil }
let(:backup_tasks) { nil }
let(:options) { build(:backup_options, :skip_none) }
subject { described_class.new(progress, definitions: definitions) }
subject { described_class.new(progress, backup_tasks: backup_tasks) }
before do
# Rspec fails with `uninitialized constant RSpec::Support::Differ` when it
@ -33,35 +33,37 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
let(:target) { instance_double(Backup::Targets::Target) }
let(:definitions) do
let(:backup_tasks) do
{ 'terraform_state' => terraform_state }
end
it 'calls the named task' do
it 'runs the provided task' do
expect(target).to receive(:dump)
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... ')
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... done')
subject.run_create_task('terraform_state')
subject.run_create_task(terraform_state)
end
describe 'disabled' do
it 'informs the user' do
context 'when disabled' do
it 'does not run the task and informs the user' do
allow(terraform_state).to receive(:enabled).and_return(false)
expect(target).not_to receive(:dump)
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... [DISABLED]')
subject.run_create_task('terraform_state')
subject.run_create_task(terraform_state)
end
end
describe 'skipped' do
it 'informs the user' do
context 'when skipped' do
it 'does not run the task and informs the user' do
stub_env('SKIP', 'terraform_state')
expect(target).not_to receive(:dump)
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping terraform states ... [SKIPPED]')
subject.run_create_task('terraform_state')
subject.run_create_task(terraform_state)
end
end
end
@ -80,7 +82,10 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
post_restore_warning: post_restore_warning)
end
let(:definitions) { { 'terraform_state' => terraform_state } }
let(:backup_tasks) do
{ 'terraform_state' => terraform_state }
end
let(:backup_information) { { backup_created_at: Time.zone.parse('2019-01-01'), gitlab_version: '12.3' } }
before do
@ -89,20 +94,22 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
end
it 'calls the named task' do
it 'runs the provided task' do
expect(target).to receive(:restore)
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... ').ordered
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... done').ordered
expect(target).to receive(:restore)
subject.run_restore_task('terraform_state')
subject.run_restore_task(terraform_state)
end
describe 'disabled' do
it 'informs the user' do
context 'when disabled' do
it 'does not run the task and informs the user' do
allow(terraform_state).to receive(:enabled).and_return(false)
expect(target).not_to receive(:restore)
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Restoring terraform states ... [DISABLED]').ordered
subject.run_restore_task('terraform_state')
subject.run_restore_task(terraform_state)
end
end
@ -116,7 +123,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
expect(Gitlab::TaskHelpers).to receive(:ask_to_continue)
expect(target).to receive(:restore)
subject.run_restore_task('terraform_state')
subject.run_restore_task(terraform_state)
end
it 'does not continue when the user quits' do
@ -126,7 +133,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
expect(Gitlab::TaskHelpers).to receive(:ask_to_continue).and_raise(Gitlab::TaskAbortedByUserError)
expect do
subject.run_restore_task('terraform_state')
subject.run_restore_task(terraform_state)
end.to raise_error(SystemExit)
end
end
@ -141,7 +148,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
expect(Gitlab::TaskHelpers).to receive(:ask_to_continue)
expect(target).to receive(:restore)
subject.run_restore_task('terraform_state')
subject.run_restore_task(terraform_state)
end
it 'does not continue when the user quits' do
@ -153,7 +160,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
expect(Gitlab::TaskHelpers).to receive(:ask_to_continue).and_raise(Gitlab::TaskAbortedByUserError)
expect do
subject.run_restore_task('terraform_state')
subject.run_restore_task(terraform_state)
end.to raise_error(SystemExit)
end
end
@ -181,7 +188,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
let(:target1) { instance_double(Backup::Targets::Target) }
let(:target2) { instance_double(Backup::Targets::Target) }
let(:definitions) do
let(:backup_tasks) do
{ 'lfs' => lfs, 'pages' => pages }
end
@ -948,7 +955,7 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
let(:target1) { instance_double(Backup::Targets::Target, pre_restore_warning: nil, post_restore_warning: nil) }
let(:target2) { instance_double(Backup::Targets::Target, pre_restore_warning: nil, post_restore_warning: nil) }
let(:definitions) do
let(:backup_tasks) do
{ 'lfs' => lfs, 'pages' => pages }
end

View File

@ -103,21 +103,6 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
with_them do
it { is_expected.to eq(result) }
end
context 'when ci_job_token_groups_allowlist feature flag is disabled' do
before do
stub_feature_flags(ci_job_token_groups_allowlist: false)
end
where(:accessed_project, :result) do
ref(:project_with_target_project_group_in_allowlist) | false
ref(:project_wo_target_project_group_in_allowlist) | false
end
with_them do
it { is_expected.to eq(result) }
end
end
end
context 'with inbound and outbound scopes enabled' do

View File

@ -142,42 +142,6 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan
end
end
context 'when querying epic types' do
let_it_be(:group_level_issue) { create(:issue, :epic, :group_level, namespace: group1) }
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => group1.full_path },
"issues(types: [EPIC]) { #{fields} }"
)
end
before_all do
group1.add_developer(current_user)
end
it 'returns group-level epics' do
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_be_empty
expect(issues_ids).to contain_exactly(group_level_issue.to_global_id.to_s)
end
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'returns no epics' do
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_be_empty
expect(issues_ids).to be_empty
end
end
end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end

View File

@ -43,15 +43,6 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
it_behaves_like 'find target'
it_behaves_like 'build target', type_iid: nil
it_behaves_like 'build target', type_iid: -1
context 'when issue belongs to a group' do
let(:container) { group }
let(:target) { create(:issue, :group_level, namespace: group) }
it_behaves_like 'find target'
it_behaves_like 'build target', type_iid: nil
it_behaves_like 'build target', type_iid: -1
end
end
context 'for work item' do

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true
module ContentEditorHelpers
module RichTextEditorHelpers
def content_editor_testid
'[data-testid="content-editor"] [contenteditable].ProseMirror'
end
def switch_to_markdown_editor
click_button("Switch to plain text editing")
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.shared_context 'IssuesFinder context' do
RSpec.shared_context 'Issues or WorkItems Finder context' do |factory|
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:group) { create(:group) }
@ -14,7 +14,7 @@ RSpec.shared_context 'IssuesFinder context' do
let_it_be(:label2) { create(:label, project: project2) }
let_it_be_with_reload(:item1) do
create(
:issue,
factory,
author: user,
assignees: [user],
project: project1,
@ -27,7 +27,7 @@ RSpec.shared_context 'IssuesFinder context' do
let_it_be_with_reload(:item2) do
create(
:issue,
factory,
author: user,
assignees: [user],
project: project2,
@ -39,7 +39,7 @@ RSpec.shared_context 'IssuesFinder context' do
let_it_be_with_reload(:item3) do
create(
:issue,
factory,
author: user2,
assignees: [user2],
project: project2,
@ -50,10 +50,10 @@ RSpec.shared_context 'IssuesFinder context' do
)
end
let_it_be_with_reload(:item4) { create(:issue, project: project3) }
let_it_be_with_reload(:item4) { create(factory, project: project3) }
let_it_be_with_reload(:item5) do
create(
:issue,
factory,
author: user,
assignees: [user],
project: project1,
@ -63,20 +63,15 @@ RSpec.shared_context 'IssuesFinder context' do
)
end
let_it_be(:group_level_item) { create(:issue, :group_level, namespace: group, author: user) }
let_it_be(:group_level_confidential_item) do
create(:issue, :confidential, :group_level, namespace: group, author: user2)
end
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
let(:items_model) { Issue }
let(:items_model) { factory.to_s.camelize.constantize }
end
RSpec.shared_context 'IssuesFinder#execute context' do
let!(:closed_item) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
RSpec.shared_context '{Issues|WorkItems}Finder#execute context' do |factory|
let!(:closed_item) { create(factory, author: user2, assignees: [user2], project: project2, state: 'closed') }
let!(:label_link) { create(:label_link, label: label, target: item2) }
let!(:label_link2) { create(:label_link, label: label2, target: item3) }
let(:search_user) { user }

View File

@ -1,792 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'edits content using the content editor' do |params = {
with_expanded_references: true,
with_quick_actions: true
}|
include ContentEditorHelpers
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
let(:modifier_key) { is_mac ? :command : :control }
it 'saves page content in local storage if the user navigates away' do
switch_to_content_editor
expect(page).to have_css(content_editor_testid)
type_in_content_editor ' Typing text in the content editor'
wait_until_hidden_field_is_updated /Typing text in the content editor/
begin
refresh
rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError
end
expect(page).to have_text('Typing text in the content editor')
end
it 'autofocuses the rich text editor when switching to rich text' do
switch_to_content_editor
expect(page).to have_css("#{content_editor_testid}:focus")
end
it 'autofocuses the plain text editor when switching back to markdown' do
switch_to_content_editor
switch_to_markdown_editor
expect(page).to have_css("textarea:focus")
end
describe 'creating and editing links' do
before do
switch_to_content_editor
end
context 'when clicking the link icon in the toolbar' do
it 'shows the link bubble menu' do
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
expect(page).to have_css('[data-testid="link-bubble-menu"]')
end
context 'if no text is selected' do
before do
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
end
it 'opens an empty inline modal to create a link' do
page.within '[data-testid="link-bubble-menu"]' do
expect(page).to have_field('link-text', with: '')
expect(page).to have_field('link-href', with: '')
end
end
context 'when the user clicks the apply button' do
it 'applies the changes to the document' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'Link to GitLab home page'
fill_in 'link-href', with: 'https://gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_css('a[href="https://gitlab.com"]')
expect(page).to have_text('Link to GitLab home page')
end
end
end
context 'when the user clicks the cancel button' do
it 'does not apply the changes to the document' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'Link to GitLab home page'
fill_in 'link-href', with: 'https://gitlab.com'
click_button 'Cancel'
end
page.within content_editor_testid do
expect(page).not_to have_css('a')
end
end
end
end
context 'if text is selected' do
before do
type_in_content_editor 'The quick brown fox jumps over the lazy dog'
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
end
it 'prefills inline modal to create a link' do
page.within '[data-testid="link-bubble-menu"]' do
expect(page).to have_field('link-text', with: 'dog')
expect(page).to have_field('link-href', with: '')
end
end
context 'when the user clicks the apply button' do
it 'applies the changes to the document' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'new dog'
fill_in 'link-href', with: 'https://en.wikipedia.org/wiki/Shiba_Inu'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://en.wikipedia.org/wiki/Shiba_Inu"]',
text: 'new dog'
)
end
end
end
end
end
context 'if cursor is placed on an existing link' do
before do
type_in_content_editor 'Link to [GitLab home **page**](https://gitlab.com)'
type_in_content_editor :left
end
it 'prefills inline modal to edit the link' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
expect(page).to have_field('link-text', with: 'GitLab home page')
expect(page).to have_field('link-href', with: 'https://gitlab.com')
end
end
it 'updates the link attributes if text is not updated' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]')
expect(page.find('a')).to have_text('GitLab home page')
expect(page).to have_selector('strong', text: 'page')
end
end
it 'updates the link attributes and text if text is updated' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
fill_in 'link-text', with: 'GitLab about page'
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]',
text: 'GitLab about page'
)
expect(page).not_to have_selector('strong')
end
end
it 'does nothing if Cancel is clicked' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
click_button 'Cancel'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://gitlab.com"]',
text: 'GitLab home page'
)
expect(page).to have_selector('strong')
end
end
context 'when the user clicks the unlink button' do
it 'removes the link' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="remove-link"]').click
end
page.within content_editor_testid do
expect(page).not_to have_selector('a')
expect(page).to have_selector('strong', text: 'page')
end
end
end
end
context 'when selection spans more than a link' do
before do
type_in_content_editor 'a [b **c**](https://gitlab.com)'
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
end
it 'prefills inline modal with the entire selection' do
page.within '[data-testid="link-bubble-menu"]' do
expect(page).to have_field('link-text', with: 'a b c')
expect(page).to have_field('link-href', with: '')
end
end
it 'expands the link and updates the link attributes if text is not updated' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]')
expect(page.find('a')).to have_text('a b c')
expect(page).to have_selector('strong', text: 'c')
end
end
it 'expands the link, updates the link attributes and text if text is updated',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419684' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'new text'
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]',
text: 'new text'
)
expect(page).not_to have_selector('strong')
end
end
end
end
describe 'selecting text' do
before do
switch_to_content_editor
# delete all text first
type_in_content_editor [modifier_key, 'a']
type_in_content_editor :backspace
type_in_content_editor 'The quick **brown** fox _jumps_ over the lazy dog!'
type_in_content_editor :enter
type_in_content_editor '[Link](https://gitlab.com)'
type_in_content_editor :enter
type_in_content_editor 'Jackdaws love my ~~big~~ sphinx of quartz!'
# select all text
type_in_content_editor [modifier_key, 'a']
end
it 'renders selected text in a .content-editor-selection class' do
page.within content_editor_testid do
assert_selected 'The quick'
assert_selected 'brown'
assert_selected 'fox'
assert_selected 'jumps'
assert_selected 'over the lazy dog!'
assert_selected 'Link'
assert_selected 'Jackdaws love my'
assert_selected 'big'
assert_selected 'sphinx of quartz!'
end
end
def assert_selected(text)
expect(page).to have_selector('.content-editor-selection', text: text)
end
end
describe 'media elements bubble menu' do
before do
switch_to_content_editor
click_attachment_button
end
it 'displays correct media bubble menu for images', :js do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
expect_media_bubble_menu_to_be_visible
end
it 'displays correct media bubble menu for video', :js do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
expect_media_bubble_menu_to_be_visible
end
end
describe 'code block' do
before do
visit(profile_preferences_path)
find('.syntax-theme').choose('Dark')
wait_for_requests
page.go_back
refresh
switch_to_content_editor
end
it 'applies theme classes to code blocks' do
expect(page).not_to have_css('.content-editor-code-block.code.highlight.dark')
type_in_content_editor [:enter, :enter]
type_in_content_editor '```js ' # trigger input rule
type_in_content_editor 'var a = 0'
expect(page).to have_css('.content-editor-code-block.code.highlight.dark')
end
end
describe 'code block bubble menu' do
before do
switch_to_content_editor
end
it 'shows a code block bubble menu for a code block' do
type_in_content_editor [:enter, :enter]
type_in_content_editor '```js ' # trigger input rule
type_in_content_editor 'var a = 0'
type_in_content_editor [:shift, :left]
expect(page).to have_css('[data-testid="code-block-bubble-menu"]')
end
it 'sets code block type to "javascript" for `js`' do
type_in_content_editor [:enter, :enter]
type_in_content_editor '```js '
type_in_content_editor 'var a = 0'
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Javascript')
end
it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do
type_in_content_editor [:enter, :enter]
type_in_content_editor '```nomnoml '
type_in_content_editor 'test'
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)')
end
end
describe 'mermaid diagram' do
before do
switch_to_content_editor
type_in_content_editor [:enter, :enter]
type_in_content_editor '```mermaid '
type_in_content_editor ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34']
end
it 'renders and updates the diagram correctly in a sandboxed iframe' do
iframe = find(content_editor_testid).find('iframe')
expect(iframe['src']).to include('/-/sandbox/mermaid')
within_frame(iframe) do
expect(find('svg .nodes').text).to include('JohnDoe12')
expect(find('svg .nodes').text).to include('HelloWorld34')
end
expect(iframe['height'].to_i).to be > 100
find(content_editor_testid).send_keys [:enter, ' JaneDoe34 --> HelloWorld56']
within_frame(iframe) do
page.has_content?('JaneDoe34')
expect(find('svg .nodes').text).to include('JaneDoe34')
expect(find('svg .nodes').text).to include('HelloWorld56')
end
end
it 'toggles the diagram when preview button is clicked',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397682' do
find('[data-testid="preview-diagram"]').click
expect(find(content_editor_testid)).not_to have_selector('iframe')
find('[data-testid="preview-diagram"]').click
iframe = find(content_editor_testid).find('iframe')
within_frame(iframe) do
expect(find('svg .nodes').text).to include('JohnDoe12')
expect(find('svg .nodes').text).to include('HelloWorld34')
end
end
end
describe 'rendering with initial content' do
it 'renders correctly with table as initial content' do
textarea = find 'textarea'
textarea.send_keys "\n\n"
textarea.send_keys "| First Header | Second Header |\n"
textarea.send_keys "|--------------|---------------|\n"
textarea.send_keys "| Content from cell 1 | Content from cell 2 |\n\n"
textarea.send_keys "Content below table"
switch_to_content_editor
expect(page).not_to have_text('An error occurred')
end
end
describe 'pasting text' do
before do
switch_to_content_editor
type_in_content_editor [modifier_key, 'a']
type_in_content_editor :delete
type_in_content_editor "Some **rich** _text_ ~~content~~ [link](https://gitlab.com)"
type_in_content_editor [modifier_key, 'a']
type_in_content_editor [modifier_key, 'x']
end
it 'pastes text with formatting if ctrl + v is pressed' do
type_in_content_editor [modifier_key, 'v']
page.within content_editor_testid do
expect(page).to have_selector('strong', text: 'rich')
expect(page).to have_selector('em', text: 'text')
expect(page).to have_selector('s', text: 'content')
expect(page).to have_selector('a[href="https://gitlab.com"]', text: 'link')
end
end
it 'does not show a loading indicator after undo paste' do
type_in_content_editor [modifier_key, 'v']
type_in_content_editor [modifier_key, 'z']
page.within content_editor_testid do
expect(page).not_to have_css('.gl-dots-loader')
end
end
it 'pastes raw text without formatting if shift + ctrl + v is pressed' do
type_in_content_editor [modifier_key, :shift, 'v']
page.within content_editor_testid do
expect(page).to have_text('Some rich text content link')
expect(page).not_to have_selector('strong')
expect(page).not_to have_selector('em')
expect(page).not_to have_selector('s')
expect(page).not_to have_selector('a')
end
end
it 'pastes raw markdown with formatting when pasting inside a markdown code block' do
type_in_content_editor '```md'
type_in_content_editor :enter
type_in_content_editor [modifier_key, 'v']
page.within content_editor_testid do
expect(page).to have_selector('pre', text: 'Some **rich** _text_ ~~content~~ [link](https://gitlab.com)')
end
end
it 'pastes raw markdown without formatting when pasting inside a plaintext code block' do
type_in_content_editor '```'
type_in_content_editor :enter
type_in_content_editor [modifier_key, 'v']
page.within content_editor_testid do
expect(page).to have_selector('pre', text: 'Some rich text content link')
end
end
it 'pastes raw text without formatting, stripping whitespaces, if shift + ctrl + v is pressed' do
type_in_content_editor " Some **rich**"
type_in_content_editor :enter
type_in_content_editor " _text_"
type_in_content_editor :enter
type_in_content_editor " ~~content~~"
type_in_content_editor :enter
type_in_content_editor " [link](https://gitlab.com)"
type_in_content_editor [modifier_key, 'a']
type_in_content_editor [modifier_key, 'x']
type_in_content_editor [modifier_key, :shift, 'v']
page.within content_editor_testid do
expect(page).to have_text('Some rich text content link')
end
end
end
describe 'autocomplete suggestions' do
let(:suggestions_dropdown) { '[data-testid="content-editor-suggestions-dropdown"]' }
before do
if defined?(project)
create(:issue, project: project, title: 'My Cool Linked Issue')
create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:label, project: project, title: 'My Cool Label')
create(:milestone, project: project, title: 'My Cool Milestone')
project.add_maintainer(create(:user, name: 'abc123', username: 'abc123'))
else # group wikis
project = create(:project, group: group)
create(:issue, project: project, title: 'My Cool Linked Issue')
create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:group_label, group: group, title: 'My Cool Label')
create(:milestone, group: group, title: 'My Cool Milestone')
project.add_maintainer(create(:user, name: 'abc123', username: 'abc123'))
end
switch_to_content_editor
type_in_content_editor :enter
stub_feature_flags(disable_all_mention: false)
end
if params[:with_expanded_references]
describe 'when expanding an issue reference' do
it 'displays full reference name' do
new_issue = create(:issue, project: project, title: 'Brand New Issue')
type_in_content_editor "##{new_issue.iid}+s "
expect(page).to have_text('Brand New Issue')
end
end
describe 'when expanding an MR reference' do
it 'displays full reference name' do
new_mr = create(:merge_request, source_project: project, source_branch: 'branch-2', title: 'Brand New MR')
type_in_content_editor "!#{new_mr.iid}+s "
expect(page).to have_text('Brand New')
end
end
end
if params[:with_quick_actions]
it 'shows suggestions for quick actions' do
type_in_content_editor '/a'
expect(find(suggestions_dropdown)).to have_text('/assign')
expect(find(suggestions_dropdown)).to have_text('/label')
end
it 'adds the correct prefix for /assign' do
type_in_content_editor '/assign'
expect(find(suggestions_dropdown)).to have_text('/assign')
send_keys :enter
expect(page).to have_text('/assign @')
end
it 'adds the correct prefix for /label' do
type_in_content_editor '/label'
expect(find(suggestions_dropdown)).to have_text('/label')
send_keys :enter
expect(page).to have_text('/label ~')
end
it 'adds the correct prefix for /milestone' do
type_in_content_editor '/milestone'
expect(find(suggestions_dropdown)).to have_text('/milestone')
send_keys :enter
expect(page).to have_text('/milestone %')
end
it 'scrolls selected item into view when navigating with keyboard' do
type_in_content_editor '/'
expect(find(suggestions_dropdown)).to have_text('label')
expect(dropdown_scroll_top).to be 0
send_keys :arrow_up
expect(dropdown_scroll_top).to be > 100
end
end
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
expect(find(suggestions_dropdown)).to have_text('abc123')
expect(find(suggestions_dropdown)).to have_text('all')
expect(find(suggestions_dropdown)).to have_text('Group Members')
type_in_content_editor 'bc'
send_keys :enter
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
end
it 'allows selecting element with tab key' do
type_in_content_editor '@abc'
expect(find(suggestions_dropdown)).to have_text('abc123')
send_keys :tab
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
end
it 'allows dismissing the suggestion popup and typing more text' do
type_in_content_editor '@ab'
expect(find(suggestions_dropdown)).to have_text('abc123')
send_keys :escape
expect(page).not_to have_css(suggestions_dropdown)
type_in_content_editor :enter
type_in_content_editor 'foobar'
# ensure that the texts are in separate paragraphs
expect(page).to have_selector('p', text: '@ab')
expect(page).to have_selector('p', text: 'foobar')
expect(page).not_to have_selector('p', text: '@abfoobar')
end
it 'allows typing more text after the popup has disappeared because no suggestions match' do
type_in_content_editor '@ab'
expect(find(suggestions_dropdown)).to have_text('abc123')
type_in_content_editor 'foo'
type_in_content_editor :enter
type_in_content_editor 'bar'
# ensure that the texts are in separate paragraphs
expect(page).to have_selector('p', text: '@abfoo')
expect(page).to have_selector('p', text: 'bar')
expect(page).not_to have_selector('p', text: '@abfoobar')
end
context 'when `disable_all_mention` is enabled' do
before do
stub_feature_flags(disable_all_mention: true)
end
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
expect(find(suggestions_dropdown)).to have_text('abc123')
expect(find(suggestions_dropdown)).not_to have_text('All Group Members')
type_in_content_editor 'bc'
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
end
end
it 'shows suggestions for merge requests' do
type_in_content_editor '!'
expect(find(suggestions_dropdown)).to have_text('My Cool Merge Request')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('!1')
end
it 'shows suggestions for issues' do
type_in_content_editor '#'
expect(find(suggestions_dropdown)).to have_text('My Cool Linked Issue')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('#1')
end
it 'shows suggestions for milestones' do
type_in_content_editor '%'
expect(find(suggestions_dropdown)).to have_text('My Cool Milestone')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('%My Cool Milestone')
end
it 'shows suggestions for emojis' do
type_in_content_editor ':smile'
expect(find(suggestions_dropdown)).to have_text('😃 smiley')
expect(find(suggestions_dropdown)).to have_text('😸 smile_cat')
send_keys :enter
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('😄')
end
it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do
type_in_content_editor '%'
expect(find(suggestions_dropdown)).to have_text('My Cool Milestone')
type_in_content_editor 'x'
expect(page).not_to have_css(suggestions_dropdown)
end
def dropdown_scroll_top
evaluate_script("document.querySelector('#{suggestions_dropdown}').scrollTop")
end
end
end
RSpec.shared_examples 'inserts diagrams.net diagram using the content editor' do
include ContentEditorHelpers
before do
switch_to_content_editor
click_attachment_button
end
it 'displays correct media bubble menu with edit diagram button' do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg'
expect_media_bubble_menu_to_be_visible
click_edit_diagram_button
expect_drawio_editor_is_opened
end
end

View File

@ -0,0 +1,249 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - autocomplete' do |params = {
with_expanded_references: true,
with_quick_actions: true
}|
include RichTextEditorHelpers
describe 'autocomplete suggestions' do
let(:suggestions_dropdown) { '[data-testid="content-editor-suggestions-dropdown"]' }
before do
if defined?(project)
create(:issue, project: project, title: 'My Cool Linked Issue')
create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:label, project: project, title: 'My Cool Label')
create(:milestone, project: project, title: 'My Cool Milestone')
project.add_maintainer(create(:user, name: 'abc123', username: 'abc123'))
else # group wikis
project = create(:project, group: group)
create(:issue, project: project, title: 'My Cool Linked Issue')
create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:group_label, group: group, title: 'My Cool Label')
create(:milestone, group: group, title: 'My Cool Milestone')
project.add_maintainer(create(:user, name: 'abc123', username: 'abc123'))
end
switch_to_content_editor
type_in_content_editor :enter
stub_feature_flags(disable_all_mention: false)
end
if params[:with_expanded_references]
describe 'when expanding an issue reference' do
it 'displays full reference name' do
new_issue = create(:issue, project: project, title: 'Brand New Issue')
type_in_content_editor "##{new_issue.iid}+s "
expect(page).to have_text('Brand New Issue')
end
end
describe 'when expanding an MR reference' do
it 'displays full reference name' do
new_mr = create(:merge_request, source_project: project, source_branch: 'branch-2', title: 'Brand New MR')
type_in_content_editor "!#{new_mr.iid}+s "
expect(page).to have_text('Brand New')
end
end
end
if params[:with_quick_actions]
it 'shows suggestions for quick actions' do
type_in_content_editor '/a'
expect(find(suggestions_dropdown)).to have_text('/assign')
expect(find(suggestions_dropdown)).to have_text('/label')
end
it 'adds the correct prefix for /assign' do
type_in_content_editor '/assign'
expect(find(suggestions_dropdown)).to have_text('/assign')
send_keys :enter
expect(page).to have_text('/assign @')
end
it 'adds the correct prefix for /label' do
type_in_content_editor '/label'
expect(find(suggestions_dropdown)).to have_text('/label')
send_keys :enter
expect(page).to have_text('/label ~')
end
it 'adds the correct prefix for /milestone' do
type_in_content_editor '/milestone'
expect(find(suggestions_dropdown)).to have_text('/milestone')
send_keys :enter
expect(page).to have_text('/milestone %')
end
it 'scrolls selected item into view when navigating with keyboard' do
type_in_content_editor '/'
expect(find(suggestions_dropdown)).to have_text('label')
expect(dropdown_scroll_top).to be 0
send_keys :arrow_up
expect(dropdown_scroll_top).to be > 100
end
end
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
expect(find(suggestions_dropdown)).to have_text('abc123')
expect(find(suggestions_dropdown)).to have_text('all')
expect(find(suggestions_dropdown)).to have_text('Group Members')
type_in_content_editor 'bc'
send_keys :enter
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
end
it 'allows selecting element with tab key' do
type_in_content_editor '@abc'
expect(find(suggestions_dropdown)).to have_text('abc123')
send_keys :tab
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
end
it 'allows dismissing the suggestion popup and typing more text' do
type_in_content_editor '@ab'
expect(find(suggestions_dropdown)).to have_text('abc123')
send_keys :escape
expect(page).not_to have_css(suggestions_dropdown)
type_in_content_editor :enter
type_in_content_editor 'foobar'
# ensure that the texts are in separate paragraphs
expect(page).to have_selector('p', text: '@ab')
expect(page).to have_selector('p', text: 'foobar')
expect(page).not_to have_selector('p', text: '@abfoobar')
end
it 'allows typing more text after the popup has disappeared because no suggestions match' do
type_in_content_editor '@ab'
expect(find(suggestions_dropdown)).to have_text('abc123')
type_in_content_editor 'foo'
type_in_content_editor :enter
type_in_content_editor 'bar'
# ensure that the texts are in separate paragraphs
expect(page).to have_selector('p', text: '@abfoo')
expect(page).to have_selector('p', text: 'bar')
expect(page).not_to have_selector('p', text: '@abfoobar')
end
context 'when `disable_all_mention` is enabled' do
before do
stub_feature_flags(disable_all_mention: true)
end
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
expect(find(suggestions_dropdown)).to have_text('abc123')
expect(find(suggestions_dropdown)).not_to have_text('All Group Members')
type_in_content_editor 'bc'
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
end
end
it 'shows suggestions for merge requests' do
type_in_content_editor '!'
expect(find(suggestions_dropdown)).to have_text('My Cool Merge Request')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('!1')
end
it 'shows suggestions for issues' do
type_in_content_editor '#'
expect(find(suggestions_dropdown)).to have_text('My Cool Linked Issue')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('#1')
end
it 'shows suggestions for milestones' do
type_in_content_editor '%'
expect(find(suggestions_dropdown)).to have_text('My Cool Milestone')
send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('%My Cool Milestone')
end
it 'shows suggestions for emojis' do
type_in_content_editor ':smile'
expect(find(suggestions_dropdown)).to have_text('😃 smiley')
expect(find(suggestions_dropdown)).to have_text('😸 smile_cat')
send_keys :enter
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('😄')
end
it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do
type_in_content_editor '%'
expect(find(suggestions_dropdown)).to have_text('My Cool Milestone')
type_in_content_editor 'x'
expect(page).not_to have_css(suggestions_dropdown)
end
def dropdown_scroll_top
evaluate_script("document.querySelector('#{suggestions_dropdown}').scrollTop")
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - code blocks' do
include RichTextEditorHelpers
describe 'code block' do
before do
visit(profile_preferences_path)
find('.syntax-theme').choose('Dark')
wait_for_requests
page.go_back
refresh
switch_to_content_editor
end
it 'applies theme classes to code blocks' do
expect(page).not_to have_css('.content-editor-code-block.code.highlight.dark')
type_in_content_editor [:enter, :enter]
type_in_content_editor '```js ' # trigger input rule
type_in_content_editor 'var a = 0'
expect(page).to have_css('.content-editor-code-block.code.highlight.dark')
end
end
describe 'code block bubble menu' do
before do
switch_to_content_editor
end
it 'shows a code block bubble menu for a code block' do
type_in_content_editor [:enter, :enter]
type_in_content_editor '```js ' # trigger input rule
type_in_content_editor 'var a = 0'
type_in_content_editor [:shift, :left]
expect(page).to have_css('[data-testid="code-block-bubble-menu"]')
end
it 'sets code block type to "javascript" for `js`' do
type_in_content_editor [:enter, :enter]
type_in_content_editor '```js '
type_in_content_editor 'var a = 0'
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Javascript')
end
it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do
type_in_content_editor [:enter, :enter]
type_in_content_editor '```nomnoml '
type_in_content_editor 'test'
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)')
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - common' do
include RichTextEditorHelpers
it 'saves page content in local storage if the user navigates away' do
switch_to_content_editor
expect(page).to have_css(content_editor_testid)
type_in_content_editor ' Typing text in the content editor'
wait_until_hidden_field_is_updated(/Typing text in the content editor/)
begin
refresh
rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError
end
expect(page).to have_text('Typing text in the content editor')
end
it 'autofocuses the rich text editor when switching to rich text' do
switch_to_content_editor
expect(page).to have_css("#{content_editor_testid}:focus")
end
it 'autofocuses the plain text editor when switching back to markdown' do
switch_to_content_editor
switch_to_markdown_editor
expect(page).to have_css("textarea:focus")
end
describe 'rendering with initial content' do
it 'renders correctly with table as initial content' do
textarea = find 'textarea'
textarea.send_keys "\n\n"
textarea.send_keys "| First Header | Second Header |\n"
textarea.send_keys "|--------------|---------------|\n"
textarea.send_keys "| Content from cell 1 | Content from cell 2 |\n\n"
textarea.send_keys "Content below table"
switch_to_content_editor
expect(page).not_to have_text('An error occurred')
end
end
end

View File

@ -0,0 +1,95 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - copy/paste' do
include RichTextEditorHelpers
let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
let(:modifier_key) { is_mac ? :command : :control }
describe 'pasting text' do
before do
switch_to_content_editor
type_in_content_editor [modifier_key, 'a']
type_in_content_editor :delete
type_in_content_editor "Some **rich** _text_ ~~content~~ [link](https://gitlab.com)"
type_in_content_editor [modifier_key, 'a']
type_in_content_editor [modifier_key, 'x']
end
it 'pastes text with formatting if ctrl + v is pressed' do
type_in_content_editor [modifier_key, 'v']
page.within content_editor_testid do
expect(page).to have_selector('strong', text: 'rich')
expect(page).to have_selector('em', text: 'text')
expect(page).to have_selector('s', text: 'content')
expect(page).to have_selector('a[href="https://gitlab.com"]', text: 'link')
end
end
it 'does not show a loading indicator after undo paste' do
type_in_content_editor [modifier_key, 'v']
type_in_content_editor [modifier_key, 'z']
page.within content_editor_testid do
expect(page).not_to have_css('.gl-dots-loader')
end
end
it 'pastes raw text without formatting if shift + ctrl + v is pressed' do
type_in_content_editor [modifier_key, :shift, 'v']
page.within content_editor_testid do
expect(page).to have_text('Some rich text content link')
expect(page).not_to have_selector('strong')
expect(page).not_to have_selector('em')
expect(page).not_to have_selector('s')
expect(page).not_to have_selector('a')
end
end
it 'pastes raw markdown with formatting when pasting inside a markdown code block' do
type_in_content_editor '```md'
type_in_content_editor :enter
type_in_content_editor [modifier_key, 'v']
page.within content_editor_testid do
expect(page).to have_selector('pre', text: 'Some **rich** _text_ ~~content~~ [link](https://gitlab.com)')
end
end
it 'pastes raw markdown without formatting when pasting inside a plaintext code block' do
type_in_content_editor '```'
type_in_content_editor :enter
type_in_content_editor [modifier_key, 'v']
page.within content_editor_testid do
expect(page).to have_selector('pre', text: 'Some rich text content link')
end
end
it 'pastes raw text without formatting, stripping whitespaces, if shift + ctrl + v is pressed' do
type_in_content_editor " Some **rich**"
type_in_content_editor :enter
type_in_content_editor " _text_"
type_in_content_editor :enter
type_in_content_editor " ~~content~~"
type_in_content_editor :enter
type_in_content_editor " [link](https://gitlab.com)"
type_in_content_editor [modifier_key, 'a']
type_in_content_editor [modifier_key, 'x']
type_in_content_editor [modifier_key, :shift, 'v']
page.within content_editor_testid do
expect(page).to have_text('Some rich text content link')
end
end
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - diagrams' do
include RichTextEditorHelpers
describe 'mermaid diagram' do
before do
switch_to_content_editor
type_in_content_editor [:enter, :enter]
type_in_content_editor '```mermaid '
type_in_content_editor ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34']
end
it 'renders and updates the diagram correctly in a sandboxed iframe' do
iframe = find(content_editor_testid).find('iframe')
expect(iframe['src']).to include('/-/sandbox/mermaid')
within_frame(iframe) do
expect(find('svg .nodes').text).to include('JohnDoe12')
expect(find('svg .nodes').text).to include('HelloWorld34')
end
expect(iframe['height'].to_i).to be > 100
find(content_editor_testid).send_keys [:enter, ' JaneDoe34 --> HelloWorld56']
within_frame(iframe) do
page.has_content?('JaneDoe34')
expect(find('svg .nodes').text).to include('JaneDoe34')
expect(find('svg .nodes').text).to include('HelloWorld56')
end
end
it 'toggles the diagram when preview button is clicked',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397682' do
find('[data-testid="preview-diagram"]').click
expect(find(content_editor_testid)).not_to have_selector('iframe')
find('[data-testid="preview-diagram"]').click
iframe = find(content_editor_testid).find('iframe')
within_frame(iframe) do
expect(find('svg .nodes').text).to include('JohnDoe12')
expect(find('svg .nodes').text).to include('HelloWorld34')
end
end
end
describe 'drawio diagram' do
before do
switch_to_content_editor
click_attachment_button
end
it 'displays correct media bubble menu with edit diagram button' do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg'
expect_media_bubble_menu_to_be_visible
click_edit_diagram_button
expect_drawio_editor_is_opened
end
end
end

View File

@ -0,0 +1,230 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - links' do
include RichTextEditorHelpers
describe 'creating and editing links' do
before do
switch_to_content_editor
end
context 'when clicking the link icon in the toolbar' do
it 'shows the link bubble menu' do
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
expect(page).to have_css('[data-testid="link-bubble-menu"]')
end
context 'if no text is selected' do
before do
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
end
it 'opens an empty inline modal to create a link' do
page.within '[data-testid="link-bubble-menu"]' do
expect(page).to have_field('link-text', with: '')
expect(page).to have_field('link-href', with: '')
end
end
context 'when the user clicks the apply button' do
it 'applies the changes to the document' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'Link to GitLab home page'
fill_in 'link-href', with: 'https://gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_css('a[href="https://gitlab.com"]')
expect(page).to have_text('Link to GitLab home page')
end
end
end
context 'when the user clicks the cancel button' do
it 'does not apply the changes to the document' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'Link to GitLab home page'
fill_in 'link-href', with: 'https://gitlab.com'
click_button 'Cancel'
end
page.within content_editor_testid do
expect(page).not_to have_css('a')
end
end
end
end
context 'if text is selected' do
before do
type_in_content_editor 'The quick brown fox jumps over the lazy dog'
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
end
it 'prefills inline modal to create a link' do
page.within '[data-testid="link-bubble-menu"]' do
expect(page).to have_field('link-text', with: 'dog')
expect(page).to have_field('link-href', with: '')
end
end
context 'when the user clicks the apply button' do
it 'applies the changes to the document' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'new dog'
fill_in 'link-href', with: 'https://en.wikipedia.org/wiki/Shiba_Inu'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://en.wikipedia.org/wiki/Shiba_Inu"]',
text: 'new dog'
)
end
end
end
end
end
context 'if cursor is placed on an existing link' do
before do
type_in_content_editor 'Link to [GitLab home **page**](https://gitlab.com)'
type_in_content_editor :left
end
it 'prefills inline modal to edit the link' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
expect(page).to have_field('link-text', with: 'GitLab home page')
expect(page).to have_field('link-href', with: 'https://gitlab.com')
end
end
it 'updates the link attributes if text is not updated' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]')
expect(page.find('a')).to have_text('GitLab home page')
expect(page).to have_selector('strong', text: 'page')
end
end
it 'updates the link attributes and text if text is updated' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
fill_in 'link-text', with: 'GitLab about page'
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]',
text: 'GitLab about page'
)
expect(page).not_to have_selector('strong')
end
end
it 'does nothing if Cancel is clicked' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="edit-link"]').click
click_button 'Cancel'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://gitlab.com"]',
text: 'GitLab home page'
)
expect(page).to have_selector('strong')
end
end
context 'when the user clicks the unlink button' do
it 'removes the link' do
page.within '[data-testid="link-bubble-menu"]' do
page.find('[data-testid="remove-link"]').click
end
page.within content_editor_testid do
expect(page).not_to have_selector('a')
expect(page).to have_selector('strong', text: 'page')
end
end
end
end
context 'when selection spans more than a link' do
before do
type_in_content_editor 'a [b **c**](https://gitlab.com)'
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
type_in_content_editor [:shift, :left]
page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click
end
it 'prefills inline modal with the entire selection' do
page.within '[data-testid="link-bubble-menu"]' do
expect(page).to have_field('link-text', with: 'a b c')
expect(page).to have_field('link-href', with: '')
end
end
it 'expands the link and updates the link attributes if text is not updated' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]')
expect(page.find('a')).to have_text('a b c')
expect(page).to have_selector('strong', text: 'c')
end
end
it 'expands the link, updates the link attributes and text if text is updated',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419684' do
page.within '[data-testid="link-bubble-menu"]' do
fill_in 'link-text', with: 'new text'
fill_in 'link-href', with: 'https://about.gitlab.com'
click_button 'Apply'
end
page.within content_editor_testid do
expect(page).to have_selector('a[href="https://about.gitlab.com"]',
text: 'new text'
)
expect(page).not_to have_selector('strong')
end
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - media' do
include RichTextEditorHelpers
describe 'media elements bubble menu' do
before do
switch_to_content_editor
click_attachment_button
end
it 'displays correct media bubble menu for images', :js do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
expect_media_bubble_menu_to_be_visible
end
it 'displays correct media bubble menu for video', :js do
display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
expect_media_bubble_menu_to_be_visible
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'rich text editor - selection' do
include RichTextEditorHelpers
let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
let(:modifier_key) { is_mac ? :command : :control }
describe 'selecting text' do
before do
switch_to_content_editor
# delete all text first
type_in_content_editor [modifier_key, 'a']
type_in_content_editor :backspace
type_in_content_editor 'The quick **brown** fox _jumps_ over the lazy dog!'
type_in_content_editor :enter
type_in_content_editor '[Link](https://gitlab.com)'
type_in_content_editor :enter
type_in_content_editor 'Jackdaws love my ~~big~~ sphinx of quartz!'
# select all text
type_in_content_editor [modifier_key, 'a']
end
it 'renders selected text in a .content-editor-selection class' do
page.within content_editor_testid do
assert_selected 'The quick'
assert_selected 'brown'
assert_selected 'fox'
assert_selected 'jumps'
assert_selected 'over the lazy dog!'
assert_selected 'Link'
assert_selected 'Jackdaws love my'
assert_selected 'big'
assert_selected 'sphinx of quartz!'
end
end
def assert_selected(text)
expect(page).to have_selector('.content-editor-selection', text: text)
end
end
end

View File

@ -149,12 +149,12 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
it_behaves_like 'edits content using the content editor', {
it_behaves_like 'rich text editor - common'
it_behaves_like 'rich text editor - autocomplete', {
with_expanded_references: false,
with_quick_actions: false
}
it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
it_behaves_like 'rich text editor - diagrams'
end
context 'when the page is in a subdir', :js do

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.shared_examples 'issues or work items finder' do |factory, execute_context|
describe '#execute' do
include_context execute_context
include_context execute_context, factory
context 'scope: all' do
let(:scope) { 'all' }
@ -208,7 +208,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
context 'when include_subgroup param not set' do
it 'returns all group items' do
expect(items).to contain_exactly(item1, item5, group_level_item)
expect(items).to contain_exactly(item1, item5)
end
context 'when projects outside the group are passed' do
@ -239,7 +239,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
let(:params) { { group_id: group.id, release_tag: 'dne-release-tag' } }
it 'ignores the release_tag parameter' do
expect(items).to contain_exactly(item1, item5, group_level_item)
expect(items).to contain_exactly(item1, item5)
end
end
end
@ -250,7 +250,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
it 'returns all group and subgroup items' do
expect(items).to contain_exactly(item1, item4, item5, group_level_item)
expect(items).to contain_exactly(item1, item4, item5)
end
context 'when mixed projects are passed' do
@ -261,26 +261,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
end
end
context 'when user has access to confidential items' do
before do
group.add_reporter(user)
end
it 'includes confidential group-level items' do
expect(items).to contain_exactly(item1, item5, group_level_item, group_level_confidential_item)
end
end
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'only returns project-level items' do
expect(items).to contain_exactly(item1, item5)
end
end
end
context 'filtering by author' do

View File

@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category: :backup_restore do
let(:enable_registry) { true }
let(:backup_restore_pid_path) { "#{Rails.application.root}/tmp/backup_restore.pid" }
let(:backup_tasks) do
let(:backup_rake_task_names) do
%w[db repo uploads builds artifacts pages lfs terraform_state registry packages ci_secure_files]
end
let(:backup_types) do
let(:backup_task_ids) do
%w[db repositories uploads builds artifacts pages lfs terraform_state registry packages ci_secure_files]
end
@ -69,7 +69,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
end
def reenable_backup_sub_tasks
backup_tasks.each do |subtask|
backup_rake_task_names.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@ -131,8 +131,9 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
allow(progress).to receive(:puts).with(delete_message).once
allow(progress).to receive(:puts).with(rewritten_message).once
allow_next_instance_of(::Backup::Manager) do |instance|
allow(instance).to receive(:run_restore_task).with('db')
allow_next_instance_of(::Backup::Manager) do |manager|
task = manager.find_task('db')
allow(manager).to receive(:run_restore_task).with(task)
end
expect(pid_file).to receive(:flock).with(File::LOCK_EX)
@ -168,9 +169,10 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
allow(File).to receive(:delete).with(backup_restore_pid_path)
allow(progress).to receive(:puts).at_least(:once)
allow_next_instance_of(::Backup::Manager) do |instance|
Array(task_name).each do |task|
allow(instance).to receive(:run_restore_task).with(task)
allow_next_instance_of(::Backup::Manager) do |manager|
Array(task_name).each do |t|
task = manager.find_task(t)
allow(manager).to receive(:run_restore_task).with(task)
end
end
end
@ -206,11 +208,13 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
before do
allow(YAML).to receive(:safe_load_file)
.and_return({ gitlab_version: gitlab_version })
expect_next_instance_of(::Backup::Manager) do |instance|
backup_types.each do |subtask|
expect(instance).to receive(:run_restore_task).with(subtask).ordered
expect_next_instance_of(::Backup::Manager) do |manager|
backup_task_ids.each do |t|
task = manager.find_task(t)
expect(manager).to receive(:run_restore_task).with(task).ordered
end
expect(instance).not_to receive(:run_restore_task)
expect(manager).not_to receive(:run_restore_task)
end
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
end
@ -259,11 +263,12 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
allow(YAML).to receive(:safe_load_file)
.and_return({ gitlab_version: Gitlab::VERSION })
expect_next_instance_of(::Backup::Manager) do |instance|
backup_types.each do |subtask|
expect(instance).to receive(:run_restore_task).with(subtask).ordered
expect_next_instance_of(::Backup::Manager) do |manager|
backup_task_ids.each do |t|
task = manager.find_task(t)
expect(manager).to receive(:run_restore_task).with(task).ordered
end
expect(instance).not_to receive(:run_restore_task)
expect(manager).not_to receive(:run_restore_task)
end
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
@ -296,7 +301,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
end
it 'prints a progress message to stdout' do
backup_tasks.each do |task|
backup_rake_task_names.each do |task|
expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout_from_any_process
end
end
@ -324,7 +329,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping ci secure files ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping ci secure files ... done")
backup_tasks.each do |task|
backup_rake_task_names.each do |task|
run_rake_task("gitlab:backup:#{task}:create")
end
end
@ -639,11 +644,12 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
allow(Rake::Task['gitlab:shell:setup'])
.to receive(:invoke).and_return(true)
expect_next_instance_of(::Backup::Manager) do |instance|
(backup_types - %w[repositories uploads]).each do |subtask|
expect(instance).to receive(:run_restore_task).with(subtask).ordered
expect_next_instance_of(::Backup::Manager) do |manager|
(backup_task_ids - %w[repositories uploads]).each do |t|
task = manager.find_task(t)
expect(manager).to receive(:run_restore_task).with(task).ordered
end
expect(instance).not_to receive(:run_restore_task)
expect(manager).not_to receive(:run_restore_task)
end
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
@ -684,11 +690,13 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
allow(Rake::Task['gitlab:shell:setup'])
.to receive(:invoke).and_return(true)
expect_next_instance_of(::Backup::Manager) do |instance|
backup_types.each do |subtask|
expect(instance).to receive(:run_restore_task).with(subtask).ordered
expect_next_instance_of(::Backup::Manager) do |manager|
backup_task_ids.each do |t|
task = manager.find_task(t)
expect(manager).to receive(:run_restore_task).with(task).ordered
end
expect(instance).not_to receive(:run_restore_task)
expect(manager).not_to receive(:run_restore_task)
end
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task("gitlab:backup:restore") }.to output.to_stdout_from_any_process