From 90e793301a277d8d88be2003c455bcbf9d007f7e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 26 Apr 2023 21:09:38 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../import_project_members_modal.vue | 10 +- .../pages/admin/jobs/components/constants.js | 4 + .../components/table/admin_jobs_table_app.vue | 41 +++-- .../queries/get_all_jobs.query.graphql | 1 - .../queries/get_all_jobs_count.query.graphql | 5 + .../super_sidebar/components/help_center.vue | 4 +- .../projects_list/projects_list.vue | 1 + .../projects_list/projects_list_item.vue | 86 ++++++++++- .../sign_and_verify_ansi2json_state.yml | 2 +- doc/development/database/required_stops.md | 26 ++++ doc/development/go_guide/go_upgrade.md | 1 + .../visibility_and_access_controls.md | 6 +- .../merge_requests/creating_merge_requests.md | 2 +- locale/gitlab.pot | 14 +- ...very_alert_closes_correct_incident_spec.rb | 12 +- .../deprecated_jquery_dropdown_spec.js | 5 +- spec/frontend/dropzone_input_spec.js | 5 +- spec/frontend/fixtures/jobs.rb | 60 +++---- .../import_project_members_modal_spec.js | 17 ++ spec/frontend/jobs/mock_data.js | 2 + .../table/admin_job_table_app_spec.js | 84 +++++++++- .../dropdown_contents_labels_view_spec.js | 146 +++++++++--------- .../components/help_center_spec.js | 8 +- .../projects_list/projects_list_item_spec.js | 66 +++++++- 24 files changed, 457 insertions(+), 151 deletions(-) create mode 100644 app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql diff --git a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue index 4eceabc9fb5..ffd3a2caa7f 100644 --- a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue @@ -2,7 +2,7 @@ import { GlFormGroup, GlModal, GlSprintf } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { importProjectMembers } from '~/api/projects_api'; -import { BV_SHOW_MODAL } from '~/lib/utils/constants'; +import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { s__, __, sprintf } from '~/locale'; import eventHub from '../event_hub'; import { @@ -81,11 +81,17 @@ export default { openModal() { this.$root.$emit(BV_SHOW_MODAL, this.$options.modalId); }, + closeModal() { + this.$root.$emit(BV_HIDE_MODAL, this.$options.modalId); + }, resetFields() { this.invalidFeedbackMessage = ''; this.projectToBeImported = {}; }, - submitImport() { + submitImport(e) { + // We never want to hide when submitting + e.preventDefault(); + this.isLoading = true; return importProjectMembers(this.projectId, this.projectToBeImported.id) .then(this.onInviteSuccess) diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/pages/admin/jobs/components/constants.js index 61d5e329fc0..57220f0727c 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/constants.js +++ b/app/assets/javascripts/pages/admin/jobs/components/constants.js @@ -1,6 +1,10 @@ import { s__, __ } from '~/locale'; import { DEFAULT_FIELDS, RAW_TEXT_WARNING } from '~/jobs/components/table/constants'; +export const JOBS_COUNT_ERROR_MESSAGE = __('There was an error fetching the number of jobs.'); +export const JOBS_FETCH_ERROR_MSG = __('There was an error fetching the jobs.'); +export const LOADING_ARIA_LABEL = __('Loading'); +export const CANCELABLE_JOBS_ERROR_MSG = __('There was an error fetching the cancelable jobs.'); export const CANCEL_JOBS_MODAL_ID = 'cancel-jobs-modal'; export const CANCEL_JOBS_MODAL_TITLE = s__('AdminArea|Are you sure?'); export const CANCEL_JOBS_BUTTON_TEXT = s__('AdminArea|Cancel all jobs'); diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue index da6739aad8b..e56ec5375c2 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue +++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue @@ -1,6 +1,5 @@ @@ -111,6 +158,43 @@ export default { accessLevelLabel }} +
+
+ {{ $options.i18n.topics }}: +
+ + {{ topicTitle(topic) }} + +
+ +
+
3 weeks, or after a before the last stop), +these scenarios may cause issues: + +- If the migration depends on another migration with a newer timestamp but introduced in a + previous release _after_ a required stop, then the new migration may run sequentially sooner + than the prerequisite migration, and thus fail. +- If the migration timestamp ID is before the last, it may be inadvertently squashed when the + team squashes other migrations from the required stop. + +- **Cause:** The migration may fail if it depends on a migration with a later timestamp introduced + in an earlier version. Or, the migration may be inadvertently squashed after a required stop. +- **Mitigation:** Aim for migration timestamps to fall inside the release dates and be sure that + they are not dated prior to the last required stop. + ### Bugs in migration related tooling In a few circumstances, bugs in migration related tooling has required us to introduce stops. While we aim diff --git a/doc/development/go_guide/go_upgrade.md b/doc/development/go_guide/go_upgrade.md index f71fe7b8dac..7fc18604a3d 100644 --- a/doc/development/go_guide/go_upgrade.md +++ b/doc/development/go_guide/go_upgrade.md @@ -150,6 +150,7 @@ if you need help finding the correct person or labels: | [Alertmanager](https://github.com/prometheus/alertmanager) | [Issue Tracker](https://gitlab.com/gitlab-org/gitlab/-/issues) | | Docker Distribution Pruner | [Issue Tracker](https://gitlab.com/gitlab-org/docker-distribution-pruner) | | Gitaly | [Issue Tracker](https://gitlab.com/gitlab-org/gitaly/-/issues) | +| GitLab CLI (`glab`). | [Issue Tracker](https://gitlab.com/gitlab-org/cli/-/issues) | GitLab Compose Kit | [Issuer Tracker](https://gitlab.com/gitlab-org/gitlab-compose-kit/-/issues) | | GitLab Container Registry | [Issue Tracker](https://gitlab.com/gitlab-org/container-registry) | | GitLab Elasticsearch Indexer | [Issue Tracker](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer/-/issues) | diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index 07067945ea6..dfb23a4017e 100644 --- a/doc/user/admin_area/settings/visibility_and_access_controls.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -161,6 +161,10 @@ For more details on group visibility, see ## Restrict visibility levels +When restricting visibility levels, consider how these restrictions interact +with permissions for subgroups and projects that inherit their visibility from +the item you're changing. + To restrict visibility levels for groups, projects, snippets, and selected pages: 1. Sign in to GitLab as a user with Administrator access level. @@ -181,7 +185,7 @@ To restrict visibility levels for groups, projects, snippets, and selected pages 1. Select **Save changes**. For more details on project visibility, see -[Project visibility](../../public_access.md). +[Project visibility](../../public_access.md). ## Configure allowed import sources diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md index 8a4a61bb80d..4ac6c6e0aa2 100644 --- a/doc/user/project/merge_requests/creating_merge_requests.md +++ b/doc/user/project/merge_requests/creating_merge_requests.md @@ -83,7 +83,7 @@ You can create a merge request when you add, edit, or upload a file to a reposit 1. [Add, edit, or upload](../repository/web_editor.md) a file to the repository. 1. In the **Commit message**, enter a reason for the commit. -1. Select the **Target branch** or create a new branch by typing the name (without spaces, capital letters, or special chars). +1. Select the **Target branch** or create a new branch by typing the name (without spaces). 1. Select the **Start a new merge request with these changes** checkbox or toggle. This checkbox or toggle is visible only if the target is not the same as the source branch, or if the source branch is protected. 1. Select **Commit changes**. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 339137cf065..68916760cb3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5910,7 +5910,7 @@ msgstr "" msgid "Ask someone with write access to resolve it." msgstr "" -msgid "Ask the Tanuki Bot" +msgid "Ask the GitLab Chat" msgstr "" msgid "Ask your group owner to set up a group runner." @@ -43734,15 +43734,15 @@ msgstr "" msgid "TanukiBot|For example, %{linkStart}what is a fork?%{linkEnd}" msgstr "" +msgid "TanukiBot|GitLab Chat" +msgstr "" + msgid "TanukiBot|Give feedback" msgstr "" msgid "TanukiBot|Sources" msgstr "" -msgid "TanukiBot|Tanuki Bot" -msgstr "" - msgid "TanukiBot|There was an error communicating with Tanuki Bot. Please reach out to GitLab support for more assistance or try again later." msgstr "" @@ -44990,6 +44990,9 @@ msgstr "" msgid "There was an error fetching the %{replicableType}" msgstr "" +msgid "There was an error fetching the cancelable jobs." +msgstr "" + msgid "There was an error fetching the deploy freezes." msgstr "" @@ -45008,6 +45011,9 @@ msgstr "" msgid "There was an error fetching the number of jobs for your project." msgstr "" +msgid "There was an error fetching the number of jobs." +msgstr "" + msgid "There was an error fetching the top labels for the selected group" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb index 695a73de078..191da25c57b 100644 --- a/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb +++ b/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true module QA - RSpec.describe 'Monitor', product_group: :respond do + RSpec.describe 'Monitor', product_group: :respond, quarantine: { + type: :bug, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/395512' + } do describe 'Recovery alert' do shared_examples 'triggers recovery alert' do it 'only closes the correct incident', :aggregate_failures do @@ -31,12 +34,7 @@ module QA context( 'when using HTTP endpoint integration', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/393842', - quarantine: { - only: { pipeline: :nightly }, - type: :bug, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/403596' - } + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/393842' ) do include_context 'sends and resolves test alerts' diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js index 439c20e0fb5..44279ec7915 100644 --- a/spec/frontend/deprecated_jquery_dropdown_spec.js +++ b/spec/frontend/deprecated_jquery_dropdown_spec.js @@ -1,8 +1,9 @@ /* eslint-disable no-param-reassign */ import $ from 'jquery'; +import htmlDeprecatedJqueryDropdown from 'test_fixtures_static/deprecated_jquery_dropdown.html'; import mockProjects from 'test_fixtures_static/projects.json'; -import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import '~/lib/utils/common_utils'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -65,7 +66,7 @@ describe('deprecatedJQueryDropdown', () => { } beforeEach(() => { - loadHTMLFixture('static/deprecated_jquery_dropdown.html'); + setHTMLFixture(htmlDeprecatedJqueryDropdown); test.dropdownContainerElement = $('.dropdown.inline'); test.$dropdownMenuElement = $('.dropdown-menu', test.dropdownContainerElement); test.projectsData = JSON.parse(JSON.stringify(mockProjects)); diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js index 179ba917e7f..57debf79c7b 100644 --- a/spec/frontend/dropzone_input_spec.js +++ b/spec/frontend/dropzone_input_spec.js @@ -1,7 +1,8 @@ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; +import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html'; import mock from 'xhr-mock'; -import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table'; @@ -48,7 +49,7 @@ describe('dropzone_input', () => { }; beforeEach(() => { - loadHTMLFixture('milestones/new-milestone.html'); + setHTMLFixture(htmlNewMilestone); form = $('#new_milestone'); form.data('uploads-path', TEST_UPLOAD_PATH); diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb index 15bbec9b9f2..6c0b87c5a68 100644 --- a/spec/frontend/fixtures/jobs.rb +++ b/spec/frontend/fixtures/jobs.rb @@ -48,7 +48,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do let!(:with_artifact) { create(:ci_build, :success, name: 'with_artifact', job_artifacts: [artifact], pipeline: pipeline) } let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) } - shared_examples 'graphql queries' do |path, jobs_query| + shared_examples 'graphql queries' do |path, jobs_query, skip_non_defaults = false| let_it_be(:variables) { {} } let_it_be(:success_path) { '' } @@ -65,25 +65,27 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end - it "#{fixtures_path}#{jobs_query}.as_guest.json" do - guest = create(:user) - project.add_guest(guest) + context 'with non default fixtures', if: !skip_non_defaults do + it "#{fixtures_path}#{jobs_query}.as_guest.json" do + guest = create(:user) + project.add_guest(guest) - post_graphql(query, current_user: guest, variables: variables) + post_graphql(query, current_user: guest, variables: variables) - expect_graphql_errors_to_be_empty - end + expect_graphql_errors_to_be_empty + end - it "#{fixtures_path}#{jobs_query}.paginated.json" do - post_graphql(query, current_user: user, variables: variables.merge({ first: 2 })) + it "#{fixtures_path}#{jobs_query}.paginated.json" do + post_graphql(query, current_user: user, variables: variables.merge({ first: 2 })) - expect_graphql_errors_to_be_empty - end + expect_graphql_errors_to_be_empty + end - it "#{fixtures_path}#{jobs_query}.empty.json" do - post_graphql(query, current_user: user, variables: variables.merge({ first: 0 })) + it "#{fixtures_path}#{jobs_query}.empty.json" do + post_graphql(query, current_user: user, variables: variables.merge({ first: 0 })) - expect_graphql_errors_to_be_empty + expect_graphql_errors_to_be_empty + end end end @@ -92,37 +94,25 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do let(:success_path) { %w[project jobs] } end + it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs_count.query.graphql', true do + let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } } + let(:success_path) { %w[project jobs] } + end + it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs.query.graphql' do let(:user) { create(:admin) } let(:success_path) { 'jobs' } end - it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_cancelable_jobs_count.query.graphql' do + it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_cancelable_jobs_count.query.graphql', true do let(:variables) { { statuses: %w[PENDING RUNNING] } } let(:user) { create(:admin) } let(:success_path) { %w[cancelable count] } end - end - describe 'get_jobs_count.query.graphql', type: :request do - let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) } - let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) } - let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) } - - fixtures_path = 'graphql/jobs/' - get_jobs_count_query = 'get_jobs_count.query.graphql' - full_path = 'frontend-fixtures/builds-project' - - let_it_be(:query) do - get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_count_query}") - end - - it "#{fixtures_path}#{get_jobs_count_query}.json" do - post_graphql(query, current_user: user, variables: { - fullPath: full_path - }) - - expect_graphql_errors_to_be_empty + it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs_count.query.graphql', true do + let(:user) { create(:admin) } + let(:success_path) { 'jobs' } end end end diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js index 74cb59a9b52..73634855850 100644 --- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js +++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js @@ -1,6 +1,8 @@ import { GlFormGroup, GlSprintf, GlModal } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; +import { createWrapper } from '@vue/test-utils'; +import { BV_HIDE_MODAL } from '~/lib/utils/constants'; import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -107,6 +109,15 @@ describe('ImportProjectMembersModal', () => { }); describe('submitting the import', () => { + it('prevents closing', () => { + const evt = { preventDefault: jest.fn() }; + createComponent(); + + findGlModal().vm.$emit('primary', evt); + + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + describe('when the import is successful with reloadPageOnSubmit', () => { beforeEach(() => { createComponent({ @@ -161,6 +172,12 @@ describe('ImportProjectMembersModal', () => { ); }); + it('hides the modal', () => { + const rootWrapper = createWrapper(wrapper.vm.$root); + + expect(rootWrapper.emitted(BV_HIDE_MODAL)).toHaveLength(1); + }); + it('does not call displaySuccessfulInvitationAlert on mount', () => { expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index de0c06e001a..253e669e889 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -1,4 +1,5 @@ import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json'; +import mockAllJobsCount from 'test_fixtures/graphql/jobs/get_all_jobs_count.query.graphql.json'; import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json'; import mockAllJobsEmpty from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.empty.json'; import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json'; @@ -22,6 +23,7 @@ export const mockJobsNodes = mockJobs.data.project.jobs.nodes; export const mockAllJobsNodes = mockAllJobs.data.jobs.nodes; export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes; export const mockJobsCountResponse = mockJobsCount; +export const mockAllJobsCountResponse = mockAllJobsCount; export const mockCancelableJobsCountResponse = mockCancelableJobsCount; export const stages = [ diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js index d4b581c3fcf..44a5878e6f2 100644 --- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js +++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js @@ -3,11 +3,11 @@ import { mount, shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { s__ } from '~/locale'; import waitForPromises from 'helpers/wait_for_promises'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue'; import getAllJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql'; +import getAllJobsCount from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql'; import getCancelableJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql'; import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue'; import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue'; @@ -16,12 +16,20 @@ import { createAlert } from '~/alert'; import { TEST_HOST } from 'spec/test_constants'; import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue'; import * as urlUtils from '~/lib/utils/url_utility'; +import { + JOBS_FETCH_ERROR_MSG, + CANCELABLE_JOBS_ERROR_MSG, + LOADING_ARIA_LABEL, + RAW_TEXT_WARNING_ADMIN, + JOBS_COUNT_ERROR_MESSAGE, +} from '~/pages/admin/jobs/components/constants'; import { mockAllJobsResponsePaginated, mockCancelableJobsCountResponse, mockAllJobsResponseEmpty, statuses, mockFailedSearchToken, + mockAllJobsCountResponse, } from '../../../../../jobs/mock_data'; Vue.use(VueApollo); @@ -35,6 +43,7 @@ describe('Job table app', () => { const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); const cancelHandler = jest.fn().mockResolvedValue(mockCancelableJobsCountResponse); const emptyHandler = jest.fn().mockResolvedValue(mockAllJobsResponseEmpty); + const countSuccessHandler = jest.fn().mockResolvedValue(mockAllJobsCountResponse); const findSkeletonLoader = () => wrapper.findComponent(JobsSkeletonLoader); const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); @@ -48,10 +57,11 @@ describe('Job table app', () => { const triggerInfiniteScroll = () => wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); - const createMockApolloProvider = (handler, cancelableHandler) => { + const createMockApolloProvider = (handler, cancelableHandler, countHandler) => { const requestHandlers = [ [getAllJobsQuery, handler], [getCancelableJobsQuery, cancelableHandler], + [getAllJobsCount, countHandler], ]; return createMockApollo(requestHandlers); @@ -60,6 +70,7 @@ describe('Job table app', () => { const createComponent = ({ handler = successHandler, cancelableHandler = cancelHandler, + countHandler = countSuccessHandler, mountFn = shallowMount, data = {}, } = {}) => { @@ -72,7 +83,7 @@ describe('Job table app', () => { provide: { jobStatuses: statuses, }, - apolloProvider: createMockApolloProvider(handler, cancelableHandler), + apolloProvider: createMockApolloProvider(handler, cancelableHandler, countHandler), }); }; @@ -133,6 +144,7 @@ describe('Job table app', () => { const pageSize = 50; expect(findLoadingSpinner().exists()).toBe(true); + expect(findLoadingSpinner().attributes('aria-label')).toBe(LOADING_ARIA_LABEL); await waitForPromises(); @@ -172,9 +184,57 @@ describe('Job table app', () => { await waitForPromises(); - expect(findAlert().text()).toBe('There was an error fetching the jobs.'); + expect(findAlert().text()).toBe(JOBS_FETCH_ERROR_MSG); expect(findTable().exists()).toBe(false); }); + + it('should show an alert if there is an error fetching the jobs count data', async () => { + createComponent({ handler: successHandler, countHandler: failedHandler }); + + await waitForPromises(); + + expect(findAlert().text()).toBe(JOBS_COUNT_ERROR_MESSAGE); + }); + + it('should show an alert if there is an error fetching the cancelable jobs data', async () => { + createComponent({ handler: successHandler, cancelableHandler: failedHandler }); + + await waitForPromises(); + + expect(findAlert().text()).toBe(CANCELABLE_JOBS_ERROR_MSG); + }); + + it('jobs table should still load if count query fails', async () => { + createComponent({ handler: successHandler, countHandler: failedHandler }); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + }); + + it('jobs table should still load if cancel query fails', async () => { + createComponent({ handler: successHandler, cancelableHandler: failedHandler }); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + }); + + it('jobs count should be zero if count query fails', async () => { + createComponent({ handler: successHandler, countHandler: failedHandler }); + + await waitForPromises(); + + expect(findTabs().props('allJobsCount')).toBe(0); + }); + + it('cancel button should be hidden if query fails', async () => { + createComponent({ handler: successHandler, cancelableHandler: failedHandler }); + + await waitForPromises(); + + expect(findCancelJobsButton().exists()).toBe(false); + }); }); describe('cancel jobs button', () => { @@ -233,11 +293,21 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); }); + it('refetches jobs count query when filtering', async () => { + createComponent(); + + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + }); + it('shows raw text warning when user inputs raw text', async () => { const expectedWarning = { - message: s__( - 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.', - ), + message: RAW_TEXT_WARNING_ADMIN, type: 'warning', }; diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js index 1fa613d15d4..5c6358a94ab 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -81,8 +81,26 @@ describe('DropdownContentsLabelsView', () => { } }; - describe('computed', () => { - describe('visibleLabels', () => { + describe('component', () => { + it('calls `focusInput` on searchInput field when the component appears', async () => { + findIntersectionObserver().vm.$emit('appear'); + + await nextTick(); + + expect(focusInputMock).toHaveBeenCalled(); + }); + + it('removes loaded labels when the component disappears', async () => { + jest.spyOn(store, 'dispatch'); + + await findIntersectionObserver().vm.$emit('disappear'); + + expect(store.dispatch).toHaveBeenCalledWith(expect.anything(), []); + }); + }); + + describe('labels', () => { + describe('when it is visible', () => { beforeEach(() => { createComponent(undefined, mountExtended); store.dispatch('receiveLabelsSuccess', mockLabels); @@ -112,6 +130,29 @@ describe('DropdownContentsLabelsView', () => { }); }); + describe('when it is clicked', () => { + beforeEach(() => { + createComponent(undefined, mountExtended); + store.dispatch('receiveLabelsSuccess', mockLabels); + }); + + it('calls action `updateSelectedLabels` with provided `label` param', () => { + findLabelItems().at(0).findComponent(GlLink).vm.$emit('click'); + + expect(updateSelectedLabelsMock).toHaveBeenCalledWith(expect.anything(), [ + { ...mockLabels[0], indeterminate: expect.anything(), set: expect.anything() }, + ]); + }); + + it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => { + store.state.allowMultiselect = false; + + findLabelItems().at(0).findComponent(GlLink).vm.$emit('click'); + + expect(toggleDropdownContentsMock).toHaveBeenCalled(); + }); + }); + describe('showNoMatchingResultsMessage', () => { it.each` searchKey | labels | labelsDescription | returnValue @@ -132,47 +173,37 @@ describe('DropdownContentsLabelsView', () => { }); }); - describe('methods', () => { + describe('create label link', () => { + it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', async () => { + jest.spyOn(store, 'dispatch'); + + await findCreateLabelLink().vm.$emit('click'); + + expect(store.dispatch).toHaveBeenCalledWith('receiveLabelsSuccess', []); + expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContentsCreateView'); + }); + }); + + describe('keyboard navigation', () => { const fakePreventDefault = jest.fn(); - describe('handleComponentAppear', () => { - it('calls `focusInput` on searchInput field', async () => { - findIntersectionObserver().vm.$emit('appear'); + beforeEach(() => { + createComponent(undefined, mountExtended); + store.dispatch('receiveLabelsSuccess', mockLabels); + }); - await nextTick(); + describe('when the "down" key is pressed', () => { + it('highlights the item', async () => { + expect(findLabelItems().at(0).classes()).not.toContain('is-focused'); - expect(focusInputMock).toHaveBeenCalled(); + await findLabelsList().trigger('keydown.down'); + + expect(findLabelItems().at(0).classes()).toContain('is-focused'); }); }); - describe('handleComponentDisappear', () => { - it('calls action `receiveLabelsSuccess` with empty array', async () => { - jest.spyOn(store, 'dispatch'); - - await findIntersectionObserver().vm.$emit('disappear'); - - expect(store.dispatch).toHaveBeenCalledWith(expect.anything(), []); - }); - }); - - describe('handleCreateLabelClick', () => { - it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', async () => { - jest.spyOn(store, 'dispatch'); - - await findCreateLabelLink().vm.$emit('click'); - - expect(store.dispatch).toHaveBeenCalledWith('receiveLabelsSuccess', []); - expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContentsCreateView'); - }); - }); - - describe('handleKeyDown', () => { - beforeEach(() => { - createComponent(undefined, mountExtended); - store.dispatch('receiveLabelsSuccess', mockLabels); - }); - - it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', async () => { + describe('when the "up" arrow key is pressed', () => { + it('un-highlights the item', async () => { await setCurrentHighlightItem(1); expect(findLabelItems().at(1).classes()).toContain('is-focused'); @@ -181,16 +212,10 @@ describe('DropdownContentsLabelsView', () => { expect(findLabelItems().at(1).classes()).not.toContain('is-focused'); }); + }); - it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', async () => { - expect(findLabelItems().at(0).classes()).not.toContain('is-focused'); - - await findLabelsList().trigger('keydown.down'); - - expect(findLabelItems().at(0).classes()).toContain('is-focused'); - }); - - it('resets the search text when the Enter key is pressed', async () => { + describe('when the "enter" key is pressed', () => { + it('resets the search text', async () => { await setCurrentHighlightItem(1); await findSearchBoxByType().vm.$emit('input', 'bug'); await findLabelsList().trigger('keydown.enter', { preventDefault: fakePreventDefault }); @@ -199,21 +224,23 @@ describe('DropdownContentsLabelsView', () => { expect(fakePreventDefault).toHaveBeenCalled(); }); - it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', async () => { + it('calls action `updateSelectedLabels` with currently highlighted label', async () => { await setCurrentHighlightItem(2); await findLabelsList().trigger('keydown.enter', { preventDefault: fakePreventDefault }); expect(updateSelectedLabelsMock).toHaveBeenCalledWith(expect.anything(), [mockLabels[2]]); }); + }); - it('calls action `toggleDropdownContents` when Esc key is pressed', async () => { + describe('when the "esc" key is pressed', () => { + it('calls action `toggleDropdownContents`', async () => { await setCurrentHighlightItem(1); await findLabelsList().trigger('keydown.esc'); expect(toggleDropdownContentsMock).toHaveBeenCalled(); }); - it('calls action `scrollIntoViewIfNeeded` in next tick when esc key is pressed', async () => { + it('scrolls dropdown content into view', async () => { const containerTop = 500; const labelTop = 0; @@ -227,29 +254,6 @@ describe('DropdownContentsLabelsView', () => { expect(findDropdownContent().element.scrollTop).toBe(labelTop - containerTop); }); }); - - describe('handleLabelClick', () => { - beforeEach(() => { - createComponent(undefined, mountExtended); - store.dispatch('receiveLabelsSuccess', mockLabels); - }); - - it('calls action `updateSelectedLabels` with provided `label` param', () => { - findLabelItems().at(0).findComponent(GlLink).vm.$emit('click'); - - expect(updateSelectedLabelsMock).toHaveBeenCalledWith(expect.anything(), [ - { ...mockLabels[0], indeterminate: expect.anything(), set: expect.anything() }, - ]); - }); - - it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => { - store.state.allowMultiselect = false; - - findLabelItems().at(0).findComponent(GlLink).vm.$emit('click'); - - expect(toggleDropdownContentsMock).toHaveBeenCalled(); - }); - }); }); describe('template', () => { diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index b441c5f531d..17a1d655561 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -103,20 +103,20 @@ describe('HelpCenter component', () => { jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); }); - it('shows Ask the Tanuki Bot with the help items via Portal', () => { + it('shows Ask the GitLab Chat with the help items', () => { expect(findDropdownGroup(0).props('group').items).toEqual([ expect.objectContaining({ icon: 'tanuki', - text: HelpCenter.i18n.tanuki, + text: HelpCenter.i18n.chat, extraAttrs: trackingAttrs('tanuki_bot_help_dropdown'), }), ...DEFAULT_HELP_ITEMS, ]); }); - describe('when Ask the Tanuki Bot button is clicked', () => { + describe('when Ask the GitLab Chat button is clicked', () => { beforeEach(() => { - findButton('Ask the Tanuki Bot').click(); + findButton('Ask the GitLab Chat').click(); }); it('closes the dropdown', () => { diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js index a8e3536059e..dc67097d763 100644 --- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js @@ -1,4 +1,4 @@ -import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui'; +import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover } from '@gitlab/ui'; import projects from 'test_fixtures/api/users/projects/get.json'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue'; @@ -13,6 +13,8 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge. import { ACCESS_LEVEL_LABELS } from '~/access_level/constants'; import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants'; +jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}1`); + describe('ProjectsListItem', () => { let wrapper; @@ -32,6 +34,8 @@ describe('ProjectsListItem', () => { const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues }); const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks }); + const findProjectTopics = () => wrapper.findByTestId('project-topics'); + const findPopover = () => findProjectTopics().findComponent(GlPopover); it('renders project avatar', () => { createComponent(); @@ -166,4 +170,64 @@ describe('ProjectsListItem', () => { expect(findForksLink().exists()).toBe(false); }); }); + + describe('if project has topics', () => { + it('renders first three topics', () => { + createComponent(); + + const firstThreeTopics = project.topics.slice(0, 3); + const firstThreeBadges = findProjectTopics().findAllComponents(GlBadge).wrappers.slice(0, 3); + const firstThreeBadgesText = firstThreeBadges.map((badge) => badge.text()); + const firstThreeBadgesHref = firstThreeBadges.map((badge) => badge.attributes('href')); + + expect(firstThreeTopics).toEqual(firstThreeBadgesText); + expect(firstThreeBadgesHref).toEqual( + firstThreeTopics.map((topic) => `/explore/projects/topics/${encodeURIComponent(topic)}`), + ); + }); + + it('renders the rest of the topics in a popover', () => { + createComponent(); + + const topics = project.topics.slice(3); + const badges = findPopover().findAllComponents(GlBadge).wrappers; + const badgesText = badges.map((badge) => badge.text()); + const badgesHref = badges.map((badge) => badge.attributes('href')); + + expect(topics).toEqual(badgesText); + expect(badgesHref).toEqual( + topics.map((topic) => `/explore/projects/topics/${encodeURIComponent(topic)}`), + ); + }); + + it('renders button to open popover', () => { + createComponent(); + + const expectedButtonId = 'project-topics-popover-1'; + + expect(wrapper.findByText('+ 2 more').attributes('id')).toBe(expectedButtonId); + expect(findPopover().props('target')).toBe(expectedButtonId); + }); + + describe('when topic has a name longer than 15 characters', () => { + it('truncates name and shows tooltip with full name', () => { + const topicWithLongName = 'topic with very very very long name'; + + createComponent({ + propsData: { + project: { + ...project, + topics: [topicWithLongName, ...project.topics], + }, + }, + }); + + const firstTopicBadge = findProjectTopics().findComponent(GlBadge); + const tooltip = getBinding(firstTopicBadge.element, 'gl-tooltip'); + + expect(firstTopicBadge.text()).toBe('topic with ver…'); + expect(tooltip.value).toBe(topicWithLongName); + }); + }); + }); });