From e9e340365ec4e20cf25f03000a357128f21d852f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Jun 2023 16:57:30 +0300 Subject: [PATCH] [Cases] Fix case view flaky tests (#160412) ## Summary This PR fixes flaky tests in `x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx`. Some of them were converted to an e2e test as they were too slow to be on a unit test. Successful runs: - https://github.com/elastic/kibana/pull/160412/commits/a4ed8390d45b628ee4328c3655164c94449e3dc8 (50 times) - https://github.com/elastic/kibana/pull/160412/commits/82ccceb03aff6f74cd30f80fcaaa0f2b99c28103 (45 times) - https://github.com/elastic/kibana/pull/160412/commits/43c47e645b26561c00de97e8a4d965b435a5491b (20 times) Fixes: https://github.com/elastic/kibana/issues/149775, https://github.com/elastic/kibana/issues/149776, https://github.com/elastic/kibana/issues/149777, https://github.com/elastic/kibana/issues/149778, https://github.com/elastic/kibana/issues/149779, https://github.com/elastic/kibana/issues/149780, https://github.com/elastic/kibana/issues/149781, https://github.com/elastic/kibana/issues/149782, https://github.com/elastic/kibana/issues/153335, https://github.com/elastic/kibana/issues/153336, https://github.com/elastic/kibana/issues/149773, https://github.com/elastic/kibana/issues/149774, https://github.com/elastic/kibana/issues/149772, https://github.com/elastic/kibana/issues/149771, https://github.com/elastic/kibana/issues/149770, https://github.com/elastic/kibana/issues/149769, https://github.com/elastic/kibana/issues/151845, https://github.com/elastic/kibana/issues/153336, https://github.com/elastic/kibana/issues/153335 Flaky test runner: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2500 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) (cherry picked from commit c0047a1b94d898a16e4a5aad94663f07b5b06e52) --- ...na_react.mock.ts => kibana_react.mock.tsx} | 7 +- .../case_view/case_view_page.test.tsx | 469 ++++++------------ .../components/case_view_activity.tsx | 2 +- .../components/edit_connector/index.tsx | 2 +- .../components/severity/sidebar_selector.tsx | 2 +- .../services/cases/single_case_view.ts | 11 + .../apps/cases/group1/view_case.ts | 338 +++++++++---- 7 files changed, 418 insertions(+), 413 deletions(-) rename x-pack/plugins/cases/public/common/lib/kibana/{kibana_react.mock.ts => kibana_react.mock.tsx} (91%) diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx similarity index 91% rename from x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts rename to x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx index 68ad770d0a8bf..eea1561076ae4 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx @@ -30,6 +30,7 @@ interface StartServiceArgs { export const createStartServicesMock = ({ license }: StartServiceArgs = {}): StartServices => { const licensingPluginMock = licensingMock.createStart(); + const triggersActionsUi = triggersActionsUiMock.createStart(); const services = { ...coreMock.createStart(), @@ -39,7 +40,11 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta navigateToPrefilledEditor: jest.fn(), }, security: securityMock.createStart(), - triggersActionsUi: triggersActionsUiMock.createStart(), + triggersActionsUi: { + actionTypeRegistry: triggersActionsUi.actionTypeRegistry, + alertsTableConfigurationRegistry: triggersActionsUi.alertsTableConfigurationRegistry, + getAlertsStateTable: jest.fn().mockReturnValue(
), + }, spaces: spacesPluginMock.createStartContract(), licensing: license != null 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 69a66653829bc..49af1aed3a075 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,10 +6,8 @@ */ import React from 'react'; -import { act, waitFor, within, screen } from '@testing-library/react'; +import { waitFor, within, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; -import { ConnectorTypes } from '../../../common/api'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import '../../common/mock/match_media'; @@ -36,14 +34,13 @@ import { defaultUseFindCaseUserActions, } from './mocks'; import type { CaseViewPageProps } from './types'; -import { userProfiles } from '../../containers/user_profiles/api.mock'; 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'; - -const mockSetTitle = jest.fn(); +import { createQueryWithMarkup } from '../../common/test_utils'; +import { useCasesFeatures } from '../../common/use_cases_features'; jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_update_case'); @@ -58,27 +55,14 @@ 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('../../common/hooks'); jest.mock('../connectors/resilient/api'); -jest.mock('../../common/lib/kibana', () => { - const originalModule = jest.requireActual('../../common/lib/kibana'); - return { - ...originalModule, - useKibana: () => { - const { services } = originalModule.useKibana(); - return { - services: { - ...services, - chrome: { setBreadcrumbs: jest.fn(), docTitle: { change: mockSetTitle } }, - }, - }; - }, - }; -}); +jest.mock('../../common/lib/kibana'); const useFetchCaseMock = useGetCase as jest.Mock; const useUrlParamsMock = useUrlParams as jest.Mock; @@ -93,12 +77,14 @@ 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; const mockGetCase = (props: Partial = {}) => { const data = { ...defaultGetCase.data, ...props.data, }; + useFetchCaseMock.mockReturnValue({ ...defaultGetCase, ...props, @@ -127,11 +113,49 @@ const userActionsStats = { describe('CaseViewPage', () => { const updateCaseProperty = defaultUpdateCaseState.mutate; const pushCaseToExternalService = jest.fn(); - const data = caseProps.caseData; - let appMockRenderer: AppMockRenderer; 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(); @@ -150,54 +174,26 @@ describe('CaseViewPage', () => { }); useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); - const license = licensingMock.createLicense({ - license: { type: 'platinum' }, - }); useGetCaseUsersMock.mockReturnValue({ isLoading: false, data: caseUsers }); - - appMockRenderer = createAppMockRenderer({ license }); - }); - - it('should render CaseViewPage', async () => { - const damagedRaccoonUser = userProfiles[0].user; - const caseDataWithDamagedRaccoon = { - ...caseData, - createdBy: { - profileUid: userProfiles[0].uid, - username: damagedRaccoonUser.username, - fullName: damagedRaccoonUser.full_name, - email: damagedRaccoonUser.email, - }, - }; - - const license = licensingMock.createLicense({ - license: { type: 'platinum' }, + useCasesFeaturesMock.mockReturnValue({ + metricsFeatures: ['alerts.count'], + pushToServiceAuthorized: true, + caseAssignmentAuthorized: true, + isAlertsEnabled: true, + isSyncAlertsEnabled: true, }); - const props = { ...caseProps, caseData: caseDataWithDamagedRaccoon }; - appMockRenderer = createAppMockRenderer({ features: { metrics: ['alerts.count'] }, license }); - const result = appMockRenderer.render(); - - expect(result.getByTestId('header-page-title')).toHaveTextContent(data.title); - expect(result.getByTestId('case-view-status-dropdown')).toHaveTextContent('Open'); - expect(result.getByTestId('case-view-metrics-panel')).toBeInTheDocument(); - expect( - within(result.getByTestId('case-view-tag-list')).getByTestId('tag-coke') - ).toHaveTextContent(data.tags[0]); - - expect( - within(result.getByTestId('case-view-tag-list')).getByTestId('tag-pepsi') - ).toHaveTextContent(data.tags[1]); + appMockRenderer = createAppMockRenderer({ license: platinumLicense }); + }); - expect(result.getAllByText(data.createdBy.fullName!)[0]).toBeInTheDocument(); + afterAll(() => { + Object.defineProperty(window, 'getComputedStyle', originalGetComputedStyle); + }); - expect( - within(result.getByTestId('description')).getByTestId('scrollable-markdown') - ).toHaveTextContent(data.description); + it('shows the metrics section', async () => { + appMockRenderer.render(); - expect(result.getByTestId('case-view-status-action-button')).toHaveTextContent( - 'Mark in progress' - ); + expect(await screen.findByTestId('case-view-metrics-panel')).toBeInTheDocument(); }); it('should show closed indicators in header when case is closed', async () => { @@ -206,40 +202,9 @@ describe('CaseViewPage', () => { caseData: basicCaseClosed, })); - const result = appMockRenderer.render(); + appMockRenderer.render(); - expect(result.getByTestId('case-view-status-dropdown')).toHaveTextContent('Closed'); - }); - - it('should update status', async () => { - const result = appMockRenderer.render(); - - const dropdown = result.getByTestId('case-view-status-dropdown'); - userEvent.click(dropdown.querySelector('button')!); - await waitForEuiPopoverOpen(); - userEvent.click(result.getByTestId('case-view-status-dropdown-closed')); - const updateObject = updateCaseProperty.mock.calls[0][0]; - - await waitFor(() => { - expect(updateCaseProperty).toHaveBeenCalledTimes(1); - expect(updateObject.updateKey).toEqual('status'); - expect(updateObject.updateValue).toEqual('closed'); - }); - }); - - it('should update title', async () => { - const result = appMockRenderer.render(); - const newTitle = 'The new title'; - userEvent.click(result.getByTestId('editable-title-edit-icon')); - userEvent.clear(result.getByTestId('editable-title-input-field')); - userEvent.type(result.getByTestId('editable-title-input-field'), newTitle); - userEvent.click(result.getByTestId('editable-title-submit-btn')); - - const updateObject = updateCaseProperty.mock.calls[0][0]; - await waitFor(() => { - expect(updateObject.updateKey).toEqual('title'); - expect(updateObject.updateValue).toEqual(newTitle); - }); + expect(await screen.findByTestId('case-view-status-dropdown')).toHaveTextContent('Closed'); }); it('should push updates on button click', async () => { @@ -254,11 +219,12 @@ describe('CaseViewPage', () => { }, })); - const result = appMockRenderer.render(); + appMockRenderer.render(); - expect(result.getByTestId('push-to-external-service')).toBeInTheDocument(); + expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); + expect(await screen.findByTestId('push-to-external-service')).toBeInTheDocument(); - userEvent.click(result.getByTestId('push-to-external-service')); + userEvent.click(screen.getByTestId('push-to-external-service')); await waitFor(() => { expect(pushCaseToExternalService).toHaveBeenCalled(); @@ -266,7 +232,7 @@ describe('CaseViewPage', () => { }); it('should disable the push button when connector is invalid', async () => { - const result = appMockRenderer.render( + appMockRenderer.render( { }} /> ); - await waitFor(() => { - expect(result.getByTestId('push-to-external-service')).toBeDisabled(); - }); - }); - it('should update connector', async () => { - const result = appMockRenderer.render( - - ); - - userEvent.click(result.getByTestId('connector-edit').querySelector('button')!); - userEvent.click(result.getByTestId('dropdown-connectors')); - await waitForEuiPopoverOpen(); - userEvent.click(result.getByTestId('dropdown-connector-resilient-2')); - - await waitFor(() => { - expect(result.getByTestId('connector-fields-resilient')).toBeInTheDocument(); - }); - - userEvent.click(result.getByTestId('edit-connectors-submit')); - - await waitFor(() => { - expect(updateCaseProperty).toHaveBeenCalledTimes(1); - const updateObject = updateCaseProperty.mock.calls[0][0]; - expect(updateObject.updateKey).toEqual('connector'); - expect(updateObject.updateValue).toEqual({ - id: 'resilient-2', - name: 'My Resilient connector', - type: ConnectorTypes.resilient, - fields: { - incidentTypes: null, - severityCode: null, - }, - }); - }); + expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); + expect(await screen.findByTestId('push-to-external-service')).toBeDisabled(); }); it('should call onComponentInitialized on mount', async () => { @@ -337,22 +260,17 @@ describe('CaseViewPage', () => { const useFetchAlertData = jest.fn().mockReturnValue([true]); useGetCaseUserActionsStatsMock.mockReturnValue({ isLoading: true }); - const result = appMockRenderer.render( - - ); - await waitFor(() => { - expect(result.getByTestId('case-view-loading-content')).toBeInTheDocument(); - expect(result.queryByTestId('user-actions-list')).not.toBeInTheDocument(); - }); + 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(); - const result = appMockRenderer.render( - - ); + appMockRenderer.render(); - userEvent.click(result.getAllByTestId('comment-action-show-alert-alert-action-id')[1]); + userEvent.click((await screen.findAllByTestId('comment-action-show-alert-alert-action-id'))[1]); await waitFor(() => { expect(showAlertDetails).toHaveBeenCalledWith('alert-id-1', 'alert-index-1'); @@ -360,23 +278,23 @@ describe('CaseViewPage', () => { }); it('should show the rule name', async () => { - const result = appMockRenderer.render(); + appMockRenderer.render(); - await waitFor(() => { - expect( - result - .getAllByTestId('user-action-alert-comment-create-action-alert-action-id')[1] - .querySelector('.euiCommentEvent__headerEvent') - ).toHaveTextContent('added an alert from Awesome rule'); - }); + 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 () => { - const result = appMockRenderer.render(); - userEvent.click(result.getByTestId('sync-alerts-switch')); - const updateObject = updateCaseProperty.mock.calls[0][0]; + 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 }); }); @@ -385,95 +303,57 @@ describe('CaseViewPage', () => { it('should show the correct connector name on the push button', async () => { useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); - const result = appMockRenderer.render( + appMockRenderer.render( ); - await waitFor(() => { - expect(result.getByTestId('push-to-external-service')).toHaveTextContent( - 'My Resilient connector' - ); - }); + 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 })); - const result = appMockRenderer.render(); + appMockRenderer.render(); - expect(result.container.querySelector('.euiCallOut--danger')).toBeInTheDocument(); + 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 })); - const result = appMockRenderer.render(); + appMockRenderer.render(); - expect(result.container.querySelector('.euiCallOut--danger')).not.toBeInTheDocument(); + expect(await screen.findByTestId('edit-connectors')).toBeInTheDocument(); + expect( + screen.queryByTestId('case-callout-a25a5b368b6409b179ef4b6c5168244f') + ).not.toBeInTheDocument(); }); }); - // FLAKY: https://github.com/elastic/kibana/issues/149775 - // FLAKY: https://github.com/elastic/kibana/issues/149776 - // FLAKY: https://github.com/elastic/kibana/issues/149777 - // FLAKY: https://github.com/elastic/kibana/issues/149778 - // FLAKY: https://github.com/elastic/kibana/issues/149779 - // FLAKY: https://github.com/elastic/kibana/issues/149780 - // FLAKY: https://github.com/elastic/kibana/issues/149781 - // FLAKY: https://github.com/elastic/kibana/issues/149782 - // FLAKY: https://github.com/elastic/kibana/issues/153335 - // FLAKY: https://github.com/elastic/kibana/issues/153336 - describe.skip('Tabs', () => { - jest.mock('@kbn/kibana-react-plugin/public', () => ({ - useKibana: () => ({ - services: { - application: { - capabilities: { - fakeCases: { - create_cases: true, - read_cases: true, - update_cases: true, - delete_cases: true, - push_cases: true, - }, - }, - }, - cases: { - ui: { - getCasesContext: () => null, - }, - helpers: { - getUICapabilities: () => ({ - all: true, - read: true, - create: true, - update: true, - delete: true, - push: true, - }), - }, - }, - notifications: { - toasts: { - addDanger: () => {}, - }, - }, - }, - }), - })); + describe('Tabs', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders tabs correctly', async () => { - const result = appMockRenderer.render(); - await act(async () => { - expect(result.getByTestId('case-view-tab-title-activity')).toBeTruthy(); - expect(result.getByTestId('case-view-tab-title-alerts')).toBeTruthy(); - expect(result.getByTestId('case-view-tab-title-files')).toBeTruthy(); - }); + 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 () => { - const result = appMockRenderer.render(); - await act(async () => { - expect(result.getByTestId('case-view-tab-content-activity')).toBeTruthy(); - }); + 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 () => { @@ -482,10 +362,11 @@ describe('CaseViewPage', () => { tabId: CASE_VIEW_PAGE_TABS.ALERTS, }, }); - const result = appMockRenderer.render(); - await act(async () => { - expect(result.getByTestId('case-view-tab-content-alerts')).toBeTruthy(); - }); + + 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 () => { @@ -494,10 +375,10 @@ describe('CaseViewPage', () => { tabId: CASE_VIEW_PAGE_TABS.ACTIVITY, }, }); - const result = appMockRenderer.render(); - await act(async () => { - expect(result.getByTestId('case-view-tab-content-activity')).toBeTruthy(); - }); + + 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 () => { @@ -506,18 +387,20 @@ describe('CaseViewPage', () => { tabId: 'what-is-love', }, }); - const result = appMockRenderer.render(); - await act(async () => { - expect(result.getByTestId('case-view-tab-content-activity')).toBeTruthy(); - expect(result.queryByTestId('case-view-tab-content-alerts')).toBeFalsy(); - }); + + 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; - const result = appMockRenderer.render(); - userEvent.click(result.getByTestId('case-view-tab-title-activity')); - await act(async () => { + 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, @@ -527,9 +410,11 @@ describe('CaseViewPage', () => { it('navigates to the alerts tab when the alerts tab is clicked', async () => { const navigateToCaseViewMock = useCaseViewNavigationMock().navigateToCaseView; - const result = appMockRenderer.render(); - userEvent.click(result.getByTestId('case-view-tab-title-alerts')); - await act(async () => { + 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, @@ -539,45 +424,45 @@ describe('CaseViewPage', () => { it('should display the alerts tab when the feature is enabled', async () => { appMockRenderer = createAppMockRenderer({ features: { alerts: { enabled: true } } }); - const result = appMockRenderer.render(); - await act(async () => { - expect(result.queryByTestId('case-view-tab-title-activity')).toBeTruthy(); - expect(result.queryByTestId('case-view-tab-title-alerts')).toBeTruthy(); - }); + 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 } } }); - const result = appMockRenderer.render(); - await act(async () => { - expect(result.queryByTestId('case-view-tab-title-activity')).toBeTruthy(); - expect(result.queryByTestId('case-view-tab-title-alerts')).toBeFalsy(); - }); + 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 } } }); - const result = appMockRenderer.render(); - - await act(async () => { - expect(result.queryByTestId('case-view-alerts-table-experimental-badge')).toBeFalsy(); + 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 } } }); - const result = appMockRenderer.render(); + appMockRenderer.render(); - await act(async () => { - expect(result.queryByTestId('case-view-alerts-table-experimental-badge')).toBeTruthy(); - }); + expect( + await screen.findByTestId('case-view-alerts-table-experimental-badge') + ).toBeInTheDocument(); }); describe('description', () => { it('renders the description correctly', async () => { appMockRenderer.render(); - const description = within(screen.getByTestId('description')); + const description = within(await screen.findByTestId('description')); expect(await description.findByText(caseData.description)).toBeInTheDocument(); }); @@ -591,37 +476,7 @@ describe('CaseViewPage', () => { appMockRenderer.render(); - await waitFor(() => { - expect(screen.getByTestId('description')).toBeInTheDocument(); - }); - }); - - it.skip('it should persist the draft of new comment while description is updated', async () => { - const newComment = 'another cool comment'; - - appMockRenderer.render(); - - userEvent.click(await screen.findByTestId('user-actions-filter-activity-button-all')); - - userEvent.type(await screen.findByTestId('euiMarkdownEditorTextArea'), newComment); - - userEvent.click(await screen.findByTestId('description-edit-icon')); - - userEvent.type(screen.getAllByTestId('euiMarkdownEditorTextArea')[0], 'Edited!'); - - userEvent.click(screen.getByTestId('editable-save-markdown')); - - expect(await screen.findByTestId('euiMarkdownEditorTextArea')).toHaveTextContent( - newComment - ); - }); - }); - - describe('breadcrumbs', () => { - it('should set the cases title', () => { - appMockRenderer.render(); - - expect(mockSetTitle).toHaveBeenCalledWith([caseProps.caseData.title, 'Cases', 'Test']); + expect(await screen.findByTestId('description')).toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index 828753d0a410b..e3b5395bbd03e 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -262,7 +262,7 @@ export const CaseViewActivity = ({ ) : null} - + {caseAssignmentAuthorized ? ( <> diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index c0c98ceaa6db3..875e2ce6ffe23 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -90,7 +90,7 @@ export const EditConnector = React.memo( !needsToBePushed; return ( - + = ({ isDisabled, }) => { return ( - +

{SEVERITY_TITLE}

diff --git a/x-pack/test/functional/services/cases/single_case_view.ts b/x-pack/test/functional/services/cases/single_case_view.ts index c3e668557af57..961ee05a33a52 100644 --- a/x-pack/test/functional/services/cases/single_case_view.ts +++ b/x-pack/test/functional/services/cases/single_case_view.ts @@ -58,6 +58,17 @@ export function CasesSingleViewServiceProvider({ getService, getPageObject }: Ft }); }, + async addComment(comment: string) { + const addCommentElement = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + + await addCommentElement.focus(); + await addCommentElement.type(comment); + + await this.submitComment(); + }, + async addVisualizationToNewComment(visName: string) { // open saved object finder const addCommentElement = await testSubjects.find('add-comment'); 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 dd584a51fa979..cfa642fd25cdd 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 @@ -9,6 +9,8 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; import { CaseStatuses } from '@kbn/cases-plugin/common'; import { CaseSeverity } from '@kbn/cases-plugin/common/api'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; + import { FtrProviderContext } from '../../../ftr_provider_context'; import { createUsersAndRoles, @@ -28,6 +30,29 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const browser = getService('browser'); describe('View case', () => { + describe('page', () => { + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + + 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-view-tab-title-activity'); + await testSubjects.existOrFail('case-view-tab-title-files'); + await testSubjects.existOrFail('description'); + + await testSubjects.existOrFail('case-view-activity'); + + await testSubjects.existOrFail('case-view-assignees'); + await testSubjects.existOrFail('sidebar-severity'); + await testSubjects.existOrFail('case-view-user-list-reporter'); + await testSubjects.existOrFail('case-view-user-list-participants'); + await testSubjects.existOrFail('case-view-tag-list'); + await testSubjects.existOrFail('cases-categories'); + await testSubjects.existOrFail('sidebar-connectors'); + }); + }); + describe('properties', () => { createOneCaseBeforeDeleteAllAfter(getPageObject, getService); @@ -54,6 +79,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { ); await commentArea.focus(); await commentArea.type('Test comment from automation'); + await testSubjects.click('submit-comment'); // validate user action @@ -182,144 +208,218 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { ); }); }); + }); - describe('draft comments', () => { - createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + describe('draft comments', () => { + createOneCaseBeforeEachDeleteAllAfterEach(getPageObject, getService); - it('persists new comment when status is updated in dropdown', async () => { - const commentArea = await find.byCssSelector( - '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' - ); - await commentArea.focus(); - await commentArea.type('Test comment from automation'); + it('persists new comment when status is updated in dropdown', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); - await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="case-status-badge-in-progress"]' - ); - // validates dropdown tag - await testSubjects.existOrFail( - 'case-view-status-dropdown > case-status-badge-popover-button-in-progress' - ); + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="case-status-badge-in-progress"]' + ); + // validates dropdown tag + await testSubjects.existOrFail( + 'case-view-status-dropdown > case-status-badge-popover-button-in-progress' + ); - await testSubjects.click('submit-comment'); + await testSubjects.click('submit-comment'); - // validate user action - const newComment = await find.byCssSelector( - '[data-test-subj*="comment-create-action"] [data-test-subj="scrollable-markdown"]' - ); - expect(await newComment.getVisibleText()).equal('Test comment from automation'); - }); + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="scrollable-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); - it('persists new comment when case is closed through the close case button', async () => { - const commentArea = await find.byCssSelector( - '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' - ); - await commentArea.focus(); - await commentArea.type('Test comment from automation'); + it('persists new comment when case is closed through the close case button', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); - await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); - await header.waitUntilLoadingHasFinished(); - await testSubjects.click('case-view-status-action-button'); - await header.waitUntilLoadingHasFinished(); + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail( - 'header-page-supplements > case-status-badge-popover-button-closed', - { - timeout: 5000, - } - ); + await testSubjects.existOrFail( + 'header-page-supplements > case-status-badge-popover-button-closed', + { + timeout: 5000, + } + ); - // validate user action - await find.byCssSelector( - '[data-test-subj*="status-update-action"] [data-test-subj="case-status-badge-closed"]' - ); - // validates dropdown tag - await testSubjects.existOrFail( - 'case-view-status-dropdown >case-status-badge-popover-button-closed' - ); + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="case-status-badge-closed"]' + ); + // validates dropdown tag + await testSubjects.existOrFail( + 'case-view-status-dropdown >case-status-badge-popover-button-closed' + ); - await testSubjects.click('submit-comment'); + await testSubjects.click('submit-comment'); - // validate user action - const newComment = await find.byCssSelector( - '[data-test-subj*="comment-create-action"] [data-test-subj="scrollable-markdown"]' - ); - expect(await newComment.getVisibleText()).equal('Test comment from automation'); - }); + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="scrollable-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); - it('persists new comment to the case when user goes to case list table and comes back to the case', async () => { - const commentArea = await find.byCssSelector( - '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' - ); - await commentArea.focus(); - await commentArea.type('Test comment from automation'); + it('persists new comment to the case when user goes to case list table and comes back to the case', async () => { + const comment = 'Test comment from automation'; - await testSubjects.click('backToCases'); + let commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type(comment); - const caseLink = await find.byCssSelector('[data-test-subj="case-details-link"'); + /** + * We need to wait for some time to + * give the localStorage a change to persist + * the comment. Otherwise, the test navigates to + * fast to the cases table and the comment is not + * persisted + */ + await setTimeoutAsync(2000); - caseLink.click(); + await testSubjects.click('backToCases'); - await testSubjects.click('submit-comment'); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); - // validate user action - const newComment = await find.byCssSelector( - '[data-test-subj*="comment-create-action"] [data-test-subj="scrollable-markdown"]' - ); - expect(await newComment.getVisibleText()).equal('Test comment from automation'); - }); + commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); - it('shows unsaved comment message when page is refreshed', async () => { - await testSubjects.click('property-actions-user-action-ellipses'); + expect(await commentArea.getVisibleText()).equal(comment); - await header.waitUntilLoadingHasFinished(); + await testSubjects.click('submit-comment'); - await testSubjects.click('property-actions-user-action-pencil'); + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="scrollable-markdown"]' + ); - await header.waitUntilLoadingHasFinished(); + expect(await newComment.getVisibleText()).equal(comment); + }); - const editCommentTextArea = await find.byCssSelector( - '[data-test-subj*="editable-markdown-form"] textarea.euiMarkdownEditorTextArea' - ); + it('shows unsaved comment message when page is refreshed', async () => { + await cases.singleCase.addComment('my comment'); + await header.waitUntilLoadingHasFinished(); - await header.waitUntilLoadingHasFinished(); + await testSubjects.click('property-actions-user-action-ellipses'); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('property-actions-user-action-pencil'); + await header.waitUntilLoadingHasFinished(); - await editCommentTextArea.focus(); - await editCommentTextArea.type('Edited comment'); + const editCommentTextArea = await find.byCssSelector( + '[data-test-subj*="editable-markdown-form"] textarea.euiMarkdownEditorTextArea' + ); - await header.waitUntilLoadingHasFinished(); + await header.waitUntilLoadingHasFinished(); - await browser.refresh(); + await editCommentTextArea.focus(); + await editCommentTextArea.type('Edited comment'); - await header.waitUntilLoadingHasFinished(); + /** + * We need to wait for some time to + * give the localStorage a change to persist + * the comment. Otherwise, the test navigates to + * fast to the cases table and the comment is not + * persisted + */ + await setTimeoutAsync(2000); - await testSubjects.existOrFail('user-action-comment-unsaved-draft'); - }); + await header.waitUntilLoadingHasFinished(); + await browser.refresh(); - it('shows unsaved description message when page is refreshed', async () => { - await testSubjects.click('description-edit-icon'); + await header.waitUntilLoadingHasFinished(); - await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('user-action-comment-unsaved-draft'); + }); - const editCommentTextArea = await find.byCssSelector( - '[data-test-subj*="editable-markdown-form"] textarea.euiMarkdownEditorTextArea' - ); + it('shows unsaved description message when page is refreshed', async () => { + await testSubjects.click('description-edit-icon'); - await header.waitUntilLoadingHasFinished(); + await header.waitUntilLoadingHasFinished(); - await editCommentTextArea.focus(); - await editCommentTextArea.type('Edited description'); + const editCommentTextArea = await find.byCssSelector( + '[data-test-subj*="editable-markdown-form"] textarea.euiMarkdownEditorTextArea' + ); - await header.waitUntilLoadingHasFinished(); + await header.waitUntilLoadingHasFinished(); - await browser.refresh(); + await editCommentTextArea.focus(); + await editCommentTextArea.type('Edited description'); - await header.waitUntilLoadingHasFinished(); + /** + * We need to wait for some time to + * give the localStorage a change to persist + * the comment. Otherwise, the test navigates to + * fast to the cases table and the comment is not + * persisted + */ + await setTimeoutAsync(2000); - await testSubjects.existOrFail('description-unsaved-draft'); - }); + await header.waitUntilLoadingHasFinished(); + + await browser.refresh(); + + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('description-unsaved-draft'); + }); + + /** + * There is this bug https://github.com/elastic/kibana/issues/157280 + * where this test randomly reproduces thus making the test flaky. + * Skipping for now until we fix it. + */ + it.skip('should persist the draft of new comment while description is updated', async () => { + let commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + + await commentArea.focus(); + await commentArea.type('Test comment from automation'); + + await testSubjects.click('description-edit-icon'); + + await header.waitUntilLoadingHasFinished(); + + const description = await find.byCssSelector( + '[data-test-subj*="editable-markdown-form"] textarea.euiMarkdownEditorTextArea' + ); + + await header.waitUntilLoadingHasFinished(); + + await description.focus(); + await description.type('Edited description'); + + await testSubjects.click('editable-save-markdown'); + await header.waitUntilLoadingHasFinished(); + + await browser.refresh(); + await header.waitUntilLoadingHasFinished(); + + commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + + expect(await commentArea.getVisibleText()).to.be('Test comment from automation'); }); }); @@ -770,6 +870,23 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); }); + + describe('breadcrumbs', () => { + after(async () => { + await cases.api.deleteAllCases(); + }); + + it('should set the cases title', async () => { + const theCase = await createAndNavigateToCase(getPageObject, getService); + const firstBreadcrumb = await testSubjects.getVisibleText('breadcrumb first'); + const middleBreadcrumb = await testSubjects.getVisibleText('breadcrumb'); + const lastBreadcrumb = await testSubjects.getVisibleText('breadcrumb last'); + + expect(firstBreadcrumb).to.be('Management'); + expect(middleBreadcrumb).to.be('Cases'); + expect(lastBreadcrumb).to.be(theCase.title); + }); + }); }); }; @@ -788,6 +905,21 @@ const createOneCaseBeforeDeleteAllAfter = ( }); }; +const createOneCaseBeforeEachDeleteAllAfterEach = ( + getPageObject: FtrProviderContext['getPageObject'], + getService: FtrProviderContext['getService'] +) => { + const cases = getService('cases'); + + beforeEach(async () => { + await createAndNavigateToCase(getPageObject, getService); + }); + + afterEach(async () => { + await cases.api.deleteAllCases(); + }); +}; + const createAndNavigateToCase = async ( getPageObject: FtrProviderContext['getPageObject'], getService: FtrProviderContext['getService'] @@ -796,8 +928,10 @@ const createAndNavigateToCase = async ( const cases = getService('cases'); await cases.navigation.navigateToApp(); - await cases.api.createNthRandomCases(1); + const theCase = await cases.api.createCase(); await cases.casesTable.waitForCasesToBeListed(); await cases.casesTable.goToFirstListedCase(); await header.waitUntilLoadingHasFinished(); + + return theCase; };