summaryrefslogtreecommitdiff
path: root/spec/frontend/issues/list
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/issues/list')
-rw-r--r--spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js68
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js211
-rw-r--r--spec/frontend/issues/list/components/issue_card_statistics_spec.js64
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js192
-rw-r--r--spec/frontend/issues/list/mock_data.js40
-rw-r--r--spec/frontend/issues/list/utils_spec.js28
6 files changed, 435 insertions, 168 deletions
diff --git a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
new file mode 100644
index 00000000000..d0d20ef03e1
--- /dev/null
+++ b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
@@ -0,0 +1,68 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
+import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
+
+describe('EmptyStateWithAnyIssues component', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ emptyStateSvgPath: 'empty/state/svg/path',
+ newIssuePath: 'new/issue/path',
+ showNewIssueLink: false,
+ };
+
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const mountComponent = (props = {}) => {
+ wrapper = shallowMount(EmptyStateWithAnyIssues, {
+ propsData: {
+ hasSearch: true,
+ isOpenTab: true,
+ ...props,
+ },
+ provide: defaultProvide,
+ });
+ };
+
+ describe('when there is a search (with no results)', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: true });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ description: IssuesListApp.i18n.noSearchResultsDescription,
+ title: IssuesListApp.i18n.noSearchResultsTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+
+ describe('when "Open" tab is active', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: false, isOpenTab: true });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ description: IssuesListApp.i18n.noOpenIssuesDescription,
+ title: IssuesListApp.i18n.noOpenIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+
+ describe('when "Closed" tab is active', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: false, isOpenTab: false });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: IssuesListApp.i18n.noClosedIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
new file mode 100644
index 00000000000..065139f10f4
--- /dev/null
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -0,0 +1,211 @@
+import { GlEmptyState, GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import { i18n } from '~/issues/list/constants';
+
+describe('EmptyStateWithoutAnyIssues component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ currentTabCount: 0,
+ exportCsvPathWithQuery: 'export/csv/path',
+ };
+
+ const defaultProvide = {
+ canCreateProjects: false,
+ emptyStateSvgPath: 'empty/state/svg/path',
+ fullPath: 'full/path',
+ isSignedIn: true,
+ jiraIntegrationPath: 'jira/integration/path',
+ newIssuePath: 'new/issue/path',
+ newProjectPath: 'new/project/path',
+ showNewIssueLink: false,
+ signInPath: 'sign/in/path',
+ };
+
+ const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findIssuesHelpPageLink = () =>
+ wrapper.findByRole('link', { name: i18n.noIssuesDescription });
+ const findJiraDocsLink = () =>
+ wrapper.findByRole('link', { name: 'Enable the Jira integration' });
+ const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewIssueLink = () => wrapper.findByRole('link', { name: i18n.newIssueLabel });
+ const findNewProjectLink = () => wrapper.findByRole('link', { name: i18n.newProjectLabel });
+
+ const mountComponent = ({ props = {}, provide = {} } = {}) => {
+ wrapper = mountExtended(EmptyStateWithoutAnyIssues, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ NewIssueDropdown: true,
+ },
+ });
+ };
+
+ describe('when signed in', () => {
+ describe('empty state', () => {
+ it('renders empty state', () => {
+ mountComponent();
+
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: i18n.noIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+
+ describe('description', () => {
+ it('renders issues docs link', () => {
+ mountComponent();
+
+ expect(findIssuesHelpPageLink().attributes('href')).toBe(
+ EmptyStateWithoutAnyIssues.issuesHelpPagePath,
+ );
+ });
+
+ describe('"create a project first" description', () => {
+ describe('when can create projects', () => {
+ it('renders', () => {
+ mountComponent({ provide: { canCreateProjects: true } });
+
+ expect(findGlEmptyState().text()).toContain(i18n.noGroupIssuesSignedInDescription);
+ });
+ });
+
+ describe('when cannot create projects', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { canCreateProjects: false } });
+
+ expect(findGlEmptyState().text()).not.toContain(
+ i18n.noGroupIssuesSignedInDescription,
+ );
+ });
+ });
+ });
+ });
+
+ describe('actions', () => {
+ describe('"New project" link', () => {
+ describe('when can create projects', () => {
+ it('renders', () => {
+ mountComponent({ provide: { canCreateProjects: true } });
+
+ expect(findNewProjectLink().attributes('href')).toBe(defaultProvide.newProjectPath);
+ });
+ });
+
+ describe('when cannot create projects', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { canCreateProjects: false } });
+
+ expect(findNewProjectLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('"New issue" link', () => {
+ describe('when can show new issue link', () => {
+ it('renders', () => {
+ mountComponent({ provide: { showNewIssueLink: true } });
+
+ expect(findNewIssueLink().attributes('href')).toBe(defaultProvide.newIssuePath);
+ });
+ });
+
+ describe('when cannot show new issue link', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { showNewIssueLink: false } });
+
+ expect(findNewIssueLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('CSV import/export buttons', () => {
+ describe('when can show csv buttons', () => {
+ it('renders', () => {
+ mountComponent({ props: { showCsvButtons: true } });
+
+ expect(findCsvImportExportButtons().props()).toMatchObject({
+ exportCsvPath: defaultProps.exportCsvPathWithQuery,
+ issuableCount: 0,
+ });
+ });
+ });
+
+ describe('when cannot show csv buttons', () => {
+ it('does not render', () => {
+ mountComponent({ props: { showCsvButtons: false } });
+
+ expect(findCsvImportExportButtons().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('new issue dropdown', () => {
+ describe('when can show new issue dropdown', () => {
+ it('renders', () => {
+ mountComponent({ props: { showNewIssueDropdown: true } });
+
+ expect(findNewIssueDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when cannot show new issue dropdown', () => {
+ it('does not render', () => {
+ mountComponent({ props: { showNewIssueDropdown: false } });
+
+ expect(findNewIssueDropdown().exists()).toBe(false);
+ });
+ });
+ });
+ });
+ });
+
+ describe('Jira section', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('shows Jira integration information', () => {
+ const paragraphs = wrapper.findAll('p');
+ expect(paragraphs.at(1).text()).toContain(i18n.jiraIntegrationTitle);
+ expect(paragraphs.at(2).text()).toMatchInterpolatedText(i18n.jiraIntegrationMessage);
+ expect(paragraphs.at(3).text()).toContain(i18n.jiraIntegrationSecondaryMessage);
+ });
+
+ it('renders Jira integration docs link', () => {
+ expect(findJiraDocsLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
+ });
+ });
+ });
+
+ describe('when signed out', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { isSignedIn: false } });
+ });
+
+ it('renders empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: i18n.noIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ primaryButtonText: i18n.noIssuesSignedOutButtonText,
+ primaryButtonLink: defaultProvide.signInPath,
+ });
+ });
+
+ it('renders issues docs link', () => {
+ expect(findGlLink().attributes('href')).toBe(EmptyStateWithoutAnyIssues.issuesHelpPagePath);
+ expect(findGlLink().text()).toBe(i18n.noIssuesDescription);
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/issue_card_statistics_spec.js b/spec/frontend/issues/list/components/issue_card_statistics_spec.js
new file mode 100644
index 00000000000..180d4ab7eb6
--- /dev/null
+++ b/spec/frontend/issues/list/components/issue_card_statistics_spec.js
@@ -0,0 +1,64 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import IssueCardStatistics from '~/issues/list/components/issue_card_statistics.vue';
+import { i18n } from '~/issues/list/constants';
+
+describe('IssueCardStatistics CE component', () => {
+ let wrapper;
+
+ const findMergeRequests = () => wrapper.findByTestId('merge-requests');
+ const findUpvotes = () => wrapper.findByTestId('issuable-upvotes');
+ const findDownvotes = () => wrapper.findByTestId('issuable-downvotes');
+
+ const mountComponent = ({ mergeRequestsCount, upvotes, downvotes } = {}) => {
+ wrapper = shallowMountExtended(IssueCardStatistics, {
+ propsData: {
+ issue: {
+ mergeRequestsCount,
+ upvotes,
+ downvotes,
+ },
+ },
+ });
+ };
+
+ describe('when issue attributes are undefined', () => {
+ it('does not render the attributes', () => {
+ mountComponent();
+
+ expect(findMergeRequests().exists()).toBe(false);
+ expect(findUpvotes().exists()).toBe(false);
+ expect(findDownvotes().exists()).toBe(false);
+ });
+ });
+
+ describe('when issue attributes are defined', () => {
+ beforeEach(() => {
+ mountComponent({ mergeRequestsCount: 1, upvotes: 5, downvotes: 9 });
+ });
+
+ it('renders merge requests', () => {
+ const mergeRequests = findMergeRequests();
+
+ expect(mergeRequests.text()).toBe('1');
+ expect(mergeRequests.attributes('title')).toBe(i18n.relatedMergeRequests);
+ expect(mergeRequests.findComponent(GlIcon).props('name')).toBe('merge-request');
+ });
+
+ it('renders upvotes', () => {
+ const upvotes = findUpvotes();
+
+ expect(upvotes.text()).toBe('5');
+ expect(upvotes.attributes('title')).toBe(i18n.upvotes);
+ expect(upvotes.findComponent(GlIcon).props('name')).toBe('thumb-up');
+ });
+
+ it('renders downvotes', () => {
+ const downvotes = findDownvotes();
+
+ expect(downvotes.text()).toBe('9');
+ expect(downvotes.attributes('title')).toBe(i18n.downvotes);
+ expect(downvotes.findComponent(GlIcon).props('name')).toBe('thumb-down');
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index d0c93c896b3..4c5d8ce3cd1 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -21,18 +21,21 @@ import {
setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
RELATIVE_POSITION,
RELATIVE_POSITION_ASC,
+ UPDATED_DESC,
urlSortParams,
} from '~/issues/list/constants';
import eventHub from '~/issues/list/eventhub';
@@ -58,10 +61,11 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import('~/issuable/bulk_update_sidebar');
+import('~/issuable');
import('~/users_select');
jest.mock('@sentry/browser');
@@ -122,10 +126,8 @@ describe('CE IssuesListApp component', () => {
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
- const findGlButton = () => wrapper.findComponent(GlButton);
const findGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAt = (index) => findGlButtons().at(index);
- const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
@@ -182,7 +184,11 @@ describe('CE IssuesListApp component', () => {
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
- sortOptions: getSortOptions(true, true),
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
tabs: IssuableListTabs,
@@ -395,9 +401,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -435,9 +441,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -486,136 +492,29 @@ describe('CE IssuesListApp component', () => {
describe('empty states', () => {
describe('when there are issues', () => {
- describe('when search returns no results', () => {
- beforeEach(() => {
- setWindowLocation(`?search=no+results`);
-
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noSearchResultsDescription,
- title: IssuesListApp.i18n.noSearchResultsTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- });
- });
-
- describe('when "Open" tab has no issues', () => {
- beforeEach(() => {
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noOpenIssuesDescription,
- title: IssuesListApp.i18n.noOpenIssuesTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- });
+ beforeEach(() => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
});
- describe('when "Closed" tab has no issues', () => {
- beforeEach(() => {
- setWindowLocation(`?state=${IssuableStates.Closed}`);
-
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noClosedIssuesTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
+ it('shows EmptyStateWithAnyIssues empty state', () => {
+ expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
+ hasSearch: false,
+ isOpenTab: true,
});
});
});
describe('when there are no issues', () => {
- describe('when user is logged in', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { hasAnyIssues: false, isSignedIn: true },
- mountFn: mount,
- });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noIssuesSignedInTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedInDescription,
- );
- });
-
- it('shows "New issue" and import/export buttons', () => {
- expect(findGlButton().text()).toBe(IssuesListApp.i18n.newIssueLabel);
- expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath);
- expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: defaultProvide.exportCsvPath,
- issuableCount: 0,
- });
- });
-
- it('shows Jira integration information', () => {
- const paragraphs = wrapper.findAll('p');
- const links = wrapper.findAll('.gl-link');
- expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
- expect(paragraphs.at(2).text()).toContain(
- 'Enable the Jira integration to view your Jira issues in GitLab.',
- );
- expect(paragraphs.at(3).text()).toContain(
- IssuesListApp.i18n.jiraIntegrationSecondaryMessage,
- );
- expect(links.at(1).text()).toBe('Enable the Jira integration');
- expect(links.at(1).attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
- });
- });
-
- describe('when user is logged in and can create projects', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { canCreateProjects: true, hasAnyIssues: false, isSignedIn: true },
- stubs: { GlEmptyState },
- });
- });
-
- it('shows empty state with additional description about creating projects', () => {
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedInDescription,
- );
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noGroupIssuesSignedInDescription,
- );
- });
-
- it('shows "New project" button', () => {
- expect(findGlButton().text()).toBe(IssuesListApp.i18n.newProjectLabel);
- expect(findGlButton().attributes('href')).toBe(defaultProvide.newProjectPath);
- });
+ beforeEach(() => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: false } });
});
- describe('when user is logged out', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { hasAnyIssues: false, isSignedIn: false },
- mountFn: mount,
- });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noIssuesSignedOutTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText,
- primaryButtonLink: defaultProvide.signInPath,
- });
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedOutDescription,
- );
+ it('shows EmptyStateWithoutAnyIssues empty state', () => {
+ expect(wrapper.findComponent(EmptyStateWithoutAnyIssues).props()).toEqual({
+ currentTabCount: 0,
+ exportCsvPathWithQuery: defaultProvide.exportCsvPath,
+ showCsvButtons: true,
+ showNewIssueDropdown: false,
});
});
});
@@ -636,8 +535,8 @@ describe('CE IssuesListApp component', () => {
it('does not render My-Reaction or Confidential tokens', () => {
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL },
]);
@@ -685,13 +584,13 @@ describe('CE IssuesListApp component', () => {
});
it('renders all tokens alphabetically', () => {
- const preloadedAuthors = [
+ const preloadedUsers = [
{ ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers },
{ type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_CONTACT },
{ type: TOKEN_TYPE_LABEL },
@@ -699,6 +598,7 @@ describe('CE IssuesListApp component', () => {
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_ORGANIZATION },
{ type: TOKEN_TYPE_RELEASE },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
{ type: TOKEN_TYPE_TYPE },
]);
});
@@ -899,7 +799,11 @@ describe('CE IssuesListApp component', () => {
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
async (sortKey) => {
- wrapper = mountComponent();
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ wrapper =
+ sortKey === CREATED_DESC
+ ? mountComponent({ provide: { initialSort: UPDATED_DESC } })
+ : mountComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('sort', sortKey);
@@ -929,9 +833,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -941,9 +845,9 @@ describe('CE IssuesListApp component', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
- findIssuableList().vm.$emit('sort', CREATED_DESC);
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
- expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: CREATED_DESC } });
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
});
it('captures error when mutation response has errors', async () => {
@@ -952,7 +856,7 @@ describe('CE IssuesListApp component', () => {
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
- findIssuableList().vm.$emit('sort', CREATED_DESC);
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
@@ -1016,9 +920,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 62fcbf7aad0..0690501dee9 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -1,7 +1,7 @@
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
@@ -132,6 +132,8 @@ export const locationSearch = [
'?search=find+issues',
'author_username=homer',
'not[author_username]=marge',
+ 'or[author_username]=burns',
+ 'or[author_username]=smithers',
'assignee_username[]=bart',
'assignee_username[]=lisa',
'assignee_username[]=5',
@@ -184,41 +186,43 @@ export const locationSearchWithSpecialValues = [
export const filteredTokens = [
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'burns', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'smithers', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'issue', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'feature', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
@@ -264,6 +268,7 @@ export const apiParams = {
weight: '3',
},
or: {
+ authorUsernames: ['burns', 'smithers'],
assigneeUsernames: ['carl', 'lenny'],
},
};
@@ -283,6 +288,7 @@ export const apiParamsWithSpecialValues = {
export const urlParams = {
author_username: 'homer',
'not[author_username]': 'marge',
+ 'or[author_username]': ['burns', 'smithers'],
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
'or[assignee_username][]': ['carl', 'lenny'],
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index 3c6332d5728..a281ed1c989 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -69,26 +69,40 @@ describe('isSortKey', () => {
describe('getSortOptions', () => {
describe.each`
- hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
- ${false} | ${false} | ${10} | ${false} | ${false}
- ${true} | ${false} | ${11} | ${true} | ${false}
- ${false} | ${true} | ${11} | ${false} | ${true}
- ${true} | ${true} | ${12} | ${true} | ${true}
+ hasIssuableHealthStatusFeature | hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsHealthStatus | containsWeight | containsBlocking
+ ${false} | ${false} | ${false} | ${10} | ${false} | ${false} | ${false}
+ ${false} | ${false} | ${true} | ${11} | ${false} | ${false} | ${true}
+ ${false} | ${true} | ${false} | ${11} | ${false} | ${true} | ${false}
+ ${false} | ${true} | ${true} | ${12} | ${false} | ${true} | ${true}
+ ${true} | ${false} | ${false} | ${11} | ${true} | ${false} | ${false}
+ ${true} | ${false} | ${true} | ${12} | ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false} | ${12} | ${true} | ${true} | ${false}
+ ${true} | ${true} | ${true} | ${13} | ${true} | ${true} | ${true}
`(
- 'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
+ 'when hasIssuableHealthStatusFeature=$hasIssuableHealthStatusFeature, hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
+ hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasBlockedIssuesFeature,
length,
+ containsHealthStatus,
containsWeight,
containsBlocking,
}) => {
- const sortOptions = getSortOptions(hasIssueWeightsFeature, hasBlockedIssuesFeature);
+ const sortOptions = getSortOptions({
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ });
it('returns the correct length of sort options', () => {
expect(sortOptions).toHaveLength(length);
});
+ it(`${containsHealthStatus ? 'contains' : 'does not contain'} health status option`, () => {
+ expect(sortOptions.some((option) => option.title === 'Health')).toBe(containsHealthStatus);
+ });
+
it(`${containsWeight ? 'contains' : 'does not contain'} weight option`, () => {
expect(sortOptions.some((option) => option.title === 'Weight')).toBe(containsWeight);
});