Skip to content

Commit a009406

Browse files
sahrensfacebook-github-bot
authored andcommitted
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
1 parent 673ef39 commit a009406

File tree

5 files changed

+246
-2
lines changed

5 files changed

+246
-2
lines changed

Libraries/Components/TextInput/TextInput.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,7 @@ const TextInput = createReactClass({
11271127
_onChange: function(event: Event) {
11281128
// Make sure to fire the mostRecentEventCount first so it is already set on
11291129
// native when the text value is set.
1130-
if (this._inputRef) {
1130+
if (this._inputRef && this._inputRef.setNativeProps) {
11311131
this._inputRef.setNativeProps({
11321132
mostRecentEventCount: event.nativeEvent.eventCount,
11331133
});
@@ -1188,7 +1188,11 @@ const TextInput = createReactClass({
11881188
nativeProps.selection = this.props.selection;
11891189
}
11901190

1191-
if (Object.keys(nativeProps).length > 0 && this._inputRef) {
1191+
if (
1192+
Object.keys(nativeProps).length > 0 &&
1193+
this._inputRef &&
1194+
this._inputRef.setNativeProps
1195+
) {
11921196
this._inputRef.setNativeProps(nativeProps);
11931197
}
11941198

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails oncall+react_native
8+
* @format
9+
* @flow-strict
10+
*/
11+
12+
'use strict';
13+
14+
const React = require('React');
15+
const ReactTestRenderer = require('react-test-renderer');
16+
const TextInput = require('TextInput');
17+
18+
import Component from '@reactions/component';
19+
20+
const {enter} = require('ReactNativeTestTools');
21+
22+
jest.unmock('TextInput');
23+
24+
describe('TextInput tests', () => {
25+
let input;
26+
let onChangeListener;
27+
let onChangeTextListener;
28+
const initialValue = 'initialValue';
29+
beforeEach(() => {
30+
onChangeListener = jest.fn();
31+
onChangeTextListener = jest.fn();
32+
const renderTree = ReactTestRenderer.create(
33+
<Component initialState={{text: initialValue}}>
34+
{({setState, state}) => (
35+
<TextInput
36+
value={state.text}
37+
onChangeText={text => {
38+
onChangeTextListener(text);
39+
setState({text});
40+
}}
41+
onChange={event => {
42+
onChangeListener(event);
43+
}}
44+
/>
45+
)}
46+
</Component>,
47+
);
48+
input = renderTree.root.findByType(TextInput);
49+
});
50+
it('has expected instance functions', () => {
51+
expect(input.instance.isFocused).toBeInstanceOf(Function); // Would have prevented S168585
52+
expect(input.instance.clear).toBeInstanceOf(Function);
53+
expect(input.instance.focus).toBeInstanceOf(Function);
54+
expect(input.instance.blur).toBeInstanceOf(Function);
55+
expect(input.instance.setNativeProps).toBeInstanceOf(Function);
56+
expect(input.instance.measure).toBeInstanceOf(Function);
57+
expect(input.instance.measureInWindow).toBeInstanceOf(Function);
58+
expect(input.instance.measureLayout).toBeInstanceOf(Function);
59+
});
60+
it('calls onChange callbacks', () => {
61+
expect(input.props.value).toBe(initialValue);
62+
const message = 'This is a test message';
63+
enter(input, message);
64+
expect(input.props.value).toBe(message);
65+
expect(onChangeTextListener).toHaveBeenCalledWith(message);
66+
expect(onChangeListener).toHaveBeenCalledWith({
67+
nativeEvent: {text: message},
68+
});
69+
});
70+
});
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
const React = require('React');
14+
const ReactTestRenderer = require('react-test-renderer');
15+
16+
const {Switch, Text, TextInput, VirtualizedList} = require('react-native');
17+
18+
import type {
19+
ReactTestInstance,
20+
ReactTestRendererNode,
21+
Predicate,
22+
} from 'react-test-renderer';
23+
24+
function byClickable(): Predicate {
25+
return withMessage(
26+
node =>
27+
// note: <Text /> lazy-mounts press handlers after the first press,
28+
// so this is a workaround for targeting text nodes.
29+
(node.type === Text &&
30+
node.props &&
31+
typeof node.props.onPress === 'function') ||
32+
// note: Special casing <Switch /> since it doesn't use touchable
33+
(node.type === Switch && node.props && node.props.disabled !== true) ||
34+
(node.instance &&
35+
typeof node.instance.touchableHandlePress === 'function'),
36+
'is clickable',
37+
);
38+
}
39+
40+
function byTestID(testID: string): Predicate {
41+
return withMessage(
42+
node => node.props && node.props.testID === testID,
43+
`testID prop equals ${testID}`,
44+
);
45+
}
46+
47+
function byTextMatching(regex: RegExp): Predicate {
48+
return withMessage(
49+
node => node.props && regex.exec(node.props.children),
50+
`text content matches ${regex.toString()}`,
51+
);
52+
}
53+
54+
function enter(instance: ReactTestInstance, text: string) {
55+
const input = instance.findByType(TextInput);
56+
input.instance._onChange({nativeEvent: {text}});
57+
}
58+
59+
// Returns null if there is no error, otherwise returns an error message string.
60+
function maximumDepthError(
61+
tree: {toJSON: () => ReactTestRendererNode},
62+
maxDepthLimit: number,
63+
): ?string {
64+
const maxDepth = maximumDepthOfJSON(tree.toJSON());
65+
if (maxDepth > maxDepthLimit) {
66+
return (
67+
`maximumDepth of ${maxDepth} exceeded limit of ${maxDepthLimit} - this is a proxy ` +
68+
'metric to protect against stack overflow errors:\n\n' +
69+
'https://fburl.com/rn-view-stack-overflow.\n\n' +
70+
'To fix, you need to remove native layers from your hierarchy, such as unnecessary View ' +
71+
'wrappers.'
72+
);
73+
} else {
74+
return null;
75+
}
76+
}
77+
78+
function expectNoConsoleWarn() {
79+
(jest: $FlowFixMe).spyOn(console, 'warn').mockImplementation((...args) => {
80+
expect(args).toBeFalsy();
81+
});
82+
}
83+
84+
function expectNoConsoleError() {
85+
let hasNotFailed = true;
86+
(jest: $FlowFixMe).spyOn(console, 'error').mockImplementation((...args) => {
87+
if (hasNotFailed) {
88+
hasNotFailed = false; // set false to prevent infinite recursion
89+
expect(args).toBeFalsy();
90+
}
91+
});
92+
}
93+
94+
// Takes a node from toJSON()
95+
function maximumDepthOfJSON(node: ReactTestRendererNode): number {
96+
if (node == null) {
97+
return 0;
98+
} else if (typeof node === 'string' || node.children == null) {
99+
return 1;
100+
} else {
101+
let maxDepth = 0;
102+
node.children.forEach(child => {
103+
maxDepth = Math.max(maximumDepthOfJSON(child) + 1, maxDepth);
104+
});
105+
return maxDepth;
106+
}
107+
}
108+
109+
function renderAndEnforceStrictMode(element: React.Node) {
110+
expectNoConsoleError();
111+
return renderWithStrictMode(element);
112+
}
113+
114+
function renderWithStrictMode(element: React.Node) {
115+
const WorkAroundBugWithStrictModeInTestRenderer = prps => prps.children;
116+
const StrictMode = (React: $FlowFixMe).StrictMode;
117+
return ReactTestRenderer.create(
118+
<WorkAroundBugWithStrictModeInTestRenderer>
119+
<StrictMode>{element}</StrictMode>
120+
</WorkAroundBugWithStrictModeInTestRenderer>,
121+
);
122+
}
123+
124+
function tap(instance: ReactTestInstance) {
125+
const touchable = instance.find(byClickable());
126+
if (touchable.type === Text && touchable.props && touchable.props.onPress) {
127+
touchable.props.onPress();
128+
} else if (touchable.type === Switch && touchable.props) {
129+
const value = !touchable.props.value;
130+
const {onChange, onValueChange} = touchable.props;
131+
onChange && onChange({nativeEvent: {value}});
132+
onValueChange && onValueChange(value);
133+
} else {
134+
// Only tap when props.disabled isn't set (or there aren't any props)
135+
if (!touchable.props || !touchable.props.disabled) {
136+
touchable.instance.touchableHandlePress({nativeEvent: {}});
137+
}
138+
}
139+
}
140+
141+
function scrollToBottom(instance: ReactTestInstance) {
142+
const list = instance.findByType(VirtualizedList);
143+
list.props && list.props.onEndReached();
144+
}
145+
146+
// To make error messages a little bit better, we attach a custom toString
147+
// implementation to a predicate
148+
function withMessage(fn: Predicate, message: string): Predicate {
149+
(fn: any).toString = () => message;
150+
return fn;
151+
}
152+
153+
export {byClickable};
154+
export {byTestID};
155+
export {byTextMatching};
156+
export {enter};
157+
export {expectNoConsoleWarn};
158+
export {expectNoConsoleError};
159+
export {maximumDepthError};
160+
export {maximumDepthOfJSON};
161+
export {renderAndEnforceStrictMode};
162+
export {renderWithStrictMode};
163+
export {scrollToBottom};
164+
export {tap};
165+
export {withMessage};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
},
207207
"devDependencies": {
208208
"@babel/core": "^7.0.0",
209+
"@reactions/component": "^2.0.2",
209210
"async": "^2.4.0",
210211
"babel-eslint": "9.0.0",
211212
"babel-generator": "^6.26.0",

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@
645645
lodash "^4.17.10"
646646
to-fast-properties "^2.0.0"
647647

648+
"@reactions/component@^2.0.2":
649+
version "2.0.2"
650+
resolved "https://registry.yarnpkg.com/@reactions/component/-/component-2.0.2.tgz#40f8c1c2c37baabe57a0c944edb9310dc1ec6642"
651+
648652
abab@^2.0.0:
649653
version "2.0.0"
650654
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"

0 commit comments

Comments
 (0)