Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									a2b7b398c7
								
							
						
					
					
						commit
						283c7bb302
					
				|  | @ -3,7 +3,6 @@ Rake/Require: | ||||||
|   Details: grace period |   Details: grace period | ||||||
|   Exclude: |   Exclude: | ||||||
|     - 'lib/tasks/gitlab/assets.rake' |     - 'lib/tasks/gitlab/assets.rake' | ||||||
|     - 'lib/tasks/gitlab/cleanup.rake' |  | ||||||
|     - 'lib/tasks/gitlab/dependency_proxy/migrate.rake' |     - 'lib/tasks/gitlab/dependency_proxy/migrate.rake' | ||||||
|     - 'lib/tasks/gitlab/docs/redirect.rake' |     - 'lib/tasks/gitlab/docs/redirect.rake' | ||||||
|     - 'lib/tasks/gitlab/graphql.rake' |     - 'lib/tasks/gitlab/graphql.rake' | ||||||
|  |  | ||||||
|  | @ -130,9 +130,6 @@ export default { | ||||||
|     displayMaskedError() { |     displayMaskedError() { | ||||||
|       return !this.canMask && this.variable.masked; |       return !this.canMask && this.variable.masked; | ||||||
|     }, |     }, | ||||||
|     isUsingRawRegexFlag() { |  | ||||||
|       return this.glFeatures.ciRemoveCharacterLimitationRawMaskedVar; |  | ||||||
|     }, |  | ||||||
|     isEditing() { |     isEditing() { | ||||||
|       return this.mode === EDIT_VARIABLE_ACTION; |       return this.mode === EDIT_VARIABLE_ACTION; | ||||||
|     }, |     }, | ||||||
|  | @ -177,7 +174,7 @@ export default { | ||||||
|       return true; |       return true; | ||||||
|     }, |     }, | ||||||
|     useRawMaskableRegexp() { |     useRawMaskableRegexp() { | ||||||
|       return this.isRaw && this.isUsingRawRegexFlag; |       return this.isRaw; | ||||||
|     }, |     }, | ||||||
|     variableValidationFeedback() { |     variableValidationFeedback() { | ||||||
|       return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; |       return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; | ||||||
|  |  | ||||||
|  | @ -77,7 +77,7 @@ export default { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <gl-intersection-observer |   <gl-intersection-observer | ||||||
|     class="gl-relative gl-top-2" |     class="gl-relative gl-top-n5" | ||||||
|     @appear="setStickyHeaderVisible(false)" |     @appear="setStickyHeaderVisible(false)" | ||||||
|     @disappear="setStickyHeaderVisible(true)" |     @disappear="setStickyHeaderVisible(true)" | ||||||
|   > |   > | ||||||
|  |  | ||||||
|  | @ -1,15 +1,30 @@ | ||||||
| <script> | <script> | ||||||
|  | import { n__ } from '~/locale'; | ||||||
| import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; | import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; | ||||||
| import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; | import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; | ||||||
| import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; | import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; | ||||||
|  | import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; | ||||||
|  | import { | ||||||
|  |   CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |   DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |   REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  | } from '~/packages_and_registries/package_registry/constants'; | ||||||
|  | import Tracking from '~/tracking'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|  |     DeleteModal, | ||||||
|     VersionRow, |     VersionRow, | ||||||
|     PackagesListLoader, |     PackagesListLoader, | ||||||
|     RegistryList, |     RegistryList, | ||||||
|   }, |   }, | ||||||
|  |   mixins: [Tracking.mixin()], | ||||||
|   props: { |   props: { | ||||||
|  |     canDestroy: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|     versions: { |     versions: { | ||||||
|       type: Array, |       type: Array, | ||||||
|       required: true, |       required: true, | ||||||
|  | @ -25,11 +40,35 @@ export default { | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       itemsToBeDeleted: [], | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     listTitle() { | ||||||
|  |       return n__('%d version', '%d versions', this.versions.length); | ||||||
|  |     }, | ||||||
|     isListEmpty() { |     isListEmpty() { | ||||||
|       return this.versions.length === 0; |       return this.versions.length === 0; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   methods: { | ||||||
|  |     deleteItemsCanceled() { | ||||||
|  |       this.track(CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); | ||||||
|  |       this.itemsToBeDeleted = []; | ||||||
|  |     }, | ||||||
|  |     deleteItemsConfirmation() { | ||||||
|  |       this.$emit('delete', this.itemsToBeDeleted); | ||||||
|  |       this.track(DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); | ||||||
|  |       this.itemsToBeDeleted = []; | ||||||
|  |     }, | ||||||
|  |     setItemsToBeDeleted(items) { | ||||||
|  |       this.itemsToBeDeleted = items; | ||||||
|  |       this.track(REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); | ||||||
|  |       this.$refs.deletePackagesModal.show(); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|  | @ -40,17 +79,34 @@ export default { | ||||||
|     <slot v-else-if="isListEmpty" name="empty-state"></slot> |     <slot v-else-if="isListEmpty" name="empty-state"></slot> | ||||||
|     <div v-else> |     <div v-else> | ||||||
|       <registry-list |       <registry-list | ||||||
|         :hidden-delete="true" |         :hidden-delete="!canDestroy" | ||||||
|         :is-loading="isLoading" |         :is-loading="isLoading" | ||||||
|         :items="versions" |         :items="versions" | ||||||
|         :pagination="pageInfo" |         :pagination="pageInfo" | ||||||
|  |         :title="listTitle" | ||||||
|  |         @delete="setItemsToBeDeleted" | ||||||
|         @prev-page="$emit('prev-page')" |         @prev-page="$emit('prev-page')" | ||||||
|         @next-page="$emit('next-page')" |         @next-page="$emit('next-page')" | ||||||
|       > |       > | ||||||
|         <template #default="{ item }"> |         <template #default="{ first, item, isSelected, selectItem }"> | ||||||
|           <version-row :package-entity="item" /> |           <!-- `first` prop is used to decide whether to show the top border | ||||||
|  |           for the first element. We want to show the top border only when | ||||||
|  |           user has permission to bulk delete versions. --> | ||||||
|  |           <version-row | ||||||
|  |             :first="canDestroy && first" | ||||||
|  |             :package-entity="item" | ||||||
|  |             :selected="isSelected(item)" | ||||||
|  |             @select="selectItem(item)" | ||||||
|  |           /> | ||||||
|         </template> |         </template> | ||||||
|       </registry-list> |       </registry-list> | ||||||
|  | 
 | ||||||
|  |       <delete-modal | ||||||
|  |         ref="deletePackagesModal" | ||||||
|  |         :items-to-be-deleted="itemsToBeDeleted" | ||||||
|  |         @confirm="deleteItemsConfirmation" | ||||||
|  |         @cancel="deleteItemsCanceled" | ||||||
|  |       /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,12 @@ | ||||||
| <script> | <script> | ||||||
| import { GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; | import { | ||||||
|  |   GlFormCheckbox, | ||||||
|  |   GlIcon, | ||||||
|  |   GlLink, | ||||||
|  |   GlSprintf, | ||||||
|  |   GlTooltipDirective, | ||||||
|  |   GlTruncate, | ||||||
|  | } from '@gitlab/ui'; | ||||||
| import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | ||||||
| import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; | import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; | ||||||
| import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; | import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; | ||||||
|  | @ -15,6 +22,7 @@ import { | ||||||
| export default { | export default { | ||||||
|   name: 'PackageVersionRow', |   name: 'PackageVersionRow', | ||||||
|   components: { |   components: { | ||||||
|  |     GlFormCheckbox, | ||||||
|     GlIcon, |     GlIcon, | ||||||
|     GlLink, |     GlLink, | ||||||
|     GlSprintf, |     GlSprintf, | ||||||
|  | @ -32,6 +40,11 @@ export default { | ||||||
|       type: Object, |       type: Object, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|  |     selected: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |       required: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     containsWebPathLink() { |     containsWebPathLink() { | ||||||
|  | @ -53,7 +66,15 @@ export default { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <list-item> |   <list-item :selected="selected" v-bind="$attrs"> | ||||||
|  |     <template #left-action> | ||||||
|  |       <gl-form-checkbox | ||||||
|  |         v-if="packageEntity.canDestroy" | ||||||
|  |         class="gl-m-0" | ||||||
|  |         :checked="selected" | ||||||
|  |         @change="$emit('select')" | ||||||
|  |       /> | ||||||
|  |     </template> | ||||||
|     <template #left-primary> |     <template #left-primary> | ||||||
|       <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> |       <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> | ||||||
|         <gl-link v-if="containsWebPathLink" class="gl-text-body gl-min-w-0" :href="packageLink"> |         <gl-link v-if="containsWebPathLink" class="gl-text-body gl-min-w-0" :href="packageLink"> | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ export default { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <list-item data-testid="package-row" v-bind="$attrs"> |   <list-item data-testid="package-row" :selected="selected" v-bind="$attrs"> | ||||||
|     <template #left-action> |     <template #left-action> | ||||||
|       <gl-form-checkbox |       <gl-form-checkbox | ||||||
|         v-if="packageEntity.canDestroy" |         v-if="packageEntity.canDestroy" | ||||||
|  |  | ||||||
|  | @ -115,6 +115,10 @@ export const DELETE_PACKAGES_TRACKING_ACTION = 'delete_packages'; | ||||||
| export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages'; | export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages'; | ||||||
| export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages'; | export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages'; | ||||||
| 
 | 
 | ||||||
|  | export const DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'delete_package_versions'; | ||||||
|  | export const REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'request_delete_package_versions'; | ||||||
|  | export const CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'cancel_delete_package_versions'; | ||||||
|  | 
 | ||||||
| export const DELETE_PACKAGES_ERROR_MESSAGE = s__( | export const DELETE_PACKAGES_ERROR_MESSAGE = s__( | ||||||
|   'PackageRegistry|Something went wrong while deleting packages.', |   'PackageRegistry|Something went wrong while deleting packages.', | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | @ -66,6 +66,7 @@ query getPackageDetails( | ||||||
|       nodes { |       nodes { | ||||||
|         id |         id | ||||||
|         name |         name | ||||||
|  |         canDestroy | ||||||
|         createdAt |         createdAt | ||||||
|         version |         version | ||||||
|         status |         status | ||||||
|  |  | ||||||
|  | @ -95,6 +95,7 @@ export default { | ||||||
|       deletePackageModalContent: DELETE_MODAL_CONTENT, |       deletePackageModalContent: DELETE_MODAL_CONTENT, | ||||||
|       filesToDelete: [], |       filesToDelete: [], | ||||||
|       mutationLoading: false, |       mutationLoading: false, | ||||||
|  |       versionsMutationLoading: false, | ||||||
|       packageEntity: {}, |       packageEntity: {}, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  | @ -146,6 +147,9 @@ export default { | ||||||
|     isLoading() { |     isLoading() { | ||||||
|       return this.$apollo.queries.packageEntity.loading; |       return this.$apollo.queries.packageEntity.loading; | ||||||
|     }, |     }, | ||||||
|  |     isVersionsLoading() { | ||||||
|  |       return this.isLoading || this.versionsMutationLoading; | ||||||
|  |     }, | ||||||
|     packageFilesLoading() { |     packageFilesLoading() { | ||||||
|       return this.isLoading || this.mutationLoading; |       return this.isLoading || this.mutationLoading; | ||||||
|     }, |     }, | ||||||
|  | @ -157,9 +161,6 @@ export default { | ||||||
|         category: packageTypeToTrackCategory(this.packageType), |         category: packageTypeToTrackCategory(this.packageType), | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|     hasVersions() { |  | ||||||
|       return this.packageEntity.versions?.nodes?.length > 0; |  | ||||||
|     }, |  | ||||||
|     versionPageInfo() { |     versionPageInfo() { | ||||||
|       return this.packageEntity?.versions?.pageInfo ?? {}; |       return this.packageEntity?.versions?.pageInfo ?? {}; | ||||||
|     }, |     }, | ||||||
|  | @ -181,6 +182,14 @@ export default { | ||||||
|         PACKAGE_TYPE_PYPI, |         PACKAGE_TYPE_PYPI, | ||||||
|       ].includes(this.packageType); |       ].includes(this.packageType); | ||||||
|     }, |     }, | ||||||
|  |     refetchQueriesData() { | ||||||
|  |       return [ | ||||||
|  |         { | ||||||
|  |           query: getPackageDetails, | ||||||
|  |           variables: this.queryVariables, | ||||||
|  |         }, | ||||||
|  |       ]; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     formatSize(size) { |     formatSize(size) { | ||||||
|  | @ -206,12 +215,7 @@ export default { | ||||||
|             ids, |             ids, | ||||||
|           }, |           }, | ||||||
|           awaitRefetchQueries: true, |           awaitRefetchQueries: true, | ||||||
|           refetchQueries: [ |           refetchQueries: this.refetchQueriesData, | ||||||
|             { |  | ||||||
|               query: getPackageDetails, |  | ||||||
|               variables: this.queryVariables, |  | ||||||
|             }, |  | ||||||
|           ], |  | ||||||
|         }); |         }); | ||||||
|         if (data?.destroyPackageFiles?.errors[0]) { |         if (data?.destroyPackageFiles?.errors[0]) { | ||||||
|           throw data.destroyPackageFiles.errors[0]; |           throw data.destroyPackageFiles.errors[0]; | ||||||
|  | @ -403,19 +407,30 @@ export default { | ||||||
|           }}</gl-badge> |           }}</gl-badge> | ||||||
|         </template> |         </template> | ||||||
| 
 | 
 | ||||||
|         <package-versions-list |         <delete-packages | ||||||
|           :is-loading="isLoading" |           :refetch-queries="refetchQueriesData" | ||||||
|           :page-info="versionPageInfo" |           show-success-alert | ||||||
|           :versions="packageEntity.versions.nodes" |           @start="versionsMutationLoading = true" | ||||||
|           @prev-page="fetchPreviousVersionsPage" |           @end="versionsMutationLoading = false" | ||||||
|           @next-page="fetchNextVersionsPage" |  | ||||||
|         > |         > | ||||||
|           <template #empty-state> |           <template #default="{ deletePackages }"> | ||||||
|             <p class="gl-mt-3" data-testid="no-versions-message"> |             <package-versions-list | ||||||
|               {{ s__('PackageRegistry|There are no other versions of this package.') }} |               :can-destroy="packageEntity.canDestroy" | ||||||
|             </p> |               :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> |           </template> | ||||||
|         </package-versions-list> |         </delete-packages> | ||||||
|       </gl-tab> |       </gl-tab> | ||||||
|     </gl-tabs> |     </gl-tabs> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -1,13 +1,8 @@ | ||||||
| <script> | <script> | ||||||
| import { | import { GlBadge, GlIcon, GlCollapsibleListbox } from '@gitlab/ui'; | ||||||
|   GlDropdown, |  | ||||||
|   GlDropdownDivider, |  | ||||||
|   GlSearchBoxByType, |  | ||||||
|   GlSprintf, |  | ||||||
|   GlLoadingIcon, |  | ||||||
| } from '@gitlab/ui'; |  | ||||||
| import { debounce, isArray } from 'lodash'; | import { debounce, isArray } from 'lodash'; | ||||||
| import { mapActions, mapGetters, mapState } from 'vuex'; | import { mapActions, mapGetters, mapState } from 'vuex'; | ||||||
|  | import { sprintf } from '~/locale'; | ||||||
| import { | import { | ||||||
|   ALL_REF_TYPES, |   ALL_REF_TYPES, | ||||||
|   SEARCH_DEBOUNCE_MS, |   SEARCH_DEBOUNCE_MS, | ||||||
|  | @ -15,21 +10,16 @@ import { | ||||||
|   REF_TYPE_BRANCHES, |   REF_TYPE_BRANCHES, | ||||||
|   REF_TYPE_TAGS, |   REF_TYPE_TAGS, | ||||||
|   REF_TYPE_COMMITS, |   REF_TYPE_COMMITS, | ||||||
|   BRANCH_REF_TYPE, |  | ||||||
|   TAG_REF_TYPE, |  | ||||||
| } from '../constants'; | } from '../constants'; | ||||||
| import createStore from '../stores'; | import createStore from '../stores'; | ||||||
| import RefResultsSection from './ref_results_section.vue'; | import { formatListBoxItems, formatErrors } from '../format_refs'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   name: 'RefSelector', |   name: 'RefSelector', | ||||||
|   components: { |   components: { | ||||||
|     GlDropdown, |     GlBadge, | ||||||
|     GlDropdownDivider, |     GlIcon, | ||||||
|     GlSearchBoxByType, |     GlCollapsibleListbox, | ||||||
|     GlSprintf, |  | ||||||
|     GlLoadingIcon, |  | ||||||
|     RefResultsSection, |  | ||||||
|   }, |   }, | ||||||
|   inheritAttrs: false, |   inheritAttrs: false, | ||||||
|   props: { |   props: { | ||||||
|  | @ -87,7 +77,6 @@ export default { | ||||||
|       required: false, |       required: false, | ||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     toggleButtonClass: { |     toggleButtonClass: { | ||||||
|       type: [String, Object, Array], |       type: [String, Object, Array], | ||||||
|       required: false, |       required: false, | ||||||
|  | @ -112,29 +101,17 @@ export default { | ||||||
|         ...this.translations, |         ...this.translations, | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|     showBranchesSection() { |     listBoxItems() { | ||||||
|       return ( |       return formatListBoxItems(this.branches, this.tags, this.commits); | ||||||
|         this.enabledRefTypes.includes(REF_TYPE_BRANCHES) && |  | ||||||
|         Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error) |  | ||||||
|       ); |  | ||||||
|     }, |     }, | ||||||
|     showTagsSection() { |     branches() { | ||||||
|       return ( |       return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : []; | ||||||
|         this.enabledRefTypes.includes(REF_TYPE_TAGS) && |  | ||||||
|         Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error) |  | ||||||
|       ); |  | ||||||
|     }, |     }, | ||||||
|     showCommitsSection() { |     tags() { | ||||||
|       return ( |       return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : []; | ||||||
|         this.enabledRefTypes.includes(REF_TYPE_COMMITS) && |  | ||||||
|         Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error) |  | ||||||
|       ); |  | ||||||
|     }, |     }, | ||||||
|     showNoResults() { |     commits() { | ||||||
|       return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection; |       return this.enabledRefTypes.includes(REF_TYPE_COMMITS) ? this.matches.commits.list : []; | ||||||
|     }, |  | ||||||
|     showSectionHeaders() { |  | ||||||
|       return this.enabledRefTypes.length > 1; |  | ||||||
|     }, |     }, | ||||||
|     extendedToggleButtonClass() { |     extendedToggleButtonClass() { | ||||||
|       const classes = [ |       const classes = [ | ||||||
|  | @ -142,7 +119,6 @@ export default { | ||||||
|           'gl-inset-border-1-red-500!': !this.state, |           'gl-inset-border-1-red-500!': !this.state, | ||||||
|           'gl-font-monospace': Boolean(this.selectedRef), |           'gl-font-monospace': Boolean(this.selectedRef), | ||||||
|         }, |         }, | ||||||
|         'gl-max-w-26', |  | ||||||
|       ]; |       ]; | ||||||
| 
 | 
 | ||||||
|       if (Array.isArray(this.toggleButtonClass)) { |       if (Array.isArray(this.toggleButtonClass)) { | ||||||
|  | @ -160,6 +136,9 @@ export default { | ||||||
|         query: this.lastQuery, |         query: this.lastQuery, | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|  |     errors() { | ||||||
|  |       return formatErrors(this.matches.branches, this.matches.tags, this.matches.commits); | ||||||
|  |     }, | ||||||
|     selectedRefForDisplay() { |     selectedRefForDisplay() { | ||||||
|       if (this.useSymbolicRefNames && this.selectedRef) { |       if (this.useSymbolicRefNames && this.selectedRef) { | ||||||
|         return this.selectedRef.replace(/^refs\/(tags|heads)\//, ''); |         return this.selectedRef.replace(/^refs\/(tags|heads)\//, ''); | ||||||
|  | @ -170,11 +149,12 @@ export default { | ||||||
|     buttonText() { |     buttonText() { | ||||||
|       return this.selectedRefForDisplay || this.i18n.noRefSelected; |       return this.selectedRefForDisplay || this.i18n.noRefSelected; | ||||||
|     }, |     }, | ||||||
|     isTagRefType() { |     noResultsMessage() { | ||||||
|       return this.refType === TAG_REF_TYPE; |       return this.lastQuery | ||||||
|     }, |         ? sprintf(this.i18n.noResultsWithQuery, { | ||||||
|     isBranchRefType() { |             query: this.lastQuery, | ||||||
|       return this.refType === BRANCH_REF_TYPE; |           }) | ||||||
|  |         : this.i18n.noResults; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|  | @ -202,9 +182,7 @@ export default { | ||||||
|     // because we need to access the .cancel() method |     // because we need to access the .cancel() method | ||||||
|     // lodash attaches to the function, which is |     // lodash attaches to the function, which is | ||||||
|     // made inaccessible by Vue. |     // made inaccessible by Vue. | ||||||
|     this.debouncedSearch = debounce(function search() { |     this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS); | ||||||
|       this.search(); |  | ||||||
|     }, SEARCH_DEBOUNCE_MS); |  | ||||||
| 
 | 
 | ||||||
|     this.setProjectId(this.projectId); |     this.setProjectId(this.projectId); | ||||||
| 
 | 
 | ||||||
|  | @ -231,14 +209,8 @@ export default { | ||||||
|       'setSelectedRef', |       'setSelectedRef', | ||||||
|     ]), |     ]), | ||||||
|     ...mapActions({ storeSearch: 'search' }), |     ...mapActions({ storeSearch: 'search' }), | ||||||
|     focusSearchBox() { |     onSearchBoxInput(searchQuery = '') { | ||||||
|       this.$refs.searchBox.$el.querySelector('input').focus(); |       this.query = searchQuery?.trim(); | ||||||
|     }, |  | ||||||
|     onSearchBoxEnter() { |  | ||||||
|       this.debouncedSearch.cancel(); |  | ||||||
|       this.search(); |  | ||||||
|     }, |  | ||||||
|     onSearchBoxInput() { |  | ||||||
|       this.debouncedSearch(); |       this.debouncedSearch(); | ||||||
|     }, |     }, | ||||||
|     selectRef(ref) { |     selectRef(ref) { | ||||||
|  | @ -248,104 +220,55 @@ export default { | ||||||
|     search() { |     search() { | ||||||
|       this.storeSearch(this.query); |       this.storeSearch(this.query); | ||||||
|     }, |     }, | ||||||
|  |     totalCountText(count) { | ||||||
|  |       return count > 999 ? this.i18n.totalCountLabel : `${count}`; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <gl-dropdown |     <gl-collapsible-listbox | ||||||
|  |       class="ref-selector gl-w-full" | ||||||
|  |       block | ||||||
|  |       searchable | ||||||
|  |       :selected="selectedRef" | ||||||
|       :header-text="i18n.dropdownHeader" |       :header-text="i18n.dropdownHeader" | ||||||
|  |       :items="listBoxItems" | ||||||
|  |       :no-results-text="noResultsMessage" | ||||||
|  |       :searching="isLoading" | ||||||
|  |       :search-placeholder="i18n.searchPlaceholder" | ||||||
|       :toggle-class="extendedToggleButtonClass" |       :toggle-class="extendedToggleButtonClass" | ||||||
|       :text="buttonText" |       :toggle-text="buttonText" | ||||||
|       class="ref-selector" |  | ||||||
|       v-bind="$attrs" |       v-bind="$attrs" | ||||||
|       v-on="$listeners" |       v-on="$listeners" | ||||||
|       @shown="focusSearchBox" |       @hidden="$emit('hide')" | ||||||
|  |       @search="onSearchBoxInput" | ||||||
|  |       @select="selectRef" | ||||||
|     > |     > | ||||||
|       <template #header> |       <template #group-label="{ group }"> | ||||||
|         <gl-search-box-by-type |         {{ group.text }} <gl-badge size="sm">{{ totalCountText(group.options.length) }}</gl-badge> | ||||||
|           ref="searchBox" |  | ||||||
|           v-model.trim="query" |  | ||||||
|           :placeholder="i18n.searchPlaceholder" |  | ||||||
|           autocomplete="off" |  | ||||||
|           data-qa-selector="ref_selector_searchbox" |  | ||||||
|           @input="onSearchBoxInput" |  | ||||||
|           @keydown.enter.prevent="onSearchBoxEnter" |  | ||||||
|         /> |  | ||||||
|       </template> |       </template> | ||||||
| 
 |       <template #list-item="{ item }"> | ||||||
|       <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" /> |         {{ item.text }} | ||||||
| 
 |         <gl-badge v-if="item.default" size="sm" variant="info">{{ | ||||||
|       <div |           i18n.defaultLabelText | ||||||
|         v-else-if="showNoResults" |         }}</gl-badge> | ||||||
|         class="gl-text-center gl-mx-3 gl-py-3" |  | ||||||
|         data-testid="no-results" |  | ||||||
|       > |  | ||||||
|         <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery"> |  | ||||||
|           <template #query> |  | ||||||
|             <b class="gl-word-break-all">{{ lastQuery }}</b> |  | ||||||
|           </template> |  | ||||||
|         </gl-sprintf> |  | ||||||
| 
 |  | ||||||
|         <span v-else>{{ i18n.noResults }}</span> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <template v-else> |  | ||||||
|         <template v-if="showBranchesSection"> |  | ||||||
|           <ref-results-section |  | ||||||
|             :section-title="i18n.branches" |  | ||||||
|             :total-count="matches.branches.totalCount" |  | ||||||
|             :items="matches.branches.list" |  | ||||||
|             :selected-ref="selectedRef" |  | ||||||
|             :error="matches.branches.error" |  | ||||||
|             :error-message="i18n.branchesErrorMessage" |  | ||||||
|             :show-header="showSectionHeaders" |  | ||||||
|             data-testid="branches-section" |  | ||||||
|             data-qa-selector="branches_section" |  | ||||||
|             :should-show-check="!useSymbolicRefNames || isBranchRefType" |  | ||||||
|             @selected="selectRef($event)" |  | ||||||
|           /> |  | ||||||
| 
 |  | ||||||
|           <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" /> |  | ||||||
|         </template> |  | ||||||
| 
 |  | ||||||
|         <template v-if="showTagsSection"> |  | ||||||
|           <ref-results-section |  | ||||||
|             :section-title="i18n.tags" |  | ||||||
|             :total-count="matches.tags.totalCount" |  | ||||||
|             :items="matches.tags.list" |  | ||||||
|             :selected-ref="selectedRef" |  | ||||||
|             :error="matches.tags.error" |  | ||||||
|             :error-message="i18n.tagsErrorMessage" |  | ||||||
|             :show-header="showSectionHeaders" |  | ||||||
|             data-testid="tags-section" |  | ||||||
|             :should-show-check="!useSymbolicRefNames || isTagRefType" |  | ||||||
|             @selected="selectRef($event)" |  | ||||||
|           /> |  | ||||||
| 
 |  | ||||||
|           <gl-dropdown-divider v-if="showCommitsSection" /> |  | ||||||
|         </template> |  | ||||||
| 
 |  | ||||||
|         <template v-if="showCommitsSection"> |  | ||||||
|           <ref-results-section |  | ||||||
|             :section-title="i18n.commits" |  | ||||||
|             :total-count="matches.commits.totalCount" |  | ||||||
|             :items="matches.commits.list" |  | ||||||
|             :selected-ref="selectedRef" |  | ||||||
|             :error="matches.commits.error" |  | ||||||
|             :error-message="i18n.commitsErrorMessage" |  | ||||||
|             :show-header="showSectionHeaders" |  | ||||||
|             data-testid="commits-section" |  | ||||||
|             @selected="selectRef($event)" |  | ||||||
|           /> |  | ||||||
|         </template> |  | ||||||
|       </template> |       </template> | ||||||
| 
 |  | ||||||
|       <template #footer> |       <template #footer> | ||||||
|         <slot name="footer" v-bind="footerSlotProps"></slot> |         <slot name="footer" v-bind="footerSlotProps"></slot> | ||||||
|  |         <div | ||||||
|  |           v-for="errorMessage in errors" | ||||||
|  |           :key="errorMessage" | ||||||
|  |           data-testid="red-selector-error-list" | ||||||
|  |           class="gl-display-flex gl-align-items-flex-start gl-text-red-500 gl-mx-4 gl-my-3" | ||||||
|  |         > | ||||||
|  |           <gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" /> | ||||||
|  |           <span>{{ errorMessage }}</span> | ||||||
|  |         </div> | ||||||
|       </template> |       </template> | ||||||
|     </gl-dropdown> |     </gl-collapsible-listbox> | ||||||
|     <input |     <input | ||||||
|       v-if="name" |       v-if="name" | ||||||
|       data-testid="selected-ref-form-field" |       data-testid="selected-ref-form-field" | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; | import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; | ||||||
| import { __ } from '~/locale'; | import { s__, __ } from '~/locale'; | ||||||
| 
 | 
 | ||||||
| export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES'; | export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES'; | ||||||
| export const REF_TYPE_TAGS = 'REF_TYPE_TAGS'; | export const REF_TYPE_TAGS = 'REF_TYPE_TAGS'; | ||||||
|  | @ -13,6 +13,7 @@ export const X_TOTAL_HEADER = 'x-total'; | ||||||
| export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS; | export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS; | ||||||
| 
 | 
 | ||||||
| export const DEFAULT_I18N = Object.freeze({ | export const DEFAULT_I18N = Object.freeze({ | ||||||
|  |   defaultLabelText: __('default'), | ||||||
|   dropdownHeader: __('Select Git revision'), |   dropdownHeader: __('Select Git revision'), | ||||||
|   searchPlaceholder: __('Search by Git revision'), |   searchPlaceholder: __('Search by Git revision'), | ||||||
|   noResultsWithQuery: __('No matching results for "%{query}"'), |   noResultsWithQuery: __('No matching results for "%{query}"'), | ||||||
|  | @ -24,4 +25,5 @@ export const DEFAULT_I18N = Object.freeze({ | ||||||
|   tags: __('Tags'), |   tags: __('Tags'), | ||||||
|   commits: __('Commits'), |   commits: __('Commits'), | ||||||
|   noRefSelected: __('No ref selected'), |   noRefSelected: __('No ref selected'), | ||||||
|  |   totalCountLabel: s__('TotalRefCountIndicator|1000+'), | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  | }; | ||||||
|  | @ -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> | ||||||
|  | @ -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> | ||||||
|  | @ -173,6 +173,7 @@ export default { | ||||||
|           :can-edit="enableEdit" |           :can-edit="enableEdit" | ||||||
|           :task-list-update-path="taskListUpdatePath" |           :task-list-update-path="taskListUpdatePath" | ||||||
|         /> |         /> | ||||||
|  |         <slot name="secondary-content"></slot> | ||||||
|         <small v-if="isUpdated" class="edited-text gl-font-sm!"> |         <small v-if="isUpdated" class="edited-text gl-font-sm!"> | ||||||
|           {{ __('Edited') }} |           {{ __('Edited') }} | ||||||
|           <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> |           <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> | ||||||
|  |  | ||||||
|  | @ -142,7 +142,7 @@ module Types | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def ephemeral_authentication_token |       def ephemeral_authentication_token | ||||||
|         return unless runner.created_via_ui? |         return unless runner.authenticated_user_registration_type? | ||||||
|         return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago |         return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago | ||||||
|         return if runner.runner_machines.any? |         return if runner.runner_machines.any? | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,6 +30,11 @@ module Ci | ||||||
|       project_type: 3 |       project_type: 3 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     enum registration_type: { | ||||||
|  |       registration_token: 0, | ||||||
|  |       authenticated_user: 1 | ||||||
|  |     }, _suffix: true | ||||||
|  | 
 | ||||||
|     # Prefix assigned to runners created from the UI, instead of registered via the command line |     # Prefix assigned to runners created from the UI, instead of registered via the command line | ||||||
|     CREATED_RUNNER_TOKEN_PREFIX = 'glrt-' |     CREATED_RUNNER_TOKEN_PREFIX = 'glrt-' | ||||||
| 
 | 
 | ||||||
|  | @ -184,6 +189,7 @@ module Ci | ||||||
|     validate :tag_constraints |     validate :tag_constraints | ||||||
|     validates :access_level, presence: true |     validates :access_level, presence: true | ||||||
|     validates :runner_type, presence: true |     validates :runner_type, presence: true | ||||||
|  |     validates :registration_type, presence: true | ||||||
| 
 | 
 | ||||||
|     validate :no_projects, unless: :project_type? |     validate :no_projects, unless: :project_type? | ||||||
|     validate :no_groups, unless: :group_type? |     validate :no_groups, unless: :group_type? | ||||||
|  | @ -196,8 +202,6 @@ module Ci | ||||||
| 
 | 
 | ||||||
|     cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at, :executor_type |     cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at, :executor_type | ||||||
| 
 | 
 | ||||||
|     attr_writer :legacy_registered |  | ||||||
| 
 |  | ||||||
|     chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout, |     chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout, | ||||||
|         error_message: 'Maximum job timeout has a value which could not be accepted' |         error_message: 'Maximum job timeout has a value which could not be accepted' | ||||||
| 
 | 
 | ||||||
|  | @ -297,13 +301,6 @@ module Ci | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def initialize(params) |  | ||||||
|       @legacy_registered = params&.delete(:legacy_registered) |  | ||||||
|       @legacy_registered = true if @legacy_registered.nil? |  | ||||||
| 
 |  | ||||||
|       super(params) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def assign_to(project, current_user = nil) |     def assign_to(project, current_user = nil) | ||||||
|       if instance_type? |       if instance_type? | ||||||
|         raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported' |         raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported' | ||||||
|  | @ -389,7 +386,7 @@ module Ci | ||||||
|     def short_sha |     def short_sha | ||||||
|       return unless token |       return unless token | ||||||
| 
 | 
 | ||||||
|       start_index = created_via_ui? ? CREATED_RUNNER_TOKEN_PREFIX.length : 0 |       start_index = authenticated_user_registration_type? ? CREATED_RUNNER_TOKEN_PREFIX.length : 0 | ||||||
|       token[start_index..start_index + 8] |       token[start_index..start_index + 8] | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -493,15 +490,11 @@ module Ci | ||||||
| 
 | 
 | ||||||
|     override :format_token |     override :format_token | ||||||
|     def format_token(token) |     def format_token(token) | ||||||
|       return token if @legacy_registered |       return token if registration_token_registration_type? | ||||||
| 
 | 
 | ||||||
|       "#{CREATED_RUNNER_TOKEN_PREFIX}#{token}" |       "#{CREATED_RUNNER_TOKEN_PREFIX}#{token}" | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def created_via_ui? |  | ||||||
|       token.start_with?(CREATED_RUNNER_TOKEN_PREFIX) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def ensure_machine(system_xid, &blk) |     def ensure_machine(system_xid, &blk) | ||||||
|       RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods |       RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -20,10 +20,9 @@ | ||||||
|   - add_page_startup_api_call @endpoint_diff_batch_url |   - add_page_startup_api_call @endpoint_diff_batch_url | ||||||
| 
 | 
 | ||||||
| .merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } } | .merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } } | ||||||
|  |   = render "projects/merge_requests/mr_title" | ||||||
|   - if moved_mr_sidebar_enabled? |   - if moved_mr_sidebar_enabled? | ||||||
|     #js-merge-sticky-header{ data: { data: sticky_header_data.to_json } } |     #js-merge-sticky-header{ data: { data: sticky_header_data.to_json } } | ||||||
|   = render "projects/merge_requests/mr_title" |  | ||||||
| 
 |  | ||||||
|   .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } |   .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } | ||||||
|     = render "projects/merge_requests/mr_box" |     = render "projects/merge_requests/mr_box" | ||||||
|     .merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" } |     .merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" } | ||||||
|  |  | ||||||
|  | @ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388986 | ||||||
| milestone: '15.9' | milestone: '15.9' | ||||||
| type: development | type: development | ||||||
| group: group::source code | group: group::source code | ||||||
| default_enabled: false | default_enabled: true | ||||||
|  |  | ||||||
|  | @ -34,6 +34,11 @@ POST /projects/:id/product_analytics/request/dry-run | ||||||
| 
 | 
 | ||||||
| The body of the load request must be a valid Cube query. | The body of the load request must be a valid Cube query. | ||||||
| 
 | 
 | ||||||
|  | NOTE: | ||||||
|  | When measuring `TrackedEvents`, you must use `TrackedEvents.*` for `dimensions` and `timeDimensions`. The same rule applies when measuring `Sessions`. | ||||||
|  | 
 | ||||||
|  | #### Tracked events example | ||||||
|  | 
 | ||||||
| ```json | ```json | ||||||
| { | { | ||||||
|   "query": { |   "query": { | ||||||
|  | @ -69,6 +74,29 @@ The body of the load request must be a valid Cube query. | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | #### Sessions example | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "query": { | ||||||
|  |     "measures": [ | ||||||
|  |       "Sessions.count" | ||||||
|  |     ], | ||||||
|  |     "timeDimensions": [ | ||||||
|  |       { | ||||||
|  |         "dimension": "Sessions.startAt", | ||||||
|  |         "granularity": "day" | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     "order": { | ||||||
|  |       "Sessions.startAt": "asc" | ||||||
|  |     }, | ||||||
|  |     "limit": 100 | ||||||
|  |   }, | ||||||
|  |   "queryType": "multi" | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ## Send metadata request to Cube | ## Send metadata request to Cube | ||||||
| 
 | 
 | ||||||
| Return Cube Metadata for the Analytics data. For example: | Return Cube Metadata for the Analytics data. For example: | ||||||
|  |  | ||||||
|  | @ -210,7 +210,10 @@ When the user is authenticated and `simple` is not set this returns something li | ||||||
|     "group_runners_enabled": true, |     "group_runners_enabled": true, | ||||||
|     "lfs_enabled": true, |     "lfs_enabled": true, | ||||||
|     "creator_id": 1, |     "creator_id": 1, | ||||||
|  |     "import_url": null, | ||||||
|  |     "import_type": null, | ||||||
|     "import_status": "none", |     "import_status": "none", | ||||||
|  |     "import_error": null, | ||||||
|     "open_issues_count": 0, |     "open_issues_count": 0, | ||||||
|     "ci_default_git_depth": 20, |     "ci_default_git_depth": 20, | ||||||
|     "ci_forward_deployment_enabled": true, |     "ci_forward_deployment_enabled": true, | ||||||
|  | @ -381,6 +384,10 @@ GET /users/:user_id/projects | ||||||
|     "created_at": "2013-09-30T13:46:02Z", |     "created_at": "2013-09-30T13:46:02Z", | ||||||
|     "last_activity_at": "2013-09-30T13:46:02Z", |     "last_activity_at": "2013-09-30T13:46:02Z", | ||||||
|     "creator_id": 3, |     "creator_id": 3, | ||||||
|  |     "import_url": null, | ||||||
|  |     "import_type": null, | ||||||
|  |     "import_status": "none", | ||||||
|  |     "import_error": null, | ||||||
|     "namespace": { |     "namespace": { | ||||||
|       "id": 3, |       "id": 3, | ||||||
|       "name": "Diaspora", |       "name": "Diaspora", | ||||||
|  | @ -482,6 +489,10 @@ GET /users/:user_id/projects | ||||||
|     "created_at": "2013-09-30T13:46:02Z", |     "created_at": "2013-09-30T13:46:02Z", | ||||||
|     "last_activity_at": "2013-09-30T13:46:02Z", |     "last_activity_at": "2013-09-30T13:46:02Z", | ||||||
|     "creator_id": 3, |     "creator_id": 3, | ||||||
|  |     "import_url": null, | ||||||
|  |     "import_type": null, | ||||||
|  |     "import_status": "none", | ||||||
|  |     "import_error": null, | ||||||
|     "namespace": { |     "namespace": { | ||||||
|       "id": 4, |       "id": 4, | ||||||
|       "name": "Brightbox", |       "name": "Brightbox", | ||||||
|  | @ -898,6 +909,8 @@ GET /projects/:id | ||||||
|     "avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg", |     "avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg", | ||||||
|     "web_url": "http://localhost:3000/groups/diaspora" |     "web_url": "http://localhost:3000/groups/diaspora" | ||||||
|   }, |   }, | ||||||
|  |   "import_url": null, | ||||||
|  |   "import_type": null, | ||||||
|   "import_status": "none", |   "import_status": "none", | ||||||
|   "import_error": null, |   "import_error": null, | ||||||
|   "permissions": { |   "permissions": { | ||||||
|  |  | ||||||
|  | @ -421,6 +421,92 @@ scope. | ||||||
| | GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. | | | GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. | | ||||||
| | GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. | | | GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. | | ||||||
| 
 | 
 | ||||||
|  | ## FAQ | ||||||
|  | 
 | ||||||
|  | ### Will my runner registration workflow break? | ||||||
|  | 
 | ||||||
|  | If no action is taken before your GitLab instance is upgraded to 16.0, then your runner registration | ||||||
|  | worflow will break. | ||||||
|  | For self-managed instances, to continue using the previous runner registration process, | ||||||
|  | you can disable the `enforce_create_runner_workflow` feature flag until GitLab 17.0. | ||||||
|  | 
 | ||||||
|  | To avoid a broken workflow, you need to first create a runner in the GitLab runners admin page. | ||||||
|  | After that, you'll need to replace the registration token you're using in your runner registration | ||||||
|  | workflow with the obtained runner authentication token. | ||||||
|  | 
 | ||||||
|  | ### What is the new runner registration process? | ||||||
|  | 
 | ||||||
|  | When the new runner registration process is introduced, you will: | ||||||
|  | 
 | ||||||
|  | 1. Create a runner directly in the GitLab UI. | ||||||
|  | 1. Receive an authentication token in return. | ||||||
|  | 1. Use the authentication token instead of the registration token. | ||||||
|  | 
 | ||||||
|  | This has added benefits such as preserved ownership records for runners, and minimizes | ||||||
|  | impact on users. | ||||||
|  | The addition of a unique system ID ensures that you can reuse the same authentication token across | ||||||
|  | multiple runners. | ||||||
|  | For example, in an auto-scaling scenario where a runner manager spawns a runner process with a | ||||||
|  | fixed authentication token. | ||||||
|  | This ID generates once at the runner's startup, persists in a sidecar file, and is sent to the | ||||||
|  | GitLab instance when requesting jobs. | ||||||
|  | This allows the GitLab instance to display which system executed a given job. | ||||||
|  | 
 | ||||||
|  | ### What is the estimated timeframe for the planned changes? | ||||||
|  | 
 | ||||||
|  | - In GitLab 15.10, we plan to implement runner creation directly in the runners administration page, | ||||||
|  |   and prepare the runner to follow the new workflow. | ||||||
|  | - In GitLab 16.0, we plan to disable registration tokens. | ||||||
|  |   For self-managed instances, to continue using | ||||||
|  |   registration tokens, you can disable the `enforce_create_runner_workflow` feature flag until | ||||||
|  |   GitLab 17.0. | ||||||
|  | 
 | ||||||
|  |   Previous `gitlab-runner` versions (that don't include the new `system_id` value) will start to be | ||||||
|  |   rejected by the GitLab instance; | ||||||
|  | - In GitLab 17.0, we plan to completely remove support for runner registration tokens. | ||||||
|  | 
 | ||||||
|  | ### How will the `gitlab-runner register` command syntax change? | ||||||
|  | 
 | ||||||
|  | The `gitlab-runner register` command will stop accepting registration tokens and instead accept new | ||||||
|  | authentication tokens generated in the GitLab runners administration page. | ||||||
|  | These authentication tokens are recognizable by their `glrt-` prefix. | ||||||
|  | 
 | ||||||
|  | Example command for GitLab 15.9: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | gitlab-runner register | ||||||
|  |     --executor "shell" \ | ||||||
|  |     --url "https://gitlab.com/" \ | ||||||
|  |     --tag-list "shell,mac,gdk,test" \ | ||||||
|  |     --run-untagged="false" \ | ||||||
|  |     --locked="false" \ | ||||||
|  |     --access-level="not_protected" \ | ||||||
|  |     --non-interactive \ | ||||||
|  |     --registration-token="GR1348941C6YcZVddc8kjtdU-yWYD" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | In GitLab 16.0, the runner will be created in the UI where some of its attributes can be | ||||||
|  | pre-configured by the creator. | ||||||
|  | Examples are the tag list, locked status, or access level. These are no longer accepted as arguments | ||||||
|  | to `register`. The following example shows the new command: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | gitlab-runner register | ||||||
|  |     --executor "shell" \ | ||||||
|  |     --url "https://gitlab.com/" \ | ||||||
|  |     --non-interactive \ | ||||||
|  |     --registration-token="grlt-2CR8_eVxiioB1QmzPZwa" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### How does this change impact auto-scaling scenarios? | ||||||
|  | 
 | ||||||
|  | In auto-scaling scenarios such as GitLab Runner Operator or GitLab Runner Helm Chart, the | ||||||
|  | registration token is replaced with the authentication token generated from the UI. | ||||||
|  | This means that the same runner configuration is reused across jobs, instead of creating a runner | ||||||
|  | for each job. | ||||||
|  | The specific runner can be identified by the unique system ID that is generated when the runner | ||||||
|  | process is started. | ||||||
|  | 
 | ||||||
| ## Status | ## Status | ||||||
| 
 | 
 | ||||||
| Status: RFC. | Status: RFC. | ||||||
|  |  | ||||||
|  | @ -124,39 +124,41 @@ deploy_staging: | ||||||
| 
 | 
 | ||||||
| ### Create a dynamic environment | ### Create a dynamic environment | ||||||
| 
 | 
 | ||||||
| To create a dynamic name and URL for an environment, you can use | To create a dynamic environment, you use [CI/CD variables](../variables/index.md) that are unique to each pipeline. | ||||||
| [predefined CI/CD variables](../variables/predefined_variables.md). For example: | 
 | ||||||
|  | Prerequisites: | ||||||
|  | 
 | ||||||
|  | - You must have at least the Developer role. | ||||||
|  | 
 | ||||||
|  | To create a dynamic environment, in your `.gitlab-ci.yml` file: | ||||||
|  | 
 | ||||||
|  | 1. Define a job in the `deploy` stage. | ||||||
|  | 1. In the job, define the following environment attributes: | ||||||
|  |    - `name`: Use a related CI/CD variable like `$CI_COMMIT_REF_SLUG`. Optionally, add a static | ||||||
|  |      prefix to the environment's name, which [groups in the UI](#group-similar-environments) all | ||||||
|  |      environments with the same prefix. | ||||||
|  |    - `url`: Optional. Prefix the hostname with a related CI/CD variable like `$CI_ENVIRONMENT_SLUG`. | ||||||
|  | 
 | ||||||
|  | NOTE: | ||||||
|  | Some characters cannot be used in environment names. For more information about the | ||||||
|  | `environment` keywords, see the [`.gitlab-ci.yml` keyword reference](../yaml/index.md#environment). | ||||||
|  | 
 | ||||||
|  | In the following example, every time the `deploy_review_app` job runs the environment's name and | ||||||
|  | URL are defined using unique values. | ||||||
| 
 | 
 | ||||||
| ```yaml | ```yaml | ||||||
| deploy_review: | deploy_review_app: | ||||||
|   stage: deploy |   stage: deploy | ||||||
|   script: |   script: make deploy | ||||||
|     - echo "Deploy a review app" |  | ||||||
|   environment: |   environment: | ||||||
|     name: review/$CI_COMMIT_REF_SLUG |     name: review/$CI_COMMIT_REF_SLUG | ||||||
|     url: https://$CI_ENVIRONMENT_SLUG.example.com |     url: https://$CI_ENVIRONMENT_SLUG.example.com | ||||||
|   rules: |   only: | ||||||
|     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH |     - branches | ||||||
|       when: never |   except: | ||||||
|     - if: $CI_COMMIT_BRANCH |     - main | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| In this example: |  | ||||||
| 
 |  | ||||||
| - The `name` is `review/$CI_COMMIT_REF_SLUG`. Because the [environment name](../yaml/index.md#environmentname) |  | ||||||
|   can contain slashes (`/`), you can use this pattern to distinguish between dynamic and static environments. |  | ||||||
| - For the `url`, you could use `$CI_COMMIT_REF_SLUG`, but because this value |  | ||||||
|   may contain a `/` or other characters that would not be valid in a domain name or URL, |  | ||||||
|   use `$CI_ENVIRONMENT_SLUG` instead. The `$CI_ENVIRONMENT_SLUG` variable is guaranteed to be unique. |  | ||||||
| 
 |  | ||||||
| You do not have to use the same prefix or only slashes (`/`) in the dynamic environment name. |  | ||||||
| However, when you use this format, you can [group similar environments](#group-similar-environments). |  | ||||||
| 
 |  | ||||||
| NOTE: |  | ||||||
| Some variables cannot be used as environment names or URLs. |  | ||||||
| For more information about the `environment` keywords, see |  | ||||||
| [the `.gitlab-ci.yml` keyword reference](../yaml/index.md#environment). |  | ||||||
| 
 |  | ||||||
| ## Deployment tier of environments | ## Deployment tier of environments | ||||||
| 
 | 
 | ||||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10. | > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10. | ||||||
|  |  | ||||||
|  | @ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w | ||||||
| 
 | 
 | ||||||
| This document describes the conventions used at GitLab for writing End-to-end (E2E) tests using the GitLab QA project. | This document describes the conventions used at GitLab for writing End-to-end (E2E) tests using the GitLab QA project. | ||||||
| 
 | 
 | ||||||
|  | Please note that this guide is an extension of the primary [testing standards and style guidelines](../index.md). If this guide defines a rule that contradicts the primary guide, this guide takes precedence. | ||||||
|  | 
 | ||||||
| ## `click_` versus `go_to_` | ## `click_` versus `go_to_` | ||||||
| 
 | 
 | ||||||
| ### When to use `click_`? | ### When to use `click_`? | ||||||
|  |  | ||||||
|  | @ -165,3 +165,9 @@ this setting. However, disabling the Container Registry disables all Container R | ||||||
| | Private project with Container Registry visibility <br/> set to **Everyone With Access** (UI) or `enabled` (API)  | View Container Registry <br/> and pull images | No                                   | No    | Yes                                    | | | Private project with Container Registry visibility <br/> set to **Everyone With Access** (UI) or `enabled` (API)  | View Container Registry <br/> and pull images | No                                   | No    | Yes                                    | | ||||||
| | Private project with Container Registry visibility <br/> set to **Only Project Members** (UI) or `private` (API)  | View Container Registry <br/> and pull images | No                                   | No    | Yes                                    | | | Private project with Container Registry visibility <br/> set to **Only Project Members** (UI) or `private` (API)  | View Container Registry <br/> and pull images | No                                   | No    | Yes                                    | | ||||||
| | Any project with Container Registry `disabled`                                                                    | All operations on Container Registry          | No                                   | No    | No                                     | | | Any project with Container Registry `disabled`                                                                    | All operations on Container Registry          | No                                   | No    | No                                     | | ||||||
|  | 
 | ||||||
|  | ## Supported image types | ||||||
|  | 
 | ||||||
|  | The Container Registry supports [Docker V2](https://docs.docker.com/registry/spec/manifest-v2-2/) and [Open Container Initiative (OCI)](https://github.com/opencontainers/image-spec/blob/main/spec.md) image formats. | ||||||
|  | 
 | ||||||
|  | OCI support means that you can host OCI-based image formats in the registry, such as [Helm 3+ chart packages](https://helm.sh/docs/topics/registries/). There is no distinction between image formats in the GitLab [API](../../../api/container_registry.md) and the UI. [Issue 38047](https://gitlab.com/gitlab-org/gitlab/-/issues/38047) addresses this distinction, starting with Helm. | ||||||
|  |  | ||||||
|  | @ -608,7 +608,7 @@ module API | ||||||
|       if file.file_storage? |       if file.file_storage? | ||||||
|         present_disk_file!(file.path, file.filename) |         present_disk_file!(file.path, file.filename) | ||||||
|       elsif supports_direct_download && file.class.direct_download_enabled? |       elsif supports_direct_download && file.class.direct_download_enabled? | ||||||
|         return redirect(signed_head_url(file)) if head_request_on_aws_file?(file) |         return redirect(ObjectStorage::S3.signed_head_url(file)) if request.head? && file.fog_credentials[:provider] == 'AWS' | ||||||
| 
 | 
 | ||||||
|         redirect(cdn_fronted_url(file)) |         redirect(cdn_fronted_url(file)) | ||||||
|       else |       else | ||||||
|  | @ -701,19 +701,6 @@ module API | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def head_request_on_aws_file?(file) |  | ||||||
|       request.head? && file.fog_credentials[:provider] == 'AWS' |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def signed_head_url(file) |  | ||||||
|       fog_storage = ::Fog::Storage.new(file.fog_credentials) |  | ||||||
|       fog_dir = fog_storage.directories.new(key: file.fog_directory) |  | ||||||
|       fog_file = fog_dir.files.new(key: file.path) |  | ||||||
|       expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration |  | ||||||
| 
 |  | ||||||
|       fog_file.collection.head_url(fog_file.key, expire_at) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     # rubocop:disable Gitlab/ModuleWithInstanceVariables |     # rubocop:disable Gitlab/ModuleWithInstanceVariables | ||||||
|     def initial_current_user |     def initial_current_user | ||||||
|       return @initial_current_user if defined?(@initial_current_user) |       return @initial_current_user if defined?(@initial_current_user) | ||||||
|  |  | ||||||
|  | @ -47,8 +47,15 @@ module Gitlab | ||||||
|       # cached settings hash to build the payload cache key to be invalidated. |       # cached settings hash to build the payload cache key to be invalidated. | ||||||
|       def clear_cache |       def clear_cache | ||||||
|         keys = cached_settings_hashes |         keys = cached_settings_hashes | ||||||
|           .map { |hash| payload_cache_key_for(hash) } |          .map { |hash| payload_cache_key_for(hash) } | ||||||
|           .push(settings_cache_key) |          .push(settings_cache_key) | ||||||
|  | 
 | ||||||
|  |         ::Gitlab::AppLogger.info( | ||||||
|  |           message: 'clear pages cache', | ||||||
|  |           keys: keys, | ||||||
|  |           type: @type, | ||||||
|  |           id: @id | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do |         Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do | ||||||
|           Rails.cache.delete_multi(keys) |           Rails.cache.delete_multi(keys) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| require 'set' |  | ||||||
| 
 | 
 | ||||||
| namespace :gitlab do | namespace :gitlab do | ||||||
|  |   require 'set' | ||||||
|  | 
 | ||||||
|   namespace :cleanup do |   namespace :cleanup do | ||||||
|     desc "GitLab | Cleanup | Block users that have been removed in LDAP" |     desc "GitLab | Cleanup | Block users that have been removed in LDAP" | ||||||
|     task block_removed_ldap_users: :gitlab_environment do |     task block_removed_ldap_users: :gitlab_environment do | ||||||
|  |  | ||||||
|  | @ -426,6 +426,11 @@ msgid_plural "%d unresolved threads" | ||||||
| msgstr[0] "" | msgstr[0] "" | ||||||
| msgstr[1] "" | msgstr[1] "" | ||||||
| 
 | 
 | ||||||
|  | msgid "%d version" | ||||||
|  | msgid_plural "%d versions" | ||||||
|  | msgstr[0] "" | ||||||
|  | msgstr[1] "" | ||||||
|  | 
 | ||||||
| msgid "%d vulnerability" | msgid "%d vulnerability" | ||||||
| msgid_plural "%d vulnerabilities" | msgid_plural "%d vulnerabilities" | ||||||
| msgstr[0] "" | msgstr[0] "" | ||||||
|  | @ -36258,15 +36263,21 @@ msgstr "" | ||||||
| msgid "Requirement %{reference} has been updated" | msgid "Requirement %{reference} has been updated" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "Requirement title cannot have more than %{limit} characters." |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "Requirements" | msgid "Requirements" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture." | msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Requirement|Legacy requirement ID: %{legacyId}" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "Requirement|Legacy requirement IDs are being deprecated. Update your links to reference this item's new ID %{id}. %{linkStart}Learn more%{linkEnd}." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "Requirement|Requirements have become work items and the legacy requirement IDs are being deprecated. Update your links to reference this item's new ID %{id}. %{linkStart}Learn more%{linkEnd}." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Requires %d approval from eligible users." | msgid "Requires %d approval from eligible users." | ||||||
| msgid_plural "Requires %d approvals from eligible users." | msgid_plural "Requires %d approvals from eligible users." | ||||||
| msgstr[0] "" | msgstr[0] "" | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ module QA | ||||||
|     module Project |     module Project | ||||||
|       module Settings |       module Settings | ||||||
|         class DefaultBranch < Page::Base |         class DefaultBranch < Page::Base | ||||||
|  |           include ::QA::Page::Component::Dropdown | ||||||
|  | 
 | ||||||
|           view 'app/views/projects/branch_defaults/_show.html.haml' do |           view 'app/views/projects/branch_defaults/_show.html.haml' do | ||||||
|             element :save_changes_button |             element :save_changes_button | ||||||
|           end |           end | ||||||
|  | @ -13,14 +15,9 @@ module QA | ||||||
|             element :default_branch_dropdown |             element :default_branch_dropdown | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           view 'app/assets/javascripts/ref/components/ref_selector.vue' do |  | ||||||
|             element :ref_selector_searchbox |  | ||||||
|           end |  | ||||||
| 
 |  | ||||||
|           def set_default_branch(branch) |           def set_default_branch(branch) | ||||||
|             find_element(:default_branch_dropdown, visible: false).click |             expand_select_list | ||||||
|             find_element(:ref_selector_searchbox, visible: false).fill_in(with: branch) |             search_and_select(branch) | ||||||
|             click_button branch |  | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def click_save_changes_button |           def click_save_changes_button | ||||||
|  |  | ||||||
|  | @ -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 |  | ||||||
|  | @ -58,10 +58,6 @@ FactoryBot.define do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     trait :created_via_ui do |  | ||||||
|       legacy_registered { false } |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     trait :without_projects do |     trait :without_projects do | ||||||
|       # we use that to create invalid runner: |       # we use that to create invalid runner: | ||||||
|       # the one without projects |       # the one without projects | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do | RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do | ||||||
|  |   include ListboxHelpers | ||||||
|  | 
 | ||||||
|   let(:user) { create(:user) } |   let(:user) { create(:user) } | ||||||
|   let(:project) { create(:project, :public, :repository) } |   let(:project) { create(:project, :public, :repository) } | ||||||
|   let(:sha) { project.commit.sha } |   let(:sha) { project.commit.sha } | ||||||
|  | @ -18,15 +20,13 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do | ||||||
|   it 'finds a tag in a list' do |   it 'finds a tag in a list' do | ||||||
|     tag_name = 'v1.0.0' |     tag_name = 'v1.0.0' | ||||||
| 
 | 
 | ||||||
|     toggle.click |  | ||||||
| 
 |  | ||||||
|     filter_by(tag_name) |     filter_by(tag_name) | ||||||
| 
 | 
 | ||||||
|     wait_for_requests |     wait_for_requests | ||||||
| 
 | 
 | ||||||
|     expect(items_count(tag_name)).to be(1) |     expect(items_count(tag_name)).to be(1) | ||||||
| 
 | 
 | ||||||
|     item(tag_name).click |     select_listbox_item tag_name | ||||||
| 
 | 
 | ||||||
|     expect(toggle).to have_content tag_name |     expect(toggle).to have_content tag_name | ||||||
|   end |   end | ||||||
|  | @ -34,22 +34,18 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do | ||||||
|   it 'finds a branch in a list' do |   it 'finds a branch in a list' do | ||||||
|     branch_name = 'audio' |     branch_name = 'audio' | ||||||
| 
 | 
 | ||||||
|     toggle.click |  | ||||||
| 
 |  | ||||||
|     filter_by(branch_name) |     filter_by(branch_name) | ||||||
| 
 | 
 | ||||||
|     wait_for_requests |     wait_for_requests | ||||||
| 
 | 
 | ||||||
|     expect(items_count(branch_name)).to be(1) |     expect(items_count(branch_name)).to be(1) | ||||||
| 
 | 
 | ||||||
|     item(branch_name).click |     select_listbox_item branch_name | ||||||
| 
 | 
 | ||||||
|     expect(toggle).to have_content branch_name |     expect(toggle).to have_content branch_name | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   it 'finds a commit in a list' do |   it 'finds a commit in a list' do | ||||||
|     toggle.click |  | ||||||
| 
 |  | ||||||
|     filter_by(sha) |     filter_by(sha) | ||||||
| 
 | 
 | ||||||
|     wait_for_requests |     wait_for_requests | ||||||
|  | @ -58,21 +54,19 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do | ||||||
| 
 | 
 | ||||||
|     expect(items_count(sha_short)).to be(1) |     expect(items_count(sha_short)).to be(1) | ||||||
| 
 | 
 | ||||||
|     item(sha_short).click |     select_listbox_item sha_short | ||||||
| 
 | 
 | ||||||
|     expect(toggle).to have_content sha_short |     expect(toggle).to have_content sha_short | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   it 'shows no results when there is no branch, tag or commit sha found' do |   it 'shows no results when there is no branch, tag or commit sha found' do | ||||||
|     non_existing_ref = 'non_existing_branch_name' |     non_existing_ref = 'non_existing_branch_name' | ||||||
| 
 |  | ||||||
|     toggle.click |  | ||||||
| 
 |  | ||||||
|     filter_by(non_existing_ref) |     filter_by(non_existing_ref) | ||||||
| 
 | 
 | ||||||
|     wait_for_requests |     wait_for_requests | ||||||
| 
 | 
 | ||||||
|     expect(find('.gl-dropdown-contents')).not_to have_content(non_existing_ref) |     click_button 'master' | ||||||
|  |     expect(toggle).not_to have_content(non_existing_ref) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def item(ref_name) |   def item(ref_name) | ||||||
|  | @ -84,6 +78,7 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def filter_by(filter_text) |   def filter_by(filter_text) | ||||||
|     fill_in _('Search by Git revision'), with: filter_text |     click_button 'master' | ||||||
|  |     send_keys filter_text | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ require "spec_helper" | ||||||
| 
 | 
 | ||||||
| RSpec.describe "User browses files", :js, feature_category: :projects do | RSpec.describe "User browses files", :js, feature_category: :projects do | ||||||
|   include RepoHelpers |   include RepoHelpers | ||||||
|  |   include ListboxHelpers | ||||||
| 
 | 
 | ||||||
|   let(:fork_message) do |   let(:fork_message) do | ||||||
|     "You're not allowed to make changes to this project directly. "\ |     "You're not allowed to make changes to this project directly. "\ | ||||||
|  | @ -282,17 +283,13 @@ RSpec.describe "User browses files", :js, feature_category: :projects do | ||||||
|       expect(page).to have_content(".gitignore").and have_content("LICENSE") |       expect(page).to have_content(".gitignore").and have_content("LICENSE") | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it "shows files from a repository with apostroph in its name" do |     it "shows files from a repository with apostrophe in its name" do | ||||||
|       ref_name = 'test' |       ref_name = 'fix' | ||||||
| 
 | 
 | ||||||
|       find(ref_selector).click |       find(ref_selector).click | ||||||
|       wait_for_requests |       wait_for_requests | ||||||
| 
 | 
 | ||||||
|       page.within(ref_selector) do |       filter_by(ref_name) | ||||||
|         fill_in 'Search by Git revision', with: ref_name |  | ||||||
|         wait_for_requests |  | ||||||
|         find('li', text: ref_name, match: :prefer_exact).click |  | ||||||
|       end |  | ||||||
| 
 | 
 | ||||||
|       expect(find(ref_selector)).to have_text(ref_name) |       expect(find(ref_selector)).to have_text(ref_name) | ||||||
| 
 | 
 | ||||||
|  | @ -307,11 +304,7 @@ RSpec.describe "User browses files", :js, feature_category: :projects do | ||||||
|       find(ref_selector).click |       find(ref_selector).click | ||||||
|       wait_for_requests |       wait_for_requests | ||||||
| 
 | 
 | ||||||
|       page.within(ref_selector) do |       filter_by(ref_name) | ||||||
|         fill_in 'Search by Git revision', with: ref_name |  | ||||||
|         wait_for_requests |  | ||||||
|         find('li', text: ref_name, match: :prefer_exact).click |  | ||||||
|       end |  | ||||||
| 
 | 
 | ||||||
|       visit(project_tree_path(project, "fix/.testdir")) |       visit(project_tree_path(project, "fix/.testdir")) | ||||||
| 
 | 
 | ||||||
|  | @ -394,4 +387,12 @@ RSpec.describe "User browses files", :js, feature_category: :projects do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def filter_by(filter_text) | ||||||
|  |     send_keys filter_text | ||||||
|  | 
 | ||||||
|  |     wait_for_requests | ||||||
|  | 
 | ||||||
|  |     select_listbox_item filter_text | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'User find project file', feature_category: :projects do | RSpec.describe 'User find project file', feature_category: :projects do | ||||||
|  |   include ListboxHelpers | ||||||
|  | 
 | ||||||
|   let(:user)    { create :user } |   let(:user)    { create :user } | ||||||
|   let(:project) { create :project, :repository } |   let(:project) { create :project, :repository } | ||||||
| 
 | 
 | ||||||
|  | @ -22,7 +24,7 @@ RSpec.describe 'User find project file', feature_category: :projects do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def ref_selector_dropdown |   def ref_selector_dropdown | ||||||
|     find('.gl-dropdown-toggle > .gl-dropdown-button-text') |     find('.gl-button-text') | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   it 'navigates to find file by shortcut', :js do |   it 'navigates to find file by shortcut', :js do | ||||||
|  | @ -99,7 +101,7 @@ RSpec.describe 'User find project file', feature_category: :projects do | ||||||
|         fill_in _('Switch branch/tag'), with: ref |         fill_in _('Switch branch/tag'), with: ref | ||||||
|         wait_for_requests |         wait_for_requests | ||||||
| 
 | 
 | ||||||
|         find('.gl-dropdown-item', text: ref).click |         select_listbox_item(ref) | ||||||
|       end |       end | ||||||
|       expect(ref_selector_dropdown).to have_text(ref) |       expect(ref_selector_dropdown).to have_text(ref) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do | ||||||
| 
 | 
 | ||||||
|     it 'HTML escapes branch name' do |     it 'HTML escapes branch name' do | ||||||
|       expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>") |       expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>") | ||||||
|       expect(page.find('.gl-dropdown-button-text')['innerHTML']).to eq(ERB::Util.html_escape(branch_name)) |       expect(page.find('.gl-new-dropdown-button-text')['innerHTML']).to include(ERB::Util.html_escape(branch_name)) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -69,18 +69,18 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do | ||||||
|       visit charts_project_graph_path(project, 'master') |       visit charts_project_graph_path(project, 'master') | ||||||
| 
 | 
 | ||||||
|       # Not a huge fan of using a HTML (CSS) selectors here as any change of them will cause a failed test |       # Not a huge fan of using a HTML (CSS) selectors here as any change of them will cause a failed test | ||||||
|       ref_selector = find('.ref-selector .gl-dropdown-toggle') |       ref_selector = find('.ref-selector .gl-new-dropdown-toggle') | ||||||
|       scroll_to(ref_selector) |       scroll_to(ref_selector) | ||||||
|       ref_selector.click |       ref_selector.click | ||||||
| 
 | 
 | ||||||
|       page.within '[data-testid="branches-section"]' do |       page.within '.gl-new-dropdown-contents' do | ||||||
|         dropdown_branch_item = find('.gl-dropdown-item', text: 'add-pdf-file') |         dropdown_branch_item = find('li', text: 'add-pdf-file') | ||||||
|         scroll_to(dropdown_branch_item) |         scroll_to(dropdown_branch_item) | ||||||
|         dropdown_branch_item.click |         dropdown_branch_item.click | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       scroll_to(find('.tree-ref-header'), align: :center) |       scroll_to(find('.tree-ref-header'), align: :center) | ||||||
|       expect(page).to have_selector '.gl-dropdown-toggle', text: ref_name |       expect(page).to have_selector '.gl-new-dropdown-toggle', text: ref_name | ||||||
|       page.within '.tree-ref-header' do |       page.within '.tree-ref-header' do | ||||||
|         expect(page).to have_selector('h4', text: ref_name) |         expect(page).to have_selector('h4', text: ref_name) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -64,7 +64,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do | ||||||
| 
 | 
 | ||||||
|         it 'shows the pipeline schedule with default ref' do |         it 'shows the pipeline schedule with default ref' do | ||||||
|           page.within('[data-testid="schedule-target-ref"]') do |           page.within('[data-testid="schedule-target-ref"]') do | ||||||
|             expect(first('.gl-dropdown-button-text').text).to eq('master') |             expect(first('.gl-button-text').text).to eq('master') | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | @ -77,7 +77,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do | ||||||
| 
 | 
 | ||||||
|         it 'shows the pipeline schedule with default ref' do |         it 'shows the pipeline schedule with default ref' do | ||||||
|           page.within('[data-testid="schedule-target-ref"]') do |           page.within('[data-testid="schedule-target-ref"]') do | ||||||
|             expect(first('.gl-dropdown-button-text').text).to eq('master') |             expect(first('.gl-button-text').text).to eq('master') | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | @ -319,7 +319,6 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def select_target_branch |   def select_target_branch | ||||||
|     find('[data-testid="schedule-target-ref"] .dropdown-toggle').click |  | ||||||
|     click_button 'master' |     click_button 'master' | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do | RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do | ||||||
|  |   include ListboxHelpers | ||||||
|  | 
 | ||||||
|   let(:user) { create(:user) } |   let(:user) { create(:user) } | ||||||
| 
 | 
 | ||||||
|   before do |   before do | ||||||
|  | @ -20,10 +22,10 @@ RSpec.describe 'Projects > Settings > User changes default branch', feature_cate | ||||||
|       wait_for_requests |       wait_for_requests | ||||||
| 
 | 
 | ||||||
|       expect(page).to have_selector(dropdown_selector) |       expect(page).to have_selector(dropdown_selector) | ||||||
|       find(dropdown_selector).click |       click_button 'master' | ||||||
|  |       send_keys 'fix' | ||||||
| 
 | 
 | ||||||
|       fill_in 'Search branch', with: 'fix' |       select_listbox_item 'fix' | ||||||
|       click_button 'fix' |  | ||||||
| 
 | 
 | ||||||
|       page.within '#branch-defaults-settings' do |       page.within '#branch-defaults-settings' do | ||||||
|         click_button 'Save changes' |         click_button 'Save changes' | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ require 'spec_helper' | ||||||
| RSpec.describe 'Projects tree', :js, feature_category: :web_ide do | RSpec.describe 'Projects tree', :js, feature_category: :web_ide do | ||||||
|   include WebIdeSpecHelpers |   include WebIdeSpecHelpers | ||||||
|   include RepoHelpers |   include RepoHelpers | ||||||
|  |   include ListboxHelpers | ||||||
| 
 | 
 | ||||||
|   let(:user) { create(:user) } |   let(:user) { create(:user) } | ||||||
|   let(:project) { create(:project, :repository) } |   let(:project) { create(:project, :repository) } | ||||||
|  | @ -160,17 +161,13 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do | ||||||
|   context 'ref switcher', :js do |   context 'ref switcher', :js do | ||||||
|     it 'switches ref to branch' do |     it 'switches ref to branch' do | ||||||
|       ref_selector = '.ref-selector' |       ref_selector = '.ref-selector' | ||||||
|       ref_name = 'feature' |       ref_name = 'fix' | ||||||
|       visit project_tree_path(project, 'master') |       visit project_tree_path(project, 'master') | ||||||
| 
 | 
 | ||||||
|       find(ref_selector).click |       click_button 'master' | ||||||
|       wait_for_requests |       send_keys ref_name | ||||||
| 
 | 
 | ||||||
|       page.within(ref_selector) do |       select_listbox_item ref_name | ||||||
|         fill_in 'Search by Git revision', with: ref_name |  | ||||||
|         wait_for_requests |  | ||||||
|         find('li', text: ref_name, match: :prefer_exact).click |  | ||||||
|       end |  | ||||||
| 
 | 
 | ||||||
|       expect(find(ref_selector)).to have_text(ref_name) |       expect(find(ref_selector)).to have_text(ref_name) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -3,6 +3,9 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do | RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do | ||||||
|  |   using RSpec::Parameterized::TableSyntax | ||||||
|  |   include ListboxHelpers | ||||||
|  | 
 | ||||||
|   let_it_be(:user) { create(:user) } |   let_it_be(:user) { create(:user) } | ||||||
|   let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) } |   let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) } | ||||||
| 
 | 
 | ||||||
|  | @ -83,14 +86,10 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat | ||||||
| 
 | 
 | ||||||
|         expect(page).to have_selector('.results', text: expected_result) |         expect(page).to have_selector('.results', text: expected_result) | ||||||
| 
 | 
 | ||||||
|         find('.ref-selector').click |         click_button 'master' | ||||||
|         wait_for_requests |         wait_for_requests | ||||||
| 
 | 
 | ||||||
|         page.within('.ref-selector') do |         select_listbox_item(ref_selector) | ||||||
|           fill_in 'Search by Git revision', with: ref_selector |  | ||||||
|           wait_for_requests |  | ||||||
|           find('li', text: ref_selector, match: :prefer_exact).click |  | ||||||
|         end |  | ||||||
| 
 | 
 | ||||||
|         expect(page).to have_selector('.results', text: expected_result) |         expect(page).to have_selector('.results', text: expected_result) | ||||||
| 
 | 
 | ||||||
|  | @ -137,18 +136,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'search result changes when refs switched' do |         it 'search result changes when refs switched' do | ||||||
|           ref = 'master' |  | ||||||
|           expect(find('.results')).not_to have_content('path = gitlab-grack') |           expect(find('.results')).not_to have_content('path = gitlab-grack') | ||||||
| 
 | 
 | ||||||
|           find('.ref-selector').click |           find('.ref-selector').click | ||||||
|           wait_for_requests |           wait_for_requests | ||||||
| 
 | 
 | ||||||
|           page.within('.ref-selector') do |           select_listbox_item('add-ipython-files') | ||||||
|             fill_in _('Search by Git revision'), with: ref |  | ||||||
|             wait_for_requests |  | ||||||
| 
 |  | ||||||
|             find('li', text: ref).click |  | ||||||
|           end |  | ||||||
| 
 | 
 | ||||||
|           expect(page).to have_selector('.results', text: 'path = gitlab-grack') |           expect(page).to have_selector('.results', text: 'path = gitlab-grack') | ||||||
|         end |         end | ||||||
|  | @ -192,18 +185,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'search result changes when refs switched' do |         it 'search result changes when refs switched' do | ||||||
|           ref = 'master' |  | ||||||
|           expect(find('.results')).not_to have_content('path = gitlab-grack') |           expect(find('.results')).not_to have_content('path = gitlab-grack') | ||||||
| 
 | 
 | ||||||
|           find('.ref-selector').click |           find('.ref-selector').click | ||||||
|           wait_for_requests |           wait_for_requests | ||||||
| 
 | 
 | ||||||
|           page.within('.ref-selector') do |           select_listbox_item('add-ipython-files') | ||||||
|             fill_in _('Search by Git revision'), with: ref |  | ||||||
|             wait_for_requests |  | ||||||
| 
 |  | ||||||
|             find('li', text: ref).click |  | ||||||
|           end |  | ||||||
| 
 | 
 | ||||||
|           expect(page).to have_selector('.results', text: 'path = gitlab-grack') |           expect(page).to have_selector('.results', text: 'path = gitlab-grack') | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana | ||||||
|       page.within(ref_selector) do |       page.within(ref_selector) do | ||||||
|         fill_in _('Search by Git revision'), with: ref_name |         fill_in _('Search by Git revision'), with: ref_name | ||||||
|         wait_for_requests |         wait_for_requests | ||||||
|         expect(find('.gl-dropdown-contents')).not_to have_content(ref_name) |         expect(find('.gl-new-dropdown-inner')).not_to have_content(ref_name) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -60,9 +60,9 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana | ||||||
|       page.within ref_row do |       page.within ref_row do | ||||||
|         ref_input = find('[name="ref"]', visible: false) |         ref_input = find('[name="ref"]', visible: false) | ||||||
|         expect(ref_input.value).to eq 'master' |         expect(ref_input.value).to eq 'master' | ||||||
|         expect(find('.gl-dropdown-button-text')).to have_content 'master' |         expect(find('.gl-button-text')).to have_content 'master' | ||||||
|         find('.ref-selector').click |         find('.ref-selector').click | ||||||
|         expect(find('.dropdown-menu')).to have_content 'test' |         expect(find('.gl-new-dropdown-inner')).to have_content 'test' | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -438,42 +438,24 @@ describe('Ci variable modal', () => { | ||||||
|         raw: true, |         raw: true, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       describe('and FF is enabled', () => { |       beforeEach(() => { | ||||||
|         beforeEach(() => { |         createComponent({ | ||||||
|           createComponent({ |           mountFn: mountExtended, | ||||||
|             mountFn: mountExtended, |           props: { selectedVariable: validRawMaskedVariable }, | ||||||
|             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); |  | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       describe('and FF is disabled', () => { |       it('should not show an error with symbols', async () => { | ||||||
|         beforeEach(() => { |         await findMaskedVariableCheckbox().trigger('click'); | ||||||
|           createComponent({ |  | ||||||
|             mountFn: mountExtended, |  | ||||||
|             props: { selectedVariable: validRawMaskedVariable }, |  | ||||||
|             provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } }, |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         it('should show an error with symbols', async () => { |         expect(findModal().text()).not.toContain(maskError); | ||||||
|           await findMaskedVariableCheckbox().trigger('click'); |       }); | ||||||
| 
 | 
 | ||||||
|           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); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,16 @@ | ||||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||||
|  | import { stubComponent } from 'helpers/stub_component'; | ||||||
|  | import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; | ||||||
| import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; | import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; | ||||||
| import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; | import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; | ||||||
| import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; | import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; | ||||||
| import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; | import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; | ||||||
|  | import Tracking from '~/tracking'; | ||||||
|  | import { | ||||||
|  |   CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |   DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |   REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  | } from '~/packages_and_registries/package_registry/constants'; | ||||||
| import { packageData } from '../../mock_data'; | import { packageData } from '../../mock_data'; | ||||||
| 
 | 
 | ||||||
| describe('PackageVersionsList', () => { | describe('PackageVersionsList', () => { | ||||||
|  | @ -24,6 +32,7 @@ describe('PackageVersionsList', () => { | ||||||
|     findRegistryList: () => wrapper.findComponent(RegistryList), |     findRegistryList: () => wrapper.findComponent(RegistryList), | ||||||
|     findEmptySlot: () => wrapper.findComponent(EmptySlotStub), |     findEmptySlot: () => wrapper.findComponent(EmptySlotStub), | ||||||
|     findListRow: () => wrapper.findAllComponents(VersionRow), |     findListRow: () => wrapper.findAllComponents(VersionRow), | ||||||
|  |     findDeletePackagesModal: () => wrapper.findComponent(DeleteModal), | ||||||
|   }; |   }; | ||||||
|   const mountComponent = (props) => { |   const mountComponent = (props) => { | ||||||
|     wrapper = shallowMountExtended(PackageVersionsList, { |     wrapper = shallowMountExtended(PackageVersionsList, { | ||||||
|  | @ -35,6 +44,11 @@ describe('PackageVersionsList', () => { | ||||||
|       }, |       }, | ||||||
|       stubs: { |       stubs: { | ||||||
|         RegistryList, |         RegistryList, | ||||||
|  |         DeleteModal: stubComponent(DeleteModal, { | ||||||
|  |           methods: { | ||||||
|  |             show: jest.fn(), | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|       }, |       }, | ||||||
|       slots: { |       slots: { | ||||||
|         'empty-state': EmptySlotStub, |         'empty-state': EmptySlotStub, | ||||||
|  | @ -144,4 +158,80 @@ describe('PackageVersionsList', () => { | ||||||
|       expect(wrapper.emitted('next-page')).toHaveLength(1); |       expect(wrapper.emitted('next-page')).toHaveLength(1); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   describe('when the user can bulk destroy versions', () => { | ||||||
|  |     let eventSpy; | ||||||
|  |     const { findDeletePackagesModal, findRegistryList } = uiElements; | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |       eventSpy = jest.spyOn(Tracking, 'event'); | ||||||
|  |       mountComponent({ canDestroy: true }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('binds the right props', () => { | ||||||
|  |       expect(uiElements.findRegistryList().props()).toMatchObject({ | ||||||
|  |         items: packageList, | ||||||
|  |         pagination: {}, | ||||||
|  |         isLoading: false, | ||||||
|  |         hiddenDelete: false, | ||||||
|  |         title: '2 versions', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('upon deletion', () => { | ||||||
|  |       beforeEach(() => { | ||||||
|  |         findRegistryList().vm.$emit('delete', packageList); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('passes itemsToBeDeleted to the modal', () => { | ||||||
|  |         expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList); | ||||||
|  |         expect(wrapper.emitted('delete')).toBeUndefined(); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('requesting delete tracks the right action', () => { | ||||||
|  |         expect(eventSpy).toHaveBeenCalledWith( | ||||||
|  |           undefined, | ||||||
|  |           REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |           expect.any(Object), | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('when modal confirms', () => { | ||||||
|  |         beforeEach(() => { | ||||||
|  |           findDeletePackagesModal().vm.$emit('confirm'); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         it('emits delete event', () => { | ||||||
|  |           expect(wrapper.emitted('delete')[0]).toEqual([packageList]); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         it('tracks the right action', () => { | ||||||
|  |           expect(eventSpy).toHaveBeenCalledWith( | ||||||
|  |             undefined, | ||||||
|  |             DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |             expect.any(Object), | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it.each(['confirm', 'cancel'])( | ||||||
|  |         'resets itemsToBeDeleted when modal emits %s', | ||||||
|  |         async (event) => { | ||||||
|  |           await findDeletePackagesModal().vm.$emit(event); | ||||||
|  | 
 | ||||||
|  |           expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       it('canceling delete tracks the right action', () => { | ||||||
|  |         findDeletePackagesModal().vm.$emit('cancel'); | ||||||
|  | 
 | ||||||
|  |         expect(eventSpy).toHaveBeenCalledWith( | ||||||
|  |           undefined, | ||||||
|  |           CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, | ||||||
|  |           expect.any(Object), | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; | import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; | ||||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||||
| import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | ||||||
|  | import ListItem from '~/vue_shared/components/registry/list_item.vue'; | ||||||
| import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; | import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; | ||||||
| import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; | import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; | ||||||
| import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; | import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; | ||||||
|  | @ -15,17 +16,20 @@ const packageVersion = packageVersions()[0]; | ||||||
| describe('VersionRow', () => { | describe('VersionRow', () => { | ||||||
|   let wrapper; |   let wrapper; | ||||||
| 
 | 
 | ||||||
|  |   const findListItem = () => wrapper.findComponent(ListItem); | ||||||
|   const findLink = () => wrapper.findComponent(GlLink); |   const findLink = () => wrapper.findComponent(GlLink); | ||||||
|   const findPackageTags = () => wrapper.findComponent(PackageTags); |   const findPackageTags = () => wrapper.findComponent(PackageTags); | ||||||
|   const findPublishMethod = () => wrapper.findComponent(PublishMethod); |   const findPublishMethod = () => wrapper.findComponent(PublishMethod); | ||||||
|   const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); |   const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); | ||||||
|   const findPackageName = () => wrapper.findComponent(GlTruncate); |   const findPackageName = () => wrapper.findComponent(GlTruncate); | ||||||
|   const findWarningIcon = () => wrapper.findComponent(GlIcon); |   const findWarningIcon = () => wrapper.findComponent(GlIcon); | ||||||
|  |   const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); | ||||||
| 
 | 
 | ||||||
|   function createComponent(packageEntity = packageVersion) { |   function createComponent({ packageEntity = packageVersion, selected = false } = {}) { | ||||||
|     wrapper = shallowMountExtended(VersionRow, { |     wrapper = shallowMountExtended(VersionRow, { | ||||||
|       propsData: { |       propsData: { | ||||||
|         packageEntity, |         packageEntity, | ||||||
|  |         selected, | ||||||
|       }, |       }, | ||||||
|       stubs: { |       stubs: { | ||||||
|         GlSprintf, |         GlSprintf, | ||||||
|  | @ -76,13 +80,47 @@ describe('VersionRow', () => { | ||||||
|     expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt); |     expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   describe('left action template', () => { | ||||||
|  |     it('does not render checkbox if not permitted', () => { | ||||||
|  |       createComponent({ packageEntity: { ...packageVersion, canDestroy: false } }); | ||||||
|  | 
 | ||||||
|  |       expect(findBulkDeleteAction().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders checkbox', () => { | ||||||
|  |       createComponent(); | ||||||
|  | 
 | ||||||
|  |       expect(findBulkDeleteAction().exists()).toBe(true); | ||||||
|  |       expect(findBulkDeleteAction().attributes('checked')).toBeUndefined(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('emits select when checked', () => { | ||||||
|  |       createComponent(); | ||||||
|  | 
 | ||||||
|  |       findBulkDeleteAction().vm.$emit('change'); | ||||||
|  | 
 | ||||||
|  |       expect(wrapper.emitted('select')).toHaveLength(1); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders checkbox in selected state if selected', () => { | ||||||
|  |       createComponent({ | ||||||
|  |         selected: true, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       expect(findBulkDeleteAction().attributes('checked')).toBe('true'); | ||||||
|  |       expect(findListItem().props('selected')).toBe(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { |   describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       createComponent({ |       createComponent({ | ||||||
|         ...packageVersion, |         packageEntity: { | ||||||
|         status: PACKAGE_ERROR_STATUS, |           ...packageVersion, | ||||||
|         _links: { |           status: PACKAGE_ERROR_STATUS, | ||||||
|           webPath: null, |           _links: { | ||||||
|  |             webPath: null, | ||||||
|  |           }, | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | @ -109,10 +147,12 @@ describe('VersionRow', () => { | ||||||
|   describe('disabled status', () => { |   describe('disabled status', () => { | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       createComponent({ |       createComponent({ | ||||||
|         ...packageVersion, |         packageEntity: { | ||||||
|         status: 'something', |           ...packageVersion, | ||||||
|         _links: { |           status: 'something', | ||||||
|           webPath: null, |           _links: { | ||||||
|  |             webPath: null, | ||||||
|  |           }, | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -47,6 +47,7 @@ describe('packages_list_row', () => { | ||||||
|   const findPublishMethod = () => wrapper.findComponent(PublishMethod); |   const findPublishMethod = () => wrapper.findComponent(PublishMethod); | ||||||
|   const findCreatedDateText = () => wrapper.findByTestId('created-date'); |   const findCreatedDateText = () => wrapper.findByTestId('created-date'); | ||||||
|   const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip); |   const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip); | ||||||
|  |   const findListItem = () => wrapper.findComponent(ListItem); | ||||||
|   const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); |   const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); | ||||||
|   const findPackageName = () => wrapper.findComponent(GlTruncate); |   const findPackageName = () => wrapper.findComponent(GlTruncate); | ||||||
| 
 | 
 | ||||||
|  | @ -212,6 +213,9 @@ describe('packages_list_row', () => { | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       expect(findBulkDeleteAction().attributes('checked')).toBe('true'); |       expect(findBulkDeleteAction().attributes('checked')).toBe('true'); | ||||||
|  |       expect(findListItem().props()).toMatchObject({ | ||||||
|  |         selected: true, | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -109,6 +109,7 @@ export const packageVersions = () => [ | ||||||
|     id: 'gid://gitlab/Packages::Package/243', |     id: 'gid://gitlab/Packages::Package/243', | ||||||
|     name: '@gitlab-org/package-15', |     name: '@gitlab-org/package-15', | ||||||
|     status: 'DEFAULT', |     status: 'DEFAULT', | ||||||
|  |     canDestroy: true, | ||||||
|     tags: { nodes: packageTags() }, |     tags: { nodes: packageTags() }, | ||||||
|     version: '1.0.1', |     version: '1.0.1', | ||||||
|     ...linksData, |     ...linksData, | ||||||
|  | @ -119,6 +120,7 @@ export const packageVersions = () => [ | ||||||
|     id: 'gid://gitlab/Packages::Package/244', |     id: 'gid://gitlab/Packages::Package/244', | ||||||
|     name: '@gitlab-org/package-15', |     name: '@gitlab-org/package-15', | ||||||
|     status: 'DEFAULT', |     status: 'DEFAULT', | ||||||
|  |     canDestroy: true, | ||||||
|     tags: { nodes: packageTags() }, |     tags: { nodes: packageTags() }, | ||||||
|     version: '1.0.2', |     version: '1.0.2', | ||||||
|     ...linksData, |     ...linksData, | ||||||
|  |  | ||||||
|  | @ -128,6 +128,7 @@ describe('PackagesApp', () => { | ||||||
|   const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge'); |   const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge'); | ||||||
|   const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message'); |   const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message'); | ||||||
|   const findDependencyRows = () => wrapper.findAllComponents(DependencyRow); |   const findDependencyRows = () => wrapper.findAllComponents(DependencyRow); | ||||||
|  |   const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1); | ||||||
|   const findDeletePackages = () => wrapper.findComponent(DeletePackages); |   const findDeletePackages = () => wrapper.findComponent(DeletePackages); | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
|  | @ -267,7 +268,7 @@ describe('PackagesApp', () => { | ||||||
| 
 | 
 | ||||||
|         await waitForPromises(); |         await waitForPromises(); | ||||||
| 
 | 
 | ||||||
|         findDeletePackages().vm.$emit('end'); |         findDeletePackageModal().vm.$emit('end'); | ||||||
| 
 | 
 | ||||||
|         expect(window.location.replace).toHaveBeenCalledWith( |         expect(window.location.replace).toHaveBeenCalledWith( | ||||||
|           'projectListUrl?showSuccessDeleteAlert=true', |           'projectListUrl?showSuccessDeleteAlert=true', | ||||||
|  | @ -281,7 +282,7 @@ describe('PackagesApp', () => { | ||||||
| 
 | 
 | ||||||
|         await waitForPromises(); |         await waitForPromises(); | ||||||
| 
 | 
 | ||||||
|         findDeletePackages().vm.$emit('end'); |         findDeletePackageModal().vm.$emit('end'); | ||||||
| 
 | 
 | ||||||
|         expect(window.location.replace).toHaveBeenCalledWith( |         expect(window.location.replace).toHaveBeenCalledWith( | ||||||
|           'groupListUrl?showSuccessDeleteAlert=true', |           'groupListUrl?showSuccessDeleteAlert=true', | ||||||
|  | @ -600,9 +601,51 @@ describe('PackagesApp', () => { | ||||||
|       await waitForPromises(); |       await waitForPromises(); | ||||||
| 
 | 
 | ||||||
|       expect(findVersionsList().props()).toMatchObject({ |       expect(findVersionsList().props()).toMatchObject({ | ||||||
|  |         canDestroy: true, | ||||||
|         versions: expect.arrayContaining(versionNodes), |         versions: expect.arrayContaining(versionNodes), | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     describe('delete packages', () => { | ||||||
|  |       it('exists and has the correct props', async () => { | ||||||
|  |         createComponent(); | ||||||
|  | 
 | ||||||
|  |         await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |         expect(findDeletePackages().props()).toMatchObject({ | ||||||
|  |           refetchQueries: [{ query: getPackageDetails, variables: {} }], | ||||||
|  |           showSuccessAlert: true, | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('deletePackages is bound to package-versions-list delete event', async () => { | ||||||
|  |         createComponent(); | ||||||
|  | 
 | ||||||
|  |         await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |         findVersionsList().vm.$emit('delete', [{ id: 1 }]); | ||||||
|  | 
 | ||||||
|  |         expect(findDeletePackages().emitted('start')).toEqual([[]]); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('start and end event set loading correctly', async () => { | ||||||
|  |         createComponent(); | ||||||
|  | 
 | ||||||
|  |         await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |         findDeletePackages().vm.$emit('start'); | ||||||
|  | 
 | ||||||
|  |         await nextTick(); | ||||||
|  | 
 | ||||||
|  |         expect(findVersionsList().props('isLoading')).toBe(true); | ||||||
|  | 
 | ||||||
|  |         findDeletePackages().vm.$emit('end'); | ||||||
|  | 
 | ||||||
|  |         await nextTick(); | ||||||
|  | 
 | ||||||
|  |         expect(findVersionsList().props('isLoading')).toBe(false); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('dependency links', () => { |   describe('dependency links', () => { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui'; | import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; | ||||||
| import { mount } from '@vue/test-utils'; |  | ||||||
| import Vue, { nextTick } from 'vue'; | import Vue, { nextTick } from 'vue'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| import MockAdapter from 'axios-mock-adapter'; | import MockAdapter from 'axios-mock-adapter'; | ||||||
|  | @ -8,13 +7,13 @@ import Vuex from 'vuex'; | ||||||
| import commit from 'test_fixtures/api/commits/commit.json'; | import commit from 'test_fixtures/api/commits/commit.json'; | ||||||
| import branches from 'test_fixtures/api/branches/branches.json'; | import branches from 'test_fixtures/api/branches/branches.json'; | ||||||
| import tags from 'test_fixtures/api/tags/tags.json'; | import tags from 'test_fixtures/api/tags/tags.json'; | ||||||
|  | import { mountExtended } from 'helpers/vue_test_utils_helper'; | ||||||
| import { trimText } from 'helpers/text_helper'; | import { trimText } from 'helpers/text_helper'; | ||||||
| import { | import { | ||||||
|   HTTP_STATUS_INTERNAL_SERVER_ERROR, |   HTTP_STATUS_INTERNAL_SERVER_ERROR, | ||||||
|   HTTP_STATUS_NOT_FOUND, |   HTTP_STATUS_NOT_FOUND, | ||||||
|   HTTP_STATUS_OK, |   HTTP_STATUS_OK, | ||||||
| } from '~/lib/utils/http_status'; | } from '~/lib/utils/http_status'; | ||||||
| import { ENTER_KEY } from '~/lib/utils/keys'; |  | ||||||
| import { sprintf } from '~/locale'; | import { sprintf } from '~/locale'; | ||||||
| import RefSelector from '~/ref/components/ref_selector.vue'; | import RefSelector from '~/ref/components/ref_selector.vue'; | ||||||
| import { | import { | ||||||
|  | @ -42,7 +41,7 @@ describe('Ref selector component', () => { | ||||||
|   let requestSpies; |   let requestSpies; | ||||||
| 
 | 
 | ||||||
|   const createComponent = (mountOverrides = {}, propsData = {}) => { |   const createComponent = (mountOverrides = {}, propsData = {}) => { | ||||||
|     wrapper = mount( |     wrapper = mountExtended( | ||||||
|       RefSelector, |       RefSelector, | ||||||
|       merge( |       merge( | ||||||
|         { |         { | ||||||
|  | @ -57,9 +56,6 @@ describe('Ref selector component', () => { | ||||||
|               wrapper.setProps({ value: selectedRef }); |               wrapper.setProps({ value: selectedRef }); | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|           stubs: { |  | ||||||
|             GlSearchBoxByType: true, |  | ||||||
|           }, |  | ||||||
|           store: createStore(), |           store: createStore(), | ||||||
|         }, |         }, | ||||||
|         mountOverrides, |         mountOverrides, | ||||||
|  | @ -91,76 +87,63 @@ describe('Ref selector component', () => { | ||||||
|       .reply((config) => commitApiCallSpy(config)); |       .reply((config) => commitApiCallSpy(config)); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |  | ||||||
|     wrapper.destroy(); |  | ||||||
|     wrapper = null; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   //
 |   //
 | ||||||
|   // Finders
 |   // Finders
 | ||||||
|   //
 |   //
 | ||||||
|   const findButtonContent = () => wrapper.find('button'); |   const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); | ||||||
| 
 | 
 | ||||||
|   const findNoResults = () => wrapper.find('[data-testid="no-results"]'); |   const findButtonToggle = () => wrapper.findByTestId('base-dropdown-toggle'); | ||||||
|  | 
 | ||||||
|  |   const findNoResults = () => wrapper.findByTestId('listbox-no-results-text'); | ||||||
| 
 | 
 | ||||||
|   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); |   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); | ||||||
| 
 | 
 | ||||||
|   const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); |   const findListBoxSection = (section) => { | ||||||
|  |     const foundSections = wrapper | ||||||
|  |       .findAll('[role="group"]') | ||||||
|  |       .filter((ul) => ul.text().includes(section)); | ||||||
|  |     return foundSections.length > 0 ? foundSections.at(0) : foundSections; | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]'); |   const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list'); | ||||||
|   const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem); |  | ||||||
|   const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0); |  | ||||||
| 
 | 
 | ||||||
|   const findTagsSection = () => wrapper.find('[data-testid="tags-section"]'); |   const findBranchesSection = () => findListBoxSection('Branches'); | ||||||
|   const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem); |   const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem); | ||||||
|   const findFirstTagDropdownItem = () => findTagDropdownItems().at(0); |  | ||||||
| 
 | 
 | ||||||
|   const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]'); |   const findTagsSection = () => findListBoxSection('Tags'); | ||||||
|   const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem); |  | ||||||
|   const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0); |  | ||||||
| 
 | 
 | ||||||
|   const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]'); |   const findCommitsSection = () => findListBoxSection('Commits'); | ||||||
|  | 
 | ||||||
|  |   const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field'); | ||||||
| 
 | 
 | ||||||
|   //
 |   //
 | ||||||
|   // Expecters
 |   // Expecters
 | ||||||
|   //
 |   //
 | ||||||
|   const branchesSectionContainsErrorMessage = () => { |   const sectionContainsErrorMessage = (message) => { | ||||||
|     const branchesSection = findBranchesSection(); |     const errorSection = findErrorListWrapper(); | ||||||
| 
 | 
 | ||||||
|     return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage); |     return errorSection ? errorSection.text().includes(message) : false; | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const tagsSectionContainsErrorMessage = () => { |  | ||||||
|     const tagsSection = findTagsSection(); |  | ||||||
| 
 |  | ||||||
|     return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const commitsSectionContainsErrorMessage = () => { |  | ||||||
|     const commitsSection = findCommitsSection(); |  | ||||||
| 
 |  | ||||||
|     return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage); |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   //
 |   //
 | ||||||
|   // Convenience methods
 |   // Convenience methods
 | ||||||
|   //
 |   //
 | ||||||
|   const updateQuery = (newQuery) => { |   const updateQuery = (newQuery) => { | ||||||
|     findSearchBox().vm.$emit('input', newQuery); |     findListbox().vm.$emit('search', newQuery); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const selectFirstBranch = async () => { |   const selectFirstBranch = async () => { | ||||||
|     findFirstBranchDropdownItem().vm.$emit('click'); |     findListbox().vm.$emit('select', fixtures.branches[0].name); | ||||||
|     await nextTick(); |     await nextTick(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const selectFirstTag = async () => { |   const selectFirstTag = async () => { | ||||||
|     findFirstTagDropdownItem().vm.$emit('click'); |     findListbox().vm.$emit('select', fixtures.tags[0].name); | ||||||
|     await nextTick(); |     await nextTick(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const selectFirstCommit = async () => { |   const selectFirstCommit = async () => { | ||||||
|     findFirstCommitDropdownItem().vm.$emit('click'); |     findListbox().vm.$emit('select', fixtures.commit.id); | ||||||
|     await nextTick(); |     await nextTick(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -195,7 +178,7 @@ describe('Ref selector component', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('when name property is provided', () => { |     describe('when name property is provided', () => { | ||||||
|       it('renders an forrm input hidden field', () => { |       it('renders an form input hidden field', () => { | ||||||
|         const name = 'default_tag'; |         const name = 'default_tag'; | ||||||
| 
 | 
 | ||||||
|         createComponent({ propsData: { name } }); |         createComponent({ propsData: { name } }); | ||||||
|  | @ -205,7 +188,7 @@ describe('Ref selector component', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('when name property is not provided', () => { |     describe('when name property is not provided', () => { | ||||||
|       it('renders an forrm input hidden field', () => { |       it('renders an form input hidden field', () => { | ||||||
|         createComponent(); |         createComponent(); | ||||||
| 
 | 
 | ||||||
|         expect(findHiddenInputField().exists()).toBe(false); |         expect(findHiddenInputField().exists()).toBe(false); | ||||||
|  | @ -224,7 +207,7 @@ describe('Ref selector component', () => { | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('adds the provided ID to the GlDropdown instance', () => { |       it('adds the provided ID to the GlDropdown instance', () => { | ||||||
|         expect(wrapper.findComponent(GlDropdown).attributes().id).toBe(id); |         expect(findListbox().attributes().id).toBe(id); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -238,7 +221,7 @@ describe('Ref selector component', () => { | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('renders the pre-selected ref name', () => { |       it('renders the pre-selected ref name', () => { | ||||||
|         expect(findButtonContent().text()).toBe(preselectedRef); |         expect(findButtonToggle().text()).toBe(preselectedRef); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('binds hidden input field to the pre-selected ref', () => { |       it('binds hidden input field to the pre-selected ref', () => { | ||||||
|  | @ -259,7 +242,7 @@ describe('Ref selector component', () => { | ||||||
|         wrapper.setProps({ value: updatedRef }); |         wrapper.setProps({ value: updatedRef }); | ||||||
| 
 | 
 | ||||||
|         await nextTick(); |         await nextTick(); | ||||||
|         expect(findButtonContent().text()).toBe(updatedRef); |         expect(findButtonToggle().text()).toBe(updatedRef); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -296,23 +279,6 @@ describe('Ref selector component', () => { | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('when the Enter is pressed', () => { |  | ||||||
|       beforeEach(() => { |  | ||||||
|         createComponent(); |  | ||||||
| 
 |  | ||||||
|         return waitForRequests({ andClearMocks: true }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('requeries the endpoints when Enter is pressed', () => { |  | ||||||
|         findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); |  | ||||||
| 
 |  | ||||||
|         return waitForRequests().then(() => { |  | ||||||
|           expect(branchesApiCallSpy).toHaveBeenCalledTimes(1); |  | ||||||
|           expect(tagsApiCallSpy).toHaveBeenCalledTimes(1); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     describe('when no results are found', () => { |     describe('when no results are found', () => { | ||||||
|       beforeEach(() => { |       beforeEach(() => { | ||||||
|         branchesApiCallSpy = jest |         branchesApiCallSpy = jest | ||||||
|  | @ -357,27 +323,10 @@ describe('Ref selector component', () => { | ||||||
| 
 | 
 | ||||||
|         it('renders the branches section in the dropdown', () => { |         it('renders the branches section in the dropdown', () => { | ||||||
|           expect(findBranchesSection().exists()).toBe(true); |           expect(findBranchesSection().exists()).toBe(true); | ||||||
|           expect(findBranchesSection().props('shouldShowCheck')).toBe(true); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         it('renders the "Branches" heading with a total number indicator', () => { |  | ||||||
|           expect( |  | ||||||
|             findBranchesSection().find('[data-testid="section-header"]').text(), |  | ||||||
|           ).toMatchInterpolatedText('Branches 123'); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("does not render an error message in the branches section's body", () => { |         it("does not render an error message in the branches section's body", () => { | ||||||
|           expect(branchesSectionContainsErrorMessage()).toBe(false); |           expect(findErrorListWrapper().exists()).toBe(false); | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         it('renders each non-default branch as a selectable item', () => { |  | ||||||
|           const dropdownItems = findBranchDropdownItems(); |  | ||||||
| 
 |  | ||||||
|           fixtures.branches.forEach((b, i) => { |  | ||||||
|             if (!b.default) { |  | ||||||
|               expect(dropdownItems.at(i).text()).toBe(b.name); |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('renders the default branch as a selectable item with a "default" badge', () => { |         it('renders the default branch as a selectable item with a "default" badge', () => { | ||||||
|  | @ -418,11 +367,11 @@ describe('Ref selector component', () => { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('renders the branches section in the dropdown', () => { |         it('renders the branches section in the dropdown', () => { | ||||||
|           expect(findBranchesSection().exists()).toBe(true); |           expect(findBranchesSection().exists()).toBe(false); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("renders an error message in the branches section's body", () => { |         it("renders an error message in the branches section's body", () => { | ||||||
|           expect(branchesSectionContainsErrorMessage()).toBe(true); |           expect(sectionContainsErrorMessage(DEFAULT_I18N.branchesErrorMessage)).toBe(true); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | @ -437,25 +386,16 @@ describe('Ref selector component', () => { | ||||||
| 
 | 
 | ||||||
|         it('renders the tags section in the dropdown', () => { |         it('renders the tags section in the dropdown', () => { | ||||||
|           expect(findTagsSection().exists()).toBe(true); |           expect(findTagsSection().exists()).toBe(true); | ||||||
|           expect(findTagsSection().props('shouldShowCheck')).toBe(true); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('renders the "Tags" heading with a total number indicator', () => { |         it('renders the "Tags" heading with a total number indicator', () => { | ||||||
|           expect( |           expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText( | ||||||
|             findTagsSection().find('[data-testid="section-header"]').text(), |             `Tags ${fixtures.tags.length}`, | ||||||
|           ).toMatchInterpolatedText('Tags 456'); |           ); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("does not render an error message in the tags section's body", () => { |         it("does not render an error message in the tags section's body", () => { | ||||||
|           expect(tagsSectionContainsErrorMessage()).toBe(false); |           expect(findErrorListWrapper().exists()).toBe(false); | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         it('renders each tag as a selectable item', () => { |  | ||||||
|           const dropdownItems = findTagDropdownItems(); |  | ||||||
| 
 |  | ||||||
|           fixtures.tags.forEach((t, i) => { |  | ||||||
|             expect(dropdownItems.at(i).text()).toBe(t.name); |  | ||||||
|           }); |  | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -485,11 +425,11 @@ describe('Ref selector component', () => { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('renders the tags section in the dropdown', () => { |         it('renders the tags section in the dropdown', () => { | ||||||
|           expect(findTagsSection().exists()).toBe(true); |           expect(findTagsSection().exists()).toBe(false); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("renders an error message in the tags section's body", () => { |         it("renders an error message in the tags section's body", () => { | ||||||
|           expect(tagsSectionContainsErrorMessage()).toBe(true); |           expect(sectionContainsErrorMessage(DEFAULT_I18N.tagsErrorMessage)).toBe(true); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | @ -509,19 +449,13 @@ describe('Ref selector component', () => { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('renders the "Commits" heading with a total number indicator', () => { |         it('renders the "Commits" heading with a total number indicator', () => { | ||||||
|           expect( |           expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText( | ||||||
|             findCommitsSection().find('[data-testid="section-header"]').text(), |             `Commits 1`, | ||||||
|           ).toMatchInterpolatedText('Commits 1'); |           ); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("does not render an error message in the comits section's body", () => { |         it("does not render an error message in the commits section's body", () => { | ||||||
|           expect(commitsSectionContainsErrorMessage()).toBe(false); |           expect(findErrorListWrapper().exists()).toBe(false); | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         it('renders each commit as a selectable item with the short SHA and commit title', () => { |  | ||||||
|           const dropdownItems = findCommitDropdownItems(); |  | ||||||
| 
 |  | ||||||
|           expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`); |  | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | @ -553,11 +487,11 @@ describe('Ref selector component', () => { | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('renders the commits section in the dropdown', () => { |         it('renders the commits section in the dropdown', () => { | ||||||
|           expect(findCommitsSection().exists()).toBe(true); |           expect(findCommitsSection().exists()).toBe(false); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("renders an error message in the commits section's body", () => { |         it("renders an error message in the commits section's body", () => { | ||||||
|           expect(commitsSectionContainsErrorMessage()).toBe(true); |           expect(sectionContainsErrorMessage(DEFAULT_I18N.commitsErrorMessage)).toBe(true); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | @ -571,26 +505,13 @@ describe('Ref selector component', () => { | ||||||
|         return waitForRequests(); |         return waitForRequests(); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('renders a checkmark by the selected item', async () => { |       describe('when a branch is selected', () => { | ||||||
|         expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass( |  | ||||||
|           'gl-visibility-hidden', |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         await selectFirstBranch(); |  | ||||||
| 
 |  | ||||||
|         expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass( |  | ||||||
|           'gl-visibility-hidden', |  | ||||||
|         ); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       describe('when a branch is seleceted', () => { |  | ||||||
|         it("displays the branch name in the dropdown's button", async () => { |         it("displays the branch name in the dropdown's button", async () => { | ||||||
|           expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); |           expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected); | ||||||
| 
 | 
 | ||||||
|           await selectFirstBranch(); |           await selectFirstBranch(); | ||||||
| 
 | 
 | ||||||
|           await nextTick(); |           expect(findButtonToggle().text()).toBe(fixtures.branches[0].name); | ||||||
|           expect(findButtonContent().text()).toBe(fixtures.branches[0].name); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("updates the v-model binding with the branch's name", async () => { |         it("updates the v-model binding with the branch's name", async () => { | ||||||
|  | @ -604,12 +525,11 @@ describe('Ref selector component', () => { | ||||||
| 
 | 
 | ||||||
|       describe('when a tag is seleceted', () => { |       describe('when a tag is seleceted', () => { | ||||||
|         it("displays the tag name in the dropdown's button", async () => { |         it("displays the tag name in the dropdown's button", async () => { | ||||||
|           expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); |           expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected); | ||||||
| 
 | 
 | ||||||
|           await selectFirstTag(); |           await selectFirstTag(); | ||||||
| 
 | 
 | ||||||
|           await nextTick(); |           expect(findButtonToggle().text()).toBe(fixtures.tags[0].name); | ||||||
|           expect(findButtonContent().text()).toBe(fixtures.tags[0].name); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("updates the v-model binding with the tag's name", async () => { |         it("updates the v-model binding with the tag's name", async () => { | ||||||
|  | @ -623,12 +543,11 @@ describe('Ref selector component', () => { | ||||||
| 
 | 
 | ||||||
|       describe('when a commit is selected', () => { |       describe('when a commit is selected', () => { | ||||||
|         it("displays the full SHA in the dropdown's button", async () => { |         it("displays the full SHA in the dropdown's button", async () => { | ||||||
|           expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected); |           expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected); | ||||||
| 
 | 
 | ||||||
|           await selectFirstCommit(); |           await selectFirstCommit(); | ||||||
| 
 | 
 | ||||||
|           await nextTick(); |           expect(findButtonToggle().text()).toBe(fixtures.commit.id); | ||||||
|           expect(findButtonContent().text()).toBe(fixtures.commit.id); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it("updates the v-model binding with the commit's full SHA", async () => { |         it("updates the v-model binding with the commit's full SHA", async () => { | ||||||
|  | @ -688,21 +607,6 @@ describe('Ref selector component', () => { | ||||||
|       expect(tagsApiCallSpy).toHaveBeenCalledTimes(1); |       expect(tagsApiCallSpy).toHaveBeenCalledTimes(1); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => { |  | ||||||
|       createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] } }); |  | ||||||
|       updateQuery('abcd1234'); |  | ||||||
|       await waitForRequests(); |  | ||||||
| 
 |  | ||||||
|       expect(findBranchesSection().exists()).toBe(true); |  | ||||||
|       expect(findCommitsSection().exists()).toBe(true); |  | ||||||
| 
 |  | ||||||
|       wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] }); |  | ||||||
|       await waitForRequests(); |  | ||||||
| 
 |  | ||||||
|       expect(findBranchesSection().exists()).toBe(false); |  | ||||||
|       expect(findCommitsSection().exists()).toBe(true); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it.each` |     it.each` | ||||||
|       enabledRefType       | findVisibleSection     | findHiddenSections |       enabledRefType       | findVisibleSection     | findHiddenSections | ||||||
|       ${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]} |       ${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]} | ||||||
|  | @ -726,8 +630,7 @@ describe('Ref selector component', () => { | ||||||
| 
 | 
 | ||||||
|   describe('validation state', () => { |   describe('validation state', () => { | ||||||
|     const invalidClass = 'gl-inset-border-1-red-500!'; |     const invalidClass = 'gl-inset-border-1-red-500!'; | ||||||
|     const isInvalidClassApplied = () => |     const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass]; | ||||||
|       wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass]; |  | ||||||
| 
 | 
 | ||||||
|     describe('valid state', () => { |     describe('valid state', () => { | ||||||
|       describe('when the state prop is not provided', () => { |       describe('when the state prop is not provided', () => { | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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'), | ||||||
|  | }; | ||||||
|  | @ -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> | ||||||
|  | `; | ||||||
|  | @ -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); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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, | ||||||
|  | }; | ||||||
|  | @ -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); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -821,7 +821,7 @@ RSpec.describe API::Helpers, feature_category: :not_owned do | ||||||
| 
 | 
 | ||||||
|         it 'redirects to a CDN-fronted URL' do |         it 'redirects to a CDN-fronted URL' do | ||||||
|           expect(helper).to receive(:redirect) |           expect(helper).to receive(:redirect) | ||||||
|           expect(helper).to receive(:signed_head_url).and_call_original |           expect(ObjectStorage::S3).to receive(:signed_head_url).and_call_original | ||||||
|           expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original |           expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original | ||||||
| 
 | 
 | ||||||
|           subject |           subject | ||||||
|  |  | ||||||
|  | @ -13,15 +13,23 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'clears the cache' do |       it 'clears the cache' do | ||||||
|  |         cached_keys = [ | ||||||
|  |           "pages_domain_for_#{type}_1_settings-hash", | ||||||
|  |           "pages_domain_for_#{type}_1" | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         expect(::Gitlab::AppLogger) | ||||||
|  |           .to receive(:info) | ||||||
|  |           .with( | ||||||
|  |             message: 'clear pages cache', | ||||||
|  |             keys: cached_keys, | ||||||
|  |             type: type, | ||||||
|  |             id: 1 | ||||||
|  |           ) | ||||||
|  | 
 | ||||||
|         expect(Rails.cache) |         expect(Rails.cache) | ||||||
|           .to receive(:delete_multi) |           .to receive(:delete_multi) | ||||||
|           .with( |           .with(cached_keys) | ||||||
|             array_including( |  | ||||||
|               [ |  | ||||||
|                 "pages_domain_for_#{type}_1", |  | ||||||
|                 "pages_domain_for_#{type}_1_settings-hash" |  | ||||||
|               ] |  | ||||||
|             )) |  | ||||||
| 
 | 
 | ||||||
|         subject.clear_cache |         subject.clear_cache | ||||||
|       end |       end | ||||||
|  | @ -31,13 +39,13 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do | ||||||
|   describe '.for_namespace' do |   describe '.for_namespace' do | ||||||
|     subject(:cache_control) { described_class.for_namespace(1) } |     subject(:cache_control) { described_class.for_namespace(1) } | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'cache_control', 'namespace' |     it_behaves_like 'cache_control', :namespace | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.for_domain' do |   describe '.for_domain' do | ||||||
|     subject(:cache_control) { described_class.for_domain(1) } |     subject(:cache_control) { described_class.for_domain(1) } | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'cache_control', 'domain' |     it_behaves_like 'cache_control', :domain | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#cache_key' do |   describe '#cache_key' do | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Ci::Runner, feature_category: :runner do | RSpec.describe Ci::Runner, type: :model, feature_category: :runner do | ||||||
|   include StubGitlabCalls |   include StubGitlabCalls | ||||||
| 
 | 
 | ||||||
|   it_behaves_like 'having unique enum values' |   it_behaves_like 'having unique enum values' | ||||||
|  | @ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do | ||||||
|   describe 'validation' do |   describe 'validation' do | ||||||
|     it { is_expected.to validate_presence_of(:access_level) } |     it { is_expected.to validate_presence_of(:access_level) } | ||||||
|     it { is_expected.to validate_presence_of(:runner_type) } |     it { is_expected.to validate_presence_of(:runner_type) } | ||||||
|  |     it { is_expected.to validate_presence_of(:registration_type) } | ||||||
| 
 | 
 | ||||||
|     context 'when runner is not allowed to pick untagged jobs' do |     context 'when runner is not allowed to pick untagged jobs' do | ||||||
|       context 'when runner does not have tags' do |       context 'when runner does not have tags' do | ||||||
|  | @ -1748,7 +1749,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when creating new runner via UI' do |     context 'when creating new runner via UI' do | ||||||
|       let(:runner) { create(:ci_runner, :created_via_ui) } |       let(:runner) { create(:ci_runner, registration_type: :authenticated_user) } | ||||||
| 
 | 
 | ||||||
|       specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) } |       specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) } | ||||||
|       it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) } |       it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) } | ||||||
|  | @ -1765,7 +1766,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when runner is created via UI' do |     context 'when runner is created via UI' do | ||||||
|       let(:runner) { create(:ci_runner, :created_via_ui) } |       let(:runner) { create(:ci_runner, registration_type: :authenticated_user) } | ||||||
| 
 | 
 | ||||||
|       it { is_expected.to start_with('glrt-') } |       it { is_expected.to start_with('glrt-') } | ||||||
|     end |     end | ||||||
|  | @ -1993,20 +1994,4 @@ RSpec.describe Ci::Runner, feature_category: :runner do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 |  | ||||||
|   describe '#created_via_ui?' do |  | ||||||
|     subject(:created_via_ui) { runner.created_via_ui? } |  | ||||||
| 
 |  | ||||||
|     context 'when runner registered from command line' do |  | ||||||
|       let(:runner) { create(:ci_runner) } |  | ||||||
| 
 |  | ||||||
|       it { is_expected.to eq false } |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when runner created via UI' do |  | ||||||
|       let(:runner) { create(:ci_runner, :created_via_ui) } |  | ||||||
| 
 |  | ||||||
|       it { is_expected.to eq true } |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -525,7 +525,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do | ||||||
|     let_it_be(:creator) { create(:user) } |     let_it_be(:creator) { create(:user) } | ||||||
| 
 | 
 | ||||||
|     let(:created_at) { Time.current } |     let(:created_at) { Time.current } | ||||||
|     let(:token_prefix) { '' } |     let(:token_prefix) { registration_type == :authenticated_user ? 'glrt-' : '' } | ||||||
|  |     let(:registration_type) {} | ||||||
|     let(:query) do |     let(:query) do | ||||||
|       %( |       %( | ||||||
|         query { |         query { | ||||||
|  | @ -539,7 +540,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do | ||||||
| 
 | 
 | ||||||
|     let(:runner) do |     let(:runner) do | ||||||
|       create(:ci_runner, :group, |       create(:ci_runner, :group, | ||||||
|              groups: [group], creator: creator, created_at: created_at, token: "#{token_prefix}abc123") |              groups: [group], creator: creator, created_at: created_at, | ||||||
|  |              registration_type: registration_type, token: "#{token_prefix}abc123") | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     before_all do |     before_all do | ||||||
|  | @ -570,7 +572,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do | ||||||
|       let(:user) { creator } |       let(:user) { creator } | ||||||
| 
 | 
 | ||||||
|       context 'with runner created in UI' do |       context 'with runner created in UI' do | ||||||
|         let(:token_prefix) { ::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX } |         let(:registration_type) { :authenticated_user } | ||||||
| 
 | 
 | ||||||
|         context 'with runner created in last 3 hours' do |         context 'with runner created in last 3 hours' do | ||||||
|           let(:created_at) { (3.hours - 1.second).ago } |           let(:created_at) { (3.hours - 1.second).ago } | ||||||
|  | @ -600,7 +602,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'with runner registered from command line' do |       context 'with runner registered from command line' do | ||||||
|         let(:token_prefix) { '' } |         let(:registration_type) { :registration_token } | ||||||
| 
 | 
 | ||||||
|         context 'with runner created in last 3 hours' do |         context 'with runner created in last 3 hours' do | ||||||
|           let(:created_at) { (3.hours - 1.second).ago } |           let(:created_at) { (3.hours - 1.second).ago } | ||||||
|  | @ -614,7 +616,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do | ||||||
|       let(:user) { create(:admin) } |       let(:user) { create(:admin) } | ||||||
| 
 | 
 | ||||||
|       context 'with runner created in UI' do |       context 'with runner created in UI' do | ||||||
|         let(:token_prefix) { ::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX } |         let(:registration_type) { :authenticated_user } | ||||||
| 
 | 
 | ||||||
|         it_behaves_like 'a protected ephemeral_authentication_token' |         it_behaves_like 'a protected ephemeral_authentication_token' | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -125,6 +125,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do | ||||||
|           expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) |           expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) | ||||||
| 
 | 
 | ||||||
|           subject |           subject | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_gitlab_http_status(:redirect) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -131,7 +131,7 @@ RSpec.describe AutoMergeService do | ||||||
|       subject |       subject | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when the head piipeline succeeded' do |     context 'when the head pipeline succeeded' do | ||||||
|       let(:pipeline_status) { :success } |       let(:pipeline_status) { :success } | ||||||
| 
 | 
 | ||||||
|       it 'returns failed' do |       it 'returns failed' do | ||||||
|  |  | ||||||
|  | @ -22,15 +22,10 @@ module Spec | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def select_branch(branch_name) |           def select_branch(branch_name) | ||||||
|             ref_selector = '.ref-selector' |  | ||||||
|             find(ref_selector).click |  | ||||||
|             wait_for_requests |             wait_for_requests | ||||||
| 
 | 
 | ||||||
|             page.within(ref_selector) do |             click_button branch_name | ||||||
|               fill_in _('Search by Git revision'), with: branch_name |             send_keys branch_name | ||||||
|               wait_for_requests |  | ||||||
|               find('li', text: branch_name, match: :prefer_exact).click |  | ||||||
|             end |  | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -15,17 +15,18 @@ module Spec | ||||||
|     module Helpers |     module Helpers | ||||||
|       module Features |       module Features | ||||||
|         module ReleasesHelpers |         module ReleasesHelpers | ||||||
|  |           include ListboxHelpers | ||||||
|  | 
 | ||||||
|           def select_new_tag_name(tag_name) |           def select_new_tag_name(tag_name) | ||||||
|             page.within '[data-testid="tag-name-field"]' do |             page.within '[data-testid="tag-name-field"]' do | ||||||
|               find('button').click |               find('button').click | ||||||
| 
 |  | ||||||
|               wait_for_all_requests |               wait_for_all_requests | ||||||
| 
 | 
 | ||||||
|               find('input[aria-label="Search or create tag"]').set(tag_name) |               find('input[aria-label="Search or create tag"]').set(tag_name) | ||||||
| 
 |  | ||||||
|               wait_for_all_requests |               wait_for_all_requests | ||||||
| 
 | 
 | ||||||
|               click_button("Create tag #{tag_name}") |               click_button("Create tag #{tag_name}") | ||||||
|  |               click_button tag_name | ||||||
|             end |             end | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|  | @ -39,7 +40,7 @@ module Spec | ||||||
| 
 | 
 | ||||||
|               wait_for_all_requests |               wait_for_all_requests | ||||||
| 
 | 
 | ||||||
|               click_button(branch_name.to_s) |               select_listbox_item(branch_name.to_s, exact_text: true) | ||||||
|             end |             end | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
		Loading…
	
		Reference in New Issue