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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-10 09:09:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-10 09:09:29 +0300
commit76c4dd062c4eeb853866ef8b6451c59f9e24221c (patch)
treefaf481c7b2f6da10c13234ad4e4a6ca1cb5a1030 /spec/frontend
parentc2858333644a2bca10fd556a5a298b4a1aaedca2 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js19
-rw-r--r--spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js24
-rw-r--r--spec/frontend/lib/print_markdown_dom_spec.js102
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_export_spec.js48
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js199
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_spec.js52
-rw-r--r--spec/frontend/projects/settings_service_desk/components/mock_data.js17
-rw-r--r--spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js74
-rw-r--r--spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js89
-rw-r--r--spec/frontend/service_desk/components/service_desk_list_app_spec.js68
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);
});