333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
*/
|
|
|
|
import { ComponentType, ReactWrapper } from 'enzyme';
|
|
|
|
import { findTestSubject } from '../find_test_subject';
|
|
import { reactRouterMock } from '../router_helpers';
|
|
import {
|
|
mountComponentSync,
|
|
mountComponentAsync,
|
|
getJSXComponentWithProps,
|
|
} from './mount_component';
|
|
import { TestBedConfig, TestBed, SetupFunc } from './types';
|
|
|
|
const defaultConfig: TestBedConfig = {
|
|
defaultProps: {},
|
|
memoryRouter: {
|
|
wrapComponent: true,
|
|
},
|
|
store: null,
|
|
};
|
|
|
|
/**
|
|
* Register a new Testbed to test a React Component.
|
|
*
|
|
* @param Component The component under test
|
|
* @param config An optional configuration object for the Testbed
|
|
*
|
|
* @example
|
|
```typescript
|
|
import { registerTestBed } from '../../../../test_utils';
|
|
import { RemoteClusterList } from '../../app/sections/remote_cluster_list';
|
|
import { remoteClustersStore } from '../../app/store';
|
|
|
|
const setup = registerTestBed(RemoteClusterList, { store: remoteClustersStore });
|
|
|
|
describe('<RemoteClusterList />, () > {
|
|
test('it should have a table', () => {
|
|
const { exists } = setup();
|
|
expect(exists('remoteClustersTable')).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
*/
|
|
export const registerTestBed = <T extends string = string>(
|
|
Component: ComponentType<any>,
|
|
config?: TestBedConfig
|
|
): SetupFunc<T> => {
|
|
const {
|
|
defaultProps = defaultConfig.defaultProps,
|
|
memoryRouter = defaultConfig.memoryRouter!,
|
|
store = defaultConfig.store,
|
|
doMountAsync = false,
|
|
} = config || {};
|
|
|
|
// Keep a reference to the React Router
|
|
let router: any;
|
|
|
|
const onRouter = (_router: any) => {
|
|
router = _router;
|
|
|
|
if (memoryRouter.onRouter) {
|
|
memoryRouter.onRouter(_router);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* In some cases, component have some logic that interacts with the react router
|
|
* _before_ the component is mounted.(Class constructor() I'm looking at you :)
|
|
*
|
|
* By adding the following lines, we make sure there is always a router available
|
|
* when instantiating the Component.
|
|
*/
|
|
onRouter(reactRouterMock);
|
|
|
|
const setup: SetupFunc<T> = props => {
|
|
// If a function is provided we execute it
|
|
const storeToMount = typeof store === 'function' ? store() : store!;
|
|
const mountConfig = {
|
|
Component,
|
|
memoryRouter,
|
|
store: storeToMount,
|
|
props: {
|
|
...defaultProps,
|
|
...props,
|
|
},
|
|
onRouter,
|
|
};
|
|
|
|
if (doMountAsync) {
|
|
return mountComponentAsync(mountConfig).then(onComponentMounted);
|
|
}
|
|
|
|
return onComponentMounted(mountComponentSync(mountConfig));
|
|
|
|
// ---------------------
|
|
|
|
function onComponentMounted(component: ReactWrapper) {
|
|
/**
|
|
* ----------------------------------------------------------------
|
|
* Utils
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
|
|
const find: TestBed<T>['find'] = (testSubject: T, sourceReactWrapper = component) => {
|
|
const testSubjectToArray = testSubject.split('.');
|
|
|
|
return testSubjectToArray.reduce((reactWrapper, subject, i) => {
|
|
const target = findTestSubject(reactWrapper, subject);
|
|
if (!target.length && i < testSubjectToArray.length - 1) {
|
|
throw new Error(
|
|
`Can't access nested test subject "${
|
|
testSubjectToArray[i + 1]
|
|
}" of unknown node "${subject}"`
|
|
);
|
|
}
|
|
return target;
|
|
}, sourceReactWrapper);
|
|
};
|
|
|
|
const exists: TestBed<T>['exists'] = (testSubject, count = 1) =>
|
|
find(testSubject).length === count;
|
|
|
|
const setProps: TestBed<T>['setProps'] = updatedProps => {
|
|
if (memoryRouter.wrapComponent !== false) {
|
|
throw new Error(
|
|
'setProps() can only be called on a component **not** wrapped by a router route.'
|
|
);
|
|
}
|
|
if (store === null) {
|
|
return component.setProps({ ...defaultProps, ...updatedProps });
|
|
}
|
|
// Update the props on the Redux Provider children
|
|
return component.setProps({
|
|
children: getJSXComponentWithProps(Component, { ...defaultProps, ...updatedProps }),
|
|
});
|
|
};
|
|
|
|
const waitForFn: TestBed<T>['waitForFn'] = async (predicate, errMessage) => {
|
|
const triggeredAt = Date.now();
|
|
|
|
const MAX_WAIT_TIME = 30000;
|
|
const WAIT_INTERVAL = 50;
|
|
|
|
const process = async (): Promise<void> => {
|
|
const isOK = await predicate();
|
|
|
|
if (isOK) {
|
|
// Great! nothing else to do here.
|
|
return;
|
|
}
|
|
|
|
const timeElapsed = Date.now() - triggeredAt;
|
|
if (timeElapsed > MAX_WAIT_TIME) {
|
|
throw new Error(errMessage);
|
|
}
|
|
|
|
return new Promise(resolve => setTimeout(resolve, WAIT_INTERVAL)).then(() => {
|
|
component.update();
|
|
return process();
|
|
});
|
|
};
|
|
|
|
return process();
|
|
};
|
|
|
|
const waitFor: TestBed<T>['waitFor'] = (testSubject: T, count = 1) => {
|
|
return waitForFn(
|
|
() => Promise.resolve(exists(testSubject, count)),
|
|
`I waited patiently for the "${testSubject}" test subject to appear with no luck. It is nowhere to be found!`
|
|
);
|
|
};
|
|
|
|
/**
|
|
* ----------------------------------------------------------------
|
|
* Forms
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
|
|
const setInputValue: TestBed<T>['form']['setInputValue'] = (
|
|
input,
|
|
value,
|
|
isAsync = false
|
|
) => {
|
|
const formInput = typeof input === 'string' ? find(input) : (input as ReactWrapper);
|
|
|
|
if (!formInput.length) {
|
|
throw new Error(`Input "${input}" was not found.`);
|
|
}
|
|
formInput.simulate('change', { target: { value } });
|
|
component.update();
|
|
|
|
if (!isAsync) {
|
|
return;
|
|
}
|
|
return new Promise(resolve => setTimeout(resolve));
|
|
};
|
|
|
|
const setSelectValue: TestBed<T>['form']['setSelectValue'] = (
|
|
select,
|
|
value,
|
|
doUpdateComponent = true
|
|
) => {
|
|
const formSelect = typeof select === 'string' ? find(select) : (select as ReactWrapper);
|
|
|
|
if (!formSelect.length) {
|
|
throw new Error(`Select "${select}" was not found.`);
|
|
}
|
|
|
|
formSelect.simulate('change', { target: { value } });
|
|
|
|
if (doUpdateComponent) {
|
|
component.update();
|
|
}
|
|
};
|
|
|
|
const selectCheckBox: TestBed<T>['form']['selectCheckBox'] = (
|
|
testSubject,
|
|
isChecked = true
|
|
) => {
|
|
const checkBox = find(testSubject);
|
|
if (!checkBox.length) {
|
|
throw new Error(`"${testSubject}" was not found.`);
|
|
}
|
|
checkBox.simulate('change', { target: { checked: isChecked } });
|
|
};
|
|
|
|
const toggleEuiSwitch: TestBed<T>['form']['toggleEuiSwitch'] = testSubject => {
|
|
const checkBox = find(testSubject);
|
|
if (!checkBox.length) {
|
|
throw new Error(`"${testSubject}" was not found.`);
|
|
}
|
|
checkBox.simulate('click');
|
|
};
|
|
|
|
const setComboBoxValue: TestBed<T>['form']['setComboBoxValue'] = (
|
|
comboBoxTestSubject,
|
|
value
|
|
) => {
|
|
const comboBox = find(comboBoxTestSubject);
|
|
const formInput = findTestSubject(comboBox, 'comboBoxSearchInput');
|
|
setInputValue(formInput, value);
|
|
|
|
// keyCode 13 === ENTER
|
|
comboBox.simulate('keydown', { keyCode: 13 });
|
|
component.update();
|
|
};
|
|
|
|
const getErrorsMessages: TestBed<T>['form']['getErrorsMessages'] = () => {
|
|
const errorMessagesWrappers = component.find('.euiFormErrorText');
|
|
return errorMessagesWrappers.map(err => err.text());
|
|
};
|
|
|
|
/**
|
|
* ----------------------------------------------------------------
|
|
* Tables
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Parse an EUI table and return meta data information about its rows and colum content.
|
|
*
|
|
* @param tableTestSubject The data test subject of the EUI table
|
|
*/
|
|
const getMetaData: TestBed<T>['table']['getMetaData'] = tableTestSubject => {
|
|
const table = find(tableTestSubject);
|
|
|
|
if (!table.length) {
|
|
throw new Error(`Eui Table "${tableTestSubject}" not found.`);
|
|
}
|
|
|
|
const rows = table
|
|
.find('tr')
|
|
.slice(1) // we remove the first row as it is the table header
|
|
.map(row => ({
|
|
reactWrapper: row,
|
|
columns: row.find('td').map(col => ({
|
|
reactWrapper: col,
|
|
// We can't access the td value with col.text() because
|
|
// eui adds an extra div in td on mobile => (.euiTableRowCell__mobileHeader)
|
|
value: col.find('.euiTableCellContent').text(),
|
|
})),
|
|
}));
|
|
|
|
// Also output the raw cell values, in the following format: [[td0, td1, td2], [td0, td1, td2]]
|
|
const tableCellsValues = rows.map(({ columns }) => columns.map(col => col.value));
|
|
return { rows, tableCellsValues };
|
|
};
|
|
|
|
/**
|
|
* ----------------------------------------------------------------
|
|
* Router
|
|
* ----------------------------------------------------------------
|
|
*/
|
|
const navigateTo = (_url: string) => {
|
|
const url =
|
|
_url[0] === '#'
|
|
? _url.replace('#', '') // remove the beginning hash as the memory router does not understand them
|
|
: _url;
|
|
router.history.push(url);
|
|
};
|
|
|
|
return {
|
|
component,
|
|
exists,
|
|
find,
|
|
setProps,
|
|
waitFor,
|
|
waitForFn,
|
|
table: {
|
|
getMetaData,
|
|
},
|
|
form: {
|
|
setInputValue,
|
|
setSelectValue,
|
|
selectCheckBox,
|
|
toggleEuiSwitch,
|
|
setComboBoxValue,
|
|
getErrorsMessages,
|
|
},
|
|
router: {
|
|
navigateTo,
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
return setup;
|
|
};
|