Skip to content

Commit

Permalink
[form lib] Fix issues + add test coverage (#64647)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored May 14, 2020
1 parent 65186b3 commit 6a3db2c
Show file tree
Hide file tree
Showing 53 changed files with 2,452 additions and 923 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Components using the react-intl module require access to the intl context.
* This is not available when mounting single components in Enzyme.
* These helper functions aim to address that and wrap a valid,
* intl context around them.
*/

import { I18nProvider, InjectedIntl, intlShape } from '@kbn/i18n/react';
import { mount, ReactWrapper, render, shallow } from 'enzyme';
import React, { ReactElement, ValidationMap } from 'react';
import { act as reactAct } from 'react-dom/test-utils';

// Use fake component to extract `intl` property to use in tests.
const { intl } = (mount(
<I18nProvider>
<br />
</I18nProvider>
).find('IntlProvider') as ReactWrapper<{}, {}, import('react-intl').IntlProvider>)
.instance()
.getChildContext();

function getOptions(context = {}, childContextTypes = {}, props = {}) {
return {
context: {
...context,
intl,
},
childContextTypes: {
...childContextTypes,
intl: intlShape,
},
...props,
};
}

/**
* When using React-Intl `injectIntl` on components, props.intl is required.
*/
function nodeWithIntlProp<T>(node: ReactElement<T>): ReactElement<T & { intl: InjectedIntl }> {
return React.cloneElement<any>(node, { intl });
}

/**
* Creates the wrapper instance using shallow with provided intl object into context
*
* @param node The React element or cheerio wrapper
* @param options properties to pass into shallow wrapper
* @return The wrapper instance around the rendered output with intl object in context
*/
export function shallowWithIntl<T>(
node: ReactElement<T>,
{
context,
childContextTypes,
...props
}: {
context?: any;
childContextTypes?: ValidationMap<any>;
} = {}
) {
const options = getOptions(context, childContextTypes, props);

return shallow(nodeWithIntlProp(node), options);
}

/**
* Creates the wrapper instance using mount with provided intl object into context
*
* @param node The React element or cheerio wrapper
* @param options properties to pass into mount wrapper
* @return The wrapper instance around the rendered output with intl object in context
*/
export function mountWithIntl<T>(
node: ReactElement<T>,
{
context,
childContextTypes,
...props
}: {
context?: any;
childContextTypes?: ValidationMap<any>;
} = {}
) {
const options = getOptions(context, childContextTypes, props);

return mount(nodeWithIntlProp(node), options);
}

/**
* Creates the wrapper instance using render with provided intl object into context
*
* @param node The React element or cheerio wrapper
* @param options properties to pass into render wrapper
* @return The wrapper instance around the rendered output with intl object in context
*/
export function renderWithIntl<T>(
node: ReactElement<T>,
{
context,
childContextTypes,
...props
}: {
context?: any;
childContextTypes?: ValidationMap<any>;
} = {}
) {
const options = getOptions(context, childContextTypes, props);

return render(nodeWithIntlProp(node), options);
}

/**
* A wrapper object to provide access to the state of a hook under test and to
* enable interaction with that hook.
*/
interface ReactHookWrapper<Args, HookValue> {
/* Ensures that async React operations have settled before and after the
* given actor callback is called. The actor callback arguments provide easy
* access to the last hook value and allow for updating the arguments passed
* to the hook body to trigger reevaluation.
*/
act: (actor: (lastHookValue: HookValue, setArgs: (args: Args) => void) => void) => void;
/* The enzyme wrapper around the test component. */
component: ReactWrapper;
/* The most recent value return the by test harness of the hook. */
getLastHookValue: () => HookValue;
/* The jest Mock function that receives the hook values for introspection. */
hookValueCallback: jest.Mock;
}

/**
* Allows for execution of hooks inside of a test component which records the
* returned values.
*
* @param body A function that calls the hook and returns data derived from it
* @param WrapperComponent A component that, if provided, will be wrapped
* around the test component. This can be useful to provide context values.
* @return {ReactHookWrapper} An object providing access to the hook state and
* functions to interact with it.
*/
export const mountHook = <Args extends {}, HookValue extends any>(
body: (args: Args) => HookValue,
WrapperComponent?: React.ComponentType,
initialArgs: Args = {} as Args
): ReactHookWrapper<Args, HookValue> => {
const hookValueCallback = jest.fn();
let component!: ReactWrapper;

const act: ReactHookWrapper<Args, HookValue>['act'] = actor => {
reactAct(() => {
actor(getLastHookValue(), (args: Args) => component.setProps(args));
component.update();
});
};

const getLastHookValue = () => {
const calls = hookValueCallback.mock.calls;
if (calls.length <= 0) {
throw Error('No recent hook value present.');
}
return calls[calls.length - 1][0];
};

const HookComponent = (props: Args) => {
hookValueCallback(body(props));
return null;
};
const TestComponent: React.FunctionComponent<Args> = args =>
WrapperComponent ? (
<WrapperComponent>
<HookComponent {...args} />
</WrapperComponent>
) : (
<HookComponent {...args} />
);

reactAct(() => {
component = mount(<TestComponent {...initialArgs} />);
});

return {
act,
component,
getLastHookValue,
hookValueCallback,
};
};

export const nextTick = () => new Promise(res => process.nextTick(res));
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ReactWrapper } from 'enzyme';

type Matcher = '=' | '~=' | '|=' | '^=' | '$=' | '*=';

const MATCHERS: Matcher[] = [
'=', // Exact match
'~=', // Exists in a space-separated list
'|=', // Begins with substring, followed by '-'
'^=', // Begins with substring
'$=', // Ends with substring
'*=',
];

/**
* Find node which matches a specific test subject selector. Returns ReactWrappers around DOM element,
* https://github.com/airbnb/enzyme/tree/master/docs/api/ReactWrapper.
* Common use cases include calling simulate or getDOMNode on the returned ReactWrapper.
*
* The ~= matcher looks for the value in space-separated list, allowing support for multiple data-test-subj
* values on a single element. See https://www.w3.org/TR/selectors-3/#attribute-selectors for more
* info on the other possible matchers.
*
* @param reactWrapper The React wrapper to search in
* @param testSubjectSelector The data test subject selector
* @param matcher optional matcher
*/
export const findTestSubject = <T = string>(
reactWrapper: ReactWrapper,
testSubjectSelector: T,
matcher: Matcher = '~='
) => {
if (!MATCHERS.includes(matcher)) {
throw new Error(
'Matcher '
.concat(matcher, ' not found in list of allowed matchers: ')
.concat(MATCHERS.join(' '))
);
}

const testSubject = reactWrapper.find(`[data-test-subj${matcher}"${testSubjectSelector}"]`);
// Restores Enzyme 2's find behavior, which was to only return ReactWrappers around DOM elements.
// Enzyme 3 returns ReactWrappers around both DOM elements and React components.
// https://github.com/airbnb/enzyme/issues/1174

return testSubject.hostNodes();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export { mountWithIntl } from './enzyme_helpers';

export { findTestSubject } from './find_test_subject';

export { WithStore } from './redux_helpers';

export { WithMemoryRouter, WithRoute, reactRouterMock } from './router_helpers';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React, { ComponentType } from 'react';
import { Provider } from 'react-redux';

export const WithStore = (store: any) => (WrappedComponent: ComponentType) => (props: any) => (
<Provider store={store}>
<WrappedComponent {...props} />
</Provider>
);
Loading

0 comments on commit 6a3db2c

Please sign in to comment.