From 3205aca88733ff8ff75f9be6daa54baa8a598d56 Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Tue, 14 Apr 2020 11:47:15 -0600
Subject: [PATCH] [SIEM] [Cases] Unit tests for case UI components (#63005)
(#63256)
---
.../components/filter_popover/index.tsx | 1 +
.../components/header_page/editable_title.tsx | 9 +-
.../siem/public/containers/case/api.ts | 4 +-
.../case/configure/use_configure.tsx | 4 +-
.../plugins/siem/public/pages/case/case.tsx | 9 +-
.../siem/public/pages/case/case_details.tsx | 6 +-
.../case/components/__mock__/case_data.tsx | 226 ++++++++++++
.../pages/case/components/__mock__/form.ts | 37 ++
.../pages/case/components/__mock__/router.ts | 39 +++
.../components/add_comment/index.test.tsx | 144 ++++++++
.../case/components/add_comment/index.tsx | 6 +-
.../components/all_cases/__mock__/index.tsx | 115 ------
.../components/all_cases/columns.test.tsx | 48 +++
.../case/components/all_cases/columns.tsx | 14 +-
.../case/components/all_cases/index.test.tsx | 66 +++-
.../all_cases/table_filters.test.tsx | 121 +++++++
.../components/all_cases/table_filters.tsx | 7 +-
.../case/components/all_cases/translations.ts | 4 +
.../pages/case/components/callout/helpers.tsx | 4 +-
.../case/components/callout/index.test.tsx | 71 ++++
.../pages/case/components/callout/index.tsx | 10 +-
.../case/components/case_status/index.tsx | 2 +-
.../components/case_view/__mock__/index.tsx | 93 -----
.../components/case_view/actions.test.tsx | 10 +-
.../case/components/case_view/actions.tsx | 1 -
.../case/components/case_view/index.test.tsx | 320 +++++++++++++----
.../pages/case/components/case_view/index.tsx | 8 +-
.../case/components/create/index.test.tsx | 121 +++++++
.../pages/case/components/create/index.tsx | 7 +-
.../case/components/tag_list/index.test.tsx | 138 ++++++++
.../pages/case/components/tag_list/index.tsx | 17 +-
.../use_push_to_service/index.test.tsx | 192 ++++++++++
.../components/use_push_to_service/index.tsx | 5 +-
.../user_action_tree/helpers.test.tsx | 143 ++++++++
.../components/user_action_tree/helpers.tsx | 12 +-
.../user_action_tree/index.test.tsx | 331 ++++++++++++++++++
.../components/user_action_tree/index.tsx | 10 +-
.../user_action_tree/user_action_item.tsx | 15 +-
.../user_action_tree/user_action_markdown.tsx | 20 +-
.../user_action_title.test.tsx | 57 +++
.../user_action_tree/user_action_title.tsx | 16 +-
.../siem/public/pages/case/translations.ts | 4 +
.../scripts/generate_case_and_comment_data.sh | 6 +-
.../case/server/scripts/generate_case_data.sh | 4 +-
44 files changed, 2117 insertions(+), 360 deletions(-)
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx
delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx
delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx
diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx
index 3c01ec18a879f..fca6396a53745 100644
--- a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx
@@ -89,6 +89,7 @@ export const FilterPopoverComponent = ({
{options.map((option, index) => (
diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx
index 165be00384779..0c6f7258d09dc 100644
--- a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx
@@ -60,12 +60,9 @@ const EditableTitleComponent: React.FC = ({
}, [changedTitle, title]);
const handleOnChange = useCallback(
- (e: ChangeEvent) => {
- onTitleChange(e.target.value);
- },
- [onTitleChange]
+ (e: ChangeEvent) => onTitleChange(e.target.value),
+ []
);
-
return editMode ? (
@@ -107,7 +104,7 @@ const EditableTitleComponent: React.FC = ({
- {isLoading && }
+ {isLoading && }
{!isLoading && (
(decodeCaseResponse(response));
};
-export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise => {
+export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise => {
const response = await KibanaServices.get().http.fetch(CASES_URL, {
method: 'DELETE',
query: { ids: JSON.stringify(caseIds) },
signal,
});
- return response === 'true' ? true : false;
+ return response;
};
export const pushCase = async (
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
index 7f57149d4e56d..1c03a09a8c2ea 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
@@ -55,7 +55,6 @@ export const useCaseConfigure = ({
setLoading(true);
const res = await getCaseConfigure({ signal: abortCtrl.signal });
if (!didCancel) {
- setLoading(false);
if (res != null) {
setConnector(res.connectorId, res.connectorName);
if (setClosureType != null) {
@@ -73,6 +72,7 @@ export const useCaseConfigure = ({
}
}
}
+ setLoading(false);
}
} catch (error) {
if (!didCancel) {
@@ -117,7 +117,6 @@ export const useCaseConfigure = ({
abortCtrl.signal
);
if (!didCancel) {
- setPersistLoading(false);
setConnector(res.connectorId);
if (setClosureType) {
setClosureType(res.closureType);
@@ -131,6 +130,7 @@ export const useCaseConfigure = ({
}
displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster);
+ setPersistLoading(false);
}
} catch (error) {
if (!didCancel) {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx
index 2ae35796387b8..aefb0a93366b8 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx
@@ -11,11 +11,9 @@ import { useGetUserSavedObjectPermissions } from '../../lib/kibana';
import { SpyRoute } from '../../utils/route/spy_routes';
import { AllCases } from './components/all_cases';
-import { getSavedObjectReadOnly, CaseCallOut } from './components/callout';
+import { savedObjectReadOnly, CaseCallOut } from './components/callout';
import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions';
-const infoReadSavedObject = getSavedObjectReadOnly();
-
export const CasesPage = React.memo(() => {
const userPermissions = useGetUserSavedObjectPermissions();
@@ -24,10 +22,11 @@ export const CasesPage = React.memo(() => {
{userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
)}
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx
index cbc7bbc62fbf9..4bb8afa7f8d42 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx
@@ -13,9 +13,7 @@ import { SpyRoute } from '../../utils/route/spy_routes';
import { getCaseUrl } from '../../components/link_to';
import { navTabs } from '../home/home_navigations';
import { CaseView } from './components/case_view';
-import { getSavedObjectReadOnly, CaseCallOut } from './components/callout';
-
-const infoReadSavedObject = getSavedObjectReadOnly();
+import { savedObjectReadOnly, CaseCallOut } from './components/callout';
export const CaseDetailsPage = React.memo(() => {
const userPermissions = useGetUserSavedObjectPermissions();
@@ -29,7 +27,7 @@ export const CaseDetailsPage = React.memo(() => {
return caseId != null ? (
<>
{userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
+
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx
new file mode 100644
index 0000000000000..64c6276fc1be2
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/case_data.tsx
@@ -0,0 +1,226 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CaseProps } from '../case_view';
+import { Case, Comment, SortFieldCase } from '../../../../containers/case/types';
+import { UseGetCasesState } from '../../../../containers/case/use_get_cases';
+import { UserAction, UserActionField } from '../../../../../../../../plugins/case/common/api/cases';
+
+const updateCase = jest.fn();
+const fetchCase = jest.fn();
+
+const basicCaseId = 'basic-case-id';
+const basicCommentId = 'basic-comment-id';
+const basicCreatedAt = '2020-02-20T23:06:33.798Z';
+const elasticUser = {
+ fullName: 'Leslie Knope',
+ username: 'lknope',
+ email: 'leslie.knope@elastic.co',
+};
+
+export const basicComment: Comment = {
+ comment: 'Solve this fast!',
+ id: basicCommentId,
+ createdAt: basicCreatedAt,
+ createdBy: elasticUser,
+ pushedAt: null,
+ pushedBy: null,
+ updatedAt: '2020-02-20T23:06:33.798Z',
+ updatedBy: {
+ username: 'elastic',
+ },
+ version: 'WzQ3LDFc',
+};
+
+export const basicCase: Case = {
+ closedAt: null,
+ closedBy: null,
+ id: basicCaseId,
+ comments: [basicComment],
+ createdAt: '2020-02-13T19:44:23.627Z',
+ createdBy: elasticUser,
+ description: 'Security banana Issue',
+ externalService: null,
+ status: 'open',
+ tags: ['defacement'],
+ title: 'Another horrible breach!!',
+ totalComment: 1,
+ updatedAt: '2020-02-19T15:02:57.995Z',
+ updatedBy: {
+ username: 'elastic',
+ },
+ version: 'WzQ3LDFd',
+};
+
+export const caseProps: CaseProps = {
+ caseId: basicCaseId,
+ userCanCrud: true,
+ caseData: basicCase,
+ fetchCase,
+ updateCase,
+};
+
+export const caseClosedProps: CaseProps = {
+ ...caseProps,
+ caseData: {
+ ...caseProps.caseData,
+ closedAt: '2020-02-20T23:06:33.798Z',
+ closedBy: {
+ username: 'elastic',
+ },
+ status: 'closed',
+ },
+};
+
+export const basicCaseClosed: Case = {
+ ...caseClosedProps.caseData,
+};
+
+const basicAction = {
+ actionAt: basicCreatedAt,
+ actionBy: elasticUser,
+ oldValue: null,
+ newValue: 'what a cool value',
+ caseId: basicCaseId,
+ commentId: null,
+};
+export const caseUserActions = [
+ {
+ ...basicAction,
+ actionBy: elasticUser,
+ actionField: ['comment'],
+ action: 'create',
+ actionId: 'tt',
+ },
+];
+
+export const useGetCasesMockState: UseGetCasesState = {
+ data: {
+ countClosedCases: 0,
+ countOpenCases: 5,
+ cases: [
+ basicCase,
+ {
+ closedAt: null,
+ closedBy: null,
+ id: '362a5c10-4e99-11ea-9290-35d05cb55c15',
+ createdAt: '2020-02-13T19:44:13.328Z',
+ createdBy: { username: 'elastic' },
+ comments: [],
+ description: 'Security banana Issue',
+ externalService: {
+ pushedAt: '2020-02-13T19:45:01.901Z',
+ pushedBy: 'elastic',
+ connectorId: 'string',
+ connectorName: 'string',
+ externalId: 'string',
+ externalTitle: 'string',
+ externalUrl: 'string',
+ },
+ status: 'open',
+ tags: ['phishing'],
+ title: 'Bad email',
+ totalComment: 0,
+ updatedAt: '2020-02-13T15:45:01.901Z',
+ updatedBy: { username: 'elastic' },
+ version: 'WzQ3LDFd',
+ },
+ {
+ closedAt: null,
+ closedBy: null,
+ id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15',
+ createdAt: '2020-02-13T19:44:11.328Z',
+ createdBy: { username: 'elastic' },
+ comments: [],
+ description: 'Security banana Issue',
+ externalService: {
+ pushedAt: '2020-02-13T19:45:01.901Z',
+ pushedBy: 'elastic',
+ connectorId: 'string',
+ connectorName: 'string',
+ externalId: 'string',
+ externalTitle: 'string',
+ externalUrl: 'string',
+ },
+ status: 'open',
+ tags: ['phishing'],
+ title: 'Bad email',
+ totalComment: 0,
+ updatedAt: '2020-02-14T19:45:01.901Z',
+ updatedBy: { username: 'elastic' },
+ version: 'WzQ3LDFd',
+ },
+ {
+ closedAt: '2020-02-13T19:44:13.328Z',
+ closedBy: { username: 'elastic' },
+ id: '31890e90-4e99-11ea-9290-35d05cb55c15',
+ createdAt: '2020-02-13T19:44:05.563Z',
+ createdBy: { username: 'elastic' },
+ comments: [],
+ description: 'Security banana Issue',
+ externalService: null,
+ status: 'closed',
+ tags: ['phishing'],
+ title: 'Uh oh',
+ totalComment: 0,
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzQ3LDFd',
+ },
+ {
+ closedAt: null,
+ closedBy: null,
+ id: '2f5b3210-4e99-11ea-9290-35d05cb55c15',
+ createdAt: '2020-02-13T19:44:01.901Z',
+ createdBy: { username: 'elastic' },
+ comments: [],
+ description: 'Security banana Issue',
+ externalService: null,
+ status: 'open',
+ tags: ['phishing'],
+ title: 'Uh oh',
+ totalComment: 0,
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzQ3LDFd',
+ },
+ ],
+ page: 1,
+ perPage: 5,
+ total: 10,
+ },
+ loading: [],
+ selectedCases: [],
+ isError: false,
+ queryParams: {
+ page: 1,
+ perPage: 5,
+ sortField: SortFieldCase.createdAt,
+ sortOrder: 'desc',
+ },
+ filterOptions: { search: '', reporters: [], tags: [], status: 'open' },
+};
+
+const basicPush = {
+ connector_id: 'connector_id',
+ connector_name: 'connector name',
+ external_id: 'external_id',
+ external_title: 'external title',
+ external_url: 'basicPush.com',
+ pushed_at: basicCreatedAt,
+ pushed_by: elasticUser,
+};
+export const getUserAction = (af: UserActionField, a: UserAction) => ({
+ ...basicAction,
+ actionId: `${af[0]}-${a}`,
+ actionField: af,
+ action: a,
+ commentId: af[0] === 'comment' ? basicCommentId : null,
+ newValue:
+ a === 'push-to-service' && af[0] === 'pushed'
+ ? JSON.stringify(basicPush)
+ : basicAction.newValue,
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts
new file mode 100644
index 0000000000000..9d2ac29bc47d7
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+export const mockFormHook = {
+ isSubmitted: false,
+ isSubmitting: false,
+ isValid: true,
+ submit: jest.fn(),
+ subscribe: jest.fn(),
+ setFieldValue: jest.fn(),
+ setFieldErrors: jest.fn(),
+ getFields: jest.fn(),
+ getFormData: jest.fn(),
+ getFieldDefaultValue: jest.fn(),
+ /* Returns a list of all errors in the form */
+ getErrors: jest.fn(),
+ reset: jest.fn(),
+ __options: {},
+ __formData$: {},
+ __addField: jest.fn(),
+ __removeField: jest.fn(),
+ __validateFields: jest.fn(),
+ __updateFormDataAt: jest.fn(),
+ __readFieldConfigFromSchema: jest.fn(),
+};
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const getFormMock = (sampleData: any) => ({
+ ...mockFormHook,
+ submit: () =>
+ Promise.resolve({
+ data: sampleData,
+ isValid: true,
+ }),
+ getFormData: () => sampleData,
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts
new file mode 100644
index 0000000000000..a20ab00852a36
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Router } from 'react-router-dom';
+// eslint-disable-next-line @kbn/eslint/module_migration
+import routeData from 'react-router';
+type Action = 'PUSH' | 'POP' | 'REPLACE';
+const pop: Action = 'POP';
+const location = {
+ pathname: '/network',
+ search: '',
+ state: '',
+ hash: '',
+};
+export const mockHistory = {
+ length: 2,
+ location,
+ action: pop,
+ push: jest.fn(),
+ replace: jest.fn(),
+ go: jest.fn(),
+ goBack: jest.fn(),
+ goForward: jest.fn(),
+ block: jest.fn(),
+ createHref: jest.fn(),
+ listen: jest.fn(),
+};
+
+export const mockLocation = {
+ pathname: '/welcome',
+ hash: '',
+ search: '',
+ state: '',
+};
+
+export { Router, routeData };
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx
new file mode 100644
index 0000000000000..74f6411f17fa0
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { AddComment } from './';
+import { TestProviders } from '../../../../mock';
+import { getFormMock } from '../__mock__/form';
+import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
+
+import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline';
+import { usePostComment } from '../../../../containers/case/use_post_comment';
+import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
+import { wait } from '../../../../lib/helpers';
+jest.mock(
+ '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
+);
+jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline');
+jest.mock('../../../../containers/case/use_post_comment');
+
+export const useFormMock = useForm as jest.Mock;
+
+const useInsertTimelineMock = useInsertTimeline as jest.Mock;
+const usePostCommentMock = usePostComment as jest.Mock;
+
+const onCommentSaving = jest.fn();
+const onCommentPosted = jest.fn();
+const postComment = jest.fn();
+const handleCursorChange = jest.fn();
+const handleOnTimelineChange = jest.fn();
+
+const addCommentProps = {
+ caseId: '1234',
+ disabled: false,
+ insertQuote: null,
+ onCommentSaving,
+ onCommentPosted,
+ showLoading: false,
+};
+
+const defaultInsertTimeline = {
+ cursorPosition: {
+ start: 0,
+ end: 0,
+ },
+ handleCursorChange,
+ handleOnTimelineChange,
+};
+
+const defaultPostCommment = {
+ isLoading: false,
+ isError: false,
+ postComment,
+};
+const sampleData = {
+ comment: 'what a cool comment',
+};
+describe('AddComment ', () => {
+ const formHookMock = getFormMock(sampleData);
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
+ usePostCommentMock.mockImplementation(() => defaultPostCommment);
+ useFormMock.mockImplementation(() => ({ form: formHookMock }));
+ jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
+ });
+
+ it('should post comment on submit click', async () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy();
+
+ wrapper
+ .find(`[data-test-subj="submit-comment"]`)
+ .first()
+ .simulate('click');
+ await wait();
+ expect(onCommentSaving).toBeCalled();
+ expect(postComment).toBeCalledWith(sampleData, onCommentPosted);
+ expect(formHookMock.reset).toBeCalled();
+ });
+
+ it('should render spinner and disable submit when loading', () => {
+ usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ 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 disabled prop passed', () => {
+ usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(
+ wrapper
+ .find(`[data-test-subj="submit-comment"]`)
+ .first()
+ .prop('isDisabled')
+ ).toBeTruthy();
+ });
+
+ it('should insert a quote if one is available', () => {
+ const sampleQuote = 'what a cool quote';
+ mount(
+
+
+
+
+
+ );
+
+ expect(formHookMock.setFieldValue).toBeCalledWith(
+ 'comment',
+ `${sampleData.comment}\n\n${sampleQuote}`
+ );
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx
index ecc57c50e28eb..eaba708948a99 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx
@@ -71,10 +71,9 @@ export const AddComment = React.memo(
form.reset();
}
}, [form, onCommentPosted, onCommentSaving]);
-
return (
);
}, [theCase]);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx
index a6da45a8c5bb1..f65736e7cd109 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx
@@ -9,11 +9,15 @@ import { mount } from 'enzyme';
import moment from 'moment-timezone';
import { AllCases } from './';
import { TestProviders } from '../../../../mock';
-import { useGetCasesMockState } from './__mock__';
+import { useGetCasesMockState } from '../__mock__/case_data';
+import * as i18n from './translations';
+
+import { getEmptyTagValue } from '../../../../components/empty_value';
import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
import { useGetCases } from '../../../../containers/case/use_get_cases';
import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status';
import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case';
+import { getCasesColumns } from './columns';
jest.mock('../../../../containers/case/use_bulk_update_case');
jest.mock('../../../../containers/case/use_delete_cases');
jest.mock('../../../../containers/case/use_get_cases');
@@ -35,6 +39,7 @@ describe('AllCases', () => {
const setSelectedCases = jest.fn();
const updateBulkStatus = jest.fn();
const fetchCasesStatus = jest.fn();
+ const emptyTag = getEmptyTagValue().props.children;
const defaultGetCases = {
...useGetCasesMockState,
@@ -115,7 +120,7 @@ describe('AllCases', () => {
.find(`[data-test-subj="case-table-column-createdBy"]`)
.first()
.text()
- ).toEqual(useGetCasesMockState.data.cases[0].createdBy.username);
+ ).toEqual(useGetCasesMockState.data.cases[0].createdBy.fullName);
expect(
wrapper
.find(`[data-test-subj="case-table-column-createdAt"]`)
@@ -129,6 +134,39 @@ describe('AllCases', () => {
.text()
).toEqual('Showing 10 cases');
});
+ it('should render empty fields', () => {
+ useGetCasesMock.mockImplementation(() => ({
+ ...defaultGetCases,
+ data: {
+ ...defaultGetCases.data,
+ cases: [
+ {
+ ...defaultGetCases.data.cases[0],
+ id: null,
+ createdAt: null,
+ createdBy: null,
+ tags: null,
+ title: null,
+ totalComment: null,
+ },
+ ],
+ },
+ }));
+ const wrapper = mount(
+
+
+
+ );
+ const checkIt = (columnName: string, key: number) => {
+ const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key);
+ if (columnName === i18n.ACTIONS) {
+ return;
+ }
+ expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName);
+ expect(column.find('span').text()).toEqual(emptyTag);
+ };
+ getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key));
+ });
it('should tableHeaderSortButton AllCases', () => {
const wrapper = mount(
@@ -165,6 +203,30 @@ describe('AllCases', () => {
version: firstCase.version,
});
});
+ it('opens case when row action icon clicked', () => {
+ useGetCasesMock.mockImplementation(() => ({
+ ...defaultGetCases,
+ filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' },
+ }));
+
+ const wrapper = mount(
+
+
+
+ );
+ wrapper
+ .find('[data-test-subj="action-open"]')
+ .first()
+ .simulate('click');
+ const firstCase = useGetCasesMockState.data.cases[0];
+ expect(dispatchUpdateCaseProperty).toBeCalledWith({
+ caseId: firstCase.id,
+ updateKey: 'status',
+ updateValue: 'open',
+ refetchCasesStatus: fetchCasesStatus,
+ version: firstCase.version,
+ });
+ });
it('Bulk delete', () => {
useGetCasesMock.mockImplementation(() => ({
...defaultGetCases,
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx
new file mode 100644
index 0000000000000..615d052347203
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { CasesTableFilters } from './table_filters';
+import { TestProviders } from '../../../../mock';
+
+import { useGetTags } from '../../../../containers/case/use_get_tags';
+import { useGetReporters } from '../../../../containers/case/use_get_reporters';
+import { DEFAULT_FILTER_OPTIONS } from '../../../../containers/case/use_get_cases';
+jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline');
+jest.mock('../../../../containers/case/use_get_reporters');
+jest.mock('../../../../containers/case/use_get_tags');
+
+const onFilterChanged = jest.fn();
+const fetchReporters = jest.fn();
+
+const props = {
+ countClosedCases: 1234,
+ countOpenCases: 99,
+ onFilterChanged,
+ initial: DEFAULT_FILTER_OPTIONS,
+};
+describe('CasesTableFilters ', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'] });
+ (useGetReporters as jest.Mock).mockReturnValue({
+ reporters: ['casetester'],
+ respReporters: [{ username: 'casetester' }],
+ isLoading: true,
+ isError: false,
+ fetchReporters,
+ });
+ });
+ it('should render the initial case count', () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(
+ wrapper
+ .find(`[data-test-subj="open-case-count"]`)
+ .last()
+ .text()
+ ).toEqual('Open cases (99)');
+ expect(
+ wrapper
+ .find(`[data-test-subj="closed-case-count"]`)
+ .last()
+ .text()
+ ).toEqual('Closed cases (1234)');
+ });
+ it('should call onFilterChange when tags change', () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="options-filter-popover-button-Tags"]`)
+ .last()
+ .simulate('click');
+ wrapper
+ .find(`[data-test-subj="options-filter-popover-item-0"]`)
+ .last()
+ .simulate('click');
+
+ expect(onFilterChanged).toBeCalledWith({ tags: ['coke'] });
+ });
+ it('should call onFilterChange when reporters change', () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="options-filter-popover-button-Reporter"]`)
+ .last()
+ .simulate('click');
+
+ wrapper
+ .find(`[data-test-subj="options-filter-popover-item-0"]`)
+ .last()
+ .simulate('click');
+
+ expect(onFilterChanged).toBeCalledWith({ reporters: [{ username: 'casetester' }] });
+ });
+ it('should call onFilterChange when search changes', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ wrapper
+ .find(`[data-test-subj="search-cases"]`)
+ .last()
+ .simulate('keyup', { keyCode: 13, target: { value: 'My search' } });
+ expect(onFilterChanged).toBeCalledWith({ search: 'My search' });
+ });
+ it('should call onFilterChange when status toggled', () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="closed-case-count"]`)
+ .last()
+ .simulate('click');
+
+ expect(onFilterChanged).toBeCalledWith({ status: 'closed' });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx
index a344dd7891010..da477a56c0a22 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx
@@ -42,7 +42,7 @@ const CasesTableFiltersComponent = ({
onFilterChanged,
initial = defaultInitial,
}: CasesTableFiltersProps) => {
- const [selectedReporters, setselectedReporters] = useState(
+ const [selectedReporters, setSelectedReporters] = useState(
initial.reporters.map(r => r.full_name ?? r.username ?? '')
);
const [search, setSearch] = useState(initial.search);
@@ -54,7 +54,7 @@ const CasesTableFiltersComponent = ({
const handleSelectedReporters = useCallback(
newReporters => {
if (!isEqual(newReporters, selectedReporters)) {
- setselectedReporters(newReporters);
+ setSelectedReporters(newReporters);
const reportersObj = respReporters.filter(
r => newReporters.includes(r.username) || newReporters.includes(r.full_name)
);
@@ -97,6 +97,7 @@ const CasesTableFiltersComponent = ({
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts
index 1bee96bc23fff..d3dcfa50ecfa5 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts
@@ -46,6 +46,10 @@ export const BULK_ACTIONS = i18n.translate('xpack.siem.case.caseTable.bulkAction
defaultMessage: 'Bulk actions',
});
+export const SERVICENOW_INCIDENT = i18n.translate('xpack.siem.case.caseTable.snIncident', {
+ defaultMessage: 'ServiceNow Incident',
+});
+
export const SEARCH_PLACEHOLDER = i18n.translate('xpack.siem.case.caseTable.searchPlaceholder', {
defaultMessage: 'e.g. case name',
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx
index 929e8640dceb6..3237104274473 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx
@@ -6,7 +6,7 @@
import * as i18n from './translations';
-export const getSavedObjectReadOnly = () => ({
+export const savedObjectReadOnly = {
title: i18n.READ_ONLY_SAVED_OBJECT_TITLE,
description: i18n.READ_ONLY_SAVED_OBJECT_MSG,
-});
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx
new file mode 100644
index 0000000000000..126ea13e96af6
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { CaseCallOut } from './';
+
+const defaultProps = {
+ title: 'hey title',
+};
+
+describe('CaseCallOut ', () => {
+ it('Renders single message callout', () => {
+ const props = {
+ ...defaultProps,
+ message: 'we have one message',
+ };
+ const wrapper = mount();
+ expect(
+ wrapper
+ .find(`[data-test-subj="callout-message"]`)
+ .last()
+ .exists()
+ ).toBeTruthy();
+ expect(
+ wrapper
+ .find(`[data-test-subj="callout-messages"]`)
+ .last()
+ .exists()
+ ).toBeFalsy();
+ });
+ it('Renders multi message callout', () => {
+ const props = {
+ ...defaultProps,
+ messages: [
+ { ...defaultProps, description: {'we have two messages'}
},
+ { ...defaultProps, description: {'for real'}
},
+ ],
+ };
+ const wrapper = mount();
+ expect(
+ wrapper
+ .find(`[data-test-subj="callout-message"]`)
+ .last()
+ .exists()
+ ).toBeFalsy();
+ expect(
+ wrapper
+ .find(`[data-test-subj="callout-messages"]`)
+ .last()
+ .exists()
+ ).toBeTruthy();
+ });
+ it('Dismisses callout', () => {
+ const props = {
+ ...defaultProps,
+ message: 'we have one message',
+ };
+ const wrapper = mount();
+ expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeTruthy();
+ wrapper
+ .find(`[data-test-subj="callout-dismiss"]`)
+ .last()
+ .simulate('click');
+ expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeFalsy();
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx
index 30a95db2d82a5..0fc93af7f318d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx
@@ -24,10 +24,12 @@ const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) =>
return showCallOut ? (
<>
-
- {!isEmpty(messages) && }
- {!isEmpty(message) && {message}
}
-
+
+ {!isEmpty(messages) && (
+
+ )}
+ {!isEmpty(message) && {message}
}
+
{i18n.DISMISS_CALLOUT}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx
index 2b16dfa150d61..718eb95767f2e 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx
@@ -84,7 +84,7 @@ const CaseStatusComp: React.FC = ({
-
+
{i18n.CASE_REFRESH}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
deleted file mode 100644
index 0e57326707e97..0000000000000
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { CaseProps } from '../index';
-import { Case } from '../../../../../containers/case/types';
-
-const updateCase = jest.fn();
-const fetchCase = jest.fn();
-
-export const caseProps: CaseProps = {
- caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
- userCanCrud: true,
- caseData: {
- closedAt: null,
- closedBy: null,
- id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
- comments: [
- {
- comment: 'Solve this fast!',
- id: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8',
- createdAt: '2020-02-20T23:06:33.798Z',
- createdBy: {
- fullName: 'Steph Milovic',
- username: 'smilovic',
- email: 'notmyrealemailfool@elastic.co',
- },
- pushedAt: null,
- pushedBy: null,
- updatedAt: '2020-02-20T23:06:33.798Z',
- updatedBy: {
- username: 'elastic',
- },
- version: 'WzQ3LDFd',
- },
- ],
- createdAt: '2020-02-13T19:44:23.627Z',
- createdBy: { fullName: null, email: 'testemail@elastic.co', username: 'elastic' },
- description: 'Security banana Issue',
- externalService: null,
- status: 'open',
- tags: ['defacement'],
- title: 'Another horrible breach!!',
- totalComment: 1,
- updatedAt: '2020-02-19T15:02:57.995Z',
- updatedBy: {
- username: 'elastic',
- },
- version: 'WzQ3LDFd',
- },
- fetchCase,
- updateCase,
-};
-
-export const caseClosedProps: CaseProps = {
- ...caseProps,
- caseData: {
- ...caseProps.caseData,
- closedAt: '2020-02-20T23:06:33.798Z',
- closedBy: {
- username: 'elastic',
- },
- status: 'closed',
- },
-};
-
-export const data: Case = {
- ...caseProps.caseData,
-};
-
-export const dataClosed: Case = {
- ...caseClosedProps.caseData,
-};
-
-export const caseUserActions = [
- {
- actionField: ['comment'],
- action: 'create',
- actionAt: '2020-03-20T17:10:09.814Z',
- actionBy: {
- fullName: 'Steph Milovic',
- username: 'smilovic',
- email: 'notmyrealemailfool@elastic.co',
- },
- newValue: 'Solve this fast!',
- oldValue: null,
- actionId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15',
- caseId: '9b833a50-6acd-11ea-8fad-af86b1071bd9',
- commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8',
- },
-];
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
index 49f5f44cba271..8a25a2121104d 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
@@ -9,7 +9,7 @@ import { mount } from 'enzyme';
import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
import { TestProviders } from '../../../../mock';
-import { data } from './__mock__';
+import { basicCase } from '../__mock__/case_data';
import { CaseViewActions } from './actions';
jest.mock('../../../../containers/case/use_delete_cases');
const useDeleteCasesMock = useDeleteCases as jest.Mock;
@@ -34,7 +34,7 @@ describe('CaseView actions', () => {
it('clicking trash toggles modal', () => {
const wrapper = mount(
-
+
);
@@ -54,12 +54,14 @@ describe('CaseView actions', () => {
}));
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy();
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
- expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([{ id: data.id, title: data.title }]);
+ expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([
+ { id: basicCase.id, title: basicCase.title },
+ ]);
});
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
index 0b08b866df964..216180eb2cf0a 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
@@ -40,7 +40,6 @@ const CaseViewActionsComponent: React.FC = ({ caseData, disable
),
[isDisplayConfirmDeleteModal, caseData]
);
- // TO DO refactor each of these const's into their own components
const propertyActions = useMemo(
() => [
{
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
index 3f5b3a3127177..3721a5a727ca5 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx
@@ -5,56 +5,43 @@
*/
import React from 'react';
-import { Router } from 'react-router-dom';
import { mount } from 'enzyme';
-/* eslint-disable @kbn/eslint/module_migration */
-import routeData from 'react-router';
-/* eslint-enable @kbn/eslint/module_migration */
-import { CaseComponent } from './';
-import { caseProps, caseClosedProps, data, dataClosed, caseUserActions } from './__mock__';
+
+import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
+import { CaseComponent, CaseView } from './';
+import {
+ basicCaseClosed,
+ caseClosedProps,
+ caseProps,
+ caseUserActions,
+} from '../__mock__/case_data';
import { TestProviders } from '../../../../mock';
import { useUpdateCase } from '../../../../containers/case/use_update_case';
+import { useGetCase } from '../../../../containers/case/use_get_case';
import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions';
import { wait } from '../../../../lib/helpers';
import { usePushToService } from '../use_push_to_service';
jest.mock('../../../../containers/case/use_update_case');
jest.mock('../../../../containers/case/use_get_case_user_actions');
+jest.mock('../../../../containers/case/use_get_case');
jest.mock('../use_push_to_service');
const useUpdateCaseMock = useUpdateCase as jest.Mock;
const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock;
const usePushToServiceMock = usePushToService as jest.Mock;
-type Action = 'PUSH' | 'POP' | 'REPLACE';
-const pop: Action = 'POP';
-const location = {
- pathname: '/network',
- search: '',
- state: '',
- hash: '',
-};
-const mockHistory = {
- length: 2,
- location,
- action: pop,
- push: jest.fn(),
- replace: jest.fn(),
- go: jest.fn(),
- goBack: jest.fn(),
- goForward: jest.fn(),
- block: jest.fn(),
- createHref: jest.fn(),
- listen: jest.fn(),
-};
-
-const mockLocation = {
- pathname: '/welcome',
- hash: '',
- search: '',
- state: '',
-};
describe('CaseView ', () => {
const updateCaseProperty = jest.fn();
const fetchCaseUserActions = jest.fn();
+ const fetchCase = jest.fn();
+ const updateCase = jest.fn();
+ const data = caseProps.caseData;
+ const defaultGetCase = {
+ isLoading: false,
+ isError: false,
+ data,
+ updateCase,
+ fetchCase,
+ };
/* eslint-disable no-console */
// Silence until enzyme fixed to use ReactTestUtils.act()
const originalError = console.error;
@@ -84,17 +71,23 @@ describe('CaseView ', () => {
participants: [data.createdBy],
};
- const defaultUsePushToServiceMock = {
- pushButton: <>{'Hello Button'}>,
- pushCallouts: null,
- };
-
beforeEach(() => {
jest.resetAllMocks();
useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions);
- usePushToServiceMock.mockImplementation(() => defaultUsePushToServiceMock);
+ usePushToServiceMock.mockImplementation(({ updateCase: updateCaseMockCall }) => ({
+ pushButton: (
+
+ ),
+ pushCallouts: null,
+ }));
});
it('should render CaseComponent', async () => {
@@ -120,7 +113,7 @@ describe('CaseView ', () => {
).toEqual(data.status);
expect(
wrapper
- .find(`[data-test-subj="case-view-tag-list"] .euiBadge__text`)
+ .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`)
.first()
.text()
).toEqual(data.tags[0]);
@@ -139,7 +132,7 @@ describe('CaseView ', () => {
).toEqual(data.createdAt);
expect(
wrapper
- .find(`[data-test-subj="case-view-description"]`)
+ .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`)
.first()
.prop('raw')
).toEqual(data.description);
@@ -148,7 +141,7 @@ describe('CaseView ', () => {
it('should show closed indicators in header when case is closed', async () => {
useUpdateCaseMock.mockImplementation(() => ({
...defaultUpdateCaseState,
- caseData: dataClosed,
+ caseData: basicCaseClosed,
}));
const wrapper = mount(
@@ -164,13 +157,13 @@ describe('CaseView ', () => {
.find(`[data-test-subj="case-view-closedAt"]`)
.first()
.prop('value')
- ).toEqual(dataClosed.closedAt);
+ ).toEqual(basicCaseClosed.closedAt);
expect(
wrapper
.find(`[data-test-subj="case-view-status"]`)
.first()
.text()
- ).toEqual(dataClosed.status);
+ ).toEqual(basicCaseClosed.status);
});
it('should dispatch update state when button is toggled', async () => {
@@ -188,7 +181,12 @@ describe('CaseView ', () => {
expect(updateCaseProperty).toHaveBeenCalled();
});
- it('should render comments', async () => {
+ it('should display EditableTitle isLoading', () => {
+ useUpdateCaseMock.mockImplementation(() => ({
+ ...defaultUpdateCaseState,
+ isLoading: true,
+ updateKey: 'title',
+ }));
const wrapper = mount(
@@ -196,32 +194,230 @@ describe('CaseView ', () => {
);
- await wait();
expect(
wrapper
- .find(
- `div[data-test-subj="user-action-${data.comments[0].id}-avatar"] [data-test-subj="user-action-avatar"]`
- )
+ .find('[data-test-subj="editable-title-loading"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="editable-title-edit-icon"]')
.first()
- .prop('name')
- ).toEqual(data.comments[0].createdBy.fullName);
+ .exists()
+ ).toBeFalsy();
+ });
+ it('should display Toggle Status isLoading', () => {
+ useUpdateCaseMock.mockImplementation(() => ({
+ ...defaultUpdateCaseState,
+ isLoading: true,
+ updateKey: 'status',
+ }));
+ const wrapper = mount(
+
+
+
+
+
+ );
expect(
wrapper
- .find(
- `div[data-test-subj="user-action-${data.comments[0].id}"] [data-test-subj="user-action-title"] strong`
- )
+ .find('[data-test-subj="toggle-case-status"]')
.first()
- .text()
- ).toEqual(data.comments[0].createdBy.username);
+ .prop('isLoading')
+ ).toBeTruthy();
+ });
+ it('should display description isLoading', () => {
+ useUpdateCaseMock.mockImplementation(() => ({
+ ...defaultUpdateCaseState,
+ isLoading: true,
+ updateKey: 'description',
+ }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(
+ wrapper
+ .find('[data-test-subj="description-action"] [data-test-subj="user-action-title-loading"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ expect(
+ wrapper
+ .find('[data-test-subj="description-action"] [data-test-subj="property-actions"]')
+ .first()
+ .exists()
+ ).toBeFalsy();
+ });
+
+ it('should display tags isLoading', () => {
+ useUpdateCaseMock.mockImplementation(() => ({
+ ...defaultUpdateCaseState,
+ isLoading: true,
+ updateKey: 'tags',
+ }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(
+ wrapper
+ .find('[data-test-subj="case-view-tag-list"] [data-test-subj="tag-list-loading"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
expect(
wrapper
- .find(
- `div[data-test-subj="user-action-${data.comments[0].id}"] [data-test-subj="markdown"]`
- )
+ .find('[data-test-subj="tag-list-edit"]')
.first()
- .prop('source')
- ).toEqual(data.comments[0].comment);
+ .exists()
+ ).toBeFalsy();
+ });
+
+ it('should update title', () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+ const newTitle = 'The new title';
+ wrapper
+ .find(`[data-test-subj="editable-title-edit-icon"]`)
+ .first()
+ .simulate('click');
+ wrapper.update();
+ wrapper
+ .find(`[data-test-subj="editable-title-input-field"]`)
+ .last()
+ .simulate('change', { target: { value: newTitle } });
+
+ wrapper.update();
+ wrapper
+ .find(`[data-test-subj="editable-title-submit-btn"]`)
+ .first()
+ .simulate('click');
+
+ wrapper.update();
+ const updateObject = updateCaseProperty.mock.calls[0][0];
+ expect(updateObject.updateKey).toEqual('title');
+ expect(updateObject.updateValue).toEqual(newTitle);
+ });
+
+ it('should push updates on button click', async () => {
+ useGetCaseUserActionsMock.mockImplementation(() => ({
+ ...defaultUseGetCaseUserActions,
+ hasDataToPush: true,
+ }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(
+ wrapper
+ .find('[data-test-subj="has-data-to-push-button"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ wrapper
+ .find('[data-test-subj="mock-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+ await wait();
+ expect(updateCase).toBeCalledWith(caseProps.caseData);
+ expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id);
+ });
+
+ it('should return null if error', () => {
+ (useGetCase as jest.Mock).mockImplementation(() => ({
+ ...defaultGetCase,
+ isError: true,
+ }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper).toEqual({});
+ });
+
+ it('should return spinner if loading', () => {
+ (useGetCase as jest.Mock).mockImplementation(() => ({
+ ...defaultGetCase,
+ isLoading: true,
+ }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="case-view-loading"]').exists()).toBeTruthy();
+ });
+
+ it('should return case view when data is there', () => {
+ (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy();
+ });
+
+ it('should refresh data on refresh', () => {
+ (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
+ const wrapper = mount(
+
+
+
+
+
+ );
+ wrapper
+ .find('[data-test-subj="case-refresh"]')
+ .first()
+ .simulate('click');
+ expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id);
+ expect(fetchCase).toBeCalled();
});
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
index 947da51365d66..3cf0405f40637 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx
@@ -271,7 +271,11 @@ export const CaseComponent = React.memo(
onChange={toggleStatusCase}
/>
- {hasDataToPush && {pushButton}}
+ {hasDataToPush && (
+
+ {pushButton}
+
+ )}
>
)}
@@ -316,7 +320,7 @@ export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => {
return (
-
+
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx
new file mode 100644
index 0000000000000..d480744fc932a
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { Create } from './';
+import { TestProviders } from '../../../../mock';
+import { getFormMock } from '../__mock__/form';
+import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
+
+import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline';
+import { usePostCase } from '../../../../containers/case/use_post_case';
+jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline');
+jest.mock('../../../../containers/case/use_post_case');
+import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks';
+import { wait } from '../../../../lib/helpers';
+import { SiemPageName } from '../../../home/types';
+jest.mock(
+ '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
+);
+
+export const useFormMock = useForm as jest.Mock;
+
+const useInsertTimelineMock = useInsertTimeline as jest.Mock;
+const usePostCaseMock = usePostCase as jest.Mock;
+
+const postCase = jest.fn();
+const handleCursorChange = jest.fn();
+const handleOnTimelineChange = jest.fn();
+
+const defaultInsertTimeline = {
+ cursorPosition: {
+ start: 0,
+ end: 0,
+ },
+ handleCursorChange,
+ handleOnTimelineChange,
+};
+const sampleData = {
+ description: 'what a great description',
+ tags: ['coke', 'pepsi'],
+ title: 'what a cool title',
+};
+const defaultPostCase = {
+ isLoading: false,
+ isError: false,
+ caseData: null,
+ postCase,
+};
+describe('Create case', () => {
+ const formHookMock = getFormMock(sampleData);
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
+ usePostCaseMock.mockImplementation(() => defaultPostCase);
+ useFormMock.mockImplementation(() => ({ form: formHookMock }));
+ jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
+ });
+
+ it('should post case on submit click', async () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="create-case-submit"]`)
+ .first()
+ .simulate('click');
+ await wait();
+ expect(postCase).toBeCalledWith(sampleData);
+ });
+
+ it('should redirect to all cases on cancel click', () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="create-case-cancel"]`)
+ .first()
+ .simulate('click');
+ expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`);
+ });
+ it('should redirect to new case when caseData is there', () => {
+ const sampleId = '777777';
+ usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } }));
+ mount(
+
+
+
+
+
+ );
+ expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(
+ `/${SiemPageName.case}/${sampleId}`
+ );
+ });
+
+ it('should render spinner when loading', () => {
+ usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true }));
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy();
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx
index 740909db408ec..53b792bb9b5eb 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx
@@ -73,7 +73,7 @@ export const Create = React.memo(() => {
const handleSetIsCancel = useCallback(() => {
setIsCancel(true);
- }, [isCancel]);
+ }, []);
if (caseData != null && caseData.id) {
return ;
@@ -85,7 +85,7 @@ export const Create = React.memo(() => {
return (
- {isLoading && }
+ {isLoading && }
{
+ const sampleTags = ['coke', 'pepsi'];
+ const formHookMock = getFormMock({ tags: sampleTags });
+ beforeEach(() => {
+ jest.resetAllMocks();
+ (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock }));
+ });
+ it('Renders no tags, and then edit', () => {
+ const wrapper = mount(
+
+
+
+ );
+ 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('Edit tag on submit', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="tag-list-edit-button"]`)
+ .last()
+ .simulate('click');
+ await act(async () => {
+ wrapper
+ .find(`[data-test-subj="edit-tags-submit"]`)
+ .last()
+ .simulate('click');
+ await wait();
+ expect(onSubmit).toBeCalledWith(sampleTags);
+ });
+ });
+ it('Cancels on cancel', async () => {
+ const props = {
+ ...defaultProps,
+ tags: ['pepsi'],
+ };
+ const wrapper = mount(
+
+
+
+ );
+ expect(
+ wrapper
+ .find(`[data-test-subj="case-tag"]`)
+ .last()
+ .exists()
+ ).toBeTruthy();
+ wrapper
+ .find(`[data-test-subj="tag-list-edit-button"]`)
+ .last()
+ .simulate('click');
+ await act(async () => {
+ expect(
+ wrapper
+ .find(`[data-test-subj="case-tag"]`)
+ .last()
+ .exists()
+ ).toBeFalsy();
+ wrapper
+ .find(`[data-test-subj="edit-tags-cancel"]`)
+ .last()
+ .simulate('click');
+ await wait();
+ wrapper.update();
+ expect(
+ wrapper
+ .find(`[data-test-subj="case-tag"]`)
+ .last()
+ .exists()
+ ).toBeTruthy();
+ });
+ });
+ it('Renders disabled button', () => {
+ const props = { ...defaultProps, disabled: true };
+ const wrapper = mount(
+
+
+
+ );
+ expect(
+ wrapper
+ .find(`[data-test-subj="tag-list-edit-button"]`)
+ .last()
+ .prop('disabled')
+ ).toBeTruthy();
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx
index f7d890ca60b16..9bac000b93235 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx
@@ -61,10 +61,11 @@ export const TagList = React.memo(
{i18n.TAGS}
- {isLoading && }
+ {isLoading && }
{!isLoading && (
-
+
-
- {tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
+
+ {tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
{tags.length > 0 &&
!isEditTags &&
tags.map((tag, key) => (
- {tag}
+
+ {tag}
+
))}
{isEditTags && (
-
+