@samitouri / QOS-React / commits / a6e20d0bd8

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