diff options
Diffstat (limited to 'spec/frontend/issues/list')
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); }); |