[Cases] UI validation for total number of comment characters (#161357)

## Summary

This PR adds UI validation for comments maximum length. 
It shows error message and disables save button when the comment exceeds
30k characters while

- **Adding a new comment**


![image](42cafdfc-6e88-4bf9-ab93-9fb61de6eb78)

- **Updating an existing comment**


![image](1d8408d1-c1cd-404c-b1ba-f4ecb94c4225)


### Checklist

Delete any items that are not applicable to this PR.
- [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

### 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>
This commit is contained in:
Janki Salvi 2023-07-10 10:09:23 +02:00 committed by GitHub
parent ac8d73ac6d
commit d8c8b7b0f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 330 additions and 146 deletions

View file

@ -292,7 +292,8 @@ export const SELECT_CASE_TITLE = i18n.translate('xpack.cases.common.allCases.cas
export const MAX_LENGTH_ERROR = (field: string, length: number) =>
i18n.translate('xpack.cases.createCase.maxLengthError', {
values: { field, length },
defaultMessage: 'The length of the {field} is too long. The maximum length is {length}.',
defaultMessage:
'The length of the {field} is too long. The maximum length is {length} characters.',
});
export const MAX_TAGS_ERROR = (length: number) =>

View file

@ -6,14 +6,14 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { waitFor, act, fireEvent } from '@testing-library/react';
import { waitFor, act, fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { noop } from 'lodash/fp';
import { noCreateCasesPermissions, TestProviders, createAppMockRenderer } from '../../common/mock';
import { CommentType } from '../../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { SECURITY_SOLUTION_OWNER, MAX_COMMENT_LENGTH } from '../../../common/constants';
import { useCreateAttachments } from '../../containers/use_create_attachments';
import type { AddCommentProps, AddCommentRefObject } from '.';
import { AddComment } from '.';
@ -52,8 +52,11 @@ const appId = 'testAppId';
const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`;
describe('AddComment ', () => {
let appMockRender: AppMockRenderer;
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer();
useCreateAttachmentsMock.mockImplementation(() => defaultResponse);
});
@ -61,22 +64,47 @@ describe('AddComment ', () => {
sessionStorage.removeItem(draftKey);
});
it('should post comment on submit click', async () => {
const wrapper = mount(
<TestProviders>
<AddComment {...addCommentProps} />
it('renders correctly', () => {
appMockRender.render(<AddComment {...addCommentProps} />);
expect(screen.getByTestId('add-comment')).toBeInTheDocument();
});
it('should render spinner and disable submit when loading', () => {
useCreateAttachmentsMock.mockImplementation(() => ({
...defaultResponse,
isLoading: true,
}));
appMockRender.render(<AddComment {...{ ...addCommentProps, showLoading: true }} />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
expect(screen.getByTestId('submit-comment')).toHaveAttribute('disabled');
});
it('should hide the component when the user does not have create permissions', () => {
useCreateAttachmentsMock.mockImplementation(() => ({
...defaultResponse,
isLoading: true,
}));
appMockRender.render(
<TestProviders permissions={noCreateCasesPermissions()}>
<AddComment {...{ ...addCommentProps }} />
</TestProviders>
);
wrapper
.find(`[data-test-subj="add-comment"] textarea`)
.first()
.simulate('change', { target: { value: sampleData.comment } });
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
});
expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy();
it('should post comment on submit click', async () => {
appMockRender.render(<AddComment {...addCommentProps} />);
const markdown = screen.getByTestId('euiMarkdownEditorTextArea');
userEvent.type(markdown, sampleData.comment);
userEvent.click(screen.getByTestId('submit-comment'));
wrapper.find(`button[data-test-subj="submit-comment"]`).first().simulate('click');
await waitFor(() => {
expect(onCommentSaving).toBeCalled();
expect(createAttachments).toBeCalledWith(
@ -94,105 +122,49 @@ describe('AddComment ', () => {
});
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('');
expect(screen.getByTestId('euiMarkdownEditorTextArea')).toHaveTextContent('');
});
});
it('should render spinner and disable submit when loading', () => {
useCreateAttachmentsMock.mockImplementation(() => ({
...defaultResponse,
isLoading: true,
}));
const wrapper = mount(
<TestProviders>
<AddComment {...{ ...addCommentProps, showLoading: true }} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy();
expect(
wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled')
).toBeTruthy();
});
it('should disable submit button when isLoading is true', () => {
useCreateAttachmentsMock.mockImplementation(() => ({
...defaultResponse,
isLoading: true,
}));
const wrapper = mount(
<TestProviders>
<AddComment {...addCommentProps} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled')
).toBeTruthy();
});
it('should hide the component when the user does not have create permissions', () => {
useCreateAttachmentsMock.mockImplementation(() => ({
...defaultResponse,
isLoading: true,
}));
const wrapper = mount(
<TestProviders permissions={noCreateCasesPermissions()}>
<AddComment {...{ ...addCommentProps }} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeFalsy();
});
it('should insert a quote', async () => {
const sampleQuote = 'what a cool quote \n with new lines';
const ref = React.createRef<AddCommentRefObject>();
const wrapper = mount(
<TestProviders>
<AddComment {...addCommentProps} ref={ref} />
</TestProviders>
);
wrapper
.find(`[data-test-subj="add-comment"] textarea`)
.first()
.simulate('change', { target: { value: sampleData.comment } });
appMockRender.render(<AddComment {...addCommentProps} ref={ref} />);
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), sampleData.comment);
await act(async () => {
ref.current!.addQuote(sampleQuote);
});
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(
`${sampleData.comment}\n\n> what a cool quote \n> with new lines \n\n`
);
await waitFor(() => {
expect(screen.getByTestId('euiMarkdownEditorTextArea').textContent).toContain(
`${sampleData.comment}\n\n> what a cool quote \n> with new lines \n\n`
);
});
});
it('should call onFocus when adding a quote', async () => {
const ref = React.createRef<AddCommentRefObject>();
mount(
<TestProviders>
<AddComment {...addCommentProps} ref={ref} />
</TestProviders>
);
appMockRender.render(<AddComment {...addCommentProps} ref={ref} />);
ref.current!.editor!.textarea!.focus = jest.fn();
await act(async () => {
ref.current!.addQuote('a comment');
});
expect(ref.current!.editor!.textarea!.focus).toHaveBeenCalled();
await waitFor(() => {
expect(ref.current!.editor!.textarea!.focus).toHaveBeenCalled();
});
});
it('should NOT call onFocus on mount', async () => {
const ref = React.createRef<AddCommentRefObject>();
mount(
<TestProviders>
<AddComment {...addCommentProps} ref={ref} />
</TestProviders>
);
appMockRender.render(<AddComment {...addCommentProps} ref={ref} />);
ref.current!.editor!.textarea!.focus = jest.fn();
expect(ref.current!.editor!.textarea!.focus).not.toHaveBeenCalled();
@ -208,12 +180,10 @@ describe('AddComment ', () => {
const mockTimelineIntegration = { ...timelineIntegrationMock };
mockTimelineIntegration.hooks.useInsertTimeline = useInsertTimelineMock;
const wrapper = mount(
<TestProviders>
<CasesTimelineIntegrationProvider timelineIntegration={mockTimelineIntegration}>
<AddComment {...addCommentProps} />
</CasesTimelineIntegrationProvider>
</TestProviders>
appMockRender.render(
<CasesTimelineIntegrationProvider timelineIntegration={mockTimelineIntegration}>
<AddComment {...addCommentProps} />
</CasesTimelineIntegrationProvider>
);
act(() => {
@ -221,7 +191,56 @@ describe('AddComment ', () => {
});
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)');
expect(screen.getByTestId('euiMarkdownEditorTextArea')).toHaveTextContent('[title](url)');
});
});
describe('errors', () => {
it('shows an error when comment is empty', async () => {
appMockRender.render(<AddComment {...addCommentProps} />);
const markdown = screen.getByTestId('euiMarkdownEditorTextArea');
userEvent.type(markdown, 'test');
userEvent.clear(markdown);
await waitFor(() => {
expect(screen.getByText('Empty comments are not allowed.')).toBeInTheDocument();
expect(screen.getByTestId('submit-comment')).toHaveAttribute('disabled');
});
});
it('shows an error when comment is of empty characters', async () => {
appMockRender.render(<AddComment {...addCommentProps} />);
const markdown = screen.getByTestId('euiMarkdownEditorTextArea');
userEvent.clear(markdown);
userEvent.type(markdown, ' ');
await waitFor(() => {
expect(screen.getByText('Empty comments are not allowed.')).toBeInTheDocument();
expect(screen.getByTestId('submit-comment')).toHaveAttribute('disabled');
});
});
it('shows an error when comment is too long', async () => {
const longComment = 'a'.repeat(MAX_COMMENT_LENGTH + 1);
appMockRender.render(<AddComment {...addCommentProps} />);
const markdown = screen.getByTestId('euiMarkdownEditorTextArea');
userEvent.paste(markdown, longComment);
await waitFor(() => {
expect(
screen.getByText(
'The length of the comment is too long. The maximum length is 30000 characters.'
)
).toBeInTheDocument();
expect(screen.getByTestId('submit-comment')).toHaveAttribute('disabled');
});
});
});
});
@ -247,9 +266,9 @@ describe('draft comment ', () => {
});
it('should clear session storage on submit', async () => {
const result = appMockRenderer.render(<AddComment {...addCommentProps} />);
appMockRenderer.render(<AddComment {...addCommentProps} />);
fireEvent.change(result.getByLabelText('caseComment'), {
fireEvent.change(screen.getByLabelText('caseComment'), {
target: { value: sampleData.comment },
});
@ -258,10 +277,10 @@ describe('draft comment ', () => {
});
await waitFor(() => {
expect(result.getByLabelText('caseComment')).toHaveValue(sessionStorage.getItem(draftKey));
expect(screen.getByLabelText('caseComment')).toHaveValue(sessionStorage.getItem(draftKey));
});
fireEvent.click(result.getByTestId('submit-comment'));
fireEvent.click(screen.getByTestId('submit-comment'));
await waitFor(() => {
expect(onCommentSaving).toBeCalled();
@ -280,7 +299,7 @@ describe('draft comment ', () => {
});
await waitFor(() => {
expect(result.getByLabelText('caseComment').textContent).toBe('');
expect(screen.getByLabelText('caseComment').textContent).toBe('');
expect(sessionStorage.getItem(draftKey)).toBe('');
});
});
@ -295,9 +314,9 @@ describe('draft comment ', () => {
});
it('should have draft comment same as existing session storage', async () => {
const result = appMockRenderer.render(<AddComment {...addCommentProps} />);
appMockRenderer.render(<AddComment {...addCommentProps} />);
expect(result.getByLabelText('caseComment')).toHaveValue('value set in storage');
expect(screen.getByLabelText('caseComment')).toHaveValue('value set in storage');
});
});
});

View file

@ -36,6 +36,7 @@ import type { AddCommentFormSchema } from './schema';
import { schema } from './schema';
import { InsertTimeline } from '../insert_timeline';
import { useCasesContext } from '../cases_context/use_cases_context';
import { MAX_COMMENT_LENGTH } from '../../../common/constants';
const MySpinner = styled(EuiLoadingSpinner)`
position: absolute;
@ -174,6 +175,9 @@ export const AddComment = React.memo(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focusOnContext]);
const isDisabled =
isLoading || !comment?.trim().length || comment.trim().length > MAX_COMMENT_LENGTH;
return (
<span id="add-comment-permLink">
{isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />}
@ -200,7 +204,7 @@ export const AddComment = React.memo(
data-test-subj="submit-comment"
fill
iconType="plusInCircle"
isDisabled={!comment || isLoading}
isDisabled={isDisabled}
isLoading={isLoading}
onClick={onSubmit}
>

View file

@ -9,10 +9,11 @@ import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form
import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import type { CommentRequestUserType } from '../../../common/api';
import { MAX_COMMENT_LENGTH } from '../../../common/constants';
import * as i18n from './translations';
const { emptyField } = fieldValidators;
const { emptyField, maxLengthField } = fieldValidators;
export interface AddCommentFormSchema {
comment: CommentRequestUserType['comment'];
@ -25,6 +26,12 @@ export const schema: FormSchema<AddCommentFormSchema> = {
{
validator: emptyField(i18n.EMPTY_COMMENTS_NOT_ALLOWED),
},
{
validator: maxLengthField({
length: MAX_COMMENT_LENGTH,
message: i18n.MAX_LENGTH_ERROR('comment', MAX_COMMENT_LENGTH),
}),
},
],
},
};

View file

@ -194,7 +194,9 @@ describe('EditCategory ', () => {
await waitFor(() => {
expect(
screen.getByText('The length of the category is too long. The maximum length is 50.')
screen.getByText(
'The length of the category is too long. The maximum length is 50 characters.'
)
).toBeInTheDocument();
});

View file

@ -132,7 +132,9 @@ describe('EditTags ', () => {
userEvent.keyboard('{enter}');
await waitFor(() => {
expect(screen.getByText('The length of the tag is too long. The maximum length is 256.'));
expect(
screen.getByText('The length of the tag is too long. The maximum length is 256 characters.')
);
});
});

View file

@ -120,7 +120,11 @@ describe('Category', () => {
expect(onSubmit).toBeCalledWith({}, false);
});
expect(screen.getByText('The length of the category is too long. The maximum length is 50.'));
expect(
screen.getByText(
'The length of the category is too long. The maximum length is 50 characters.'
)
);
});
it('can set a category from existing ones', async () => {

View file

@ -106,7 +106,9 @@ describe('Description', () => {
await waitFor(() => {
expect(
screen.getByText('The length of the description is too long. The maximum length is 30000.')
screen.getByText(
'The length of the description is too long. The maximum length is 30000 characters.'
)
).toBeInTheDocument();
});
});

View file

@ -325,7 +325,9 @@ describe('Create case', () => {
await waitFor(() => {
expect(
screen.getByText('The length of the name is too long. The maximum length is 160.')
screen.getByText(
'The length of the name is too long. The maximum length is 160 characters.'
)
).toBeInTheDocument();
});

View file

@ -103,7 +103,9 @@ describe('Tags', () => {
userEvent.keyboard('{enter}');
await waitFor(() => {
expect(screen.getByText('The length of the tag is too long. The maximum length is 256.'));
expect(
screen.getByText('The length of the tag is too long. The maximum length is 256 characters.')
);
});
});
});

View file

@ -122,6 +122,23 @@ describe('Description', () => {
await waitFor(() => {
expect(screen.getByText('A description is required.')).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveAttribute('disabled');
});
});
it('shows an error when description is a sting of empty characters', async () => {
const res = appMockRender.render(
<Description {...defaultProps} onUpdateField={onUpdateField} />
);
userEvent.click(res.getByTestId('description-edit-icon'));
userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea'));
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), ' ');
await waitFor(() => {
expect(screen.getByText('A description is required.')).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveAttribute('disabled');
});
});
@ -141,8 +158,11 @@ describe('Description', () => {
await waitFor(() => {
expect(
screen.getByText('The length of the description is too long. The maximum length is 30000.')
screen.getByText(
'The length of the description is too long. The maximum length is 30000 characters.'
)
).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveAttribute('disabled');
});
});

View file

@ -209,7 +209,7 @@ describe('EditableTitle', () => {
wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click');
wrapper.update();
expect(wrapper.find('.euiFormErrorText').text()).toBe(
'The length of the title is too long. The maximum length is 160.'
'The length of the title is too long. The maximum length is 160 characters.'
);
expect(submitTitle).not.toHaveBeenCalled();
@ -263,7 +263,7 @@ describe('EditableTitle', () => {
wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click');
wrapper.update();
expect(wrapper.find('.euiFormErrorText').text()).toBe(
'The length of the title is too long. The maximum length is 160.'
'The length of the title is too long. The maximum length is 160 characters.'
);
// write a shorter one

View file

@ -8,21 +8,19 @@
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
import React from 'react';
import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import * as i18n from '../case_view/translations';
interface EditableMarkdownFooterProps {
handleSaveAction: () => Promise<void>;
handleCancelAction: () => void;
isSaveDisabled: boolean;
}
const EditableMarkdownFooterComponent: React.FC<EditableMarkdownFooterProps> = ({
handleSaveAction,
handleCancelAction,
isSaveDisabled,
}) => {
const [{ content }] = useFormData<{ content: string }>({ watch: ['content'] });
return (
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
@ -42,7 +40,7 @@ const EditableMarkdownFooterComponent: React.FC<EditableMarkdownFooterProps> = (
fill
iconType="save"
onClick={handleSaveAction}
disabled={!content}
disabled={isSaveDisabled}
size="s"
>
{i18n.SAVE}

View file

@ -6,14 +6,17 @@
*/
import React from 'react';
import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { useForm, Form, FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { waitFor, fireEvent, screen, render, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import * as i18n from '../../common/translations';
const { emptyField, maxLengthField } = fieldValidators;
import { EditableMarkdown } from '.';
import { TestProviders } from '../../common/mock';
import type { Content } from '../user_actions/schema';
import { schema } from '../user_actions/schema';
jest.mock('../../common/lib/kibana');
@ -21,10 +24,27 @@ const onChangeEditable = jest.fn();
const onSaveContent = jest.fn();
const newValue = 'Hello from Tehas';
const emptyValue = '';
const hyperlink = `[hyperlink](http://elastic.co)`;
const draftStorageKey = `cases.testAppId.caseId.markdown-id.markdownEditor`;
const content = `A link to a timeline ${hyperlink}`;
const maxLength = 5000;
const mockSchema: FormSchema<{ content: string }> = {
content: {
type: FIELD_TYPES.TEXTAREA,
validations: [
{
validator: emptyField(i18n.REQUIRED_FIELD),
},
{
validator: maxLengthField({
length: maxLength,
message: i18n.MAX_LENGTH_ERROR('textarea', maxLength),
}),
},
],
},
};
const editorRef: React.MutableRefObject<null | undefined> = { current: null };
const defaultProps = {
@ -36,7 +56,7 @@ const defaultProps = {
onChangeEditable,
onSaveContent,
fieldName: 'content',
formSchema: schema,
formSchema: mockSchema,
editorRef,
};
@ -45,10 +65,10 @@ describe('EditableMarkdown', () => {
children,
testProviderProps = {},
}) => {
const { form } = useForm<Content>({
const { form } = useForm<{ content: string }>({
defaultValue: { content },
options: { stripEmptyFields: false },
schema,
schema: mockSchema,
});
return (
@ -100,20 +120,6 @@ describe('EditableMarkdown', () => {
expect(onSaveContent).not.toHaveBeenCalled();
});
it('Save button disabled if current text is empty', async () => {
render(
<MockHookWrapperComponent>
<EditableMarkdown {...defaultProps} />
</MockHookWrapperComponent>
);
fireEvent.change(screen.getByTestId('euiMarkdownEditorTextArea'), { value: emptyValue });
await waitFor(() => {
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
it('Cancel button click calls only onChangeEditable', async () => {
render(
<MockHookWrapperComponent>
@ -129,6 +135,65 @@ describe('EditableMarkdown', () => {
});
});
describe('errors', () => {
it('Shows error message and save button disabled if current text is empty', async () => {
render(
<MockHookWrapperComponent>
<EditableMarkdown {...defaultProps} />
</MockHookWrapperComponent>
);
userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea'));
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), '');
await waitFor(() => {
expect(screen.getByText('Required field')).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
it('Shows error message and save button disabled if current text is of empty characters', async () => {
render(
<MockHookWrapperComponent>
<EditableMarkdown {...defaultProps} />
</MockHookWrapperComponent>
);
userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea'));
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), ' ');
await waitFor(() => {
expect(screen.getByText('Required field')).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
it('Shows error message and save button disabled if current text is too long', async () => {
const longComment = 'b'.repeat(maxLength + 1);
render(
<MockHookWrapperComponent>
<EditableMarkdown {...defaultProps} />
</MockHookWrapperComponent>
);
const markdown = screen.getByTestId('euiMarkdownEditorTextArea');
userEvent.paste(markdown, longComment);
await waitFor(() => {
expect(
screen.getByText(
`The length of the textarea is too long. The maximum length is ${maxLength} characters.`
)
).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
});
describe('draft comment ', () => {
beforeAll(() => {
jest.useFakeTimers();

View file

@ -46,7 +46,7 @@ const EditableMarkDownRenderer = forwardRef<
options: { stripEmptyFields: false },
schema: formSchema,
});
const { submit, setFieldValue } = form;
const { submit, setFieldValue, isValid: isFormValid } = form;
const setComment = useCallback(
(newComment) => {
@ -90,6 +90,7 @@ const EditableMarkDownRenderer = forwardRef<
<EditableMarkdownFooter
handleSaveAction={handleSaveAction}
handleCancelAction={handleCancelAction}
isSaveDisabled={isFormValid !== undefined && !isFormValid}
/>
),
initialValue: content,

View file

@ -12,6 +12,7 @@ import userEvent from '@testing-library/user-event';
import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { UserActionMarkdown } from './markdown_form';
import { MAX_COMMENT_LENGTH } from '../../../common/constants';
jest.mock('../../common/lib/kibana');
jest.mock('../../common/navigation/hooks');
@ -58,6 +59,53 @@ describe('UserActionMarkdown ', () => {
expect(screen.getByTestId('editable-cancel-markdown')).toBeInTheDocument();
});
describe('errors', () => {
it('Shows error message and save button disabled if current text is empty', async () => {
appMockRenderer.render(<UserActionMarkdown {...{ ...defaultProps, isEditable: true }} />);
userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea'));
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), '');
await waitFor(() => {
expect(screen.getByText('Empty comments are not allowed.')).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
it('Shows error message and save button disabled if current text is of empty characters', async () => {
appMockRenderer.render(<UserActionMarkdown {...{ ...defaultProps, isEditable: true }} />);
userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea'));
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), ' ');
await waitFor(() => {
expect(screen.getByText('Empty comments are not allowed.')).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
it('Shows error message and save button disabled if current text is too long', async () => {
const longComment = 'b'.repeat(MAX_COMMENT_LENGTH + 1);
appMockRenderer.render(<UserActionMarkdown {...{ ...defaultProps, isEditable: true }} />);
const markdown = screen.getByTestId('euiMarkdownEditorTextArea');
userEvent.paste(markdown, longComment);
await waitFor(() => {
expect(
screen.getByText(
'The length of the comment is too long. The maximum length is 30000 characters.'
)
).toBeInTheDocument();
expect(screen.getByTestId('editable-save-markdown')).toHaveProperty('disabled');
});
});
});
describe('useForm stale state bug', () => {
const oldContent = defaultProps.content;
const appendContent = ' appended content';

View file

@ -9,8 +9,9 @@ import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form
import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import * as i18n from '../../common/translations';
import { MAX_COMMENT_LENGTH } from '../../../common/constants';
const { emptyField } = fieldValidators;
const { emptyField, maxLengthField } = fieldValidators;
export interface Content {
content: string;
}
@ -19,7 +20,13 @@ export const schema: FormSchema<Content> = {
type: FIELD_TYPES.TEXTAREA,
validations: [
{
validator: emptyField(i18n.REQUIRED_FIELD),
validator: emptyField(i18n.EMPTY_COMMENTS_NOT_ALLOWED),
},
{
validator: maxLengthField({
length: MAX_COMMENT_LENGTH,
message: i18n.MAX_LENGTH_ERROR('comment', MAX_COMMENT_LENGTH),
}),
},
],
},

View file

@ -82,7 +82,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
const title = await find.byCssSelector('[data-test-subj="caseTitle"]');
expect(await title.getVisibleText()).contain(
'The length of the name is too long. The maximum length is 160.'
'The length of the name is too long. The maximum length is 160 characters.'
);
const description = await testSubjects.find('caseDescription');
@ -90,12 +90,12 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
const tags = await testSubjects.find('caseTags');
expect(await tags.getVisibleText()).contain(
'The length of the tag is too long. The maximum length is 256.'
'The length of the tag is too long. The maximum length is 256 characters.'
);
const category = await testSubjects.find('case-create-form-category');
expect(await category.getVisibleText()).contain(
'The length of the category is too long. The maximum length is 50.'
'The length of the category is too long. The maximum length is 50 characters.'
);
});

View file

@ -82,7 +82,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
const error = await find.byCssSelector('.euiFormErrorText');
expect(await error.getVisibleText()).equal(
'The length of the title is too long. The maximum length is 160.'
'The length of the title is too long. The maximum length is 160 characters.'
);
await testSubjects.click('editable-title-cancel-btn');
@ -135,7 +135,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
const error = await find.byCssSelector('.euiFormErrorText');
expect(await error.getVisibleText()).equal(
'The length of the category is too long. The maximum length is 50.'
'The length of the category is too long. The maximum length is 50 characters.'
);
await testSubjects.click('edit-category-cancel');
@ -167,7 +167,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
const error = await find.byCssSelector('.euiFormErrorText');
expect(await error.getVisibleText()).equal(
'The length of the tag is too long. The maximum length is 256.'
'The length of the tag is too long. The maximum length is 256 characters.'
);
await testSubjects.click('edit-tags-cancel');