Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue10
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/constants.js4
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue41
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql5
-rw-r--r--app/assets/javascripts/super_sidebar/components/help_center.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue86
-rw-r--r--config/feature_flags/development/sign_and_verify_ansi2json_state.yml2
-rw-r--r--doc/development/database/required_stops.md26
-rw-r--r--doc/development/go_guide/go_upgrade.md1
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md6
-rw-r--r--doc/user/project/merge_requests/creating_merge_requests.md2
-rw-r--r--locale/gitlab.pot14
-rw-r--r--qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb12
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js5
-rw-r--r--spec/frontend/dropzone_input_spec.js5
-rw-r--r--spec/frontend/fixtures/jobs.rb60
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js17
-rw-r--r--spec/frontend/jobs/mock_data.js2
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js84
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js136
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js66
24 files changed, 452 insertions, 146 deletions
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 @@
<script>
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
import { validateQueryString } from '~/jobs/components/filtered_search/utils';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
@@ -9,14 +8,24 @@ import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_
import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
import { createAlert } from '~/alert';
import JobsSkeletonLoader from '../jobs_skeleton_loader.vue';
-import { DEFAULT_FIELDS_ADMIN, RAW_TEXT_WARNING_ADMIN } from '../constants';
+import {
+ DEFAULT_FIELDS_ADMIN,
+ RAW_TEXT_WARNING_ADMIN,
+ JOBS_COUNT_ERROR_MESSAGE,
+ JOBS_FETCH_ERROR_MSG,
+ LOADING_ARIA_LABEL,
+ CANCELABLE_JOBS_ERROR_MSG,
+} from '../constants';
import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
+import GetAllJobsCount from './graphql/queries/get_all_jobs_count.query.graphql';
import CancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
export default {
i18n: {
- jobsFetchErrorMsg: __('There was an error fetching the jobs.'),
- loadingAriaLabel: __('Loading'),
+ jobsCountErrorMsg: JOBS_COUNT_ERROR_MESSAGE,
+ jobsFetchErrorMsg: JOBS_FETCH_ERROR_MSG,
+ loadingAriaLabel: LOADING_ARIA_LABEL,
+ cancelableJobsErrorMsg: CANCELABLE_JOBS_ERROR_MSG,
},
filterSearchBoxStyles:
'gl-my-0 gl-p-5 gl-bg-gray-10 gl-text-gray-900 gl-border-b gl-border-gray-100',
@@ -51,22 +60,36 @@ export default {
return this.variables;
},
update(data) {
- const { jobs: { nodes: list = [], pageInfo = {}, count } = {} } = data || {};
+ const { jobs: { nodes: list = [], pageInfo = {} } = {} } = data || {};
return {
list,
pageInfo,
- count,
};
},
error() {
this.error = this.$options.i18n.jobsFetchErrorMsg;
},
},
+ jobsCount: {
+ query: GetAllJobsCount,
+ update(data) {
+ return data?.jobs?.count || 0;
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ error() {
+ this.error = this.$options.i18n.jobsCountErrorMsg;
+ },
+ },
cancelable: {
query: CancelableJobs,
update(data) {
this.isCancelable = data.cancelable.count !== 0;
},
+ error() {
+ this.error = this.$options.i18n.cancelableJobsErrorMsg;
+ },
},
},
data() {
@@ -81,6 +104,7 @@ export default {
filterSearchTriggered: false,
DEFAULT_FIELDS_ADMIN,
isCancelable: false,
+ jobsCount: null,
};
},
computed: {
@@ -109,9 +133,6 @@ export default {
showFilteredSearch() {
return !this.scope;
},
- jobsCount() {
- return this.jobs.count;
- },
showLoadingSpinner() {
return this.loading && this.infiniteScrollingTriggered;
},
@@ -160,6 +181,7 @@ export default {
});
this.$apollo.queries.jobs.refetch({ statuses: null });
+ this.$apollo.queries.jobsCount.refetch({ statuses: null });
return;
}
@@ -183,6 +205,7 @@ export default {
});
this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
+ this.$apollo.queries.jobsCount.refetch({ statuses: filter.value.data });
}
});
},
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql
index 374009efa15..6e63f0c81e6 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql
+++ b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql
@@ -1,6 +1,5 @@
query getAllJobs($after: String, $first: Int = 50, $statuses: [CiJobStatus!]) {
jobs(after: $after, first: $first, statuses: $statuses) {
- count
pageInfo {
endCursor
hasNextPage
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql
new file mode 100644
index 00000000000..8c59230b2b8
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql
@@ -0,0 +1,5 @@
+query getAllJobsCount($statuses: [CiJobStatus!]) {
+ jobs(statuses: $statuses) {
+ count
+ }
+}
diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue
index 70e1780aae1..d09d3f28ba2 100644
--- a/app/assets/javascripts/super_sidebar/components/help_center.vue
+++ b/app/assets/javascripts/super_sidebar/components/help_center.vue
@@ -38,7 +38,7 @@ export default {
shortcuts: __('Keyboard shortcuts'),
version: __('Your GitLab version'),
whatsnew: __("What's new"),
- tanuki: __('Ask the Tanuki Bot'),
+ chat: __('Ask the GitLab Chat'),
},
props: {
sidebarData: {
@@ -71,7 +71,7 @@ export default {
items: [
this.sidebarData.show_tanuki_bot && {
icon: 'tanuki',
- text: this.$options.i18n.tanuki,
+ text: this.$options.i18n.chat,
action: this.showTanukiBotChat,
extraAttrs: {
...this.trackingAttrs('tanuki_bot_help_dropdown'),
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
index 1ace1c52a68..64ce4b66213 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
@@ -11,6 +11,7 @@ export default {
* id: number | string;
* name: string;
* webUrl: string;
+ * topics: string[];
* forksCount?: number;
* avatarUrl: string | null;
* starCount: number;
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index f77fd029e93..714ffd60c25 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -1,5 +1,14 @@
<script>
-import { GlAvatarLabeled, GlIcon, GlLink, GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlAvatarLabeled,
+ GlIcon,
+ GlLink,
+ GlBadge,
+ GlTooltipDirective,
+ GlPopover,
+ GlSprintf,
+} from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
@@ -7,6 +16,10 @@ import { FEATURABLE_ENABLED } from '~/featurable/constants';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
+import { truncate } from '~/lib/utils/text_utility';
+
+const MAX_TOPICS_TO_SHOW = 3;
+const MAX_TOPIC_TITLE_LENGTH = 15;
export default {
i18n: {
@@ -14,6 +27,9 @@ export default {
forks: __('Forks'),
issues: __('Issues'),
archived: __('Archived'),
+ topics: __('Topics'),
+ topicsPopoverTargetText: __('+ %{count} more'),
+ moreTopics: __('More topics'),
},
components: {
GlAvatarLabeled,
@@ -21,6 +37,8 @@ export default {
UserAccessRoleBadge,
GlLink,
GlBadge,
+ GlPopover,
+ GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -33,6 +51,7 @@ export default {
* id: number | string;
* name: string;
* webUrl: string;
+ * topics: string[];
* forksCount?: number;
* avatarUrl: string | null;
* starCount: number;
@@ -49,6 +68,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ topicsPopoverTarget: uniqueId('project-topics-popover-'),
+ };
+ },
computed: {
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.project.visibility];
@@ -83,9 +107,32 @@ export default {
isIssuesEnabled() {
return this.project.issuesAccessLevel === FEATURABLE_ENABLED;
},
+ hasTopics() {
+ return this.project.topics.length;
+ },
+ visibleTopics() {
+ return this.project.topics.slice(0, MAX_TOPICS_TO_SHOW);
+ },
+ popoverTopics() {
+ return this.project.topics.slice(MAX_TOPICS_TO_SHOW);
+ },
},
methods: {
numberToMetricPrefix,
+ topicPath(topic) {
+ return `/explore/projects/topics/${encodeURIComponent(topic)}`;
+ },
+ topicTitle(topic) {
+ return truncate(topic, MAX_TOPIC_TITLE_LENGTH);
+ },
+ topicTooltipTitle(topic) {
+ // Matches conditional in app/assets/javascripts/lib/utils/text_utility.js#L88
+ if (topic.length - 1 > MAX_TOPIC_TITLE_LENGTH) {
+ return topic;
+ }
+
+ return null;
+ },
},
};
</script>
@@ -111,6 +158,43 @@ export default {
accessLevelLabel
}}</user-access-role-badge>
</template>
+ <div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
+ <div
+ class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2"
+ >
+ <span class="gl-p-2 gl-text-secondary">{{ $options.i18n.topics }}:</span>
+ <div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
+ <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
+ {{ topicTitle(topic) }}
+ </gl-badge>
+ </div>
+ <template v-if="popoverTopics.length">
+ <div
+ :id="topicsPopoverTarget"
+ class="gl-p-2 gl-text-secondary"
+ role="button"
+ tabindex="0"
+ >
+ <gl-sprintf :message="$options.i18n.topicsPopoverTargetText">
+ <template #count>{{ popoverTopics.length }}</template>
+ </gl-sprintf>
+ </div>
+ <gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics">
+ <div class="gl-font-base gl-font-weight-normal gl-mx-n2 gl-my-n2">
+ <div
+ v-for="topic in popoverTopics"
+ :key="topic"
+ class="gl-p-2 gl-display-inline-block"
+ >
+ <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
+ {{ topicTitle(topic) }}
+ </gl-badge>
+ </div>
+ </div>
+ </gl-popover>
+ </template>
+ </div>
+ </div>
</gl-avatar-labeled>
<div
class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-mt-0"
diff --git a/config/feature_flags/development/sign_and_verify_ansi2json_state.yml b/config/feature_flags/development/sign_and_verify_ansi2json_state.yml
index 7d9ac48dba6..af9286dc3cd 100644
--- a/config/feature_flags/development/sign_and_verify_ansi2json_state.yml
+++ b/config/feature_flags/development/sign_and_verify_ansi2json_state.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/404718
milestone: '15.11'
type: development
group: group::pipeline execution
-default_enabled: false
+default_enabled: true
diff --git a/doc/development/database/required_stops.md b/doc/development/database/required_stops.md
index b706babbc5e..e4f66f4424f 100644
--- a/doc/development/database/required_stops.md
+++ b/doc/development/database/required_stops.md
@@ -33,6 +33,32 @@ You may need to introduce a required stop for mitigation when:
- **Cause:** The dependent migration may fail if the background migration is incomplete.
- **Mitigation:** Ensure that all background migrations are finalized before authoring dependent migrations.
+### Remove a migration
+
+If a migration is removed, you may need to introduce a required stop to ensure customers
+don't miss the required change.
+
+- **Cause:** Dependent migrations may fail, or the application may not function, because a required
+ migration was removed.
+- **Mitigation:** Ensure migrations are only removed after they've been a part of a planned
+ required stop.
+
+### A migration timestamp is very old
+
+If a migration timestamp is very old (> 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,13 +43734,13 @@ msgstr ""
msgid "TanukiBot|For example, %{linkStart}what is a fork?%{linkEnd}"
msgstr ""
-msgid "TanukiBot|Give feedback"
+msgid "TanukiBot|GitLab Chat"
msgstr ""
-msgid "TanukiBot|Sources"
+msgid "TanukiBot|Give feedback"
msgstr ""
-msgid "TanukiBot|Tanuki Bot"
+msgid "TanukiBot|Sources"
msgstr ""
msgid "TanukiBot|There was an error communicating with Tanuki Bot. Please reach out to GitLab support for more assistance or try again later."
@@ -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';
@@ -17,11 +17,19 @@ 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', () => {
- const fakePreventDefault = jest.fn();
+ describe('create label link', () => {
+ it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', async () => {
+ jest.spyOn(store, 'dispatch');
- describe('handleComponentAppear', () => {
- it('calls `focusInput` on searchInput field', async () => {
- findIntersectionObserver().vm.$emit('appear');
+ await findCreateLabelLink().vm.$emit('click');
- await nextTick();
-
- expect(focusInputMock).toHaveBeenCalled();
- });
+ expect(store.dispatch).toHaveBeenCalledWith('receiveLabelsSuccess', []);
+ expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContentsCreateView');
});
+ });
- describe('handleComponentDisappear', () => {
- it('calls action `receiveLabelsSuccess` with empty array', async () => {
- jest.spyOn(store, 'dispatch');
-
- await findIntersectionObserver().vm.$emit('disappear');
+ describe('keyboard navigation', () => {
+ const fakePreventDefault = jest.fn();
- expect(store.dispatch).toHaveBeenCalledWith(expect.anything(), []);
- });
+ beforeEach(() => {
+ createComponent(undefined, mountExtended);
+ store.dispatch('receiveLabelsSuccess', mockLabels);
});
- describe('handleCreateLabelClick', () => {
- it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', async () => {
- jest.spyOn(store, 'dispatch');
+ describe('when the "down" key is pressed', () => {
+ it('highlights the item', async () => {
+ expect(findLabelItems().at(0).classes()).not.toContain('is-focused');
- await findCreateLabelLink().vm.$emit('click');
+ await findLabelsList().trigger('keydown.down');
- expect(store.dispatch).toHaveBeenCalledWith('receiveLabelsSuccess', []);
- expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContentsCreateView');
+ expect(findLabelItems().at(0).classes()).toContain('is-focused');
});
});
- 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);
+ });
+ });
+ });
});