diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-10 09:09:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-10 09:09:29 +0300 |
commit | 76c4dd062c4eeb853866ef8b6451c59f9e24221c (patch) | |
tree | faf481c7b2f6da10c13234ad4e4a6ca1cb5a1030 /spec/frontend | |
parent | c2858333644a2bca10fd556a5a298b4a1aaedca2 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
10 files changed, 661 insertions, 31 deletions
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js index 445fb637076..7dce23f72c0 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlFormInput } from '@gitlab/ui'; +import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue'; @@ -10,6 +10,8 @@ import { EVENT_LABEL, EVENT_ACTION, ENVIRONMENT_SCOPE_LINK_TITLE, + AWS_TIP_TITLE, + AWS_TIP_MESSAGE, groupString, instanceString, projectString, @@ -28,10 +30,6 @@ describe('Ci variable modal', () => { const mockVariables = mockVariablesWithScopes(instanceString); const defaultProvide = { - awsLogoSvgPath: '/logo', - awsTipCommandsLink: '/tips', - awsTipDeployLink: '/deploy', - awsTipLearnLink: '/learn-link', containsVariableReferenceLink: '/reference', environmentScopeLink: '/help/environments', glFeatures: { @@ -171,7 +169,7 @@ describe('Ci variable modal', () => { it('does not show AWS guidance tip', () => { const tip = findAWSTip(); - expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(false); }); }); @@ -184,13 +182,18 @@ describe('Ci variable modal', () => { key: AWS_ACCESS_KEY_ID, value: 'AKIAIOSFODNN7EXAMPLEjdhy', }; - createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } }); + createComponent({ + mountFn: shallowMountExtended, + props: { selectedVariable: AWSKeyVariable }, + }); }); it('shows AWS guidance tip', () => { const tip = findAWSTip(); - expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(true); + expect(tip.props('title')).toBe(AWS_TIP_TITLE); + expect(tip.findComponent(GlSprintf).attributes('message')).toBe(AWS_TIP_MESSAGE); }); }); diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js index f1df4208fa2..6ce86852095 100644 --- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js +++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js @@ -1,6 +1,7 @@ import { GlTable, GlBadge } from '@gitlab/ui'; import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -25,17 +26,26 @@ describe('TriggersList', () => { const findInvalidBadge = (i) => findCell(i, 0).findComponent(GlBadge); const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]'); const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]'); - const findRevealHideButton = () => wrapper.findByTestId('reveal-hide-values-button'); + const findRevealHideButton = () => + document.querySelector('[data-testid="reveal-hide-values-button"]'); describe('With triggers set', () => { beforeEach(async () => { + setHTMLFixture(` + <button data-testid="reveal-hide-values-button">Reveal values</button> + `); + createComponent(); await nextTick(); }); + afterEach(() => { + resetHTMLFixture(); + }); + it('displays a table with expected headers', () => { - const headers = ['Token', 'Description', 'Owner', 'Last Used', '']; + const headers = ['Token', 'Description', 'Owner', 'Last Used', 'Actions']; headers.forEach((header, i) => { expect(findHeaderAt(i).text()).toBe(header); }); @@ -44,16 +54,16 @@ describe('TriggersList', () => { it('displays a "Reveal/Hide values" button', async () => { const revealHideButton = findRevealHideButton(); - expect(revealHideButton.exists()).toBe(true); - expect(revealHideButton.text()).toBe('Reveal values'); + expect(Boolean(revealHideButton)).toBe(true); + expect(revealHideButton.innerText).toBe('Reveal values'); - await revealHideButton.vm.$emit('click'); + await revealHideButton.click(); - expect(revealHideButton.text()).toBe('Hide values'); + expect(revealHideButton.innerText).toBe('Hide values'); }); it('displays a table with rows', async () => { - await findRevealHideButton().vm.$emit('click'); + await findRevealHideButton().click(); expect(findRows()).toHaveLength(triggers.length); diff --git a/spec/frontend/lib/print_markdown_dom_spec.js b/spec/frontend/lib/print_markdown_dom_spec.js new file mode 100644 index 00000000000..7f28417228e --- /dev/null +++ b/spec/frontend/lib/print_markdown_dom_spec.js @@ -0,0 +1,102 @@ +import printJS from 'print-js'; +import printMarkdownDom from '~/lib/print_markdown_dom'; + +jest.mock('print-js', () => jest.fn()); + +describe('print util', () => { + describe('print markdown dom', () => { + beforeEach(() => { + document.body.innerHTML = `<div id='target'></div>`; + }); + + const getTarget = () => document.getElementById('target'); + + const contentValues = [ + { + title: 'test title', + expectedTitle: '<h2 class="gl-mt-0 gl-mb-5">test title</h2>', + content: '', + expectedContent: '<div class="md"></div>', + }, + { + title: 'test title', + expectedTitle: '<h2 class="gl-mt-0 gl-mb-5">test title</h2>', + content: '<p>test content</p>', + expectedContent: '<div class="md"><p>test content</p></div>', + }, + { + title: 'test title', + expectedTitle: '<h2 class="gl-mt-0 gl-mb-5">test title</h2>', + content: '<details><summary>test detail</summary><p>test detail content</p></details>', + expectedContent: + '<div class="md"><details open=""><summary>test detail</summary><p>test detail content</p></details></div>', + }, + { + title: undefined, + expectedTitle: '', + content: '', + expectedContent: '<div class="md"></div>', + }, + { + title: undefined, + expectedTitle: '', + content: '<p>test content</p>', + expectedContent: '<div class="md"><p>test content</p></div>', + }, + { + title: undefined, + expectedTitle: '', + content: '<details><summary>test detail</summary><p>test detail content</p></details>', + expectedContent: + '<div class="md"><details open=""><summary>test detail</summary><p>test detail content</p></details></div>', + }, + ]; + + it.each(contentValues)( + 'should print with title ($title) and content ($content)', + async ({ title, expectedTitle, content, expectedContent }) => { + const target = getTarget(); + target.innerHTML = content; + const stylesheet = 'test stylesheet'; + + await printMarkdownDom({ + target, + title, + stylesheet, + }); + + expect(printJS).toHaveBeenCalledWith({ + printable: expectedTitle + expectedContent, + type: 'raw-html', + documentTitle: title, + scanStyles: false, + css: stylesheet, + }); + }, + ); + }); + + describe('ignore selectors', () => { + beforeEach(() => { + document.body.innerHTML = `<div id='target'><div><div class='ignore-me'></div></div></div>`; + }); + + it('should ignore dom if ignoreSelectors', async () => { + const target = document.getElementById('target'); + const ignoreSelectors = ['.ignore-me']; + + await printMarkdownDom({ + target, + ignoreSelectors, + }); + + expect(printJS).toHaveBeenCalledWith({ + printable: '<div class="md"><div></div></div>', + type: 'raw-html', + documentTitle: undefined, + scanStyles: false, + css: [], + }); + }); + }); +}); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_export_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_export_spec.js new file mode 100644 index 00000000000..b7002412561 --- /dev/null +++ b/spec/frontend/pages/shared/wikis/components/wiki_export_spec.js @@ -0,0 +1,48 @@ +import { GlDisclosureDropdown } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import WikiExport from '~/pages/shared/wikis/components/wiki_export.vue'; +import printMarkdownDom from '~/lib/print_markdown_dom'; + +jest.mock('~/lib/print_markdown_dom'); + +describe('pages/shared/wikis/components/wiki_export', () => { + let wrapper; + + const createComponent = (provide) => { + wrapper = shallowMount(WikiExport, { + provide, + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findPrintItem = () => + findDropdown() + .props('items') + .find((x) => x.text === 'Print as PDF'); + + describe('print', () => { + beforeEach(() => { + document.body.innerHTML = '<div id="content-body">Content</div>'; + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should print the content', () => { + createComponent({ + target: '#content-body', + title: 'test title', + stylesheet: [], + }); + + findPrintItem().action(); + + expect(printMarkdownDom).toHaveBeenCalledWith({ + target: document.querySelector('#content-body'), + title: 'test title', + stylesheet: [], + }); + }); + }); +}); diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js new file mode 100644 index 00000000000..6c5dcc3ff5c --- /dev/null +++ b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js @@ -0,0 +1,199 @@ +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import CustomEmailForm from '~/projects/settings_service_desk/components/custom_email_form.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE } from '~/projects/settings_service_desk/custom_email_constants'; + +describe('CustomEmailForm', () => { + let wrapper; + + const defaultProps = { + incomingEmail: 'incoming@example.com', + submitting: false, + }; + + const findForm = () => wrapper.find('form'); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); + const findInputByTestId = (testId) => wrapper.findByTestId(testId).find('input'); + const findCustomEmailInput = () => findInputByTestId('form-custom-email'); + const findSmtpAddressInput = () => findInputByTestId('form-smtp-address'); + const findSmtpPortInput = () => findInputByTestId('form-smtp-port'); + const findSmtpUsernameInput = () => findInputByTestId('form-smtp-username'); + const findSmtpPasswordInput = () => findInputByTestId('form-smtp-password'); + const findSubmit = () => wrapper.findByTestId('form-submit'); + + const clickButtonAndExpectNoSubmitEvent = async () => { + await nextTick(); + findForm().trigger('submit'); + + expect(findSubmit().find('button').attributes('disabled')).toBeDefined(); + expect(wrapper.emitted('submit')).toEqual(undefined); + }; + + const createWrapper = (props = {}) => { + wrapper = extendedWrapper(mount(CustomEmailForm, { propsData: { ...defaultProps, ...props } })); + }; + + it('renders a copy to clipboard button', () => { + createWrapper(); + + expect(findClipboardButton().exists()).toBe(true); + expect(findClipboardButton().props()).toEqual( + expect.objectContaining({ + title: I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE, + text: defaultProps.incomingEmail, + }), + ); + }); + + it('form inputs are disabled when submitting', () => { + createWrapper({ submitting: true }); + + expect(findCustomEmailInput().attributes('disabled')).toBeDefined(); + expect(findSmtpAddressInput().attributes('disabled')).toBeDefined(); + expect(findSmtpPortInput().attributes('disabled')).toBeDefined(); + expect(findSmtpUsernameInput().attributes('disabled')).toBeDefined(); + expect(findSmtpPasswordInput().attributes('disabled')).toBeDefined(); + expect(findSubmit().props('loading')).toBe(true); + }); + + describe('form validation and submit event', () => { + it('is invalid when form inputs are empty', async () => { + createWrapper(); + + await nextTick(); + findForm().trigger('submit'); + + expect(wrapper.emitted('submit')).toEqual(undefined); + }); + + describe('with inputs set', () => { + beforeEach(() => { + createWrapper(); + + findCustomEmailInput().setValue('user@example.com'); + findCustomEmailInput().trigger('change'); + + findSmtpAddressInput().setValue('smtp.example.com'); + findSmtpAddressInput().trigger('change'); + + findSmtpPortInput().setValue('587'); + findSmtpPortInput().trigger('change'); + + findSmtpUsernameInput().setValue('user@example.com'); + findSmtpUsernameInput().trigger('change'); + + findSmtpPasswordInput().setValue('supersecret'); + findSmtpPasswordInput().trigger('change'); + }); + + it('is invalid when malformed email provided', async () => { + findCustomEmailInput().setValue('userexample.com'); + findCustomEmailInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findCustomEmailInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when email is not set', async () => { + findCustomEmailInput().setValue(''); + findCustomEmailInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findCustomEmailInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when smtp address is not set', async () => { + findSmtpAddressInput().setValue(''); + findSmtpAddressInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpAddressInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when smtp port is not set', async () => { + findSmtpPortInput().setValue(''); + findSmtpPortInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpPortInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when smtp port is not an integer', async () => { + findSmtpPortInput().setValue('20m2'); + findSmtpPortInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpPortInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when smtp port is 0', async () => { + findSmtpPortInput().setValue('0'); + findSmtpPortInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpPortInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when smtp username is not set', async () => { + findSmtpUsernameInput().setValue(''); + findSmtpUsernameInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpUsernameInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when password is too short', async () => { + findSmtpPasswordInput().setValue('2short'); + findSmtpPasswordInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpPasswordInput().classes()).toContain('is-invalid'); + }); + + it('is invalid when password is not set', async () => { + findSmtpPasswordInput().setValue(''); + findSmtpPasswordInput().trigger('change'); + + await clickButtonAndExpectNoSubmitEvent(); + expect(findSmtpPasswordInput().classes()).toContain('is-invalid'); + }); + + it('sets smtpUsername automatically when empty based on customEmail', async () => { + const email = 'support@example.com'; + + findSmtpUsernameInput().setValue(''); + findSmtpUsernameInput().trigger('change'); + + findCustomEmailInput().setValue(email); + findCustomEmailInput().trigger('change'); + + await nextTick(); + + expect(findSmtpUsernameInput().element.value).toBe(email); + expect(wrapper.html()).not.toContain('is-invalid'); + }); + + it('is valid and emits submit event with form data', async () => { + await nextTick(); + + expect(wrapper.html()).not.toContain('is-invalid'); + + findForm().trigger('submit'); + + expect(wrapper.emitted('submit')).toEqual([ + [ + { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_password: 'supersecret', + smtp_port: '587', + smtp_username: 'user@example.com', + }, + ], + ]); + }); + }); + }); +}); diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js index f167d2e9d6e..4517508f5df 100644 --- a/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js @@ -7,11 +7,17 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; import CustomEmail from '~/projects/settings_service_desk/components/custom_email.vue'; +import CustomEmailForm from '~/projects/settings_service_desk/components/custom_email_form.vue'; import { FEEDBACK_ISSUE_URL, I18N_GENERIC_ERROR, + I18N_TOAST_SAVED, } from '~/projects/settings_service_desk/custom_email_constants'; -import { MOCK_CUSTOM_EMAIL_EMPTY } from './mock_data'; +import { + MOCK_CUSTOM_EMAIL_EMPTY, + MOCK_CUSTOM_EMAIL_STARTED, + MOCK_CUSTOM_EMAIL_FORM_SUBMIT, +} from './mock_data'; describe('CustomEmail', () => { let axiosMock; @@ -22,10 +28,17 @@ describe('CustomEmail', () => { customEmailEndpoint: '/flightjs/Flight/-/service_desk/custom_email', }; + const showToast = jest.fn(); + const createWrapper = (props = {}) => { wrapper = extendedWrapper( mount(CustomEmail, { propsData: { ...defaultProps, ...props }, + mocks: { + $toast: { + show: showToast, + }, + }, }), ); }; @@ -40,6 +53,7 @@ describe('CustomEmail', () => { afterEach(() => { axiosMock.restore(); + showToast.mockReset(); }); it('displays link to feedback issue', () => { @@ -52,7 +66,7 @@ describe('CustomEmail', () => { beforeEach(() => { axiosMock .onGet(defaultProps.customEmailEndpoint) - .reply(HTTP_STATUS_OK, MOCK_CUSTOM_EMAIL_EMPTY); + .replyOnce(HTTP_STATUS_OK, MOCK_CUSTOM_EMAIL_EMPTY); createWrapper(); }); @@ -64,6 +78,40 @@ describe('CustomEmail', () => { // loading completed expect(findLoadingIcon().exists()).toBe(false); }); + + it('displays form', async () => { + await waitForPromises(); + + expect(wrapper.findComponent(CustomEmailForm).exists()).toBe(true); + }); + + describe('when CustomEmailForm emits submit event with valid params', () => { + beforeEach(() => { + axiosMock + .onPost(defaultProps.customEmailEndpoint) + .replyOnce(HTTP_STATUS_OK, MOCK_CUSTOM_EMAIL_STARTED); + }); + + it('creates custom email', async () => { + createWrapper(); + await nextTick(); + + const spy = jest.spyOn(axios, 'post'); + + wrapper.findComponent(CustomEmailForm).vm.$emit('submit', MOCK_CUSTOM_EMAIL_FORM_SUBMIT); + + expect(wrapper.findComponent(CustomEmailForm).emitted('submit')).toEqual([ + [MOCK_CUSTOM_EMAIL_FORM_SUBMIT], + ]); + await waitForPromises(); + + expect(spy).toHaveBeenCalledWith( + defaultProps.customEmailEndpoint, + MOCK_CUSTOM_EMAIL_FORM_SUBMIT, + ); + expect(showToast).toHaveBeenCalledWith(I18N_TOAST_SAVED); + }); + }); }); describe('when initial resource loading returns 404', () => { diff --git a/spec/frontend/projects/settings_service_desk/components/mock_data.js b/spec/frontend/projects/settings_service_desk/components/mock_data.js index ea88a6cfccd..87fbd354041 100644 --- a/spec/frontend/projects/settings_service_desk/components/mock_data.js +++ b/spec/frontend/projects/settings_service_desk/components/mock_data.js @@ -15,3 +15,20 @@ export const MOCK_CUSTOM_EMAIL_EMPTY = { custom_email_smtp_address: null, error_message: null, }; + +export const MOCK_CUSTOM_EMAIL_STARTED = { + custom_email: 'user@example.com', + custom_email_enabled: false, + custom_email_verification_state: 'started', + custom_email_verification_error: null, + custom_email_smtp_address: 'smtp.example.com', + error_message: null, +}; + +export const MOCK_CUSTOM_EMAIL_FORM_SUBMIT = { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_password: 'supersecret', + smtp_port: '587', + smtp_username: 'user@example.com', +}; diff --git a/spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js b/spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js new file mode 100644 index 00000000000..ce8a78767d4 --- /dev/null +++ b/spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js @@ -0,0 +1,74 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import EmptyStateWithAnyIssues from '~/service_desk/components/empty_state_with_any_issues.vue'; +import { + noSearchResultsTitle, + noSearchResultsDescription, + infoBannerUserNote, + noOpenIssuesTitle, + noClosedIssuesTitle, +} from '~/service_desk/constants'; + +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(); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + description: noSearchResultsDescription, + title: noSearchResultsTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + }); + + describe('when "Open" tab is active', () => { + beforeEach(() => { + mountComponent({ hasSearch: false }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + description: infoBannerUserNote, + title: noOpenIssuesTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + }); + + describe('when "Closed" tab is active', () => { + beforeEach(() => { + mountComponent({ hasSearch: false, isClosedTab: true, isOpenTab: false }); + }); + + it('shows empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + title: noClosedIssuesTitle, + svgPath: defaultProvide.emptyStateSvgPath, + }); + }); + }); +}); diff --git a/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js b/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js new file mode 100644 index 00000000000..bf4951c7310 --- /dev/null +++ b/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js @@ -0,0 +1,89 @@ +import { GlEmptyState, GlLink } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import EmptyStateWithoutAnyIssues from '~/service_desk/components/empty_state_without_any_issues.vue'; +import { infoBannerTitle, noIssuesSignedOutButtonText, learnMore } from '~/service_desk/constants'; + +describe('EmptyStateWithoutAnyIssues component', () => { + let wrapper; + + const defaultProvide = { + emptyStateSvgPath: 'empty/state/svg/path', + isSignedIn: true, + signInPath: 'sign/in/path', + canAdminIssues: true, + isServiceDeskEnabled: true, + serviceDeskEmailAddress: 'email@address.com', + }; + + const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); + const findGlLink = () => wrapper.findComponent(GlLink); + const findIssuesHelpPageLink = () => wrapper.findByRole('link', { name: learnMore }); + + const mountComponent = ({ provide = {} } = {}) => { + wrapper = mountExtended(EmptyStateWithoutAnyIssues, { + provide: { + ...defaultProvide, + ...provide, + }, + }); + }; + + describe('when signed in', () => { + beforeEach(() => { + mountComponent(); + }); + + it('renders empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + title: infoBannerTitle, + svgPath: defaultProvide.emptyStateSvgPath, + contentClass: 'gl-max-w-80!', + }); + }); + + it('renders description with service desk docs link', () => { + expect(findIssuesHelpPageLink().attributes('href')).toBe( + EmptyStateWithoutAnyIssues.serviceDeskHelpPagePath, + ); + }); + + it('renders email address, when user can admin issues and service desk is enabled', () => { + expect(wrapper.text()).toContain(wrapper.vm.serviceDeskEmailAddress); + }); + + it('does not render email address, when user can not admin issues', () => { + mountComponent({ provide: { canAdminIssues: false } }); + + expect(wrapper.text()).not.toContain(wrapper.vm.serviceDeskEmailAddress); + }); + + it('does not render email address, when service desk is not setup', () => { + mountComponent({ provide: { isServiceDeskEnabled: false } }); + + expect(wrapper.text()).not.toContain(wrapper.vm.serviceDeskEmailAddress); + }); + }); + + describe('when signed out', () => { + beforeEach(() => { + mountComponent({ provide: { isSignedIn: false } }); + }); + + it('renders empty state', () => { + expect(findGlEmptyState().props()).toMatchObject({ + title: infoBannerTitle, + svgPath: defaultProvide.emptyStateSvgPath, + primaryButtonText: noIssuesSignedOutButtonText, + primaryButtonLink: defaultProvide.signInPath, + contentClass: 'gl-max-w-80!', + }); + }); + + it('renders service desk docs link', () => { + expect(findGlLink().attributes('href')).toBe( + EmptyStateWithoutAnyIssues.serviceDeskHelpPagePath, + ); + expect(findGlLink().text()).toBe(learnMore); + }); + }); +}); diff --git a/spec/frontend/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/service_desk/components/service_desk_list_app_spec.js index 0a7b2376db7..5c3b7095447 100644 --- a/spec/frontend/service_desk/components/service_desk_list_app_spec.js +++ b/spec/frontend/service_desk/components/service_desk_list_app_spec.js @@ -17,6 +17,9 @@ import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_servi import getServiceDeskIssuesCountsQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql'; import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue'; import InfoBanner from '~/service_desk/components/info_banner.vue'; +import EmptyStateWithAnyIssues from '~/service_desk/components/empty_state_with_any_issues.vue'; +import EmptyStateWithoutAnyIssues from '~/service_desk/components/empty_state_without_any_issues.vue'; + import { TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_AUTHOR, @@ -29,6 +32,7 @@ import { } from '~/vue_shared/components/filtered_search_bar/constants'; import { getServiceDeskIssuesQueryResponse, + getServiceDeskIssuesQueryEmptyResponse, getServiceDeskIssuesCountsQueryResponse, filteredTokens, urlParams, @@ -70,6 +74,9 @@ describe('CE ServiceDeskListApp', () => { const mockServiceDeskIssuesQueryResponseHandler = jest .fn() .mockResolvedValue(defaultQueryResponse); + const mockServiceDeskIssuesQueryEmptyResponseHandler = jest + .fn() + .mockResolvedValue(getServiceDeskIssuesQueryEmptyResponse); const mockServiceDeskIssuesCountsQueryResponseHandler = jest .fn() .mockResolvedValue(getServiceDeskIssuesCountsQueryResponse); @@ -143,21 +150,48 @@ describe('CE ServiceDeskListApp', () => { expect(findInfoBanner().exists()).toBe(true); }); - it('does not render when Service Desk is not supported and has any number of issues', async () => { + it('does not render when Service Desk is not supported and has any number of issues', () => { wrapper = createComponent({ provide: { isServiceDeskSupported: false } }); - await waitForPromises(); expect(findInfoBanner().exists()).toBe(false); }); - it('does not render, when there are no issues', async () => { - wrapper = createComponent({ provide: { hasAnyIssues: false } }); - await waitForPromises(); + it('does not render, when there are no issues', () => { + wrapper = createComponent({ + serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler, + }); expect(findInfoBanner().exists()).toBe(false); }); }); + describe('Empty states', () => { + describe('when there are issues', () => { + it('shows EmptyStateWithAnyIssues component', () => { + setWindowLocation(locationSearch); + wrapper = createComponent({ + serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler, + }); + + expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({ + hasSearch: true, + isOpenTab: true, + }); + }); + }); + + describe('when there are no issues', () => { + it('shows EmptyStateWithoutAnyIssues component', () => { + wrapper = createComponent({ + provide: { hasAnyIssues: false }, + serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler, + }); + + expect(wrapper.findComponent(EmptyStateWithoutAnyIssues).exists()).toBe(true); + }); + }); + }); + describe('Initial url params', () => { describe('search', () => { it('is set from the url params', () => { @@ -169,10 +203,11 @@ describe('CE ServiceDeskListApp', () => { }); describe('state', () => { - it('is set from the url params', () => { + it('is set from the url params', async () => { const initialState = STATUS_ALL; setWindowLocation(`?state=${initialState}`); wrapper = createComponent(); + await waitForPromises(); expect(findIssuableList().props('currentTab')).toBe(initialState); }); @@ -199,6 +234,7 @@ describe('CE ServiceDeskListApp', () => { describe('when user is signed out', () => { beforeEach(() => { wrapper = createComponent({ provide: { isSignedIn: false } }); + return waitForPromises(); }); it('does not render My-Reaction or Confidential tokens', () => { @@ -221,6 +257,7 @@ describe('CE ServiceDeskListApp', () => { }; wrapper = createComponent(); + return waitForPromises(); }); it('renders all tokens alphabetically', () => { @@ -243,9 +280,10 @@ describe('CE ServiceDeskListApp', () => { describe('Events', () => { describe('when "click-tab" event is emitted by IssuableList', () => { - beforeEach(() => { + beforeEach(async () => { wrapper = createComponent(); router.push = jest.fn(); + await waitForPromises(); findIssuableList().vm.$emit('click-tab', STATUS_CLOSED); }); @@ -265,6 +303,7 @@ describe('CE ServiceDeskListApp', () => { it('updates IssuableList with url params', async () => { wrapper = createComponent(); router.push = jest.fn(); + await waitForPromises(); findIssuableList().vm.$emit('filter', filteredTokens); await nextTick(); @@ -278,10 +317,10 @@ describe('CE ServiceDeskListApp', () => { describe('Errors', () => { describe.each` - error | responseHandler | message - ${'fetching issues'} | ${'serviceDeskIssuesQueryResponseHandler'} | ${ServiceDeskListApp.i18n.errorFetchingIssues} - ${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponseHandler'} | ${ServiceDeskListApp.i18n.errorFetchingCounts} - `('when there is an error $error', ({ responseHandler, message }) => { + error | responseHandler + ${'fetching issues'} | ${'serviceDeskIssuesQueryResponseHandler'} + ${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponseHandler'} + `('when there is an error $error', ({ responseHandler }) => { beforeEach(() => { wrapper = createComponent({ [responseHandler]: jest.fn().mockRejectedValue(new Error('ERROR')), @@ -290,14 +329,13 @@ describe('CE ServiceDeskListApp', () => { }); it('shows an error message', () => { - expect(findIssuableList().props('error')).toBe(message); expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR')); }); }); }); describe('When providing token for labels', () => { - it('passes function to fetchLatestLabels property if frontend caching is enabled', () => { + it('passes function to fetchLatestLabels property if frontend caching is enabled', async () => { wrapper = createComponent({ provide: { glFeatures: { @@ -305,11 +343,12 @@ describe('CE ServiceDeskListApp', () => { }, }, }); + await waitForPromises(); expect(typeof findLabelsToken().fetchLatestLabels).toBe('function'); }); - it('passes null to fetchLatestLabels property if frontend caching is disabled', () => { + it('passes null to fetchLatestLabels property if frontend caching is disabled', async () => { wrapper = createComponent({ provide: { glFeatures: { @@ -317,6 +356,7 @@ describe('CE ServiceDeskListApp', () => { }, }, }); + await waitForPromises(); expect(findLabelsToken().fetchLatestLabels).toBe(null); }); |