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
Exclude:
- 'lib/tasks/gitlab/assets.rake'
- 'lib/tasks/gitlab/cleanup.rake'
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
- 'lib/tasks/gitlab/docs/redirect.rake'
- 'lib/tasks/gitlab/graphql.rake'

View File

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

View File

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

View File

@ -1,15 +1,30 @@
<script>
import { n__ } from '~/locale';
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 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 {
components: {
DeleteModal,
VersionRow,
PackagesListLoader,
RegistryList,
},
mixins: [Tracking.mixin()],
props: {
canDestroy: {
type: Boolean,
required: false,
default: false,
},
versions: {
type: Array,
required: true,
@ -25,11 +40,35 @@ export default {
default: false,
},
},
data() {
return {
itemsToBeDeleted: [],
};
},
computed: {
listTitle() {
return n__('%d version', '%d versions', this.versions.length);
},
isListEmpty() {
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>
<template>
@ -40,17 +79,34 @@ export default {
<slot v-else-if="isListEmpty" name="empty-state"></slot>
<div v-else>
<registry-list
:hidden-delete="true"
:hidden-delete="!canDestroy"
:is-loading="isLoading"
:items="versions"
:pagination="pageInfo"
:title="listTitle"
@delete="setItemsToBeDeleted"
@prev-page="$emit('prev-page')"
@next-page="$emit('next-page')"
>
<template #default="{ item }">
<version-row :package-entity="item" />
<template #default="{ first, item, isSelected, selectItem }">
<!-- `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>
</registry-list>
<delete-modal
ref="deletePackagesModal"
:items-to-be-deleted="itemsToBeDeleted"
@confirm="deleteItemsConfirmation"
@cancel="deleteItemsCanceled"
/>
</div>
</div>
</template>

View File

@ -1,5 +1,12 @@
<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 PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
@ -15,6 +22,7 @@ import {
export default {
name: 'PackageVersionRow',
components: {
GlFormCheckbox,
GlIcon,
GlLink,
GlSprintf,
@ -32,6 +40,11 @@ export default {
type: Object,
required: true,
},
selected: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
containsWebPathLink() {
@ -53,7 +66,15 @@ export default {
</script>
<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>
<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">

View File

@ -100,7 +100,7 @@ export default {
</script>
<template>
<list-item data-testid="package-row" v-bind="$attrs">
<list-item data-testid="package-row" :selected="selected" v-bind="$attrs">
<template #left-action>
<gl-form-checkbox
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 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__(
'PackageRegistry|Something went wrong while deleting packages.',
);

View File

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

View File

@ -95,6 +95,7 @@ export default {
deletePackageModalContent: DELETE_MODAL_CONTENT,
filesToDelete: [],
mutationLoading: false,
versionsMutationLoading: false,
packageEntity: {},
};
},
@ -146,6 +147,9 @@ export default {
isLoading() {
return this.$apollo.queries.packageEntity.loading;
},
isVersionsLoading() {
return this.isLoading || this.versionsMutationLoading;
},
packageFilesLoading() {
return this.isLoading || this.mutationLoading;
},
@ -157,9 +161,6 @@ export default {
category: packageTypeToTrackCategory(this.packageType),
};
},
hasVersions() {
return this.packageEntity.versions?.nodes?.length > 0;
},
versionPageInfo() {
return this.packageEntity?.versions?.pageInfo ?? {};
},
@ -181,6 +182,14 @@ export default {
PACKAGE_TYPE_PYPI,
].includes(this.packageType);
},
refetchQueriesData() {
return [
{
query: getPackageDetails,
variables: this.queryVariables,
},
];
},
},
methods: {
formatSize(size) {
@ -206,12 +215,7 @@ export default {
ids,
},
awaitRefetchQueries: true,
refetchQueries: [
{
query: getPackageDetails,
variables: this.queryVariables,
},
],
refetchQueries: this.refetchQueriesData,
});
if (data?.destroyPackageFiles?.errors[0]) {
throw data.destroyPackageFiles.errors[0];
@ -403,19 +407,30 @@ export default {
}}</gl-badge>
</template>
<package-versions-list
:is-loading="isLoading"
:page-info="versionPageInfo"
:versions="packageEntity.versions.nodes"
@prev-page="fetchPreviousVersionsPage"
@next-page="fetchNextVersionsPage"
<delete-packages
:refetch-queries="refetchQueriesData"
show-success-alert
@start="versionsMutationLoading = true"
@end="versionsMutationLoading = false"
>
<template #empty-state>
<p class="gl-mt-3" data-testid="no-versions-message">
{{ s__('PackageRegistry|There are no other versions of this package.') }}
</p>
<template #default="{ deletePackages }">
<package-versions-list
:can-destroy="packageEntity.canDestroy"
:is-loading="isVersionsLoading"
:page-info="versionPageInfo"
:versions="packageEntity.versions.nodes"
@delete="deletePackages"
@prev-page="fetchPreviousVersionsPage"
@next-page="fetchNextVersionsPage"
>
<template #empty-state>
<p class="gl-mt-3" data-testid="no-versions-message">
{{ s__('PackageRegistry|There are no other versions of this package.') }}
</p>
</template>
</package-versions-list>
</template>
</package-versions-list>
</delete-packages>
</gl-tab>
</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>
import {
GlDropdown,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
GlLoadingIcon,
} from '@gitlab/ui';
import { GlBadge, GlIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce, isArray } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { sprintf } from '~/locale';
import {
ALL_REF_TYPES,
SEARCH_DEBOUNCE_MS,
@ -15,21 +10,16 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
BRANCH_REF_TYPE,
TAG_REF_TYPE,
} from '../constants';
import createStore from '../stores';
import RefResultsSection from './ref_results_section.vue';
import { formatListBoxItems, formatErrors } from '../format_refs';
export default {
name: 'RefSelector',
components: {
GlDropdown,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
GlLoadingIcon,
RefResultsSection,
GlBadge,
GlIcon,
GlCollapsibleListbox,
},
inheritAttrs: false,
props: {
@ -87,7 +77,6 @@ export default {
required: false,
default: '',
},
toggleButtonClass: {
type: [String, Object, Array],
required: false,
@ -112,29 +101,17 @@ export default {
...this.translations,
};
},
showBranchesSection() {
return (
this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
);
listBoxItems() {
return formatListBoxItems(this.branches, this.tags, this.commits);
},
showTagsSection() {
return (
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
);
branches() {
return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : [];
},
showCommitsSection() {
return (
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
);
tags() {
return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : [];
},
showNoResults() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
},
showSectionHeaders() {
return this.enabledRefTypes.length > 1;
commits() {
return this.enabledRefTypes.includes(REF_TYPE_COMMITS) ? this.matches.commits.list : [];
},
extendedToggleButtonClass() {
const classes = [
@ -142,7 +119,6 @@ export default {
'gl-inset-border-1-red-500!': !this.state,
'gl-font-monospace': Boolean(this.selectedRef),
},
'gl-max-w-26',
];
if (Array.isArray(this.toggleButtonClass)) {
@ -160,6 +136,9 @@ export default {
query: this.lastQuery,
};
},
errors() {
return formatErrors(this.matches.branches, this.matches.tags, this.matches.commits);
},
selectedRefForDisplay() {
if (this.useSymbolicRefNames && this.selectedRef) {
return this.selectedRef.replace(/^refs\/(tags|heads)\//, '');
@ -170,11 +149,12 @@ export default {
buttonText() {
return this.selectedRefForDisplay || this.i18n.noRefSelected;
},
isTagRefType() {
return this.refType === TAG_REF_TYPE;
},
isBranchRefType() {
return this.refType === BRANCH_REF_TYPE;
noResultsMessage() {
return this.lastQuery
? sprintf(this.i18n.noResultsWithQuery, {
query: this.lastQuery,
})
: this.i18n.noResults;
},
},
watch: {
@ -202,9 +182,7 @@ export default {
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue.
this.debouncedSearch = debounce(function search() {
this.search();
}, SEARCH_DEBOUNCE_MS);
this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
@ -231,14 +209,8 @@ export default {
'setSelectedRef',
]),
...mapActions({ storeSearch: 'search' }),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
onSearchBoxEnter() {
this.debouncedSearch.cancel();
this.search();
},
onSearchBoxInput() {
onSearchBoxInput(searchQuery = '') {
this.query = searchQuery?.trim();
this.debouncedSearch();
},
selectRef(ref) {
@ -248,104 +220,55 @@ export default {
search() {
this.storeSearch(this.query);
},
totalCountText(count) {
return count > 999 ? this.i18n.totalCountLabel : `${count}`;
},
},
};
</script>
<template>
<div>
<gl-dropdown
<gl-collapsible-listbox
class="ref-selector gl-w-full"
block
searchable
:selected="selectedRef"
:header-text="i18n.dropdownHeader"
:items="listBoxItems"
:no-results-text="noResultsMessage"
:searching="isLoading"
:search-placeholder="i18n.searchPlaceholder"
:toggle-class="extendedToggleButtonClass"
:text="buttonText"
class="ref-selector"
:toggle-text="buttonText"
v-bind="$attrs"
v-on="$listeners"
@shown="focusSearchBox"
@hidden="$emit('hide')"
@search="onSearchBoxInput"
@select="selectRef"
>
<template #header>
<gl-search-box-by-type
ref="searchBox"
v-model.trim="query"
:placeholder="i18n.searchPlaceholder"
autocomplete="off"
data-qa-selector="ref_selector_searchbox"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"
/>
<template #group-label="{ group }">
{{ group.text }} <gl-badge size="sm">{{ totalCountText(group.options.length) }}</gl-badge>
</template>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
<div
v-else-if="showNoResults"
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>
</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 #list-item="{ item }">
{{ item.text }}
<gl-badge v-if="item.default" size="sm" variant="info">{{
i18n.defaultLabelText
}}</gl-badge>
</template>
<template #footer>
<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>
</gl-dropdown>
</gl-collapsible-listbox>
<input
v-if="name"
data-testid="selected-ref-form-field"

View File

@ -1,5 +1,5 @@
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_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 DEFAULT_I18N = Object.freeze({
defaultLabelText: __('default'),
dropdownHeader: __('Select Git revision'),
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
@ -24,4 +25,5 @@ export const DEFAULT_I18N = Object.freeze({
tags: __('Tags'),
commits: __('Commits'),
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"
:task-list-update-path="taskListUpdatePath"
/>
<slot name="secondary-content"></slot>
<small v-if="isUpdated" class="edited-text gl-font-sm!">
{{ __('Edited') }}
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />

View File

@ -142,7 +142,7 @@ module Types
end
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 if runner.runner_machines.any?

View File

@ -30,6 +30,11 @@ module Ci
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
CREATED_RUNNER_TOKEN_PREFIX = 'glrt-'
@ -184,6 +189,7 @@ module Ci
validate :tag_constraints
validates :access_level, presence: true
validates :runner_type, presence: true
validates :registration_type, presence: true
validate :no_projects, unless: :project_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
attr_writer :legacy_registered
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
error_message: 'Maximum job timeout has a value which could not be accepted'
@ -297,13 +301,6 @@ module Ci
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)
if instance_type?
raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
@ -389,7 +386,7 @@ module Ci
def short_sha
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]
end
@ -493,15 +490,11 @@ module Ci
override :format_token
def format_token(token)
return token if @legacy_registered
return token if registration_token_registration_type?
"#{CREATED_RUNNER_TOKEN_PREFIX}#{token}"
end
def created_via_ui?
token.start_with?(CREATED_RUNNER_TOKEN_PREFIX)
end
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
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
.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?
#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 } }
= 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?}" }

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388986
milestone: '15.9'
type: development
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.
NOTE:
When measuring `TrackedEvents`, you must use `TrackedEvents.*` for `dimensions` and `timeDimensions`. The same rule applies when measuring `Sessions`.
#### Tracked events example
```json
{
"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
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,
"lfs_enabled": true,
"creator_id": 1,
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"open_issues_count": 0,
"ci_default_git_depth": 20,
"ci_forward_deployment_enabled": true,
@ -381,6 +384,10 @@ GET /users/:user_id/projects
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"namespace": {
"id": 3,
"name": "Diaspora",
@ -482,6 +489,10 @@ GET /users/:user_id/projects
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"namespace": {
"id": 4,
"name": "Brightbox",
@ -898,6 +909,8 @@ GET /projects/:id
"avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg",
"web_url": "http://localhost:3000/groups/diaspora"
},
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"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` | 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: RFC.

View File

@ -124,39 +124,41 @@ deploy_staging:
### Create a dynamic environment
To create a dynamic name and URL for an environment, you can use
[predefined CI/CD variables](../variables/predefined_variables.md). For example:
To create a dynamic environment, you use [CI/CD variables](../variables/index.md) that are unique to each pipeline.
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
deploy_review:
deploy_review_app:
stage: deploy
script:
- echo "Deploy a review app"
script: make deploy
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_ENVIRONMENT_SLUG.example.com
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: never
- if: $CI_COMMIT_BRANCH
only:
- branches
except:
- 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
> [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.
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_`
### 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 **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 |
## 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?
present_disk_file!(file.path, file.filename)
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))
else
@ -701,19 +701,6 @@ module API
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
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)

View File

@ -47,8 +47,15 @@ module Gitlab
# cached settings hash to build the payload cache key to be invalidated.
def clear_cache
keys = cached_settings_hashes
.map { |hash| payload_cache_key_for(hash) }
.push(settings_cache_key)
.map { |hash| payload_cache_key_for(hash) }
.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
Rails.cache.delete_multi(keys)

View File

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

View File

@ -426,6 +426,11 @@ msgid_plural "%d unresolved threads"
msgstr[0] ""
msgstr[1] ""
msgid "%d version"
msgid_plural "%d versions"
msgstr[0] ""
msgstr[1] ""
msgid "%d vulnerability"
msgid_plural "%d vulnerabilities"
msgstr[0] ""
@ -36258,15 +36263,21 @@ msgstr ""
msgid "Requirement %{reference} has been updated"
msgstr ""
msgid "Requirement title cannot have more than %{limit} characters."
msgstr ""
msgid "Requirements"
msgstr ""
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
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_plural "Requires %d approvals from eligible users."
msgstr[0] ""

View File

@ -5,6 +5,8 @@ module QA
module Project
module Settings
class DefaultBranch < Page::Base
include ::QA::Page::Component::Dropdown
view 'app/views/projects/branch_defaults/_show.html.haml' do
element :save_changes_button
end
@ -13,14 +15,9 @@ module QA
element :default_branch_dropdown
end
view 'app/assets/javascripts/ref/components/ref_selector.vue' do
element :ref_selector_searchbox
end
def set_default_branch(branch)
find_element(:default_branch_dropdown, visible: false).click
find_element(:ref_selector_searchbox, visible: false).fill_in(with: branch)
click_button branch
expand_select_list
search_and_select(branch)
end
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
trait :created_via_ui do
legacy_registered { false }
end
trait :without_projects do
# we use that to create invalid runner:
# the one without projects

View File

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

View File

@ -4,6 +4,7 @@ require "spec_helper"
RSpec.describe "User browses files", :js, feature_category: :projects do
include RepoHelpers
include ListboxHelpers
let(:fork_message) do
"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")
end
it "shows files from a repository with apostroph in its name" do
ref_name = 'test'
it "shows files from a repository with apostrophe in its name" do
ref_name = 'fix'
find(ref_selector).click
wait_for_requests
page.within(ref_selector) do
fill_in 'Search by Git revision', with: ref_name
wait_for_requests
find('li', text: ref_name, match: :prefer_exact).click
end
filter_by(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
wait_for_requests
page.within(ref_selector) do
fill_in 'Search by Git revision', with: ref_name
wait_for_requests
find('li', text: ref_name, match: :prefer_exact).click
end
filter_by(ref_name)
visit(project_tree_path(project, "fix/.testdir"))
@ -394,4 +387,12 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
end
end
end
def filter_by(filter_text)
send_keys filter_text
wait_for_requests
select_listbox_item filter_text
end
end

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User find project file', feature_category: :projects do
include ListboxHelpers
let(:user) { create :user }
let(:project) { create :project, :repository }
@ -22,7 +24,7 @@ RSpec.describe 'User find project file', feature_category: :projects do
end
def ref_selector_dropdown
find('.gl-dropdown-toggle > .gl-dropdown-button-text')
find('.gl-button-text')
end
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
wait_for_requests
find('.gl-dropdown-item', text: ref).click
select_listbox_item(ref)
end
expect(ref_selector_dropdown).to have_text(ref)
end

View File

@ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects 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.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
@ -69,18 +69,18 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
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
ref_selector = find('.ref-selector .gl-dropdown-toggle')
ref_selector = find('.ref-selector .gl-new-dropdown-toggle')
scroll_to(ref_selector)
ref_selector.click
page.within '[data-testid="branches-section"]' do
dropdown_branch_item = find('.gl-dropdown-item', text: 'add-pdf-file')
page.within '.gl-new-dropdown-contents' do
dropdown_branch_item = find('li', text: 'add-pdf-file')
scroll_to(dropdown_branch_item)
dropdown_branch_item.click
end
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
expect(page).to have_selector('h4', text: ref_name)
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
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
@ -77,7 +77,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default 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
@ -319,7 +319,6 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
end
def select_target_branch
find('[data-testid="schedule-target-ref"] .dropdown-toggle').click
click_button 'master'
end

View File

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

View File

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

View File

@ -3,6 +3,9 @@
require 'spec_helper'
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_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)
find('.ref-selector').click
click_button 'master'
wait_for_requests
page.within('.ref-selector') do
fill_in 'Search by Git revision', with: ref_selector
wait_for_requests
find('li', text: ref_selector, match: :prefer_exact).click
end
select_listbox_item(ref_selector)
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
it 'search result changes when refs switched' do
ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack')
find('.ref-selector').click
wait_for_requests
page.within('.ref-selector') do
fill_in _('Search by Git revision'), with: ref
wait_for_requests
find('li', text: ref).click
end
select_listbox_item('add-ipython-files')
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
@ -192,18 +185,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
end
it 'search result changes when refs switched' do
ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack')
find('.ref-selector').click
wait_for_requests
page.within('.ref-selector') do
fill_in _('Search by Git revision'), with: ref
wait_for_requests
find('li', text: ref).click
end
select_listbox_item('add-ipython-files')
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end

View File

@ -34,7 +34,7 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within(ref_selector) do
fill_in _('Search by Git revision'), with: ref_name
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
@ -60,9 +60,9 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
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
expect(find('.dropdown-menu')).to have_content 'test'
expect(find('.gl-new-dropdown-inner')).to have_content 'test'
end
end
end

View File

@ -438,42 +438,24 @@ describe('Ci variable modal', () => {
raw: true,
};
describe('and FF is enabled', () => {
beforeEach(() => {
createComponent({
mountFn: mountExtended,
props: { selectedVariable: validRawMaskedVariable },
});
});
it('should not show an error with symbols', async () => {
await findMaskedVariableCheckbox().trigger('click');
expect(findModal().text()).not.toContain(maskError);
});
it('should not show an error when length is less than 8', async () => {
await findValueField().vm.$emit('input', 'a');
await findMaskedVariableCheckbox().trigger('click');
expect(findModal().text()).toContain(maskError);
beforeEach(() => {
createComponent({
mountFn: mountExtended,
props: { selectedVariable: validRawMaskedVariable },
});
});
describe('and FF is disabled', () => {
beforeEach(() => {
createComponent({
mountFn: mountExtended,
props: { selectedVariable: validRawMaskedVariable },
provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } },
});
});
it('should not show an error with symbols', async () => {
await findMaskedVariableCheckbox().trigger('click');
it('should show an error with symbols', async () => {
await findMaskedVariableCheckbox().trigger('click');
expect(findModal().text()).not.toContain(maskError);
});
expect(findModal().text()).toContain(maskError);
});
it('should not show an error when length is less than 8', async () => {
await findValueField().vm.$emit('input', 'a');
await findMaskedVariableCheckbox().trigger('click');
expect(findModal().text()).toContain(maskError);
});
});

View File

@ -1,8 +1,16 @@
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 PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.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 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';
describe('PackageVersionsList', () => {
@ -24,6 +32,7 @@ describe('PackageVersionsList', () => {
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
findListRow: () => wrapper.findAllComponents(VersionRow),
findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
};
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackageVersionsList, {
@ -35,6 +44,11 @@ describe('PackageVersionsList', () => {
},
stubs: {
RegistryList,
DeleteModal: stubComponent(DeleteModal, {
methods: {
show: jest.fn(),
},
}),
},
slots: {
'empty-state': EmptySlotStub,
@ -144,4 +158,80 @@ describe('PackageVersionsList', () => {
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 { 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 PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
@ -15,17 +16,20 @@ const packageVersion = packageVersions()[0];
describe('VersionRow', () => {
let wrapper;
const findListItem = () => wrapper.findComponent(ListItem);
const findLink = () => wrapper.findComponent(GlLink);
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
function createComponent(packageEntity = packageVersion) {
function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, {
propsData: {
packageEntity,
selected,
},
stubs: {
GlSprintf,
@ -76,13 +80,47 @@ describe('VersionRow', () => {
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`, () => {
beforeEach(() => {
createComponent({
...packageVersion,
status: PACKAGE_ERROR_STATUS,
_links: {
webPath: null,
packageEntity: {
...packageVersion,
status: PACKAGE_ERROR_STATUS,
_links: {
webPath: null,
},
},
});
});
@ -109,10 +147,12 @@ describe('VersionRow', () => {
describe('disabled status', () => {
beforeEach(() => {
createComponent({
...packageVersion,
status: 'something',
_links: {
webPath: null,
packageEntity: {
...packageVersion,
status: 'something',
_links: {
webPath: null,
},
},
});
});

View File

@ -47,6 +47,7 @@ describe('packages_list_row', () => {
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate);
@ -212,6 +213,9 @@ describe('packages_list_row', () => {
});
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',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.1',
...linksData,
@ -119,6 +120,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.2',
...linksData,

View File

@ -128,6 +128,7 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
afterEach(() => {
@ -267,7 +268,7 @@ describe('PackagesApp', () => {
await waitForPromises();
findDeletePackages().vm.$emit('end');
findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true',
@ -281,7 +282,7 @@ describe('PackagesApp', () => {
await waitForPromises();
findDeletePackages().vm.$emit('end');
findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true',
@ -600,9 +601,51 @@ describe('PackagesApp', () => {
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({
canDestroy: true,
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', () => {

View File

@ -1,5 +1,4 @@
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@ -8,13 +7,13 @@ import Vuex from 'vuex';
import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.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 {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import {
@ -42,7 +41,7 @@ describe('Ref selector component', () => {
let requestSpies;
const createComponent = (mountOverrides = {}, propsData = {}) => {
wrapper = mount(
wrapper = mountExtended(
RefSelector,
merge(
{
@ -57,9 +56,6 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: selectedRef });
},
},
stubs: {
GlSearchBoxByType: true,
},
store: createStore(),
},
mountOverrides,
@ -91,76 +87,63 @@ describe('Ref selector component', () => {
.reply((config) => commitApiCallSpy(config));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
//
// 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 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 findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
const findBranchesSection = () => findListBoxSection('Branches');
const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
const findTagsSection = () => findListBoxSection('Tags');
const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
const findCommitsSection = () => findListBoxSection('Commits');
const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field');
//
// Expecters
//
const branchesSectionContainsErrorMessage = () => {
const branchesSection = findBranchesSection();
const sectionContainsErrorMessage = (message) => {
const errorSection = findErrorListWrapper();
return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
};
const tagsSectionContainsErrorMessage = () => {
const tagsSection = findTagsSection();
return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
};
const commitsSectionContainsErrorMessage = () => {
const commitsSection = findCommitsSection();
return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
return errorSection ? errorSection.text().includes(message) : false;
};
//
// Convenience methods
//
const updateQuery = (newQuery) => {
findSearchBox().vm.$emit('input', newQuery);
findListbox().vm.$emit('search', newQuery);
};
const selectFirstBranch = async () => {
findFirstBranchDropdownItem().vm.$emit('click');
findListbox().vm.$emit('select', fixtures.branches[0].name);
await nextTick();
};
const selectFirstTag = async () => {
findFirstTagDropdownItem().vm.$emit('click');
findListbox().vm.$emit('select', fixtures.tags[0].name);
await nextTick();
};
const selectFirstCommit = async () => {
findFirstCommitDropdownItem().vm.$emit('click');
findListbox().vm.$emit('select', fixtures.commit.id);
await nextTick();
};
@ -195,7 +178,7 @@ describe('Ref selector component', () => {
});
describe('when name property is provided', () => {
it('renders an forrm input hidden field', () => {
it('renders an form input hidden field', () => {
const name = 'default_tag';
createComponent({ propsData: { name } });
@ -205,7 +188,7 @@ describe('Ref selector component', () => {
});
describe('when name property is not provided', () => {
it('renders an forrm input hidden field', () => {
it('renders an form input hidden field', () => {
createComponent();
expect(findHiddenInputField().exists()).toBe(false);
@ -224,7 +207,7 @@ describe('Ref selector component', () => {
});
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', () => {
expect(findButtonContent().text()).toBe(preselectedRef);
expect(findButtonToggle().text()).toBe(preselectedRef);
});
it('binds hidden input field to the pre-selected ref', () => {
@ -259,7 +242,7 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: updatedRef });
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', () => {
beforeEach(() => {
branchesApiCallSpy = jest
@ -357,27 +323,10 @@ describe('Ref selector component', () => {
it('renders the branches section in the dropdown', () => {
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", () => {
expect(branchesSectionContainsErrorMessage()).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);
}
});
expect(findErrorListWrapper().exists()).toBe(false);
});
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', () => {
expect(findBranchesSection().exists()).toBe(true);
expect(findBranchesSection().exists()).toBe(false);
});
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', () => {
expect(findTagsSection().exists()).toBe(true);
expect(findTagsSection().props('shouldShowCheck')).toBe(true);
});
it('renders the "Tags" heading with a total number indicator', () => {
expect(
findTagsSection().find('[data-testid="section-header"]').text(),
).toMatchInterpolatedText('Tags 456');
expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
`Tags ${fixtures.tags.length}`,
);
});
it("does not render an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).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);
});
expect(findErrorListWrapper().exists()).toBe(false);
});
});
@ -485,11 +425,11 @@ describe('Ref selector component', () => {
});
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", () => {
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', () => {
expect(
findCommitsSection().find('[data-testid="section-header"]').text(),
).toMatchInterpolatedText('Commits 1');
expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
`Commits 1`,
);
});
it("does not render an error message in the comits section's body", () => {
expect(commitsSectionContainsErrorMessage()).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}`);
it("does not render an error message in the commits section's body", () => {
expect(findErrorListWrapper().exists()).toBe(false);
});
});
@ -553,11 +487,11 @@ describe('Ref selector component', () => {
});
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", () => {
expect(commitsSectionContainsErrorMessage()).toBe(true);
expect(sectionContainsErrorMessage(DEFAULT_I18N.commitsErrorMessage)).toBe(true);
});
});
});
@ -571,26 +505,13 @@ describe('Ref selector component', () => {
return waitForRequests();
});
it('renders a checkmark by the selected item', async () => {
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', () => {
describe('when a branch is selected', () => {
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 nextTick();
expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
expect(findButtonToggle().text()).toBe(fixtures.branches[0].name);
});
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', () => {
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 nextTick();
expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
expect(findButtonToggle().text()).toBe(fixtures.tags[0].name);
});
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', () => {
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 nextTick();
expect(findButtonContent().text()).toBe(fixtures.commit.id);
expect(findButtonToggle().text()).toBe(fixtures.commit.id);
});
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);
});
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`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
@ -726,8 +630,7 @@ describe('Ref selector component', () => {
describe('validation state', () => {
const invalidClass = 'gl-inset-border-1-red-500!';
const isInvalidClassApplied = () =>
wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass];
const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass];
describe('valid state', () => {
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
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
subject

View File

@ -13,15 +13,23 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
end
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)
.to receive(:delete_multi)
.with(
array_including(
[
"pages_domain_for_#{type}_1",
"pages_domain_for_#{type}_1_settings-hash"
]
))
.with(cached_keys)
subject.clear_cache
end
@ -31,13 +39,13 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
describe '.for_namespace' do
subject(:cache_control) { described_class.for_namespace(1) }
it_behaves_like 'cache_control', 'namespace'
it_behaves_like 'cache_control', :namespace
end
describe '.for_domain' do
subject(:cache_control) { described_class.for_domain(1) }
it_behaves_like 'cache_control', 'domain'
it_behaves_like 'cache_control', :domain
end
describe '#cache_key' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Ci::Runner, feature_category: :runner do
RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
include StubGitlabCalls
it_behaves_like 'having unique enum values'
@ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe 'validation' do
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(:registration_type) }
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
@ -1748,7 +1749,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
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) }
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
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-') }
end
@ -1993,20 +1994,4 @@ RSpec.describe Ci::Runner, feature_category: :runner do
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

View File

@ -525,7 +525,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:creator) { create(:user) }
let(:created_at) { Time.current }
let(:token_prefix) { '' }
let(:token_prefix) { registration_type == :authenticated_user ? 'glrt-' : '' }
let(:registration_type) {}
let(:query) do
%(
query {
@ -539,7 +540,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:runner) do
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
before_all do
@ -570,7 +572,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:user) { creator }
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
let(:created_at) { (3.hours - 1.second).ago }
@ -600,7 +602,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
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
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) }
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'
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)
subject
expect(response).to have_gitlab_http_status(:redirect)
end
end

View File

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

View File

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

View File

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