Skip to content

Commit 0dd21e5

Browse files
cnasikasumbopepatokibanamachine
authored
[Cases] Fix cases flaky tests (#176863)
## Summary With the help of @umbopepato, @js-jankisalvi, and @adcoelho, we realized that one possible source of flakiness is the `CasesContextProvider` that every test uses under the hood. In this PR I refactor the `CasesContextProvider` to render immediately its children and not wait for the `appId` and `appTitle` to be defined. I make them optional and make all changes needed in the code to accommodate the presence of an optional `appId`. Also, I refactored some components that I believed could introduce flakiness. ## Issues Fixes #175570 Fixes #175956 Fixes #175935 Fixes #175934 Fixes #175655 Fixes #176643 Fixes #175204 Flaky test runner: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5255, https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5261, https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5264, https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5267 ### 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 scenario ### 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) --------- Co-authored-by: Umberto Pepato <[email protected]> Co-authored-by: kibanamachine <[email protected]>
1 parent 162426c commit 0dd21e5

36 files changed

+578
-398
lines changed

x-pack/plugins/cases/public/common/apm/use_cases_transactions.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ describe('cases transactions', () => {
8080
);
8181
expect(mockAddLabels).toHaveBeenCalledWith({ alert_count: 3 });
8282
});
83+
84+
it('should not start any transactions if the app ID is not defined', () => {
85+
const { result } = renderUseCreateCaseWithAttachmentsTransaction();
86+
87+
result.current.startTransaction();
88+
89+
expect(mockStartTransaction).not.toHaveBeenCalled();
90+
expect(mockAddLabels).not.toHaveBeenCalled();
91+
});
8392
});
8493

8594
describe('useAddAttachmentToExistingCaseTransaction', () => {
@@ -104,5 +113,14 @@ describe('cases transactions', () => {
104113
);
105114
expect(mockAddLabels).toHaveBeenCalledWith({ alert_count: 3 });
106115
});
116+
117+
it('should not start any transactions if the app ID is not defined', () => {
118+
const { result } = renderUseAddAttachmentToExistingCaseTransaction();
119+
120+
result.current.startTransaction({ attachments: bulkAttachments });
121+
122+
expect(mockStartTransaction).not.toHaveBeenCalled();
123+
expect(mockAddLabels).not.toHaveBeenCalled();
124+
});
107125
});
108126
});

x-pack/plugins/cases/public/common/apm/use_cases_transactions.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const BULK_ADD_ATTACHMENT_TO_NEW_CASE = 'bulkAddAttachmentsToNewCase' as const;
1717
const ADD_ATTACHMENT_TO_EXISTING_CASE = 'addAttachmentToExistingCase' as const;
1818
const BULK_ADD_ATTACHMENT_TO_EXISTING_CASE = 'bulkAddAttachmentsToExistingCase' as const;
1919

20-
export type StartCreateCaseWithAttachmentsTransaction = (param: {
21-
appId: string;
20+
export type StartCreateCaseWithAttachmentsTransaction = (param?: {
21+
appId?: string;
2222
attachments?: CaseAttachmentsWithoutOwner;
2323
}) => Transaction | undefined;
2424

@@ -28,11 +28,17 @@ export const useCreateCaseWithAttachmentsTransaction = () => {
2828

2929
const startCreateCaseWithAttachmentsTransaction =
3030
useCallback<StartCreateCaseWithAttachmentsTransaction>(
31-
({ appId, attachments }) => {
31+
({ appId, attachments } = {}) => {
32+
if (!appId) {
33+
return;
34+
}
35+
3236
if (!attachments) {
3337
return startTransaction(`Cases [${appId}] ${CREATE_CASE}`);
3438
}
39+
3540
const alertCount = getAlertCount(attachments);
41+
3642
if (alertCount <= 1) {
3743
return startTransaction(`Cases [${appId}] ${ADD_ATTACHMENT_TO_NEW_CASE}`);
3844
}
@@ -48,7 +54,7 @@ export const useCreateCaseWithAttachmentsTransaction = () => {
4854
};
4955

5056
export type StartAddAttachmentToExistingCaseTransaction = (param: {
51-
appId: string;
57+
appId?: string;
5258
attachments: CaseAttachmentsWithoutOwner;
5359
}) => Transaction | undefined;
5460

@@ -59,13 +65,20 @@ export const useAddAttachmentToExistingCaseTransaction = () => {
5965
const startAddAttachmentToExistingCaseTransaction =
6066
useCallback<StartAddAttachmentToExistingCaseTransaction>(
6167
({ appId, attachments }) => {
68+
if (!appId) {
69+
return;
70+
}
71+
6272
const alertCount = getAlertCount(attachments);
73+
6374
if (alertCount <= 1) {
6475
return startTransaction(`Cases [${appId}] ${ADD_ATTACHMENT_TO_EXISTING_CASE}`);
6576
}
77+
6678
const transaction = startTransaction(
6779
`Cases [${appId}] ${BULK_ADD_ATTACHMENT_TO_EXISTING_CASE}`
6880
);
81+
6982
transaction?.addLabels({ alert_count: alertCount });
7083
return transaction;
7184
},

x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export const useKibana = jest.fn().mockReturnValue({
2525
export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http);
2626
export const useTimeZone = jest.fn();
2727
export const useDateFormat = jest.fn();
28-
export const useBasePath = jest.fn(() => '/test/base/path');
2928
export const useToasts = jest
3029
.fn()
3130
.mockReturnValue(notificationServiceMock.createStartContract().toasts);

x-pack/plugins/cases/public/common/lib/kibana/hooks.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ export const useTimeZone = (): string => {
3030
return timeZone === 'Browser' ? moment.tz.guess() : timeZone;
3131
};
3232

33-
export const useBasePath = (): string => useKibana().services.http.basePath.get();
34-
3533
export const useToasts = (): StartServices['notifications']['toasts'] =>
3634
useKibana().services.notifications.toasts;
3735

@@ -116,12 +114,12 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => {
116114
* Returns a full URL to the provided page path by using
117115
* kibana's `getUrlForApp()`
118116
*/
119-
export const useAppUrl = (appId: string) => {
117+
export const useAppUrl = (appId?: string) => {
120118
const { getUrlForApp } = useKibana().services.application;
121119

122120
const getAppUrl = useCallback(
123121
(options?: { deepLinkId?: string; path?: string; absolute?: boolean }) =>
124-
getUrlForApp(appId, options),
122+
getUrlForApp(appId ?? '', options),
125123
[appId, getUrlForApp]
126124
);
127125
return { getAppUrl };
@@ -131,7 +129,7 @@ export const useAppUrl = (appId: string) => {
131129
* Navigate to any app using kibana's `navigateToApp()`
132130
* or by url using `navigateToUrl()`
133131
*/
134-
export const useNavigateTo = (appId: string) => {
132+
export const useNavigateTo = (appId?: string) => {
135133
const { navigateToApp, navigateToUrl } = useKibana().services.application;
136134

137135
const navigateTo = useCallback(
@@ -144,7 +142,7 @@ export const useNavigateTo = (appId: string) => {
144142
if (url) {
145143
navigateToUrl(url);
146144
} else {
147-
navigateToApp(appId, options);
145+
navigateToApp(appId ?? '', options);
148146
}
149147
},
150148
[appId, navigateToApp, navigateToUrl]
@@ -156,7 +154,7 @@ export const useNavigateTo = (appId: string) => {
156154
* Returns navigateTo and getAppUrl navigation hooks
157155
*
158156
*/
159-
export const useNavigation = (appId: string) => {
157+
export const useNavigation = (appId?: string) => {
160158
const { navigateTo } = useNavigateTo(appId);
161159
const { getAppUrl } = useAppUrl(appId);
162160
return { navigateTo, getAppUrl };

x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import React from 'react';
99
import { BehaviorSubject } from 'rxjs';
1010

1111
import type { PublicAppInfo } from '@kbn/core/public';
12+
import { AppStatus } from '@kbn/core/public';
1213
import type { RecursivePartial } from '@elastic/eui/src/components/common';
1314
import { coreMock } from '@kbn/core/public/mocks';
1415
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@@ -52,7 +53,21 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
5253

5354
services.application.currentAppId$ = new BehaviorSubject<string>('testAppId');
5455
services.application.applications$ = new BehaviorSubject<Map<string, PublicAppInfo>>(
55-
new Map([['testAppId', { category: { label: 'Test' } } as unknown as PublicAppInfo]])
56+
new Map([
57+
[
58+
'testAppId',
59+
{
60+
id: 'testAppId',
61+
title: 'test-title',
62+
category: { id: 'test-label-id', label: 'Test' },
63+
status: AppStatus.accessible,
64+
visibleIn: ['globalSearch'],
65+
appRoute: `/app/some-id`,
66+
keywords: [],
67+
deepLinks: [],
68+
},
69+
],
70+
])
5671
);
5772

5873
services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({

x-pack/plugins/cases/public/common/mock/test_providers.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { ScopedFilesClient } from '@kbn/files-plugin/public';
1919
import { euiDarkVars } from '@kbn/ui-theme';
2020
import { I18nProvider } from '@kbn/i18n-react';
2121
import { createMockFilesClient } from '@kbn/shared-ux-file-mocks';
22-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
22+
import { QueryClient } from '@tanstack/react-query';
2323

2424
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
2525
import { FilesContext } from '@kbn/shared-ux-file-context';
@@ -108,10 +108,9 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
108108
permissions,
109109
getFilesClient,
110110
}}
111+
queryClient={queryClient}
111112
>
112-
<QueryClientProvider client={queryClient}>
113-
<FilesContext client={createMockFilesClient()}>{children}</FilesContext>
114-
</QueryClientProvider>
113+
<FilesContext client={createMockFilesClient()}>{children}</FilesContext>
115114
</CasesProvider>
116115
</MemoryRouter>
117116
</ThemeProvider>
@@ -191,8 +190,9 @@ export const createAppMockRenderer = ({
191190
releasePhase,
192191
getFilesClient,
193192
}}
193+
queryClient={queryClient}
194194
>
195-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
195+
{children}
196196
</CasesProvider>
197197
</MemoryRouter>
198198
</ThemeProvider>

x-pack/plugins/cases/public/common/use_cases_toast.test.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import { alertComment, basicComment, mockCase } from '../containers/mock';
1414
import React from 'react';
1515
import userEvent from '@testing-library/user-event';
1616
import type { SupportedCaseAttachment } from '../types';
17-
import { getByTestId } from '@testing-library/react';
17+
import { getByTestId, queryByTestId, screen } from '@testing-library/react';
1818
import { OWNER_INFO } from '../../common/constants';
19+
import { useCasesContext } from '../components/cases_context/use_cases_context';
1920

2021
jest.mock('./lib/kibana');
22+
jest.mock('../components/cases_context/use_cases_context');
2123

2224
const useToastsMock = useToasts as jest.Mock;
2325
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
26+
const useCasesContextMock = useCasesContext as jest.Mock;
2427

2528
describe('Use cases toast hook', () => {
2629
const successMock = jest.fn();
@@ -66,6 +69,8 @@ describe('Use cases toast hook', () => {
6669
getUrlForApp,
6770
navigateToUrl,
6871
};
72+
73+
useCasesContextMock.mockReturnValue({ appId: 'testAppId' });
6974
});
7075

7176
describe('showSuccessAttach', () => {
@@ -147,6 +152,7 @@ describe('Use cases toast hook', () => {
147152
describe('Toast content', () => {
148153
let appMockRender: AppMockRenderer;
149154
const onViewCaseClick = jest.fn();
155+
150156
beforeEach(() => {
151157
appMockRender = createAppMockRenderer();
152158
onViewCaseClick.mockReset();
@@ -213,9 +219,16 @@ describe('Use cases toast hook', () => {
213219
const result = appMockRender.render(
214220
<CaseToastSuccessContent onViewCaseClick={onViewCaseClick} />
215221
);
222+
216223
userEvent.click(result.getByTestId('toaster-content-case-view-link'));
217224
expect(onViewCaseClick).toHaveBeenCalled();
218225
});
226+
227+
it('hides the view case link when onViewCaseClick is not defined', () => {
228+
appMockRender.render(<CaseToastSuccessContent />);
229+
230+
expect(screen.queryByTestId('toaster-content-case-view-link')).not.toBeInTheDocument();
231+
});
219232
});
220233

221234
describe('Toast navigation', () => {
@@ -267,6 +280,31 @@ describe('Use cases toast hook', () => {
267280
path: '/mock-id',
268281
});
269282
});
283+
284+
it('does not navigates to a case if the appId is not defined', () => {
285+
useCasesContextMock.mockReturnValue({ appId: undefined });
286+
287+
const { result } = renderHook(
288+
() => {
289+
return useCasesToast();
290+
},
291+
{ wrapper: TestProviders }
292+
);
293+
294+
result.current.showSuccessAttach({
295+
theCase: { ...mockCase, owner: 'in-valid' },
296+
title: 'Custom title',
297+
});
298+
299+
const mockParams = successMock.mock.calls[0][0];
300+
const el = document.createElement('div');
301+
mockParams.text(el);
302+
const button = queryByTestId(el, 'toaster-content-case-view-link');
303+
304+
expect(button).toBeNull();
305+
expect(getUrlForApp).not.toHaveBeenCalled();
306+
expect(navigateToUrl).not.toHaveBeenCalled();
307+
});
270308
});
271309
});
272310

x-pack/plugins/cases/public/common/use_cases_toast.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,18 @@ export const useCasesToast = () => {
140140
? OWNER_INFO[theCase.owner].appId
141141
: appId;
142142

143-
const url = getUrlForApp(appIdToNavigateTo, {
144-
deepLinkId: 'cases',
145-
path: generateCaseViewPath({ detailName: theCase.id }),
146-
});
143+
const url =
144+
appIdToNavigateTo != null
145+
? getUrlForApp(appIdToNavigateTo, {
146+
deepLinkId: 'cases',
147+
path: generateCaseViewPath({ detailName: theCase.id }),
148+
})
149+
: null;
147150

148151
const onViewCaseClick = () => {
149-
navigateToUrl(url);
152+
if (url) {
153+
navigateToUrl(url);
154+
}
150155
};
151156

152157
const renderTitle = getToastTitle({ theCase, title, attachments });
@@ -157,7 +162,10 @@ export const useCasesToast = () => {
157162
iconType: 'check',
158163
title: toMountPoint(<Title>{renderTitle}</Title>),
159164
text: toMountPoint(
160-
<CaseToastSuccessContent content={renderContent} onViewCaseClick={onViewCaseClick} />
165+
<CaseToastSuccessContent
166+
content={renderContent}
167+
onViewCaseClick={url != null ? onViewCaseClick : undefined}
168+
/>
161169
),
162170
});
163171
},
@@ -186,7 +194,7 @@ export const CaseToastSuccessContent = ({
186194
onViewCaseClick,
187195
content,
188196
}: {
189-
onViewCaseClick: () => void;
197+
onViewCaseClick?: () => void;
190198
content?: string;
191199
}) => {
192200
return (
@@ -196,14 +204,16 @@ export const CaseToastSuccessContent = ({
196204
{content}
197205
</EuiTextStyled>
198206
) : null}
199-
<EuiButtonEmpty
200-
size="xs"
201-
flush="left"
202-
onClick={onViewCaseClick}
203-
data-test-subj="toaster-content-case-view-link"
204-
>
205-
{VIEW_CASE}
206-
</EuiButtonEmpty>
207+
{onViewCaseClick !== undefined ? (
208+
<EuiButtonEmpty
209+
size="xs"
210+
flush="left"
211+
onClick={onViewCaseClick}
212+
data-test-subj="toaster-content-case-view-link"
213+
>
214+
{VIEW_CASE}
215+
</EuiButtonEmpty>
216+
) : null}
207217
</>
208218
);
209219
};

x-pack/plugins/cases/public/components/add_comment/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const AddComment = React.memo(
7575
const [focusOnContext, setFocusOnContext] = useState(false);
7676
const { permissions, owner, appId } = useCasesContext();
7777
const { isLoading, mutate: createAttachments } = useCreateAttachments();
78-
const draftStorageKey = getMarkdownEditorStorageKey(appId, caseId, id);
78+
const draftStorageKey = getMarkdownEditorStorageKey({ appId, caseId, commentId: id });
7979

8080
const { form } = useForm<AddCommentFormSchema>({
8181
defaultValue: initialCommentValue,

x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { LOCAL_STORAGE_KEYS } from '../../../../common/constants';
1414
import type { FilterConfig, FilterConfigState } from './types';
1515
import { useCustomFieldsFilterConfig } from './use_custom_fields_filter_config';
1616
import { useCasesContext } from '../../cases_context/use_cases_context';
17-
import { deflattenCustomFieldKey, isFlattenCustomField } from '../utils';
17+
import { deflattenCustomFieldKey, getLocalStorageKey, isFlattenCustomField } from '../utils';
1818

1919
const mergeSystemAndCustomFieldConfigs = ({
2020
systemFilterConfig,
@@ -52,7 +52,7 @@ const shouldBeActive = ({
5252
const useActiveByFilterKeyState = ({ filterOptions }: { filterOptions: FilterOptions }) => {
5353
const { appId } = useCasesContext();
5454
const [activeByFilterKey, setActiveByFilterKey] = useLocalStorage<FilterConfigState[]>(
55-
`${appId}.${LOCAL_STORAGE_KEYS.casesTableFiltersConfig}`,
55+
getLocalStorageKey(LOCAL_STORAGE_KEYS.casesTableFiltersConfig, appId),
5656
[]
5757
);
5858

0 commit comments

Comments
 (0)