From b6d291d1034752515b56439d4b2437f085cd2147 Mon Sep 17 00:00:00 2001 From: Julian Gernun <17549662+jcger@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:30:49 +0100 Subject: [PATCH] [Cases] Table Solution Filter Not Rendering Any Checked Option When All Selected (#172460) Co-authored-by: Christos Nasikas --- .../all_cases/all_cases_list.test.tsx | 102 ----------------- .../components/all_cases/all_cases_list.tsx | 3 +- .../all_cases/solution_filter.test.tsx | 4 +- .../components/all_cases/solution_filter.tsx | 35 +----- .../use_system_filter_config.tsx | 17 +-- .../all_cases/table_filters.test.tsx | 61 +++++++++- .../components/all_cases/table_filters.tsx | 3 - .../public/containers/use_get_cases.test.tsx | 104 +++++++++++++++++- .../cases/public/containers/use_get_cases.tsx | 15 +++ x-pack/test/functional/services/cases/list.ts | 15 ++- .../apps/cases/group2/attachment_framework.ts | 16 +++ 11 files changed, 207 insertions(+), 168 deletions(-) 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 2971da7a1041a..028849f48fdb4 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 @@ -516,7 +516,6 @@ describe('AllCasesListGeneric', () => { filterOptions: { ...DEFAULT_FILTER_OPTIONS, searchFields: ['title', 'description'], - owner: ['securitySolution'], category: ['twix'], }, queryParams: DEFAULT_QUERY_PARAMS, @@ -645,82 +644,6 @@ describe('AllCasesListGeneric', () => { }); describe('Solutions', () => { - it('should set the owner to all available solutions when deselecting all solutions', async () => { - const { getByTestId } = render( - - - - ); - - expect(useGetCasesMock).toHaveBeenCalledWith({ - filterOptions: { - search: '', - searchFields: ['title', 'description'], - severity: [], - reporters: [], - status: [], - tags: [], - assignees: [], - owner: ['securitySolution', 'observability'], - category: [], - customFields: {}, - }, - queryParams: DEFAULT_QUERY_PARAMS, - }); - - userEvent.click(getByTestId('options-filter-popover-button-owner')); - - await waitForEuiPopoverOpen(); - - userEvent.click( - getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`), - undefined, - { - skipPointerEventsCheck: true, - } - ); - - expect(useGetCasesMock).toBeCalledWith({ - filterOptions: { - search: '', - searchFields: ['title', 'description'], - severity: [], - reporters: [], - status: [], - tags: [], - assignees: [], - owner: ['securitySolution'], - category: [], - customFields: {}, - }, - queryParams: DEFAULT_QUERY_PARAMS, - }); - - userEvent.click( - getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`), - undefined, - { - skipPointerEventsCheck: true, - } - ); - - expect(useGetCasesMock).toHaveBeenLastCalledWith({ - filterOptions: { - search: '', - searchFields: ['title', 'description'], - severity: [], - reporters: [], - status: [], - tags: [], - assignees: [], - owner: ['securitySolution', 'observability'], - category: [], - customFields: {}, - }, - queryParams: DEFAULT_QUERY_PARAMS, - }); - }); - it('should hide the solutions filter if the owner is provided', async () => { const { queryByTestId } = render( @@ -730,30 +653,6 @@ describe('AllCasesListGeneric', () => { expect(queryByTestId('options-filter-popover-button-owner')).toBeFalsy(); }); - - it('should call useGetCases with the correct owner on initial render', async () => { - render( - - - - ); - - expect(useGetCasesMock).toHaveBeenCalledWith({ - filterOptions: { - search: '', - searchFields: ['title', 'description'], - severity: [], - reporters: [], - status: [], - tags: [], - assignees: [], - owner: ['securitySolution'], - category: [], - customFields: {}, - }, - queryParams: DEFAULT_QUERY_PARAMS, - }); - }); }); describe('Actions', () => { @@ -1102,7 +1001,6 @@ describe('AllCasesListGeneric', () => { expect(useGetCasesMock).toHaveBeenLastCalledWith({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, - owner: [SECURITY_SOLUTION_OWNER], assignees: [], }, queryParams: DEFAULT_QUERY_PARAMS, 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 a997e25a59f2d..449f7a190f6f0 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 @@ -63,10 +63,10 @@ export const AllCasesList = React.memo( const isLoading = useIsLoadingCases(); const hasOwner = !!owner.length; + const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); const initialFilterOptions = { ...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: [firstAvailableStatus] }), - owner: hasOwner ? owner : availableSolutions, }; const { queryParams, setQueryParams, filterOptions, setFilterOptions } = useAllCasesState( @@ -210,7 +210,6 @@ export const AllCasesList = React.memo( availableSolutions={hasOwner ? [] : availableSolutions} hiddenStatuses={hiddenStatuses} onCreateCasePressed={onCreateCasePressed} - initialFilterOptions={initialFilterOptions} isSelectorView={isSelectorView} isLoading={isLoadingCurrentUserProfile} currentUserProfile={currentUserProfile} diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx index 69c757e354b86..1118a633c479f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.test.tsx @@ -100,7 +100,7 @@ describe('SolutionFilter ', () => { expect(onChange).toHaveBeenCalledWith({ filterId: 'owner', - selectedOptionKeys: [solutions[0]], + selectedOptionKeys: [], }); }); }); @@ -168,7 +168,7 @@ describe('SolutionFilter ', () => { expect(onChange).toHaveBeenCalledWith({ filterId: 'owner', - selectedOptionKeys: [solutions[0], solutions[1]], + selectedOptionKeys: [], }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx index 6f4eabddc0f8f..f2002e4c7899b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx @@ -43,37 +43,6 @@ export const SolutionFilterComponent = ({ const options = mapToMultiSelectOption(hasOwner ? owner : availableSolutions); const solutions = availableSolutions.map((solution) => mapToReadableSolutionName(solution)); - /** - * If the user selects and deselects all solutions then the owner is set to an empty array. - * This results in fetching all cases the user has access to including - * the ones with read access. We want to show only the cases the user has full access to. - * For that reason we fallback to availableSolutions if the owner is empty. - * - * If the consumer of cases has passed an owner we fallback to the provided owner - */ - const _onChange = ({ - filterId, - selectedOptionKeys: newOptions, - }: { - filterId: string; - selectedOptionKeys: string[]; - }) => { - if (hasOwner) { - onChange({ - filterId, - selectedOptionKeys: newOptions.length === 0 ? owner : newOptions, - }); - } else { - onChange({ - filterId, - selectedOptionKeys: newOptions.length === 0 ? availableSolutions : newOptions, - }); - } - }; - - const selectedOptionsInFilter = - selectedOptionKeys.length === availableSolutions.length ? [] : selectedOptionKeys; - const renderOption = (option: EuiSelectableOption) => { const solution = solutions.find((solutionData) => solutionData.id === option.label) as Solution; return ( @@ -90,10 +59,10 @@ export const SolutionFilterComponent = ({ ); }; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx index 0b522f6b066c8..ba2ca2d5f363f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx @@ -7,7 +7,6 @@ import React from 'react'; -import type { FilterOptions } from '../../../../common/ui'; import type { CaseStatuses } from '../../../../common/types/domain'; import { MAX_TAGS_FILTER_LENGTH, MAX_CATEGORY_FILTER_LENGTH } from '../../../../common/constants'; import { MultiSelectFilter, mapToMultiSelectOption } from '../multi_select_filter'; @@ -28,7 +27,6 @@ interface UseFilterConfigProps { countOpenCases: number | null; currentUserProfile: CurrentUserProfile; hiddenStatuses?: CaseStatuses[]; - initialFilterOptions: Partial; isLoading: boolean; isSelectorView?: boolean; onFilterOptionsChange: FilterChangeHandler; @@ -44,7 +42,6 @@ export const getSystemFilterConfig = ({ countOpenCases, currentUserProfile, hiddenStatuses, - initialFilterOptions, isLoading, isSelectorView, onFilterOptionsChange, @@ -69,7 +66,7 @@ export const getSystemFilterConfig = ({ isAvailable: true, getEmptyOptions: () => { return { - severity: initialFilterOptions.severity || [], + severity: [], }; }, render: ({ filterOptions }: FilterConfigRenderParams) => ( @@ -86,7 +83,7 @@ export const getSystemFilterConfig = ({ isAvailable: true, getEmptyOptions: () => { return { - status: initialFilterOptions.status || [], + status: [], }; }, render: ({ filterOptions }: FilterConfigRenderParams) => ( @@ -107,7 +104,7 @@ export const getSystemFilterConfig = ({ isAvailable: caseAssignmentAuthorized && !isSelectorView, getEmptyOptions: () => { return { - assignees: initialFilterOptions.assignees || [], + assignees: [], }; }, render: ({ filterOptions }: FilterConfigRenderParams) => { @@ -128,7 +125,7 @@ export const getSystemFilterConfig = ({ isAvailable: true, getEmptyOptions: () => { return { - tags: initialFilterOptions.tags || [], + tags: [], }; }, render: ({ filterOptions }: FilterConfigRenderParams) => ( @@ -150,7 +147,7 @@ export const getSystemFilterConfig = ({ isAvailable: true, getEmptyOptions: () => { return { - category: initialFilterOptions.category || [], + category: [], }; }, render: ({ filterOptions }: FilterConfigRenderParams) => ( @@ -172,7 +169,7 @@ export const getSystemFilterConfig = ({ isAvailable: availableSolutions.length > 1, getEmptyOptions: () => { return { - owner: initialFilterOptions.owner || [], + owner: [], }; }, render: ({ filterOptions }: FilterConfigRenderParams) => ( @@ -195,7 +192,6 @@ export const useSystemFilterConfig = ({ countOpenCases, currentUserProfile, hiddenStatuses, - initialFilterOptions, isLoading, isSelectorView, onFilterOptionsChange, @@ -210,7 +206,6 @@ export const useSystemFilterConfig = ({ countOpenCases, currentUserProfile, hiddenStatuses, - initialFilterOptions, isLoading, isSelectorView, onFilterOptionsChange, diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index f4b93724ed4b9..0abed867a29b3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -262,6 +262,9 @@ describe('CasesTableFilters ', () => { describe('Solution filter', () => { it('shows Solution filter when provided more than 1 availableSolutions', () => { + appMockRender = createAppMockRenderer({ + owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], + }); appMockRender.render( { }); it('does not show Solution filter when provided less than 1 availableSolutions', () => { - appMockRender.render( - - ); + appMockRender = createAppMockRenderer({ + owner: [], + }); + appMockRender.render(); expect(screen.queryByTestId('options-filter-popover-button-owner')).not.toBeInTheDocument(); }); it('does not select a solution on initial render', () => { + appMockRender = createAppMockRenderer({ + owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], + }); appMockRender.render( { ); }); - it('should reset the filter setting all available solutions when deactivated', async () => { + it('should reset the filter when deactivated', async () => { + appMockRender = createAppMockRenderer({ + owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], + }); + + const overrideProps = { + ...props, + filterOptions: { + ...props.filterOptions, + owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], + }, + }; + appMockRender.render( ); @@ -306,8 +324,39 @@ describe('CasesTableFilters ', () => { expect(onFilterChanged).toHaveBeenCalledWith({ ...DEFAULT_FILTER_OPTIONS, + owner: [], + }); + }); + + it('should check all options when all options are selected', async () => { + appMockRender = createAppMockRenderer({ owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], }); + + const overrideProps = { + ...props, + filterOptions: { + ...props.filterOptions, + owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], + }, + }; + + appMockRender.render( + + ); + + userEvent.click(screen.getByRole('button', { name: 'Solution' })); + await waitForEuiPopoverOpen(); + + const allOptions = screen.getAllByRole('option'); + expect(allOptions).toHaveLength(2); + expect(allOptions[0]).toHaveAttribute('aria-checked', 'true'); + expect(allOptions[0]).toHaveTextContent('Security'); + expect(allOptions[1]).toHaveAttribute('aria-checked', 'true'); + expect(allOptions[1]).toHaveTextContent('Observability'); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index e9e4350b6619f..dbdc947418eca 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -28,7 +28,6 @@ export interface CasesTableFiltersProps { availableSolutions: string[]; isSelectorView?: boolean; onCreateCasePressed?: () => void; - initialFilterOptions: Partial; isLoading: boolean; currentUserProfile: CurrentUserProfile; filterOptions: FilterOptions; @@ -49,7 +48,6 @@ const CasesTableFiltersComponent = ({ availableSolutions, isSelectorView = false, onCreateCasePressed, - initialFilterOptions, isLoading, currentUserProfile, filterOptions, @@ -78,7 +76,6 @@ const CasesTableFiltersComponent = ({ countOpenCases, currentUserProfile, hiddenStatuses, - initialFilterOptions, isLoading, isSelectorView, onFilterOptionsChange, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index cd63e741327b5..72eb3326f199a 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -11,10 +11,11 @@ import { useGetCases } from './use_get_cases'; import * as api from './api'; import type { AppMockRenderer } from '../common/mock'; import { createAppMockRenderer } from '../common/mock'; -import { useToasts } from '../common/lib/kibana'; +import { useToasts } from '../common/lib/kibana/hooks'; +import { OWNERS } from '../../common/constants'; jest.mock('./api'); -jest.mock('../common/lib/kibana'); +jest.mock('../common/lib/kibana/hooks'); describe('useGetCases', () => { const abortCtrl = new AbortController(); @@ -24,8 +25,8 @@ describe('useGetCases', () => { let appMockRender: AppMockRenderer; beforeEach(() => { - appMockRender = createAppMockRenderer(); jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); }); it('calls getCases with correct arguments', async () => { @@ -33,9 +34,10 @@ describe('useGetCases', () => { const { waitForNextUpdate } = renderHook(() => useGetCases(), { wrapper: appMockRender.AppWrapper, }); + await waitForNextUpdate(); expect(spyOnGetCases).toBeCalledWith({ - filterOptions: { ...DEFAULT_FILTER_OPTIONS }, + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: ['securitySolution'] }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); @@ -46,6 +48,7 @@ describe('useGetCases', () => { spyOnGetCases.mockImplementation(() => { throw new Error('Something went wrong'); }); + const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); @@ -56,4 +59,97 @@ describe('useGetCases', () => { await waitForNextUpdate(); expect(addError).toHaveBeenCalled(); }); + + it('should set all owners when no owner is provided', async () => { + appMockRender = createAppMockRenderer({ owner: [] }); + + appMockRender.coreStart.application.capabilities = { + ...appMockRender.coreStart.application.capabilities, + observabilityCases: { + create_cases: true, + read_cases: true, + update_cases: true, + push_cases: true, + cases_connectors: true, + delete_cases: true, + cases_settings: true, + }, + securitySolutionCases: { + create_cases: true, + read_cases: true, + update_cases: true, + push_cases: true, + cases_connectors: true, + delete_cases: true, + cases_settings: true, + }, + }; + + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const { waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: appMockRender.AppWrapper, + }); + + await waitForNextUpdate(); + + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [...OWNERS] }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + + it('should set only the available owners when no owner is provided', async () => { + appMockRender = createAppMockRenderer({ owner: [] }); + const spyOnGetCases = jest.spyOn(api, 'getCases'); + + const { waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: appMockRender.AppWrapper, + }); + + await waitForNextUpdate(); + + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: ['cases'] }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + + it('should use the app owner when the filter options do not specify the owner', async () => { + appMockRender = createAppMockRenderer({ owner: ['observability'] }); + const spyOnGetCases = jest.spyOn(api, 'getCases'); + + const { waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: appMockRender.AppWrapper, + }); + + await waitForNextUpdate(); + + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: ['observability'] }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + + it('respects the owner in the filter options if provided', async () => { + appMockRender = createAppMockRenderer({ owner: ['observability'] }); + const spyOnGetCases = jest.spyOn(api, 'getCases'); + + const { waitForNextUpdate } = renderHook( + () => useGetCases({ filterOptions: { owner: ['my-owner'] } }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + await waitForNextUpdate(); + + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: ['my-owner'] }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index c182cfbfe3cc1..327f1a99cbe9b 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -13,6 +13,9 @@ import { useToasts } from '../common/lib/kibana'; import * as i18n from './translations'; import { getCases } from './api'; import type { ServerError } from '../types'; +import { useCasesContext } from '../components/cases_context/use_cases_context'; +import { useAvailableCasesOwners } from '../components/app/use_available_owners'; +import { getAllPermissionsExceptFrom } from '../utils/permissions'; export const initialData: CasesFindResponseUI = { cases: [], @@ -31,6 +34,17 @@ export const useGetCases = ( } = {} ): UseQueryResult => { const toasts = useToasts(); + const { owner } = useCasesContext(); + const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete')); + + const hasOwner = !!owner.length; + const initialOwner = hasOwner ? owner : availableSolutions; + + const ownerFilter = + params.filterOptions?.owner != null && params.filterOptions.owner.length > 0 + ? { owner: params.filterOptions.owner } + : { owner: initialOwner }; + return useQuery( casesQueriesKeys.cases(params), ({ signal }) => { @@ -38,6 +52,7 @@ export const useGetCases = ( filterOptions: { ...DEFAULT_FILTER_OPTIONS, ...(params.filterOptions ?? {}), + ...ownerFilter, }, queryParams: { ...DEFAULT_QUERY_PARAMS, diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 77e250cf838d0..61d588dc261ed 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -183,11 +183,16 @@ export function CasesTableServiceProvider( await casesCommon.selectFirstRowInAssigneesPopover(); }, - async filterByOwner(owner: string) { - await common.clickAndValidate( - 'options-filter-popover-button-owner', - `options-filter-popover-item-${owner}` - ); + async filterByOwner( + owner: string, + options: { popupAlreadyOpen: boolean } = { popupAlreadyOpen: false } + ) { + if (!options.popupAlreadyOpen) { + await common.clickAndValidate( + 'options-filter-popover-button-owner', + `options-filter-popover-item-${owner}` + ); + } await testSubjects.click(`options-filter-popover-item-${owner}`); }, diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index cb9dffb548888..fc313fc502fd8 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -318,6 +318,22 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { } }); + it('filters with multiple selection', async () => { + await openModal(); + + let popupAlreadyOpen = false; + for (const [owner] of createdCases.entries()) { + await cases.casesTable.filterByOwner(owner, { popupAlreadyOpen }); + popupAlreadyOpen = true; + } + await cases.casesTable.waitForTableToFinishLoading(); + + for (const caseId of createdCases.values()) { + await testSubjects.existOrFail(`cases-table-row-${caseId}`); + } + await closeModal(); + }); + it('attaches correctly', async () => { for (const [owner, currentCaseId] of createdCases.entries()) { await openModal();