mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
fix: more merge issues
This commit is contained in:
parent
a1cc1384a1
commit
f3794b0ce9
2 changed files with 535 additions and 933 deletions
|
@ -6,16 +6,18 @@
|
|||
*/
|
||||
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { act, waitFor, renderHook } from '@testing-library/react';
|
||||
import type { TimelineArgs, UseTimelineEventsProps } from '.';
|
||||
import { initSortDefault, useTimelineEvents } from '.';
|
||||
import * as useTimelineEventsModule from '.';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { TimelineId } from '../../../common/types/timeline';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { mockTimelineData } from '../../common/mock';
|
||||
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
|
||||
import { useFetchNotes } from '../../notes/hooks/use_fetch_notes';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { getMockTimelineSearchSubscription } from '../../common/mock/mock_timeline_search_service';
|
||||
|
||||
const { initSortDefault, useTimelineEvents } = useTimelineEventsModule;
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
|
@ -31,10 +33,6 @@ jest.mock('../../notes/hooks/use_fetch_notes');
|
|||
const onLoadMock = jest.fn();
|
||||
const useFetchNotesMock = useFetchNotes as jest.Mock;
|
||||
|
||||
const mockEvents = mockTimelineData.slice(0, 10);
|
||||
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
jest.mock('../../common/lib/apm/use_track_http_request');
|
||||
jest.mock('../../common/hooks/use_experimental_features');
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
@ -46,55 +44,7 @@ jest.mock('../../common/lib/kibana', () => ({
|
|||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
capabilities: {
|
||||
siem: {
|
||||
crud: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
search: {
|
||||
search: jest.fn().mockImplementation((args) => {
|
||||
mockSearch();
|
||||
return {
|
||||
subscribe: jest.fn().mockImplementation(({ next }) => {
|
||||
const timeoutHandler = setTimeout(() => {
|
||||
next({
|
||||
isRunning: false,
|
||||
isPartial: false,
|
||||
inspect: {
|
||||
dsl: [],
|
||||
response: [],
|
||||
},
|
||||
edges: mockEvents.map((item) => ({ node: item })),
|
||||
pageInfo: {
|
||||
activePage: args.pagination.activePage,
|
||||
totalPages: 10,
|
||||
},
|
||||
rawResponse: {},
|
||||
totalCount: mockTimelineData.length,
|
||||
});
|
||||
}, 50);
|
||||
return {
|
||||
unsubscribe: jest.fn(() => {
|
||||
clearTimeout(timeoutHandler);
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addWarning: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
useKibana: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock;
|
||||
|
@ -112,7 +62,40 @@ mockUseRouteSpy.mockReturnValue([
|
|||
},
|
||||
]);
|
||||
|
||||
describe('useTimelineEvents', () => {
|
||||
const startDate: string = '2020-07-07T08:20:18.966Z';
|
||||
const endDate: string = '3000-01-01T00:00:00.000Z';
|
||||
const props: UseTimelineEventsProps = {
|
||||
dataViewId: 'data-view-id',
|
||||
endDate,
|
||||
id: TimelineId.active,
|
||||
indexNames: ['filebeat-*'],
|
||||
fields: ['@timestamp', 'event.kind'],
|
||||
filterQuery: '*',
|
||||
startDate,
|
||||
limit: 25,
|
||||
runtimeMappings: {},
|
||||
sort: initSortDefault,
|
||||
skip: false,
|
||||
};
|
||||
|
||||
const { mockTimelineSearchSubscription: mockSearchSubscription, mockSearchWithArgs: mockSearch } =
|
||||
getMockTimelineSearchSubscription();
|
||||
|
||||
const loadNextBatch = async (result: { current: [DataLoadingState, TimelineArgs] }) => {
|
||||
act(() => {
|
||||
result.current[1].loadNextBatch();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loadingMore);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
};
|
||||
|
||||
describe('useTimelineEventsHandler', () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -123,155 +106,136 @@ describe('useTimelineEvents', () => {
|
|||
useFetchNotesMock.mockReturnValue({
|
||||
onLoad: onLoadMock,
|
||||
});
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
capabilities: {
|
||||
siem: {
|
||||
crud: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
search: {
|
||||
search: mockSearchSubscription,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addWarning: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const startDate: string = '2020-07-07T08:20:18.966Z';
|
||||
const endDate: string = '3000-01-01T00:00:00.000Z';
|
||||
const props: UseTimelineEventsProps = {
|
||||
dataViewId: 'data-view-id',
|
||||
endDate,
|
||||
id: TimelineId.active,
|
||||
indexNames: ['filebeat-*'],
|
||||
fields: ['@timestamp', 'event.kind'],
|
||||
filterQuery: '',
|
||||
startDate,
|
||||
limit: 25,
|
||||
runtimeMappings: {},
|
||||
sort: initSortDefault,
|
||||
skip: false,
|
||||
};
|
||||
test('should init empty response', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loading,
|
||||
{
|
||||
events: [],
|
||||
id: TimelineId.active,
|
||||
inspect: expect.objectContaining({ dsl: [], response: [] }),
|
||||
loadNextBatch: expect.any(Function),
|
||||
pageInfo: expect.objectContaining({
|
||||
activePage: 0,
|
||||
querySize: 0,
|
||||
}),
|
||||
refetch: expect.any(Function),
|
||||
totalCount: -1,
|
||||
refreshedAt: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
test('should make events search request correctly', async () => {
|
||||
const { result } = renderHook<[DataLoadingState, TimelineArgs], UseTimelineEventsProps>(
|
||||
(args) => useTimelineEvents(args),
|
||||
{
|
||||
initialProps: props,
|
||||
}
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
expect(result.current[1].events).toHaveLength(25);
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loaded,
|
||||
{
|
||||
events: [],
|
||||
events: expect.any(Array),
|
||||
id: TimelineId.active,
|
||||
inspect: result.current[1].inspect,
|
||||
loadPage: result.current[1].loadPage,
|
||||
pageInfo: result.current[1].pageInfo,
|
||||
loadNextBatch: result.current[1].loadNextBatch,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
querySize: 25,
|
||||
},
|
||||
refetch: result.current[1].refetch,
|
||||
totalCount: -1,
|
||||
refreshedAt: 0,
|
||||
totalCount: 32,
|
||||
refreshedAt: result.current[1].refreshedAt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path query', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate, rerender } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, startDate: '', endDate: '' },
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({ ...props, startDate, endDate });
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loaded,
|
||||
{
|
||||
events: mockEvents,
|
||||
id: TimelineId.active,
|
||||
inspect: result.current[1].inspect,
|
||||
loadPage: result.current[1].loadPage,
|
||||
pageInfo: result.current[1].pageInfo,
|
||||
refetch: result.current[1].refetch,
|
||||
totalCount: 32,
|
||||
refreshedAt: result.current[1].refreshedAt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('should mock cache for active timeline when switching page', async () => {
|
||||
const { result, rerender } = renderHook<
|
||||
[DataLoadingState, TimelineArgs],
|
||||
UseTimelineEventsProps
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
});
|
||||
|
||||
test('Mock cache for active timeline when switching page', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate, rerender } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, startDate: '', endDate: '' },
|
||||
});
|
||||
mockUseRouteSpy.mockReturnValue([
|
||||
{
|
||||
pageName: SecurityPageName.timelines,
|
||||
detailName: undefined,
|
||||
tabName: undefined,
|
||||
search: '',
|
||||
pathName: '/timelines',
|
||||
},
|
||||
]);
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({ ...props, startDate, endDate });
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({ ...props, startDate, endDate });
|
||||
|
||||
mockUseRouteSpy.mockReturnValue([
|
||||
{
|
||||
pageName: SecurityPageName.timelines,
|
||||
detailName: undefined,
|
||||
tabName: undefined,
|
||||
search: '',
|
||||
pathName: '/timelines',
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loaded,
|
||||
{
|
||||
events: mockEvents,
|
||||
id: TimelineId.active,
|
||||
inspect: result.current[1].inspect,
|
||||
loadPage: result.current[1].loadPage,
|
||||
pageInfo: result.current[1].pageInfo,
|
||||
refetch: result.current[1].refetch,
|
||||
totalCount: 32,
|
||||
refreshedAt: result.current[1].refreshedAt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toEqual(DataLoadingState.loaded);
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(result.current[1].events).toHaveLength(25);
|
||||
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loaded,
|
||||
{
|
||||
events: expect.any(Array),
|
||||
id: TimelineId.active,
|
||||
inspect: result.current[1].inspect,
|
||||
loadNextBatch: result.current[1].loadNextBatch,
|
||||
pageInfo: result.current[1].pageInfo,
|
||||
refetch: result.current[1].refetch,
|
||||
totalCount: 32,
|
||||
refreshedAt: result.current[1].refreshedAt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Correlation pagination is calling search strategy when switching page', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate, rerender } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: {
|
||||
...props,
|
||||
language: 'eql',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
query: 'find it EQL',
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({
|
||||
const { result, rerender } = renderHook<
|
||||
[DataLoadingState, TimelineArgs],
|
||||
UseTimelineEventsProps
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: {
|
||||
...props,
|
||||
startDate,
|
||||
endDate,
|
||||
language: 'eql',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'category',
|
||||
|
@ -280,32 +244,102 @@ describe('useTimelineEvents', () => {
|
|||
query: 'find it EQL',
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitFor(() => new Promise((resolve) => resolve(null)));
|
||||
rerender({
|
||||
...props,
|
||||
startDate,
|
||||
endDate,
|
||||
language: 'eql',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
query: 'find it EQL',
|
||||
size: 100,
|
||||
},
|
||||
});
|
||||
// useEffect on params request
|
||||
await waitFor(() => new Promise((resolve) => resolve(null)));
|
||||
mockSearch.mockReset();
|
||||
act(() => {
|
||||
result.current[1].loadNextBatch();
|
||||
});
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
describe('error/invalid states', () => {
|
||||
const uniqueError = 'UNIQUE_ERROR';
|
||||
const onError = jest.fn();
|
||||
const mockSubscribeWithError = jest.fn(({ error }) => {
|
||||
error(uniqueError);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onError.mockClear();
|
||||
mockSubscribeWithError.mockClear();
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: {
|
||||
search: () => ({
|
||||
subscribe: jest.fn().mockImplementation(({ error }) => {
|
||||
const requestTimeout = setTimeout(() => {
|
||||
mockSubscribeWithError({ error });
|
||||
}, 100);
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
clearTimeout(requestTimeout);
|
||||
},
|
||||
};
|
||||
}),
|
||||
}),
|
||||
showError: onError,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should broadcast correct loading state when request throws error', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(DataLoadingState.loading);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onError).toHaveBeenCalledWith(uniqueError);
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
});
|
||||
test('should should not fire any request when indexName is empty', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, indexNames: [] },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
mockSearch.mockReset();
|
||||
result.current[1].loadPage(4);
|
||||
await waitForNextUpdate();
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should query again when a new field is added', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate, rerender } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, startDate: '', endDate: '' },
|
||||
describe('fields', () => {
|
||||
test('should query again when a new field is added', async () => {
|
||||
const { rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({ ...props, startDate, endDate });
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
mockSearch.mockClear();
|
||||
|
||||
rerender({
|
||||
|
@ -315,83 +349,332 @@ describe('useTimelineEvents', () => {
|
|||
fields: ['@timestamp', 'event.kind', 'event.category'],
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
test('should not query again when a field is removed', async () => {
|
||||
const { rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should not query again when a field is removed', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate, rerender } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, startDate: '', endDate: '' },
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({ ...props, startDate, endDate });
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
mockSearch.mockClear();
|
||||
|
||||
rerender({ ...props, startDate, endDate, fields: ['@timestamp'] });
|
||||
rerender({ ...props, fields: ['@timestamp'] });
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(0);
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0));
|
||||
});
|
||||
});
|
||||
|
||||
test('should not query again when a removed field is added back', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate, rerender } = renderHook<
|
||||
UseTimelineEventsProps,
|
||||
[DataLoadingState, TimelineArgs]
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, startDate: '', endDate: '' },
|
||||
test('should not query again when a removed field is added back', async () => {
|
||||
const { rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
rerender({ ...props, startDate, endDate });
|
||||
// useEffect on params request
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
mockSearch.mockClear();
|
||||
|
||||
// remove `event.kind` from default fields
|
||||
rerender({ ...props, startDate, endDate, fields: ['@timestamp'] });
|
||||
rerender({ ...props, fields: ['@timestamp'] });
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(0);
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0));
|
||||
|
||||
// request default Fields
|
||||
rerender({ ...props, startDate, endDate });
|
||||
rerender({ ...props });
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(0);
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0));
|
||||
});
|
||||
});
|
||||
|
||||
test('should return the combined list of events for all the pages when multiple pages are queried', async () => {
|
||||
await act(async () => {
|
||||
describe('batching', () => {
|
||||
test('should broadcast correct loading state based on the batch being fetched', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events).toHaveLength(10);
|
||||
});
|
||||
|
||||
result.current[1].loadPage(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toEqual(DataLoadingState.loadingMore);
|
||||
expect(result.current[0]).toBe(DataLoadingState.loading);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events).toHaveLength(20);
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current[1].loadNextBatch();
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(DataLoadingState.loadingMore);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
});
|
||||
|
||||
test('should request incremental batches when next batch has been requested', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 1, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 2, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should fetch new columns data for the all the batches ', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
|
||||
////////
|
||||
// fetch 2 more batches before requesting new column
|
||||
////////
|
||||
await loadNextBatch(result);
|
||||
|
||||
await loadNextBatch(result);
|
||||
///////
|
||||
|
||||
rerender({ ...props, fields: [...props.fields, 'new_column'] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fields: ['@timestamp', 'event.kind', 'new_column'],
|
||||
pagination: { activePage: 0, querySize: 75 },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should reset batch to 0th when the data is `refetched`', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 1, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
act(() => {
|
||||
result.current[1].refetch();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should query all batches when new column is added', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 1, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
rerender({ ...props, fields: [...props.fields, 'new_column'] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 50 } })
|
||||
);
|
||||
});
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 2, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should combine batches correctly when new column is added', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, limit: 5 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(5);
|
||||
});
|
||||
|
||||
//////////////////////
|
||||
// Batch 2
|
||||
await loadNextBatch(result);
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(10);
|
||||
});
|
||||
//////////////////////
|
||||
|
||||
//////////////////////
|
||||
// Batch 3
|
||||
await loadNextBatch(result);
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(15);
|
||||
});
|
||||
//////////////////////
|
||||
|
||||
///////////////////////////////////////////
|
||||
// add new column
|
||||
// Fetch all 3 batches together
|
||||
rerender({ ...props, limit: 5, fields: [...props.fields, 'new_column'] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loadingMore);
|
||||
});
|
||||
|
||||
// should fetch all the records together
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(result.current[1].events.length).toBe(15);
|
||||
expect(result.current[1].pageInfo).toMatchObject({
|
||||
activePage: 0,
|
||||
querySize: 15,
|
||||
});
|
||||
});
|
||||
///////////////////////////////////////////
|
||||
|
||||
//////////////////////
|
||||
// subsequent batch should be fetched incrementally
|
||||
// Batch 4
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(20);
|
||||
expect(result.current[1].pageInfo).toMatchObject({
|
||||
activePage: 3,
|
||||
querySize: 5,
|
||||
});
|
||||
});
|
||||
//////////////////////
|
||||
|
||||
//////////////////////
|
||||
// Batch 5
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(25);
|
||||
expect(result.current[1].pageInfo).toMatchObject({
|
||||
activePage: 4,
|
||||
querySize: 5,
|
||||
});
|
||||
});
|
||||
//////////////////////
|
||||
});
|
||||
|
||||
test('should request 0th batch (refetch) when batchSize is changed', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, limit: 5 },
|
||||
});
|
||||
|
||||
//////////////////////
|
||||
// Batch 2
|
||||
await loadNextBatch(result);
|
||||
|
||||
//////////////////////
|
||||
// Batch 3
|
||||
await loadNextBatch(result);
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
// change the batch size
|
||||
rerender({ ...props, limit: 10 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 10 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct list of events ( 0th batch ) when batchSize is changed', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, limit: 5 },
|
||||
});
|
||||
|
||||
//////////////////////
|
||||
// Batch 2
|
||||
await loadNextBatch(result);
|
||||
|
||||
//////////////////////
|
||||
// Batch 3
|
||||
await loadNextBatch(result);
|
||||
|
||||
// change the batch size
|
||||
rerender({ ...props, limit: 10 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loading);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(result.current[1].events.length).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,681 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { act, waitFor, renderHook } from '@testing-library/react';
|
||||
import type { TimelineArgs, UseTimelineEventsProps } from '.';
|
||||
import * as useTimelineEventsModule from '.';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { TimelineId } from '../../../common/types/timeline';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
|
||||
import { useFetchNotes } from '../../notes/hooks/use_fetch_notes';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { getMockTimelineSearchSubscription } from '../../common/mock/mock_timeline_search_service';
|
||||
|
||||
const { initSortDefault, useTimelineEvents } = useTimelineEventsModule;
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../notes/hooks/use_fetch_notes');
|
||||
const onLoadMock = jest.fn();
|
||||
const useFetchNotesMock = useFetchNotes as jest.Mock;
|
||||
|
||||
jest.mock('../../common/lib/apm/use_track_http_request');
|
||||
jest.mock('../../common/hooks/use_experimental_features');
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
||||
jest.mock('../../common/lib/kibana', () => ({
|
||||
useToasts: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
useKibana: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock;
|
||||
jest.mock('../../common/utils/route/use_route_spy', () => ({
|
||||
useRouteSpy: jest.fn(),
|
||||
}));
|
||||
|
||||
mockUseRouteSpy.mockReturnValue([
|
||||
{
|
||||
pageName: SecurityPageName.overview,
|
||||
detailName: undefined,
|
||||
tabName: undefined,
|
||||
search: '',
|
||||
pathName: '/overview',
|
||||
},
|
||||
]);
|
||||
|
||||
const startDate: string = '2020-07-07T08:20:18.966Z';
|
||||
const endDate: string = '3000-01-01T00:00:00.000Z';
|
||||
const props: UseTimelineEventsProps = {
|
||||
dataViewId: 'data-view-id',
|
||||
endDate,
|
||||
id: TimelineId.active,
|
||||
indexNames: ['filebeat-*'],
|
||||
fields: ['@timestamp', 'event.kind'],
|
||||
filterQuery: '*',
|
||||
startDate,
|
||||
limit: 25,
|
||||
runtimeMappings: {},
|
||||
sort: initSortDefault,
|
||||
skip: false,
|
||||
};
|
||||
|
||||
const { mockTimelineSearchSubscription: mockSearchSubscription, mockSearchWithArgs: mockSearch } =
|
||||
getMockTimelineSearchSubscription();
|
||||
|
||||
const loadNextBatch = async (result: { current: [DataLoadingState, TimelineArgs] }) => {
|
||||
act(() => {
|
||||
result.current[1].loadNextBatch();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loadingMore);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
};
|
||||
|
||||
describe('useTimelineEventsHandler', () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||
|
||||
beforeEach(() => {
|
||||
mockSearch.mockReset();
|
||||
useFetchNotesMock.mockClear();
|
||||
onLoadMock.mockClear();
|
||||
|
||||
useFetchNotesMock.mockReturnValue({
|
||||
onLoad: onLoadMock,
|
||||
});
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
capabilities: {
|
||||
siem: {
|
||||
crud: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
search: {
|
||||
search: mockSearchSubscription,
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
toasts: {
|
||||
addWarning: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should init empty response', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loading,
|
||||
{
|
||||
events: [],
|
||||
id: TimelineId.active,
|
||||
inspect: expect.objectContaining({ dsl: [], response: [] }),
|
||||
loadNextBatch: expect.any(Function),
|
||||
pageInfo: expect.objectContaining({
|
||||
activePage: 0,
|
||||
querySize: 0,
|
||||
}),
|
||||
refetch: expect.any(Function),
|
||||
totalCount: -1,
|
||||
refreshedAt: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should make events search request correctly', async () => {
|
||||
const { result } = renderHook<[DataLoadingState, TimelineArgs], UseTimelineEventsProps>(
|
||||
(args) => useTimelineEvents(args),
|
||||
{
|
||||
initialProps: props,
|
||||
}
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
expect(result.current[1].events).toHaveLength(25);
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loaded,
|
||||
{
|
||||
events: expect.any(Array),
|
||||
id: TimelineId.active,
|
||||
inspect: result.current[1].inspect,
|
||||
loadNextBatch: result.current[1].loadNextBatch,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
querySize: 25,
|
||||
},
|
||||
refetch: result.current[1].refetch,
|
||||
totalCount: 32,
|
||||
refreshedAt: result.current[1].refreshedAt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should mock cache for active timeline when switching page', async () => {
|
||||
const { result, rerender } = renderHook<
|
||||
[DataLoadingState, TimelineArgs],
|
||||
UseTimelineEventsProps
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
mockUseRouteSpy.mockReturnValue([
|
||||
{
|
||||
pageName: SecurityPageName.timelines,
|
||||
detailName: undefined,
|
||||
tabName: undefined,
|
||||
search: '',
|
||||
pathName: '/timelines',
|
||||
},
|
||||
]);
|
||||
|
||||
rerender({ ...props, startDate, endDate });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toEqual(DataLoadingState.loaded);
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(result.current[1].events).toHaveLength(25);
|
||||
|
||||
expect(result.current).toEqual([
|
||||
DataLoadingState.loaded,
|
||||
{
|
||||
events: expect.any(Array),
|
||||
id: TimelineId.active,
|
||||
inspect: result.current[1].inspect,
|
||||
loadNextBatch: result.current[1].loadNextBatch,
|
||||
pageInfo: result.current[1].pageInfo,
|
||||
refetch: result.current[1].refetch,
|
||||
totalCount: 32,
|
||||
refreshedAt: result.current[1].refreshedAt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Correlation pagination is calling search strategy when switching page', async () => {
|
||||
const { result, rerender } = renderHook<
|
||||
[DataLoadingState, TimelineArgs],
|
||||
UseTimelineEventsProps
|
||||
>((args) => useTimelineEvents(args), {
|
||||
initialProps: {
|
||||
...props,
|
||||
language: 'eql',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
query: 'find it EQL',
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// useEffect on params request
|
||||
await waitFor(() => new Promise((resolve) => resolve(null)));
|
||||
rerender({
|
||||
...props,
|
||||
startDate,
|
||||
endDate,
|
||||
language: 'eql',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
query: 'find it EQL',
|
||||
size: 100,
|
||||
},
|
||||
});
|
||||
// useEffect on params request
|
||||
await waitFor(() => new Promise((resolve) => resolve(null)));
|
||||
mockSearch.mockReset();
|
||||
act(() => {
|
||||
result.current[1].loadNextBatch();
|
||||
});
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
describe('error/invalid states', () => {
|
||||
const uniqueError = 'UNIQUE_ERROR';
|
||||
const onError = jest.fn();
|
||||
const mockSubscribeWithError = jest.fn(({ error }) => {
|
||||
error(uniqueError);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onError.mockClear();
|
||||
mockSubscribeWithError.mockClear();
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: {
|
||||
search: () => ({
|
||||
subscribe: jest.fn().mockImplementation(({ error }) => {
|
||||
const requestTimeout = setTimeout(() => {
|
||||
mockSubscribeWithError({ error });
|
||||
}, 100);
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
clearTimeout(requestTimeout);
|
||||
},
|
||||
};
|
||||
}),
|
||||
}),
|
||||
showError: onError,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should broadcast correct loading state when request throws error', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(DataLoadingState.loading);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onError).toHaveBeenCalledWith(uniqueError);
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
});
|
||||
test('should should not fire any request when indexName is empty', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, indexNames: [] },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields', () => {
|
||||
test('should query again when a new field is added', async () => {
|
||||
const { rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
rerender({
|
||||
...props,
|
||||
startDate,
|
||||
endDate,
|
||||
fields: ['@timestamp', 'event.kind', 'event.category'],
|
||||
});
|
||||
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
test('should not query again when a field is removed', async () => {
|
||||
const { rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
mockSearch.mockClear();
|
||||
|
||||
rerender({ ...props, fields: ['@timestamp'] });
|
||||
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0));
|
||||
});
|
||||
test('should not query again when a removed field is added back', async () => {
|
||||
const { rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
mockSearch.mockClear();
|
||||
|
||||
// remove `event.kind` from default fields
|
||||
rerender({ ...props, fields: ['@timestamp'] });
|
||||
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0));
|
||||
|
||||
// request default Fields
|
||||
rerender({ ...props });
|
||||
|
||||
await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('batching', () => {
|
||||
test('should broadcast correct loading state based on the batch being fetched', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loading);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current[1].loadNextBatch();
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(DataLoadingState.loadingMore);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
});
|
||||
|
||||
test('should request incremental batches when next batch has been requested', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 1, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 2, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should fetch new columns data for the all the batches ', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
});
|
||||
|
||||
////////
|
||||
// fetch 2 more batches before requesting new column
|
||||
////////
|
||||
await loadNextBatch(result);
|
||||
|
||||
await loadNextBatch(result);
|
||||
///////
|
||||
|
||||
rerender({ ...props, fields: [...props.fields, 'new_column'] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fields: ['@timestamp', 'event.kind', 'new_column'],
|
||||
pagination: { activePage: 0, querySize: 75 },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should reset batch to 0th when the data is `refetched`', async () => {
|
||||
const { result } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 1, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
act(() => {
|
||||
result.current[1].refetch();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
expect(mockSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should query all batches when new column is added', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 1, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
rerender({ ...props, fields: [...props.fields, 'new_column'] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 50 } })
|
||||
);
|
||||
});
|
||||
mockSearch.mockClear();
|
||||
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 2, querySize: 25 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should combine batches correctly when new column is added', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, limit: 5 },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(5);
|
||||
});
|
||||
|
||||
//////////////////////
|
||||
// Batch 2
|
||||
await loadNextBatch(result);
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(10);
|
||||
});
|
||||
//////////////////////
|
||||
|
||||
//////////////////////
|
||||
// Batch 3
|
||||
await loadNextBatch(result);
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(15);
|
||||
});
|
||||
//////////////////////
|
||||
|
||||
///////////////////////////////////////////
|
||||
// add new column
|
||||
// Fetch all 3 batches together
|
||||
rerender({ ...props, limit: 5, fields: [...props.fields, 'new_column'] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loadingMore);
|
||||
});
|
||||
|
||||
// should fetch all the records together
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(result.current[1].events.length).toBe(15);
|
||||
expect(result.current[1].pageInfo).toMatchObject({
|
||||
activePage: 0,
|
||||
querySize: 15,
|
||||
});
|
||||
});
|
||||
///////////////////////////////////////////
|
||||
|
||||
//////////////////////
|
||||
// subsequent batch should be fetched incrementally
|
||||
// Batch 4
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(20);
|
||||
expect(result.current[1].pageInfo).toMatchObject({
|
||||
activePage: 3,
|
||||
querySize: 5,
|
||||
});
|
||||
});
|
||||
//////////////////////
|
||||
|
||||
//////////////////////
|
||||
// Batch 5
|
||||
await loadNextBatch(result);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[1].events.length).toBe(25);
|
||||
expect(result.current[1].pageInfo).toMatchObject({
|
||||
activePage: 4,
|
||||
querySize: 5,
|
||||
});
|
||||
});
|
||||
//////////////////////
|
||||
});
|
||||
|
||||
test('should request 0th batch (refetch) when batchSize is changed', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, limit: 5 },
|
||||
});
|
||||
|
||||
//////////////////////
|
||||
// Batch 2
|
||||
await loadNextBatch(result);
|
||||
|
||||
//////////////////////
|
||||
// Batch 3
|
||||
await loadNextBatch(result);
|
||||
|
||||
mockSearch.mockClear();
|
||||
|
||||
// change the batch size
|
||||
rerender({ ...props, limit: 10 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(mockSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagination: { activePage: 0, querySize: 10 } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct list of events ( 0th batch ) when batchSize is changed', async () => {
|
||||
const { result, rerender } = renderHook((args) => useTimelineEvents(args), {
|
||||
initialProps: { ...props, limit: 5 },
|
||||
});
|
||||
|
||||
//////////////////////
|
||||
// Batch 2
|
||||
await loadNextBatch(result);
|
||||
|
||||
//////////////////////
|
||||
// Batch 3
|
||||
await loadNextBatch(result);
|
||||
|
||||
// change the batch size
|
||||
rerender({ ...props, limit: 10 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loading);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toBe(DataLoadingState.loaded);
|
||||
expect(result.current[1].events.length).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue