New TextInput-test that would have prevented S168585
Summary: Adds a basic test that would have prevented S168585. We should expand coverage of this and other components as well. Reviewed By: TheSavior Differential Revision: D13038064 fbshipit-source-id: 14cf4742efd53d7bca2a3f8d1c5c34ebc6227674
This commit is contained in:
parent
673ef39561
commit
a00940693e
|
@ -1127,7 +1127,7 @@ const TextInput = createReactClass({
|
|||
_onChange: function(event: Event) {
|
||||
// Make sure to fire the mostRecentEventCount first so it is already set on
|
||||
// native when the text value is set.
|
||||
if (this._inputRef) {
|
||||
if (this._inputRef && this._inputRef.setNativeProps) {
|
||||
this._inputRef.setNativeProps({
|
||||
mostRecentEventCount: event.nativeEvent.eventCount,
|
||||
});
|
||||
|
@ -1188,7 +1188,11 @@ const TextInput = createReactClass({
|
|||
nativeProps.selection = this.props.selection;
|
||||
}
|
||||
|
||||
if (Object.keys(nativeProps).length > 0 && this._inputRef) {
|
||||
if (
|
||||
Object.keys(nativeProps).length > 0 &&
|
||||
this._inputRef &&
|
||||
this._inputRef.setNativeProps
|
||||
) {
|
||||
this._inputRef.setNativeProps(nativeProps);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails oncall+react_native
|
||||
* @format
|
||||
* @flow-strict
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const ReactTestRenderer = require('react-test-renderer');
|
||||
const TextInput = require('TextInput');
|
||||
|
||||
import Component from '@reactions/component';
|
||||
|
||||
const {enter} = require('ReactNativeTestTools');
|
||||
|
||||
jest.unmock('TextInput');
|
||||
|
||||
describe('TextInput tests', () => {
|
||||
let input;
|
||||
let onChangeListener;
|
||||
let onChangeTextListener;
|
||||
const initialValue = 'initialValue';
|
||||
beforeEach(() => {
|
||||
onChangeListener = jest.fn();
|
||||
onChangeTextListener = jest.fn();
|
||||
const renderTree = ReactTestRenderer.create(
|
||||
<Component initialState={{text: initialValue}}>
|
||||
{({setState, state}) => (
|
||||
<TextInput
|
||||
value={state.text}
|
||||
onChangeText={text => {
|
||||
onChangeTextListener(text);
|
||||
setState({text});
|
||||
}}
|
||||
onChange={event => {
|
||||
onChangeListener(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Component>,
|
||||
);
|
||||
input = renderTree.root.findByType(TextInput);
|
||||
});
|
||||
it('has expected instance functions', () => {
|
||||
expect(input.instance.isFocused).toBeInstanceOf(Function); // Would have prevented S168585
|
||||
expect(input.instance.clear).toBeInstanceOf(Function);
|
||||
expect(input.instance.focus).toBeInstanceOf(Function);
|
||||
expect(input.instance.blur).toBeInstanceOf(Function);
|
||||
expect(input.instance.setNativeProps).toBeInstanceOf(Function);
|
||||
expect(input.instance.measure).toBeInstanceOf(Function);
|
||||
expect(input.instance.measureInWindow).toBeInstanceOf(Function);
|
||||
expect(input.instance.measureLayout).toBeInstanceOf(Function);
|
||||
});
|
||||
it('calls onChange callbacks', () => {
|
||||
expect(input.props.value).toBe(initialValue);
|
||||
const message = 'This is a test message';
|
||||
enter(input, message);
|
||||
expect(input.props.value).toBe(message);
|
||||
expect(onChangeTextListener).toHaveBeenCalledWith(message);
|
||||
expect(onChangeListener).toHaveBeenCalledWith({
|
||||
nativeEvent: {text: message},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const ReactTestRenderer = require('react-test-renderer');
|
||||
|
||||
const {Switch, Text, TextInput, VirtualizedList} = require('react-native');
|
||||
|
||||
import type {
|
||||
ReactTestInstance,
|
||||
ReactTestRendererNode,
|
||||
Predicate,
|
||||
} from 'react-test-renderer';
|
||||
|
||||
function byClickable(): Predicate {
|
||||
return withMessage(
|
||||
node =>
|
||||
// note: <Text /> lazy-mounts press handlers after the first press,
|
||||
// so this is a workaround for targeting text nodes.
|
||||
(node.type === Text &&
|
||||
node.props &&
|
||||
typeof node.props.onPress === 'function') ||
|
||||
// note: Special casing <Switch /> since it doesn't use touchable
|
||||
(node.type === Switch && node.props && node.props.disabled !== true) ||
|
||||
(node.instance &&
|
||||
typeof node.instance.touchableHandlePress === 'function'),
|
||||
'is clickable',
|
||||
);
|
||||
}
|
||||
|
||||
function byTestID(testID: string): Predicate {
|
||||
return withMessage(
|
||||
node => node.props && node.props.testID === testID,
|
||||
`testID prop equals ${testID}`,
|
||||
);
|
||||
}
|
||||
|
||||
function byTextMatching(regex: RegExp): Predicate {
|
||||
return withMessage(
|
||||
node => node.props && regex.exec(node.props.children),
|
||||
`text content matches ${regex.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
function enter(instance: ReactTestInstance, text: string) {
|
||||
const input = instance.findByType(TextInput);
|
||||
input.instance._onChange({nativeEvent: {text}});
|
||||
}
|
||||
|
||||
// Returns null if there is no error, otherwise returns an error message string.
|
||||
function maximumDepthError(
|
||||
tree: {toJSON: () => ReactTestRendererNode},
|
||||
maxDepthLimit: number,
|
||||
): ?string {
|
||||
const maxDepth = maximumDepthOfJSON(tree.toJSON());
|
||||
if (maxDepth > maxDepthLimit) {
|
||||
return (
|
||||
`maximumDepth of ${maxDepth} exceeded limit of ${maxDepthLimit} - this is a proxy ` +
|
||||
'metric to protect against stack overflow errors:\n\n' +
|
||||
'https://fburl.com/rn-view-stack-overflow.\n\n' +
|
||||
'To fix, you need to remove native layers from your hierarchy, such as unnecessary View ' +
|
||||
'wrappers.'
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function expectNoConsoleWarn() {
|
||||
(jest: $FlowFixMe).spyOn(console, 'warn').mockImplementation((...args) => {
|
||||
expect(args).toBeFalsy();
|
||||
});
|
||||
}
|
||||
|
||||
function expectNoConsoleError() {
|
||||
let hasNotFailed = true;
|
||||
(jest: $FlowFixMe).spyOn(console, 'error').mockImplementation((...args) => {
|
||||
if (hasNotFailed) {
|
||||
hasNotFailed = false; // set false to prevent infinite recursion
|
||||
expect(args).toBeFalsy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Takes a node from toJSON()
|
||||
function maximumDepthOfJSON(node: ReactTestRendererNode): number {
|
||||
if (node == null) {
|
||||
return 0;
|
||||
} else if (typeof node === 'string' || node.children == null) {
|
||||
return 1;
|
||||
} else {
|
||||
let maxDepth = 0;
|
||||
node.children.forEach(child => {
|
||||
maxDepth = Math.max(maximumDepthOfJSON(child) + 1, maxDepth);
|
||||
});
|
||||
return maxDepth;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAndEnforceStrictMode(element: React.Node) {
|
||||
expectNoConsoleError();
|
||||
return renderWithStrictMode(element);
|
||||
}
|
||||
|
||||
function renderWithStrictMode(element: React.Node) {
|
||||
const WorkAroundBugWithStrictModeInTestRenderer = prps => prps.children;
|
||||
const StrictMode = (React: $FlowFixMe).StrictMode;
|
||||
return ReactTestRenderer.create(
|
||||
<WorkAroundBugWithStrictModeInTestRenderer>
|
||||
<StrictMode>{element}</StrictMode>
|
||||
</WorkAroundBugWithStrictModeInTestRenderer>,
|
||||
);
|
||||
}
|
||||
|
||||
function tap(instance: ReactTestInstance) {
|
||||
const touchable = instance.find(byClickable());
|
||||
if (touchable.type === Text && touchable.props && touchable.props.onPress) {
|
||||
touchable.props.onPress();
|
||||
} else if (touchable.type === Switch && touchable.props) {
|
||||
const value = !touchable.props.value;
|
||||
const {onChange, onValueChange} = touchable.props;
|
||||
onChange && onChange({nativeEvent: {value}});
|
||||
onValueChange && onValueChange(value);
|
||||
} else {
|
||||
// Only tap when props.disabled isn't set (or there aren't any props)
|
||||
if (!touchable.props || !touchable.props.disabled) {
|
||||
touchable.instance.touchableHandlePress({nativeEvent: {}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom(instance: ReactTestInstance) {
|
||||
const list = instance.findByType(VirtualizedList);
|
||||
list.props && list.props.onEndReached();
|
||||
}
|
||||
|
||||
// To make error messages a little bit better, we attach a custom toString
|
||||
// implementation to a predicate
|
||||
function withMessage(fn: Predicate, message: string): Predicate {
|
||||
(fn: any).toString = () => message;
|
||||
return fn;
|
||||
}
|
||||
|
||||
export {byClickable};
|
||||
export {byTestID};
|
||||
export {byTextMatching};
|
||||
export {enter};
|
||||
export {expectNoConsoleWarn};
|
||||
export {expectNoConsoleError};
|
||||
export {maximumDepthError};
|
||||
export {maximumDepthOfJSON};
|
||||
export {renderAndEnforceStrictMode};
|
||||
export {renderWithStrictMode};
|
||||
export {scrollToBottom};
|
||||
export {tap};
|
||||
export {withMessage};
|
|
@ -206,6 +206,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@reactions/component": "^2.0.2",
|
||||
"async": "^2.4.0",
|
||||
"babel-eslint": "9.0.0",
|
||||
"babel-generator": "^6.26.0",
|
||||
|
|
|
@ -645,6 +645,10 @@
|
|||
lodash "^4.17.10"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@reactions/component@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@reactions/component/-/component-2.0.2.tgz#40f8c1c2c37baabe57a0c944edb9310dc1ec6642"
|
||||
|
||||
abab@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
|
||||
|
|
Loading…
Reference in New Issue