Skip to content

Commit 7f8c6f0

Browse files
committed
test(ml): add unit tests for MlDataSourcePicker and DataSourceContextProvider components
- Introduced comprehensive tests for the MlDataSourcePicker component, covering rendering, data view selection, and session management functionalities. - Added tests for the DataSourceContextProvider to ensure proper rendering based on URL parameters and error handling. - Enhanced overall test coverage for AIOps components, improving reliability and maintainability of the codebase.
1 parent 7ca8f0e commit 7f8c6f0

3 files changed

Lines changed: 408 additions & 23 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render, screen, fireEvent, act } from '@testing-library/react';
10+
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
11+
import { MlDataSourcePicker } from './ml_data_source_picker';
12+
import type { MlDataSourcePickerServices } from './ml_data_source_picker';
13+
14+
jest.mock('react-router-dom', () => ({
15+
useLocation: jest.fn(() => ({ pathname: '/jobs/new_job/step/data_view', search: '' })),
16+
}));
17+
18+
let capturedDataViewPickerProps: Record<string, any> = {};
19+
const MockDataViewPicker = (props: any) => {
20+
capturedDataViewPickerProps = props;
21+
return (
22+
<div data-test-subj="mockDataViewPicker">
23+
<span>{props.trigger?.label}</span>
24+
</div>
25+
);
26+
};
27+
28+
jest.mock('./ml_open_session_flyout', () => ({
29+
MlOpenSessionFlyout: (props: any) => {
30+
return (
31+
<div data-test-subj="mockOpenSessionFlyout">
32+
<button onClick={props.onClose} data-test-subj="closeSessionPanel">
33+
Close
34+
</button>
35+
<button
36+
onClick={() => props.onOpenSavedSearch('saved-search-id-1')}
37+
data-test-subj="openSavedSearch"
38+
>
39+
Open Saved Search
40+
</button>
41+
</div>
42+
);
43+
},
44+
}));
45+
46+
const mockNavigateToPath = jest.fn();
47+
const mockGetIdsWithTitle = jest.fn().mockResolvedValue([]);
48+
const mockOpenEditor = jest.fn().mockResolvedValue(() => {});
49+
50+
const buildServices = (overrides?: Partial<MlDataSourcePickerServices>): MlDataSourcePickerServices =>
51+
({
52+
dataViews: { getIdsWithTitle: mockGetIdsWithTitle },
53+
dataViewEditor: {
54+
userPermissions: { editDataView: jest.fn(() => true) },
55+
},
56+
dataViewFieldEditor: { openEditor: mockOpenEditor },
57+
http: { basePath: { prepend: jest.fn((p: string) => p) } },
58+
application: { capabilities: {} },
59+
contentManagement: { client: {} },
60+
uiSettings: {},
61+
...overrides,
62+
} as unknown as MlDataSourcePickerServices);
63+
64+
const MockSavedObjectFinder = () => <div data-test-subj="mockSavedObjectFinder" />;
65+
66+
const renderComponent = (props: { currentDataView: any; services?: MlDataSourcePickerServices }) =>
67+
render(
68+
<IntlProvider locale="en">
69+
<MlDataSourcePicker
70+
currentDataView={props.currentDataView}
71+
services={props.services ?? buildServices()}
72+
navigateToPath={mockNavigateToPath}
73+
DataViewPickerComponent={MockDataViewPicker}
74+
SavedObjectFinderComponent={MockSavedObjectFinder}
75+
/>
76+
</IntlProvider>
77+
);
78+
79+
describe('MlDataSourcePicker', () => {
80+
beforeEach(() => {
81+
jest.clearAllMocks();
82+
capturedDataViewPickerProps = {};
83+
});
84+
85+
it('renders DataViewPicker with "Select data view" label when currentDataView is null', async () => {
86+
await act(async () => {
87+
renderComponent({ currentDataView: null });
88+
});
89+
90+
expect(screen.getByTestId('mockDataViewPicker')).toBeDefined();
91+
expect(screen.getByText('Select data view')).toBeDefined();
92+
expect(capturedDataViewPickerProps.trigger?.label).toBe('Select data view');
93+
});
94+
95+
it('renders DataViewPicker with the data view name when currentDataView is provided', async () => {
96+
const mockDataView = {
97+
id: 'dv-1',
98+
getName: jest.fn(() => 'My Data View'),
99+
};
100+
101+
await act(async () => {
102+
renderComponent({ currentDataView: mockDataView });
103+
});
104+
105+
expect(screen.getByText('My Data View')).toBeDefined();
106+
expect(capturedDataViewPickerProps.trigger?.label).toBe('My Data View');
107+
expect(capturedDataViewPickerProps.currentDataViewId).toBe('dv-1');
108+
});
109+
110+
it('calls navigateToPath with ?index=<id> when onChangeDataView is triggered', async () => {
111+
await act(async () => {
112+
renderComponent({ currentDataView: null });
113+
});
114+
115+
await act(async () => {
116+
capturedDataViewPickerProps.onChangeDataView('test-index-id');
117+
});
118+
119+
expect(mockNavigateToPath).toHaveBeenCalledWith(
120+
'/jobs/new_job/step/data_view?index=test-index-id'
121+
);
122+
});
123+
124+
it('renders MlOpenSessionFlyout when "Open Discover session" button is clicked', async () => {
125+
await act(async () => {
126+
renderComponent({ currentDataView: null });
127+
});
128+
129+
expect(screen.queryByTestId('mockOpenSessionFlyout')).toBeNull();
130+
131+
await act(async () => {
132+
fireEvent.click(screen.getByTestId('mlOpenDiscoverSessionButton'));
133+
});
134+
135+
expect(screen.getByTestId('mockOpenSessionFlyout')).toBeDefined();
136+
});
137+
138+
it('onOpenSavedSearch calls navigateToPath with ?savedSearchId=<id> and hides the flyout', async () => {
139+
await act(async () => {
140+
renderComponent({ currentDataView: null });
141+
});
142+
143+
await act(async () => {
144+
fireEvent.click(screen.getByTestId('mlOpenDiscoverSessionButton'));
145+
});
146+
147+
expect(screen.getByTestId('mockOpenSessionFlyout')).toBeDefined();
148+
149+
await act(async () => {
150+
fireEvent.click(screen.getByTestId('openSavedSearch'));
151+
});
152+
153+
expect(mockNavigateToPath).toHaveBeenCalledWith(
154+
'/jobs/new_job/step/data_view?savedSearchId=saved-search-id-1'
155+
);
156+
expect(screen.queryByTestId('mockOpenSessionFlyout')).toBeNull();
157+
});
158+
159+
it('onDataViewCreated navigates and refreshes data views when the new view has an id', async () => {
160+
const refreshedViews = [{ id: 'new-dv', title: 'New DV' }];
161+
mockGetIdsWithTitle.mockResolvedValueOnce([]).mockResolvedValueOnce(refreshedViews);
162+
163+
await act(async () => {
164+
renderComponent({ currentDataView: null });
165+
});
166+
167+
await act(async () => {
168+
await capturedDataViewPickerProps.onDataViewCreated({ id: 'new-dv-id' });
169+
});
170+
171+
expect(mockNavigateToPath).toHaveBeenCalledWith(
172+
'/jobs/new_job/step/data_view?index=new-dv-id'
173+
);
174+
expect(mockGetIdsWithTitle).toHaveBeenCalledTimes(2);
175+
});
176+
177+
it('onAddField is defined and calls dataViewFieldEditor.openEditor when canEditDataView=true and currentDataView is set', async () => {
178+
const mockDataView = {
179+
id: 'dv-1',
180+
getName: jest.fn(() => 'My Data View'),
181+
};
182+
183+
await act(async () => {
184+
renderComponent({ currentDataView: mockDataView });
185+
});
186+
187+
expect(capturedDataViewPickerProps.onAddField).toBeDefined();
188+
189+
await act(async () => {
190+
await capturedDataViewPickerProps.onAddField();
191+
});
192+
193+
expect(mockOpenEditor).toHaveBeenCalledWith({
194+
ctx: { dataView: mockDataView },
195+
onSave: expect.any(Function),
196+
});
197+
});
198+
199+
it('onAddField is undefined when canEditDataView=false', async () => {
200+
const mockDataView = {
201+
id: 'dv-1',
202+
getName: jest.fn(() => 'My Data View'),
203+
};
204+
205+
const services = buildServices({
206+
dataViewEditor: {
207+
userPermissions: { editDataView: jest.fn(() => false) },
208+
},
209+
} as any);
210+
211+
await act(async () => {
212+
renderComponent({ currentDataView: mockDataView, services });
213+
});
214+
215+
expect(capturedDataViewPickerProps.onAddField).toBeUndefined();
216+
});
217+
});
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render, screen, waitFor } from '@testing-library/react';
10+
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
11+
import { DataSourceContextProvider } from './data_source_context';
12+
import { useMlKibana } from '../kibana';
13+
14+
jest.mock('../kibana', () => ({
15+
useMlKibana: jest.fn(),
16+
}));
17+
18+
jest.mock('../../util/index_utils', () => ({
19+
getDataViewAndSavedSearchCallback: jest.fn(() => async (id: string) => ({
20+
dataView: { id, title: 'mock-saved-search-data-view' },
21+
savedSearch: { id: 'mock-saved-search' },
22+
})),
23+
}));
24+
25+
jest.mock('../../jobs/new_job/utils/new_job_utils', () => ({
26+
createSearchItems: jest.fn(() => ({ combinedQuery: { match_all: {} } })),
27+
}));
28+
29+
const mockLocationSearch = jest.fn(() => '');
30+
jest.mock('react-router-dom', () => ({
31+
useLocation: () => ({ pathname: '/', search: mockLocationSearch() }),
32+
}));
33+
34+
const mockGet = jest.fn();
35+
const mockGetDefaultDataView = jest.fn();
36+
const mockGetDataViewAndSavedSearch = jest.fn();
37+
38+
const buildKibanaMock = () => ({
39+
services: {
40+
data: {
41+
dataViews: {
42+
get: mockGet,
43+
getDefaultDataView: mockGetDefaultDataView,
44+
},
45+
},
46+
savedSearch: mockGetDataViewAndSavedSearch,
47+
uiSettings: {},
48+
},
49+
});
50+
51+
const renderProvider = (children = <div data-test-subj="child-content">Hello</div>) =>
52+
render(
53+
<IntlProvider locale="en">
54+
<DataSourceContextProvider>{children}</DataSourceContextProvider>
55+
</IntlProvider>
56+
);
57+
58+
describe('DataSourceContextProvider', () => {
59+
beforeEach(() => {
60+
jest.clearAllMocks();
61+
(useMlKibana as jest.Mock).mockReturnValue(buildKibanaMock());
62+
});
63+
64+
it('renders children when default data view is found (no URL params)', async () => {
65+
mockLocationSearch.mockReturnValue('');
66+
mockGetDefaultDataView.mockResolvedValue({
67+
id: 'default-dv',
68+
title: 'Default Data View',
69+
});
70+
71+
renderProvider();
72+
73+
await waitFor(() => {
74+
expect(screen.getByTestId('child-content')).toBeInTheDocument();
75+
});
76+
77+
expect(mockGetDefaultDataView).toHaveBeenCalled();
78+
});
79+
80+
it('renders children when index URL param is present', async () => {
81+
mockLocationSearch.mockReturnValue('?index=my-index-id');
82+
mockGet.mockResolvedValue({
83+
id: 'my-index-id',
84+
title: 'My Index',
85+
});
86+
87+
renderProvider();
88+
89+
await waitFor(() => {
90+
expect(screen.getByTestId('child-content')).toBeInTheDocument();
91+
});
92+
93+
expect(mockGet).toHaveBeenCalledWith('my-index-id');
94+
});
95+
96+
it('renders children when savedSearchId URL param is present', async () => {
97+
const { getDataViewAndSavedSearchCallback } = jest.requireMock('../../util/index_utils');
98+
getDataViewAndSavedSearchCallback.mockReturnValue(async (id: string) => ({
99+
dataView: { id: 'dv-from-saved-search', title: 'From Saved Search' },
100+
savedSearch: { id },
101+
}));
102+
103+
mockLocationSearch.mockReturnValue('?savedSearchId=my-saved-search');
104+
105+
renderProvider();
106+
107+
await waitFor(() => {
108+
expect(screen.getByTestId('child-content')).toBeInTheDocument();
109+
});
110+
});
111+
112+
it('shows error state when resolveDataSource throws', async () => {
113+
mockLocationSearch.mockReturnValue('?index=bad-index');
114+
mockGet.mockRejectedValue(new Error('Data view not found'));
115+
116+
renderProvider();
117+
118+
await waitFor(() => {
119+
expect(
120+
screen.getByText('Unable to fetch data view or saved Discover session')
121+
).toBeInTheDocument();
122+
});
123+
124+
expect(screen.queryByTestId('child-content')).not.toBeInTheDocument();
125+
});
126+
127+
it('renders null initially before the async resolve completes', async () => {
128+
mockLocationSearch.mockReturnValue('');
129+
let resolvePromise: (value: any) => void;
130+
mockGetDefaultDataView.mockReturnValue(
131+
new Promise((resolve) => {
132+
resolvePromise = resolve;
133+
})
134+
);
135+
136+
const { container } = renderProvider();
137+
138+
expect(container.firstChild).toBeNull();
139+
140+
await waitFor(() => {
141+
resolvePromise!({ id: 'default-dv', title: 'Default' });
142+
});
143+
144+
await waitFor(() => {
145+
expect(screen.getByTestId('child-content')).toBeInTheDocument();
146+
});
147+
});
148+
149+
it('shows error when dataViewId is an empty string', async () => {
150+
mockLocationSearch.mockReturnValue('?index=');
151+
152+
renderProvider();
153+
154+
await waitFor(() => {
155+
expect(
156+
screen.getByText('Unable to fetch data view or saved Discover session')
157+
).toBeInTheDocument();
158+
});
159+
});
160+
});

0 commit comments

Comments
 (0)