Add traversal for Fiber test renderer (#10377)
Not clear the path to shipping this but this gives us a migration path internally that we need right now (replaces https://fburl.com/udq9ksvk).
Ben Alpert committed
Aug 7, 2017 at 16:47 UTC
a6e20d0bd83996f33a8c9580fa58876ae30cf89c
4 files changed
+425
-13
.flowconfig
+1
@@ -24,6 +24,7 @@ module.system=haste
24
25
esproposal.class_static_fields=enable
26
esproposal.class_instance_fields=enable
27
+unsafe.enable_getters_and_setters=true
28
29
munge_underscores=false
30
src/renderers/testing/ReactTestRendererFeatureFlags.js
new
+17
@@ -0,0 +1,17 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ReactTestRendererFeatureFlags
10
+ * @flow
11
+ */
12
+
13
+const ReactTestRendererFeatureFlags = {
14
+ enableTraversal: false,
15
+};
16
+
17
+module.exports = ReactTestRendererFeatureFlags;
src/renderers/testing/ReactTestRendererFiberEntry.js
+250
-13
@@ -14,11 +14,14 @@
14
'use strict';
15
16
var ReactFiberReconciler = require('ReactFiberReconciler');
17
+var ReactFiberTreeReflection = require('ReactFiberTreeReflection');
18
var ReactGenericBatching = require('ReactGenericBatching');
19
+var ReactTestRendererFeatureFlags = require('ReactTestRendererFeatureFlags');
20
var emptyObject = require('fbjs/lib/emptyObject');
21
var ReactTypeOfWork = require('ReactTypeOfWork');
22
var invariant = require('fbjs/lib/invariant');
23
var {
24
+ Fragment,
25
FunctionalComponent,
26
ClassComponent,
27
HostComponent,
@@ -61,8 +64,29 @@ type TextInstance = {|
64
tag: 'TEXT',
65
|};
66
67
+type FindOptions = $Shape<{
68
+ // performs a "greedy" search: if a matching node is found, will continue
69
+ // to search within the matching node's children. (default: true)
70
+ deep: boolean,
71
+}>;
72
+
73
+export type Predicate = (node: ReactTestInstance) => ?boolean;
74
+
75
const UPDATE_SIGNAL = {};
76
77
+function getPublicInstance(inst: Instance | TextInstance): * {
78
+ switch (inst.tag) {
79
+ case 'INSTANCE':
80
+ const createNodeMock = inst.rootContainerInstance.createNodeMock;
81
+ return createNodeMock({
82
+ type: inst.type,
83
+ props: inst.props,
84
+ });
85
+ default:
86
+ return inst;
87
+ }
88
+}
89
+
90
function appendChild(
91
parentInstance: Instance | Container,
92
child: Instance | TextInstance,
@@ -225,18 +249,7 @@ var TestRenderer = ReactFiberReconciler({
249
250
useSyncScheduling: true,
251
228
- getPublicInstance(inst: Instance | TextInstance): * {
229
- switch (inst.tag) {
230
- case 'INSTANCE':
231
- const createNodeMock = inst.rootContainerInstance.createNodeMock;
232
- return createNodeMock({
233
- type: inst.type,
234
- props: inst.props,
235
- });
236
- default:
237
- return inst;
238
- }
239
- },
252
+ getPublicInstance,
253
});
254
255
var defaultTestOptions = {
@@ -325,6 +338,219 @@ function toTree(node: ?Fiber) {
338
}
339
}
340
341
+const fiberToWrapper = new WeakMap();
342
+function wrapFiber(fiber: Fiber): ReactTestInstance {
343
+ let wrapper = fiberToWrapper.get(fiber);
344
+ if (wrapper === undefined && fiber.alternate !== null) {
345
+ wrapper = fiberToWrapper.get(fiber.alternate);
346
+ }
347
+ if (wrapper === undefined) {
348
+ wrapper = new ReactTestInstance(fiber);
349
+ fiberToWrapper.set(fiber, wrapper);
350
+ }
351
+ return wrapper;
352
+}
353
+
354
+const validWrapperTypes = new Set([
355
+ FunctionalComponent,
356
+ ClassComponent,
357
+ HostComponent,
358
+]);
359
+
360
+class ReactTestInstance {
361
+ _fiber: Fiber;
362
+
363
+ _currentFiber(): Fiber {
364
+ // Throws if this component has been unmounted.
365
+ const fiber = ReactFiberTreeReflection.findCurrentFiberUsingSlowPath(
366
+ this._fiber,
367
+ );
368
+ invariant(
369
+ fiber !== null,
370
+ "Can't read from currently-mounting component. This error is likely " +
371
+ 'caused by a bug in React. Please file an issue.',
372
+ );
373
+ return fiber;
374
+ }
375
+
376
+ constructor(fiber: Fiber) {
377
+ invariant(
378
+ validWrapperTypes.has(fiber.tag),
379
+ 'Unexpected object passed to ReactTestInstance constructor (tag: %s). ' +
380
+ 'This is probably a bug in React.',
381
+ fiber.tag,
382
+ );
383
+ this._fiber = fiber;
384
+ }
385
+
386
+ get instance() {
387
+ if (this._fiber.tag === HostComponent) {
388
+ return getPublicInstance(this._fiber.stateNode);
389
+ } else {
390
+ return this._fiber.stateNode;
391
+ }
392
+ }
393
+
394
+ get type() {
395
+ return this._fiber.type;
396
+ }
397
+
398
+ get props(): Object {
399
+ return this._currentFiber().memoizedProps;
400
+ }
401
+
402
+ get parent(): ?ReactTestInstance {
403
+ const parent = this._fiber.return;
404
+ return parent === null || parent.tag === HostRoot
405
+ ? null
406
+ : wrapFiber(parent);
407
+ }
408
+
409
+ get children(): Array<ReactTestInstance | string> {
410
+ const children = [];
411
+ const startingNode = this._currentFiber();
412
+ let node: Fiber = startingNode;
413
+ if (node.child === null) {
414
+ return children;
415
+ }
416
+ node.child.return = node;
417
+ node = node.child;
418
+ outer: while (true) {
419
+ let descend = false;
420
+ switch (node.tag) {
421
+ case FunctionalComponent:
422
+ case ClassComponent:
423
+ case HostComponent:
424
+ children.push(wrapFiber(node));
425
+ break;
426
+ case HostText:
427
+ children.push('' + node.memoizedProps);
428
+ break;
429
+ case Fragment:
430
+ descend = true;
431
+ break;
432
+ default:
433
+ invariant(
434
+ false,
435
+ 'Unsupported component type %s in test renderer. ' +
436
+ 'This is probably a bug in React.',
437
+ node.tag,
438
+ );
439
+ }
440
+ if (descend && node.child !== null) {
441
+ node.child.return = node;
442
+ node = node.child;
443
+ continue;
444
+ }
445
+ while (node.sibling === null) {
446
+ if (node.return === startingNode) {
447
+ break outer;
448
+ }
449
+ node = (node.return: any);
450
+ }
451
+ (node.sibling: any).return = node.return;
452
+ node = (node.sibling: any);
453
+ }
454
+ return children;
455
+ }
456
+
457
+ // Custom search functions
458
+ find(predicate: Predicate): ReactTestInstance {
459
+ return expectOne(
460
+ this.findAll(predicate, {deep: false}),
461
+ `matching custom predicate: ${predicate.toString()}`,
462
+ );
463
+ }
464
+
465
+ findByType(type: any): ReactTestInstance {
466
+ return expectOne(
467
+ this.findAllByType(type, {deep: false}),
468
+ `with node type: "${type.displayName || type.name}"`,
469
+ );
470
+ }
471
+
472
+ findByProps(props: Object): ReactTestInstance {
473
+ return expectOne(
474
+ this.findAllByProps(props, {deep: false}),
475
+ `with props: ${JSON.stringify(props)}`,
476
+ );
477
+ }
478
+
479
+ findAll(
480
+ predicate: Predicate,
481
+ options: ?FindOptions = null,
482
+ ): Array<ReactTestInstance> {
483
+ return findAll(this, predicate, options);
484
+ }
485
+
486
+ findAllByType(
487
+ type: any,
488
+ options: ?FindOptions = null,
489
+ ): Array<ReactTestInstance> {
490
+ return findAll(this, node => node.type === type, options);
491
+ }
492
+
493
+ findAllByProps(
494
+ props: Object,
495
+ options: ?FindOptions = null,
496
+ ): Array<ReactTestInstance> {
497
+ return findAll(
498
+ this,
499
+ node => node.props && propsMatch(node.props, props),
500
+ options,
501
+ );
502
+ }
503
+}
504
+
505
+function findAll(
506
+ root: ReactTestInstance,
507
+ predicate: Predicate,
508
+ options: ?FindOptions,
509
+): Array<ReactTestInstance> {
510
+ const deep = options ? options.deep : true;
511
+ const results = [];
512
+
513
+ if (predicate(root)) {
514
+ results.push(root);
515
+ if (!deep) {
516
+ return results;
517
+ }
518
+ }
519
+
520
+ for (const child of root.children) {
521
+ if (typeof child === 'string') {
522
+ continue;
523
+ }
524
+ results.push(...findAll(child, predicate, options));
525
+ }
526
+
527
+ return results;
528
+}
529
+
530
+function expectOne(
531
+ all: Array<ReactTestInstance>,
532
+ message: string,
533
+): ReactTestInstance {
534
+ if (all.length === 1) {
535
+ return all[0];
536
+ }
537
+
538
+ const prefix = all.length === 0
539
+ ? 'No instances found '
540
+ : `Expected 1 but found ${all.length} instances `;
541
+
542
+ throw new Error(prefix + message);
543
+}
544
+
545
+function propsMatch(props: Object, filter: Object): boolean {
546
+ for (const key in filter) {
547
+ if (props[key] !== filter[key]) {
548
+ return false;
549
+ }
550
+ }
551
+ return true;
552
+}
553
+
554
var ReactTestRendererFiber = {
555
create(element: ReactElement<any>, options: TestRendererOptions) {
556
var createNodeMock = defaultTestOptions.createNodeMock;
@@ -336,11 +562,22 @@ var ReactTestRendererFiber = {
562
createNodeMock,
563
tag: 'CONTAINER',
564
};
339
- var root: ?FiberRoot = TestRenderer.createContainer(container);
565
+ var root: FiberRoot | null = TestRenderer.createContainer(container);
566
invariant(root != null, 'something went wrong');
567
TestRenderer.updateContainer(element, root, null, null);
568
569
return {
570
+ get root() {
571
+ if (!ReactTestRendererFeatureFlags.enableTraversal) {
572
+ throw new Error(
573
+ 'Test renderer traversal is experimental and not enabled',
574
+ );
575
+ }
576
+ if (root === null || root.current.child === null) {
577
+ throw new Error("Can't access .root on unmounted test renderer");
578
+ }
579
+ return wrapFiber(root.current.child);
580
+ },
581
toJSON() {
582
if (root == null || root.current == null || container == null) {
583
return null;
src/renderers/testing/__tests__/ReactTestRendererTraversal-test.js
new
+157
@@ -0,0 +1,157 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+const React = require('react');
15
+let ReactTestRenderer;
16
+
17
+const RCTView = 'RCTView';
18
+const View = props => <RCTView {...props} />;
19
+
20
+describe('ReactTestRendererTraversal', () => {
21
+ beforeEach(() => {
22
+ jest.resetModules();
23
+ const ReactTestRendererFeatureFlags = require('ReactTestRendererFeatureFlags');
24
+ ReactTestRendererFeatureFlags.enableTraversal = true;
25
+ ReactTestRenderer = require('react-test-renderer');
26
+ });
27
+
28
+ class Example extends React.Component {
29
+ render() {
30
+ return (
31
+ <View>
32
+ <View foo="foo">
33
+ <View bar="bar" />
34
+ <View bar="bar" baz="baz" itself="itself" />
35
+ <View />
36
+ <ExampleSpread bar="bar" />
37
+ <ExampleFn bar="bar" bing="bing" />
38
+ <ExampleNull bar="bar" />
39
+ <ExampleNull null="null">
40
+ <View void="void" />
41
+ <View void="void" />
42
+ </ExampleNull>
43
+ </View>
44
+ </View>
45
+ );
46
+ }
47
+ }
48
+ class ExampleSpread extends React.Component {
49
+ render = () => <View {...this.props} />;
50
+ }
51
+ const ExampleFn = props => <View baz="baz" />;
52
+ const ExampleNull = props => null;
53
+
54
+ it('initializes', () => {
55
+ const render = ReactTestRenderer.create(<Example />);
56
+ const hasFooProp = node => node.props.hasOwnProperty('foo');
57
+
58
+ // assert .props, .type and .parent attributes
59
+ const foo = render.root.find(hasFooProp);
60
+ expect(foo.props.children).toHaveLength(7);
61
+ expect(foo.type).toBe(View);
62
+ expect(render.root.parent).toBe(null);
63
+ expect(foo.children[0].parent).toBe(foo);
64
+ });
65
+
66
+ it('searches via .find() / .findAll()', () => {
67
+ const render = ReactTestRenderer.create(<Example />);
68
+ const hasFooProp = node => node.props.hasOwnProperty('foo');
69
+ const hasBarProp = node => node.props.hasOwnProperty('bar');
70
+ const hasBazProp = node => node.props.hasOwnProperty('baz');
71
+ const hasBingProp = node => node.props.hasOwnProperty('bing');
72
+ const hasNullProp = node => node.props.hasOwnProperty('null');
73
+ const hasVoidProp = node => node.props.hasOwnProperty('void');
74
+ const hasItselfProp = node => node.props.hasOwnProperty('itself');
75
+
76
+ expect(() => render.root.find(hasFooProp)).not.toThrow(); // 1 match
77
+ expect(() => render.root.find(hasBarProp)).toThrow(); // >1 matches
78
+ expect(() => render.root.find(hasBazProp)).toThrow(); // >1 matches
79
+ expect(() => render.root.find(hasBingProp)).not.toThrow(); // 1 match
80
+ expect(() => render.root.find(hasNullProp)).not.toThrow(); // 1 match
81
+ expect(() => render.root.find(hasVoidProp)).toThrow(); // 0 matches
82
+
83
+ // same assertion as .find(), but confirm length
84
+ expect(render.root.findAll(hasFooProp, {deep: false})).toHaveLength(1);
85
+ expect(render.root.findAll(hasBarProp, {deep: false})).toHaveLength(5);
86
+ expect(render.root.findAll(hasBazProp, {deep: false})).toHaveLength(2);
87
+ expect(render.root.findAll(hasBingProp, {deep: false})).toHaveLength(1);
88
+ expect(render.root.findAll(hasNullProp, {deep: false})).toHaveLength(1);
89
+ expect(render.root.findAll(hasVoidProp, {deep: false})).toHaveLength(0);
90
+
91
+ // note: with {deep: true}, .findAll() will continue to
92
+ // search children, even after finding a match
93
+ expect(render.root.findAll(hasFooProp)).toHaveLength(2);
94
+ expect(render.root.findAll(hasBarProp)).toHaveLength(9);
95
+ expect(render.root.findAll(hasBazProp)).toHaveLength(4);
96
+ expect(render.root.findAll(hasBingProp)).toHaveLength(1); // no spread
97
+ expect(render.root.findAll(hasNullProp)).toHaveLength(1); // no spread
98
+ expect(render.root.findAll(hasVoidProp)).toHaveLength(0);
99
+
100
+ const bing = render.root.find(hasBingProp);
101
+ expect(bing.find(hasBarProp)).toBe(bing);
102
+ expect(bing.find(hasBingProp)).toBe(bing);
103
+ expect(bing.findAll(hasBazProp, {deep: false})).toHaveLength(1);
104
+ expect(bing.findAll(hasBazProp)).toHaveLength(2);
105
+
106
+ const foo = render.root.find(hasFooProp);
107
+ expect(foo.findAll(hasFooProp, {deep: false})).toHaveLength(1);
108
+ expect(foo.findAll(hasFooProp)).toHaveLength(2);
109
+
110
+ const itself = foo.find(hasItselfProp);
111
+ expect(itself.find(hasBarProp)).toBe(itself);
112
+ expect(itself.find(hasBazProp)).toBe(itself);
113
+ expect(itself.findAll(hasBazProp, {deep: false})).toHaveLength(1);
114
+ expect(itself.findAll(hasBazProp)).toHaveLength(2);
115
+ });
116
+
117
+ it('searches via .findByType() / .findAllByType()', () => {
118
+ const render = ReactTestRenderer.create(<Example />);
119
+
120
+ expect(() => render.root.findByType(ExampleFn)).not.toThrow(); // 1 match
121
+ expect(() => render.root.findByType(View)).not.toThrow(); // 1 match
122
+ // note: there are clearly multiple <View /> in general, but there
123
+ // is only one being rendered at root node level
124
+ expect(() => render.root.findByType(ExampleNull)).toThrow(); // 2 matches
125
+
126
+ expect(render.root.findAllByType(ExampleFn)).toHaveLength(1);
127
+ expect(render.root.findAllByType(View, {deep: false})).toHaveLength(1);
128
+ expect(render.root.findAllByType(View)).toHaveLength(7);
129
+ expect(render.root.findAllByType(ExampleNull)).toHaveLength(2);
130
+
131
+ const nulls = render.root.findAllByType(ExampleNull);
132
+ expect(nulls[0].findAllByType(View)).toHaveLength(0);
133
+ expect(nulls[1].findAllByType(View)).toHaveLength(0);
134
+
135
+ const fn = render.root.findAllByType(ExampleFn);
136
+ expect(fn[0].findAllByType(View)).toHaveLength(1);
137
+ });
138
+
139
+ it('searches via .findByProps() / .findAllByProps()', () => {
140
+ const render = ReactTestRenderer.create(<Example />);
141
+ const foo = 'foo';
142
+ const bar = 'bar';
143
+ const baz = 'baz';
144
+
145
+ expect(() => render.root.findByProps({foo})).not.toThrow(); // 1 match
146
+ expect(() => render.root.findByProps({bar})).toThrow(); // >1 matches
147
+ expect(() => render.root.findByProps({baz})).toThrow(); // >1 matches
148
+
149
+ expect(render.root.findAllByProps({foo}, {deep: false})).toHaveLength(1);
150
+ expect(render.root.findAllByProps({bar}, {deep: false})).toHaveLength(5);
151
+ expect(render.root.findAllByProps({baz}, {deep: false})).toHaveLength(2);
152
+
153
+ expect(render.root.findAllByProps({foo})).toHaveLength(2);
154
+ expect(render.root.findAllByProps({bar})).toHaveLength(9);
155
+ expect(render.root.findAllByProps({baz})).toHaveLength(4);
156
+ });
157
+});