diff --git a/.eslintignore b/.eslintignore
index 87837497a0..0ffbe9528c 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,5 @@
# We can probably lint these later but not important at this point
+src/renderers/art
src/shared/vendor
# But not in docs/_js/examples/*
docs/_js/*.js
diff --git a/package.json b/package.json
index 6397c7e19f..1b20001d2e 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"private": true,
"version": "16.0.0-alpha",
"devDependencies": {
+ "art": "^0.10.1",
"async": "^1.5.0",
"babel-cli": "^6.6.5",
"babel-core": "^6.0.0",
diff --git a/src/renderers/art/ReactART.js b/src/renderers/art/ReactART.js
new file mode 100644
index 0000000000..c1d61dbe37
--- /dev/null
+++ b/src/renderers/art/ReactART.js
@@ -0,0 +1,643 @@
+/**
+ * Copyright (c) 2013-present Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactARTFifteen
+ */
+
+'use strict';
+
+require('art/modes/current').setCurrent(
+ require('art/modes/fast-noSideEffects') // Flip this to DOM mode for debugging
+);
+
+const Transform = require('art/core/transform');
+const Mode = require('art/modes/current');
+
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const ReactInstanceMap = require('ReactInstanceMap');
+const ReactMultiChild = require('ReactMultiChild');
+const ReactUpdates = require('ReactUpdates');
+
+const emptyObject = require('emptyObject');
+const invariant = require('invariant');
+
+const assign = require('object-assign');
+const pooledTransform = new Transform();
+
+// Utilities
+
+function childrenAsString(children) {
+ if (!children) {
+ return '';
+ }
+ if (typeof children === 'string') {
+ return children;
+ }
+ if (children.length) {
+ return children.join('\n');
+ }
+ return '';
+}
+
+function createComponent(name) {
+ const ReactARTComponent = function(element) {
+ this.node = null;
+ this.subscriptions = null;
+ this.listeners = null;
+ this._mountImage = null;
+ this._renderedChildren = null;
+ this.construct(element);
+ };
+ ReactARTComponent.displayName = name;
+ for (let i = 1, l = arguments.length; i < l; i++) {
+ assign(ReactARTComponent.prototype, arguments[i]);
+ }
+
+ return ReactARTComponent;
+}
+
+/**
+ * Insert `node` into `parentNode` after `referenceNode`.
+ */
+function injectAfter(parentNode, referenceNode, node) {
+ let beforeNode;
+ if (node.parentNode === parentNode &&
+ node.previousSibling === referenceNode) {
+ return;
+ }
+ if (referenceNode == null) {
+ // node is supposed to be first.
+ beforeNode = parentNode.firstChild;
+ } else {
+ // node is supposed to be after referenceNode.
+ beforeNode = referenceNode.nextSibling;
+ }
+ if (beforeNode && beforeNode.previousSibling !== node) {
+ // Cases where `node === beforeNode` should get filtered out by earlier
+ // checks and the behavior isn't well-defined.
+ invariant(
+ node !== beforeNode,
+ 'ReactART: Can not insert node before itself'
+ );
+ node.injectBefore(beforeNode);
+ } else if (node.parentNode !== parentNode) {
+ node.inject(parentNode);
+ }
+}
+
+// ContainerMixin for components that can hold ART nodes
+
+const ContainerMixin = assign({}, ReactMultiChild.Mixin, {
+
+ /**
+ * Moves a child component to the supplied index.
+ *
+ * @param {ReactComponent} child Component to move.
+ * @param {number} toIndex Destination index of the element.
+ * @protected
+ */
+ moveChild: function(child, afterNode, toIndex, lastIndex) {
+ const childNode = child._mountImage;
+ injectAfter(this.node, afterNode, childNode);
+ },
+
+ /**
+ * Creates a child component.
+ *
+ * @param {ReactComponent} child Component to create.
+ * @param {object} childNode ART node to insert.
+ * @protected
+ */
+ createChild: function(child, afterNode, childNode) {
+ child._mountImage = childNode;
+ injectAfter(this.node, afterNode, childNode);
+ },
+
+ /**
+ * Removes a child component.
+ *
+ * @param {ReactComponent} child Child to remove.
+ * @protected
+ */
+ removeChild: function(child) {
+ child._mountImage.eject();
+ child._mountImage = null;
+ },
+
+ updateChildrenAtRoot: function(nextChildren, transaction) {
+ this.updateChildren(nextChildren, transaction, emptyObject);
+ },
+
+ mountAndInjectChildrenAtRoot: function(children, transaction) {
+ this.mountAndInjectChildren(children, transaction, emptyObject);
+ },
+
+ /**
+ * Override to bypass batch updating because it is not necessary.
+ *
+ * @param {?object} nextChildren.
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ * @override {ReactMultiChild.Mixin.updateChildren}
+ */
+ updateChildren: function(nextChildren, transaction, context) {
+ this._updateChildren(nextChildren, transaction, context);
+ },
+
+ // Shorthands
+
+ mountAndInjectChildren: function(children, transaction, context) {
+ const mountedImages = this.mountChildren(
+ children,
+ transaction,
+ context
+ );
+ // Each mount image corresponds to one of the flattened children
+ let i = 0;
+ for (let key in this._renderedChildren) {
+ if (this._renderedChildren.hasOwnProperty(key)) {
+ const child = this._renderedChildren[key];
+ child._mountImage = mountedImages[i];
+ mountedImages[i].inject(this.node);
+ i++;
+ }
+ }
+ }
+
+});
+
+// Surface is a React DOM Component, not an ART component. It serves as the
+// entry point into the ART reconciler.
+
+const Surface = React.createClass({
+
+ displayName: 'Surface',
+
+ mixins: [ContainerMixin],
+
+ componentDidMount: function() {
+ const domNode = ReactDOM.findDOMNode(this);
+
+ this.node = Mode.Surface(+this.props.width, +this.props.height, domNode);
+
+ const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
+ transaction.perform(
+ this.mountAndInjectChildren,
+ this,
+ this.props.children,
+ transaction,
+ ReactInstanceMap.get(this)._context
+ );
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+ },
+
+ componentDidUpdate: function(oldProps) {
+ const node = this.node;
+ if (this.props.width != oldProps.width ||
+ this.props.height != oldProps.height) {
+ node.resize(+this.props.width, +this.props.height);
+ }
+
+ const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
+ transaction.perform(
+ this.updateChildren,
+ this,
+ this.props.children,
+ transaction,
+ ReactInstanceMap.get(this)._context
+ );
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+
+ if (node.render) {
+ node.render();
+ }
+ },
+
+ componentWillUnmount: function() {
+ this.unmountChildren();
+ },
+
+ render: function() {
+ // This is going to be a placeholder because we don't know what it will
+ // actually resolve to because ART may render canvas, vml or svg tags here.
+ // We only allow a subset of properties since others might conflict with
+ // ART's properties.
+ const props = this.props;
+
+ // TODO: ART's Canvas Mode overrides surface title and cursor
+ const Tag = Mode.Surface.tagName;
+ return (
+
+ );
+ }
+
+});
+
+// Various nodes that can go into a surface
+
+const EventTypes = {
+ onMouseMove: 'mousemove',
+ onMouseOver: 'mouseover',
+ onMouseOut: 'mouseout',
+ onMouseUp: 'mouseup',
+ onMouseDown: 'mousedown',
+ onClick: 'click'
+};
+
+const NodeMixin = {
+
+ construct: function(element) {
+ this._currentElement = element;
+ },
+
+ getNativeNode: function() {
+ return this.node;
+ },
+
+ getPublicInstance: function() {
+ return this.node;
+ },
+
+ putEventListener: function(type, listener) {
+ const subscriptions = this.subscriptions || (this.subscriptions = {});
+ const listeners = this.listeners || (this.listeners = {});
+ listeners[type] = listener;
+ if (listener) {
+ if (!subscriptions[type]) {
+ subscriptions[type] = this.node.subscribe(type, listener, this);
+ }
+ } else {
+ if (subscriptions[type]) {
+ subscriptions[type]();
+ delete subscriptions[type];
+ }
+ }
+ },
+
+ handleEvent: function(event) {
+ const listener = this.listeners[event.type];
+ if (!listener) {
+ return;
+ }
+ if (typeof listener === 'function') {
+ listener.call(this, event);
+ } else if (listener.handleEvent) {
+ listener.handleEvent(event);
+ }
+ },
+
+ destroyEventListeners: function() {
+ const subscriptions = this.subscriptions;
+ if (subscriptions) {
+ for (let type in subscriptions) {
+ subscriptions[type]();
+ }
+ }
+ this.subscriptions = null;
+ this.listeners = null;
+ },
+
+ applyNodeProps: function(oldProps, props) {
+ const node = this.node;
+
+ const scaleX = props.scaleX != null ? props.scaleX :
+ props.scale != null ? props.scale : 1;
+ const scaleY = props.scaleY != null ? props.scaleY :
+ props.scale != null ? props.scale : 1;
+
+ pooledTransform
+ .transformTo(1, 0, 0, 1, 0, 0)
+ .move(props.x || 0, props.y || 0)
+ .rotate(props.rotation || 0, props.originX, props.originY)
+ .scale(scaleX, scaleY, props.originX, props.originY);
+
+ if (props.transform != null) {
+ pooledTransform.transform(props.transform);
+ }
+
+ if (node.xx !== pooledTransform.xx || node.yx !== pooledTransform.yx ||
+ node.xy !== pooledTransform.xy || node.yy !== pooledTransform.yy ||
+ node.x !== pooledTransform.x || node.y !== pooledTransform.y) {
+ node.transformTo(pooledTransform);
+ }
+
+ if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) {
+ node.indicate(props.cursor, props.title);
+ }
+
+ if (node.blend && props.opacity !== oldProps.opacity) {
+ node.blend(props.opacity == null ? 1 : props.opacity);
+ }
+
+ if (props.visible !== oldProps.visible) {
+ if (props.visible == null || props.visible) {
+ node.show();
+ } else {
+ node.hide();
+ }
+ }
+
+ for (let type in EventTypes) {
+ this.putEventListener(EventTypes[type], props[type]);
+ }
+ },
+
+ mountComponentIntoNode: function(rootID, container) {
+ throw new Error(
+ 'You cannot render an ART component standalone. ' +
+ 'You need to wrap it in a Surface.'
+ );
+ }
+
+};
+
+// Group
+
+const Group = createComponent('Group', NodeMixin, ContainerMixin, {
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ this.node = Mode.Group();
+ const props = this._currentElement.props;
+ this.applyGroupProps(emptyObject, props);
+ this.mountAndInjectChildren(props.children, transaction, context);
+ return this.node;
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+ this.applyGroupProps(oldProps, props);
+ this.updateChildren(props.children, transaction, context);
+ this._currentElement = nextComponent;
+ },
+
+ applyGroupProps: function(oldProps, props) {
+ this.node.width = props.width;
+ this.node.height = props.height;
+ this.applyNodeProps(oldProps, props);
+ },
+
+ unmountComponent: function() {
+ this.destroyEventListeners();
+ this.unmountChildren();
+ }
+
+});
+
+// ClippingRectangle
+const ClippingRectangle = createComponent(
+ 'ClippingRectangle', NodeMixin, ContainerMixin, {
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ this.node = Mode.ClippingRectangle();
+ const props = this._currentElement.props;
+ this.applyClippingProps(emptyObject, props);
+ this.mountAndInjectChildren(props.children, transaction, context);
+ return this.node;
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+ this.applyClippingProps(oldProps, props);
+ this.updateChildren(props.children, transaction, context);
+ this._currentElement = nextComponent;
+ },
+
+ applyClippingProps: function(oldProps, props) {
+ this.node.width = props.width;
+ this.node.height = props.height;
+ this.node.x = props.x;
+ this.node.y = props.y;
+ this.applyNodeProps(oldProps, props);
+ },
+
+ unmountComponent: function() {
+ this.destroyEventListeners();
+ this.unmountChildren();
+ }
+
+});
+
+
+// Renderables
+
+const RenderableMixin = assign({}, NodeMixin, {
+
+ applyRenderableProps: function(oldProps, props) {
+ if (oldProps.fill !== props.fill) {
+ if (props.fill && props.fill.applyFill) {
+ props.fill.applyFill(this.node);
+ } else {
+ this.node.fill(props.fill);
+ }
+ }
+ if (
+ oldProps.stroke !== props.stroke ||
+ oldProps.strokeWidth !== props.strokeWidth ||
+ oldProps.strokeCap !== props.strokeCap ||
+ oldProps.strokeJoin !== props.strokeJoin ||
+ // TODO: Consider a deep check of stokeDash.
+ // This may benefit the VML version in IE.
+ oldProps.strokeDash !== props.strokeDash
+ ) {
+ this.node.stroke(
+ props.stroke,
+ props.strokeWidth,
+ props.strokeCap,
+ props.strokeJoin,
+ props.strokeDash
+ );
+ }
+ this.applyNodeProps(oldProps, props);
+ },
+
+ unmountComponent: function() {
+ this.destroyEventListeners();
+ }
+
+});
+
+// Shape
+
+const Shape = createComponent('Shape', RenderableMixin, {
+
+ construct: function(element) {
+ this._currentElement = element;
+ this._oldDelta = null;
+ this._oldPath = null;
+ },
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ this.node = Mode.Shape();
+ const props = this._currentElement.props;
+ this.applyShapeProps(emptyObject, props);
+ return this.node;
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+ this.applyShapeProps(oldProps, props);
+ this._currentElement = nextComponent;
+ },
+
+ applyShapeProps: function(oldProps, props) {
+ const oldDelta = this._oldDelta;
+ const oldPath = this._oldPath;
+ const path = props.d || childrenAsString(props.children);
+
+ if (path.delta !== oldDelta ||
+ path !== oldPath ||
+ oldProps.width !== props.width ||
+ oldProps.height !== props.height) {
+
+ this.node.draw(
+ path,
+ props.width,
+ props.height
+ );
+
+ this._oldPath = path;
+ this._oldDelta = path.delta;
+ }
+
+ this.applyRenderableProps(oldProps, props);
+ }
+
+});
+
+// Text
+
+const Text = createComponent('Text', RenderableMixin, {
+
+ construct: function(element) {
+ this._currentElement = element;
+ this._oldString = null;
+ },
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ const props = this._currentElement.props;
+ const newString = childrenAsString(props.children);
+ this.node = Mode.Text(newString, props.font, props.alignment, props.path);
+ this._oldString = newString;
+ this.applyRenderableProps(emptyObject, props);
+ return this.node;
+ },
+
+ isSameFont: function(oldFont, newFont) {
+ if (oldFont === newFont) {
+ return true;
+ }
+ if (typeof newFont === 'string' || typeof oldFont === 'string') {
+ return false;
+ }
+ return (
+ newFont.fontSize === oldFont.fontSize &&
+ newFont.fontStyle === oldFont.fontStyle &&
+ newFont.fontVariant === oldFont.fontVariant &&
+ newFont.fontWeight === oldFont.fontWeight &&
+ newFont.fontFamily === oldFont.fontFamily
+ );
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+
+ const oldString = this._oldString;
+ const newString = childrenAsString(props.children);
+
+ if (oldString !== newString ||
+ !this.isSameFont(oldProps.font, props.font) ||
+ oldProps.alignment !== props.alignment ||
+ oldProps.path !== props.path) {
+ this.node.draw(
+ newString,
+ props.font,
+ props.alignment,
+ props.path
+ );
+ this._oldString = newString;
+ }
+
+ this.applyRenderableProps(oldProps, props);
+ this._currentElement = nextComponent;
+ }
+
+});
+
+// Declarative fill type objects - API design not finalized
+
+const slice = Array.prototype.slice;
+
+function LinearGradient(stops, x1, y1, x2, y2) {
+ this.args = slice.call(arguments);
+}
+
+LinearGradient.prototype.applyFill = function(node) {
+ node.fillLinear.apply(node, this.args);
+};
+
+function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
+ this.args = slice.call(arguments);
+}
+
+RadialGradient.prototype.applyFill = function(node) {
+ node.fillRadial.apply(node, this.args);
+};
+
+function Pattern(url, width, height, left, top) {
+ this.args = slice.call(arguments);
+}
+
+Pattern.prototype.applyFill = function(node) {
+ node.fillImage.apply(node, this.args);
+};
+
+module.exports = {
+ ClippingRectangle,
+ Group,
+ LinearGradient,
+ Path: Mode.Path,
+ Pattern,
+ RadialGradient,
+ Shape,
+ Surface,
+ Text,
+ Transform,
+};
diff --git a/src/renderers/art/__tests__/ReactART-test.js b/src/renderers/art/__tests__/ReactART-test.js
new file mode 100644
index 0000000000..37a06a4d82
--- /dev/null
+++ b/src/renderers/art/__tests__/ReactART-test.js
@@ -0,0 +1,289 @@
+/**
+ * Copyright (c) 2013-present Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+/*jslint evil: true */
+
+'use strict';
+
+jest
+ .unmock('ReactARTFifteen');
+
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var ReactTestUtils = require('ReactTestUtils');
+
+var Group;
+var Shape;
+var Surface;
+var TestComponent;
+
+var Missing = {};
+
+var ReactART = require('ReactARTFifteen');
+var ARTSVGMode = require('art/modes/svg');
+var ARTCurrentMode = require('art/modes/current');
+
+function testDOMNodeStructure(domNode, expectedStructure) {
+ expect(domNode).toBeDefined();
+ expect(domNode.nodeName).toBe(expectedStructure.nodeName);
+ for (var prop in expectedStructure) {
+ if (!expectedStructure.hasOwnProperty(prop)) continue;
+ if (prop != 'nodeName' && prop != 'children') {
+ if (expectedStructure[prop] === Missing) {
+ expect(domNode.hasAttribute(prop)).toBe(false);
+ } else {
+ expect(domNode.getAttribute(prop)).toBe(expectedStructure[prop]);
+ }
+ }
+ }
+ if (expectedStructure.children) {
+ expectedStructure.children.forEach(function(subTree, index) {
+ testDOMNodeStructure(domNode.childNodes[index], subTree);
+ });
+ }
+}
+
+describe('ReactART', function() {
+
+ beforeEach(function() {
+ ARTCurrentMode.setCurrent(ARTSVGMode);
+
+ Group = ReactART.Group;
+ Shape = ReactART.Shape;
+ Surface = ReactART.Surface;
+
+ TestComponent = React.createClass({
+ render: function() {
+
+ var a =
+ ;
+
+ var b =
+
+ M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666
+ h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994
+ h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z
+ ;
+
+ var c = ;
+
+ return (
+
+
+ {this.props.flipped ? [b, a, c] : [a, b, c]}
+
+
+ );
+ }
+ });
+ });
+
+ it('should have the correct lifecycle state', function() {
+ var instance = ;
+ instance = ReactTestUtils.renderIntoDocument(instance);
+ var group = instance.refs.group;
+ // Duck type test for an ART group
+ expect(typeof group.indicate).toBe('function');
+ });
+
+ it('should render a reasonable SVG structure in SVG mode', function() {
+ var instance = ;
+ instance = ReactTestUtils.renderIntoDocument(instance);
+
+ var expectedStructure = {
+ nodeName: 'svg',
+ width: '150',
+ height: '200',
+ children: [
+ { nodeName: 'defs' },
+ {
+ nodeName: 'g',
+ children: [
+ {
+ nodeName: 'defs',
+ children: [
+ { nodeName: 'linearGradient' }
+ ]
+ },
+ { nodeName: 'path' },
+ { nodeName: 'path' },
+ { nodeName: 'g' }
+ ]
+ }
+ ]
+ };
+
+ var realNode = ReactDOM.findDOMNode(instance);
+ testDOMNodeStructure(realNode, expectedStructure);
+ });
+
+ it('should be able to reorder components', function() {
+ var container = document.createElement('div');
+ var instance = ReactDOM.render(, container);
+
+ var expectedStructure = {
+ nodeName: 'svg',
+ children: [
+ { nodeName: 'defs' },
+ {
+ nodeName: 'g',
+ children: [
+ { nodeName: 'defs' },
+ { nodeName: 'path', opacity: '0.1' },
+ { nodeName: 'path', opacity: Missing },
+ { nodeName: 'g' }
+ ]
+ }
+ ]
+ };
+
+ var realNode = ReactDOM.findDOMNode(instance);
+ testDOMNodeStructure(realNode, expectedStructure);
+
+ ReactDOM.render(, container);
+
+ var expectedNewStructure = {
+ nodeName: 'svg',
+ children: [
+ { nodeName: 'defs' },
+ {
+ nodeName: 'g',
+ children: [
+ { nodeName: 'defs' },
+ { nodeName: 'path', opacity: Missing },
+ { nodeName: 'path', opacity: '0.1' },
+ { nodeName: 'g' }
+ ]
+ }
+ ]
+ };
+
+ testDOMNodeStructure(realNode, expectedNewStructure);
+ });
+
+ it('should be able to reorder many components', function() {
+ var container = document.createElement('div');
+
+ var Component = React.createClass({
+ render: function() {
+ var chars = this.props.chars.split('');
+ return (
+
+ {chars.map((text) => )}
+
+ );
+ },
+ });
+
+ // Mini multi-child stress test: lots of reorders, some adds, some removes.
+ var before = 'abcdefghijklmnopqrst';
+ var after = 'mxhpgwfralkeoivcstzy';
+
+ var instance = ReactDOM.render(, container);
+ var realNode = ReactDOM.findDOMNode(instance);
+ expect(realNode.textContent).toBe(before);
+
+ instance = ReactDOM.render(, container);
+ expect(realNode.textContent).toBe(after);
+
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it('renders composite with lifecycle inside group', function() {
+ var mounted = false;
+ var CustomShape = React.createClass({
+ render: function() {
+ return ;
+ },
+ componentDidMount: function() {
+ mounted = true;
+ }
+ });
+ ReactTestUtils.renderIntoDocument(
+
+
+
+
+
+ );
+ expect(mounted).toBe(true);
+ });
+
+ it('resolves refs before componentDidMount', function() {
+ var CustomShape = React.createClass({
+ render: function() {
+ return ;
+ }
+ });
+ var ref = null;
+ var Outer = React.createClass({
+ componentDidMount: function() {
+ ref = this.refs.test;
+ },
+ render: function() {
+ return (
+
+
+
+
+
+ );
+ }
+ });
+ ReactTestUtils.renderIntoDocument();
+ expect(ref.constructor).toBe(CustomShape);
+ });
+
+ it('resolves refs before componentDidUpdate', function() {
+ var CustomShape = React.createClass({
+ render: function() {
+ return ;
+ }
+ });
+ var ref = {};
+ var Outer = React.createClass({
+ componentDidMount: function() {
+ ref = this.refs.test;
+ },
+ componentDidUpdate: function() {
+ ref = this.refs.test;
+ },
+ render: function() {
+ return (
+
+
+ {this.props.mountCustomShape && }
+
+
+ );
+ }
+ });
+ var container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(ref).not.toBeDefined();
+ ReactDOM.render(, container);
+ expect(ref.constructor).toBe(CustomShape);
+ });
+
+});