Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9db4bab965
commit
dbdf272ee1
|
|
@ -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'
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ query getOrganizationGroups(
|
|||
projectsCount
|
||||
groupMembersCount
|
||||
visibility
|
||||
maxAccessLevel {
|
||||
integerValue
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ query getOrganizationProjects(
|
|||
userPermissions {
|
||||
removeProject
|
||||
}
|
||||
maxAccessLevel {
|
||||
integerValue
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export default () => ({
|
|||
* resourceId: String,
|
||||
* pageType: String,
|
||||
* emptyListIllustration: String,
|
||||
* emptyListHelpUrl: String,
|
||||
* comingSoon: { projectPath: String, suggestedContributions : String } | null;
|
||||
* }
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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') } }
|
||||
|
|
|
|||
|
|
@ -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') } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ describe('Mutations Registry Store', () => {
|
|||
pageType: 'groups',
|
||||
userCanDelete: '',
|
||||
emptyListIllustration: 'foo',
|
||||
emptyListHelpUrl: 'baz',
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue