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:
Spencer Ahrens 2018-11-15 20:42:47 -08:00 committed by Facebook Github Bot
parent 673ef39561
commit a00940693e
5 changed files with 246 additions and 2 deletions

View File

@ -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);
}

View File

@ -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},
});
});
});

View File

@ -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};

View File

@ -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",

View File

@ -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"