Skip to content

Commit a6e20d0

Browse files
authored
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).
1 parent efcac24 commit a6e20d0

File tree

4 files changed

+425
-13
lines changed

4 files changed

+425
-13
lines changed

.flowconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.system=haste
2424

2525
esproposal.class_static_fields=enable
2626
esproposal.class_instance_fields=enable
27+
unsafe.enable_getters_and_setters=true
2728

2829
munge_underscores=false
2930

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: 250 additions & 13 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,
@@ -61,8 +64,29 @@ type TextInstance = {|
6164
tag: 'TEXT',
6265
|};
6366

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+
6475
const UPDATE_SIGNAL = {};
6576

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+
6690
function appendChild(
6791
parentInstance: Instance | Container,
6892
child: Instance | TextInstance,
@@ -225,18 +249,7 @@ var TestRenderer = ReactFiberReconciler({
225249

226250
useSyncScheduling: true,
227251

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

242255
var defaultTestOptions = {
@@ -325,6 +338,219 @@ function toTree(node: ?Fiber) {
325338
}
326339
}
327340

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+
328554
var ReactTestRendererFiber = {
329555
create(element: ReactElement<any>, options: TestRendererOptions) {
330556
var createNodeMock = defaultTestOptions.createNodeMock;
@@ -336,11 +562,22 @@ var ReactTestRendererFiber = {
336562
createNodeMock,
337563
tag: 'CONTAINER',
338564
};
339-
var root: ?FiberRoot = TestRenderer.createContainer(container);
565+
var root: FiberRoot | null = TestRenderer.createContainer(container);
340566
invariant(root != null, 'something went wrong');
341567
TestRenderer.updateContainer(element, root, null, null);
342568

343569
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+
},
344581
toJSON() {
345582
if (root == null || root.current == null || container == null) {
346583
return null;

0 commit comments

Comments
 (0)