mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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**  - **Updating an existing comment**  ### 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:
parent
ac8d73ac6d
commit
d8c8b7b0f0
19 changed files with 330 additions and 146 deletions
|
@ -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) =>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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.')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue