Skip to content

Commit

Permalink
[Cases] UI validations for max description characters, max tag charac…
Browse files Browse the repository at this point in the history
…ters and maximum tags per case (elastic#161087)

## Summary

This PR adds UI validations for

- maximum 30000 characters per description
- maximum 256 characters per tag
- maximum 200 tags per case

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

**Flaky test runner:**

https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2555

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
js-jankisalvi and kibanamachine authored Jul 6, 2023
1 parent 8543d5f commit 97dd41f
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 124 deletions.
21 changes: 1 addition & 20 deletions x-pack/plugins/cases/common/utils/validators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,9 @@
*/

import { MAX_ASSIGNEES_PER_CASE } from '../constants';
import { isInvalidTag, areTotalAssigneesInvalid } from './validators';
import { areTotalAssigneesInvalid } from './validators';

describe('validators', () => {
describe('isInvalidTag', () => {
it('validates a whitespace correctly', () => {
expect(isInvalidTag(' ')).toBe(true);
});

it('validates an empty string correctly', () => {
expect(isInvalidTag('')).toBe(true);
});

it('returns false if the string is not empty', () => {
expect(isInvalidTag('string')).toBe(false);
});

it('returns false if the string contains spaces', () => {
// Ending space has been put intentionally
expect(isInvalidTag('my string ')).toBe(false);
});
});

describe('areTotalAssigneesInvalid', () => {
const generateAssignees = (num: number) =>
Array.from(Array(num).keys()).map((uid) => {
Expand Down
2 changes: 0 additions & 2 deletions x-pack/plugins/cases/common/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
import type { CaseAssignees } from '../api';
import { MAX_ASSIGNEES_PER_CASE } from '../constants';

export const isInvalidTag = (value: string) => value.trim() === '';

export const areTotalAssigneesInvalid = (assignees?: CaseAssignees): boolean => {
if (assignees == null) {
return false;
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/cases/public/common/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ export const MAX_LENGTH_ERROR = (field: string, length: number) =>
defaultMessage: 'The length of the {field} is too long. The maximum length is {length}.',
});

export const MAX_TAGS_ERROR = (length: number) =>
i18n.translate('xpack.cases.createCase.maxTagsError', {
values: { length },
defaultMessage: 'Too many tags. The maximum number of allowed tags is {length}',
});

export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appropriateLicense', {
defaultMessage: 'appropriate license',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,18 @@
*/

import React from 'react';
import { mount } from 'enzyme';
import { waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import type { EditTagsProps } from './edit_tags';
import { EditTags } from './edit_tags';
import { getFormMock } from '../../__mock__/form';
import { readCasesPermissions, TestProviders } from '../../../common/mock';
import { waitFor } from '@testing-library/react';
import { useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib/hooks/use_form';
import { readCasesPermissions, TestProviders, createAppMockRenderer } from '../../../common/mock';
import type { AppMockRenderer } from '../../../common/mock';
import { useGetTags } from '../../../containers/use_get_tags';
import { MAX_LENGTH_PER_TAG } from '../../../../common/constants';

jest.mock('@kbn/es-ui-shared-plugin/static/forms/hook_form_lib/hooks/use_form');
jest.mock('../../../containers/use_get_tags');
jest.mock(
'@kbn/es-ui-shared-plugin/static/forms/hook_form_lib/components/form_data_provider',
() => ({
FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) =>
children({ tags: ['rad', 'dude'] }),
})
);
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
EuiFieldText: () => <input />,
};
});

const onSubmit = jest.fn();
const defaultProps: EditTagsProps = {
isLoading: false,
Expand All @@ -40,80 +26,123 @@ const defaultProps: EditTagsProps = {
};

describe('EditTags ', () => {
let appMockRender: AppMockRenderer;

const sampleTags = ['coke', 'pepsi'];
const fetchTags = jest.fn();
const formHookMock = getFormMock({ tags: sampleTags });

beforeEach(() => {
jest.resetAllMocks();
(useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock }));

(useGetTags as jest.Mock).mockImplementation(() => ({
data: sampleTags,
refetch: fetchTags,
}));
appMockRender = createAppMockRenderer();
});

it('Renders no tags, and then edit', () => {
const wrapper = mount(
<TestProviders>
<EditTags {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="no-tags"]`).last().exists()).toBeTruthy();
wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click');
expect(wrapper.find(`[data-test-subj="no-tags"]`).last().exists()).toBeFalsy();
expect(wrapper.find(`[data-test-subj="edit-tags"]`).last().exists()).toBeTruthy();
it('renders no tags, and then edit', async () => {
appMockRender.render(<EditTags {...defaultProps} />);

expect(screen.getByTestId('no-tags')).toBeInTheDocument();

userEvent.click(screen.getByTestId('tag-list-edit-button'));

await waitFor(() => {
expect(screen.queryByTestId('no-tags')).not.toBeInTheDocument();
expect(screen.getByTestId('edit-tags')).toBeInTheDocument();
});
});

it('Edit tag on submit', async () => {
const wrapper = mount(
<TestProviders>
<EditTags {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click');
wrapper.find(`[data-test-subj="edit-tags-submit"]`).last().simulate('click');
await waitFor(() => expect(onSubmit).toBeCalledWith(sampleTags));
it('edit tag from options on submit', async () => {
appMockRender.render(<EditTags {...defaultProps} />);

userEvent.click(screen.getByTestId('tag-list-edit-button'));

userEvent.type(screen.getByRole('combobox'), `${sampleTags[0]}{enter}`);

userEvent.click(screen.getByTestId('edit-tags-submit'));

await waitFor(() => expect(onSubmit).toBeCalledWith([sampleTags[0]]));
});

it('Tag options render with new tags added', () => {
const wrapper = mount(
<TestProviders>
<EditTags {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click');
expect(
wrapper.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`).first().prop('options')
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
it('add new tags on submit', async () => {
appMockRender.render(<EditTags {...defaultProps} />);

userEvent.click(screen.getByTestId('tag-list-edit-button'));

await waitFor(() => {
expect(screen.getByTestId('edit-tags')).toBeInTheDocument();
});

userEvent.type(screen.getByRole('combobox'), 'dude{enter}');

userEvent.click(screen.getByTestId('edit-tags-submit'));

await waitFor(() => expect(onSubmit).toBeCalledWith(['dude']));
});

it('Cancels on cancel', () => {
const props = {
...defaultProps,
tags: ['pepsi'],
};
const wrapper = mount(
<TestProviders>
<EditTags {...props} />
</TestProviders>
);
it('cancels on cancel', async () => {
appMockRender.render(<EditTags {...defaultProps} />);

userEvent.click(screen.getByTestId('tag-list-edit-button'));

userEvent.type(screen.getByRole('combobox'), 'new{enter}');

await waitFor(() => {
expect(screen.getByTestId('comboBoxInput')).toHaveTextContent('new');
});

userEvent.click(screen.getByTestId('edit-tags-cancel'));

await waitFor(() => {
expect(onSubmit).not.toBeCalled();
expect(screen.getByTestId('no-tags')).toBeInTheDocument();
});
});

expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click');
it('shows error when tag is empty', async () => {
appMockRender.render(<EditTags {...defaultProps} />);

expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeFalsy();
wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click');
wrapper.update();
expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
userEvent.click(screen.getByTestId('tag-list-edit-button'));

await waitFor(() => {
expect(screen.getByTestId('edit-tags')).toBeInTheDocument();
});

userEvent.type(screen.getByRole('combobox'), ' {enter}');

await waitFor(() => {
expect(screen.getByText('A tag must contain at least one non-space character.'));
});
});

it('shows error when tag is too long', async () => {
const longTag = 'z'.repeat(MAX_LENGTH_PER_TAG + 1);

appMockRender.render(<EditTags {...defaultProps} />);

userEvent.click(screen.getByTestId('tag-list-edit-button'));

await waitFor(() => {
expect(screen.getByTestId('edit-tags')).toBeInTheDocument();
});

userEvent.paste(screen.getByRole('combobox'), `${longTag}`);
userEvent.keyboard('{enter}');

await waitFor(() => {
expect(screen.getByText('The length of the tag is too long. The maximum length is 256.'));
});
});

it('does not render when the user does not have update permissions', () => {
const wrapper = mount(
appMockRender.render(
<TestProviders permissions={readCasesPermissions()}>
<EditTags {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="tag-list-edit"]`).exists()).toBeFalsy();

expect(screen.queryByTestId('tag-list-edit')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const CategoryFormFieldComponent: React.FC<Props> = ({
label={CATEGORY}
error={errorMessage}
isInvalid={isInvalid}
data-test-subj="case-create-form-category"
fullWidth
>
<CategoryComponent
Expand Down
50 changes: 45 additions & 5 deletions x-pack/plugins/cases/public/components/create/description.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React from 'react';
import { waitFor } from '@testing-library/react';
import { waitFor, screen } from '@testing-library/react';
import userEvent, { specialChars } from '@testing-library/user-event';

import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
Expand All @@ -16,6 +16,7 @@ import type { FormProps } from './schema';
import { schema } from './schema';
import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { MAX_DESCRIPTION_LENGTH } from '../../../common/constants';

describe('Description', () => {
let globalForm: FormHook;
Expand Down Expand Up @@ -45,29 +46,68 @@ describe('Description', () => {
});

it('it renders', async () => {
const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Description {...defaultProps} />
</MockHookWrapperComponent>
);

expect(result.getByTestId('caseDescription')).toBeInTheDocument();
expect(screen.getByTestId('caseDescription')).toBeInTheDocument();
});

it('it changes the description', async () => {
const result = appMockRender.render(
appMockRender.render(
<MockHookWrapperComponent>
<Description {...defaultProps} />
</MockHookWrapperComponent>
);

const description = screen.getByTestId('euiMarkdownEditorTextArea');

userEvent.type(
result.getByRole('textbox'),
description,
`${specialChars.selectAll}${specialChars.delete}My new description`
);

await waitFor(() => {
expect(globalForm.getFormData()).toEqual({ description: 'My new description' });
});
});

it('shows an error when description is empty', async () => {
appMockRender.render(
<MockHookWrapperComponent>
<Description {...defaultProps} />
</MockHookWrapperComponent>
);

const description = screen.getByTestId('euiMarkdownEditorTextArea');

userEvent.clear(description);
userEvent.type(description, ' ');

await waitFor(() => {
expect(screen.getByText('A description is required.')).toBeInTheDocument();
});
});

it('shows an error when description is too long', async () => {
const longDescription = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1);

appMockRender.render(
<MockHookWrapperComponent>
<Description {...defaultProps} />
</MockHookWrapperComponent>
);

const description = screen.getByTestId('euiMarkdownEditorTextArea');

userEvent.paste(description, longDescription);

await waitFor(() => {
expect(
screen.getByText('The length of the description is too long. The maximum length is 30000.')
).toBeInTheDocument();
});
});
});
Loading

0 comments on commit 97dd41f

Please sign in to comment.