Skip to content

Commit 0eb48f3

Browse files
committed
Add traversal for Fiber test renderer
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).
1 parent 230d412 commit 0eb48f3

3 files changed

Lines changed: 389 additions & 12 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 215 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
'use strict';
1515

1616
var ReactFiberReconciler = require('ReactFiberReconciler');
17+
var ReactFiberTreeReflection = require('ReactFiberTreeReflection');
1718
var ReactGenericBatching = require('ReactGenericBatching');
19+
var ReactTestRendererFeatureFlags = require('ReactTestRendererFeatureFlags');
1820
var emptyObject = require('fbjs/lib/emptyObject');
1921
var ReactTypeOfWork = require('ReactTypeOfWork');
2022
var invariant = require('fbjs/lib/invariant');
2123
var {
24+
Fragment,
2225
FunctionalComponent,
2326
ClassComponent,
2427
HostComponent,
@@ -63,6 +66,19 @@ type TextInstance = {|
6366

6467
const UPDATE_SIGNAL = {};
6568

69+
function getPublicInstance(inst: Instance | TextInstance): * {
70+
switch (inst.tag) {
71+
case 'INSTANCE':
72+
const createNodeMock = inst.rootContainerInstance.createNodeMock;
73+
return createNodeMock({
74+
type: inst.type,
75+
props: inst.props,
76+
});
77+
default:
78+
return inst;
79+
}
80+
}
81+
6682
function appendChild(
6783
parentInstance: Instance | Container,
6884
child: Instance | TextInstance,
@@ -225,18 +241,7 @@ var TestRenderer = ReactFiberReconciler({
225241

226242
useSyncScheduling: true,
227243

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-
},
244+
getPublicInstance,
240245
});
241246

242247
var defaultTestOptions = {
@@ -325,6 +330,193 @@ function toTree(node: ?Fiber) {
325330
}
326331
}
327332

333+
const fiberToWrapper = new WeakMap();
334+
function wrapFiber(fiber: Fiber): ReactTestInstance {
335+
let wrapper = fiberToWrapper.get(fiber);
336+
if (wrapper === undefined && fiber.alternate !== null) {
337+
wrapper = fiberToWrapper.get(fiber.alternate);
338+
}
339+
if (wrapper === undefined) {
340+
wrapper = new ReactTestInstance(fiber);
341+
fiberToWrapper.set(fiber, wrapper);
342+
}
343+
return wrapper;
344+
}
345+
346+
const validWrapperTypes = new Set([
347+
FunctionalComponent,
348+
ClassComponent,
349+
HostComponent,
350+
]);
351+
352+
class ReactTestInstance {
353+
_fiber: Fiber;
354+
355+
_currentFiber(): Fiber {
356+
const mounted = ReactFiberTreeReflection.isFiberMounted(this._fiber);
357+
invariant(mounted, 'Can\'t read properties from unmounted component');
358+
return ReactFiberTreeReflection.findCurrentFiberUsingSlowPath(this._fiber);
359+
}
360+
361+
constructor(fiber: Fiber) {
362+
invariant(
363+
validWrapperTypes.has(fiber.tag),
364+
'Unexpected object passed to ReactTestInstance constructor (tag: %s). ' +
365+
'This is probably a bug in React.',
366+
fiber.tag
367+
);
368+
this._fiber = fiber;
369+
}
370+
371+
get instance() {
372+
if (this._fiber.tag === HostComponent) {
373+
return getPublicInstance(this._fiber.stateNode);
374+
} else {
375+
return this._fiber.stateNode;
376+
}
377+
}
378+
379+
get type() {
380+
return this._fiber.type;
381+
}
382+
383+
get props(): Object {
384+
return this._currentFiber().memoizedProps;
385+
}
386+
387+
get parent(): Object {
388+
const parent = this._fiber.return;
389+
return parent.tag === HostRoot ? null : wrapFiber(parent);
390+
}
391+
392+
get children(): Array<ReactTestInstance | string> {
393+
const children = [];
394+
const startingNode = this._currentFiber();
395+
if (startingNode.child === null) {
396+
return children;
397+
}
398+
let node = startingNode;
399+
node.child.return = node;
400+
node = node.child;
401+
while (true) {
402+
let descend = false;
403+
switch (node.tag) {
404+
case FunctionalComponent:
405+
case ClassComponent:
406+
case HostComponent:
407+
children.push(wrapFiber(node));
408+
break;
409+
case HostText:
410+
children.push('' + node.memoizedProps);
411+
break;
412+
case Fragment:
413+
descend = true;
414+
break;
415+
default:
416+
invariant(
417+
false,
418+
'Unsupported component type %s in test renderer. ' +
419+
'This is probably a bug in React.',
420+
node.tag
421+
);
422+
}
423+
if (descend && node.child !== null) {
424+
node.child.return = node;
425+
node = node.child;
426+
continue;
427+
}
428+
while (node.sibling === null) {
429+
if (node.return === startingNode) {
430+
return children;
431+
}
432+
node = node.return;
433+
}
434+
node.sibling.return = node.return;
435+
node = node.sibling;
436+
}
437+
}
438+
439+
// Custom search functions
440+
find(predicate: Predicate): ReactTestInstance {
441+
return expectOne(
442+
this.findAll(predicate, {deep: false}),
443+
`matching custom predicate: ${predicate.toString()}`
444+
);
445+
}
446+
447+
findByType(type: any): ReactTestInstance {
448+
return expectOne(
449+
this.findAllByType(type, {deep: false}),
450+
`with node type: "${type.displayName || type.name}"`
451+
);
452+
}
453+
454+
findByProps(props: Object): ReactTestInstance {
455+
return expectOne(
456+
this.findAllByProps(props, {deep: false}),
457+
`with props: ${JSON.stringify(props)}`
458+
);
459+
}
460+
461+
findAll(predicate: Predicate, options: ?FindOptions = null): Array<ReactTestInstance> {
462+
return findAll(this, predicate, options);
463+
}
464+
465+
findAllByType(type: any, options: ?FindOptions = null): Array<ReactTestInstance> {
466+
return findAll(this, node => node.type === type, options);
467+
}
468+
469+
findAllByProps(props: Object, options: ?FindOptions = null): Array<ReactTestInstance> {
470+
return findAll(this, node => node.props && propsMatch(node.props, props), options);
471+
}
472+
}
473+
474+
function findAll(
475+
root: ReactTestInstance,
476+
predicate: Predicate,
477+
options: ?FindOptions,
478+
): Array<ReactTestInstance> {
479+
const deep = options ? options.deep : true;
480+
const results = [];
481+
482+
if (predicate(root)) {
483+
results.push(root);
484+
if (!deep) {
485+
return results;
486+
}
487+
}
488+
489+
for (const child of root.children) {
490+
if (typeof child === 'string') {
491+
continue;
492+
}
493+
results.push(...findAll(child, predicate, options));
494+
}
495+
496+
return results;
497+
}
498+
499+
function expectOne(all: Array<ReactTestInstance>, message: string): ReactTestInstance {
500+
if (all.length === 1) {
501+
return all[0];
502+
}
503+
504+
const prefix = all.length === 0
505+
? 'No instances found '
506+
: `Expected 1 but found ${all.length} instances `;
507+
508+
throw new Error(prefix + message);
509+
}
510+
511+
function propsMatch(props: Object, filter: Object): boolean {
512+
for (const key in filter) {
513+
if (props[key] !== filter[key]) {
514+
return false;
515+
}
516+
}
517+
return true;
518+
}
519+
328520
var ReactTestRendererFiber = {
329521
create(element: ReactElement<any>, options: TestRendererOptions) {
330522
var createNodeMock = defaultTestOptions.createNodeMock;
@@ -341,6 +533,17 @@ var ReactTestRendererFiber = {
341533
TestRenderer.updateContainer(element, root, null, null);
342534

343535
return {
536+
get root() {
537+
if (!ReactTestRendererFeatureFlags.enableTraversal) {
538+
throw new Error(
539+
'Test renderer traversal is experimental and not enabled'
540+
);
541+
}
542+
if (root === null) {
543+
throw new Error('Can\'t access .root on unmounted test renderer');
544+
}
545+
return wrapFiber(root.current.child);
546+
},
344547
toJSON() {
345548
if (root == null || root.current == null || container == null) {
346549
return null;

0 commit comments

Comments
 (0)