Skip to content

Test renderer traversal #7516

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

Closed
wants to merge 1 commit into from
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
77 changes: 74 additions & 3 deletions src/renderers/testing/ReactTestMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var emptyObject = require('emptyObject');
var getHostComponentFromComposite = require('getHostComponentFromComposite');
var instantiateReactComponent = require('instantiateReactComponent');
var invariant = require('invariant');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var warning = require('warning');

/**
* Temporary (?) hack so that we can store all top-level pending updates on
Expand Down Expand Up @@ -77,17 +79,82 @@ function batchedMountComponentIntoNode(
return image;
}

var ReactTestInstance = function(component) {
var ReactTestInstance = function(isTopLevel, component) {
this._isTopLevel = isTopLevel;
this._component = component;
this._typeChanged = false;
};
ReactTestInstance.prototype._getInternalInstance = function() {
invariant(
!this._typeChanged,
'ReactTestRenderer: Can\'t inspect or traverse after changing component ' +
'type or key. Fix the earlier warning and try again.'
);
var component = this._component;
if (this._isTopLevel) {
component = component._renderedComponent;
}
invariant(
// _unmounted is present on test (host) components, not on composites
component && !component._unmounted && component._renderedComponent !== null,
'ReactTestRenderer: Can\'t inspect or traverse unmounted components.'
);
return component;
};
ReactTestInstance.prototype.isText = function() {
var el = this._getInternalInstance()._currentElement;
return typeof el === 'string' || typeof el === 'number';
};
ReactTestInstance.prototype.getInstance = function() {
return this._component._renderedComponent.getPublicInstance();
return this._getInternalInstance().getPublicInstance();
};
ReactTestInstance.prototype.getType = function() {
return this._getInternalInstance()._currentElement.type || null;
};
ReactTestInstance.prototype.getProps = function() {
return this._getInternalInstance()._currentElement.props || null;
};
ReactTestInstance.prototype.getChildren = function() {
var instance = this._getInternalInstance();
var el = instance._currentElement;
if (React.isValidElement(el)) {
var children;
if (typeof el.type === 'function') {
children = [instance._renderedComponent];
} else {
children = Object.keys(this._renderedChildren)
Copy link

Choose a reason for hiding this comment

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

Should this be instance._renderedChildren?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes.

.map((childKey) => this._renderedChildren[childKey]);
}
return children
.filter((child) =>
child._currentElement !== null && child._currentElement !== false
)
.map((child) => new ReactTestInstance(false, child));
} else if (typeof el === 'string' || typeof el === 'number') {
return [];
} else {
invariant(false, 'Unrecognized React node %s', el);
}
};
ReactTestInstance.prototype.update = function(nextElement) {
invariant(
this._isTopLevel,
'ReactTestRenderer: .update() can only be called at the top level.'
);
invariant(
this._component,
"ReactTestRenderer: .update() can't be called after unmount."
);
var prevElement = this._component._currentElement.props.child;
// TODO: Change to invariant in React 16
if (!shouldUpdateReactComponent(prevElement, nextElement)) {
warning(
false,
'ReactTestRenderer: Component type and key must be preserved when ' +
'updating. If necessary, call ReactTestRenderer.create again instead.'
);
this._typeChanged = true;
}
var nextWrappedElement = React.createElement(
TopLevelWrapper,
{ child: nextElement }
Expand All @@ -107,6 +174,10 @@ ReactTestInstance.prototype.update = function(nextElement) {
});
};
ReactTestInstance.prototype.unmount = function(nextElement) {
invariant(
this._isTopLevel,
'ReactTestRenderer: .unmount() can only be called at the top level.'
);
var component = this._component;
ReactUpdates.batchedUpdates(function() {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(true);
Expand Down Expand Up @@ -149,7 +220,7 @@ var ReactTestMount = {
batchedMountComponentIntoNode,
instance
);
return new ReactTestInstance(instance);
return new ReactTestInstance(true, instance);
},

};
Expand Down
16 changes: 13 additions & 3 deletions src/renderers/testing/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function getRenderedHostOrTextFromComponent(component) {
// =============================================================================

var ReactTestComponent = function(element) {
this._unmounted = false;
this._currentElement = element;
this._renderedChildren = null;
this._topLevelWrapper = null;
Expand Down Expand Up @@ -66,7 +67,10 @@ ReactTestComponent.prototype.getPublicInstance = function() {
// Maybe we'll revise later if someone has a good use case.
return null;
};
ReactTestComponent.prototype.unmountComponent = function() {};
ReactTestComponent.prototype.unmountComponent = function() {
this._unmounted = true;
this.unmountChildren(/* safely */ false);
};
ReactTestComponent.prototype.toJSON = function() {
var {children, ...props} = this._currentElement.props;
var childrenJSON = [];
Expand All @@ -93,27 +97,33 @@ Object.assign(ReactTestComponent.prototype, ReactMultiChild.Mixin);
// =============================================================================

var ReactTestTextComponent = function(element) {
this._unmounted = false;
this._currentElement = element;
};
ReactTestTextComponent.prototype.mountComponent = function() {};
ReactTestTextComponent.prototype.receiveComponent = function(nextElement) {
this._currentElement = nextElement;
};
ReactTestTextComponent.prototype.getHostNode = function() {};
ReactTestTextComponent.prototype.unmountComponent = function() {};
ReactTestTextComponent.prototype.unmountComponent = function() {
this._unmounted = true;
};
ReactTestTextComponent.prototype.toJSON = function() {
return this._currentElement;
};

// =============================================================================

var ReactTestEmptyComponent = function(element) {
this._unmounted = false;
this._currentElement = null;
};
ReactTestEmptyComponent.prototype.mountComponent = function() {};
ReactTestEmptyComponent.prototype.receiveComponent = function() {};
ReactTestEmptyComponent.prototype.getHostNode = function() {};
ReactTestEmptyComponent.prototype.unmountComponent = function() {};
ReactTestEmptyComponent.prototype.unmountComponent = function() {
this._unmounted = true;
};
ReactTestEmptyComponent.prototype.toJSON = function() {};

// =============================================================================
Expand Down
26 changes: 22 additions & 4 deletions src/renderers/testing/__tests__/ReactTestRenderer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ describe('ReactTestRenderer', function() {
});
});

it('updates types', function() {
it('updates types with a warning', function() {
spyOn(console, 'error');

var renderer = ReactTestRenderer.create(<div>mouse</div>);
expect(renderer.toJSON()).toEqual({
type: 'div',
Expand All @@ -125,6 +127,18 @@ describe('ReactTestRenderer', function() {
props: {},
children: ['mice'],
});

expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toEqual(
'Warning: ReactTestRenderer: Component type and key must be preserved ' +
'when updating. If necessary, call ReactTestRenderer.create again ' +
'instead.'
);

expect(() => renderer.getType()).toThrow(new Error(
'ReactTestRenderer: Can\'t inspect or traverse after changing ' +
'component type or key. Fix the earlier warning and try again.'
));
});

it('updates children', function() {
Expand Down Expand Up @@ -178,16 +192,20 @@ describe('ReactTestRenderer', function() {
}
}

var renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
renderer.update(<Log key="bar" name="Bar" />);
var renderer = ReactTestRenderer.create(
<div><Log key="foo" name="Foo" /></div>
);
renderer.update(<div><Log key="bar" name="Bar" /></div>);
renderer.unmount();

expect(log).toEqual([
'render Foo',
'mount Foo',
'unmount Foo',

'render Bar',
'unmount Foo',
'mount Bar',

'unmount Bar',
]);
});
Expand Down