diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-26 12:11:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-26 12:11:08 +0300 |
commit | 79cd3f3a38777b1436107bd1e3205f593e1a3bd1 (patch) | |
tree | 4bc714410905fed21c92057b2a83149150dbe18d | |
parent | a8f6578cb24cb3a688b7a5be674867fa311b0b38 (diff) |
Add latest changes from gitlab-org/gitlab@master
18 files changed, 196 insertions, 393 deletions
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue index 2f7fb542d0e..a5e306b5372 100644 --- a/app/assets/javascripts/milestones/components/milestone_combobox.vue +++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue @@ -1,19 +1,10 @@ <script> -import { - GlDropdown, - GlDropdownDivider, - GlDropdownSectionHeader, - GlDropdownItem, - GlLoadingIcon, - GlSearchBoxByType, - GlIcon, -} from '@gitlab/ui'; +import { GlBadge, GlButton, GlCollapsibleListbox } from '@gitlab/ui'; import { debounce, isEqual } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; import { s__, __, sprintf } from '~/locale'; import createStore from '../stores'; -import MilestoneResultsSection from './milestone_results_section.vue'; const SEARCH_DEBOUNCE_MS = 250; @@ -21,14 +12,9 @@ export default { name: 'MilestoneCombobox', store: createStore(), components: { - GlDropdown, - GlDropdownDivider, - GlDropdownSectionHeader, - GlDropdownItem, - GlLoadingIcon, - GlSearchBoxByType, - GlIcon, - MilestoneResultsSection, + GlCollapsibleListbox, + GlBadge, + GlButton, }, props: { value: { @@ -56,27 +42,43 @@ export default { required: false, }, }, - data() { - return { - searchQuery: '', - }; - }, translations: { - milestone: s__('MilestoneCombobox|Milestone'), selectMilestone: s__('MilestoneCombobox|Select milestone'), noMilestone: s__('MilestoneCombobox|No milestone'), - noResultsLabel: s__('MilestoneCombobox|No matching results'), - searchMilestones: s__('MilestoneCombobox|Search Milestones'), - searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'), projectMilestones: s__('MilestoneCombobox|Project milestones'), groupMilestones: s__('MilestoneCombobox|Group milestones'), + unselect: __('Unselect'), }, computed: { ...mapState(['matches', 'selectedMilestones']), - ...mapGetters(['isLoading', 'groupMilestonesEnabled']), + ...mapGetters(['isLoading']), + allMilestones() { + const { groupMilestones, projectMilestones } = this.matches || {}; + const milestones = []; + + if (projectMilestones?.totalCount) { + milestones.push({ + id: 'project-milestones', + text: this.$options.translations.projectMilestones, + options: projectMilestones.list, + totalCount: projectMilestones.totalCount, + }); + } + + if (groupMilestones?.totalCount) { + milestones.push({ + id: 'group-milestones', + text: this.$options.translations.groupMilestones, + options: groupMilestones.list, + totalCount: groupMilestones.totalCount, + }); + } + + return milestones; + }, selectedMilestonesLabel() { const { selectedMilestones } = this; - const firstMilestoneName = selectedMilestones[0]; + const [firstMilestoneName] = selectedMilestones; if (selectedMilestones.length === 0) { return this.$options.translations.noMilestone; @@ -92,20 +94,6 @@ export default { numberOfOtherMilestones, }); }, - showProjectMilestoneSection() { - return Boolean( - this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error, - ); - }, - showGroupMilestoneSection() { - return ( - this.groupMilestonesEnabled && - Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error) - ); - }, - showNoResults() { - return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection; - }, }, watch: { // Keep the Vuex store synchronized if the parent @@ -127,8 +115,8 @@ export default { // because we need to access the .cancel() method // lodash attaches to the function, which is // made inaccessible by Vue. - this.debouncedSearch = debounce(function search() { - this.search(this.searchQuery); + this.debouncedSearch = debounce(function search(q) { + this.search(q); }, SEARCH_DEBOUNCE_MS); this.setProjectId(this.projectId); @@ -143,22 +131,14 @@ export default { 'setGroupMilestonesAvailable', 'setSelectedMilestones', 'clearSelectedMilestones', - 'toggleMilestones', 'search', 'fetchMilestones', ]), - focusSearchBox() { - this.$refs.searchBox.$el.querySelector('input').focus(); - }, - onSearchBoxEnter() { - this.debouncedSearch.cancel(); - this.search(this.searchQuery); + onSearchBoxInput(q) { + this.debouncedSearch(q); }, - onSearchBoxInput() { - this.debouncedSearch(); - }, - selectMilestone(milestone) { - this.toggleMilestones(milestone); + selectMilestone(milestones) { + this.setSelectedMilestones(milestones); this.$emit('input', this.selectedMilestones); }, selectNoMilestone() { @@ -170,84 +150,42 @@ export default { </script> <template> - <gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox"> - <template #button-content> - <span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{ - selectedMilestonesLabel - }}</span> - <gl-icon name="chevron-down" /> - </template> - - <gl-dropdown-section-header> - <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span> - </gl-dropdown-section-header> - - <gl-dropdown-divider /> - - <gl-search-box-by-type - ref="searchBox" - v-model.trim="searchQuery" - class="gl-m-3" - :placeholder="$options.translations.searchMilestones" - @input="onSearchBoxInput" - @keydown.enter.prevent="onSearchBoxEnter" - /> - - <gl-dropdown-item - :is-checked="selectedMilestones.length === 0" - is-check-item - @click="selectNoMilestone()" - > - {{ $options.translations.noMilestone }} - </gl-dropdown-item> - - <gl-dropdown-divider /> - - <template v-if="isLoading"> - <gl-loading-icon size="sm" /> - <gl-dropdown-divider /> + <gl-collapsible-listbox + :header-text="$options.translations.selectMilestone" + :items="allMilestones" + :reset-button-label="$options.translations.unselect" + :searching="isLoading" + :selected="selectedMilestones" + :toggle-text="selectedMilestonesLabel" + block + multiple + searchable + @reset="selectNoMilestone" + @search="onSearchBoxInput" + @select="selectMilestone" + > + <template #group-label="{ group }"> + <span :data-testid="`${group.id}-section`" + >{{ group.text }}<gl-badge size="sm" class="gl-ml-2">{{ group.totalCount }}</gl-badge></span + > </template> - <template v-else-if="showNoResults"> - <div class="dropdown-item-space"> - <span data-testid="milestone-combobox-no-results" class="gl-pl-6">{{ - $options.translations.noResultsLabel - }}</span> + <template #footer> + <div + class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!" + > + <gl-button + v-for="(item, idx) in extraLinks" + :key="idx" + :href="item.url" + is-check-item + data-testid="milestone-combobox-extra-links" + category="tertiary" + block + class="gl-justify-content-start! gl-mt-2!" + > + {{ item.text }} + </gl-button> </div> - <gl-dropdown-divider /> - </template> - <template v-else> - <milestone-results-section - v-if="showProjectMilestoneSection" - :section-title="$options.translations.projectMilestones" - :total-count="matches.projectMilestones.totalCount" - :items="matches.projectMilestones.list" - :selected-milestones="selectedMilestones" - :error="matches.projectMilestones.error" - :error-message="$options.translations.searchErrorMessage" - data-testid="project-milestones-section" - @selected="selectMilestone($event)" - /> - - <milestone-results-section - v-if="showGroupMilestoneSection" - :section-title="$options.translations.groupMilestones" - :total-count="matches.groupMilestones.totalCount" - :items="matches.groupMilestones.list" - :selected-milestones="selectedMilestones" - :error="matches.groupMilestones.error" - :error-message="$options.translations.searchErrorMessage" - data-testid="group-milestones-section" - @selected="selectMilestone($event)" - /> </template> - <gl-dropdown-item - v-for="(item, idx) in extraLinks" - :key="idx" - :href="item.url" - is-check-item - data-testid="milestone-combobox-extra-links" - > - {{ item.text }} - </gl-dropdown-item> - </gl-dropdown> + </gl-collapsible-listbox> </template> diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js index 1f88c0a1ea6..c0cd58cc5d2 100644 --- a/app/assets/javascripts/milestones/stores/mutations.js +++ b/app/assets/javascripts/milestones/stores/mutations.js @@ -37,7 +37,7 @@ export default { }, [types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) { state.matches.projectMilestones = { - list: response.data.map(({ title }) => ({ title })), + list: response.data.map(({ title }) => ({ text: title, value: title })), totalCount: parseInt(response.headers['x-total'], 10) || response.data.length, error: null, }; @@ -51,7 +51,7 @@ export default { }, [types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) { state.matches.groupMilestones = { - list: response.data.map(({ title }) => ({ title })), + list: response.data.map(({ title }) => ({ text: title, value: title })), totalCount: parseInt(response.headers['x-total'], 10) || response.data.length, error: null, }; diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index c68fbceb4f6..df9f333afe5 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -176,8 +176,7 @@ export default { </p> <form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm"> <tag-field /> - <gl-form-group> - <label for="release-title">{{ __('Release title') }}</label> + <gl-form-group :label="__('Release title')"> <gl-form-input id="release-title" ref="releaseTitleInput" @@ -186,17 +185,14 @@ export default { class="form-control" /> </gl-form-group> - <gl-form-group class="w-50" data-testid="milestones-field"> - <label>{{ __('Milestones') }}</label> - <div class="d-flex flex-column col-md-6 col-sm-10 pl-0"> - <milestone-combobox - v-model="releaseMilestones" - :project-id="projectId" - :group-id="groupId" - :group-milestones-available="groupMilestonesAvailable" - :extra-links="milestoneComboboxExtraLinks" - /> - </div> + <gl-form-group :label="__('Milestones')" class="gl-w-30" data-testid="milestones-field"> + <milestone-combobox + v-model="releaseMilestones" + :project-id="projectId" + :group-id="groupId" + :group-milestones-available="groupMilestonesAvailable" + :extra-links="milestoneComboboxExtraLinks" + /> </gl-form-group> <gl-form-group :label="__('Release date')" label-for="release-released-at"> <template #label-description> @@ -214,8 +210,7 @@ export default { </template> <gl-datepicker id="release-released-at" v-model="releasedAt" :default-date="releasedAt" /> </gl-form-group> - <gl-form-group data-testid="release-notes"> - <label for="release-notes">{{ __('Release notes') }}</label> + <gl-form-group :label="__('Release notes')" data-testid="release-notes"> <div class="common-note-form"> <markdown-field :can-attach-file="true" diff --git a/config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml b/config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml new file mode 100644 index 00000000000..b5f44795c20 --- /dev/null +++ b/config/feature_flags/ops/enforce_ci_builds_pagination_limit.yml @@ -0,0 +1,8 @@ +--- +name: enforce_ci_builds_pagination_limit +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135162 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429453 +milestone: '16.6' +type: ops +group: group::pipeline execution +default_enabled: false diff --git a/doc/development/code_review.md b/doc/development/code_review.md index e5f79a55a06..b9f24c5615f 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -295,6 +295,10 @@ up confusion or verify that the end result matches what they had in mind, to database specialists to get input on the data model or specific queries, or to any other developer to get an in-depth review of the solution. +If you know you'll need many merge requests to deliver a feature (for example, you created a proof of concept and it is clear the feature will consist of 10+ merge requests), +consider identifying reviewers and maintainers who possess the necessary understanding of the feature (you share the context with them). Then direct all merge requests to these reviewers. +The best DRI for finding these reviewers is the EM or Staff Engineer. Having stable reviewer counterparts for multiple merge requests with the same context improves efficiency. + If your merge request touches more than one domain (for example, Dynamic Analysis and GraphQL), ask for reviews from an expert from each domain. If an author is unsure if a merge request needs a [domain expert's](#domain-experts) opinion, diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb index 6f0a2ff7f62..47e4135db27 100644 --- a/lib/api/ci/jobs.rb +++ b/lib/api/ci/jobs.rb @@ -57,7 +57,7 @@ module API builds = filter_builds(builds, params[:scope]) builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project) - present paginate_with_strategies(builds, paginator_params: { without_count: true }), with: Entities::Ci::Job + present paginate_with_strategies(builds, user_project, paginator_params: { without_count: true }), with: Entities::Ci::Job end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb index 5fbc3081ee8..4353ba0e99a 100644 --- a/lib/api/helpers/pagination_strategies.rb +++ b/lib/api/helpers/pagination_strategies.rb @@ -50,7 +50,7 @@ module API offset_limit = limit_for_scope(request_scope) if (Gitlab::Pagination::Keyset.available_for_type?(relation) || cursor_based_keyset_pagination_supported?(relation)) && - cursor_based_keyset_pagination_enforced?(relation) && + cursor_based_keyset_pagination_enforced?(request_scope, relation) && offset_limit_exceeded?(offset_limit) return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \ @@ -65,8 +65,8 @@ module API Gitlab::Pagination::CursorBasedKeyset.available_for_type?(relation) end - def cursor_based_keyset_pagination_enforced?(relation) - Gitlab::Pagination::CursorBasedKeyset.enforced_for_type?(relation) + def cursor_based_keyset_pagination_enforced?(request_scope, relation) + Gitlab::Pagination::CursorBasedKeyset.enforced_for_type?(request_scope, relation) end def keyset_pagination_enabled? diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb index 81dcc54ff35..9e8c0c530a9 100644 --- a/lib/gitlab/pagination/cursor_based_keyset.rb +++ b/lib/gitlab/pagination/cursor_based_keyset.rb @@ -34,8 +34,10 @@ module Gitlab order_satisfied?(relation, cursor_based_request_context) end - def self.enforced_for_type?(relation) - ENFORCED_TYPES.include?(relation.klass) + def self.enforced_for_type?(request_scope, relation) + enforced = ENFORCED_TYPES + enforced += [::Ci::Build] if ::Feature.enabled?(:enforce_ci_builds_pagination_limit, request_scope, type: :ops) + enforced.include?(relation.klass) end def self.order_satisfied?(relation, cursor_based_request_context) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1d7f90efca8..e6a6625e9fa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -30142,27 +30142,15 @@ msgstr "" msgid "Milestone(s) not found: %{milestones}" msgstr "" -msgid "MilestoneCombobox|An error occurred while searching for milestones" -msgstr "" - msgid "MilestoneCombobox|Group milestones" msgstr "" -msgid "MilestoneCombobox|Milestone" -msgstr "" - -msgid "MilestoneCombobox|No matching results" -msgstr "" - msgid "MilestoneCombobox|No milestone" msgstr "" msgid "MilestoneCombobox|Project milestones" msgstr "" -msgid "MilestoneCombobox|Search Milestones" -msgstr "" - msgid "MilestoneCombobox|Select milestone" msgstr "" @@ -50856,6 +50844,9 @@ msgstr "" msgid "Unschedule job" msgstr "" +msgid "Unselect" +msgstr "" + msgid "Unselect \"Expand variable reference\" if you want to use the variable value as a raw string." msgstr "" diff --git a/qa/gdk/Dockerfile.gdk b/qa/gdk/Dockerfile.gdk index 9326beba93f..84d0499c4a6 100644 --- a/qa/gdk/Dockerfile.gdk +++ b/qa/gdk/Dockerfile.gdk @@ -1,4 +1,4 @@ -FROM registry.gitlab.com/gitlab-org/gitlab-development-kit/asdf-bootstrapped-verify:main@sha256:fb833ce8f838e38104ee3b499a99f86e7a5844a4d4d8fdeeababd2d717504f3e as base +FROM registry.gitlab.com/gitlab-org/gitlab-development-kit/asdf-bootstrapped-verify/main:d843a4d237bbb9c2f04d2cbddc89fd6dadeb86cf as base ENV GITLAB_LICENSE_MODE=test \ GDK_KILL_CONFIRM=true diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index 8fe04305083..da88cb22011 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -34,7 +34,13 @@ module QA let!(:source_api_client) { source_admin_api_client } let!(:source_group) do - create(:sandbox, api_client: source_api_client, path: gitlab_source_group) + paths = gitlab_source_group.split("/") + sandbox = build(:sandbox, api_client: source_api_client, path: paths.first).reload! + next sandbox if paths.size == 1 + + paths[1..].each_with_object([sandbox]) do |path, arr| + arr << build(:group, api_client: source_api_client, sandbox: arr.last, path: path).reload! + end.last end # generate unique target group because source group has a static name diff --git a/spec/features/markdown/keyboard_shortcuts_spec.rb b/spec/features/markdown/keyboard_shortcuts_spec.rb index 6f128e16041..ba88278199a 100644 --- a/spec/features/markdown/keyboard_shortcuts_spec.rb +++ b/spec/features/markdown/keyboard_shortcuts_spec.rb @@ -97,8 +97,8 @@ RSpec.describe 'Markdown keyboard shortcuts', :js, feature_category: :team_plann context 'Vue.js markdown editor' do let(:path_to_visit) { new_project_release_path(project) } - let(:markdown_field) { find_field('Release notes') } - let(:non_markdown_field) { find_field('Release title') } + let(:markdown_field) { find_field('release-notes') } + let(:non_markdown_field) { find_field('release-title') } it_behaves_like 'keyboard shortcuts' it_behaves_like 'no side effects' diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb index 678c8df666f..319053ddcb5 100644 --- a/spec/features/projects/releases/user_creates_release_spec.rb +++ b/spec/features/projects/releases/user_creates_release_spec.rb @@ -148,8 +148,7 @@ RSpec.describe 'User creates release', :js, feature_category: :continuous_delive fill_release_title(release_title) - select_milestone(milestone_1.title) - select_milestone(milestone_2.title) + select_milestones(milestone_1.title, milestone_2.title) fill_release_notes(release_notes) diff --git a/spec/features/projects/releases/user_views_edit_release_spec.rb b/spec/features/projects/releases/user_views_edit_release_spec.rb index b3f21a2d328..203b4ce82e3 100644 --- a/spec/features/projects/releases/user_views_edit_release_spec.rb +++ b/spec/features/projects/releases/user_views_edit_release_spec.rb @@ -20,8 +20,8 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery end def fill_out_form_and_click(button_to_click) - fill_in 'Release title', with: 'Updated Release title' - fill_in 'Release notes', with: 'Updated Release notes' + fill_in 'release-title', with: 'Updated Release title', fill_options: { clear: :backspace } + fill_in 'release-notes', with: 'Updated Release notes' click_link_or_button button_to_click @@ -44,8 +44,8 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery expect(page).to have_content('Releases are based on Git tags. We recommend tags that use semantic versioning, for example 1.0.0, 2.1.0-pre.') expect(find_field('Tag name', disabled: true).value).to eq(release.tag) - expect(find_field('Release title').value).to eq(release.name) - expect(find_field('Release notes').value).to eq(release.description) + expect(find_field('release-title').value).to eq(release.name) + expect(find_field('release-notes').value).to eq(release.description) expect(page).to have_button('Save changes') expect(page).to have_link('Cancel') diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js index 53abf6dc544..3e2cd354d53 100644 --- a/spec/frontend/milestones/components/milestone_combobox_spec.js +++ b/spec/frontend/milestones/components/milestone_combobox_spec.js @@ -1,12 +1,11 @@ -import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; +import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { ENTER_KEY } from '~/lib/utils/keys'; import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue'; import createStore from '~/milestones/stores/'; import { projectMilestones, groupMilestones } from '../mock_data'; @@ -52,9 +51,6 @@ describe('Milestone combobox component', () => { wrapper.setProps({ value: selectedMilestone }); }, }, - stubs: { - GlSearchBoxByType: true, - }, store: createStore(), }); }; @@ -89,57 +85,25 @@ describe('Milestone combobox component', () => { // // Finders // - const findButtonContent = () => wrapper.find('[data-testid="milestone-combobox-button-content"]'); - - const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]'); - + const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findButtonContent = () => wrapper.find('[data-testid="base-dropdown-toggle"]'); + const findNoResults = () => wrapper.find('[data-testid="listbox-no-results-text"]'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findProjectMilestonesSection = () => - wrapper.find('[data-testid="project-milestones-section"]'); - const findProjectMilestonesDropdownItems = () => - findProjectMilestonesSection().findAllComponents(GlDropdownItem); - const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0); - - const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]'); - const findGroupMilestonesDropdownItems = () => - findGroupMilestonesSection().findAllComponents(GlDropdownItem); - const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0); - - // - // Expecters - // - const projectMilestoneSectionContainsErrorMessage = () => { - const projectMilestoneSection = findProjectMilestonesSection(); - - return projectMilestoneSection - .text() - .includes('An error occurred while searching for milestones'); - }; - - const groupMilestoneSectionContainsErrorMessage = () => { - const groupMilestoneSection = findGroupMilestonesSection(); - - return groupMilestoneSection - .text() - .includes('An error occurred while searching for milestones'); - }; + findGlCollapsibleListbox().find('[data-testid="project-milestones-section"]'); + const findGroupMilestonesSection = () => + findGlCollapsibleListbox().find('[data-testid="group-milestones-section"]'); + const findDropdownItems = () => findGlCollapsibleListbox().findAllComponents(GlListboxItem); // // Convenience methods // const updateQuery = (newQuery) => { - findSearchBox().vm.$emit('input', newQuery); + findGlCollapsibleListbox().vm.$emit('search', newQuery); }; - const selectFirstProjectMilestone = () => { - findFirstProjectMilestonesDropdownItem().vm.$emit('click'); - }; - - const selectFirstGroupMilestone = () => { - findFirstGroupMilestonesDropdownItem().vm.$emit('click'); + const selectItem = (item) => { + findGlCollapsibleListbox().vm.$emit('select', item); }; const waitForRequests = async ({ andClearMocks } = { andClearMocks: false }) => { @@ -224,22 +188,6 @@ describe('Milestone combobox component', () => { }); }); - describe('when the Enter is pressed', () => { - beforeEach(() => { - createComponent(); - - return waitForRequests({ andClearMocks: true }); - }); - - it('requeries the search when Enter is pressed', () => { - findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); - - return waitForRequests().then(() => { - expect(searchApiCallSpy).toHaveBeenCalledTimes(1); - }); - }); - }); - describe('when no results are found', () => { beforeEach(() => { projectMilestonesApiCallSpy = jest @@ -257,7 +205,7 @@ describe('Milestone combobox component', () => { describe('when the search query is empty', () => { it('renders a "no results" message', () => { - expect(findNoResults().text()).toBe('No matching results'); + expect(findNoResults().text()).toBe('No results found'); }); }); }); @@ -275,22 +223,14 @@ describe('Milestone combobox component', () => { }); it('renders the "Project milestones" heading with a total number indicator', () => { - expect( - findProjectMilestonesSection() - .find('[data-testid="milestone-results-section-header"]') - .text(), - ).toBe('Project milestones 6'); - }); - - it("does not render an error message in the project milestone section's body", () => { - expect(projectMilestoneSectionContainsErrorMessage()).toBe(false); + expect(findProjectMilestonesSection().text()).toBe('Project milestones 6'); }); it('renders each project milestones as a selectable item', () => { - const dropdownItems = findProjectMilestonesDropdownItems(); + const dropdownItems = findDropdownItems(); - projectMilestones.forEach((milestone, i) => { - expect(dropdownItems.at(i).text()).toBe(milestone.title); + projectMilestones.forEach((milestone) => { + expect(dropdownItems.filter((x) => x.text() === milestone.title).exists()).toBe(true); }); }); }); @@ -323,12 +263,8 @@ describe('Milestone combobox component', () => { return waitForRequests(); }); - it('renders the project milestones section in the dropdown', () => { - expect(findProjectMilestonesSection().exists()).toBe(true); - }); - - it("renders an error message in the project milestones section's body", () => { - expect(projectMilestoneSectionContainsErrorMessage()).toBe(true); + it('does not render the project milestones section in the dropdown', () => { + expect(findProjectMilestonesSection().exists()).toBe(false); }); }); @@ -339,52 +275,24 @@ describe('Milestone combobox component', () => { return waitForRequests(); }); - it('renders a checkmark by the selected item', async () => { - selectFirstProjectMilestone(); - - await nextTick(); - - expect( - findFirstProjectMilestonesDropdownItem() - .find('svg') - .classes('gl-dropdown-item-check-icon'), - ).toBe(true); - - selectFirstProjectMilestone(); - - await nextTick(); - - expect( - findFirstProjectMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'), - ).toBe(true); - }); + describe('when a project milestone is selected', () => { + const item = 'v1.0'; - describe('when a project milestones is selected', () => { beforeEach(() => { createComponent(); projectMilestonesApiCallSpy = jest .fn() .mockReturnValue([HTTP_STATUS_OK, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]); + selectItem([item]); return waitForRequests(); }); - it("displays the project milestones name in the dropdown's button", async () => { - selectFirstProjectMilestone(); - await nextTick(); - - expect(findButtonContent().text()).toBe('v1.0'); - - selectFirstProjectMilestone(); - await nextTick(); - - expect(findButtonContent().text()).toBe('No milestone'); + it("displays the project milestones name in the dropdown's button", () => { + expect(findButtonContent().text()).toBe(item); }); - it('updates the v-model binding with the project milestone title', async () => { - selectFirstProjectMilestone(); - await nextTick(); - + it('updates the v-model binding with the project milestone title', () => { expect(wrapper.emitted().input[0][0]).toStrictEqual(['v1.0']); }); }); @@ -404,22 +312,14 @@ describe('Milestone combobox component', () => { }); it('renders the "Group milestones" heading with a total number indicator', () => { - expect( - findGroupMilestonesSection() - .find('[data-testid="milestone-results-section-header"]') - .text(), - ).toBe('Group milestones 6'); - }); - - it("does not render an error message in the group milestone section's body", () => { - expect(groupMilestoneSectionContainsErrorMessage()).toBe(false); + expect(findGroupMilestonesSection().text()).toBe('Group milestones 6'); }); it('renders each group milestones as a selectable item', () => { - const dropdownItems = findGroupMilestonesDropdownItems(); + const dropdownItems = findDropdownItems(); - groupMilestones.forEach((milestone, i) => { - expect(dropdownItems.at(i).text()).toBe(milestone.title); + groupMilestones.forEach((milestone) => { + expect(dropdownItems.filter((x) => x.text() === milestone.title).exists()).toBe(true); }); }); }); @@ -452,74 +352,8 @@ describe('Milestone combobox component', () => { return waitForRequests(); }); - it('renders the group milestones section in the dropdown', () => { - expect(findGroupMilestonesSection().exists()).toBe(true); - }); - - it("renders an error message in the group milestones section's body", () => { - expect(groupMilestoneSectionContainsErrorMessage()).toBe(true); - }); - }); - - describe('selection', () => { - beforeEach(() => { - createComponent(); - - return waitForRequests(); - }); - - it('renders a checkmark by the selected item', async () => { - selectFirstGroupMilestone(); - - await nextTick(); - - expect( - findFirstGroupMilestonesDropdownItem() - .find('svg') - .classes('gl-dropdown-item-check-icon'), - ).toBe(true); - - selectFirstGroupMilestone(); - - await nextTick(); - - expect( - findFirstGroupMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'), - ).toBe(true); - }); - - describe('when a group milestones is selected', () => { - beforeEach(() => { - createComponent(); - groupMilestonesApiCallSpy = jest - .fn() - .mockReturnValue([ - HTTP_STATUS_OK, - [{ title: 'group-v1.0' }], - { [X_TOTAL_HEADER]: '1' }, - ]); - - return waitForRequests(); - }); - - it("displays the group milestones name in the dropdown's button", async () => { - selectFirstGroupMilestone(); - await nextTick(); - - expect(findButtonContent().text()).toBe('group-v1.0'); - - selectFirstGroupMilestone(); - await nextTick(); - - expect(findButtonContent().text()).toBe('No milestone'); - }); - - it('updates the v-model binding with the group milestone title', async () => { - selectFirstGroupMilestone(); - await nextTick(); - - expect(wrapper.emitted().input[0][0]).toStrictEqual(['group-v1.0']); - }); + it('does not render the group milestones section', () => { + expect(findGroupMilestonesSection().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js index a53d6ca5de1..e3a5e4d00f4 100644 --- a/spec/frontend/milestones/stores/mutations_spec.js +++ b/spec/frontend/milestones/stores/mutations_spec.js @@ -163,10 +163,12 @@ describe('Milestones combobox Vuex store mutations', () => { expect(state.matches.projectMilestones).toEqual({ list: [ { - title: 'v0.1', + text: 'v0.1', + value: 'v0.1', }, { - title: 'v0.2', + text: 'v0.2', + value: 'v0.2', }, ], error: null, @@ -192,10 +194,12 @@ describe('Milestones combobox Vuex store mutations', () => { expect(state.matches.projectMilestones).toEqual({ list: [ { - title: 'v0.1', + text: 'v0.1', + value: 'v0.1', }, { - title: 'v0.2', + text: 'v0.2', + value: 'v0.2', }, ], error: null, @@ -245,10 +249,12 @@ describe('Milestones combobox Vuex store mutations', () => { expect(state.matches.groupMilestones).toEqual({ list: [ { - title: 'group-0.1', + text: 'group-0.1', + value: 'group-0.1', }, { - title: 'group-0.2', + text: 'group-0.2', + value: 'group-0.2', }, ], error: null, @@ -274,10 +280,12 @@ describe('Milestones combobox Vuex store mutations', () => { expect(state.matches.groupMilestones).toEqual({ list: [ { - title: 'group-0.1', + text: 'group-0.1', + value: 'group-0.1', }, { - title: 'group-0.2', + text: 'group-0.2', + value: 'group-0.2', }, ], error: null, diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb index effe767e41d..e5958549a81 100644 --- a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb +++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb @@ -28,7 +28,9 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do end describe '.enforced_for_type?' do - subject { described_class.enforced_for_type?(relation) } + let_it_be(:project) { create(:project) } + + subject { described_class.enforced_for_type?(project, relation) } context 'when relation is Group' do let(:relation) { Group.all } @@ -45,7 +47,21 @@ RSpec.describe Gitlab::Pagination::CursorBasedKeyset do context 'when relation is Ci::Build' do let(:relation) { Ci::Build.all } - it { is_expected.to be false } + before do + stub_feature_flags(enforce_ci_builds_pagination_limit: false) + end + + context 'when feature flag enforce_ci_builds_pagination_limit is enabled' do + before do + stub_feature_flags(enforce_ci_builds_pagination_limit: project) + end + + it { is_expected.to be true } + end + + context 'when feature fllag enforce_ci_builds_pagination_limit is disabled' do + it { is_expected.to be false } + end end end diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb index d5846aad15d..c3fbd128a28 100644 --- a/spec/support/helpers/features/releases_helpers.rb +++ b/spec/support/helpers/features/releases_helpers.rb @@ -44,20 +44,22 @@ module Features end def fill_release_title(release_title) - fill_in('Release title', with: release_title) + fill_in('release-title', with: release_title) end - def select_milestone(milestone_title) - page.within '[data-testid="milestones-field"]' do - find('button').click + def select_milestones(*milestone_titles) + within_testid 'milestones-field' do + find_by_testid('base-dropdown-toggle').click wait_for_all_requests - find('input[aria-label="Search Milestones"]').set(milestone_title) + milestone_titles.each do |milestone_title| + find('input[type="search"]').set(milestone_title) - wait_for_all_requests + wait_for_all_requests - find('button', text: milestone_title, match: :first).click + find('[role="option"]', text: milestone_title).click + end end end |