Skip to content

Commit b97254e

Browse files
committed
Add tests for updated components
1 parent f151267 commit b97254e

3 files changed

Lines changed: 482 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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, waitFor } from '@testing-library/react';
10+
import userEvent from '@testing-library/user-event';
11+
import { EuiThemeProvider } from '@elastic/eui';
12+
import { I18nProvider } from '@kbn/i18n-react';
13+
import { CreateIndexModal } from './create_index_modal';
14+
15+
const mockCreateIndex = jest.fn();
16+
jest.mock('../../../../services', () => ({
17+
createIndex: (...args: unknown[]) => mockCreateIndex(...args),
18+
}));
19+
20+
const mockShowSuccessToast = jest.fn();
21+
jest.mock('../../../../services/notification', () => ({
22+
notificationService: {
23+
showSuccessToast: (...args: unknown[]) => mockShowSuccessToast(...args),
24+
},
25+
}));
26+
27+
jest.mock('./utils', () => ({
28+
generateRandomIndexName: () => 'search-abcd',
29+
isValidIndexName: (name: string) => {
30+
if (!name || name !== name.toLowerCase() || name.length === 0) return false;
31+
return true;
32+
},
33+
}));
34+
35+
const renderModal = (props: Partial<React.ComponentProps<typeof CreateIndexModal>> = {}) => {
36+
const defaultProps = {
37+
closeModal: jest.fn(),
38+
loadIndices: jest.fn(),
39+
};
40+
41+
return render(
42+
<I18nProvider>
43+
<EuiThemeProvider>
44+
<CreateIndexModal {...defaultProps} {...props} />
45+
</EuiThemeProvider>
46+
</I18nProvider>
47+
);
48+
};
49+
50+
describe('CreateIndexModal', () => {
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
});
54+
55+
it('renders the modal with title and description', () => {
56+
renderModal();
57+
expect(screen.getByText('Create your index')).toBeInTheDocument();
58+
expect(
59+
screen.getByText('An index stores and defines the schema of your data.')
60+
).toBeInTheDocument();
61+
});
62+
63+
it('pre-fills the index name with a generated random name', () => {
64+
renderModal();
65+
const input = screen.getByTestId('createIndexNameFieldText');
66+
expect(input).toHaveValue('search-abcd');
67+
});
68+
69+
it('shows index name and index mode form fields', () => {
70+
renderModal();
71+
expect(screen.getByTestId('createIndexNameFieldText')).toBeInTheDocument();
72+
expect(screen.getByTestId('indexModeField')).toBeInTheDocument();
73+
});
74+
75+
it('shows a validation error for invalid index names', async () => {
76+
renderModal();
77+
const input = screen.getByTestId('createIndexNameFieldText');
78+
await userEvent.clear(input);
79+
await userEvent.type(input, 'INVALID');
80+
81+
expect(screen.getByText('Index name is not valid')).toBeInTheDocument();
82+
});
83+
84+
it('clears validation error when a valid name is entered', async () => {
85+
renderModal();
86+
const input = screen.getByTestId('createIndexNameFieldText');
87+
88+
await userEvent.clear(input);
89+
await userEvent.type(input, 'INVALID');
90+
expect(screen.getByText('Index name is not valid')).toBeInTheDocument();
91+
92+
await userEvent.clear(input);
93+
await userEvent.type(input, 'valid-name');
94+
expect(screen.queryByText('Index name is not valid')).not.toBeInTheDocument();
95+
});
96+
97+
it('calls closeModal when cancel button is clicked', () => {
98+
const closeModal = jest.fn();
99+
renderModal({ closeModal });
100+
fireEvent.click(screen.getByTestId('createIndexCancelButton'));
101+
expect(closeModal).toHaveBeenCalled();
102+
});
103+
104+
it('toggles the API code block when Show/Hide API button is clicked', () => {
105+
renderModal();
106+
expect(screen.getByText('Show API')).toBeInTheDocument();
107+
108+
fireEvent.click(screen.getByTestId('createIndexShowApiButton'));
109+
expect(screen.getByText('Hide API')).toBeInTheDocument();
110+
expect(screen.getByText(/PUT search-abcd/)).toBeInTheDocument();
111+
112+
fireEvent.click(screen.getByTestId('createIndexShowApiButton'));
113+
expect(screen.getByText('Show API')).toBeInTheDocument();
114+
});
115+
116+
it('reflects the selected index mode in the API code block', async () => {
117+
renderModal();
118+
fireEvent.click(screen.getByTestId('createIndexShowApiButton'));
119+
120+
expect(screen.getByText(/\"mode\":\"standard\"/)).toBeInTheDocument();
121+
});
122+
123+
it('creates the index on submit with valid name', async () => {
124+
const closeModal = jest.fn();
125+
const loadIndices = jest.fn();
126+
mockCreateIndex.mockResolvedValue({ error: undefined });
127+
128+
renderModal({ closeModal, loadIndices });
129+
fireEvent.click(screen.getByTestId('createIndexSaveButton'));
130+
131+
await waitFor(() => {
132+
expect(mockCreateIndex).toHaveBeenCalledWith('search-abcd', 'standard');
133+
});
134+
135+
await waitFor(() => {
136+
expect(mockShowSuccessToast).toHaveBeenCalled();
137+
expect(closeModal).toHaveBeenCalled();
138+
expect(loadIndices).toHaveBeenCalled();
139+
});
140+
});
141+
142+
it('displays an error callout when index creation fails', async () => {
143+
mockCreateIndex.mockResolvedValue({ error: { message: 'Index already exists' } });
144+
145+
renderModal();
146+
fireEvent.click(screen.getByTestId('createIndexSaveButton'));
147+
148+
await waitFor(() => {
149+
expect(screen.getByText('Error creating index')).toBeInTheDocument();
150+
expect(screen.getByText(/Index already exists/)).toBeInTheDocument();
151+
});
152+
});
153+
154+
it('does not submit when the index name is invalid', async () => {
155+
renderModal();
156+
const input = screen.getByTestId('createIndexNameFieldText');
157+
158+
await userEvent.clear(input);
159+
160+
fireEvent.click(screen.getByTestId('createIndexSaveButton'));
161+
162+
expect(mockCreateIndex).not.toHaveBeenCalled();
163+
});
164+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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 type { SearchHit } from '@elastic/elasticsearch/lib/api/types';
11+
12+
import type { Index } from '../../../../../../../common';
13+
import { DetailsPageOverview } from './details_page_overview';
14+
import {
15+
setupEnvironment,
16+
WithAppDependencies,
17+
} from '../../../../../../../__jest__/client_integration/helpers/setup_environment';
18+
import {
19+
testIndexMock,
20+
testIndexName,
21+
testIndexMappings,
22+
testUserStartPrivilegesResponse,
23+
} from '../../../../../../../__jest__/client_integration/index_details_page/mocks';
24+
25+
jest.mock('@kbn/code-editor');
26+
27+
const mockUseCloudConnectStatus = jest.fn();
28+
jest.mock('@kbn/search-api-panels', () => ({
29+
...jest.requireActual('@kbn/search-api-panels'),
30+
useCloudConnectStatus: (...args: unknown[]) => mockUseCloudConnectStatus(...args),
31+
EisCloudConnectPromoCallout: (props: { promoId: string }) => (
32+
<div data-test-subj={`${props.promoId}-cloud-connect-callout`}>Cloud Connect Promo</div>
33+
),
34+
EisUpdateCallout: (props: { promoId: string; handleOnClick: () => void }) => (
35+
<div data-test-subj={`${props.promoId}-eis-update-callout`}>
36+
EIS Update Callout
37+
<button data-test-subj="eisUpdateCalloutCtaBtn" onClick={props.handleOnClick}>
38+
Update
39+
</button>
40+
</div>
41+
),
42+
}));
43+
44+
const mockHasElserOnMlNodeSemanticTextField = jest.fn();
45+
jest.mock('../../../../../components/mappings_editor/lib/utils', () => ({
46+
...jest.requireActual('../../../../../components/mappings_editor/lib/utils'),
47+
hasElserOnMlNodeSemanticTextField: (...args: unknown[]) =>
48+
mockHasElserOnMlNodeSemanticTextField(...args),
49+
}));
50+
51+
jest.mock('../update_elser_mappings/update_elser_mappings_modal', () => ({
52+
UpdateElserMappingsModal: (props: { indexName: string }) => (
53+
<div data-test-subj="updateElserMappingsModal">Update ELSER Mappings for {props.indexName}</div>
54+
),
55+
}));
56+
57+
describe('DetailsPageOverview', () => {
58+
let httpSetup: ReturnType<typeof setupEnvironment>['httpSetup'];
59+
let httpRequestsMockHelpers: ReturnType<typeof setupEnvironment>['httpRequestsMockHelpers'];
60+
61+
beforeEach(() => {
62+
jest.clearAllMocks();
63+
const mockEnvironment = setupEnvironment();
64+
({ httpSetup, httpRequestsMockHelpers } = mockEnvironment);
65+
66+
httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, testIndexMappings);
67+
httpRequestsMockHelpers.setUserStartPrivilegesResponse(
68+
testIndexName,
69+
testUserStartPrivilegesResponse
70+
);
71+
httpRequestsMockHelpers.setLoadIndexDocCountResponse({ [testIndexName]: 1 });
72+
httpRequestsMockHelpers.setInferenceModels([]);
73+
74+
mockUseCloudConnectStatus.mockReturnValue({
75+
isLoading: true,
76+
isCloudConnected: false,
77+
isCloudConnectedWithEisEnabled: false,
78+
});
79+
mockHasElserOnMlNodeSemanticTextField.mockReturnValue(false);
80+
});
81+
82+
const renderComponent = (
83+
overrides: {
84+
indexDetails?: Index;
85+
sampleDocuments?: SearchHit[];
86+
isDocumentsLoading?: boolean;
87+
documentsError?: unknown;
88+
appDeps?: Record<string, unknown>;
89+
} = {}
90+
) => {
91+
const defaultProps = {
92+
indexDetails: overrides.indexDetails ?? testIndexMock,
93+
sampleDocuments: overrides.sampleDocuments ?? [],
94+
isDocumentsLoading: overrides.isDocumentsLoading ?? false,
95+
documentsError: overrides.documentsError ?? undefined,
96+
};
97+
98+
const Comp = WithAppDependencies(() => <DetailsPageOverview {...defaultProps} />, httpSetup, {
99+
url: { locators: { get: () => ({ navigate: jest.fn(), getUrl: jest.fn() }) } },
100+
...overrides.appDeps,
101+
});
102+
103+
return render(<Comp />);
104+
};
105+
106+
it('renders the QuickStats section', async () => {
107+
renderComponent();
108+
await waitFor(() => {
109+
expect(screen.getByText('Storage')).toBeInTheDocument();
110+
});
111+
});
112+
113+
it('renders the "Add data to this index" heading', async () => {
114+
renderComponent();
115+
await waitFor(() => {
116+
expect(screen.getByText('Add data to this index')).toBeInTheDocument();
117+
});
118+
});
119+
120+
it('renders the bulk API description with a docs link', async () => {
121+
renderComponent();
122+
await waitFor(() => {
123+
expect(screen.getByText('Learn more.')).toBeInTheDocument();
124+
});
125+
});
126+
127+
it('renders code snippet with the index name', async () => {
128+
renderComponent();
129+
await waitFor(() => {
130+
expect(screen.getByText(/test_index/)).toBeInTheDocument();
131+
});
132+
});
133+
134+
describe('EisCloudConnectPromoCallout', () => {
135+
it('does not render when cloud connect status is loading', async () => {
136+
mockUseCloudConnectStatus.mockReturnValue({
137+
isLoading: true,
138+
isCloudConnected: false,
139+
isCloudConnectedWithEisEnabled: false,
140+
});
141+
142+
renderComponent();
143+
await waitFor(() => {
144+
expect(screen.getByText('Add data to this index')).toBeInTheDocument();
145+
});
146+
expect(
147+
screen.queryByTestId('indexDetailsOverview-cloud-connect-callout')
148+
).not.toBeInTheDocument();
149+
});
150+
151+
it('renders when not loading and not cloud connected', async () => {
152+
mockUseCloudConnectStatus.mockReturnValue({
153+
isLoading: false,
154+
isCloudConnected: false,
155+
isCloudConnectedWithEisEnabled: false,
156+
});
157+
158+
renderComponent();
159+
await waitFor(() => {
160+
expect(
161+
screen.getByTestId('indexDetailsOverview-cloud-connect-callout')
162+
).toBeInTheDocument();
163+
});
164+
});
165+
166+
it('does not render when already cloud connected', async () => {
167+
mockUseCloudConnectStatus.mockReturnValue({
168+
isLoading: false,
169+
isCloudConnected: true,
170+
isCloudConnectedWithEisEnabled: false,
171+
});
172+
173+
renderComponent();
174+
await waitFor(() => {
175+
expect(screen.getByText('Add data to this index')).toBeInTheDocument();
176+
});
177+
expect(
178+
screen.queryByTestId('indexDetailsOverview-cloud-connect-callout')
179+
).not.toBeInTheDocument();
180+
});
181+
});
182+
183+
describe('EisUpdateCallout', () => {
184+
it('does not render when there are no ELSER semantic text fields', async () => {
185+
mockHasElserOnMlNodeSemanticTextField.mockReturnValue(false);
186+
187+
renderComponent();
188+
await waitFor(() => {
189+
expect(screen.getByText('Add data to this index')).toBeInTheDocument();
190+
});
191+
expect(
192+
screen.queryByTestId('indexDetailsOverview-eis-update-callout')
193+
).not.toBeInTheDocument();
194+
});
195+
196+
it('renders when ELSER semantic text fields are present', async () => {
197+
mockHasElserOnMlNodeSemanticTextField.mockReturnValue(true);
198+
199+
renderComponent();
200+
await waitFor(() => {
201+
expect(screen.getByTestId('indexDetailsOverview-eis-update-callout')).toBeInTheDocument();
202+
});
203+
});
204+
});
205+
206+
describe('UpdateElserMappingsModal', () => {
207+
it('does not render by default', async () => {
208+
mockHasElserOnMlNodeSemanticTextField.mockReturnValue(true);
209+
210+
renderComponent();
211+
await waitFor(() => {
212+
expect(screen.getByTestId('indexDetailsOverview-eis-update-callout')).toBeInTheDocument();
213+
});
214+
expect(screen.queryByTestId('updateElserMappingsModal')).not.toBeInTheDocument();
215+
});
216+
217+
it('renders after clicking the EIS update callout CTA button', async () => {
218+
mockHasElserOnMlNodeSemanticTextField.mockReturnValue(true);
219+
220+
renderComponent();
221+
await waitFor(() => {
222+
expect(screen.getByTestId('eisUpdateCalloutCtaBtn')).toBeInTheDocument();
223+
});
224+
225+
screen.getByTestId('eisUpdateCalloutCtaBtn').click();
226+
227+
await waitFor(() => {
228+
expect(screen.getByTestId('updateElserMappingsModal')).toBeInTheDocument();
229+
});
230+
});
231+
});
232+
});

0 commit comments

Comments
 (0)