diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 2429e68cbefa5..4386f996608f6 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { basicCase } from '../../containers/mock'; +import { basicCase, basicCaseClosed } from '../../containers/mock'; import type { CaseActionBarProps } from '.'; import { CaseActionBar } from '.'; import { @@ -74,6 +74,18 @@ describe('CaseActionBar', () => { ); }); + it('should show the status as closed when the case is closed', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toBe( + 'Closed' + ); + }); + it('should show the correct date', () => { const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index a2fb4a8ae1a08..59ca9a98de12e 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -6,248 +6,104 @@ */ import React from 'react'; -import { waitFor, within, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { waitFor, screen } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import '../../common/mock/match_media'; -import { useCaseViewNavigation, useUrlParams } from '../../common/navigation/hooks'; -import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; -import { basicCaseClosed, connectorsMock, getCaseUsersMockResponse } from '../../containers/mock'; -import type { UseGetCase } from '../../containers/use_get_case'; -import { useGetCase } from '../../containers/use_get_case'; -import { useGetCaseMetrics } from '../../containers/use_get_case_metrics'; -import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; -import { useGetTags } from '../../containers/use_get_tags'; -import { usePostPushToService } from '../../containers/use_post_push_to_service'; -import { useGetCaseConnectors } from '../../containers/use_get_case_connectors'; -import { useUpdateCase } from '../../containers/use_update_case'; -import { useGetCaseUsers } from '../../containers/use_get_case_users'; +import { useUrlParams } from '../../common/navigation/hooks'; import { CaseViewPage } from './case_view_page'; -import { - caseData, - caseViewProps, - defaultGetCase, - defaultGetCaseMetrics, - defaultInfiniteUseFindCaseUserActions, - defaultUpdateCaseState, - defaultUseFindCaseUserActions, -} from './mocks'; +import { caseData, caseViewProps } from './mocks'; import type { CaseViewPageProps } from './types'; -import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; -import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; -import { useInfiniteFindCaseUserActions } from '../../containers/use_infinite_find_case_user_actions'; -import { useGetCaseUserActionsStats } from '../../containers/use_get_case_user_actions_stats'; -import { createQueryWithMarkup } from '../../common/test_utils'; -import { useCasesFeatures } from '../../common/use_cases_features'; -import { CaseMetricsFeature } from '../../../common/types/api'; +import { waitForComponentToUpdate } from '../../common/test_utils'; +import { useCasesTitleBreadcrumbs } from '../use_breadcrumbs'; -jest.mock('../../containers/use_get_action_license'); -jest.mock('../../containers/use_update_case'); -jest.mock('../../containers/use_get_case_metrics'); -jest.mock('../../containers/use_find_case_user_actions'); -jest.mock('../../containers/use_infinite_find_case_user_actions'); -jest.mock('../../containers/use_get_case_user_actions_stats'); -jest.mock('../../containers/use_get_tags'); -jest.mock('../../containers/use_get_case'); -jest.mock('../../containers/configure/use_get_supported_action_connectors'); -jest.mock('../../containers/use_post_push_to_service'); -jest.mock('../../containers/use_get_case_connectors'); -jest.mock('../../containers/use_get_case_users'); -jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); -jest.mock('../../common/use_cases_features'); -jest.mock('../user_actions/timestamp', () => ({ - UserActionTimestamp: () => <>, -})); jest.mock('../../common/navigation/hooks'); +jest.mock('../use_breadcrumbs'); +jest.mock('./use_on_refresh_case_view_page'); jest.mock('../../common/hooks'); -jest.mock('../connectors/resilient/api'); jest.mock('../../common/lib/kibana'); -const useFetchCaseMock = useGetCase as jest.Mock; -const useUrlParamsMock = useUrlParams as jest.Mock; -const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock; -const useUpdateCaseMock = useUpdateCase as jest.Mock; -const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock; -const useInfiniteFindCaseUserActionsMock = useInfiniteFindCaseUserActions as jest.Mock; -const useGetCaseUserActionsStatsMock = useGetCaseUserActionsStats as jest.Mock; -const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; -const usePostPushToServiceMock = usePostPushToService as jest.Mock; -const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock; -const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock; -const useGetTagsMock = useGetTags as jest.Mock; -const useGetCaseUsersMock = useGetCaseUsers as jest.Mock; -const useCasesFeaturesMock = useCasesFeatures as jest.Mock; +jest.mock('../header_page', () => ({ + HeaderPage: jest + .fn() + .mockReturnValue(
{'Case view header'}
), +})); -const mockGetCase = (props: Partial = {}) => { - const data = { - ...defaultGetCase.data, - ...props.data, - }; +jest.mock('./metrics', () => ({ + CaseViewMetrics: jest + .fn() + .mockReturnValue(
{'Case view metrics'}
), +})); - useFetchCaseMock.mockReturnValue({ - ...defaultGetCase, - ...props, - data, - }); -}; +jest.mock('./components/case_view_activity', () => ({ + CaseViewActivity: jest + .fn() + .mockReturnValue(
{'Case view activity'}
), +})); -export const caseProps: CaseViewPageProps = { +jest.mock('./components/case_view_alerts', () => ({ + CaseViewAlerts: jest + .fn() + .mockReturnValue(
{'Case view alerts'}
), +})); + +jest.mock('./components/case_view_files', () => ({ + CaseViewFiles: jest + .fn() + .mockReturnValue(
{'Case view files'}
), +})); + +const useUrlParamsMock = useUrlParams as jest.Mock; +const useCasesTitleBreadcrumbsMock = useCasesTitleBreadcrumbs as jest.Mock; + +const caseProps: CaseViewPageProps = { ...caseViewProps, - caseId: caseData.id, caseData, fetchCase: jest.fn(), }; -export const caseClosedProps: CaseViewPageProps = { - ...caseProps, - caseData: basicCaseClosed, -}; - -const userActionsStats = { - total: 21, - totalComments: 9, - totalOtherActions: 11, -}; - describe('CaseViewPage', () => { - const updateCaseProperty = defaultUpdateCaseState.mutate; - const pushCaseToExternalService = jest.fn(); - const caseConnectors = getCaseConnectorsMockResponse(); - const caseUsers = getCaseUsersMockResponse(); - let appMockRenderer: AppMockRenderer; - // eslint-disable-next-line prefer-object-spread - const originalGetComputedStyle = Object.assign({}, window.getComputedStyle); - - const platinumLicense = licensingMock.createLicense({ - license: { type: 'platinum' }, - }); - - beforeAll(() => { - // The JSDOM implementation is too slow - // Especially for dropdowns that try to position themselves - // perf issue - https://github.com/jsdom/jsdom/issues/3234 - Object.defineProperty(window, 'getComputedStyle', { - value: (el: HTMLElement) => { - /** - * This is based on the jsdom implementation of getComputedStyle - * https://github.com/jsdom/jsdom/blob/9dae17bf0ad09042cfccd82e6a9d06d3a615d9f4/lib/jsdom/browser/Window.js#L779-L820 - * - * It is missing global style parsing and will only return styles applied directly to an element. - * Will not return styles that are global or from emotion - */ - const declaration = new CSSStyleDeclaration(); - const { style } = el; - - Array.prototype.forEach.call(style, (property: string) => { - declaration.setProperty( - property, - style.getPropertyValue(property), - style.getPropertyPriority(property) - ); - }); - - return declaration; - }, - configurable: true, - writable: true, - }); - }); - beforeEach(() => { jest.clearAllMocks(); - mockGetCase(); - useUpdateCaseMock.mockReturnValue(defaultUpdateCaseState); - useGetCaseMetricsMock.mockReturnValue(defaultGetCaseMetrics); - useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions); - useInfiniteFindCaseUserActionsMock.mockReturnValue(defaultInfiniteUseFindCaseUserActions); - useGetCaseUserActionsStatsMock.mockReturnValue({ data: userActionsStats, isLoading: false }); - usePostPushToServiceMock.mockReturnValue({ - isLoading: false, - mutateAsync: pushCaseToExternalService, - }); - useGetCaseConnectorsMock.mockReturnValue({ - isLoading: false, - data: caseConnectors, - }); - useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); - useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); - useGetCaseUsersMock.mockReturnValue({ isLoading: false, data: caseUsers }); - useCasesFeaturesMock.mockReturnValue({ - metricsFeatures: [CaseMetricsFeature.ALERTS_COUNT], - pushToServiceAuthorized: true, - caseAssignmentAuthorized: true, - isAlertsEnabled: true, - isSyncAlertsEnabled: true, - }); - - appMockRenderer = createAppMockRenderer({ license: platinumLicense }); - }); - - afterAll(() => { - Object.defineProperty(window, 'getComputedStyle', originalGetComputedStyle); + useUrlParamsMock.mockReturnValue({}); + appMockRenderer = createAppMockRenderer(); }); - it('shows the metrics section', async () => { + it('shows the header section', async () => { appMockRenderer.render(); - expect(await screen.findByTestId('case-view-metrics-panel')).toBeInTheDocument(); + expect(await screen.findByTestId('test-case-view-header')).toBeInTheDocument(); }); - it('should show closed indicators in header when case is closed', async () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - caseData: basicCaseClosed, - })); - - appMockRenderer.render(); + it('shows the metrics section', async () => { + appMockRenderer.render(); - expect(await screen.findByTestId('case-view-status-dropdown')).toHaveTextContent('Closed'); + expect(await screen.findByTestId('test-case-view-metrics')).toBeInTheDocument(); }); - it('should push updates on button click', async () => { - useGetCaseConnectorsMock.mockImplementation(() => ({ - isLoading: false, - data: { - ...caseConnectors, - 'resilient-2': { - ...caseConnectors['resilient-2'], - push: { ...caseConnectors['resilient-2'].push, needsToBePushed: true }, - }, - }, - })); - + it('shows the activity section', async () => { appMockRenderer.render(); - expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); - expect(await screen.findByTestId('push-to-external-service')).toBeInTheDocument(); - - userEvent.click(screen.getByTestId('push-to-external-service')); - - await waitFor(() => { - expect(pushCaseToExternalService).toHaveBeenCalled(); - }); + expect(await screen.findByTestId('test-case-view-activity')).toBeInTheDocument(); }); - it('should disable the push button when connector is invalid', async () => { + it('should set the breadcrumbs correctly', async () => { + const onComponentInitialized = jest.fn(); + appMockRenderer.render( - + ); - expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); - expect(await screen.findByTestId('push-to-external-service')).toBeDisabled(); + await waitFor(() => { + expect(useCasesTitleBreadcrumbsMock).toHaveBeenCalledWith(caseProps.caseData.title); + }); }); it('should call onComponentInitialized on mount', async () => { const onComponentInitialized = jest.fn(); + appMockRenderer.render( ); @@ -257,229 +113,21 @@ describe('CaseViewPage', () => { }); }); - it('should show loading content when loading user actions stats', async () => { - const useFetchAlertData = jest.fn().mockReturnValue([true]); - useGetCaseUserActionsStatsMock.mockReturnValue({ isLoading: true }); - - appMockRenderer.render(); - - expect(await screen.findByTestId('case-view-loading-content')).toBeInTheDocument(); - expect(screen.queryByTestId('user-actions-list')).not.toBeInTheDocument(); - }); - - it('should call show alert details with expected arguments', async () => { - const showAlertDetails = jest.fn(); - appMockRenderer.render(); - - userEvent.click((await screen.findAllByTestId('comment-action-show-alert-alert-action-id'))[1]); - - await waitFor(() => { - expect(showAlertDetails).toHaveBeenCalledWith('alert-id-1', 'alert-index-1'); - }); - }); - - it('should show the rule name', async () => { - appMockRenderer.render(); - - expect( - ( - await screen.findAllByTestId('user-action-alert-comment-create-action-alert-action-id') - )[1].querySelector('.euiCommentEvent__headerEvent') - ).toHaveTextContent('added an alert from Awesome rule'); - }); - - it('should update settings', async () => { - appMockRenderer.render(); - - userEvent.click(await screen.findByTestId('sync-alerts-switch')); - - await waitFor(() => { - const updateObject = updateCaseProperty.mock.calls[0][0]; - - expect(updateObject.updateKey).toEqual('settings'); - expect(updateObject.updateValue).toEqual({ syncAlerts: false }); - }); - }); - - it('should show the correct connector name on the push button', async () => { - useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); + it('should call onComponentInitialized only once', async () => { + const onComponentInitialized = jest.fn(); - appMockRenderer.render( - + const { rerender } = appMockRenderer.render( + ); - expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); - expect(await screen.findByText('Update My Resilient connector incident')).toBeInTheDocument(); - }); - - describe('Callouts', () => { - const errorText = - 'The connector used to send updates to the external service has been deleted or you do not have the appropriate licenseExternal link(opens in a new tab or window) to use it. To update cases in external systems, select a different connector or create a new one.'; - - it('it shows the danger callout when a connector has been deleted', async () => { - useGetConnectorsMock.mockImplementation(() => ({ data: [], isLoading: false })); - appMockRenderer.render(); - - expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); - - const getByText = createQueryWithMarkup(screen.getByText); - expect(getByText(errorText)).toBeInTheDocument(); - }); - - it('it does NOT shows the danger callout when connectors are loading', async () => { - useGetConnectorsMock.mockImplementation(() => ({ data: [], isLoading: true })); - appMockRenderer.render(); - - expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); - expect( - screen.queryByTestId('case-callout-a25a5b368b6409b179ef4b6c5168244f') - ).not.toBeInTheDocument(); - }); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/149777 - describe.skip('Tabs', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders tabs correctly', async () => { - appMockRenderer.render(); - - expect(await screen.findByRole('tablist')).toBeInTheDocument(); - - expect(await screen.findByTestId('case-view-tab-title-activity')).toBeInTheDocument(); - expect(await screen.findByTestId('case-view-tab-title-alerts')).toBeInTheDocument(); - expect(await screen.findByTestId('case-view-tab-title-files')).toBeInTheDocument(); - }); - - it('renders the activity tab by default', async () => { - appMockRenderer.render(); - expect(await screen.findByTestId('case-view-tab-content-activity')).toBeInTheDocument(); - }); - - it('renders the alerts tab when the query parameter tabId has alerts', async () => { - useUrlParamsMock.mockReturnValue({ - urlParams: { - tabId: CASE_VIEW_PAGE_TABS.ALERTS, - }, - }); - - appMockRenderer.render(); - - expect(await screen.findByTestId('case-view-tab-content-alerts')).toBeInTheDocument(); - expect(await screen.findByTestId('alerts-table')).toBeInTheDocument(); - }); - - it('renders the activity tab when the query parameter tabId has activity', async () => { - useUrlParamsMock.mockReturnValue({ - urlParams: { - tabId: CASE_VIEW_PAGE_TABS.ACTIVITY, - }, - }); - - appMockRenderer.render(); - - expect(await screen.findByTestId('case-view-tab-content-activity')).toBeInTheDocument(); - }); - - it('renders the activity tab when the query parameter tabId has an unknown value', async () => { - useUrlParamsMock.mockReturnValue({ - urlParams: { - tabId: 'what-is-love', - }, - }); - - appMockRenderer.render(); - - expect(await screen.findByTestId('case-view-tab-content-activity')).toBeInTheDocument(); - expect(screen.queryByTestId('case-view-tab-content-alerts')).not.toBeInTheDocument(); - }); - - it('navigates to the activity tab when the activity tab is clicked', async () => { - const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; - appMockRenderer.render(); - - userEvent.click(await screen.findByTestId('case-view-tab-title-activity')); - - await waitFor(() => { - expect(navigateToCaseViewMock).toHaveBeenCalledWith({ - detailName: caseData.id, - tabId: CASE_VIEW_PAGE_TABS.ACTIVITY, - }); - }); - }); - - it('navigates to the alerts tab when the alerts tab is clicked', async () => { - const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; - appMockRenderer.render(); - - userEvent.click(await screen.findByTestId('case-view-tab-title-alerts')); - - await waitFor(async () => { - expect(navigateToCaseViewMock).toHaveBeenCalledWith({ - detailName: caseData.id, - tabId: CASE_VIEW_PAGE_TABS.ALERTS, - }); - }); - }); - - it('should display the alerts tab when the feature is enabled', async () => { - appMockRenderer = createAppMockRenderer({ features: { alerts: { enabled: true } } }); - appMockRenderer.render(); - - expect(await screen.findByTestId('case-view-tab-title-activity')).toBeInTheDocument(); - expect(await screen.findByTestId('case-view-tab-title-alerts')).toBeInTheDocument(); - }); - - it('should not display the alerts tab when the feature is disabled', async () => { - appMockRenderer = createAppMockRenderer({ features: { alerts: { enabled: false } } }); - appMockRenderer.render(); - - expect(await screen.findByTestId('case-view-tab-title-activity')).toBeInTheDocument(); - expect(screen.queryByTestId('case-view-tab-title-alerts')).not.toBeInTheDocument(); - }); - - it('should not show the experimental badge on the alerts table', async () => { - appMockRenderer = createAppMockRenderer({ - features: { alerts: { isExperimental: false } }, - }); - appMockRenderer.render(); - - expect( - screen.queryByTestId('case-view-alerts-table-experimental-badge') - ).not.toBeInTheDocument(); - }); - - it('should show the experimental badge on the alerts table', async () => { - appMockRenderer = createAppMockRenderer({ features: { alerts: { isExperimental: true } } }); - appMockRenderer.render(); - - expect( - await screen.findByTestId('case-view-alerts-table-experimental-badge') - ).toBeInTheDocument(); + await waitFor(() => { + expect(onComponentInitialized).toHaveBeenCalled(); }); - describe('description', () => { - it('renders the description correctly', async () => { - appMockRenderer.render(); + rerender(); - const description = within(await screen.findByTestId('description')); + await waitForComponentToUpdate(); - expect(await description.findByText(caseData.description)).toBeInTheDocument(); - }); - - it('should display description when case is loading', async () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - isLoading: true, - updateKey: 'description', - })); - - appMockRenderer.render(); - - expect(await screen.findByTestId('description')).toBeInTheDocument(); - }); - }); + expect(onComponentInitialized).toBeCalledTimes(1); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index 30af8a4a00552..4d9e3ba640449 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; import { useUrlParams } from '../../common/navigation'; import { useCasesContext } from '../cases_context/use_cases_context'; @@ -23,6 +23,14 @@ import type { CaseViewPageProps } from './types'; import { useRefreshCaseViewPage } from './use_on_refresh_case_view_page'; import { useOnUpdateField } from './use_on_update_field'; +const getActiveTabId = (tabId?: string) => { + if (tabId && Object.values(CASE_VIEW_PAGE_TABS).includes(tabId as CASE_VIEW_PAGE_TABS)) { + return tabId; + } + + return CASE_VIEW_PAGE_TABS.ACTIVITY; +}; + export const CaseViewPage = React.memo( ({ caseData, @@ -39,12 +47,7 @@ export const CaseViewPage = React.memo( useCasesTitleBreadcrumbs(caseData.title); - const activeTabId = useMemo(() => { - if (urlParams.tabId && Object.values(CASE_VIEW_PAGE_TABS).includes(urlParams.tabId)) { - return urlParams.tabId; - } - return CASE_VIEW_PAGE_TABS.ACTIVITY; - }, [urlParams.tabId]); + const activeTabId = getActiveTabId(urlParams?.tabId); const init = useRef(true); const timelineUi = useTimelineContext()?.ui; @@ -113,15 +116,12 @@ export const CaseViewPage = React.memo( onUpdateField={onUpdateField} /> - - - {activeTabId === CASE_VIEW_PAGE_TABS.ACTIVITY && ( { expect(screen.queryByTestId('case-view-files-stats-badge')).not.toBeInTheDocument(); }); - it('the files tab count has a different colour if the tab is not active', async () => { + it('the files tab count has a different color if the tab is not active', async () => { appMockRenderer.render(); expect( @@ -140,7 +140,7 @@ describe('CaseViewTabs', () => { expect(badge).toHaveTextContent('3'); }); - it('the alerts tab count has a different colour if the tab is not active', async () => { + it('the alerts tab count has a different color if the tab is not active', async () => { appMockRenderer.render( ); @@ -191,4 +191,54 @@ describe('CaseViewTabs', () => { }); }); }); + + it('should display the alerts tab when the feature is enabled', async () => { + appMockRenderer = createAppMockRenderer({ features: { alerts: { enabled: true } } }); + + appMockRenderer.render( + + ); + + expect(await screen.findByTestId('case-view-tab-title-alerts')).toBeInTheDocument(); + }); + + it('should not display the alerts tab when the feature is disabled', async () => { + appMockRenderer = createAppMockRenderer({ features: { alerts: { enabled: false } } }); + + appMockRenderer.render( + + ); + + expect(await screen.findByTestId('case-view-tabs')).toBeInTheDocument(); + expect(screen.queryByTestId('case-view-tab-title-alerts')).not.toBeInTheDocument(); + }); + + it('should not show the experimental badge on the alerts table', async () => { + appMockRenderer = createAppMockRenderer({ + features: { alerts: { isExperimental: false } }, + }); + + appMockRenderer.render( + + ); + + expect(await screen.findByTestId('case-view-tabs')).toBeInTheDocument(); + expect( + screen.queryByTestId('case-view-alerts-table-experimental-badge') + ).not.toBeInTheDocument(); + }); + + it('should show the experimental badge on the alerts table', async () => { + appMockRenderer = createAppMockRenderer({ + features: { alerts: { isExperimental: true } }, + }); + + appMockRenderer.render( + + ); + + expect( + await screen.findByTestId('case-view-alerts-table-experimental-badge') + ).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx index 671ce6606a141..cf5b2e7bd6802 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_tabs.tsx @@ -143,7 +143,7 @@ export const CaseViewTabs = React.memo(({ caseData, activeTab return ( <> - {renderTabs()} + {renderTabs()} ); diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 4ba3f912b9d0b..ecbde67ba15df 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -86,7 +86,6 @@ export const CaseView = React.memo( {getLegacyUrlConflictCallout()} void; caseData: CaseUI; } diff --git a/x-pack/plugins/cases/public/components/description/index.test.tsx b/x-pack/plugins/cases/public/components/description/index.test.tsx index 386d6b6e7154f..8c801b8efae95 100644 --- a/x-pack/plugins/cases/public/components/description/index.test.tsx +++ b/x-pack/plugins/cases/public/components/description/index.test.tsx @@ -144,6 +144,14 @@ describe('Description', () => { expect(screen.queryByTestId('description-edit-icon')).not.toBeInTheDocument(); }); + it('should display description when case is loading', async () => { + appMockRender.render( + + ); + + expect(await screen.findByTestId('description')).toBeInTheDocument(); + }); + describe('draft message', () => { const draftStorageKey = `cases.testAppId.basic-case-id.description.markdownEditor`; diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index b68641526c46a..1f50670b05291 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -15,13 +15,15 @@ import { EditConnector } from '.'; import { type AppMockRenderer, createAppMockRenderer, - readCasesPermissions, - noPushCasesPermissions, TestProviders, noConnectorsCasePermission, + noCasesPermissions, } from '../../common/mock'; import { basicCase, connectorsMock } from '../../containers/mock'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; +import type { ReturnUsePushToService } from '../use_push_to_service'; +import { usePushToService } from '../use_push_to_service'; +import { ConnectorTypes } from '../../../common'; const onSubmit = jest.fn(); const caseConnectors = getCaseConnectorsMockResponse(); @@ -34,35 +36,50 @@ const defaultProps: EditConnectorProps = { onSubmit, }; +jest.mock('../use_push_to_service'); + +const handlePushToService = jest.fn(); +const usePushToServiceMock = usePushToService as jest.Mock; + +const errorMsg = { id: 'test-error-msg', title: 'My error msg', description: 'My error desc' }; + +const usePushToServiceMockRes: ReturnUsePushToService = { + errorsMsg: [], + hasErrorMessages: false, + needsToBePushed: true, + hasBeenPushed: true, + isLoading: false, + hasLicenseError: false, + hasPushPermissions: true, + handlePushToService, +}; + describe('EditConnector ', () => { let appMockRender: AppMockRenderer; beforeEach(() => { jest.clearAllMocks(); appMockRender = createAppMockRenderer(); + usePushToServiceMock.mockReturnValue(usePushToServiceMockRes); }); - it('Renders the none connector', async () => { + it('renders an error message correctly', async () => { + usePushToServiceMock.mockReturnValue({ + ...usePushToServiceMockRes, + errorsMsg: [errorMsg], + hasErrorMessages: true, + }); + render( ); - expect( - await screen.findByText( - 'To create and update a case in an external system, select a connector.' - ) - ).toBeInTheDocument(); - - userEvent.click(screen.getByTestId('connector-edit-button')); - - await waitFor(() => { - expect(screen.getAllByTestId('dropdown-connector-no-connector').length).toBeGreaterThan(0); - }); + expect(await screen.findByText(errorMsg.description)).toBeInTheDocument(); }); - it('Edit external service on submit', async () => { + it('calls onSubmit when changing connector', async () => { render( @@ -97,6 +114,31 @@ describe('EditConnector ', () => { ); }); + it('should call handlePushToService when pushing to an external service', async () => { + usePushToServiceMock.mockReturnValue({ ...usePushToServiceMockRes, needsToBePushed: true }); + const props = { + ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { + ...defaultProps.caseData.connector, + id: 'servicenow-1', + }, + }, + }; + + render( + + + + ); + + expect(await screen.findByTestId('push-to-external-service')).toBeInTheDocument(); + userEvent.click(screen.getByTestId('push-to-external-service')); + + await waitFor(() => expect(handlePushToService).toHaveBeenCalled()); + }); + it('reverts to the initial selection if the caseData do not change', async () => { const props = { ...defaultProps, @@ -201,6 +243,7 @@ describe('EditConnector ', () => { it('does not shows the callouts when is loading', async () => { const props = { ...defaultProps, isLoading: true }; + usePushToServiceMock.mockReturnValue({ ...usePushToServiceMockRes, errorsMsg: [errorMsg] }); render( @@ -215,7 +258,7 @@ describe('EditConnector ', () => { it('does not allow the connector to be edited when the user does not have write permissions', async () => { render( - + ); @@ -229,25 +272,15 @@ describe('EditConnector ', () => { }); }); - it('display the callout message when none is selected', async () => { - // default props has the none connector as selected - const result = appMockRender.render(); - - await waitFor(() => { - expect(result.getByTestId('push-callouts')).toBeInTheDocument(); - }); - }); - it('shows the actions permission message if the user does not have read access to actions', async () => { appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: false, show: false }, }; - const result = appMockRender.render(); - await waitFor(() => { - expect(result.getByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument(); - }); + appMockRender.render(); + + expect(await screen.findByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument(); }); it('does not show the actions permission message if the user has read access to actions', async () => { @@ -256,35 +289,34 @@ describe('EditConnector ', () => { actions: { save: true, show: true }, }; - const result = appMockRender.render(); - await waitFor(() => { - expect(result.queryByTestId('edit-connector-permissions-error-msg')).toBe(null); - }); + appMockRender.render(); + + expect(screen.queryByTestId('edit-connector-permissions-error-msg')).not.toBeInTheDocument(); }); it('does not show the callout if the user does not have read access to actions', async () => { const props = { ...defaultProps, connectors: [] }; + appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: false, show: false }, }; - const result = appMockRender.render(); - await waitFor(() => { - expect(result.getByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument(); - expect(result.queryByTestId('push-callouts')).toBe(null); - }); + appMockRender.render(); + + expect(await screen.findByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument(); + expect(screen.queryByTestId('push-callouts')).not.toBeInTheDocument(); }); - it('does not show the callout if the user does not have access to cases connectors', async () => { + it('does not show the callouts if the user does not have access to cases connectors', async () => { + usePushToServiceMock.mockReturnValue({ ...usePushToServiceMockRes, errorsMsg: [errorMsg] }); const props = { ...defaultProps, connectors: [] }; + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); - const result = appMockRender.render(); - await waitFor(() => { - expect(result.getByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument(); - expect(result.queryByTestId('push-callouts')).toBe(null); - }); + appMockRender.render(); + + expect(screen.queryByTestId('push-callouts')).toBe(null); }); it('does not show the connectors previewer if the user does not have read access to actions', async () => { @@ -294,16 +326,16 @@ describe('EditConnector ', () => { actions: { save: false, show: false }, }; - const result = appMockRender.render(); - expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); + appMockRender.render(); + expect(screen.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); }); it('does not show the connectors previewer if the user does not have access to cases connectors', async () => { const props = { ...defaultProps, connectors: [] }; appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); - const result = appMockRender.render(); - expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); + appMockRender.render(); + expect(screen.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); }); it('does not show the connectors form if the user does not have read access to actions', async () => { @@ -313,16 +345,16 @@ describe('EditConnector ', () => { actions: { save: false, show: false }, }; - const result = appMockRender.render(); - expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); + appMockRender.render(); + expect(screen.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); }); it('does not show the connectors form if the user does not have access to cases connectors', async () => { const props = { ...defaultProps, connectors: [] }; appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); - const result = appMockRender.render(); - expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); + appMockRender.render(); + expect(screen.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); }); it('does not show the push button if the user does not have read access to actions', async () => { @@ -331,32 +363,45 @@ describe('EditConnector ', () => { actions: { save: false, show: false }, }; - const result = appMockRender.render(); - await waitFor(() => { - expect(result.queryByTestId('push-to-external-service')).toBe(null); - }); + appMockRender.render(); + + expect(screen.queryByTestId('push-to-external-service')).not.toBeInTheDocument(); }); it('does not show the push button if the user does not have push permissions', async () => { - appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() }); - const result = appMockRender.render(); + usePushToServiceMock.mockReturnValue({ ...usePushToServiceMockRes, hasPushPermissions: false }); + appMockRender.render(); - await waitFor(() => { - expect(result.queryByTestId('push-to-external-service')).toBe(null); - }); + expect(screen.queryByTestId('push-to-external-service')).not.toBeInTheDocument(); + }); + + it('disable the push button when connector is invalid', async () => { + usePushToServiceMock.mockReturnValue({ ...usePushToServiceMockRes, needsToBePushed: true }); + + appMockRender.render( + + ); + + expect(await screen.findByTestId('push-to-external-service')).toBeDisabled(); }); it('does not show the push button if the user does not have access to cases actions', async () => { appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); - const result = appMockRender.render(); - await waitFor(() => { - expect(result.queryByTestId('push-to-external-service')).toBe(null); - }); + appMockRender.render(); + + expect(screen.queryByTestId('push-to-external-service')).not.toBeInTheDocument(); }); it('does not show the edit connectors pencil if the user does not have read access to actions', async () => { const props = { ...defaultProps, connectors: [] }; + appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: false, show: false }, @@ -364,10 +409,8 @@ describe('EditConnector ', () => { appMockRender.render(); - await waitFor(() => { - expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument(); - expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); - }); + expect(await screen.findByTestId('connector-edit-header')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); }); it('does not show the edit connectors pencil if the user does not have access to case connectors', async () => { @@ -378,21 +421,36 @@ describe('EditConnector ', () => { appMockRender.render(); - await waitFor(() => { - expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument(); - expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); - }); + expect(await screen.findByTestId('connector-edit-header')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); }); it('does not show the edit connectors pencil if the user does not have push permissions', async () => { const props = { ...defaultProps, connectors: [] }; - appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() }); + usePushToServiceMock.mockReturnValue({ ...usePushToServiceMockRes, hasPushPermissions: false }); appMockRender.render(); - await waitFor(() => { - expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument(); - expect(screen.queryByTestId('connector-edit-button')).toBe(null); - }); + expect(await screen.findByTestId('connector-edit-header')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); + }); + + it('should show the correct connector name on the push button', async () => { + const props = { + ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { + id: 'resilient-2', + name: 'old name', + type: ConnectorTypes.resilient, + fields: null, + }, + }, + }; + + appMockRender.render(); + + expect(await screen.findByText('Update My Resilient connector incident')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx index ce4e453ecbbf1..b81f9d1448aa2 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions_stats.test.tsx @@ -22,7 +22,7 @@ const initialData = { isLoading: true, }; -describe('UseGetCaseUserActionsStats', () => { +describe('useGetCaseUserActionsStats', () => { let appMockRender: AppMockRenderer; beforeEach(() => { diff --git a/x-pack/test/functional/services/cases/navigation.ts b/x-pack/test/functional/services/cases/navigation.ts index 8d3ba0e73a24c..f0d4fb52ba5e4 100644 --- a/x-pack/test/functional/services/cases/navigation.ts +++ b/x-pack/test/functional/services/cases/navigation.ts @@ -22,8 +22,9 @@ export function CasesNavigationProvider({ getPageObject, getService }: FtrProvid await common.clickAndValidate('configure-case-button', 'case-configure-title'); }, - async navigateToSingleCase(app: string = 'cases', caseId: string) { - await common.navigateToUrlWithBrowserHistory(app, caseId); + async navigateToSingleCase(app: string = 'cases', caseId: string, tabId?: string) { + const search = tabId != null ? `?tabId=${tabId}` : ''; + await common.navigateToUrlWithBrowserHistory(app, caseId, search); }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts index 6979c45f867ba..cab3cc82e8a4b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts @@ -46,7 +46,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('should show the case view page correctly', async () => { await testSubjects.existOrFail('case-view-title'); await testSubjects.existOrFail('header-page-supplements'); + await testSubjects.existOrFail('case-action-bar-wrapper'); + await testSubjects.existOrFail('case-view-tabs'); + await testSubjects.existOrFail('case-view-tab-title-alerts'); await testSubjects.existOrFail('case-view-tab-title-activity'); await testSubjects.existOrFail('case-view-tab-title-files'); await testSubjects.existOrFail('description'); @@ -1013,11 +1016,26 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('Tabs', () => { createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + it('renders tabs correctly', async () => { + await testSubjects.existOrFail('case-view-tab-title-activity'); + await testSubjects.existOrFail('case-view-tab-title-files'); + await testSubjects.existOrFail('case-view-tab-title-alerts'); + }); + it('shows the "activity" tab by default', async () => { await testSubjects.existOrFail('case-view-tab-title-activity'); await testSubjects.existOrFail('case-view-tab-content-activity'); }); + it("shows the 'activity' tab when clicked", async () => { + // Go to the files tab first + await testSubjects.click('case-view-tab-title-files'); + await testSubjects.existOrFail('case-view-tab-content-files'); + + await testSubjects.click('case-view-tab-title-activity'); + await testSubjects.existOrFail('case-view-tab-content-activity'); + }); + it("shows the 'alerts' tab when clicked", async () => { await testSubjects.click('case-view-tab-title-alerts'); await testSubjects.existOrFail('case-view-tab-content-alerts'); @@ -1027,6 +1045,36 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('case-view-tab-title-files'); await testSubjects.existOrFail('case-view-tab-content-files'); }); + + describe('Query params', () => { + it('renders the activity tab when the query parameter tabId=activity', async () => { + const theCase = await createAndNavigateToCase(getPageObject, getService); + + await cases.navigation.navigateToSingleCase('cases', theCase.id, 'activity'); + await testSubjects.existOrFail('case-view-tab-title-activity'); + }); + + it('renders the activity tab when the query parameter tabId=alerts', async () => { + const theCase = await createAndNavigateToCase(getPageObject, getService); + + await cases.navigation.navigateToSingleCase('cases', theCase.id, 'alerts'); + await testSubjects.existOrFail('case-view-tab-title-activity'); + }); + + it('renders the activity tab when the query parameter tabId=files', async () => { + const theCase = await createAndNavigateToCase(getPageObject, getService); + + await cases.navigation.navigateToSingleCase('cases', theCase.id, 'files'); + await testSubjects.existOrFail('case-view-tab-content-files'); + }); + + it('renders the activity tab when the query parameter tabId has an unknown value', async () => { + const theCase = await createAndNavigateToCase(getPageObject, getService); + + await cases.navigation.navigateToSingleCase('cases', theCase.id, 'fake'); + await testSubjects.existOrFail('case-view-tab-title-activity'); + }); + }); }); describe('Files', () => {