From 8f0c6a12e6ff35aecd99124e039c69df27b52969 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Fri, 29 Nov 2024 16:14:27 +0100 Subject: [PATCH 1/2] [Security Solution] Adds callback `onUpdatePageIndex` to get current `pageIndex` in Unified Data table (#201240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Handles resolution for - Notes fetching data for all Timeline Records which leads to performance issues. - https://github.com/elastic/kibana/issues/201330 ## Issue - Notes fetching data for all Timeline Records Currently, there was no way for consumer of `UnifiedDataGrid` to get the current `pageIndex`. Security Solution needs to get the current `pageIndex` so the items on the current page can be calculated. @elastic/kibana-data-discovery , please let us know if you have any opinion here. This results in notes being fetched for all Timeline Records which means minimum of 500 records and if user has queries 5000 records ( for example ), a request will be made to query notes for all those 5000 notes which leads to performance issue and sometimes error as shown below: ![image](https://github.com/user-attachments/assets/6fcfe05d-340c-4dcb-a273-5af53ed12945) ## 👨‍💻 Changes This adds attribute `pageIndex` to timeline state. ```javascript { "pageIndex": number } ``` `pageIndex` helps with getting the events for that particular page. ## 🟡 Caveat - Currently this `pageIndex` is shared between Query and EQL tabs which can lead to wonky behavior at time. - Additionally, as of now table maintains its own page index and consumer component cannot effect the `pageIndex` of the UnifiedDataGrid. (cherry picked from commit de9d5465df5900936991d79306cb2cbbe63f4623) # Conflicts: # src/platform/packages/shared/kbn-unified-data-table/src/components/data_table.test.tsx # src/platform/packages/shared/kbn-unified-data-table/src/components/data_table.tsx # x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx # x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx # x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx # x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx # x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx # x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx --- .../timeline/tabs/eql/index.test.tsx | 345 ++++++++++++++++++ .../timelines/containers/index.test.tsx | 9 +- 2 files changed, 349 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx new file mode 100644 index 0000000000000..a71f99715131e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx @@ -0,0 +1,345 @@ +/* + * 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 type { ComponentProps } from 'react'; +import React, { useEffect } from 'react'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import { createMockStore, mockGlobalState, mockTimelineData } from '../../../../../common/mock'; +import { TestProviders } from '../../../../../common/mock/test_providers'; + +import type { Props as EqlTabContentComponentProps } from '.'; +import EqlTabContentComponent from '.'; +import { TimelineId } from '../../../../../../common/types/timeline'; +import { useTimelineEvents } from '../../../../containers'; +import { useTimelineEventsDetails } from '../../../../containers/details'; +import { useSourcererDataView } from '../../../../../sourcerer/containers'; +import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import type { ExperimentalFeatures } from '../../../../../../common'; +import { allowedExperimentalValues } from '../../../../../../common'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import * as notesApi from '../../../../../notes/api/api'; +import { timelineActions } from '../../../../store'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; +import { defaultRowRenderers } from '../../body/renderers'; +import { useDispatch } from 'react-redux'; +import { TimelineTabs } from '@kbn/securitysolution-data-table'; + +const SPECIAL_TEST_TIMEOUT = 30000; + +jest.mock('../../../../containers', () => ({ + useTimelineEvents: jest.fn(), +})); +jest.mock('../../../../containers/details', () => ({ + useTimelineEventsDetails: jest.fn(), +})); +jest.mock('../../../fields_browser', () => ({ + useFieldBrowserOptions: jest.fn(), +})); + +jest.mock('../../../../../sourcerer/containers'); +jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({ + useSignalHelpers: () => ({ signalIndexNeedsInit: false }), +})); + +jest.mock('../../../../../common/hooks/use_experimental_features'); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); + +jest.mock('../../../../../common/lib/kibana'); + +let useTimelineEventsMock = jest.fn(); + +const loadPageMock = jest.fn(); + +const mockState = { + ...structuredClone(mockGlobalState), +}; +mockState.timeline.timelineById[TimelineId.test].activeTab = TimelineTabs.eql; + +const TestComponent = (props: Partial>) => { + const testComponentDefaultProps: ComponentProps = { + timelineId: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, + }; + + const dispatch = useDispatch(); + + useEffect(() => { + // Unified field list can be a culprit for long load times, so we wait for the timeline to be interacted with to load + dispatch(timelineActions.showTimeline({ id: TimelineId.test, show: true })); + + // populating timeline so that it is not blank + dispatch( + timelineActions.updateEqlOptions({ + id: TimelineId.test, + field: 'query', + value: 'any where true', + }) + ); + }, [dispatch]); + + return ; +}; + +describe('EQL Tab', () => { + const props = {} as EqlTabContentComponentProps; + + beforeAll(() => { + // https://github.com/atlassian/react-beautiful-dnd/blob/4721a518356f72f1dac45b5fd4ee9d466aa2996b/docs/guides/setup-problem-detection-and-error-recovery.md#disable-logging + Object.defineProperty(window, '__@hello-pangea/dnd-disable-dev-warnings', { + get() { + return true; + }, + }); + }); + + beforeEach(() => { + useTimelineEventsMock = jest.fn(() => [ + false, + { + events: mockTimelineData.slice(0, 1), + pageInfo: { + activePage: 0, + totalPages: 10, + }, + }, + ]); + (useTimelineEvents as jest.Mock).mockImplementation(useTimelineEventsMock); + (useTimelineEventsDetails as jest.Mock).mockReturnValue([false, {}]); + + (useSourcererDataView as jest.Mock).mockReturnValue(mockSourcererScope); + + (useIsExperimentalFeatureEnabledMock as jest.Mock).mockImplementation( + (feature: keyof ExperimentalFeatures) => { + return allowedExperimentalValues[feature]; + } + ); + + HTMLElement.prototype.getBoundingClientRect = jest.fn(() => { + return { + width: 1000, + height: 1000, + x: 0, + y: 0, + } as DOMRect; + }); + }); + + describe('rendering', () => { + const fetchNotesMock = jest.spyOn(notesApi, 'fetchNotesByDocumentIds'); + test('should render the timeline table', async () => { + fetchNotesMock.mockImplementation(jest.fn()); + render( + + + + ); + + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + }); + + test('it renders the timeline column headers', async () => { + render( + + + + ); + + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + }); + + test('should render correct placeholder when there are not results', async () => { + (useTimelineEvents as jest.Mock).mockReturnValue([ + false, + { + events: [], + pageInfo: { + activePage: 0, + totalPages: 10, + }, + }, + ]); + + render( + + + + ); + + expect(await screen.findByText('No results found')).toBeVisible(); + }); + + describe('pagination', () => { + beforeEach(() => { + // pagination tests need more than 1 record so here + // we return 5 records instead of just 1. + useTimelineEventsMock = jest.fn(() => [ + false, + { + events: structuredClone(mockTimelineData.slice(0, 5)), + pageInfo: { + activePage: 0, + totalPages: 5, + }, + refreshedAt: Date.now(), + /* + * `totalCount` could be any number w.r.t this test + * and actually means total hits on elastic search + * and not the fecthed number of records. + * + * This helps in testing `sampleSize` and `loadMore` + */ + totalCount: 50, + loadPage: loadPageMock, + }, + ]); + + (useTimelineEvents as jest.Mock).mockImplementation(useTimelineEventsMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it( + 'should load notes for current page only', + async () => { + const mockStateWithNoteInTimeline = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [TimelineId.test]: { + ...mockGlobalState.timeline.timelineById[TimelineId.test], + /* 1 record for each page */ + activeTab: TimelineTabs.eql, + itemsPerPage: 1, + itemsPerPageOptions: [1, 2, 3, 4, 5], + savedObjectId: 'timeline-1', // match timelineId in mocked notes data + pinnedEventIds: { '1': true }, + }, + }, + }, + }; + + render( + + + + ); + + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + + expect(screen.getByTestId('pagination-button-previous')).toBeVisible(); + + expect(screen.getByTestId('pagination-button-0')).toHaveAttribute('aria-current', 'true'); + expect(fetchNotesMock).toHaveBeenCalledWith(['1']); + + // Page : 2 + + fetchNotesMock.mockClear(); + expect(screen.getByTestId('pagination-button-1')).toBeVisible(); + + fireEvent.click(screen.getByTestId('pagination-button-1')); + + await waitFor(() => { + expect(screen.getByTestId('pagination-button-1')).toHaveAttribute( + 'aria-current', + 'true' + ); + + expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [mockTimelineData[1]._id]); + }); + + // Page : 3 + + fetchNotesMock.mockClear(); + expect(screen.getByTestId('pagination-button-2')).toBeVisible(); + fireEvent.click(screen.getByTestId('pagination-button-2')); + + await waitFor(() => { + expect(screen.getByTestId('pagination-button-2')).toHaveAttribute( + 'aria-current', + 'true' + ); + + expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [mockTimelineData[2]._id]); + }); + }, + SPECIAL_TEST_TIMEOUT + ); + + it( + 'should load notes for correct page size', + async () => { + const mockStateWithNoteInTimeline = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [TimelineId.test]: { + ...mockGlobalState.timeline.timelineById[TimelineId.test], + /* 1 record for each page */ + itemsPerPage: 1, + pageIndex: 0, + itemsPerPageOptions: [1, 2, 3, 4, 5], + savedObjectId: 'timeline-1', // match timelineId in mocked notes data + pinnedEventIds: { '1': true }, + }, + }, + }, + }; + + render( + + + + ); + + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + + expect(screen.getByTestId('pagination-button-previous')).toBeVisible(); + + expect(screen.getByTestId('pagination-button-0')).toHaveAttribute('aria-current', 'true'); + expect(screen.getByTestId('tablePaginationPopoverButton')).toHaveTextContent( + 'Rows per page: 1' + ); + fireEvent.click(screen.getByTestId('tablePaginationPopoverButton')); + + await waitFor(() => { + expect(screen.getByTestId('tablePagination-2-rows')).toBeVisible(); + }); + + fetchNotesMock.mockClear(); + fireEvent.click(screen.getByTestId('tablePagination-2-rows')); + + await waitFor(() => { + expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [ + mockTimelineData[0]._id, + mockTimelineData[1]._id, + ]); + }); + }, + SPECIAL_TEST_TIMEOUT + ); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx index 40c6e89478e2d..546146f9a39ab 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -170,10 +170,9 @@ describe('useTimelineEvents', () => { >((args) => useTimelineEvents(args), { initialProps: { ...props, startDate: '', endDate: '' }, }); - // useEffect on params request await waitFor(() => new Promise((resolve) => resolve(null))); - rerender({ ...props, startDate, endDate }); + rerender({ ...props, startDate: '', endDate: '' }); // useEffect on params request await waitFor(() => { expect(mockSearch).toHaveBeenCalledTimes(2); @@ -286,7 +285,7 @@ describe('useTimelineEvents', () => { // useEffect on params request await waitFor(() => new Promise((resolve) => resolve(null))); - expect(mockSearch).toHaveBeenCalledTimes(2); + expect(mockSearch).toHaveBeenCalledTimes(1); mockSearch.mockClear(); rerender({ @@ -310,7 +309,7 @@ describe('useTimelineEvents', () => { // useEffect on params request await waitFor(() => new Promise((resolve) => resolve(null))); - expect(mockSearch).toHaveBeenCalledTimes(2); + expect(mockSearch).toHaveBeenCalledTimes(1); mockSearch.mockClear(); rerender({ ...props, startDate, endDate, fields: ['@timestamp'] }); @@ -329,7 +328,7 @@ describe('useTimelineEvents', () => { // useEffect on params request await waitFor(() => new Promise((resolve) => resolve(null))); - expect(mockSearch).toHaveBeenCalledTimes(2); + expect(mockSearch).toHaveBeenCalledTimes(1); mockSearch.mockClear(); // remove `event.kind` from default fields From 5038b8b019b70bba3f3e5bdf9d651b0ec9b1d019 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Thu, 6 Feb 2025 17:07:10 +0100 Subject: [PATCH 2/2] fix: merge issue. remove obsolete file --- .../timeline/tabs/eql/index.test.tsx | 345 ------------------ 1 file changed, 345 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx deleted file mode 100644 index a71f99715131e..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx +++ /dev/null @@ -1,345 +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 type { ComponentProps } from 'react'; -import React, { useEffect } from 'react'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import { createMockStore, mockGlobalState, mockTimelineData } from '../../../../../common/mock'; -import { TestProviders } from '../../../../../common/mock/test_providers'; - -import type { Props as EqlTabContentComponentProps } from '.'; -import EqlTabContentComponent from '.'; -import { TimelineId } from '../../../../../../common/types/timeline'; -import { useTimelineEvents } from '../../../../containers'; -import { useTimelineEventsDetails } from '../../../../containers/details'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; -import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import type { ExperimentalFeatures } from '../../../../../../common'; -import { allowedExperimentalValues } from '../../../../../../common'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import * as notesApi from '../../../../../notes/api/api'; -import { timelineActions } from '../../../../store'; -import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; -import { defaultRowRenderers } from '../../body/renderers'; -import { useDispatch } from 'react-redux'; -import { TimelineTabs } from '@kbn/securitysolution-data-table'; - -const SPECIAL_TEST_TIMEOUT = 30000; - -jest.mock('../../../../containers', () => ({ - useTimelineEvents: jest.fn(), -})); -jest.mock('../../../../containers/details', () => ({ - useTimelineEventsDetails: jest.fn(), -})); -jest.mock('../../../fields_browser', () => ({ - useFieldBrowserOptions: jest.fn(), -})); - -jest.mock('../../../../../sourcerer/containers'); -jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({ - useSignalHelpers: () => ({ signalIndexNeedsInit: false }), -})); - -jest.mock('../../../../../common/hooks/use_experimental_features'); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -jest.mock('../../../../../common/lib/kibana'); - -let useTimelineEventsMock = jest.fn(); - -const loadPageMock = jest.fn(); - -const mockState = { - ...structuredClone(mockGlobalState), -}; -mockState.timeline.timelineById[TimelineId.test].activeTab = TimelineTabs.eql; - -const TestComponent = (props: Partial>) => { - const testComponentDefaultProps: ComponentProps = { - timelineId: TimelineId.test, - renderCellValue: DefaultCellRenderer, - rowRenderers: defaultRowRenderers, - }; - - const dispatch = useDispatch(); - - useEffect(() => { - // Unified field list can be a culprit for long load times, so we wait for the timeline to be interacted with to load - dispatch(timelineActions.showTimeline({ id: TimelineId.test, show: true })); - - // populating timeline so that it is not blank - dispatch( - timelineActions.updateEqlOptions({ - id: TimelineId.test, - field: 'query', - value: 'any where true', - }) - ); - }, [dispatch]); - - return ; -}; - -describe('EQL Tab', () => { - const props = {} as EqlTabContentComponentProps; - - beforeAll(() => { - // https://github.com/atlassian/react-beautiful-dnd/blob/4721a518356f72f1dac45b5fd4ee9d466aa2996b/docs/guides/setup-problem-detection-and-error-recovery.md#disable-logging - Object.defineProperty(window, '__@hello-pangea/dnd-disable-dev-warnings', { - get() { - return true; - }, - }); - }); - - beforeEach(() => { - useTimelineEventsMock = jest.fn(() => [ - false, - { - events: mockTimelineData.slice(0, 1), - pageInfo: { - activePage: 0, - totalPages: 10, - }, - }, - ]); - (useTimelineEvents as jest.Mock).mockImplementation(useTimelineEventsMock); - (useTimelineEventsDetails as jest.Mock).mockReturnValue([false, {}]); - - (useSourcererDataView as jest.Mock).mockReturnValue(mockSourcererScope); - - (useIsExperimentalFeatureEnabledMock as jest.Mock).mockImplementation( - (feature: keyof ExperimentalFeatures) => { - return allowedExperimentalValues[feature]; - } - ); - - HTMLElement.prototype.getBoundingClientRect = jest.fn(() => { - return { - width: 1000, - height: 1000, - x: 0, - y: 0, - } as DOMRect; - }); - }); - - describe('rendering', () => { - const fetchNotesMock = jest.spyOn(notesApi, 'fetchNotesByDocumentIds'); - test('should render the timeline table', async () => { - fetchNotesMock.mockImplementation(jest.fn()); - render( - - - - ); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - }); - - test('it renders the timeline column headers', async () => { - render( - - - - ); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - }); - - test('should render correct placeholder when there are not results', async () => { - (useTimelineEvents as jest.Mock).mockReturnValue([ - false, - { - events: [], - pageInfo: { - activePage: 0, - totalPages: 10, - }, - }, - ]); - - render( - - - - ); - - expect(await screen.findByText('No results found')).toBeVisible(); - }); - - describe('pagination', () => { - beforeEach(() => { - // pagination tests need more than 1 record so here - // we return 5 records instead of just 1. - useTimelineEventsMock = jest.fn(() => [ - false, - { - events: structuredClone(mockTimelineData.slice(0, 5)), - pageInfo: { - activePage: 0, - totalPages: 5, - }, - refreshedAt: Date.now(), - /* - * `totalCount` could be any number w.r.t this test - * and actually means total hits on elastic search - * and not the fecthed number of records. - * - * This helps in testing `sampleSize` and `loadMore` - */ - totalCount: 50, - loadPage: loadPageMock, - }, - ]); - - (useTimelineEvents as jest.Mock).mockImplementation(useTimelineEventsMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it( - 'should load notes for current page only', - async () => { - const mockStateWithNoteInTimeline = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - [TimelineId.test]: { - ...mockGlobalState.timeline.timelineById[TimelineId.test], - /* 1 record for each page */ - activeTab: TimelineTabs.eql, - itemsPerPage: 1, - itemsPerPageOptions: [1, 2, 3, 4, 5], - savedObjectId: 'timeline-1', // match timelineId in mocked notes data - pinnedEventIds: { '1': true }, - }, - }, - }, - }; - - render( - - - - ); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - expect(screen.getByTestId('pagination-button-previous')).toBeVisible(); - - expect(screen.getByTestId('pagination-button-0')).toHaveAttribute('aria-current', 'true'); - expect(fetchNotesMock).toHaveBeenCalledWith(['1']); - - // Page : 2 - - fetchNotesMock.mockClear(); - expect(screen.getByTestId('pagination-button-1')).toBeVisible(); - - fireEvent.click(screen.getByTestId('pagination-button-1')); - - await waitFor(() => { - expect(screen.getByTestId('pagination-button-1')).toHaveAttribute( - 'aria-current', - 'true' - ); - - expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [mockTimelineData[1]._id]); - }); - - // Page : 3 - - fetchNotesMock.mockClear(); - expect(screen.getByTestId('pagination-button-2')).toBeVisible(); - fireEvent.click(screen.getByTestId('pagination-button-2')); - - await waitFor(() => { - expect(screen.getByTestId('pagination-button-2')).toHaveAttribute( - 'aria-current', - 'true' - ); - - expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [mockTimelineData[2]._id]); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - - it( - 'should load notes for correct page size', - async () => { - const mockStateWithNoteInTimeline = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - [TimelineId.test]: { - ...mockGlobalState.timeline.timelineById[TimelineId.test], - /* 1 record for each page */ - itemsPerPage: 1, - pageIndex: 0, - itemsPerPageOptions: [1, 2, 3, 4, 5], - savedObjectId: 'timeline-1', // match timelineId in mocked notes data - pinnedEventIds: { '1': true }, - }, - }, - }, - }; - - render( - - - - ); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - expect(screen.getByTestId('pagination-button-previous')).toBeVisible(); - - expect(screen.getByTestId('pagination-button-0')).toHaveAttribute('aria-current', 'true'); - expect(screen.getByTestId('tablePaginationPopoverButton')).toHaveTextContent( - 'Rows per page: 1' - ); - fireEvent.click(screen.getByTestId('tablePaginationPopoverButton')); - - await waitFor(() => { - expect(screen.getByTestId('tablePagination-2-rows')).toBeVisible(); - }); - - fetchNotesMock.mockClear(); - fireEvent.click(screen.getByTestId('tablePagination-2-rows')); - - await waitFor(() => { - expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [ - mockTimelineData[0]._id, - mockTimelineData[1]._id, - ]); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - }); - }); -});