Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createComponentMock option to test renderer #8982

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,8 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js
* toTree() renders complicated trees of composites and hosts
* can update text nodes when rendered as root
* can render and update root fragments
* allows createComponentMock option to be passed in, simulating shallow
* createComponentMock can mock out specific components

src/shared/utils/__tests__/KeyEscapeUtils-test.js
* should properly escape and wrap user defined keys
Expand Down
2 changes: 2 additions & 0 deletions src/renderers/shared/fiber/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type Fiber = {
_debugSource?: Source | null,
_debugOwner?: Fiber | ReactInstance | null, // Stack compatible
_debugIsCurrentlyTiming?: boolean,
_unmockedType?: any,

// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
Expand Down Expand Up @@ -222,6 +223,7 @@ var createFiber = function(tag: TypeOfWork, key: null | string): Fiber {
fiber._debugSource = null;
fiber._debugOwner = null;
fiber._debugIsCurrentlyTiming = false;
fiber._unmockedType = null;
if (typeof Object.preventExtensions === 'function') {
Object.preventExtensions(fiber);
}
Expand Down
17 changes: 17 additions & 0 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
memoizeState,
);

if (__DEV__) {
var hasMockingBehavior = typeof config.mockComponent === 'function';
var mockComponent = config.mockComponent || (() => {});
}

function markChildAsProgressed(current, workInProgress, priorityLevel) {
// We now have clones. Let's store them as the currently progressed work.
workInProgress.progressedChild = workInProgress.child;
Expand Down Expand Up @@ -794,6 +799,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
workInProgress.child = workInProgress.progressedChild;
}

if (__DEV__) {
if (hasMockingBehavior) {
switch (workInProgress.tag) {
case IndeterminateComponent:
case FunctionalComponent:
case ClassComponent:
mockComponent(workInProgress, hostContext.getRootHostContainer());
break;
}
}
}

switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(
Expand Down
2 changes: 2 additions & 0 deletions src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
resetAfterCommit(): void,

useSyncScheduling?: boolean,

mockComponent?: Function,
};

export type Reconciler<C, I, TI> = {
Expand Down
86 changes: 81 additions & 5 deletions src/renderers/testing/ReactTestRendererFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ var emptyObject = require('fbjs/lib/emptyObject');
var ReactTypeOfWork = require('ReactTypeOfWork');
var invariant = require('fbjs/lib/invariant');
var {
IndeterminateComponent,
FunctionalComponent,
ClassComponent,
HostComponent,
Fragment,
HostText,
HostRoot,
} = ReactTypeOfWork;
Expand All @@ -41,6 +43,7 @@ type ReactTestRendererNode = ReactTestRendererJSON | string;
type Container = {|
children: Array<Instance | TextInstance>,
createNodeMock: Function,
createComponentMock: Function,
tag: 'CONTAINER',
|};

Expand Down Expand Up @@ -217,6 +220,29 @@ var TestRenderer = ReactFiberReconciler({
setTimeout(fn, 0, {timeRemaining: Infinity});
},

mockComponent(component: Fiber, rootContainer: Container) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not be exposing the Fiber data structure to renderers. I have some refactoring ideas where this will break. Instead, pass the individual pieces as arguments.

invariant(
component._unmockedType === null,
'Trying to mock an already mocked component',
);
const mockedFn = rootContainer.createComponentMock({
type: component.type,
props: component.pendingProps,
});
invariant(
typeof mockedFn === 'function',
'createComponentMock() must return a function. Found %s instead.',
typeof mockedFn,
);
if (mockedFn !== component.type) {
component._unmockedType = component.type;
component.type = mockedFn;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never mutate the Fiber here. We should instead return the mock and let the algorithm initialize it in the appropriate place.

// force the fiber to be indeterminate so that users can mock a class component
// into a functional component and vice versa
component.tag = IndeterminateComponent;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indicates to me that we're not overriding it soon enough. Maybe we need to do the override earlier?

This also doesn't allow you to mock it to a string to render a host node for example.

}
},

useSyncScheduling: true,

getPublicInstance(inst) {
Expand All @@ -237,6 +263,9 @@ var defaultTestOptions = {
createNodeMock: function() {
return null;
},
createComponentMock: function(component: {type: Function, props: any}) {
return component.type;
},
};

function toJSON(inst: Instance | TextInstance): ReactTestRendererNode {
Expand Down Expand Up @@ -277,6 +306,46 @@ function nodeAndSiblingsArray(nodeWithSibling: ?Fiber) {
return array;
}

function childrenToTree(node) {
if (!node) {
return null;
}
const children = nodeAndSiblingsArray(node);
if (children.length === 0) {
return null;
} else if (children.length === 1) {
return toTree(children[0]);
} else {
return flatten(children.map(toTree));
}
}

function flatten(arr) {
const result = [];
const stack = [{i: 0, array: arr}];
while (stack.length) {
let n = stack.pop();
while (n.i < n.array.length) {
const el = n.array[n.i];
n.i += 1;
if (Array.isArray(el)) {
stack.push(n);
stack.push({i: 0, array: el});
break;
}
result.push(el);
}
}
return result;
}

function publicType(node: Fiber) {
if (node._unmockedType !== null) {
return node._unmockedType;
}
return node.type;
}

function toTree(node: ?Fiber) {
if (node == null) {
return null;
Expand All @@ -287,26 +356,28 @@ function toTree(node: ?Fiber) {
case ClassComponent:
return {
nodeType: 'component',
type: node.type,
type: publicType(node),
props: {...node.memoizedProps},
instance: node.stateNode,
rendered: toTree(node.child),
rendered: childrenToTree(node.child),
};
case Fragment: // 10
return childrenToTree(node.child);
case FunctionalComponent: // 1
return {
nodeType: 'component',
type: node.type,
type: publicType(node),
props: {...node.memoizedProps},
instance: null,
rendered: toTree(node.child),
rendered: childrenToTree(node.child),
};
case HostComponent: // 5
return {
nodeType: 'host',
type: node.type,
props: {...node.memoizedProps},
instance: null, // TODO: use createNodeMock here somehow?
rendered: nodeAndSiblingsArray(node.child).map(toTree),
rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this faster? This is super inefficient atm. We have plenty of examples in the codebase of linked list tree traversal without the intermediate arrays.

But why are only host components flattened and not other types?

};
case HostText: // 6
return node.stateNode.text;
Expand All @@ -325,9 +396,14 @@ var ReactTestFiberRenderer = {
if (options && typeof options.createNodeMock === 'function') {
createNodeMock = options.createNodeMock;
}
var createComponentMock = defaultTestOptions.createComponentMock;
if (options && typeof options.createComponentMock === 'function') {
createComponentMock = options.createComponentMock;
}
var container = {
children: [],
createNodeMock,
createComponentMock,
tag: 'CONTAINER',
};
var root: ?FiberRoot = TestRenderer.createContainer(container);
Expand Down
Loading