From 6c3eb0f31f317876f603aece593a40e03e7c4596 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 27 Sep 2022 15:30:19 +0300 Subject: [PATCH] Improvements --- x-pack/plugins/cases/common/ui/types.ts | 11 - .../cases/public/common/translations.ts | 4 - .../public/common/use_cases_toast.test.tsx | 355 +++++++++++------- .../all_cases/all_cases_list.test.tsx | 3 +- .../components/all_cases/all_cases_list.tsx | 3 +- .../public/components/all_cases/columns.tsx | 23 +- .../public/components/all_cases/table.tsx | 3 + ...s_loading.tsx => use_is_loading_cases.tsx} | 0 .../all_cases/use_on_refresh_cases.test.tsx | 36 ++ .../components/all_cases/utility_bar.tsx | 13 +- .../confirm_delete_case/translations.ts | 6 - .../cases/public/containers/__mocks__/api.ts | 5 +- 12 files changed, 286 insertions(+), 176 deletions(-) rename x-pack/plugins/cases/public/components/all_cases/{use_is_loading.tsx => use_is_loading_cases.tsx} (100%) create mode 100644 x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index c3d3456db28a6..65a7fec902f7b 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -134,12 +134,6 @@ export interface ApiProps { signal: AbortSignal; } -export interface BulkUpdateStatus { - status: string; - id: string; - version: string; -} - export interface ActionLicense { id: string; name: string; @@ -148,11 +142,6 @@ export interface ActionLicense { enabledInLicense: boolean; } -export interface DeleteCase { - id: string; - title: string; -} - export interface FieldMappings { id: string; title?: string; diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index fad719f11e85b..9748339878222 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -297,10 +297,6 @@ export const READ_ACTIONS_PERMISSIONS_ERROR_MSG = i18n.translate( } ); -export const DELETE = i18n.translate('xpack.cases.caseTable.delete', { - defaultMessage: 'Delete', -}); - export const DELETED_CASES = (totalCases: number) => i18n.translate('xpack.cases.containers.deletedCases', { values: { totalCases }, diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index e8661387e1e64..cfa2d3a6e0052 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -23,6 +23,7 @@ const useKibanaMock = useKibana as jest.Mocked; describe('Use cases toast hook', () => { const successMock = jest.fn(); + const errorMock = jest.fn(); const getUrlForApp = jest.fn().mockReturnValue(`/app/cases/${mockCase.id}`); const navigateToUrl = jest.fn(); @@ -51,6 +52,7 @@ describe('Use cases toast hook', () => { useToastsMock.mockImplementation(() => { return { addSuccess: successMock, + addError: errorMock, }; }); @@ -63,159 +65,260 @@ describe('Use cases toast hook', () => { }; }); - describe('Toast hook', () => { - it('should create a success toast when invoked with a case', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ - theCase: mockCase, + describe('showSuccessAttach', () => { + describe('Toast hook', () => { + it('should create a success toast when invoked with a case', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + }); + expect(successMock).toHaveBeenCalled(); }); - expect(successMock).toHaveBeenCalled(); }); - }); - describe('toast title', () => { - it('should create a success toast when invoked with a case and a custom title', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); - validateTitle('Custom title'); - }); + describe('toast title', () => { + it('should create a success toast when invoked with a case and a custom title', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); + validateTitle('Custom title'); + }); - it('should display the alert sync title when called with an alert attachment (1 alert)', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [alertComment as SupportedCaseAttachment], + it('should display the alert sync title when called with an alert attachment (1 alert)', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateTitle('An alert was added to "Another horrible breach!!'); + }); + + it('should display the alert sync title when called with an alert attachment (multiple alerts)', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + const alert = { + ...alertComment, + alertId: ['1234', '54321'], + } as SupportedCaseAttachment; + + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alert], + }); + validateTitle('Alerts were added to "Another horrible breach!!'); + }); + + it('should display a generic title when called with a non-alert attachament', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [basicComment as SupportedCaseAttachment], + }); + validateTitle('Another horrible breach!! has been updated'); }); - validateTitle('An alert was added to "Another horrible breach!!'); }); - it('should display the alert sync title when called with an alert attachment (multiple alerts)', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - const alert = { - ...alertComment, - alertId: ['1234', '54321'], - } as SupportedCaseAttachment; - - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [alert], + describe('Toast content', () => { + let appMockRender: AppMockRenderer; + const onViewCaseClick = jest.fn(); + beforeEach(() => { + appMockRender = createAppMockRenderer(); + onViewCaseClick.mockReset(); + }); + + it('should create a success toast when invoked with a case and a custom content', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); + validateContent('Custom content'); + }); + + it('renders an alert-specific content when called with an alert attachment and sync on', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('The alert statuses are synched with the case status.'); + }); + + it('renders empty content when called with an alert attachment and sync off', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('View case'); + }); + + it('renders a correct successful message content', () => { + const result = appMockRender.render( + + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('renders a correct successful message without content', () => { + const result = appMockRender.render( + + ); + expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('Calls the onViewCaseClick when clicked', () => { + const result = appMockRender.render( + + ); + userEvent.click(result.getByTestId('toaster-content-case-view-link')); + expect(onViewCaseClick).toHaveBeenCalled(); }); - validateTitle('Alerts were added to "Another horrible breach!!'); }); - it('should display a generic title when called with a non-alert attachament', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [basicComment as SupportedCaseAttachment], + describe('Toast navigation', () => { + const tests = Object.entries(OWNER_INFO).map(([owner, ownerInfo]) => [ + owner, + ownerInfo.appId, + ]); + + it.each(tests)('should navigate correctly with owner %s and appId %s', (owner, appId) => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showSuccessAttach({ + theCase: { ...mockCase, owner }, + title: 'Custom title', + }); + + navigateToCase(); + + expect(getUrlForApp).toHaveBeenCalledWith(appId, { + deepLinkId: 'cases', + path: '/mock-id', + }); + + expect(navigateToUrl).toHaveBeenCalledWith('/app/cases/mock-id'); + }); + + it('navigates to the current app if the owner is invalid', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showSuccessAttach({ + theCase: { ...mockCase, owner: 'in-valid' }, + title: 'Custom title', + }); + + navigateToCase(); + + expect(getUrlForApp).toHaveBeenCalledWith('testAppId', { + deepLinkId: 'cases', + path: '/mock-id', + }); }); - validateTitle('Another horrible breach!! has been updated'); }); }); - describe('Toast content', () => { - let appMockRender: AppMockRenderer; - const onViewCaseClick = jest.fn(); - beforeEach(() => { - appMockRender = createAppMockRenderer(); - onViewCaseClick.mockReset(); - }); + describe('showErrorToast', () => { + it('should show an error toast', () => { + const error = new Error('showErrorToast: an error occurred'); - it('should create a success toast when invoked with a case and a custom content', () => { const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); - validateContent('Custom content'); + + result.current.showErrorToast(error); + + expect(errorMock).toHaveBeenCalledWith(error, { title: error.message }); }); - it('renders an alert-specific content when called with an alert attachment and sync on', () => { + it('should override the title', () => { + const error = new Error('showErrorToast: an error occurred'); + const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [alertComment as SupportedCaseAttachment], - }); - validateContent('The alert statuses are synched with the case status.'); + + result.current.showErrorToast(error, { title: 'my title' }); + + expect(errorMock).toHaveBeenCalledWith(error, { title: 'my title' }); }); - it('renders empty content when called with an alert attachment and sync off', () => { + it('should not show an error toast if the error is AbortError', () => { + const error = new Error('showErrorToast: an error occurred'); + error.name = 'AbortError'; + const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, - attachments: [alertComment as SupportedCaseAttachment], - }); - validateContent('View case'); - }); - it('renders a correct successful message content', () => { - const result = appMockRender.render( - - ); - expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); - expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); - expect(onViewCaseClick).not.toHaveBeenCalled(); - }); - - it('renders a correct successful message without content', () => { - const result = appMockRender.render( - - ); - expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); - expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); - expect(onViewCaseClick).not.toHaveBeenCalled(); - }); + result.current.showErrorToast(error); - it('Calls the onViewCaseClick when clicked', () => { - const result = appMockRender.render( - - ); - userEvent.click(result.getByTestId('toaster-content-case-view-link')); - expect(onViewCaseClick).toHaveBeenCalled(); + expect(errorMock).not.toHaveBeenCalled(); }); - }); - describe('Toast navigation', () => { - const tests = Object.entries(OWNER_INFO).map(([owner, ownerInfo]) => [owner, ownerInfo.appId]); + it('should show the body message if it is a ServerError', () => { + const error = new Error('showErrorToast: an error occurred'); + // @ts-expect-error: need to create a ServerError + error.body = { message: 'message error' }; - it.each(tests)('should navigate correctly with owner %s and appId %s', (owner, appId) => { const { result } = renderHook( () => { return useCasesToast(); @@ -223,22 +326,16 @@ describe('Use cases toast hook', () => { { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: { ...mockCase, owner }, - title: 'Custom title', - }); + result.current.showErrorToast(error); - navigateToCase(); - - expect(getUrlForApp).toHaveBeenCalledWith(appId, { - deepLinkId: 'cases', - path: '/mock-id', + expect(errorMock).toHaveBeenCalledWith(new Error('message error'), { + title: 'message error', }); - - expect(navigateToUrl).toHaveBeenCalledWith('/app/cases/mock-id'); }); + }); - it('navigates to the current app if the owner is invalid', () => { + describe('showSuccessToast', () => { + it('should show a success toast', () => { const { result } = renderHook( () => { return useCasesToast(); @@ -246,17 +343,9 @@ describe('Use cases toast hook', () => { { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: { ...mockCase, owner: 'in-valid' }, - title: 'Custom title', - }); - - navigateToCase(); + result.current.showSuccessToast('my title'); - expect(getUrlForApp).toHaveBeenCalledWith('testAppId', { - deepLinkId: 'cases', - path: '/mock-id', - }); + expect(successMock).toHaveBeenCalledWith('my title'); }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 12a3f6de15fb4..1cc3a5f0263e5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -731,8 +731,7 @@ describe('AllCasesListGeneric', () => { }); }); - // TODO: Fix - it.skip('should deselect cases when refreshing', async () => { + it('should deselect cases when refreshing', async () => { render( diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 8f0dbddf03dad..c7b2d4895f94a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -38,7 +38,7 @@ import { import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; import { getAllPermissionsExceptFrom } from '../../utils/permissions'; -import { useIsLoadingCases } from './use_is_loading'; +import { useIsLoadingCases } from './use_is_loading_cases'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -286,6 +286,7 @@ export const AllCasesList = React.memo( sorting={sorting} tableRef={tableRef} tableRowProps={tableRowProps} + deselectCases={deselectCases} /> ); diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 3a8e151197112..30a2035860eba 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -24,7 +24,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { UserProfileWithAvatar } from '@kbn/user-profile-components'; -import { Case, DeleteCase, UpdateByKey } from '../../../common/ui/types'; +import { Case, UpdateByKey } from '../../../common/ui/types'; import { CaseStatuses, ActionConnector, CaseSeverity } from '../../../common/api'; import { OWNER_INFO } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; @@ -126,27 +126,26 @@ export const useCasesColumns = ({ const { isAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures(); const { permissions } = useCasesContext(); - const [deleteThisCase, setDeleteThisCase] = useState({ - id: '', - title: '', - }); + const [caseToBeDeleted, setCaseToBeDeleted] = useState(); const { updateCaseProperty, isLoading: isLoadingUpdateCase } = useUpdateCase(); - const onDeleteAction = useCallback((deleteCase: Case) => { + const onDeleteAction = useCallback((theCase: Case) => { setIsModalVisible(true); - setDeleteThisCase({ id: deleteCase.id, title: deleteCase.title }); + setCaseToBeDeleted(theCase.id); }, []); const closeModal = useCallback(() => setIsModalVisible(false), []); const onConfirmDeletion = useCallback(() => { setIsModalVisible(false); - deleteCases({ - caseIds: [deleteThisCase.id], - successToasterTitle: i18n.DELETED_CASES(1), - }); - }, [deleteCases, deleteThisCase.id]); + if (caseToBeDeleted) { + deleteCases({ + caseIds: [caseToBeDeleted], + successToasterTitle: i18n.DELETED_CASES(1), + }); + } + }, [caseToBeDeleted, deleteCases]); const handleDispatchUpdate = useCallback( ({ updateKey, updateValue, caseData }: UpdateByKey) => { diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index b682dc12751e4..208c26e47648c 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -41,6 +41,7 @@ interface CasesTableProps { sorting: EuiBasicTableProps['sorting']; tableRef: MutableRefObject; tableRowProps: EuiBasicTableProps['rowProps']; + deselectCases: () => void; } const Div = styled.div` @@ -64,6 +65,7 @@ export const CasesTable: FunctionComponent = ({ sorting, tableRef, tableRowProps, + deselectCases, }) => { const { permissions } = useCasesContext(); const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation(); @@ -90,6 +92,7 @@ export const CasesTable: FunctionComponent = ({ enableBulkActions={showActions} filterOptions={filterOptions} selectedCases={selectedCases} + deselectCases={deselectCases} /> { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('should refresh data on refresh', async () => { + const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); + + const { result } = renderHook(() => useRefreshCases(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current(); + }); + + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index 6ae790b4c5b9a..fdfcdc17d472c 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -23,15 +23,14 @@ import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; import { useRefreshCases } from './use_on_refresh_cases'; -interface OwnProps { +interface Props { data: Cases; enableBulkActions: boolean; filterOptions: FilterOptions; selectedCases: Case[]; + deselectCases: () => void; } -type Props = OwnProps; - export const getStatusToasterMessage = (status: CaseStatuses, cases: Case[]): string => { const totalCases = cases.length; const caseTitle = totalCases === 1 ? cases[0].title : ''; @@ -52,6 +51,7 @@ export const CasesTableUtilityBar: FunctionComponent = ({ enableBulkActions = false, filterOptions, selectedCases, + deselectCases, }) => { const [isModalVisible, setIsModalVisible] = useState(false); const onCloseModal = useCallback(() => setIsModalVisible(false), []); @@ -104,6 +104,11 @@ export const CasesTableUtilityBar: FunctionComponent = ({ }); }, [deleteCases, selectedCases]); + const onRefresh = useCallback(() => { + deselectCases(); + refreshCases(); + }, [deselectCases, refreshCases]); + return ( @@ -132,7 +137,7 @@ export const CasesTableUtilityBar: FunctionComponent = ({ {i18n.REFRESH} diff --git a/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts b/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts index f8e4ab2a83a73..e6f5072f08f89 100644 --- a/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts +++ b/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts @@ -14,12 +14,6 @@ export const DELETE_TITLE = (caseTitle: string) => defaultMessage: 'Delete "{caseTitle}"', }); -export const DELETE_SELECTED_CASES = (quantity: number, title: string) => - i18n.translate('xpack.cases.confirmDeleteCase.selectedCases', { - values: { quantity, title }, - defaultMessage: 'Delete "{quantity, plural, =1 {{title}} other {Selected {quantity} cases}}"', - }); - export const CONFIRM_QUESTION = (quantity: number) => i18n.translate('xpack.cases.confirmDeleteCase.confirmQuestion', { values: { quantity }, diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index ad9191eec7e96..d01f927d3c324 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -8,7 +8,6 @@ import { ActionLicense, Cases, - BulkUpdateStatus, Case, CasesStatus, CaseUserActions, @@ -28,7 +27,7 @@ import { pushedCase, tags, } from '../mock'; -import { ResolvedCase, SeverityAll } from '../../../common/ui/types'; +import { CaseUpdateRequest, ResolvedCase, SeverityAll } from '../../../common/ui/types'; import { CasePatchRequest, CasePostRequest, @@ -100,7 +99,7 @@ export const patchCase = async ( ): Promise => Promise.resolve([basicCase]); export const updateCases = async ( - cases: BulkUpdateStatus[], + cases: CaseUpdateRequest[], signal: AbortSignal ): Promise => Promise.resolve(allCases.cases);