Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-14 18:12:38 +00:00
parent a2b7b398c7
commit 283c7bb302
65 changed files with 1353 additions and 778 deletions

View File

@ -3,7 +3,6 @@ Rake/Require:
Details: grace period Details: grace period
Exclude: Exclude:
- 'lib/tasks/gitlab/assets.rake' - 'lib/tasks/gitlab/assets.rake'
- 'lib/tasks/gitlab/cleanup.rake'
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake' - 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
- 'lib/tasks/gitlab/docs/redirect.rake' - 'lib/tasks/gitlab/docs/redirect.rake'
- 'lib/tasks/gitlab/graphql.rake' - 'lib/tasks/gitlab/graphql.rake'

View File

@ -130,9 +130,6 @@ export default {
displayMaskedError() { displayMaskedError() {
return !this.canMask && this.variable.masked; return !this.canMask && this.variable.masked;
}, },
isUsingRawRegexFlag() {
return this.glFeatures.ciRemoveCharacterLimitationRawMaskedVar;
},
isEditing() { isEditing() {
return this.mode === EDIT_VARIABLE_ACTION; return this.mode === EDIT_VARIABLE_ACTION;
}, },
@ -177,7 +174,7 @@ export default {
return true; return true;
}, },
useRawMaskableRegexp() { useRawMaskableRegexp() {
return this.isRaw && this.isUsingRawRegexFlag; return this.isRaw;
}, },
variableValidationFeedback() { variableValidationFeedback() {
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;

View File

@ -77,7 +77,7 @@ export default {
<template> <template>
<gl-intersection-observer <gl-intersection-observer
class="gl-relative gl-top-2" class="gl-relative gl-top-n5"
@appear="setStickyHeaderVisible(false)" @appear="setStickyHeaderVisible(false)"
@disappear="setStickyHeaderVisible(true)" @disappear="setStickyHeaderVisible(true)"
> >

View File

@ -1,15 +1,30 @@
<script> <script>
import { n__ } from '~/locale';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import Tracking from '~/tracking';
export default { export default {
components: { components: {
DeleteModal,
VersionRow, VersionRow,
PackagesListLoader, PackagesListLoader,
RegistryList, RegistryList,
}, },
mixins: [Tracking.mixin()],
props: { props: {
canDestroy: {
type: Boolean,
required: false,
default: false,
},
versions: { versions: {
type: Array, type: Array,
required: true, required: true,
@ -25,11 +40,35 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
itemsToBeDeleted: [],
};
},
computed: { computed: {
listTitle() {
return n__('%d version', '%d versions', this.versions.length);
},
isListEmpty() { isListEmpty() {
return this.versions.length === 0; return this.versions.length === 0;
}, },
}, },
methods: {
deleteItemsCanceled() {
this.track(CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION);
this.itemsToBeDeleted = [];
},
deleteItemsConfirmation() {
this.$emit('delete', this.itemsToBeDeleted);
this.track(DELETE_PACKAGE_VERSIONS_TRACKING_ACTION);
this.itemsToBeDeleted = [];
},
setItemsToBeDeleted(items) {
this.itemsToBeDeleted = items;
this.track(REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION);
this.$refs.deletePackagesModal.show();
},
},
}; };
</script> </script>
<template> <template>
@ -40,17 +79,34 @@ export default {
<slot v-else-if="isListEmpty" name="empty-state"></slot> <slot v-else-if="isListEmpty" name="empty-state"></slot>
<div v-else> <div v-else>
<registry-list <registry-list
:hidden-delete="true" :hidden-delete="!canDestroy"
:is-loading="isLoading" :is-loading="isLoading"
:items="versions" :items="versions"
:pagination="pageInfo" :pagination="pageInfo"
:title="listTitle"
@delete="setItemsToBeDeleted"
@prev-page="$emit('prev-page')" @prev-page="$emit('prev-page')"
@next-page="$emit('next-page')" @next-page="$emit('next-page')"
> >
<template #default="{ item }"> <template #default="{ first, item, isSelected, selectItem }">
<version-row :package-entity="item" /> <!-- `first` prop is used to decide whether to show the top border
for the first element. We want to show the top border only when
user has permission to bulk delete versions. -->
<version-row
:first="canDestroy && first"
:package-entity="item"
:selected="isSelected(item)"
@select="selectItem(item)"
/>
</template> </template>
</registry-list> </registry-list>
<delete-modal
ref="deletePackagesModal"
:items-to-be-deleted="itemsToBeDeleted"
@confirm="deleteItemsConfirmation"
@cancel="deleteItemsCanceled"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,12 @@
<script> <script>
import { GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; import {
GlFormCheckbox,
GlIcon,
GlLink,
GlSprintf,
GlTooltipDirective,
GlTruncate,
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
@ -15,6 +22,7 @@ import {
export default { export default {
name: 'PackageVersionRow', name: 'PackageVersionRow',
components: { components: {
GlFormCheckbox,
GlIcon, GlIcon,
GlLink, GlLink,
GlSprintf, GlSprintf,
@ -32,6 +40,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
selected: {
type: Boolean,
default: false,
required: false,
},
}, },
computed: { computed: {
containsWebPathLink() { containsWebPathLink() {
@ -53,7 +66,15 @@ export default {
</script> </script>
<template> <template>
<list-item> <list-item :selected="selected" v-bind="$attrs">
<template #left-action>
<gl-form-checkbox
v-if="packageEntity.canDestroy"
class="gl-m-0"
:checked="selected"
@change="$emit('select')"
/>
</template>
<template #left-primary> <template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link v-if="containsWebPathLink" class="gl-text-body gl-min-w-0" :href="packageLink"> <gl-link v-if="containsWebPathLink" class="gl-text-body gl-min-w-0" :href="packageLink">

View File

@ -100,7 +100,7 @@ export default {
</script> </script>
<template> <template>
<list-item data-testid="package-row" v-bind="$attrs"> <list-item data-testid="package-row" :selected="selected" v-bind="$attrs">
<template #left-action> <template #left-action>
<gl-form-checkbox <gl-form-checkbox
v-if="packageEntity.canDestroy" v-if="packageEntity.canDestroy"

View File

@ -115,6 +115,10 @@ export const DELETE_PACKAGES_TRACKING_ACTION = 'delete_packages';
export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages'; export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages';
export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages'; export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages';
export const DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'delete_package_versions';
export const REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'request_delete_package_versions';
export const CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'cancel_delete_package_versions';
export const DELETE_PACKAGES_ERROR_MESSAGE = s__( export const DELETE_PACKAGES_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting packages.', 'PackageRegistry|Something went wrong while deleting packages.',
); );

View File

@ -66,6 +66,7 @@ query getPackageDetails(
nodes { nodes {
id id
name name
canDestroy
createdAt createdAt
version version
status status

View File

@ -95,6 +95,7 @@ export default {
deletePackageModalContent: DELETE_MODAL_CONTENT, deletePackageModalContent: DELETE_MODAL_CONTENT,
filesToDelete: [], filesToDelete: [],
mutationLoading: false, mutationLoading: false,
versionsMutationLoading: false,
packageEntity: {}, packageEntity: {},
}; };
}, },
@ -146,6 +147,9 @@ export default {
isLoading() { isLoading() {
return this.$apollo.queries.packageEntity.loading; return this.$apollo.queries.packageEntity.loading;
}, },
isVersionsLoading() {
return this.isLoading || this.versionsMutationLoading;
},
packageFilesLoading() { packageFilesLoading() {
return this.isLoading || this.mutationLoading; return this.isLoading || this.mutationLoading;
}, },
@ -157,9 +161,6 @@ export default {
category: packageTypeToTrackCategory(this.packageType), category: packageTypeToTrackCategory(this.packageType),
}; };
}, },
hasVersions() {
return this.packageEntity.versions?.nodes?.length > 0;
},
versionPageInfo() { versionPageInfo() {
return this.packageEntity?.versions?.pageInfo ?? {}; return this.packageEntity?.versions?.pageInfo ?? {};
}, },
@ -181,6 +182,14 @@ export default {
PACKAGE_TYPE_PYPI, PACKAGE_TYPE_PYPI,
].includes(this.packageType); ].includes(this.packageType);
}, },
refetchQueriesData() {
return [
{
query: getPackageDetails,
variables: this.queryVariables,
},
];
},
}, },
methods: { methods: {
formatSize(size) { formatSize(size) {
@ -206,12 +215,7 @@ export default {
ids, ids,
}, },
awaitRefetchQueries: true, awaitRefetchQueries: true,
refetchQueries: [ refetchQueries: this.refetchQueriesData,
{
query: getPackageDetails,
variables: this.queryVariables,
},
],
}); });
if (data?.destroyPackageFiles?.errors[0]) { if (data?.destroyPackageFiles?.errors[0]) {
throw data.destroyPackageFiles.errors[0]; throw data.destroyPackageFiles.errors[0];
@ -403,10 +407,19 @@ export default {
}}</gl-badge> }}</gl-badge>
</template> </template>
<delete-packages
:refetch-queries="refetchQueriesData"
show-success-alert
@start="versionsMutationLoading = true"
@end="versionsMutationLoading = false"
>
<template #default="{ deletePackages }">
<package-versions-list <package-versions-list
:is-loading="isLoading" :can-destroy="packageEntity.canDestroy"
:is-loading="isVersionsLoading"
:page-info="versionPageInfo" :page-info="versionPageInfo"
:versions="packageEntity.versions.nodes" :versions="packageEntity.versions.nodes"
@delete="deletePackages"
@prev-page="fetchPreviousVersionsPage" @prev-page="fetchPreviousVersionsPage"
@next-page="fetchNextVersionsPage" @next-page="fetchNextVersionsPage"
> >
@ -416,6 +429,8 @@ export default {
</p> </p>
</template> </template>
</package-versions-list> </package-versions-list>
</template>
</delete-packages>
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>

View File

@ -1,138 +0,0 @@
<script>
import { GlDropdownSectionHeader, GlDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RefResultsSection',
components: {
GlDropdownSectionHeader,
GlDropdownItem,
GlBadge,
GlIcon,
},
props: {
showHeader: {
type: Boolean,
required: false,
default: true,
},
sectionTitle: {
type: String,
required: true,
},
totalCount: {
type: Number,
required: true,
},
/**
* An array of object that have the following properties:
*
* - name (String, required): The name of the ref that will be displayed
* - value (String, optional): The value that will be selected when the ref
* is selected. If not provided, `name` will be used as the value.
* For example, commits use the short SHA for `name`
* and long SHA for `value`.
* - subtitle (String, optional): Text to render underneath the name.
* For example, used to render the commit's title underneath its SHA.
* - default (Boolean, optional): Whether or not to render a "default"
* indicator next to the item. Used to indicate
* the project's default branch.
*
*/
items: {
type: Array,
required: true,
validator: (items) => Array.isArray(items) && items.every((item) => item.name),
},
/**
* The currently selected ref.
* Used to render a check mark by the selected item.
* */
selectedRef: {
type: String,
required: false,
default: '',
},
/**
* An error object that indicates that an error
* occurred while fetching items for this section
*/
error: {
type: Error,
required: false,
default: null,
},
/** The message to display if an error occurs */
errorMessage: {
type: String,
required: false,
default: '',
},
shouldShowCheck: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
totalCountText() {
return this.totalCount > 999 ? s__('TotalRefCountIndicator|1000+') : `${this.totalCount}`;
},
},
methods: {
showCheck(item) {
if (!this.shouldShowCheck) {
return false;
}
return item.name === this.selectedRef || item.value === this.selectedRef;
},
},
};
</script>
<template>
<div>
<gl-dropdown-section-header v-if="showHeader">
<div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
</div>
</gl-dropdown-section-header>
<template v-if="error">
<div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3">
<gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
<span>{{ errorMessage }}</span>
</div>
</template>
<template v-else>
<gl-dropdown-item
v-for="item in items"
:key="item.name"
@click="$emit('selected', item.value || item.name)"
>
<div class="gl-display-flex align-items-start">
<gl-icon
name="mobile-issue-close"
class="gl-mr-2 gl-flex-shrink-0"
:class="{ 'gl-visibility-hidden': !showCheck(item) }"
/>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
<span class="gl-font-monospace">{{ item.name }}</span>
<span class="gl-text-gray-400">{{ item.subtitle }}</span>
</div>
<gl-badge v-if="item.default" size="sm" variant="info">{{
s__('DefaultBranchLabel|default')
}}</gl-badge>
</div>
</gl-dropdown-item>
</template>
</div>
</template>

View File

@ -1,13 +1,8 @@
<script> <script>
import { import { GlBadge, GlIcon, GlCollapsibleListbox } from '@gitlab/ui';
GlDropdown,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce, isArray } from 'lodash'; import { debounce, isArray } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { sprintf } from '~/locale';
import { import {
ALL_REF_TYPES, ALL_REF_TYPES,
SEARCH_DEBOUNCE_MS, SEARCH_DEBOUNCE_MS,
@ -15,21 +10,16 @@ import {
REF_TYPE_BRANCHES, REF_TYPE_BRANCHES,
REF_TYPE_TAGS, REF_TYPE_TAGS,
REF_TYPE_COMMITS, REF_TYPE_COMMITS,
BRANCH_REF_TYPE,
TAG_REF_TYPE,
} from '../constants'; } from '../constants';
import createStore from '../stores'; import createStore from '../stores';
import RefResultsSection from './ref_results_section.vue'; import { formatListBoxItems, formatErrors } from '../format_refs';
export default { export default {
name: 'RefSelector', name: 'RefSelector',
components: { components: {
GlDropdown, GlBadge,
GlDropdownDivider, GlIcon,
GlSearchBoxByType, GlCollapsibleListbox,
GlSprintf,
GlLoadingIcon,
RefResultsSection,
}, },
inheritAttrs: false, inheritAttrs: false,
props: { props: {
@ -87,7 +77,6 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
toggleButtonClass: { toggleButtonClass: {
type: [String, Object, Array], type: [String, Object, Array],
required: false, required: false,
@ -112,29 +101,17 @@ export default {
...this.translations, ...this.translations,
}; };
}, },
showBranchesSection() { listBoxItems() {
return ( return formatListBoxItems(this.branches, this.tags, this.commits);
this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
);
}, },
showTagsSection() { branches() {
return ( return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : [];
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
);
}, },
showCommitsSection() { tags() {
return ( return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : [];
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
);
}, },
showNoResults() { commits() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection; return this.enabledRefTypes.includes(REF_TYPE_COMMITS) ? this.matches.commits.list : [];
},
showSectionHeaders() {
return this.enabledRefTypes.length > 1;
}, },
extendedToggleButtonClass() { extendedToggleButtonClass() {
const classes = [ const classes = [
@ -142,7 +119,6 @@ export default {
'gl-inset-border-1-red-500!': !this.state, 'gl-inset-border-1-red-500!': !this.state,
'gl-font-monospace': Boolean(this.selectedRef), 'gl-font-monospace': Boolean(this.selectedRef),
}, },
'gl-max-w-26',
]; ];
if (Array.isArray(this.toggleButtonClass)) { if (Array.isArray(this.toggleButtonClass)) {
@ -160,6 +136,9 @@ export default {
query: this.lastQuery, query: this.lastQuery,
}; };
}, },
errors() {
return formatErrors(this.matches.branches, this.matches.tags, this.matches.commits);
},
selectedRefForDisplay() { selectedRefForDisplay() {
if (this.useSymbolicRefNames && this.selectedRef) { if (this.useSymbolicRefNames && this.selectedRef) {
return this.selectedRef.replace(/^refs\/(tags|heads)\//, ''); return this.selectedRef.replace(/^refs\/(tags|heads)\//, '');
@ -170,11 +149,12 @@ export default {
buttonText() { buttonText() {
return this.selectedRefForDisplay || this.i18n.noRefSelected; return this.selectedRefForDisplay || this.i18n.noRefSelected;
}, },
isTagRefType() { noResultsMessage() {
return this.refType === TAG_REF_TYPE; return this.lastQuery
}, ? sprintf(this.i18n.noResultsWithQuery, {
isBranchRefType() { query: this.lastQuery,
return this.refType === BRANCH_REF_TYPE; })
: this.i18n.noResults;
}, },
}, },
watch: { watch: {
@ -202,9 +182,7 @@ export default {
// because we need to access the .cancel() method // because we need to access the .cancel() method
// lodash attaches to the function, which is // lodash attaches to the function, which is
// made inaccessible by Vue. // made inaccessible by Vue.
this.debouncedSearch = debounce(function search() { this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS);
this.search();
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId); this.setProjectId(this.projectId);
@ -231,14 +209,8 @@ export default {
'setSelectedRef', 'setSelectedRef',
]), ]),
...mapActions({ storeSearch: 'search' }), ...mapActions({ storeSearch: 'search' }),
focusSearchBox() { onSearchBoxInput(searchQuery = '') {
this.$refs.searchBox.$el.querySelector('input').focus(); this.query = searchQuery?.trim();
},
onSearchBoxEnter() {
this.debouncedSearch.cancel();
this.search();
},
onSearchBoxInput() {
this.debouncedSearch(); this.debouncedSearch();
}, },
selectRef(ref) { selectRef(ref) {
@ -248,104 +220,55 @@ export default {
search() { search() {
this.storeSearch(this.query); this.storeSearch(this.query);
}, },
totalCountText(count) {
return count > 999 ? this.i18n.totalCountLabel : `${count}`;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-dropdown <gl-collapsible-listbox
class="ref-selector gl-w-full"
block
searchable
:selected="selectedRef"
:header-text="i18n.dropdownHeader" :header-text="i18n.dropdownHeader"
:items="listBoxItems"
:no-results-text="noResultsMessage"
:searching="isLoading"
:search-placeholder="i18n.searchPlaceholder"
:toggle-class="extendedToggleButtonClass" :toggle-class="extendedToggleButtonClass"
:text="buttonText" :toggle-text="buttonText"
class="ref-selector"
v-bind="$attrs" v-bind="$attrs"
v-on="$listeners" v-on="$listeners"
@shown="focusSearchBox" @hidden="$emit('hide')"
@search="onSearchBoxInput"
@select="selectRef"
> >
<template #header> <template #group-label="{ group }">
<gl-search-box-by-type {{ group.text }} <gl-badge size="sm">{{ totalCountText(group.options.length) }}</gl-badge>
ref="searchBox"
v-model.trim="query"
:placeholder="i18n.searchPlaceholder"
autocomplete="off"
data-qa-selector="ref_selector_searchbox"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"
/>
</template> </template>
<template #list-item="{ item }">
<gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" /> {{ item.text }}
<gl-badge v-if="item.default" size="sm" variant="info">{{
<div i18n.defaultLabelText
v-else-if="showNoResults" }}</gl-badge>
class="gl-text-center gl-mx-3 gl-py-3"
data-testid="no-results"
>
<gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
<template #query>
<b class="gl-word-break-all">{{ lastQuery }}</b>
</template> </template>
</gl-sprintf>
<span v-else>{{ i18n.noResults }}</span>
</div>
<template v-else>
<template v-if="showBranchesSection">
<ref-results-section
:section-title="i18n.branches"
:total-count="matches.branches.totalCount"
:items="matches.branches.list"
:selected-ref="selectedRef"
:error="matches.branches.error"
:error-message="i18n.branchesErrorMessage"
:show-header="showSectionHeaders"
data-testid="branches-section"
data-qa-selector="branches_section"
:should-show-check="!useSymbolicRefNames || isBranchRefType"
@selected="selectRef($event)"
/>
<gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
</template>
<template v-if="showTagsSection">
<ref-results-section
:section-title="i18n.tags"
:total-count="matches.tags.totalCount"
:items="matches.tags.list"
:selected-ref="selectedRef"
:error="matches.tags.error"
:error-message="i18n.tagsErrorMessage"
:show-header="showSectionHeaders"
data-testid="tags-section"
:should-show-check="!useSymbolicRefNames || isTagRefType"
@selected="selectRef($event)"
/>
<gl-dropdown-divider v-if="showCommitsSection" />
</template>
<template v-if="showCommitsSection">
<ref-results-section
:section-title="i18n.commits"
:total-count="matches.commits.totalCount"
:items="matches.commits.list"
:selected-ref="selectedRef"
:error="matches.commits.error"
:error-message="i18n.commitsErrorMessage"
:show-header="showSectionHeaders"
data-testid="commits-section"
@selected="selectRef($event)"
/>
</template>
</template>
<template #footer> <template #footer>
<slot name="footer" v-bind="footerSlotProps"></slot> <slot name="footer" v-bind="footerSlotProps"></slot>
<div
v-for="errorMessage in errors"
:key="errorMessage"
data-testid="red-selector-error-list"
class="gl-display-flex gl-align-items-flex-start gl-text-red-500 gl-mx-4 gl-my-3"
>
<gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
<span>{{ errorMessage }}</span>
</div>
</template> </template>
</gl-dropdown> </gl-collapsible-listbox>
<input <input
v-if="name" v-if="name"
data-testid="selected-ref-form-field" data-testid="selected-ref-form-field"

View File

@ -1,5 +1,5 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale'; import { s__, __ } from '~/locale';
export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES'; export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS'; export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
@ -13,6 +13,7 @@ export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS; export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const DEFAULT_I18N = Object.freeze({ export const DEFAULT_I18N = Object.freeze({
defaultLabelText: __('default'),
dropdownHeader: __('Select Git revision'), dropdownHeader: __('Select Git revision'),
searchPlaceholder: __('Search by Git revision'), searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'), noResultsWithQuery: __('No matching results for "%{query}"'),
@ -24,4 +25,5 @@ export const DEFAULT_I18N = Object.freeze({
tags: __('Tags'), tags: __('Tags'),
commits: __('Commits'), commits: __('Commits'),
noRefSelected: __('No ref selected'), noRefSelected: __('No ref selected'),
totalCountLabel: s__('TotalRefCountIndicator|1000+'),
}); });

View File

@ -0,0 +1,60 @@
import { DEFAULT_I18N } from './constants';
function convertToListBoxItems(items) {
return items.map((item) => ({
text: item.name,
value: item.value || item.name,
default: item.default,
}));
}
/**
* Format multiple lists to array of group options for listbox
* @param branches list of branches
* @param tags list of tags
* @param commits list of commits
* @returns {*[]} array of group items with header and options
*/
export const formatListBoxItems = (branches, tags, commits) => {
const listBoxItems = [];
const addToFinalResult = (items, header) => {
if (items && items.length > 0) {
listBoxItems.push({
text: header,
options: convertToListBoxItems(items),
});
}
};
addToFinalResult(branches, DEFAULT_I18N.branches);
addToFinalResult(tags, DEFAULT_I18N.tags);
addToFinalResult(commits, DEFAULT_I18N.commits);
return listBoxItems;
};
/**
* Check error existence and add to final array
* @param branches list of branches
* @param tags list of tags
* @param commits list of commits
* @returns {*[]} array of error messages
*/
export const formatErrors = (branches, tags, commits) => {
const errorsList = [];
if (branches && branches.error) {
errorsList.push(DEFAULT_I18N.branchesErrorMessage);
}
if (tags && tags.error) {
errorsList.push(DEFAULT_I18N.tagsErrorMessage);
}
if (commits && commits.error) {
errorsList.push(DEFAULT_I18N.commitsErrorMessage);
}
return errorsList;
};

View File

@ -0,0 +1,133 @@
<script>
import { GlIntersectionObserver } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
/*
* We only highlight the chunk that is currently visible to the user.
* By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly.
*
* Content that is not visible to the user (i.e. not highlighted) does not need to look nice,
* so by rendering raw (non-highlighted) text, the browser spends less resources on painting
* content that is not immediately relevant.
* Why use plaintext as opposed to hiding content entirely?
* If content is hidden entirely, native find text ( + F) won't work.
*/
export default {
components: {
GlIntersectionObserver,
},
directives: {
SafeHtml,
},
mixins: [glFeatureFlagMixin()],
props: {
isHighlighted: {
type: Boolean,
required: true,
},
chunkIndex: {
type: Number,
required: false,
default: 0,
},
rawContent: {
type: String,
required: true,
},
highlightedContent: {
type: String,
required: true,
},
totalLines: {
type: Number,
required: false,
default: 0,
},
startingFrom: {
type: Number,
required: false,
default: 0,
},
blamePath: {
type: String,
required: true,
},
},
data() {
return {
hasAppeared: false,
isLoading: true,
};
},
computed: {
shouldHighlight() {
return Boolean(this.highlightedContent) && (this.hasAppeared || this.isHighlighted);
},
lines() {
return this.content.split('\n');
},
pageSearchString() {
if (!this.glFeatures.fileLineBlame) return '';
const page = getPageParamValue(this.number);
return getPageSearchString(this.blamePath, page);
},
},
created() {
if (this.chunkIndex === 0) {
// Display first chunk ASAP in order to improve perceived performance
this.isLoading = false;
return;
}
window.requestIdleCallback(() => {
this.isLoading = false;
});
},
methods: {
handleChunkAppear() {
this.hasAppeared = true;
},
calculateLineNumber(index) {
return this.startingFrom + index + 1;
},
},
};
</script>
<template>
<gl-intersection-observer @appear="handleChunkAppear">
<div class="gl-display-flex">
<div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
<div
v-for="(n, index) in totalLines"
:key="index"
data-testid="line-numbers"
class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
>
<a
v-if="glFeatures.fileLineBlame"
class="gl-user-select-none gl-shadow-none! file-line-blame"
:href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
></a>
<a
:id="`L${calculateLineNumber(index)}`"
class="gl-user-select-none gl-shadow-none! file-line-num"
:href="`#L${calculateLineNumber(index)}`"
:data-line-number="calculateLineNumber(index)"
>
{{ calculateLineNumber(index) }}
</a>
</div>
</div>
<div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
<!-- Placeholder for line numbers while content is not highlighted -->
</div>
<pre
class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
><code v-if="shouldHighlight" v-once v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
</div>
</gl-intersection-observer>
</template>

View File

@ -0,0 +1,58 @@
<script>
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
import Chunk from './components/chunk.vue';
export default {
components: {
Chunk,
},
directives: {
SafeHtml,
},
mixins: [Tracking.mixin()],
inject: {
highlightWorker: { default: null },
},
props: {
blob: {
type: Object,
required: true,
},
chunks: {
type: Array,
required: false,
default: () => [],
},
},
created() {
this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
addBlobLinksTracking();
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div
class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
:class="$options.userColorScheme"
data-type="simple"
:data-path="blob.path"
data-qa-selector="blob_viewer_file_content"
>
<chunk
v-for="(chunk, _, index) in chunks"
:key="index"
:chunk-index="index"
:is-highlighted="Boolean(chunk.isHighlighted)"
:raw-content="chunk.rawContent"
:highlighted-content="chunk.highlightedContent"
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
:blame-path="blob.blamePath"
/>
</div>
</template>

View File

@ -173,6 +173,7 @@ export default {
:can-edit="enableEdit" :can-edit="enableEdit"
:task-list-update-path="taskListUpdatePath" :task-list-update-path="taskListUpdatePath"
/> />
<slot name="secondary-content"></slot>
<small v-if="isUpdated" class="edited-text gl-font-sm!"> <small v-if="isUpdated" class="edited-text gl-font-sm!">
{{ __('Edited') }} {{ __('Edited') }}
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />

View File

@ -142,7 +142,7 @@ module Types
end end
def ephemeral_authentication_token def ephemeral_authentication_token
return unless runner.created_via_ui? return unless runner.authenticated_user_registration_type?
return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago
return if runner.runner_machines.any? return if runner.runner_machines.any?

View File

@ -30,6 +30,11 @@ module Ci
project_type: 3 project_type: 3
} }
enum registration_type: {
registration_token: 0,
authenticated_user: 1
}, _suffix: true
# Prefix assigned to runners created from the UI, instead of registered via the command line # Prefix assigned to runners created from the UI, instead of registered via the command line
CREATED_RUNNER_TOKEN_PREFIX = 'glrt-' CREATED_RUNNER_TOKEN_PREFIX = 'glrt-'
@ -184,6 +189,7 @@ module Ci
validate :tag_constraints validate :tag_constraints
validates :access_level, presence: true validates :access_level, presence: true
validates :runner_type, presence: true validates :runner_type, presence: true
validates :registration_type, presence: true
validate :no_projects, unless: :project_type? validate :no_projects, unless: :project_type?
validate :no_groups, unless: :group_type? validate :no_groups, unless: :group_type?
@ -196,8 +202,6 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at, :executor_type cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at, :executor_type
attr_writer :legacy_registered
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout, chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
error_message: 'Maximum job timeout has a value which could not be accepted' error_message: 'Maximum job timeout has a value which could not be accepted'
@ -297,13 +301,6 @@ module Ci
end end
end end
def initialize(params)
@legacy_registered = params&.delete(:legacy_registered)
@legacy_registered = true if @legacy_registered.nil?
super(params)
end
def assign_to(project, current_user = nil) def assign_to(project, current_user = nil)
if instance_type? if instance_type?
raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported' raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
@ -389,7 +386,7 @@ module Ci
def short_sha def short_sha
return unless token return unless token
start_index = created_via_ui? ? CREATED_RUNNER_TOKEN_PREFIX.length : 0 start_index = authenticated_user_registration_type? ? CREATED_RUNNER_TOKEN_PREFIX.length : 0
token[start_index..start_index + 8] token[start_index..start_index + 8]
end end
@ -493,15 +490,11 @@ module Ci
override :format_token override :format_token
def format_token(token) def format_token(token)
return token if @legacy_registered return token if registration_token_registration_type?
"#{CREATED_RUNNER_TOKEN_PREFIX}#{token}" "#{CREATED_RUNNER_TOKEN_PREFIX}#{token}"
end end
def created_via_ui?
token.start_with?(CREATED_RUNNER_TOKEN_PREFIX)
end
def ensure_machine(system_xid, &blk) def ensure_machine(system_xid, &blk)
RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods
end end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module ObjectStorage
module S3
def self.signed_head_url(file)
fog_storage = ::Fog::Storage.new(file.fog_credentials)
fog_dir = fog_storage.directories.new(key: file.fog_directory)
fog_file = fog_dir.files.new(key: file.path)
expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration
fog_file.collection.head_url(fog_file.key, expire_at)
end
end
end

View File

@ -20,10 +20,9 @@
- add_page_startup_api_call @endpoint_diff_batch_url - add_page_startup_api_call @endpoint_diff_batch_url
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } } .merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } }
= render "projects/merge_requests/mr_title"
- if moved_mr_sidebar_enabled? - if moved_mr_sidebar_enabled?
#js-merge-sticky-header{ data: { data: sticky_header_data.to_json } } #js-merge-sticky-header{ data: { data: sticky_header_data.to_json } }
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box" = render "projects/merge_requests/mr_box"
.merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" } .merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" }

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388986
milestone: '15.9' milestone: '15.9'
type: development type: development
group: group::source code group: group::source code
default_enabled: false default_enabled: true

View File

@ -34,6 +34,11 @@ POST /projects/:id/product_analytics/request/dry-run
The body of the load request must be a valid Cube query. The body of the load request must be a valid Cube query.
NOTE:
When measuring `TrackedEvents`, you must use `TrackedEvents.*` for `dimensions` and `timeDimensions`. The same rule applies when measuring `Sessions`.
#### Tracked events example
```json ```json
{ {
"query": { "query": {
@ -69,6 +74,29 @@ The body of the load request must be a valid Cube query.
} }
``` ```
#### Sessions example
```json
{
"query": {
"measures": [
"Sessions.count"
],
"timeDimensions": [
{
"dimension": "Sessions.startAt",
"granularity": "day"
}
],
"order": {
"Sessions.startAt": "asc"
},
"limit": 100
},
"queryType": "multi"
}
```
## Send metadata request to Cube ## Send metadata request to Cube
Return Cube Metadata for the Analytics data. For example: Return Cube Metadata for the Analytics data. For example:

View File

@ -210,7 +210,10 @@ When the user is authenticated and `simple` is not set this returns something li
"group_runners_enabled": true, "group_runners_enabled": true,
"lfs_enabled": true, "lfs_enabled": true,
"creator_id": 1, "creator_id": 1,
"import_url": null,
"import_type": null,
"import_status": "none", "import_status": "none",
"import_error": null,
"open_issues_count": 0, "open_issues_count": 0,
"ci_default_git_depth": 20, "ci_default_git_depth": 20,
"ci_forward_deployment_enabled": true, "ci_forward_deployment_enabled": true,
@ -381,6 +384,10 @@ GET /users/:user_id/projects
"created_at": "2013-09-30T13:46:02Z", "created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3, "creator_id": 3,
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"namespace": { "namespace": {
"id": 3, "id": 3,
"name": "Diaspora", "name": "Diaspora",
@ -482,6 +489,10 @@ GET /users/:user_id/projects
"created_at": "2013-09-30T13:46:02Z", "created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3, "creator_id": 3,
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"namespace": { "namespace": {
"id": 4, "id": 4,
"name": "Brightbox", "name": "Brightbox",
@ -898,6 +909,8 @@ GET /projects/:id
"avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg", "avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg",
"web_url": "http://localhost:3000/groups/diaspora" "web_url": "http://localhost:3000/groups/diaspora"
}, },
"import_url": null,
"import_type": null,
"import_status": "none", "import_status": "none",
"import_error": null, "import_error": null,
"permissions": { "permissions": {

View File

@ -421,6 +421,92 @@ scope.
| GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. | | GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. |
| GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. | | GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. |
## FAQ
### Will my runner registration workflow break?
If no action is taken before your GitLab instance is upgraded to 16.0, then your runner registration
worflow will break.
For self-managed instances, to continue using the previous runner registration process,
you can disable the `enforce_create_runner_workflow` feature flag until GitLab 17.0.
To avoid a broken workflow, you need to first create a runner in the GitLab runners admin page.
After that, you'll need to replace the registration token you're using in your runner registration
workflow with the obtained runner authentication token.
### What is the new runner registration process?
When the new runner registration process is introduced, you will:
1. Create a runner directly in the GitLab UI.
1. Receive an authentication token in return.
1. Use the authentication token instead of the registration token.
This has added benefits such as preserved ownership records for runners, and minimizes
impact on users.
The addition of a unique system ID ensures that you can reuse the same authentication token across
multiple runners.
For example, in an auto-scaling scenario where a runner manager spawns a runner process with a
fixed authentication token.
This ID generates once at the runner's startup, persists in a sidecar file, and is sent to the
GitLab instance when requesting jobs.
This allows the GitLab instance to display which system executed a given job.
### What is the estimated timeframe for the planned changes?
- In GitLab 15.10, we plan to implement runner creation directly in the runners administration page,
and prepare the runner to follow the new workflow.
- In GitLab 16.0, we plan to disable registration tokens.
For self-managed instances, to continue using
registration tokens, you can disable the `enforce_create_runner_workflow` feature flag until
GitLab 17.0.
Previous `gitlab-runner` versions (that don't include the new `system_id` value) will start to be
rejected by the GitLab instance;
- In GitLab 17.0, we plan to completely remove support for runner registration tokens.
### How will the `gitlab-runner register` command syntax change?
The `gitlab-runner register` command will stop accepting registration tokens and instead accept new
authentication tokens generated in the GitLab runners administration page.
These authentication tokens are recognizable by their `glrt-` prefix.
Example command for GitLab 15.9:
```shell
gitlab-runner register
--executor "shell" \
--url "https://gitlab.com/" \
--tag-list "shell,mac,gdk,test" \
--run-untagged="false" \
--locked="false" \
--access-level="not_protected" \
--non-interactive \
--registration-token="GR1348941C6YcZVddc8kjtdU-yWYD"
```
In GitLab 16.0, the runner will be created in the UI where some of its attributes can be
pre-configured by the creator.
Examples are the tag list, locked status, or access level. These are no longer accepted as arguments
to `register`. The following example shows the new command:
```shell
gitlab-runner register
--executor "shell" \
--url "https://gitlab.com/" \
--non-interactive \
--registration-token="grlt-2CR8_eVxiioB1QmzPZwa"
```
### How does this change impact auto-scaling scenarios?
In auto-scaling scenarios such as GitLab Runner Operator or GitLab Runner Helm Chart, the
registration token is replaced with the authentication token generated from the UI.
This means that the same runner configuration is reused across jobs, instead of creating a runner
for each job.
The specific runner can be identified by the unique system ID that is generated when the runner
process is started.
## Status ## Status
Status: RFC. Status: RFC.

View File

@ -124,39 +124,41 @@ deploy_staging:
### Create a dynamic environment ### Create a dynamic environment
To create a dynamic name and URL for an environment, you can use To create a dynamic environment, you use [CI/CD variables](../variables/index.md) that are unique to each pipeline.
[predefined CI/CD variables](../variables/predefined_variables.md). For example:
Prerequisites:
- You must have at least the Developer role.
To create a dynamic environment, in your `.gitlab-ci.yml` file:
1. Define a job in the `deploy` stage.
1. In the job, define the following environment attributes:
- `name`: Use a related CI/CD variable like `$CI_COMMIT_REF_SLUG`. Optionally, add a static
prefix to the environment's name, which [groups in the UI](#group-similar-environments) all
environments with the same prefix.
- `url`: Optional. Prefix the hostname with a related CI/CD variable like `$CI_ENVIRONMENT_SLUG`.
NOTE:
Some characters cannot be used in environment names. For more information about the
`environment` keywords, see the [`.gitlab-ci.yml` keyword reference](../yaml/index.md#environment).
In the following example, every time the `deploy_review_app` job runs the environment's name and
URL are defined using unique values.
```yaml ```yaml
deploy_review: deploy_review_app:
stage: deploy stage: deploy
script: script: make deploy
- echo "Deploy a review app"
environment: environment:
name: review/$CI_COMMIT_REF_SLUG name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_ENVIRONMENT_SLUG.example.com url: https://$CI_ENVIRONMENT_SLUG.example.com
rules: only:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - branches
when: never except:
- if: $CI_COMMIT_BRANCH - main
``` ```
In this example:
- The `name` is `review/$CI_COMMIT_REF_SLUG`. Because the [environment name](../yaml/index.md#environmentname)
can contain slashes (`/`), you can use this pattern to distinguish between dynamic and static environments.
- For the `url`, you could use `$CI_COMMIT_REF_SLUG`, but because this value
may contain a `/` or other characters that would not be valid in a domain name or URL,
use `$CI_ENVIRONMENT_SLUG` instead. The `$CI_ENVIRONMENT_SLUG` variable is guaranteed to be unique.
You do not have to use the same prefix or only slashes (`/`) in the dynamic environment name.
However, when you use this format, you can [group similar environments](#group-similar-environments).
NOTE:
Some variables cannot be used as environment names or URLs.
For more information about the `environment` keywords, see
[the `.gitlab-ci.yml` keyword reference](../yaml/index.md#environment).
## Deployment tier of environments ## Deployment tier of environments
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10.

View File

@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
This document describes the conventions used at GitLab for writing End-to-end (E2E) tests using the GitLab QA project. This document describes the conventions used at GitLab for writing End-to-end (E2E) tests using the GitLab QA project.
Please note that this guide is an extension of the primary [testing standards and style guidelines](../index.md). If this guide defines a rule that contradicts the primary guide, this guide takes precedence.
## `click_` versus `go_to_` ## `click_` versus `go_to_`
### When to use `click_`? ### When to use `click_`?

View File

@ -165,3 +165,9 @@ this setting. However, disabling the Container Registry disables all Container R
| Private project with Container Registry visibility <br/> set to **Everyone With Access** (UI) or `enabled` (API) | View Container Registry <br/> and pull images | No | No | Yes | | Private project with Container Registry visibility <br/> set to **Everyone With Access** (UI) or `enabled` (API) | View Container Registry <br/> and pull images | No | No | Yes |
| Private project with Container Registry visibility <br/> set to **Only Project Members** (UI) or `private` (API) | View Container Registry <br/> and pull images | No | No | Yes | | Private project with Container Registry visibility <br/> set to **Only Project Members** (UI) or `private` (API) | View Container Registry <br/> and pull images | No | No | Yes |
| Any project with Container Registry `disabled` | All operations on Container Registry | No | No | No | | Any project with Container Registry `disabled` | All operations on Container Registry | No | No | No |
## Supported image types
The Container Registry supports [Docker V2](https://docs.docker.com/registry/spec/manifest-v2-2/) and [Open Container Initiative (OCI)](https://github.com/opencontainers/image-spec/blob/main/spec.md) image formats.
OCI support means that you can host OCI-based image formats in the registry, such as [Helm 3+ chart packages](https://helm.sh/docs/topics/registries/). There is no distinction between image formats in the GitLab [API](../../../api/container_registry.md) and the UI. [Issue 38047](https://gitlab.com/gitlab-org/gitlab/-/issues/38047) addresses this distinction, starting with Helm.

View File

@ -608,7 +608,7 @@ module API
if file.file_storage? if file.file_storage?
present_disk_file!(file.path, file.filename) present_disk_file!(file.path, file.filename)
elsif supports_direct_download && file.class.direct_download_enabled? elsif supports_direct_download && file.class.direct_download_enabled?
return redirect(signed_head_url(file)) if head_request_on_aws_file?(file) return redirect(ObjectStorage::S3.signed_head_url(file)) if request.head? && file.fog_credentials[:provider] == 'AWS'
redirect(cdn_fronted_url(file)) redirect(cdn_fronted_url(file))
else else
@ -701,19 +701,6 @@ module API
private private
def head_request_on_aws_file?(file)
request.head? && file.fog_credentials[:provider] == 'AWS'
end
def signed_head_url(file)
fog_storage = ::Fog::Storage.new(file.fog_credentials)
fog_dir = fog_storage.directories.new(key: file.fog_directory)
fog_file = fog_dir.files.new(key: file.path)
expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration
fog_file.collection.head_url(fog_file.key, expire_at)
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def initial_current_user def initial_current_user
return @initial_current_user if defined?(@initial_current_user) return @initial_current_user if defined?(@initial_current_user)

View File

@ -50,6 +50,13 @@ module Gitlab
.map { |hash| payload_cache_key_for(hash) } .map { |hash| payload_cache_key_for(hash) }
.push(settings_cache_key) .push(settings_cache_key)
::Gitlab::AppLogger.info(
message: 'clear pages cache',
keys: keys,
type: @type,
id: @id
)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
Rails.cache.delete_multi(keys) Rails.cache.delete_multi(keys)
end end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'set'
namespace :gitlab do namespace :gitlab do
require 'set'
namespace :cleanup do namespace :cleanup do
desc "GitLab | Cleanup | Block users that have been removed in LDAP" desc "GitLab | Cleanup | Block users that have been removed in LDAP"
task block_removed_ldap_users: :gitlab_environment do task block_removed_ldap_users: :gitlab_environment do

View File

@ -426,6 +426,11 @@ msgid_plural "%d unresolved threads"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d version"
msgid_plural "%d versions"
msgstr[0] ""
msgstr[1] ""
msgid "%d vulnerability" msgid "%d vulnerability"
msgid_plural "%d vulnerabilities" msgid_plural "%d vulnerabilities"
msgstr[0] "" msgstr[0] ""
@ -36258,15 +36263,21 @@ msgstr ""
msgid "Requirement %{reference} has been updated" msgid "Requirement %{reference} has been updated"
msgstr "" msgstr ""
msgid "Requirement title cannot have more than %{limit} characters."
msgstr ""
msgid "Requirements" msgid "Requirements"
msgstr "" msgstr ""
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture." msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
msgstr "" msgstr ""
msgid "Requirement|Legacy requirement ID: %{legacyId}"
msgstr ""
msgid "Requirement|Legacy requirement IDs are being deprecated. Update your links to reference this item's new ID %{id}. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "Requirement|Requirements have become work items and the legacy requirement IDs are being deprecated. Update your links to reference this item's new ID %{id}. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "Requires %d approval from eligible users." msgid "Requires %d approval from eligible users."
msgid_plural "Requires %d approvals from eligible users." msgid_plural "Requires %d approvals from eligible users."
msgstr[0] "" msgstr[0] ""

View File

@ -5,6 +5,8 @@ module QA
module Project module Project
module Settings module Settings
class DefaultBranch < Page::Base class DefaultBranch < Page::Base
include ::QA::Page::Component::Dropdown
view 'app/views/projects/branch_defaults/_show.html.haml' do view 'app/views/projects/branch_defaults/_show.html.haml' do
element :save_changes_button element :save_changes_button
end end
@ -13,14 +15,9 @@ module QA
element :default_branch_dropdown element :default_branch_dropdown
end end
view 'app/assets/javascripts/ref/components/ref_selector.vue' do
element :ref_selector_searchbox
end
def set_default_branch(branch) def set_default_branch(branch)
find_element(:default_branch_dropdown, visible: false).click expand_select_list
find_element(:ref_selector_searchbox, visible: false).fill_in(with: branch) search_and_select(branch)
click_button branch
end end
def click_save_changes_button def click_save_changes_button

View File

@ -1,85 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_authoring do
context 'when job is configured to only run on merge_request_events' do
let(:mr_only_job_name) { 'mr_only_job' }
let(:non_mr_only_job_name) { 'non_mr_only_job' }
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-only-job'
end
end
let!(:runner) do
Resource::ProjectRunner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = [executor]
end
end
let!(:ci_file) do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
#{mr_only_job_name}:
tags: ["#{executor}"]
script: echo 'OK'
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
#{non_mr_only_job_name}:
tags: ["#{executor}"]
script: echo 'OK'
rules:
- if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
YAML
}
]
)
end
end
let(:merge_request) do
Resource::MergeRequest.fabricate_via_api! do |merge_request|
merge_request.project = project
merge_request.description = Faker::Lorem.sentence
merge_request.target_new_branch = false
merge_request.file_name = 'new.txt'
merge_request.file_content = Faker::Lorem.sentence
end
end
before do
Flow::Login.sign_in
# TODO: We should remove (wait) revisiting logic when
# https://gitlab.com/gitlab-org/gitlab/-/issues/385332 is resolved
Support::Waiter.wait_until do
merge_request.visit!
Page::MergeRequest::Show.perform(&:click_pipeline_link)
Page::Project::Pipeline::Show.perform(&:has_merge_request_badge_tag?)
end
end
after do
runner.remove_via_api!
end
it 'only runs the job configured to run on merge requests', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347662' do
Page::Project::Pipeline::Show.perform do |pipeline|
aggregate_failures do
expect(pipeline).to have_job(mr_only_job_name)
expect(pipeline).to have_no_job(non_mr_only_job_name)
end
end
end
end
end
end

View File

@ -58,10 +58,6 @@ FactoryBot.define do
end end
end end
trait :created_via_ui do
legacy_registered { false }
end
trait :without_projects do trait :without_projects do
# we use that to create invalid runner: # we use that to create invalid runner:
# the one without projects # the one without projects

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
include ListboxHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:sha) { project.commit.sha } let(:sha) { project.commit.sha }
@ -18,15 +20,13 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
it 'finds a tag in a list' do it 'finds a tag in a list' do
tag_name = 'v1.0.0' tag_name = 'v1.0.0'
toggle.click
filter_by(tag_name) filter_by(tag_name)
wait_for_requests wait_for_requests
expect(items_count(tag_name)).to be(1) expect(items_count(tag_name)).to be(1)
item(tag_name).click select_listbox_item tag_name
expect(toggle).to have_content tag_name expect(toggle).to have_content tag_name
end end
@ -34,22 +34,18 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
it 'finds a branch in a list' do it 'finds a branch in a list' do
branch_name = 'audio' branch_name = 'audio'
toggle.click
filter_by(branch_name) filter_by(branch_name)
wait_for_requests wait_for_requests
expect(items_count(branch_name)).to be(1) expect(items_count(branch_name)).to be(1)
item(branch_name).click select_listbox_item branch_name
expect(toggle).to have_content branch_name expect(toggle).to have_content branch_name
end end
it 'finds a commit in a list' do it 'finds a commit in a list' do
toggle.click
filter_by(sha) filter_by(sha)
wait_for_requests wait_for_requests
@ -58,21 +54,19 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
expect(items_count(sha_short)).to be(1) expect(items_count(sha_short)).to be(1)
item(sha_short).click select_listbox_item sha_short
expect(toggle).to have_content sha_short expect(toggle).to have_content sha_short
end end
it 'shows no results when there is no branch, tag or commit sha found' do it 'shows no results when there is no branch, tag or commit sha found' do
non_existing_ref = 'non_existing_branch_name' non_existing_ref = 'non_existing_branch_name'
toggle.click
filter_by(non_existing_ref) filter_by(non_existing_ref)
wait_for_requests wait_for_requests
expect(find('.gl-dropdown-contents')).not_to have_content(non_existing_ref) click_button 'master'
expect(toggle).not_to have_content(non_existing_ref)
end end
def item(ref_name) def item(ref_name)
@ -84,6 +78,7 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
end end
def filter_by(filter_text) def filter_by(filter_text)
fill_in _('Search by Git revision'), with: filter_text click_button 'master'
send_keys filter_text
end end
end end

View File

@ -4,6 +4,7 @@ require "spec_helper"
RSpec.describe "User browses files", :js, feature_category: :projects do RSpec.describe "User browses files", :js, feature_category: :projects do
include RepoHelpers include RepoHelpers
include ListboxHelpers
let(:fork_message) do let(:fork_message) do
"You're not allowed to make changes to this project directly. "\ "You're not allowed to make changes to this project directly. "\
@ -282,17 +283,13 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
expect(page).to have_content(".gitignore").and have_content("LICENSE") expect(page).to have_content(".gitignore").and have_content("LICENSE")
end end
it "shows files from a repository with apostroph in its name" do it "shows files from a repository with apostrophe in its name" do
ref_name = 'test' ref_name = 'fix'
find(ref_selector).click find(ref_selector).click
wait_for_requests wait_for_requests
page.within(ref_selector) do filter_by(ref_name)
fill_in 'Search by Git revision', with: ref_name
wait_for_requests
find('li', text: ref_name, match: :prefer_exact).click
end
expect(find(ref_selector)).to have_text(ref_name) expect(find(ref_selector)).to have_text(ref_name)
@ -307,11 +304,7 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
find(ref_selector).click find(ref_selector).click
wait_for_requests wait_for_requests
page.within(ref_selector) do filter_by(ref_name)
fill_in 'Search by Git revision', with: ref_name
wait_for_requests
find('li', text: ref_name, match: :prefer_exact).click
end
visit(project_tree_path(project, "fix/.testdir")) visit(project_tree_path(project, "fix/.testdir"))
@ -394,4 +387,12 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
end end
end end
end end
def filter_by(filter_text)
send_keys filter_text
wait_for_requests
select_listbox_item filter_text
end
end end

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'User find project file', feature_category: :projects do RSpec.describe 'User find project file', feature_category: :projects do
include ListboxHelpers
let(:user) { create :user } let(:user) { create :user }
let(:project) { create :project, :repository } let(:project) { create :project, :repository }
@ -22,7 +24,7 @@ RSpec.describe 'User find project file', feature_category: :projects do
end end
def ref_selector_dropdown def ref_selector_dropdown
find('.gl-dropdown-toggle > .gl-dropdown-button-text') find('.gl-button-text')
end end
it 'navigates to find file by shortcut', :js do it 'navigates to find file by shortcut', :js do
@ -99,7 +101,7 @@ RSpec.describe 'User find project file', feature_category: :projects do
fill_in _('Switch branch/tag'), with: ref fill_in _('Switch branch/tag'), with: ref
wait_for_requests wait_for_requests
find('.gl-dropdown-item', text: ref).click select_listbox_item(ref)
end end
expect(ref_selector_dropdown).to have_text(ref) expect(ref_selector_dropdown).to have_text(ref)
end end

View File

@ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
it 'HTML escapes branch name' do it 'HTML escapes branch name' do
expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>") expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
expect(page.find('.gl-dropdown-button-text')['innerHTML']).to eq(ERB::Util.html_escape(branch_name)) expect(page.find('.gl-new-dropdown-button-text')['innerHTML']).to include(ERB::Util.html_escape(branch_name))
end end
end end
@ -69,18 +69,18 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
visit charts_project_graph_path(project, 'master') visit charts_project_graph_path(project, 'master')
# Not a huge fan of using a HTML (CSS) selectors here as any change of them will cause a failed test # Not a huge fan of using a HTML (CSS) selectors here as any change of them will cause a failed test
ref_selector = find('.ref-selector .gl-dropdown-toggle') ref_selector = find('.ref-selector .gl-new-dropdown-toggle')
scroll_to(ref_selector) scroll_to(ref_selector)
ref_selector.click ref_selector.click
page.within '[data-testid="branches-section"]' do page.within '.gl-new-dropdown-contents' do
dropdown_branch_item = find('.gl-dropdown-item', text: 'add-pdf-file') dropdown_branch_item = find('li', text: 'add-pdf-file')
scroll_to(dropdown_branch_item) scroll_to(dropdown_branch_item)
dropdown_branch_item.click dropdown_branch_item.click
end end
scroll_to(find('.tree-ref-header'), align: :center) scroll_to(find('.tree-ref-header'), align: :center)
expect(page).to have_selector '.gl-dropdown-toggle', text: ref_name expect(page).to have_selector '.gl-new-dropdown-toggle', text: ref_name
page.within '.tree-ref-header' do page.within '.tree-ref-header' do
expect(page).to have_selector('h4', text: ref_name) expect(page).to have_selector('h4', text: ref_name)
end end

View File

@ -64,7 +64,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default ref' do it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do page.within('[data-testid="schedule-target-ref"]') do
expect(first('.gl-dropdown-button-text').text).to eq('master') expect(first('.gl-button-text').text).to eq('master')
end end
end end
end end
@ -77,7 +77,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default ref' do it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do page.within('[data-testid="schedule-target-ref"]') do
expect(first('.gl-dropdown-button-text').text).to eq('master') expect(first('.gl-button-text').text).to eq('master')
end end
end end
end end
@ -319,7 +319,6 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
end end
def select_target_branch def select_target_branch
find('[data-testid="schedule-target-ref"] .dropdown-toggle').click
click_button 'master' click_button 'master'
end end

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do
include ListboxHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
@ -20,10 +22,10 @@ RSpec.describe 'Projects > Settings > User changes default branch', feature_cate
wait_for_requests wait_for_requests
expect(page).to have_selector(dropdown_selector) expect(page).to have_selector(dropdown_selector)
find(dropdown_selector).click click_button 'master'
send_keys 'fix'
fill_in 'Search branch', with: 'fix' select_listbox_item 'fix'
click_button 'fix'
page.within '#branch-defaults-settings' do page.within '#branch-defaults-settings' do
click_button 'Save changes' click_button 'Save changes'

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects tree', :js, feature_category: :web_ide do RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
include WebIdeSpecHelpers include WebIdeSpecHelpers
include RepoHelpers include RepoHelpers
include ListboxHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
@ -160,17 +161,13 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
context 'ref switcher', :js do context 'ref switcher', :js do
it 'switches ref to branch' do it 'switches ref to branch' do
ref_selector = '.ref-selector' ref_selector = '.ref-selector'
ref_name = 'feature' ref_name = 'fix'
visit project_tree_path(project, 'master') visit project_tree_path(project, 'master')
find(ref_selector).click click_button 'master'
wait_for_requests send_keys ref_name
page.within(ref_selector) do select_listbox_item ref_name
fill_in 'Search by Git revision', with: ref_name
wait_for_requests
find('li', text: ref_name, match: :prefer_exact).click
end
expect(find(ref_selector)).to have_text(ref_name) expect(find(ref_selector)).to have_text(ref_name)
end end

View File

@ -3,6 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
include ListboxHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) } let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
@ -83,14 +86,10 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
expect(page).to have_selector('.results', text: expected_result) expect(page).to have_selector('.results', text: expected_result)
find('.ref-selector').click click_button 'master'
wait_for_requests wait_for_requests
page.within('.ref-selector') do select_listbox_item(ref_selector)
fill_in 'Search by Git revision', with: ref_selector
wait_for_requests
find('li', text: ref_selector, match: :prefer_exact).click
end
expect(page).to have_selector('.results', text: expected_result) expect(page).to have_selector('.results', text: expected_result)
@ -137,18 +136,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
end end
it 'search result changes when refs switched' do it 'search result changes when refs switched' do
ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack') expect(find('.results')).not_to have_content('path = gitlab-grack')
find('.ref-selector').click find('.ref-selector').click
wait_for_requests wait_for_requests
page.within('.ref-selector') do select_listbox_item('add-ipython-files')
fill_in _('Search by Git revision'), with: ref
wait_for_requests
find('li', text: ref).click
end
expect(page).to have_selector('.results', text: 'path = gitlab-grack') expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end end
@ -192,18 +185,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
end end
it 'search result changes when refs switched' do it 'search result changes when refs switched' do
ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack') expect(find('.results')).not_to have_content('path = gitlab-grack')
find('.ref-selector').click find('.ref-selector').click
wait_for_requests wait_for_requests
page.within('.ref-selector') do select_listbox_item('add-ipython-files')
fill_in _('Search by Git revision'), with: ref
wait_for_requests
find('li', text: ref).click
end
expect(page).to have_selector('.results', text: 'path = gitlab-grack') expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end end

View File

@ -34,7 +34,7 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within(ref_selector) do page.within(ref_selector) do
fill_in _('Search by Git revision'), with: ref_name fill_in _('Search by Git revision'), with: ref_name
wait_for_requests wait_for_requests
expect(find('.gl-dropdown-contents')).not_to have_content(ref_name) expect(find('.gl-new-dropdown-inner')).not_to have_content(ref_name)
end end
end end
@ -60,9 +60,9 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within ref_row do page.within ref_row do
ref_input = find('[name="ref"]', visible: false) ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master' expect(ref_input.value).to eq 'master'
expect(find('.gl-dropdown-button-text')).to have_content 'master' expect(find('.gl-button-text')).to have_content 'master'
find('.ref-selector').click find('.ref-selector').click
expect(find('.dropdown-menu')).to have_content 'test' expect(find('.gl-new-dropdown-inner')).to have_content 'test'
end end
end end
end end

View File

@ -438,7 +438,6 @@ describe('Ci variable modal', () => {
raw: true, raw: true,
}; };
describe('and FF is enabled', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
mountFn: mountExtended, mountFn: mountExtended,
@ -460,23 +459,6 @@ describe('Ci variable modal', () => {
}); });
}); });
describe('and FF is disabled', () => {
beforeEach(() => {
createComponent({
mountFn: mountExtended,
props: { selectedVariable: validRawMaskedVariable },
provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } },
});
});
it('should show an error with symbols', async () => {
await findMaskedVariableCheckbox().trigger('click');
expect(findModal().text()).toContain(maskError);
});
});
});
describe('when the mask state is invalid', () => { describe('when the mask state is invalid', () => {
beforeEach(async () => { beforeEach(async () => {
const [variable] = mockVariables; const [variable] = mockVariables;

View File

@ -1,8 +1,16 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import Tracking from '~/tracking';
import {
CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import { packageData } from '../../mock_data'; import { packageData } from '../../mock_data';
describe('PackageVersionsList', () => { describe('PackageVersionsList', () => {
@ -24,6 +32,7 @@ describe('PackageVersionsList', () => {
findRegistryList: () => wrapper.findComponent(RegistryList), findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub), findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
findListRow: () => wrapper.findAllComponents(VersionRow), findListRow: () => wrapper.findAllComponents(VersionRow),
findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
}; };
const mountComponent = (props) => { const mountComponent = (props) => {
wrapper = shallowMountExtended(PackageVersionsList, { wrapper = shallowMountExtended(PackageVersionsList, {
@ -35,6 +44,11 @@ describe('PackageVersionsList', () => {
}, },
stubs: { stubs: {
RegistryList, RegistryList,
DeleteModal: stubComponent(DeleteModal, {
methods: {
show: jest.fn(),
},
}),
}, },
slots: { slots: {
'empty-state': EmptySlotStub, 'empty-state': EmptySlotStub,
@ -144,4 +158,80 @@ describe('PackageVersionsList', () => {
expect(wrapper.emitted('next-page')).toHaveLength(1); expect(wrapper.emitted('next-page')).toHaveLength(1);
}); });
}); });
describe('when the user can bulk destroy versions', () => {
let eventSpy;
const { findDeletePackagesModal, findRegistryList } = uiElements;
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent({ canDestroy: true });
});
it('binds the right props', () => {
expect(uiElements.findRegistryList().props()).toMatchObject({
items: packageList,
pagination: {},
isLoading: false,
hiddenDelete: false,
title: '2 versions',
});
});
describe('upon deletion', () => {
beforeEach(() => {
findRegistryList().vm.$emit('delete', packageList);
});
it('passes itemsToBeDeleted to the modal', () => {
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList);
expect(wrapper.emitted('delete')).toBeUndefined();
});
it('requesting delete tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
undefined,
REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
expect.any(Object),
);
});
describe('when modal confirms', () => {
beforeEach(() => {
findDeletePackagesModal().vm.$emit('confirm');
});
it('emits delete event', () => {
expect(wrapper.emitted('delete')[0]).toEqual([packageList]);
});
it('tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
undefined,
DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
expect.any(Object),
);
});
});
it.each(['confirm', 'cancel'])(
'resets itemsToBeDeleted when modal emits %s',
async (event) => {
await findDeletePackagesModal().vm.$emit(event);
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
},
);
it('canceling delete tracks the right action', () => {
findDeletePackagesModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
undefined,
CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
expect.any(Object),
);
});
});
});
}); });

View File

@ -1,6 +1,7 @@
import { GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
@ -15,17 +16,20 @@ const packageVersion = packageVersions()[0];
describe('VersionRow', () => { describe('VersionRow', () => {
let wrapper; let wrapper;
const findListItem = () => wrapper.findComponent(ListItem);
const findLink = () => wrapper.findComponent(GlLink); const findLink = () => wrapper.findComponent(GlLink);
const findPackageTags = () => wrapper.findComponent(PackageTags); const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPublishMethod = () => wrapper.findComponent(PublishMethod); const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findPackageName = () => wrapper.findComponent(GlTruncate); const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon); const findWarningIcon = () => wrapper.findComponent(GlIcon);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
function createComponent(packageEntity = packageVersion) { function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, { wrapper = shallowMountExtended(VersionRow, {
propsData: { propsData: {
packageEntity, packageEntity,
selected,
}, },
stubs: { stubs: {
GlSprintf, GlSprintf,
@ -76,14 +80,48 @@ describe('VersionRow', () => {
expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt); expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt);
}); });
describe('left action template', () => {
it('does not render checkbox if not permitted', () => {
createComponent({ packageEntity: { ...packageVersion, canDestroy: false } });
expect(findBulkDeleteAction().exists()).toBe(false);
});
it('renders checkbox', () => {
createComponent();
expect(findBulkDeleteAction().exists()).toBe(true);
expect(findBulkDeleteAction().attributes('checked')).toBeUndefined();
});
it('emits select when checked', () => {
createComponent();
findBulkDeleteAction().vm.$emit('change');
expect(wrapper.emitted('select')).toHaveLength(1);
});
it('renders checkbox in selected state if selected', () => {
createComponent({
selected: true,
});
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
expect(findListItem().props('selected')).toBe(true);
});
});
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
packageEntity: {
...packageVersion, ...packageVersion,
status: PACKAGE_ERROR_STATUS, status: PACKAGE_ERROR_STATUS,
_links: { _links: {
webPath: null, webPath: null,
}, },
},
}); });
}); });
@ -109,11 +147,13 @@ describe('VersionRow', () => {
describe('disabled status', () => { describe('disabled status', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
packageEntity: {
...packageVersion, ...packageVersion,
status: 'something', status: 'something',
_links: { _links: {
webPath: null, webPath: null,
}, },
},
}); });
}); });

View File

@ -47,6 +47,7 @@ describe('packages_list_row', () => {
const findPublishMethod = () => wrapper.findComponent(PublishMethod); const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date'); const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip); const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate); const findPackageName = () => wrapper.findComponent(GlTruncate);
@ -212,6 +213,9 @@ describe('packages_list_row', () => {
}); });
expect(findBulkDeleteAction().attributes('checked')).toBe('true'); expect(findBulkDeleteAction().attributes('checked')).toBe('true');
expect(findListItem().props()).toMatchObject({
selected: true,
});
}); });
}); });

View File

@ -109,6 +109,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/243', id: 'gid://gitlab/Packages::Package/243',
name: '@gitlab-org/package-15', name: '@gitlab-org/package-15',
status: 'DEFAULT', status: 'DEFAULT',
canDestroy: true,
tags: { nodes: packageTags() }, tags: { nodes: packageTags() },
version: '1.0.1', version: '1.0.1',
...linksData, ...linksData,
@ -119,6 +120,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244', id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15', name: '@gitlab-org/package-15',
status: 'DEFAULT', status: 'DEFAULT',
canDestroy: true,
tags: { nodes: packageTags() }, tags: { nodes: packageTags() },
version: '1.0.2', version: '1.0.2',
...linksData, ...linksData,

View File

@ -128,6 +128,7 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge'); const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message'); const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow); const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
const findDeletePackages = () => wrapper.findComponent(DeletePackages); const findDeletePackages = () => wrapper.findComponent(DeletePackages);
afterEach(() => { afterEach(() => {
@ -267,7 +268,7 @@ describe('PackagesApp', () => {
await waitForPromises(); await waitForPromises();
findDeletePackages().vm.$emit('end'); findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith( expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true', 'projectListUrl?showSuccessDeleteAlert=true',
@ -281,7 +282,7 @@ describe('PackagesApp', () => {
await waitForPromises(); await waitForPromises();
findDeletePackages().vm.$emit('end'); findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith( expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true', 'groupListUrl?showSuccessDeleteAlert=true',
@ -600,9 +601,51 @@ describe('PackagesApp', () => {
await waitForPromises(); await waitForPromises();
expect(findVersionsList().props()).toMatchObject({ expect(findVersionsList().props()).toMatchObject({
canDestroy: true,
versions: expect.arrayContaining(versionNodes), versions: expect.arrayContaining(versionNodes),
}); });
}); });
describe('delete packages', () => {
it('exists and has the correct props', async () => {
createComponent();
await waitForPromises();
expect(findDeletePackages().props()).toMatchObject({
refetchQueries: [{ query: getPackageDetails, variables: {} }],
showSuccessAlert: true,
});
});
it('deletePackages is bound to package-versions-list delete event', async () => {
createComponent();
await waitForPromises();
findVersionsList().vm.$emit('delete', [{ id: 1 }]);
expect(findDeletePackages().emitted('start')).toEqual([[]]);
});
it('start and end event set loading correctly', async () => {
createComponent();
await waitForPromises();
findDeletePackages().vm.$emit('start');
await nextTick();
expect(findVersionsList().props('isLoading')).toBe(true);
findDeletePackages().vm.$emit('end');
await nextTick();
expect(findVersionsList().props('isLoading')).toBe(false);
});
});
}); });
describe('dependency links', () => { describe('dependency links', () => {

View File

@ -1,5 +1,4 @@
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
@ -8,13 +7,13 @@ import Vuex from 'vuex';
import commit from 'test_fixtures/api/commits/commit.json'; import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json'; import branches from 'test_fixtures/api/branches/branches.json';
import tags from 'test_fixtures/api/tags/tags.json'; import tags from 'test_fixtures/api/tags/tags.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { import {
HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND, HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_OK, HTTP_STATUS_OK,
} from '~/lib/utils/http_status'; } from '~/lib/utils/http_status';
import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue'; import RefSelector from '~/ref/components/ref_selector.vue';
import { import {
@ -42,7 +41,7 @@ describe('Ref selector component', () => {
let requestSpies; let requestSpies;
const createComponent = (mountOverrides = {}, propsData = {}) => { const createComponent = (mountOverrides = {}, propsData = {}) => {
wrapper = mount( wrapper = mountExtended(
RefSelector, RefSelector,
merge( merge(
{ {
@ -57,9 +56,6 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: selectedRef }); wrapper.setProps({ value: selectedRef });
}, },
}, },
stubs: {
GlSearchBoxByType: true,
},
store: createStore(), store: createStore(),
}, },
mountOverrides, mountOverrides,
@ -91,76 +87,63 @@ describe('Ref selector component', () => {
.reply((config) => commitApiCallSpy(config)); .reply((config) => commitApiCallSpy(config));
}); });
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
// //
// Finders // Finders
// //
const findButtonContent = () => wrapper.find('button'); const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findNoResults = () => wrapper.find('[data-testid="no-results"]'); const findButtonToggle = () => wrapper.findByTestId('base-dropdown-toggle');
const findNoResults = () => wrapper.findByTestId('listbox-no-results-text');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const findListBoxSection = (section) => {
const foundSections = wrapper
.findAll('[role="group"]')
.filter((ul) => ul.text().includes(section));
return foundSections.length > 0 ? foundSections.at(0) : foundSections;
};
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]'); const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]'); const findBranchesSection = () => findListBoxSection('Branches');
const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem); const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]'); const findTagsSection = () => findListBoxSection('Tags');
const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]'); const findCommitsSection = () => findListBoxSection('Commits');
const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field');
// //
// Expecters // Expecters
// //
const branchesSectionContainsErrorMessage = () => { const sectionContainsErrorMessage = (message) => {
const branchesSection = findBranchesSection(); const errorSection = findErrorListWrapper();
return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage); return errorSection ? errorSection.text().includes(message) : false;
};
const tagsSectionContainsErrorMessage = () => {
const tagsSection = findTagsSection();
return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
};
const commitsSectionContainsErrorMessage = () => {
const commitsSection = findCommitsSection();
return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
}; };
// //
// Convenience methods // Convenience methods
// //
const updateQuery = (newQuery) => { const updateQuery = (newQuery) => {
findSearchBox().vm.$emit('input', newQuery); findListbox().vm.$emit('search', newQuery);
}; };
const selectFirstBranch = async () => { const selectFirstBranch = async () => {
findFirstBranchDropdownItem().vm.$emit('click'); findListbox().vm.$emit('select', fixtures.branches[0].name);
await nextTick(); await nextTick();
}; };
const selectFirstTag = async () => { const selectFirstTag = async () => {
findFirstTagDropdownItem().vm.$emit('click'); findListbox().vm.$emit('select', fixtures.tags[0].name);
await nextTick(); await nextTick();
}; };
const selectFirstCommit = async () => { const selectFirstCommit = async () => {
findFirstCommitDropdownItem().vm.$emit('click'); findListbox().vm.$emit('select', fixtures.commit.id);
await nextTick(); await nextTick();
}; };
@ -195,7 +178,7 @@ describe('Ref selector component', () => {
}); });
describe('when name property is provided', () => { describe('when name property is provided', () => {
it('renders an forrm input hidden field', () => { it('renders an form input hidden field', () => {
const name = 'default_tag'; const name = 'default_tag';
createComponent({ propsData: { name } }); createComponent({ propsData: { name } });
@ -205,7 +188,7 @@ describe('Ref selector component', () => {
}); });
describe('when name property is not provided', () => { describe('when name property is not provided', () => {
it('renders an forrm input hidden field', () => { it('renders an form input hidden field', () => {
createComponent(); createComponent();
expect(findHiddenInputField().exists()).toBe(false); expect(findHiddenInputField().exists()).toBe(false);
@ -224,7 +207,7 @@ describe('Ref selector component', () => {
}); });
it('adds the provided ID to the GlDropdown instance', () => { it('adds the provided ID to the GlDropdown instance', () => {
expect(wrapper.findComponent(GlDropdown).attributes().id).toBe(id); expect(findListbox().attributes().id).toBe(id);
}); });
}); });
@ -238,7 +221,7 @@ describe('Ref selector component', () => {
}); });
it('renders the pre-selected ref name', () => { it('renders the pre-selected ref name', () => {
expect(findButtonContent().text()).toBe(preselectedRef); expect(findButtonToggle().text()).toBe(preselectedRef);
}); });
it('binds hidden input field to the pre-selected ref', () => { it('binds hidden input field to the pre-selected ref', () => {
@ -259,7 +242,7 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: updatedRef }); wrapper.setProps({ value: updatedRef });
await nextTick(); await nextTick();
expect(findButtonContent().text()).toBe(updatedRef); expect(findButtonToggle().text()).toBe(updatedRef);
}); });
}); });
@ -296,23 +279,6 @@ describe('Ref selector component', () => {
}); });
}); });
describe('when the Enter is pressed', () => {
beforeEach(() => {
createComponent();
return waitForRequests({ andClearMocks: true });
});
it('requeries the endpoints when Enter is pressed', () => {
findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
return waitForRequests().then(() => {
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
});
});
describe('when no results are found', () => { describe('when no results are found', () => {
beforeEach(() => { beforeEach(() => {
branchesApiCallSpy = jest branchesApiCallSpy = jest
@ -357,27 +323,10 @@ describe('Ref selector component', () => {
it('renders the branches section in the dropdown', () => { it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true); expect(findBranchesSection().exists()).toBe(true);
expect(findBranchesSection().props('shouldShowCheck')).toBe(true);
});
it('renders the "Branches" heading with a total number indicator', () => {
expect(
findBranchesSection().find('[data-testid="section-header"]').text(),
).toMatchInterpolatedText('Branches 123');
}); });
it("does not render an error message in the branches section's body", () => { it("does not render an error message in the branches section's body", () => {
expect(branchesSectionContainsErrorMessage()).toBe(false); expect(findErrorListWrapper().exists()).toBe(false);
});
it('renders each non-default branch as a selectable item', () => {
const dropdownItems = findBranchDropdownItems();
fixtures.branches.forEach((b, i) => {
if (!b.default) {
expect(dropdownItems.at(i).text()).toBe(b.name);
}
});
}); });
it('renders the default branch as a selectable item with a "default" badge', () => { it('renders the default branch as a selectable item with a "default" badge', () => {
@ -418,11 +367,11 @@ describe('Ref selector component', () => {
}); });
it('renders the branches section in the dropdown', () => { it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true); expect(findBranchesSection().exists()).toBe(false);
}); });
it("renders an error message in the branches section's body", () => { it("renders an error message in the branches section's body", () => {
expect(branchesSectionContainsErrorMessage()).toBe(true); expect(sectionContainsErrorMessage(DEFAULT_I18N.branchesErrorMessage)).toBe(true);
}); });
}); });
}); });
@ -437,25 +386,16 @@ describe('Ref selector component', () => {
it('renders the tags section in the dropdown', () => { it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true); expect(findTagsSection().exists()).toBe(true);
expect(findTagsSection().props('shouldShowCheck')).toBe(true);
}); });
it('renders the "Tags" heading with a total number indicator', () => { it('renders the "Tags" heading with a total number indicator', () => {
expect( expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
findTagsSection().find('[data-testid="section-header"]').text(), `Tags ${fixtures.tags.length}`,
).toMatchInterpolatedText('Tags 456'); );
}); });
it("does not render an error message in the tags section's body", () => { it("does not render an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).toBe(false); expect(findErrorListWrapper().exists()).toBe(false);
});
it('renders each tag as a selectable item', () => {
const dropdownItems = findTagDropdownItems();
fixtures.tags.forEach((t, i) => {
expect(dropdownItems.at(i).text()).toBe(t.name);
});
}); });
}); });
@ -485,11 +425,11 @@ describe('Ref selector component', () => {
}); });
it('renders the tags section in the dropdown', () => { it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true); expect(findTagsSection().exists()).toBe(false);
}); });
it("renders an error message in the tags section's body", () => { it("renders an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).toBe(true); expect(sectionContainsErrorMessage(DEFAULT_I18N.tagsErrorMessage)).toBe(true);
}); });
}); });
}); });
@ -509,19 +449,13 @@ describe('Ref selector component', () => {
}); });
it('renders the "Commits" heading with a total number indicator', () => { it('renders the "Commits" heading with a total number indicator', () => {
expect( expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
findCommitsSection().find('[data-testid="section-header"]').text(), `Commits 1`,
).toMatchInterpolatedText('Commits 1'); );
}); });
it("does not render an error message in the comits section's body", () => { it("does not render an error message in the commits section's body", () => {
expect(commitsSectionContainsErrorMessage()).toBe(false); expect(findErrorListWrapper().exists()).toBe(false);
});
it('renders each commit as a selectable item with the short SHA and commit title', () => {
const dropdownItems = findCommitDropdownItems();
expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
}); });
}); });
@ -553,11 +487,11 @@ describe('Ref selector component', () => {
}); });
it('renders the commits section in the dropdown', () => { it('renders the commits section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(true); expect(findCommitsSection().exists()).toBe(false);
}); });
it("renders an error message in the commits section's body", () => { it("renders an error message in the commits section's body", () => {
expect(commitsSectionContainsErrorMessage()).toBe(true); expect(sectionContainsErrorMessage(DEFAULT_I18N.commitsErrorMessage)).toBe(true);
}); });
}); });
}); });
@ -571,26 +505,13 @@ describe('Ref selector component', () => {
return waitForRequests(); return waitForRequests();
}); });
it('renders a checkmark by the selected item', async () => { describe('when a branch is selected', () => {
expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass(
'gl-visibility-hidden',
);
await selectFirstBranch();
expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass(
'gl-visibility-hidden',
);
});
describe('when a branch is seleceted', () => {
it("displays the branch name in the dropdown's button", async () => { it("displays the branch name in the dropdown's button", async () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstBranch(); await selectFirstBranch();
await nextTick(); expect(findButtonToggle().text()).toBe(fixtures.branches[0].name);
expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
}); });
it("updates the v-model binding with the branch's name", async () => { it("updates the v-model binding with the branch's name", async () => {
@ -604,12 +525,11 @@ describe('Ref selector component', () => {
describe('when a tag is seleceted', () => { describe('when a tag is seleceted', () => {
it("displays the tag name in the dropdown's button", async () => { it("displays the tag name in the dropdown's button", async () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstTag(); await selectFirstTag();
await nextTick(); expect(findButtonToggle().text()).toBe(fixtures.tags[0].name);
expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
}); });
it("updates the v-model binding with the tag's name", async () => { it("updates the v-model binding with the tag's name", async () => {
@ -623,12 +543,11 @@ describe('Ref selector component', () => {
describe('when a commit is selected', () => { describe('when a commit is selected', () => {
it("displays the full SHA in the dropdown's button", async () => { it("displays the full SHA in the dropdown's button", async () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstCommit(); await selectFirstCommit();
await nextTick(); expect(findButtonToggle().text()).toBe(fixtures.commit.id);
expect(findButtonContent().text()).toBe(fixtures.commit.id);
}); });
it("updates the v-model binding with the commit's full SHA", async () => { it("updates the v-model binding with the commit's full SHA", async () => {
@ -688,21 +607,6 @@ describe('Ref selector component', () => {
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1); expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
}); });
it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] } });
updateQuery('abcd1234');
await waitForRequests();
expect(findBranchesSection().exists()).toBe(true);
expect(findCommitsSection().exists()).toBe(true);
wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
await waitForRequests();
expect(findBranchesSection().exists()).toBe(false);
expect(findCommitsSection().exists()).toBe(true);
});
it.each` it.each`
enabledRefType | findVisibleSection | findHiddenSections enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]} ${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
@ -726,8 +630,7 @@ describe('Ref selector component', () => {
describe('validation state', () => { describe('validation state', () => {
const invalidClass = 'gl-inset-border-1-red-500!'; const invalidClass = 'gl-inset-border-1-red-500!';
const isInvalidClassApplied = () => const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass];
wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass];
describe('valid state', () => { describe('valid state', () => {
describe('when the state prop is not provided', () => { describe('when the state prop is not provided', () => {

View File

@ -0,0 +1,38 @@
import { formatListBoxItems, formatErrors } from '~/ref/format_refs';
import { DEFAULT_I18N } from '~/ref/constants';
import {
MOCK_BRANCHES,
MOCK_COMMITS,
MOCK_ERROR,
MOCK_TAGS,
FORMATTED_BRANCHES,
FORMATTED_TAGS,
FORMATTED_COMMITS,
} from './mock_data';
describe('formatListBoxItems', () => {
it.each`
branches | tags | commits | expectedResult
${MOCK_BRANCHES} | ${MOCK_TAGS} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_TAGS, FORMATTED_COMMITS]}
${MOCK_BRANCHES} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_COMMITS]}
${[]} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
${undefined} | ${undefined} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
${MOCK_BRANCHES} | ${undefined} | ${null} | ${[FORMATTED_BRANCHES]}
`('should correctly format listbox items', ({ branches, tags, commits, expectedResult }) => {
expect(formatListBoxItems(branches, tags, commits)).toEqual(expectedResult);
});
});
describe('formatErrors', () => {
const { branchesErrorMessage, tagsErrorMessage, commitsErrorMessage } = DEFAULT_I18N;
it.each`
branches | tags | commits | expectedResult
${MOCK_ERROR} | ${MOCK_ERROR} | ${MOCK_ERROR} | ${[branchesErrorMessage, tagsErrorMessage, commitsErrorMessage]}
${MOCK_ERROR} | ${[]} | ${MOCK_ERROR} | ${[branchesErrorMessage, commitsErrorMessage]}
${[]} | ${[]} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
${undefined} | ${undefined} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
${MOCK_ERROR} | ${undefined} | ${null} | ${[branchesErrorMessage]}
`('should correctly format listbox errors', ({ branches, tags, commits, expectedResult }) => {
expect(formatErrors(branches, tags, commits)).toEqual(expectedResult);
});
});

View File

@ -0,0 +1,87 @@
export const MOCK_BRANCHES = [
{
default: true,
name: 'main',
value: undefined,
},
{
default: false,
name: 'test1',
value: undefined,
},
{
default: false,
name: 'test2',
value: undefined,
},
];
export const MOCK_TAGS = [
{
name: 'test_tag',
value: undefined,
},
{
name: 'test_tag2',
value: undefined,
},
];
export const MOCK_COMMITS = [
{
name: 'test_commit',
value: undefined,
},
];
export const FORMATTED_BRANCHES = {
text: 'Branches',
options: [
{
default: true,
text: 'main',
value: 'main',
},
{
default: false,
text: 'test1',
value: 'test1',
},
{
default: false,
text: 'test2',
value: 'test2',
},
],
};
export const FORMATTED_TAGS = {
text: 'Tags',
options: [
{
text: 'test_tag',
value: 'test_tag',
default: undefined,
},
{
text: 'test_tag2',
value: 'test_tag2',
default: undefined,
},
],
};
export const FORMATTED_COMMITS = {
text: 'Commits',
options: [
{
text: 'test_commit',
value: 'test_commit',
default: undefined,
},
],
};
export const MOCK_ERROR = {
error: new Error('test_error'),
};

View File

@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Chunk component rendering isHighlighted is true renders line numbers 1`] = `
<div
class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
data-testid="line-numbers"
>
<a
class="gl-user-select-none gl-shadow-none! file-line-blame"
href="some/blame/path.js#L71"
/>
<a
class="gl-user-select-none gl-shadow-none! file-line-num"
data-line-number="71"
href="#L71"
id="L71"
>
71
</a>
</div>
`;

View File

@ -0,0 +1,87 @@
import { nextTick } from 'vue';
import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { CHUNK_1, CHUNK_2 } from '../mock_data';
describe('Chunk component', () => {
let wrapper;
let idleCallbackSpy;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
propsData: { ...CHUNK_1, ...props },
provide: { glFeatures: { fileLineBlame: true } },
});
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
createComponent();
});
afterEach(() => wrapper.destroy());
describe('Intersection observer', () => {
it('renders an Intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
it('renders highlighted content if appear event is emitted', async () => {
createComponent({ chunkIndex: 1, isHighlighted: false });
findIntersectionObserver().vm.$emit('appear');
await nextTick();
expect(findContent().exists()).toBe(true);
});
});
describe('rendering', () => {
it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
jest.clearAllMocks();
expect(window.requestIdleCallback).not.toHaveBeenCalled();
expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
it('does not render content if browser is not in idle state', () => {
idleCallbackSpy.mockRestore();
createComponent({ chunkIndex: 1, ...CHUNK_2 });
expect(findLineNumbers()).toHaveLength(0);
expect(findContent().exists()).toBe(false);
});
describe('isHighlighted is false', () => {
beforeEach(() => createComponent(CHUNK_2));
it('does not render line numbers', () => {
expect(findLineNumbers()).toHaveLength(0);
});
it('renders raw content', () => {
expect(findContent().text()).toBe(CHUNK_2.rawContent);
});
});
describe('isHighlighted is true', () => {
beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
it('renders line numbers', () => {
expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
// Opted for a snapshot test here since the output is simple and verifies native HTML elements
expect(findLineNumbers().at(0).element).toMatchSnapshot();
});
it('renders highlighted content', () => {
expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
});
});
});
});

View File

@ -0,0 +1,24 @@
const path = 'some/path.js';
const blamePath = 'some/blame/path.js';
export const LANGUAGE_MOCK = 'docker';
export const BLOB_DATA_MOCK = { language: LANGUAGE_MOCK, path, blamePath };
export const CHUNK_1 = {
isHighlighted: true,
rawContent: 'chunk 1 raw',
highlightedContent: 'chunk 1 highlighted',
totalLines: 70,
startingFrom: 0,
blamePath,
};
export const CHUNK_2 = {
isHighlighted: false,
rawContent: 'chunk 2 raw',
highlightedContent: 'chunk 2 highlighted',
totalLines: 40,
startingFrom: 70,
blamePath,
};

View File

@ -0,0 +1,47 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
const createComponent = () => {
wrapper = shallowMountExtended(SourceViewer, {
propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
});
};
const findChunks = () => wrapper.findAllComponents(Chunk);
beforeEach(() => {
jest.spyOn(Tracking, 'event');
return createComponent();
});
afterEach(() => wrapper.destroy());
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
});
it('adds blob links tracking', () => {
expect(addBlobLinksTracking).toHaveBeenCalled();
});
});
describe('rendering', () => {
it('renders a Chunk component for each chunk', () => {
expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
});
});
});

View File

@ -821,7 +821,7 @@ RSpec.describe API::Helpers, feature_category: :not_owned do
it 'redirects to a CDN-fronted URL' do it 'redirects to a CDN-fronted URL' do
expect(helper).to receive(:redirect) expect(helper).to receive(:redirect)
expect(helper).to receive(:signed_head_url).and_call_original expect(ObjectStorage::S3).to receive(:signed_head_url).and_call_original
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original
subject subject

View File

@ -13,15 +13,23 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
end end
it 'clears the cache' do it 'clears the cache' do
cached_keys = [
"pages_domain_for_#{type}_1_settings-hash",
"pages_domain_for_#{type}_1"
]
expect(::Gitlab::AppLogger)
.to receive(:info)
.with(
message: 'clear pages cache',
keys: cached_keys,
type: type,
id: 1
)
expect(Rails.cache) expect(Rails.cache)
.to receive(:delete_multi) .to receive(:delete_multi)
.with( .with(cached_keys)
array_including(
[
"pages_domain_for_#{type}_1",
"pages_domain_for_#{type}_1_settings-hash"
]
))
subject.clear_cache subject.clear_cache
end end
@ -31,13 +39,13 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
describe '.for_namespace' do describe '.for_namespace' do
subject(:cache_control) { described_class.for_namespace(1) } subject(:cache_control) { described_class.for_namespace(1) }
it_behaves_like 'cache_control', 'namespace' it_behaves_like 'cache_control', :namespace
end end
describe '.for_domain' do describe '.for_domain' do
subject(:cache_control) { described_class.for_domain(1) } subject(:cache_control) { described_class.for_domain(1) }
it_behaves_like 'cache_control', 'domain' it_behaves_like 'cache_control', :domain
end end
describe '#cache_key' do describe '#cache_key' do

View File

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::Runner, feature_category: :runner do RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
include StubGitlabCalls include StubGitlabCalls
it_behaves_like 'having unique enum values' it_behaves_like 'having unique enum values'
@ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) } it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) } it { is_expected.to validate_presence_of(:runner_type) }
it { is_expected.to validate_presence_of(:registration_type) }
context 'when runner is not allowed to pick untagged jobs' do context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do context 'when runner does not have tags' do
@ -1748,7 +1749,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end end
context 'when creating new runner via UI' do context 'when creating new runner via UI' do
let(:runner) { create(:ci_runner, :created_via_ui) } let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) } specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) } it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
@ -1765,7 +1766,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end end
context 'when runner is created via UI' do context 'when runner is created via UI' do
let(:runner) { create(:ci_runner, :created_via_ui) } let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
it { is_expected.to start_with('glrt-') } it { is_expected.to start_with('glrt-') }
end end
@ -1993,20 +1994,4 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end end
end end
end end
describe '#created_via_ui?' do
subject(:created_via_ui) { runner.created_via_ui? }
context 'when runner registered from command line' do
let(:runner) { create(:ci_runner) }
it { is_expected.to eq false }
end
context 'when runner created via UI' do
let(:runner) { create(:ci_runner, :created_via_ui) }
it { is_expected.to eq true }
end
end
end end

View File

@ -525,7 +525,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:creator) { create(:user) } let_it_be(:creator) { create(:user) }
let(:created_at) { Time.current } let(:created_at) { Time.current }
let(:token_prefix) { '' } let(:token_prefix) { registration_type == :authenticated_user ? 'glrt-' : '' }
let(:registration_type) {}
let(:query) do let(:query) do
%( %(
query { query {
@ -539,7 +540,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:runner) do let(:runner) do
create(:ci_runner, :group, create(:ci_runner, :group,
groups: [group], creator: creator, created_at: created_at, token: "#{token_prefix}abc123") groups: [group], creator: creator, created_at: created_at,
registration_type: registration_type, token: "#{token_prefix}abc123")
end end
before_all do before_all do
@ -570,7 +572,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:user) { creator } let(:user) { creator }
context 'with runner created in UI' do context 'with runner created in UI' do
let(:token_prefix) { ::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX } let(:registration_type) { :authenticated_user }
context 'with runner created in last 3 hours' do context 'with runner created in last 3 hours' do
let(:created_at) { (3.hours - 1.second).ago } let(:created_at) { (3.hours - 1.second).ago }
@ -600,7 +602,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end end
context 'with runner registered from command line' do context 'with runner registered from command line' do
let(:token_prefix) { '' } let(:registration_type) { :registration_token }
context 'with runner created in last 3 hours' do context 'with runner created in last 3 hours' do
let(:created_at) { (3.hours - 1.second).ago } let(:created_at) { (3.hours - 1.second).ago }
@ -614,7 +616,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:user) { create(:admin) } let(:user) { create(:admin) }
context 'with runner created in UI' do context 'with runner created in UI' do
let(:token_prefix) { ::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX } let(:registration_type) { :authenticated_user }
it_behaves_like 'a protected ephemeral_authentication_token' it_behaves_like 'a protected ephemeral_authentication_token'
end end

View File

@ -125,6 +125,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url)
subject subject
expect(response).to have_gitlab_http_status(:redirect)
end end
end end

View File

@ -131,7 +131,7 @@ RSpec.describe AutoMergeService do
subject subject
end end
context 'when the head piipeline succeeded' do context 'when the head pipeline succeeded' do
let(:pipeline_status) { :success } let(:pipeline_status) { :success }
it 'returns failed' do it 'returns failed' do

View File

@ -22,15 +22,10 @@ module Spec
end end
def select_branch(branch_name) def select_branch(branch_name)
ref_selector = '.ref-selector'
find(ref_selector).click
wait_for_requests wait_for_requests
page.within(ref_selector) do click_button branch_name
fill_in _('Search by Git revision'), with: branch_name send_keys branch_name
wait_for_requests
find('li', text: branch_name, match: :prefer_exact).click
end
end end
end end
end end

View File

@ -15,17 +15,18 @@ module Spec
module Helpers module Helpers
module Features module Features
module ReleasesHelpers module ReleasesHelpers
include ListboxHelpers
def select_new_tag_name(tag_name) def select_new_tag_name(tag_name)
page.within '[data-testid="tag-name-field"]' do page.within '[data-testid="tag-name-field"]' do
find('button').click find('button').click
wait_for_all_requests wait_for_all_requests
find('input[aria-label="Search or create tag"]').set(tag_name) find('input[aria-label="Search or create tag"]').set(tag_name)
wait_for_all_requests wait_for_all_requests
click_button("Create tag #{tag_name}") click_button("Create tag #{tag_name}")
click_button tag_name
end end
end end
@ -39,7 +40,7 @@ module Spec
wait_for_all_requests wait_for_all_requests
click_button(branch_name.to_s) select_listbox_item(branch_name.to_s, exact_text: true)
end end
end end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ObjectStorage::S3, feature_category: :source_code_management do
describe '.signed_head_url' do
subject { described_class.signed_head_url(package_file.file) }
let(:package_file) { create(:package_file) }
context 'when the provider is AWS' do
before do
stub_lfs_object_storage(config: Gitlab.config.lfs.object_store.merge(
connection: {
provider: 'AWS',
aws_access_key_id: 'test',
aws_secret_access_key: 'test'
}
))
end
it 'generates a signed url' do
expect_next_instance_of(Fog::AWS::Storage::Files) do |instance|
expect(instance).to receive(:head_url).and_return(a_valid_url)
end
subject
end
it 'delegates to Fog::AWS::Storage::Files#head_url' do
expect_next_instance_of(Fog::AWS::Storage::Files) do |instance|
expect(instance).to receive(:head_url).and_return('stubbed_url')
end
expect(subject).to eq('stubbed_url')
end
end
end
end