Skip to content

Commit

Permalink
[Enterprise Search] Switch to React Testing Library for Enterprise Se…
Browse files Browse the repository at this point in the history
…arch (elastic#170514)

## Summary

Added a test helper to create required providers. This way now we can
mount all of our logics and test the changes. It also does a lot of
heavy lifting and simplifies tests.
Full mocking should still be available with previous helpers and a bit
of working around.


### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
efegurkan and kibanamachine authored Nov 3, 2023
1 parent cd490ee commit 4e065da
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import { httpServiceMock } from '@kbn/core/public/mocks';

export const mockHttpValues = {
http: httpServiceMock.createSetupContract(),
errorConnectingMessage: '',
http: httpServiceMock.createSetupContract(),
readOnlyMode: false,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 { mockHttpValues } from '../../../__mocks__/kea_logic';

import { nextTick } from '@kbn/test-jest-helpers';

import { startAccessControlSync } from './start_access_control_sync_api_logic';

describe('startAccessControlSyncApiLogic', () => {
describe('startAccessControlSync', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});

it('calls correct api', async () => {
const promise = Promise.resolve('result');
http.post.mockReturnValue(promise);
const connectorId = 'test-connector-id-123';

const result = startAccessControlSync({ connectorId });
await nextTick();
expect(http.post).toHaveBeenCalledWith(
`/internal/enterprise_search/connectors/${connectorId}/start_access_control_sync`
);
await expect(result).resolves.toEqual('result');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 React from 'react';

import { screen } from '@testing-library/react';

import '@testing-library/jest-dom';

import { HttpSetupMock } from '@kbn/core-http-browser-mocks';

import { HttpLogic } from '../../../shared/http';
import { TestHelper } from '../../../test_helpers/test_utils.test_helper';
import { FetchDefaultPipelineApiLogic } from '../../api/connector/get_default_pipeline_api_logic';

import { Settings } from './settings';

test('displays Settings Save-Reset buttons disabled by default', async () => {
TestHelper.prepare();
TestHelper.mountLogic(FetchDefaultPipelineApiLogic);
TestHelper.appendCallback(() => {
const http = HttpLogic.values.http as HttpSetupMock;
http.get.mockImplementationOnce(() =>
Promise.resolve({
extract_binary_content: true,
name: 'test',
reduce_whitespace: true,
run_ml_inference: true,
})
);
});
TestHelper.render(<Settings />);

const saveButton = screen.getByTestId('entSearchContentSettingsSaveButton');
const resetButton = screen.getByTestId('entSearchContentSettingsResetButton');

expect(saveButton).toHaveTextContent('Save');
expect(saveButton).toBeDisabled();
expect(resetButton).toHaveTextContent('Reset');
expect(resetButton).toBeDisabled();
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { SettingsPanel } from './settings_panel';
export const Settings: React.FC = () => {
const { makeRequest, setPipeline } = useActions(SettingsLogic);
const { defaultPipeline, hasNoChanges, isLoading, pipelineState } = useValues(SettingsLogic);

const {
extract_binary_content: extractBinaryContent,
reduce_whitespace: reduceWhitespace,
Expand Down Expand Up @@ -62,6 +61,7 @@ export const Settings: React.FC = () => {
disabled={hasNoChanges}
isLoading={isLoading}
onClick={() => makeRequest(pipelineState)}
data-test-subj={'entSearchContentSettingsSaveButton'}
>
{i18n.translate('xpack.enterpriseSearch.content.settings.saveButtonLabel', {
defaultMessage: 'Save',
Expand All @@ -71,6 +71,7 @@ export const Settings: React.FC = () => {
disabled={hasNoChanges}
isLoading={isLoading}
onClick={() => setPipeline(defaultPipeline)}
data-test-subj={'entSearchContentSettingsResetButton'}
>
{i18n.translate('xpack.enterpriseSearch.content.settings.resetButtonLabel', {
defaultMessage: 'Reset',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

import { kea, MakeLogicType } from 'kea';

import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from '@kbn/core/public';
import { HttpInterceptorResponseError, HttpResponse, HttpSetup } from '@kbn/core/public';

import { ERROR_CONNECTING_HEADER, READ_ONLY_MODE_HEADER } from '../../../../common/constants';

export interface HttpValues {
errorConnectingMessage: string;
http: HttpSetup;
httpInterceptors: Function[];
errorConnectingMessage: string;
readOnlyMode: boolean;
}

Expand All @@ -26,33 +26,21 @@ interface HttpActions {
}

export const HttpLogic = kea<MakeLogicType<HttpValues, HttpActions>>({
path: ['enterprise_search', 'http_logic'],
actions: {
initializeHttpInterceptors: () => null,
onConnectionError: (errorConnectingMessage) => ({ errorConnectingMessage }),
setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }),
setReadOnlyMode: (readOnlyMode) => ({ readOnlyMode }),
},
reducers: ({ props }) => ({
http: [props.http, {}],
httpInterceptors: [
[],
{
setHttpInterceptors: (_, { httpInterceptors }) => httpInterceptors,
},
],
errorConnectingMessage: [
props.errorConnectingMessage || '',
{
onConnectionError: (_, { errorConnectingMessage }) => errorConnectingMessage,
},
],
readOnlyMode: [
props.readOnlyMode || false,
{
setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode,
},
],
events: ({ values, actions }) => ({
afterMount: () => {
actions.initializeHttpInterceptors();
},
beforeUnmount: () => {
values.httpInterceptors.forEach((removeInterceptorFn?: Function) => {
if (removeInterceptorFn) removeInterceptorFn();
});
},
}),
listeners: ({ values, actions }) => ({
initializeHttpInterceptors: () => {
Expand Down Expand Up @@ -94,30 +82,42 @@ export const HttpLogic = kea<MakeLogicType<HttpValues, HttpActions>>({
actions.setHttpInterceptors(httpInterceptors);
},
}),
events: ({ values, actions }) => ({
afterMount: () => {
actions.initializeHttpInterceptors();
},
beforeUnmount: () => {
values.httpInterceptors.forEach((removeInterceptorFn?: Function) => {
if (removeInterceptorFn) removeInterceptorFn();
});
},
path: ['enterprise_search', 'http_logic'],
reducers: ({ props }) => ({
errorConnectingMessage: [
props.errorConnectingMessage || '',
{
onConnectionError: (_, { errorConnectingMessage }) => errorConnectingMessage,
},
],
http: [props.http, {}],
httpInterceptors: [
[],
{
setHttpInterceptors: (_, { httpInterceptors }) => httpInterceptors,
},
],
readOnlyMode: [
props.readOnlyMode || false,
{
setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode,
},
],
}),
});

/**
* Mount/props helper
*/
interface HttpLogicProps {
http: HttpSetup;
errorConnectingMessage?: string;
http: HttpSetup;
readOnlyMode?: boolean;
}

export const mountHttpLogic = (props: HttpLogicProps) => {
HttpLogic(props);
const unmount = HttpLogic.mount();
return unmount;
return HttpLogic.mount();
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export { KibanaLogic, mountKibanaLogic } from './kibana_logic';
export type { KibanaLogicProps } from './kibana_logic';
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { createHref, CreateHrefOptions } from '../react_router_helpers';
type RequiredFieldsOnly<T> = {
[K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K];
};
interface KibanaLogicProps {
export interface KibanaLogicProps {
application: ApplicationStart;
capabilities: Capabilities;
charts: ChartsPluginStart;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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 { mockHistory } from '../__mocks__/react_router';

import React from 'react';

import { render as testingLibraryRender } from '@testing-library/react';

import { LogicWrapper, Provider, resetContext } from 'kea';

import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { ApplicationStart } from '@kbn/core-application-browser';
import { Capabilities } from '@kbn/core-capabilities-common';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { I18nProvider } from '@kbn/i18n-react';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { mlPluginMock } from '@kbn/ml-plugin/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';

import { mountHttpLogic } from '../shared/http';
import { mountKibanaLogic, KibanaLogicProps } from '../shared/kibana';

export const mockKibanaProps: KibanaLogicProps = {
application: {
getUrlForApp: jest.fn(
(appId: string, options?: { path?: string }) => `/app/${appId}${options?.path}`
),
} as unknown as ApplicationStart,
capabilities: {} as Capabilities,
charts: chartPluginMock.createStartContract(),
cloud: {
...cloudMock.createSetup(),
isCloudEnabled: false,
},
config: {
canDeployEntSearch: true,
host: 'http://localhost:3002',
ui: {
enabled: true,
},
},
data: dataPluginMock.createStartContract(),
guidedOnboarding: {},
history: mockHistory,
isSidebarEnabled: true,
lens: {
EmbeddableComponent: jest.fn(),
stateHelperApi: jest.fn().mockResolvedValue({
formula: jest.fn(),
}),
} as unknown as LensPublicStart,
ml: mlPluginMock.createStartContract(),
navigateToUrl: jest.fn(),
productAccess: {
hasAppSearchAccess: true,
hasWorkplaceSearchAccess: true,
},
productFeatures: {
hasConnectors: true,
hasDefaultIngestPipeline: true,
hasDocumentLevelSecurityEnabled: true,
hasIncrementalSyncEnabled: true,
hasNativeConnectors: true,
hasWebCrawler: true,
},
renderHeaderActions: jest.fn(),
security: securityMock.createStart(),
setBreadcrumbs: jest.fn(),
setChromeIsVisible: jest.fn(),
setDocTitle: jest.fn(),
share: sharePluginMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
};

type LogicFile = LogicWrapper<any>;
const DEFAULT_VALUES = {
httpLogicValues: {
http: httpServiceMock.createSetupContract(),
},
kibanaLogicValues: mockKibanaProps,
};

interface PrepareOptions {
mockValues: typeof DEFAULT_VALUES;
noDefaultActions: boolean;
}

interface TestHelper {
actionsToRun: Array<() => void>;
appendCallback: (callback: () => void) => void;
defaultActions: () => TestHelper['actionsToRun'];
defaultMockValues: typeof DEFAULT_VALUES;
mountLogic: (logicFile: LogicFile, props?: object) => void;
prepare: (options?: PrepareOptions) => void;
render: (children: JSX.Element) => void;
}

export const TestHelper: TestHelper = {
actionsToRun: [],
appendCallback: (callback) => {
TestHelper.actionsToRun.push(callback);
},
defaultActions: () => {
return [
() => {
resetContext();
},
() => {
mountHttpLogic(TestHelper.defaultMockValues.httpLogicValues);
mountKibanaLogic(TestHelper.defaultMockValues.kibanaLogicValues);
},
];
},
defaultMockValues: DEFAULT_VALUES,
mountLogic: (logicFile, props?) => {
TestHelper.actionsToRun.push(() => logicFile.call(logicFile, props || undefined));
},
prepare: (options?) => {
TestHelper.defaultMockValues = { ...DEFAULT_VALUES, ...(options?.mockValues || {}) };
if (!options || !options.noDefaultActions) {
TestHelper.actionsToRun = TestHelper.defaultActions();
}
},
render: (children) => {
TestHelper.actionsToRun.forEach((action) => {
action();
});
testingLibraryRender(
<I18nProvider>
<Provider>{children}</Provider>
</I18nProvider>
);
},
};
5 changes: 4 additions & 1 deletion x-pack/plugins/enterprise_search/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
"@kbn/share-plugin",
"@kbn/search-api-panels",
"@kbn/search-connectors",
"@kbn/logs-shared-plugin"
"@kbn/logs-shared-plugin",
"@kbn/core-http-browser-mocks",
"@kbn/core-application-browser",
"@kbn/core-capabilities-common"
]
}

0 comments on commit 4e065da

Please sign in to comment.