Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fa69a57b46
commit
90e793301a
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
query getAllJobs($after: String, $first: Int = 50, $statuses: [CiJobStatus!]) {
|
||||
jobs(after: $after, first: $first, statuses: $statuses) {
|
||||
count
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
query getAllJobsCount($statuses: [CiJobStatus!]) {
|
||||
jobs(statuses: $statuses) {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export default {
|
|||
* id: number | string;
|
||||
* name: string;
|
||||
* webUrl: string;
|
||||
* topics: string[];
|
||||
* forksCount?: number;
|
||||
* avatarUrl: string | null;
|
||||
* starCount: number;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue