diff --git a/.eslintrc b/.eslintrc
index a68e9195a..d639d726a 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -10,6 +10,7 @@
"new-cap": [2, { "capIsNewExceptions": ["AND"] }],
"react/jsx-pascal-case": [2, { "allowAllCaps": true }],
"react/no-find-dom-node": 1,
+ "import/first": 0,
"no-underscore-dangle": [2, {
"allowAfterThis": true,
"allow": [
diff --git a/.travis.yml b/.travis.yml
index 92f8f6d2e..46c458d5e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -41,6 +41,8 @@ matrix:
env: KARMA=true REACT=15.4
- node_js: "6"
env: KARMA=true REACT=15
+ - node_js: "6"
+ env: KARMA=true REACT=16
allow_failures:
- node_js: "6"
env: EXAMPLE=react-native
@@ -48,8 +50,13 @@ matrix:
env: EXAMPLE=mocha
- node_js: "6"
env: EXAMPLE=karma
+ - node_js: "6"
+ env: EXAMPLE=karma-webpack
+ - node_js: "6"
+ env: EXAMPLE=jest
env:
- REACT=0.13
- REACT=0.14
- REACT=15.4
- REACT=15
+ - REACT=16
diff --git a/docs/README.md b/docs/README.md
index 4fe21f837..88d1c780f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -130,4 +130,5 @@
* [Selectors](/docs/api/selector.md)
* [Change Log](/CHANGELOG.md)
* [Future](/docs/future.md)
+ * [Adapter & Compatibility Proposal](/docs/future/compatibility.md)
* [Contributing Guide](/CONTRIBUTING.md)
diff --git a/docs/future.md b/docs/future.md
index fc1c1b436..9ce5c318d 100644
--- a/docs/future.md
+++ b/docs/future.md
@@ -7,6 +7,11 @@ There are several things we'd like to address with Enzyme that often get asked.
of projects that we plan on addressing in the near future:
+### Enzyme Adapter & Compatibility
+
+[See the full proposal](future/compatibility.md)
+
+
#### Improved CSS Selector Support
Currently, "hierarchical" CSS selectors are not supported in Enzyme. That means that the only CSS
diff --git a/docs/future/compatibility.md b/docs/future/compatibility.md
new file mode 100644
index 000000000..a6788f7aa
--- /dev/null
+++ b/docs/future/compatibility.md
@@ -0,0 +1,184 @@
+# Enzyme Adapter & Compatibility Proposal
+
+
+## Motivation
+
+This proposal is attempting to address a handful of pain points that Enzyme has been
+subject to for quite a while. This proposal has resulted mostly [#715](https://github.com/airbnb/enzyme/issues/715),
+and a resulting discussion among core maintainers of this project.
+
+The desired results of this proposal are the following:
+
+1. Cleaner code, easier maintenance, less bug prone.
+
+By standardizing on a single tree specification, the implementation of Enzyme would no longer have
+to take into account the matrix of supported structures and nuanced differences between different
+versions of React, as well as to some extent the differences between `mount` and `shallow`.
+
+2. Additional libraries can provide compatible adapters
+
+React API-compatible libraries such as `preact` and `inferno` would be able to provide adapters to Enzyme
+for their corresponding libraries, and be able to take full advantage of Enzyme's APIs.
+
+3. Better user experience (ie, bundlers won't complain about missing deps)
+
+Enzyme has had a long-standing issue with static-analysis bundlers such as Webpack and Browserify because
+of our usage of internal React APIs. With this change, this would be minimized if not removed entirely,
+since these things can be localized into the adapter modules, and users will only install the ones they need.
+
+Additionally, we can even attempt to remove the use of internal react APIs by lobbying for react-maintained packages
+such as `react-test-renderer` to utilize the React Standard Tree (RST) format (details below).
+
+4. Standardization and interopability with other tools
+
+If we can agree on the tree format (specified below as "React Standard Tree"), other tools can start to use and
+understand this format as well. Standardization is a good thing, and could allow tools to be built that maybe
+don't even exist yet.
+
+
+## Proposal
+
+
+### React Standard Tree (RST)
+
+This proposal hinges on a standard tree specification. Keep in mind that this tree needs to account for more
+than what is currently satisfied by the output of something like `react-test-renderer`, which is currently
+only outputting the "host" nodes (ie, HTML elements). We need a tree format that allows for expressing a full
+react component tree, including composite components.
+
+```
+// Strings and Numbers are rendered as literals.
+type LiteralValue = string | number
+
+// A "node" in an RST is either a LiteralValue, or an RSTNode
+type Node = LiteralValue | RSTNode
+
+// if node.type
+type RenderedNode = RSTNode | [Node]
+
+type SourceLocation = {|
+ fileName: string
+ lineNumber: number
+|}
+
+type NodeType = 'class' | 'function' | 'host';
+
+// An RSTNode has this specific shape
+type RSTNode = {|
+ // Either a string or a function. A string is considered a "host" node, and
+ // a function would be a composite component. It would be the component constructor or
+ // an SFC in the case of a function.
+ type: string | function;
+
+ // This node's type
+ nodeType: NodeType;
+
+ // The props object passed to the node, which will include `children` in its raw form,
+ // exactly as it was passed to the component.
+ props: object;
+
+ // The backing instance to the node. Can be null in the case of "host" nodes and SFCs.
+ // Enzyme will expect instances to have the _public interface_ of a React Component, as would
+ // be expected in the corresponding React release returned by `getTargetVersion` of the
+ // renderer. Alternative React libraries can choose to provide an object here that implements
+ // the same interface, and Enzyme functionality that uses this will continue to work (An example
+ // of this would be the `setState()` prototype method).
+ instance: ComponentInstance?;
+
+ // For a given node, this corresponds roughly to the result of the `render` function with the
+ // provided props, but transformed into an RST. For "host" nodes, this will always be `null` or
+ // an Array. For "composite" nodes, this will always be `null` or an `RSTNode`.
+ rendered: RenderedNode?;
+
+ // an optional property with source information (useful in debug messages) that would be provided
+ // by this babel transform: https://babeljs.io/docs/plugins/transform-react-jsx-source/
+ __source?: SourceLocation;
+|}
+```
+
+### Enzyme Adapter Protocol
+
+**Definitions:**
+
+An `Element` is considered to be whatever data structure is returned by the JSX pragma being used. In the
+react case, this would be the data structure returned from `React.createElement`
+
+
+```
+type RendererOptions = {
+ // An optional predicate function that takes in an `Element` and returns
+ // whether or not the underlying Renderer should treat it as a "Host" node
+ // or not. This function should only be called with elements that are
+ // not required to be considered "host" nodes (ie, with a string `type`),
+ // so the default implementation of `isHost` is just a function that returns
+ // false.
+ ?isHost(Element): boolean;
+}
+
+type EnzymeAdapter = {
+ // This is a method that will return a semver version string for the _react_ version that
+ // it expects enzyme to target. This will allow enzyme to know what to expect in the `instance`
+ // that it finds on an RSTNode, as well as intelligently toggle behavior across react versions
+ // etc. For react adapters, this will likely just be `() => React.Version`, but for other
+ // adapters for libraries like inferno or preact, it will allow those libraries to specify
+ // a version of the API that they are committing to.
+ getTargetApiVersion(): string;
+
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ createRenderer(?options: RendererOptions): EnzymeRenderer;
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ nodeToElement(RSTNode): Element;
+}
+
+type EnzymeRenderer = {
+ // both initial render and updates for the renderer.
+ render(Element): void;
+
+ // retrieve a frozen-in-time copy of the RST.
+ getNode(): RSTNode?;
+}
+```
+
+
+### Using different adapters with Enzyme
+
+At the top level, Enzyme would expose a `configure` method, which would allow for an `adapter`
+option to be specified and globally configure Enzyme's adapter preference:
+
+```
+import Enzyme from 'enzyme';
+import ThirdPartyEnzymeAdapter from 'third-party-enzyme-adapter';
+
+Enzyme.configure({ adapter: ThirdPartyEnzymeAdapter });
+
+```
+
+Additionally, each wrapper Enzyme exposes will allow for an overriding `adapter` option that will use a
+given adapter for just that wrapper:
+
+```
+import { shallow } from 'enzyme';
+import ThirdPartyEnzymeAdapter from 'third-party-enzyme-adapter';
+
+shallow( , { adapter: ThirdPartyEnzymeAdapter });
+```
+
+Enzyme will build adapters for all major versions of React since React 0.13, though will deprecate
+adapters as usage of a particular major version fades.
+
+```
+import React13Adapter from 'enzyme-adapter-react-13';
+import React14Adapter from 'enzyme-adapter-react-14';
+import React15Adapter from 'enzyme-adapter-react-15';
+// ...
+```
+
+### Validation
+
+Enzyme will provide an `validate(node): Error?` method that will traverse down a provided `RSTNode` and
+return an `Error` if any deviations from the spec are encountered, and `null` otherwise. This will
+provide a way for implementors of the adapters to determine whether or not they are in compliance or not.
diff --git a/docs/future/migration.md b/docs/future/migration.md
new file mode 100644
index 000000000..b56592862
--- /dev/null
+++ b/docs/future/migration.md
@@ -0,0 +1,372 @@
+# Migration Guide for Enzyme v2.x to v3.x
+
+The change from Enzyme v2.x to v3.x is a more significant change than in previous major releases,
+due to the fact that the internal implementation has been almost completely rewritten.
+
+The goal of this rewrite was to address a lot of the major issues that have plagued Enzyme since
+its initial release. It was also to simultaneously remove a lot of the dependence that Enzyme has
+on react internals, and to make enzyme more "pluggable", paving the way for Enzyme to be used
+with "React-like" libraries such as Preact and Inferno.
+
+We have done our best to make Enzyme v3 as API compatible with v2.x as possible, however there are
+a hand full of breaking changes that we decided we needed to make, intentionally, in order to
+support this new architecture.
+
+Airbnb has one of the largest Enzyme test suites, coming in at around 30,000 enzyme unit tests.
+After upgrading Enzyme to v3.x in Airbnb's code base, 99.6% of these tests succeeded with no
+modifications at all. Most of the tests that broke we found to be easy to fix, and some we found to
+actually be depending on what could arguably be considered a bug in v2.x, and the breakage was
+desired.
+
+In this guide, we will go over a couple of the most common breakages that we ran into, and how to
+fix them. Hopefully this will make your upgrade path that much easier.
+
+
+## Configuring your Adapter
+
+Enzyme now has an "Adapter" system. This means that you now need to install Enzyme along with
+another module that provides the Adapter that tells Enzyme how to work with your version of React
+(or whatever other react-like library you are using).
+
+At the time of writing this, Enzyme publishes "officially supported" adapters for React 0.13.x,
+0.14.x, 15.x, and 16.x. These adapters are npm packages of the form `enzyme-adapter-react-{{version}}`.
+
+You will want to configure Enzyme with the adapter you'd like to use before using enzyme in your
+tests. The way to do this is whith `Enzyme.configure(...)`. For example, if your project depends
+on React 16, you would want to configure Enzyme this way:
+
+```js
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+Enzyme.configure({ adapter: new Adapter() });
+```
+
+The list of adapter npm packages for React semver ranges are as follows:
+
+- `enzyme-adapter-react-16` for `^16.0.0-0`
+- `enzyme-adapter-react-15` for `^15.5.0`
+- `enzyme-adapter-react-15.4` for `>= 15.0.0 && <15.5.0`
+- `enzyme-adapter-react-14` for `^0.14.x`
+- `enzyme-adapter-react-13` for `^0.13.x`
+
+
+## Element referential identity is no longer preserved
+
+Enzyme's new architecture means that the react "render tree" is transformed into an intermediate
+representation that is common across all react versions so that Enzyme can properly traverse it
+independent of React's internal representations. A side effect of this is that Enzyme no longer
+has access to the actual object references that were returned from `render` in your React
+components. This normally isn't much of a problem, but can manifest as a test failure in some
+cases.
+
+For example, consider the following example:
+
+
+```js
+import React from 'react';
+import Icon from './path/to/Icon';
+
+const ICONS = {
+ success: ,
+ failure: ,
+};
+
+const StatusLabel = ({ id, label }) =>
+//
+//
+
+// Expected tree output
+// tree = renderer.getNode();
+tree = {
+ type: 'div',
+ nodeType: 'host',
+ props: {},
+ rendered: [
+ {
+ type: Foo,
+ props: {},
+ nodeType: 'host',
+ rendered: null,
+ },
+ ],
+};
+
+renderer = Adapter.createRenderer({
+ // In this case, we treat `Bar` as a host node
+ // but `Qoo` is not, so gets rendered
+ isHost: el => [Bar].includes(el.type),
+});
+
+renderer.render(
);
+
+// Conceptual debug output:
+//
+//
+//
+// Hello World!
+//
+//
+//
+
+// Expected tree output
+// tree = renderer.getNode();
+tree = {
+ type: Bam,
+ nodeType: 'function',
+ props: {},
+ rendered: {
+ type: Bar,
+ nodeType: 'host',
+ props: { special: true },
+ rendered: [
+ {
+ type: Qoo,
+ nodeType: 'function',
+ props: {},
+ rendered: {
+ type: 'span',
+ nodeType: 'host',
+ props: { className: 'Qoo' },
+ rendered: ['Hello World!'],
+ },
+ },
+ ],
+ },
+};
diff --git a/install-relevant-react.sh b/install-relevant-react.sh
index 6303d0ffe..ee3332cc5 100644
--- a/install-relevant-react.sh
+++ b/install-relevant-react.sh
@@ -19,3 +19,7 @@ fi
if [ "$REACT" = "15" ]; then
npm run react:15
fi
+
+if [ "$REACT" = "16" ]; then
+ npm run react:16
+fi
diff --git a/karma.conf.js b/karma.conf.js
index c2387224b..02979e540 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -1,35 +1,42 @@
-/* eslint-disable no-var,prefer-arrow-callback,vars-on-top */
+/* eslint-disable no-var,prefer-arrow-callback,vars-on-top, import/no-extraneous-dependencies */
require('babel-register');
var IgnorePlugin = require('webpack').IgnorePlugin;
-var REACT013 = require('./src/version').REACT013;
-var REACT155 = require('./src/version').REACT155;
+var is = require('./test/_helpers/version').is;
function getPlugins() {
- var plugins = [];
-
- /*
- this list of conditional IgnorePlugins mirrors the conditional
- requires in src/react-compat.js and exists to avoid error
- output from the webpack compilation
- */
-
- if (!REACT013) {
- plugins.push(new IgnorePlugin(/react\/lib\/ExecutionEnvironment/));
- plugins.push(new IgnorePlugin(/react\/lib\/ReactContext/));
- plugins.push(new IgnorePlugin(/react\/addons/));
+ const adapter13 = new IgnorePlugin(/adapters\/ReactThirteenAdapter/);
+ const adapter14 = new IgnorePlugin(/adapters\/ReactFourteenAdapter/);
+ const adapter154 = new IgnorePlugin(/adapters\/ReactFifteenFourAdapter/);
+ const adapter15 = new IgnorePlugin(/adapters\/ReactFifteenFiveAdapter/);
+ const adapter16 = new IgnorePlugin(/adapters\/ReactSixteenAdapter/);
+
+ var plugins = [
+ adapter13,
+ adapter14,
+ adapter154,
+ adapter15,
+ adapter16,
+ ];
+
+ function not(x) {
+ return function notPredicate(y) {
+ return y !== x;
+ };
}
- if (REACT013) {
- plugins.push(new IgnorePlugin(/react-dom/));
- }
- if (REACT013 || REACT155) {
- plugins.push(new IgnorePlugin(/react-addons-test-utils/));
- }
- if (!REACT155) {
- plugins.push(new IgnorePlugin(/react-test-renderer/));
- plugins.push(new IgnorePlugin(/react-dom\/test-utils/));
- plugins.push(new IgnorePlugin(/create-react-class/));
+
+ // we want to ignore all of the adapters *except* the one we are currently using
+ if (is('0.13.x')) {
+ plugins = plugins.filter(not(adapter13));
+ } else if (is('0.14.x')) {
+ plugins = plugins.filter(not(adapter14));
+ } else if (is('^15.5.0')) {
+ plugins = plugins.filter(not(adapter15));
+ } else if (is('^15.0.0-0')) {
+ plugins = plugins.filter(not(adapter154));
+ } else if (is('^16.0.0-0')) {
+ plugins = plugins.filter(not(adapter16));
}
return plugins;
diff --git a/package.json b/package.json
index 9bdd9f0c4..4117b6509 100644
--- a/package.json
+++ b/package.json
@@ -21,13 +21,14 @@
"test:watch": "mocha --recursive --watch test",
"test:karma": "karma start",
"test:env": "sh ./example-test.sh",
- "test:all": "npm run react:13 && npm run test:only && npm run react:14 && npm run test:only && npm run react:15.4 && npm run test:only && npm run react:15 && npm run test:only",
+ "test:all": "npm run react:13 && npm run test:only && npm run react:14 && npm run test:only && npm run react:15.4 && npm run test:only && npm run react:15 && npm run test:only && npm run react:16 && npm run test:only",
"clean-local-npm": "rimraf node_modules/.bin/npm node_modules/.bin/npm.cmd",
- "react:clean": "npm run clean-local-npm && rimraf node_modules/react node_modules/react-dom node_modules/react-addons-test-utils node_modules/react-test-renderer && npm prune",
+ "react:clean": "npm run clean-local-npm && rimraf node_modules/react node_modules/react-dom node_modules/react-addons-test-utils node_modules/react-test-renderer node_modules/create-react-class && npm prune",
"react:13": "npm run react:clean && npm install && npm i --no-save react@0.13",
"react:14": "npm run react:clean && npm install && npm i --no-save react@0.14 react-dom@0.14 react-addons-test-utils@0.14",
"react:15.4": "npm run react:clean && npm install && npm i --no-save react@15.4 react-dom@15.4 react-addons-test-utils@15.4",
"react:15": "npm run react:clean && npm install && npm i --no-save react@15 react-dom@15 create-react-class@15 react-test-renderer@^15.5.4",
+ "react:16": "npm run react:clean && npm install && npm i --no-save react@^16.0.0-0 react-dom@^16.0.0-0 create-react-class@^15.6.0 react-test-renderer@^16.0.0-0",
"docs:clean": "rimraf _book",
"docs:lint": "eslint --ext md --config .eslintrc-markdown .",
"docs:prepare": "gitbook install",
@@ -66,6 +67,8 @@
"object.entries": "^1.0.4",
"object.values": "^1.0.4",
"prop-types": "^15.5.10",
+ "raf": "^3.3.2",
+ "semver": "^5.3.0",
"uuid": "^3.0.1"
},
"devDependencies": {
@@ -102,13 +105,13 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.8.1",
"mocha": "^3.5.0",
- "react": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x",
"rimraf": "^2.6.1",
"safe-publish-latest": "^1.1.1",
"sinon": "^2.4.1",
- "webpack": "^1.13.3"
+ "webpack": "^1.13.3",
+ "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0"
},
"peerDependencies": {
- "react": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x"
+ "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0"
}
}
diff --git a/src/Debug.js b/src/Debug.js
index 5cf02494f..3a707475b 100644
--- a/src/Debug.js
+++ b/src/Debug.js
@@ -1,25 +1,12 @@
import without from 'lodash/without';
import escape from 'lodash/escape';
import compact from 'lodash/compact';
-import objectValues from 'object.values';
import functionName from 'function.prototype.name';
import {
- childrenOfNode,
-} from './ShallowTraversal';
-import {
- renderedChildrenOfInst,
-} from './MountedTraversal';
-import {
- isDOMComponent,
- isCompositeComponent,
- isElement,
-} from './react-compat';
-import {
- internalInstance,
propsOfNode,
-} from './Utils';
-import { REACT013 } from './version';
+ childrenOfNode,
+} from './RSTTraversal';
export function typeName(node) {
return typeof node.type === 'function'
@@ -83,60 +70,3 @@ export function debugNode(node, indentLength = 2, options = {}) {
export function debugNodes(nodes, options = {}) {
return nodes.map(node => debugNode(node, undefined, options)).join('\n\n\n');
}
-
-export function debugInst(inst, indentLength = 2, options = {}) {
- if (typeof inst === 'string' || typeof inst === 'number') return escape(inst);
- if (!inst) return '';
-
- if (inst._stringText) {
- return inst._stringText;
- }
-
- if (!inst.getPublicInstance) {
- const internal = internalInstance(inst);
- return debugInst(internal, indentLength, options);
- }
- const publicInst = inst.getPublicInstance();
-
- if (typeof publicInst === 'string' || typeof publicInst === 'number') return escape(publicInst);
- if (!publicInst && !inst._renderedComponent) return '';
-
- // do stuff with publicInst
- const currentElement = inst._currentElement;
- const type = typeName(currentElement);
- const props = options.ignoreProps ? '' : propsString(currentElement);
- const children = [];
- if (isDOMComponent(publicInst)) {
- const renderedChildren = renderedChildrenOfInst(inst);
- if (!renderedChildren) {
- children.push(...childrenOfNode(currentElement));
- } else {
- children.push(...objectValues(renderedChildren));
- }
- } else if (
- !REACT013 &&
- isElement(currentElement) &&
- typeof currentElement.type === 'function'
- ) {
- children.push(inst._renderedComponent);
- } else if (
- REACT013 &&
- isCompositeComponent(publicInst)
- ) {
- children.push(inst._renderedComponent);
- }
-
- const childrenStrs = compact(children.map(n => debugInst(n, indentLength, options)));
-
- const beforeProps = props ? ' ' : '';
- const nodeClose = childrenStrs.length ? `${type}>` : '/>';
- const afterProps = childrenStrs.length
- ? '>'
- : ' ';
- const childrenIndented = indentChildren(childrenStrs, indentLength);
- return `<${type}${beforeProps}${props}${afterProps}${childrenIndented}${nodeClose}`;
-}
-
-export function debugInsts(insts, options = {}) {
- return insts.map(inst => debugInst(inst, undefined, options)).join('\n\n\n');
-}
diff --git a/src/MountedTraversal.js b/src/MountedTraversal.js
deleted file mode 100644
index 8c4638e7a..000000000
--- a/src/MountedTraversal.js
+++ /dev/null
@@ -1,307 +0,0 @@
-import isEmpty from 'lodash/isEmpty';
-import values from 'object.values';
-import isSubset from 'is-subset';
-import {
- internalInstance,
- nodeEqual,
- nodeMatches,
- propsOfNode,
- isFunctionalComponent,
- splitSelector,
- selectorType,
- isCompoundSelector,
- AND,
- SELECTOR,
- nodeHasType,
- nodeHasProperty,
-} from './Utils';
-import {
- isDOMComponent,
- isCompositeComponent,
- isCompositeComponentWithType,
- isElement,
- findDOMNode,
-} from './react-compat';
-import { REACT013 } from './version';
-
-export function getNode(inst) {
- if (!inst || inst._store || typeof inst === 'string') {
- return inst;
- }
- if (inst._currentElement) {
- return inst._currentElement;
- }
- if (internalInstance(inst)) {
- return internalInstance(inst)._currentElement;
- }
- if (inst._reactInternalInstance) {
- return inst._reactInternalInstance._currentElement;
- }
- if (inst._reactInternalComponent) {
- return inst._reactInternalComponent._currentElement;
- }
- return inst;
-}
-
-export function instEqual(a, b, lenComp) {
- return nodeEqual(getNode(a), getNode(b), lenComp);
-}
-
-export function instMatches(a, b, lenComp) {
- return nodeMatches(getNode(a), getNode(b), lenComp);
-}
-
-export function instHasClassName(inst, className) {
- const node = findDOMNode(inst);
- if (node === null) { // inst renders null
- return false;
- }
- if (node.classList) {
- return node.classList.contains(className);
- }
- let classes = node.className || '';
- if (typeof classes === 'object') {
- classes = classes.baseVal;
- }
- classes = classes.replace(/\s/g, ' ');
- return ` ${classes} `.indexOf(` ${className} `) > -1;
-}
-
-function hasClassName(inst, className) {
- if (!isDOMComponent(inst)) {
- return false;
- }
- return instHasClassName(inst, className);
-}
-
-export function instHasId(inst, id) {
- if (!isDOMComponent(inst)) return false;
- const instId = findDOMNode(inst).id || '';
- return instId === id;
-}
-
-function isFunctionalComponentWithType(inst, func) {
- return isFunctionalComponent(inst) && getNode(inst).type === func;
-}
-
-export function instHasType(inst, type) {
- switch (typeof type) {
- case 'string':
- return nodeHasType(getNode(inst), type);
- case 'function':
- return isCompositeComponentWithType(inst, type) ||
- isFunctionalComponentWithType(inst, type);
- default:
- return false;
- }
-}
-
-export function instHasProperty(inst, propKey, stringifiedPropValue) {
- if (!isDOMComponent(inst)) return false;
-
- const node = getNode(inst);
-
- return nodeHasProperty(node, propKey, stringifiedPropValue);
-}
-
-// called with private inst
-export function renderedChildrenOfInst(inst) {
- return REACT013
- ? inst._renderedComponent._renderedChildren
- : inst._renderedChildren;
-}
-
-// called with a private instance
-export function childrenOfInstInternal(inst) {
- if (!inst) {
- return [];
- }
- if (!inst.getPublicInstance) {
- const internal = internalInstance(inst);
- return childrenOfInstInternal(internal);
- }
-
- const publicInst = inst.getPublicInstance();
- const currentElement = inst._currentElement;
- if (isDOMComponent(publicInst)) {
- const renderedChildren = renderedChildrenOfInst(inst);
- return values(renderedChildren || {}).filter((node) => {
- if (REACT013 && !node.getPublicInstance) {
- return false;
- }
- if (typeof node._stringText !== 'undefined') {
- return false;
- }
- return true;
- }).map((node) => {
- if (!REACT013 && typeof node._currentElement.type === 'function') {
- return node._instance;
- }
- if (typeof node._stringText === 'string') {
- return node;
- }
- return node.getPublicInstance();
- });
- } else if (
- !REACT013 &&
- isElement(currentElement) &&
- typeof currentElement.type === 'function'
- ) {
- return childrenOfInstInternal(inst._renderedComponent);
- } else if (
- REACT013 &&
- isCompositeComponent(publicInst)
- ) {
- return childrenOfInstInternal(inst._renderedComponent);
- }
- return [];
-}
-
-export function internalInstanceOrComponent(node) {
- if (REACT013) {
- return node;
- } else if (node._reactInternalComponent) {
- return node._reactInternalComponent;
- } else if (node._reactInternalInstance) {
- return node._reactInternalInstance;
- }
- return node;
-}
-
-export function childrenOfInst(node) {
- return childrenOfInstInternal(internalInstanceOrComponent(node));
-}
-
-// This function should be called with an "internal instance". Nevertheless, if it is
-// called with a "public instance" instead, the function will call itself with the
-// internal instance and return the proper result.
-function findAllInRenderedTreeInternal(inst, test) {
- if (!inst) {
- return [];
- }
-
- if (!inst.getPublicInstance) {
- const internal = internalInstance(inst);
- return findAllInRenderedTreeInternal(internal, test);
- }
- const publicInst = inst.getPublicInstance() || inst._instance;
- let ret = test(publicInst) ? [publicInst] : [];
- const currentElement = inst._currentElement;
- if (isDOMComponent(publicInst)) {
- const renderedChildren = renderedChildrenOfInst(inst);
- values(renderedChildren || {}).filter((node) => {
- if (REACT013 && !node.getPublicInstance) {
- return false;
- }
- return true;
- }).forEach((node) => {
- ret = ret.concat(findAllInRenderedTreeInternal(node, test));
- });
- } else if (
- !REACT013 &&
- isElement(currentElement) &&
- typeof currentElement.type === 'function'
- ) {
- ret = ret.concat(
- findAllInRenderedTreeInternal(
- inst._renderedComponent,
- test,
- ),
- );
- } else if (
- REACT013 &&
- isCompositeComponent(publicInst)
- ) {
- ret = ret.concat(
- findAllInRenderedTreeInternal(
- inst._renderedComponent,
- test,
- ),
- );
- }
- return ret;
-}
-
-// This function could be called with a number of different things technically, so we need to
-// pass the *right* thing to our internal helper.
-export function treeFilter(node, test) {
- return findAllInRenderedTreeInternal(internalInstanceOrComponent(node), test);
-}
-
-function pathFilter(path, fn) {
- return path.filter(tree => treeFilter(tree, fn).length !== 0);
-}
-
-export function pathToNode(node, root) {
- const queue = [root];
- const path = [];
-
- const hasNode = testNode => node === testNode;
-
- while (queue.length) {
- const current = queue.pop();
- const children = childrenOfInst(current);
-
- if (current === node) return pathFilter(path, hasNode);
-
- path.push(current);
-
- if (children.length === 0) {
- // leaf node. if it isn't the node we are looking for, we pop.
- path.pop();
- }
- queue.push(...children);
- }
-
- return null;
-}
-
-export function parentsOfInst(inst, root) {
- return pathToNode(inst, root).reverse();
-}
-
-export function instMatchesObjectProps(inst, props) {
- if (!isDOMComponent(inst)) return false;
- const node = getNode(inst);
- return isSubset(propsOfNode(node), props);
-}
-
-export function buildInstPredicate(selector) {
- switch (typeof selector) {
- case 'function':
- // selector is a component constructor
- return inst => instHasType(inst, selector);
-
- case 'string':
- if (isCompoundSelector.test(selector)) {
- return AND(splitSelector(selector).map(buildInstPredicate));
- }
-
- switch (selectorType(selector)) {
- case SELECTOR.CLASS_TYPE:
- return inst => hasClassName(inst, selector.slice(1));
- case SELECTOR.ID_TYPE:
- return inst => instHasId(inst, selector.slice(1));
- case SELECTOR.PROP_TYPE: {
- const propKey = selector.split(/\[([a-zA-Z][a-zA-Z_\d\-:]*?)(=|])/)[1];
- const propValue = selector.split(/=(.*?)]/)[1];
-
- return node => instHasProperty(node, propKey, propValue);
- }
- default:
- // selector is a string. match to DOM tag or constructor displayName
- return inst => instHasType(inst, selector);
- }
-
- case 'object':
- if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) {
- return node => instMatchesObjectProps(node, selector);
- }
- throw new TypeError(
- 'Enzyme::Selector does not support an array, null, or empty object as a selector',
- );
-
- default:
- throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor');
- }
-}
diff --git a/src/ShallowTraversal.js b/src/RSTTraversal.js
similarity index 90%
rename from src/ShallowTraversal.js
rename to src/RSTTraversal.js
index 49f3c44d0..730be555c 100644
--- a/src/ShallowTraversal.js
+++ b/src/RSTTraversal.js
@@ -1,9 +1,8 @@
-import React from 'react';
import isEmpty from 'lodash/isEmpty';
+import flatten from 'lodash/flatten';
import isSubset from 'is-subset';
import functionName from 'function.prototype.name';
import {
- propsOfNode,
splitSelector,
isCompoundSelector,
selectorType,
@@ -13,17 +12,13 @@ import {
nodeHasProperty,
} from './Utils';
+export function propsOfNode(node) {
+ return (node && node.props) || {};
+}
export function childrenOfNode(node) {
if (!node) return [];
- const maybeArray = propsOfNode(node).children;
- const result = [];
- React.Children.forEach(maybeArray, (child) => {
- if (child !== null && child !== false && typeof child !== 'undefined') {
- result.push(child);
- }
- });
- return result;
+ return Array.isArray(node.rendered) ? flatten(node.rendered, true) : [node.rendered];
}
export function hasClassName(node, className) {
@@ -110,7 +105,7 @@ export function buildPredicate(selector) {
return node => nodeHasId(node, selector.slice(1));
case SELECTOR.PROP_TYPE: {
- const propKey = selector.split(/\[([a-zA-Z-]*?)(=|])/)[1];
+ const propKey = selector.split(/\[([a-zA-Z][a-zA-Z_\d\-:]*?)(=|])/)[1];
const propValue = selector.split(/=(.*?)]/)[1];
return node => nodeHasProperty(node, propKey, propValue);
diff --git a/src/ReactWrapper.jsx b/src/ReactWrapper.jsx
index 2ab47c56c..af3e22009 100644
--- a/src/ReactWrapper.jsx
+++ b/src/ReactWrapper.jsx
@@ -7,34 +7,27 @@ import compact from 'lodash/compact';
import ComplexSelector from './ComplexSelector';
import createWrapperComponent from './ReactWrapperComponent';
import {
- instHasClassName,
- childrenOfInst,
- parentsOfInst,
- buildInstPredicate,
- instEqual,
- instMatches,
- treeFilter,
- getNode,
- internalInstanceOrComponent,
-} from './MountedTraversal';
-import {
- renderWithOptions,
- Simulate,
- findDOMNode,
- unmountComponentAtNode,
-} from './react-compat';
-import {
- mapNativeEventNames,
containsChildrenSubArray,
- propsOfNode,
typeOfNode,
displayNameOfNode,
ITERATOR_SYMBOL,
+ nodeEqual,
+ nodeMatches,
+ getAdapter,
} from './Utils';
import {
- debugInsts,
+ debugNodes,
} from './Debug';
-import { REACT15 } from './version';
+import {
+ propsOfNode,
+ hasClassName,
+ childrenOfNode,
+ parentsOfNode,
+ treeFilter,
+ buildPredicate,
+} from './RSTTraversal';
+
+const noop = () => {};
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
@@ -46,7 +39,7 @@ import { REACT15 } from './version';
* @returns {ReactWrapper}
*/
function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
- return wrapper.flatMap(n => filter(n.getNode(), predicate));
+ return wrapper.flatMap(n => filter(n.getNodeInternal(), predicate));
}
/**
@@ -58,7 +51,15 @@ function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
* @returns {ReactWrapper}
*/
function filterWhereUnwrapped(wrapper, predicate) {
- return wrapper.wrap(compact(wrapper.getNodes().filter(predicate)));
+ return wrapper.wrap(compact(wrapper.getNodesInternal().filter(predicate)));
+}
+
+function getFromRenderer(renderer) {
+ const root = renderer.getNode();
+ return {
+ component: root.instance,
+ node: root.rendered,
+ };
}
/**
@@ -73,25 +74,29 @@ class ReactWrapper {
}
if (!root) {
+ this.renderer = getAdapter(options).createRenderer({ mode: 'mount', ...options });
const ReactWrapperComponent = createWrapperComponent(nodes, options);
- this.component = renderWithOptions(
- (
-
- ),
- options,
+ this.renderer.render(
+
,
);
this.root = this;
- this.node = this.component.getWrappedComponent();
- this.nodes = [this.node];
+ const {
+ component,
+ node,
+ } = getFromRenderer(this.renderer);
+ this.component = component;
+ this.node = node;
+ this.nodes = [node];
this.length = 1;
} else {
- this.component = null;
+ this.renderer = root.renderer;
this.root = root;
if (!nodes) {
+ this.node = null;
this.nodes = [];
} else if (!Array.isArray(nodes)) {
this.node = nodes;
@@ -101,12 +106,13 @@ class ReactWrapper {
this.nodes = nodes;
}
this.length = this.nodes.length;
+ this.component = null;
}
- this.options = options;
+ this.options = root ? root.options : options;
this.complexSelector = new ComplexSelector(
- buildInstPredicate,
+ buildPredicate,
findWhereUnwrapped,
- childrenOfInst,
+ childrenOfNode,
);
}
@@ -115,7 +121,7 @@ class ReactWrapper {
*
* @return {ReactComponent}
*/
- getNode() {
+ getNodeInternal() {
if (this.length !== 1) {
throw new Error(
'ReactWrapper::getNode() can only be called when wrapping one node',
@@ -129,10 +135,47 @@ class ReactWrapper {
*
* @return {Array
}
*/
- getNodes() {
+ getNodesInternal() {
return this.nodes;
}
+ /**
+ * Returns the wrapped ReactElement.
+ *
+ * @return {ReactElement}
+ */
+ getElement() {
+ if (this.length !== 1) {
+ throw new Error(
+ 'ReactWrapper::getElement() can only be called when wrapping one node',
+ );
+ }
+ return getAdapter(this.options).nodeToElement(this.node);
+ }
+
+ /**
+ * Returns the wrapped ReactElements.
+ *
+ * @return {Array}
+ */
+ getElements() {
+ return this.nodes.map(getAdapter(this.options).nodeToElement);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getNode() {
+ throw new Error(
+ 'ReactWrapper::getNode() is no longer supported. Use ReactWrapper::instance() instead',
+ );
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getNodes() {
+ throw new Error(
+ 'ReactWrapper::getNodes() is no longer supported.',
+ );
+ }
+
/**
* Returns the outer most DOMComponent of the current wrapper.
*
@@ -141,7 +184,8 @@ class ReactWrapper {
* @returns {DOMComponent}
*/
getDOMNode() {
- return this.single('getDOMNode', findDOMNode);
+ const adapter = getAdapter(this.options);
+ return this.single('getDOMNode', n => adapter.nodeToHostNode(n));
}
/**
@@ -157,7 +201,7 @@ class ReactWrapper {
if (this.root !== this) {
throw new Error('ReactWrapper::ref(refname) can only be called on the root');
}
- return this.wrap(this.instance().refs[refname]);
+ return this.instance().refs[refname];
}
/**
@@ -174,10 +218,10 @@ class ReactWrapper {
* @returns {ReactComponent}
*/
instance() {
- if (this.root !== this) {
- throw new Error('ReactWrapper::instance() can only be called on the root');
+ if (this.length !== 1) {
+ throw new Error('ReactWrapper::instance() can only be called on single nodes');
}
- return this.component.getInstance();
+ return this.node.instance;
}
/**
@@ -190,11 +234,16 @@ class ReactWrapper {
*/
update() {
if (this.root !== this) {
- // TODO(lmr): this requirement may not be necessary for the ReactWrapper
throw new Error('ReactWrapper::update() can only be called on the root');
}
this.single('update', () => {
- this.component.forceUpdate();
+ const {
+ component,
+ node,
+ } = getFromRenderer(this.renderer);
+ this.component = component;
+ this.node = node;
+ this.nodes = [node];
});
return this;
}
@@ -211,6 +260,7 @@ class ReactWrapper {
}
this.single('unmount', () => {
this.component.setState({ mount: false });
+ this.update();
});
return this;
}
@@ -227,6 +277,7 @@ class ReactWrapper {
}
this.single('mount', () => {
this.component.setState({ mount: true });
+ this.update();
});
return this;
}
@@ -245,11 +296,17 @@ class ReactWrapper {
* @param {Function} cb - callback function
* @returns {ReactWrapper}
*/
- setProps(props, callback = undefined) {
+ setProps(props, callback = noop) {
if (this.root !== this) {
throw new Error('ReactWrapper::setProps() can only be called on the root');
}
- this.component.setChildProps(props, callback);
+ if (typeof callback !== 'function') {
+ throw new TypeError('ReactWrapper::setProps() expects a function as its second argument');
+ }
+ this.component.setChildProps(props, () => {
+ this.update();
+ callback();
+ });
return this;
}
@@ -266,11 +323,17 @@ class ReactWrapper {
* @param {Function} cb - callback function
* @returns {ReactWrapper}
*/
- setState(state, callback = undefined) {
+ setState(state, callback = noop) {
if (this.root !== this) {
throw new Error('ReactWrapper::setState() can only be called on the root');
}
- this.instance().setState(state, callback);
+ if (typeof callback !== 'function') {
+ throw new TypeError('ReactWrapper::setState() expects a function as its second argument');
+ }
+ this.instance().setState(state, () => {
+ this.update();
+ callback();
+ });
return this;
}
@@ -294,6 +357,7 @@ class ReactWrapper {
);
}
this.component.setChildContext(context);
+ this.update();
return this;
}
@@ -314,7 +378,7 @@ class ReactWrapper {
* @returns {Boolean}
*/
matchesElement(node) {
- return this.single('matchesElement', () => instMatches(node, this.getNode(), (a, b) => a <= b));
+ return this.single('matchesElement', () => nodeMatches(node, this.getNodeInternal(), (a, b) => a <= b));
}
/**
@@ -331,8 +395,8 @@ class ReactWrapper {
*/
contains(nodeOrNodes) {
const predicate = Array.isArray(nodeOrNodes)
- ? other => containsChildrenSubArray(instEqual, other, nodeOrNodes)
- : other => instEqual(nodeOrNodes, other);
+ ? other => containsChildrenSubArray(nodeEqual, other, nodeOrNodes)
+ : other => nodeEqual(nodeOrNodes, other);
return findWhereUnwrapped(this, predicate).length > 0;
}
@@ -353,7 +417,7 @@ class ReactWrapper {
* @returns {Boolean}
*/
containsMatchingElement(node) {
- const predicate = other => instMatches(node, other, (a, b) => a <= b);
+ const predicate = other => nodeMatches(node, other, (a, b) => a <= b);
return findWhereUnwrapped(this, predicate).length > 0;
}
@@ -424,7 +488,7 @@ class ReactWrapper {
* @returns {boolean}
*/
is(selector) {
- const predicate = buildInstPredicate(selector);
+ const predicate = buildPredicate(selector);
return this.single('is', n => predicate(n));
}
@@ -434,16 +498,7 @@ class ReactWrapper {
* @returns {boolean}
*/
isEmptyRender() {
- return this.single('isEmptyRender', (n) => {
- // Stateful components and stateless function components have different internal structures,
- // so we need to find the correct internal instance, and validate the rendered node type
- // equals 2, which is the `ReactNodeTypes.EMPTY` value.
- if (REACT15) {
- return internalInstanceOrComponent(n)._renderedNodeType === 2;
- }
-
- return findDOMNode(n) === null;
- });
+ return this.single('isEmptyRender', n => n.rendered === null);
}
/**
@@ -465,7 +520,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
filter(selector) {
- const predicate = buildInstPredicate(selector);
+ const predicate = buildPredicate(selector);
return filterWhereUnwrapped(this, predicate);
}
@@ -477,7 +532,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
not(selector) {
- const predicate = buildInstPredicate(selector);
+ const predicate = buildPredicate(selector);
return filterWhereUnwrapped(this, n => !predicate(n));
}
@@ -491,7 +546,8 @@ class ReactWrapper {
* @returns {String}
*/
text() {
- return this.single('text', n => findDOMNode(n).textContent);
+ const adapter = getAdapter(this.options);
+ return this.single('text', n => adapter.nodeToHostNode(n).textContent);
}
/**
@@ -503,7 +559,9 @@ class ReactWrapper {
*/
html() {
return this.single('html', (n) => {
- const node = findDOMNode(n);
+ if (n === null) return null;
+ const adapter = getAdapter(this.options);
+ const node = adapter.nodeToHostNode(n);
return node === null ? null :
node.outerHTML.replace(/\sdata-(reactid|reactroot)+="([^"]*)+"/g, '');
});
@@ -531,13 +589,8 @@ class ReactWrapper {
*/
simulate(event, mock = {}) {
this.single('simulate', (n) => {
- const mappedEvent = mapNativeEventNames(event);
- const eventFn = Simulate[mappedEvent];
- if (!eventFn) {
- throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
- }
-
- eventFn(findDOMNode(n), mock);
+ this.renderer.simulateEvent(n, event, mock);
+ this.root.update();
});
return this;
}
@@ -586,7 +639,11 @@ class ReactWrapper {
if (this.root !== this) {
throw new Error('ReactWrapper::context() can only be called on the root');
}
- const _context = this.single('context', () => this.instance().context);
+ const instance = this.single('context', () => this.instance());
+ if (instance === null) {
+ throw new Error('ReactWrapper::context() can only be called on components with instances');
+ }
+ const _context = instance.context;
if (name !== undefined) {
return _context[name];
}
@@ -600,7 +657,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
children(selector) {
- const allChildren = this.flatMap(n => childrenOfInst(n.getNode()));
+ const allChildren = this.flatMap(n => childrenOfNode(n.getNodeInternal()).filter(x => typeof x === 'object'));
return selector ? allChildren.filter(selector) : allChildren;
}
@@ -625,7 +682,7 @@ class ReactWrapper {
*/
parents(selector) {
const allParents = this.wrap(
- this.single('parents', n => parentsOfInst(n, this.root.getNode())),
+ this.single('parents', n => parentsOfNode(n, this.root.getNodeInternal())),
);
return selector ? allParents.filter(selector) : allParents;
}
@@ -664,7 +721,7 @@ class ReactWrapper {
* @returns {String}
*/
key() {
- return this.single('key', n => getNode(n).key);
+ return this.single('key', n => n.key);
}
/**
@@ -674,7 +731,7 @@ class ReactWrapper {
* @returns {String|Function}
*/
type() {
- return this.single('type', n => typeOfNode(getNode(n)));
+ return this.single('type', n => typeOfNode(n));
}
/**
@@ -685,7 +742,7 @@ class ReactWrapper {
* @returns {String}
*/
name() {
- return this.single('name', n => displayNameOfNode(getNode(n)));
+ return this.single('name', n => displayNameOfNode(n));
}
/**
@@ -704,7 +761,7 @@ class ReactWrapper {
'hasClass() expects a class name, not a CSS selector.',
);
}
- return this.single('hasClass', n => instHasClassName(n, className));
+ return this.single('hasClass', n => hasClassName(n, className));
}
/**
@@ -715,7 +772,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
forEach(fn) {
- this.getNodes().forEach((n, i) => fn.call(this, this.wrap(n), i));
+ this.getNodesInternal().forEach((n, i) => fn.call(this, this.wrap(n), i));
return this;
}
@@ -727,7 +784,7 @@ class ReactWrapper {
* @returns {Array}
*/
map(fn) {
- return this.getNodes().map((n, i) => fn.call(this, this.wrap(n), i));
+ return this.getNodesInternal().map((n, i) => fn.call(this, this.wrap(n), i));
}
/**
@@ -739,7 +796,7 @@ class ReactWrapper {
* @returns {*}
*/
reduce(fn, initialValue) {
- return this.getNodes().reduce(
+ return this.getNodesInternal().reduce(
(accum, n, i) => fn.call(this, accum, this.wrap(n), i),
initialValue,
);
@@ -754,7 +811,7 @@ class ReactWrapper {
* @returns {*}
*/
reduceRight(fn, initialValue) {
- return this.getNodes().reduceRight(
+ return this.getNodesInternal().reduceRight(
(accum, n, i) => fn.call(this, accum, this.wrap(n), i),
initialValue,
);
@@ -769,7 +826,7 @@ class ReactWrapper {
* @returns {ShallowWrapper}
*/
slice(begin, end) {
- return this.wrap(this.getNodes().slice(begin, end));
+ return this.wrap(this.getNodesInternal().slice(begin, end));
}
/**
@@ -782,8 +839,8 @@ class ReactWrapper {
if (this.root === this) {
throw new Error('ReactWrapper::some() can not be called on the root');
}
- const predicate = buildInstPredicate(selector);
- return this.getNodes().some(predicate);
+ const predicate = buildPredicate(selector);
+ return this.getNodesInternal().some(predicate);
}
/**
@@ -793,7 +850,7 @@ class ReactWrapper {
* @returns {Boolean}
*/
someWhere(predicate) {
- return this.getNodes().some((n, i) => predicate.call(this, this.wrap(n), i));
+ return this.getNodesInternal().some((n, i) => predicate.call(this, this.wrap(n), i));
}
/**
@@ -803,8 +860,8 @@ class ReactWrapper {
* @returns {Boolean}
*/
every(selector) {
- const predicate = buildInstPredicate(selector);
- return this.getNodes().every(predicate);
+ const predicate = buildPredicate(selector);
+ return this.getNodesInternal().every(predicate);
}
/**
@@ -814,7 +871,7 @@ class ReactWrapper {
* @returns {Boolean}
*/
everyWhere(predicate) {
- return this.getNodes().every((n, i) => predicate.call(this, this.wrap(n), i));
+ return this.getNodesInternal().every((n, i) => predicate.call(this, this.wrap(n), i));
}
/**
@@ -826,7 +883,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
flatMap(fn) {
- const nodes = this.getNodes().map((n, i) => fn.call(this, this.wrap(n), i));
+ const nodes = this.getNodesInternal().map((n, i) => fn.call(this, this.wrap(n), i));
const flattened = flatten(nodes, true);
const uniques = unique(flattened);
const compacted = compact(uniques);
@@ -851,7 +908,7 @@ class ReactWrapper {
* @returns {ReactElement}
*/
get(index) {
- return this.getNodes()[index];
+ return this.getElements()[index];
}
/**
@@ -861,7 +918,7 @@ class ReactWrapper {
* @returns {ReactWrapper}
*/
at(index) {
- return this.wrap(this.getNodes()[index]);
+ return this.wrap(this.getNodesInternal()[index]);
}
/**
@@ -918,7 +975,7 @@ class ReactWrapper {
`Method “${fnName}” is only meant to be run on a single node. ${this.length} found instead.`,
);
}
- return callback.call(this, this.getNode());
+ return callback.call(this, this.getNodeInternal());
}
/**
@@ -943,7 +1000,7 @@ class ReactWrapper {
* @returns {String}
*/
debug(options = {}) {
- return debugInsts(this.getNodes(), options);
+ return debugNodes(this.getNodesInternal(), options);
}
/**
@@ -976,7 +1033,7 @@ class ReactWrapper {
'`mount()`.',
);
}
- unmountComponentAtNode(this.options.attachTo);
+ this.renderer.unmount();
}
}
@@ -985,7 +1042,20 @@ if (ITERATOR_SYMBOL) {
Object.defineProperty(ReactWrapper.prototype, ITERATOR_SYMBOL, {
configurable: true,
value: function iterator() {
- return this.nodes[ITERATOR_SYMBOL]();
+ const iter = this.nodes[ITERATOR_SYMBOL]();
+ const adapter = getAdapter(this.options);
+ return {
+ next() {
+ const next = iter.next();
+ if (next.done) {
+ return { done: true };
+ }
+ return {
+ done: false,
+ value: adapter.nodeToElement(next.value),
+ };
+ },
+ };
},
});
}
diff --git a/src/ShallowWrapper.js b/src/ShallowWrapper.js
index 1e081a13b..586a03781 100644
--- a/src/ShallowWrapper.js
+++ b/src/ShallowWrapper.js
@@ -9,34 +9,27 @@ import {
nodeEqual,
nodeMatches,
containsChildrenSubArray,
- propFromEvent,
withSetStateAllowed,
- propsOfNode,
typeOfNode,
isReactElementAlike,
displayNameOfNode,
isFunctionalComponent,
isCustomComponentElement,
ITERATOR_SYMBOL,
+ getAdapter,
} from './Utils';
import {
debugNodes,
} from './Debug';
import {
+ propsOfNode,
getTextFromNode,
hasClassName,
childrenOfNode,
parentsOfNode,
treeFilter,
buildPredicate,
-} from './ShallowTraversal';
-import {
- createShallowRenderer,
- renderToStaticMarkup,
- batchedUpdates,
- isDOMComponentElement,
-} from './react-compat';
-import { REACT155 } from './version';
+} from './RSTTraversal';
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
@@ -48,7 +41,7 @@ import { REACT155 } from './version';
* @returns {ShallowWrapper}
*/
function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
- return wrapper.flatMap(n => filter(n.getNode(), predicate));
+ return wrapper.flatMap(n => filter(n.getNodeInternal(), predicate));
}
/**
@@ -60,7 +53,7 @@ function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
* @returns {ShallowWrapper}
*/
function filterWhereUnwrapped(wrapper, predicate) {
- return wrapper.wrap(compact(wrapper.getNodes().filter(predicate)));
+ return wrapper.wrap(compact(wrapper.getNodesInternal().filter(predicate)));
}
/**
@@ -98,15 +91,11 @@ function validateOptions(options) {
}
}
-
-function performBatchedUpdates(wrapper, fn) {
- const renderer = wrapper.root.renderer;
- if (REACT155 && renderer.unstable_batchedUpdates) {
- // React 15.5+ exposes batching on shallow renderer itself
- return renderer.unstable_batchedUpdates(fn);
+function getRootNode(node) {
+ if (node.nodeType === 'host') {
+ return node;
}
- // React <15.5: Fallback to ReactDOM
- return batchedUpdates(fn);
+ return node.rendered;
}
/**
@@ -118,27 +107,25 @@ class ShallowWrapper {
if (!root) {
this.root = this;
this.unrendered = nodes;
- this.renderer = createShallowRenderer();
- withSetStateAllowed(() => {
- performBatchedUpdates(this, () => {
- this.renderer.render(nodes, options.context);
- const instance = this.instance();
- if (
- options.lifecycleExperimental &&
- instance &&
- typeof instance.componentDidMount === 'function'
- ) {
- instance.componentDidMount();
- }
+ this.renderer = getAdapter(options).createRenderer({ mode: 'shallow', ...options });
+ this.renderer.render(nodes, options.context);
+ const instance = this.renderer.getNode().instance;
+ if (
+ options.lifecycleExperimental &&
+ instance &&
+ typeof instance.componentDidMount === 'function'
+ ) {
+ this.renderer.batchedUpdates(() => {
+ instance.componentDidMount();
});
- });
- this.node = this.renderer.getRenderOutput();
+ }
+ this.node = getRootNode(this.renderer.getNode());
this.nodes = [this.node];
this.length = 1;
} else {
this.root = root;
this.unrendered = null;
- this.renderer = null;
+ this.renderer = root.renderer;
if (!Array.isArray(nodes)) {
this.node = nodes;
this.nodes = [nodes];
@@ -148,22 +135,31 @@ class ShallowWrapper {
}
this.length = this.nodes.length;
}
- this.options = options;
+ this.options = root ? root.options : options;
this.complexSelector = new ComplexSelector(buildPredicate, findWhereUnwrapped, childrenOfNode);
}
+ getNodeInternal() {
+ if (this.length !== 1) {
+ throw new Error(
+ 'ShallowWrapper::getNode() can only be called when wrapping one node',
+ );
+ }
+ return this.node;
+ }
+
/**
* Returns the wrapped ReactElement.
*
* @return {ReactElement}
*/
- getNode() {
+ getElement() {
if (this.length !== 1) {
throw new Error(
- 'ShallowWrapper::getNode() can only be called when wrapping one node',
+ 'ShallowWrapper::getElement() can only be called when wrapping one node',
);
}
- return this.root === this ? this.renderer.getRenderOutput() : this.node;
+ return getAdapter(this.options).nodeToElement(this.node);
}
/**
@@ -171,8 +167,26 @@ class ShallowWrapper {
*
* @return {Array}
*/
+ getElements() {
+ return this.nodes.map(getAdapter(this.options).nodeToElement);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getNode() {
+ throw new Error(
+ 'ShallowWrapper::getNode() is no longer supported. Use ShallowWrapper::getElement() instead',
+ );
+ }
+
+ getNodesInternal() {
+ return this.nodes;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
getNodes() {
- return this.root === this ? [this.renderer.getRenderOutput()] : this.nodes;
+ throw new Error(
+ 'ShallowWrapper::getNodes() is no longer supported. Use ShallowWrapper::getElements() instead',
+ );
}
/**
@@ -192,7 +206,7 @@ class ShallowWrapper {
if (this.root !== this) {
throw new Error('ShallowWrapper::instance() can only be called on the root');
}
- return this.renderer._instance ? this.renderer._instance._instance : null;
+ return this.renderer.getNode().instance;
}
/**
@@ -208,7 +222,7 @@ class ShallowWrapper {
throw new Error('ShallowWrapper::update() can only be called on the root');
}
this.single('update', () => {
- this.node = this.renderer.getRenderOutput();
+ this.node = getRootNode(this.renderer.getNode());
this.nodes = [this.node];
});
return this;
@@ -227,13 +241,19 @@ class ShallowWrapper {
rerender(props, context) {
this.single('rerender', () => {
withSetStateAllowed(() => {
- const instance = this.instance();
+ // NOTE(lmr): In react 16, instances will be null for SFCs, but
+ // rerendering with props/context is still a valid thing to do. In
+ // this case, state will be undefined, but props/context will exist.
+ const instance = this.instance() || {};
const state = instance.state;
- const prevProps = instance.props;
- const prevContext = instance.context;
+ const prevProps = instance.props || this.unrendered.props;
+ const prevContext = instance.context || this.options.context;
const nextProps = props || prevProps;
const nextContext = context || prevContext;
- performBatchedUpdates(this, () => {
+ if (context) {
+ this.options = { ...this.options, context: nextContext };
+ }
+ this.renderer.batchedUpdates(() => {
let shouldRender = true;
// dirty hack:
// make sure that componentWillReceiveProps is called before shouldComponentUpdate
@@ -326,7 +346,7 @@ class ShallowWrapper {
if (this.root !== this) {
throw new Error('ShallowWrapper::setState() can only be called on the root');
}
- if (isFunctionalComponent(this.instance())) {
+ if (this.instance() === null || isFunctionalComponent(this.instance())) {
throw new Error('ShallowWrapper::setState() can only be called on class components');
}
this.single('setState', () => {
@@ -373,16 +393,20 @@ class ShallowWrapper {
* @returns {Boolean}
*/
contains(nodeOrNodes) {
- if (!isReactElementAlike(nodeOrNodes)) {
+ const adapter = getAdapter(this.options);
+ if (!isReactElementAlike(nodeOrNodes, adapter)) {
throw new Error(
'ShallowWrapper::contains() can only be called with ReactElement (or array of them), ' +
'string or number as argument.',
);
}
-
const predicate = Array.isArray(nodeOrNodes)
- ? other => containsChildrenSubArray(nodeEqual, other, nodeOrNodes)
- : other => nodeEqual(nodeOrNodes, other);
+ ? other => containsChildrenSubArray(
+ nodeEqual,
+ other,
+ nodeOrNodes.map(adapter.elementToNode),
+ )
+ : other => nodeEqual(adapter.elementToNode(nodeOrNodes), other);
return findWhereUnwrapped(this, predicate).length > 0;
}
@@ -472,7 +496,7 @@ class ShallowWrapper {
* @returns {Boolean}
*/
equals(node) {
- return this.single('equals', () => nodeEqual(this.getNode(), node));
+ return this.single('equals', () => nodeEqual(this.getNodeInternal(), node));
}
/**
@@ -493,7 +517,7 @@ class ShallowWrapper {
* @returns {Boolean}
*/
matchesElement(node) {
- return this.single('matchesElement', () => nodeMatches(node, this.getNode(), (a, b) => a <= b));
+ return this.single('matchesElement', () => nodeMatches(node, this.getNodeInternal(), (a, b) => a <= b));
}
/**
@@ -585,7 +609,12 @@ class ShallowWrapper {
* @returns {String}
*/
html() {
- return this.single('html', n => (this.type() === null ? null : renderToStaticMarkup(n)));
+ return this.single('html', (n) => {
+ if (this.type() === null) return null;
+ const adapter = getAdapter(this.options);
+ const renderer = adapter.createRenderer({ ...this.options, mode: 'string' });
+ return renderer.render(adapter.nodeToElement(n));
+ });
}
/**
@@ -618,18 +647,10 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
simulate(event, ...args) {
- const handler = this.prop(propFromEvent(event));
- if (handler) {
- withSetStateAllowed(() => {
- // TODO(lmr): create/use synthetic events
- // TODO(lmr): emulate React's event propagation
- performBatchedUpdates(this, () => {
- handler(...args);
- });
- this.root.update();
- });
- }
- return this;
+ return this.single('simulate', (n) => {
+ this.renderer.simulateEvent(n, event, ...args);
+ this.root.update();
+ });
}
/**
@@ -656,7 +677,7 @@ class ShallowWrapper {
if (this.root !== this) {
throw new Error('ShallowWrapper::state() can only be called on the root');
}
- if (isFunctionalComponent(this.instance())) {
+ if (this.instance() === null || isFunctionalComponent(this.instance())) {
throw new Error('ShallowWrapper::state() can only be called on class components');
}
const _state = this.single('state', () => this.instance().state);
@@ -685,6 +706,12 @@ class ShallowWrapper {
'a context option',
);
}
+ if (this.instance() === null) {
+ throw new Error(
+ 'ShallowWrapper::context() can only be called on wrapped nodes that have a non-null ' +
+ 'instance',
+ );
+ }
const _context = this.single('context', () => this.instance().context);
if (name) {
return _context[name];
@@ -699,7 +726,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
children(selector) {
- const allChildren = this.flatMap(n => childrenOfNode(n.getNode()));
+ const allChildren = this.flatMap(n => childrenOfNode(n.getNodeInternal()));
return selector ? allChildren.filter(selector) : allChildren;
}
@@ -724,7 +751,7 @@ class ShallowWrapper {
*/
parents(selector) {
const allParents = this.wrap(
- this.single('parents', n => parentsOfNode(n, this.root.getNode())),
+ this.single('parents', n => parentsOfNode(n, this.root.node)),
);
return selector ? allParents.filter(selector) : allParents;
}
@@ -756,7 +783,9 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
shallow(options) {
- return this.single('shallow', n => this.wrap(n, null, options));
+ return this.single('shallow', n => this.wrap(
+ getAdapter(this.options).nodeToElement(n), null, options),
+ );
}
/**
@@ -826,7 +855,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
forEach(fn) {
- this.getNodes().forEach((n, i) => fn.call(this, this.wrap(n), i));
+ this.getNodesInternal().forEach((n, i) => fn.call(this, this.wrap(n), i));
return this;
}
@@ -838,7 +867,7 @@ class ShallowWrapper {
* @returns {Array}
*/
map(fn) {
- return this.getNodes().map((n, i) => fn.call(this, this.wrap(n), i));
+ return this.getNodesInternal().map((n, i) => fn.call(this, this.wrap(n), i));
}
/**
@@ -850,7 +879,7 @@ class ShallowWrapper {
* @returns {*}
*/
reduce(fn, initialValue) {
- return this.getNodes().reduce(
+ return this.getNodesInternal().reduce(
(accum, n, i) => fn.call(this, accum, this.wrap(n), i),
initialValue,
);
@@ -865,7 +894,7 @@ class ShallowWrapper {
* @returns {*}
*/
reduceRight(fn, initialValue) {
- return this.getNodes().reduceRight(
+ return this.getNodesInternal().reduceRight(
(accum, n, i) => fn.call(this, accum, this.wrap(n), i),
initialValue,
);
@@ -880,7 +909,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
slice(begin, end) {
- return this.wrap(this.getNodes().slice(begin, end));
+ return this.wrap(this.getNodesInternal().slice(begin, end));
}
/**
@@ -894,7 +923,7 @@ class ShallowWrapper {
throw new Error('ShallowWrapper::some() can not be called on the root');
}
const predicate = buildPredicate(selector);
- return this.getNodes().some(predicate);
+ return this.getNodesInternal().some(predicate);
}
/**
@@ -904,7 +933,7 @@ class ShallowWrapper {
* @returns {Boolean}
*/
someWhere(predicate) {
- return this.getNodes().some((n, i) => predicate.call(this, this.wrap(n), i));
+ return this.getNodesInternal().some((n, i) => predicate.call(this, this.wrap(n), i));
}
/**
@@ -915,7 +944,7 @@ class ShallowWrapper {
*/
every(selector) {
const predicate = buildPredicate(selector);
- return this.getNodes().every(predicate);
+ return this.getNodesInternal().every(predicate);
}
/**
@@ -925,7 +954,7 @@ class ShallowWrapper {
* @returns {Boolean}
*/
everyWhere(predicate) {
- return this.getNodes().every((n, i) => predicate.call(this, this.wrap(n), i));
+ return this.getNodesInternal().every((n, i) => predicate.call(this, this.wrap(n), i));
}
/**
@@ -937,7 +966,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
flatMap(fn) {
- const nodes = this.getNodes().map((n, i) => fn.call(this, this.wrap(n), i));
+ const nodes = this.getNodesInternal().map((n, i) => fn.call(this, this.wrap(n), i));
const flattened = flatten(nodes, true);
const uniques = unique(flattened);
const compacted = compact(uniques);
@@ -963,7 +992,7 @@ class ShallowWrapper {
* @returns {ReactElement}
*/
get(index) {
- return this.getNodes()[index];
+ return getAdapter(this.options).nodeToElement(this.getNodesInternal()[index]);
}
/**
@@ -973,7 +1002,7 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
at(index) {
- return this.wrap(this.getNodes()[index]);
+ return this.wrap(this.getNodesInternal()[index]);
}
/**
@@ -1030,7 +1059,7 @@ class ShallowWrapper {
`Method “${fnName}” is only meant to be run on a single node. ${this.length} found instead.`,
);
}
- return callback.call(this, this.getNode());
+ return callback.call(this, this.getNodeInternal());
}
/**
@@ -1055,7 +1084,7 @@ class ShallowWrapper {
* @returns {String}
*/
debug(options = {}) {
- return debugNodes(this.getNodes(), options);
+ return debugNodes(this.getNodesInternal(), options);
}
/**
@@ -1077,15 +1106,17 @@ class ShallowWrapper {
* @returns {ShallowWrapper}
*/
dive(options = {}) {
+ const adapter = getAdapter(this.options);
const name = 'dive';
return this.single(name, (n) => {
- if (isDOMComponentElement(n)) {
- throw new TypeError(`ShallowWrapper::${name}() can not be called on DOM components`);
+ if (n && n.nodeType === 'host') {
+ throw new TypeError(`ShallowWrapper::${name}() can not be called on Host Components`);
}
- if (!isCustomComponentElement(n)) {
+ const el = getAdapter(this.options).nodeToElement(n);
+ if (!isCustomComponentElement(el, adapter)) {
throw new TypeError(`ShallowWrapper::${name}() can only be called on components`);
}
- return this.wrap(n, null, { ...this.options, ...options });
+ return this.wrap(el, null, { ...this.options, ...options });
});
}
}
@@ -1094,7 +1125,20 @@ if (ITERATOR_SYMBOL) {
Object.defineProperty(ShallowWrapper.prototype, ITERATOR_SYMBOL, {
configurable: true,
value: function iterator() {
- return this.nodes[ITERATOR_SYMBOL]();
+ const iter = this.nodes[ITERATOR_SYMBOL]();
+ const adapter = getAdapter(this.options);
+ return {
+ next() {
+ const next = iter.next();
+ if (next.done) {
+ return { done: true };
+ }
+ return {
+ done: false,
+ value: adapter.nodeToElement(next.value),
+ };
+ },
+ };
},
});
}
diff --git a/src/Utils.js b/src/Utils.js
index 8dd62bfeb..63079867b 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1,56 +1,35 @@
/* eslint no-use-before-define:0 */
import isEqual from 'lodash/isEqual';
-import React from 'react';
import is from 'object-is';
import uuidv4 from 'uuid/v4';
import entries from 'object.entries';
import functionName from 'function.prototype.name';
-import {
- isDOMComponent,
- findDOMNode,
- childrenToArray,
-} from './react-compat';
-import {
- REACT013,
- REACT15,
-} from './version';
+import configuration from './configuration';
+import validateAdapter from './validateAdapter';
export const ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
-function internalInstanceKey(node) {
- return Object.keys(Object(node)).filter(key => key.match(/^__reactInternalInstance\$/))[0];
-}
-
-export function internalInstance(inst) {
- return inst._reactInternalInstance ||
- inst[internalInstanceKey(inst)];
+export function getAdapter(options = {}) {
+ if (options.adapter) {
+ validateAdapter(options.adapter);
+ return options.adapter;
+ }
+ const adapter = configuration.get().adapter;
+ validateAdapter(adapter);
+ return adapter;
}
+// TODO(lmr): we shouldn't need this
export function isFunctionalComponent(inst) {
return !!inst && !!inst.constructor && typeof inst.constructor === 'function' &&
functionName(inst.constructor) === 'StatelessComponent';
}
-export function isCustomComponentElement(inst) {
- return !!inst && React.isValidElement(inst) && typeof inst.type === 'function';
+export function isCustomComponentElement(inst, adapter) {
+ return !!inst && adapter.isValidElement(inst) && typeof inst.type === 'function';
}
-export function propsOfNode(node) {
- if (REACT013 && node && node._store) {
- return (node._store.props) || {};
- }
- if (node && node._reactInternalComponent && node._reactInternalComponent._currentElement) {
- return (node._reactInternalComponent._currentElement.props) || {};
- }
- if (node && node._currentElement) {
- return (node._currentElement.props) || {};
- }
- if (REACT15 && node) {
- if (internalInstance(node) && internalInstance(node)._currentElement) {
- return (internalInstance(node)._currentElement.props) || {};
- }
- }
-
+function propsOfNode(node) {
return (node && node.props) || {};
}
@@ -58,10 +37,6 @@ export function typeOfNode(node) {
return node ? node.type : null;
}
-export function getNode(node) {
- return isDOMComponent(node) ? findDOMNode(node) : node;
-}
-
export function nodeHasType(node, type) {
if (!type || !node) return false;
if (!node.type) return false;
@@ -86,11 +61,11 @@ function internalChildrenCompare(a, b, lenComp, isLoose) {
return true;
}
-export function childrenMatch(a, b, lenComp) {
+function childrenMatch(a, b, lenComp) {
return internalChildrenCompare(a, b, lenComp, true);
}
-export function childrenEqual(a, b, lenComp) {
+function childrenEqual(a, b, lenComp) {
return internalChildrenCompare(a, b, lenComp, false);
}
@@ -167,6 +142,22 @@ function arraysEqual(match, left, right) {
return left.length === right.length && left.every((el, i) => match(el, right[i]));
}
+function childrenToArray(children) {
+ const result = [];
+
+ const push = (el) => {
+ if (el === null || el === false || el === undefined) return;
+ result.push(el);
+ };
+
+ if (Array.isArray(children)) {
+ children.forEach(push);
+ } else {
+ push(children);
+ }
+ return result;
+}
+
export function childrenToSimplifiedArray(nodeChildren) {
const childrenArray = childrenToArray(nodeChildren);
const simplifiedArray = [];
@@ -198,17 +189,11 @@ function isTextualNode(node) {
return typeof node === 'string' || typeof node === 'number';
}
-export function isReactElementAlike(arg) {
- return React.isValidElement(arg) || isTextualNode(arg) || Array.isArray(arg);
-}
-
-// 'click' => 'onClick'
-// 'mouseEnter' => 'onMouseEnter'
-export function propFromEvent(event) {
- const nativeEvent = mapNativeEventNames(event);
- return `on${nativeEvent[0].toUpperCase()}${nativeEvent.slice(1)}`;
+export function isReactElementAlike(arg, adapter) {
+ return adapter.isValidElement(arg) || isTextualNode(arg) || Array.isArray(arg);
}
+// TODO(lmr): can we get rid of this outside of the adapter?
export function withSetStateAllowed(fn) {
// NOTE(lmr):
// this is currently here to circumvent a React bug where `setState()` is
@@ -273,7 +258,7 @@ export function isPseudoClassSelector(selector) {
return false;
}
-export function selectorError(selector, type = '') {
+function selectorError(selector, type = '') {
return new TypeError(
`Enzyme received a ${type} CSS selector ('${selector}') that it does not currently support`,
);
@@ -376,54 +361,6 @@ export function nodeHasProperty(node, propKey, stringifiedPropValue) {
return Object.prototype.hasOwnProperty.call(nodeProps, propKey);
}
-export function mapNativeEventNames(event) {
- const nativeToReactEventMap = {
- compositionend: 'compositionEnd',
- compositionstart: 'compositionStart',
- compositionupdate: 'compositionUpdate',
- keydown: 'keyDown',
- keyup: 'keyUp',
- keypress: 'keyPress',
- contextmenu: 'contextMenu',
- dblclick: 'doubleClick',
- doubleclick: 'doubleClick', // kept for legacy. TODO: remove with next major.
- dragend: 'dragEnd',
- dragenter: 'dragEnter',
- dragexist: 'dragExit',
- dragleave: 'dragLeave',
- dragover: 'dragOver',
- dragstart: 'dragStart',
- mousedown: 'mouseDown',
- mousemove: 'mouseMove',
- mouseout: 'mouseOut',
- mouseover: 'mouseOver',
- mouseup: 'mouseUp',
- touchcancel: 'touchCancel',
- touchend: 'touchEnd',
- touchmove: 'touchMove',
- touchstart: 'touchStart',
- canplay: 'canPlay',
- canplaythrough: 'canPlayThrough',
- durationchange: 'durationChange',
- loadeddata: 'loadedData',
- loadedmetadata: 'loadedMetadata',
- loadstart: 'loadStart',
- ratechange: 'rateChange',
- timeupdate: 'timeUpdate',
- volumechange: 'volumeChange',
- beforeinput: 'beforeInput',
- };
-
- if (!REACT013) {
- // these could not be simulated in React 0.13:
- // https://github.com/facebook/react/issues/1297
- nativeToReactEventMap.mouseenter = 'mouseEnter';
- nativeToReactEventMap.mouseleave = 'mouseLeave';
- }
-
- return nativeToReactEventMap[event] || event;
-}
-
export function displayNameOfNode(node) {
const { type } = node;
diff --git a/src/adapters/.eslintrc b/src/adapters/.eslintrc
new file mode 100644
index 000000000..557c66e59
--- /dev/null
+++ b/src/adapters/.eslintrc
@@ -0,0 +1,12 @@
+{
+ "rules": {
+ "import/no-extraneous-dependencies": 0,
+ "import/no-unresolved": 0,
+ "import/extensions": 0,
+ "react/no-deprecated": 0,
+ "react/no-find-dom-node": 0,
+ "react/no-multi-comp": 0,
+ "no-underscore-dangle": 0,
+ "class-methods-use-this": 0
+ }
+}
diff --git a/src/adapters/EnzymeAdapter.js b/src/adapters/EnzymeAdapter.js
new file mode 100644
index 000000000..76122a50b
--- /dev/null
+++ b/src/adapters/EnzymeAdapter.js
@@ -0,0 +1,40 @@
+function unimplementedError(methodName, classname) {
+ return new Error(
+ `${methodName} is a required method of ${classname}, but was not implemented.`,
+ );
+}
+
+class EnzymeAdapter {
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createRenderer(options) {
+ throw unimplementedError('createRenderer', 'EnzymeAdapter');
+ }
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ nodeToElement(node) {
+ throw unimplementedError('nodeToElement', 'EnzymeAdapter');
+ }
+
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ isValidElement(element) {
+ throw unimplementedError('isValidElement', 'EnzymeAdapter');
+ }
+
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createElement(type, props, ...children) {
+ throw unimplementedError('createElement', 'EnzymeAdapter');
+ }
+}
+
+EnzymeAdapter.MODES = {
+ STRING: 'string',
+ MOUNT: 'mount',
+ SHALLOW: 'shallow',
+};
+
+module.exports = EnzymeAdapter;
diff --git a/src/adapters/ReactFifteenAdapter.js b/src/adapters/ReactFifteenAdapter.js
new file mode 100644
index 000000000..0d7602338
--- /dev/null
+++ b/src/adapters/ReactFifteenAdapter.js
@@ -0,0 +1,213 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactDOMServer from 'react-dom/server';
+import TestUtils from 'react-dom/test-utils';
+import PropTypes from 'prop-types';
+import values from 'object.values';
+import EnzymeAdapter from './EnzymeAdapter';
+import elementToTree from './elementToTree';
+import {
+ mapNativeEventNames,
+ propFromEvent,
+ withSetStateAllowed,
+ assertDomAvailable,
+} from './Utils';
+
+function compositeTypeToNodeType(type) {
+ switch (type) {
+ case 0:
+ case 1: return 'class';
+ case 2: return 'function';
+ default:
+ throw new Error(`Enzyme Internal Error: unknown composite type ${type}`);
+ }
+}
+
+function instanceToTree(inst) {
+ if (!inst || typeof inst !== 'object') {
+ return inst;
+ }
+ const el = inst._currentElement;
+ if (!el) {
+ return null;
+ }
+ if (inst._renderedChildren) {
+ return {
+ nodeType: inst._hostNode ? 'host' : compositeTypeToNodeType(inst._compositeType),
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: values(inst._renderedChildren).map(instanceToTree),
+ };
+ }
+ if (inst._hostNode) {
+ if (typeof el !== 'object') {
+ return el;
+ }
+ const children = inst._renderedChildren || { '.0': el.props.children };
+ return {
+ nodeType: 'host',
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: values(children).map(instanceToTree),
+ };
+ }
+ if (inst._renderedComponent) {
+ return {
+ nodeType: compositeTypeToNodeType(inst._compositeType),
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: instanceToTree(inst._renderedComponent),
+ };
+ }
+ throw new Error('Enzyme Internal Error: unknown instance encountered');
+}
+
+class SimpleWrapper extends React.Component {
+ render() {
+ return this.props.node || null;
+ }
+}
+
+SimpleWrapper.propTypes = { node: PropTypes.node.isRequired };
+
+class ReactFifteenAdapter extends EnzymeAdapter {
+ createMountRenderer(options) {
+ assertDomAvailable('mount');
+ const domNode = options.attachTo || global.document.createElement('div');
+ let instance = null;
+ return {
+ render(el/* , context */) {
+ const wrappedEl = React.createElement(SimpleWrapper, {
+ node: el,
+ });
+ instance = ReactDOM.render(wrappedEl, domNode);
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(domNode);
+ },
+ getNode() {
+ return instanceToTree(instance._reactInternalInstance._renderedComponent);
+ },
+ simulateEvent(node, event, mock) {
+ const mappedEvent = mapNativeEventNames(event);
+ const eventFn = TestUtils.Simulate[mappedEvent];
+ if (!eventFn) {
+ throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
+ }
+ // eslint-disable-next-line react/no-find-dom-node
+ eventFn(ReactDOM.findDOMNode(node.instance), mock);
+ },
+ batchedUpdates(fn) {
+ return ReactDOM.unstable_batchedUpdates(fn);
+ },
+ };
+ }
+
+ createShallowRenderer(/* options */) {
+ const renderer = TestUtils.createRenderer();
+ let isDOM = false;
+ let cachedNode = null;
+ return {
+ render(el, context) {
+ cachedNode = el;
+ /* eslint consistent-return: 0 */
+ if (typeof el.type === 'string') {
+ isDOM = true;
+ } else {
+ isDOM = false;
+ return withSetStateAllowed(() => renderer.render(el, context));
+ }
+ },
+ unmount() {
+ renderer.unmount();
+ },
+ getNode() {
+ if (isDOM) {
+ return elementToTree(cachedNode);
+ }
+ const output = renderer.getRenderOutput();
+ return {
+ nodeType: 'class',
+ type: cachedNode.type,
+ props: cachedNode.props,
+ key: cachedNode.key,
+ ref: cachedNode.ref,
+ instance: renderer._instance._instance,
+ rendered: elementToTree(output),
+ };
+ },
+ simulateEvent(node, event, ...args) {
+ const handler = node.props[propFromEvent(event)];
+ if (handler) {
+ withSetStateAllowed(() => {
+ // TODO(lmr): create/use synthetic events
+ // TODO(lmr): emulate React's event propagation
+ renderer.unstable_batchedUpdates(() => {
+ handler(...args);
+ });
+ });
+ }
+ },
+ batchedUpdates(fn) {
+ return withSetStateAllowed(() => renderer.unstable_batchedUpdates(fn));
+ },
+ };
+ }
+
+ createStringRenderer(/* options */) {
+ return {
+ render(el /* , context */) {
+ return ReactDOMServer.renderToStaticMarkup(el);
+ },
+ };
+ }
+
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createRenderer(options) {
+ switch (options.mode) {
+ case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
+ case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
+ case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
+ default:
+ throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
+ }
+ }
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ nodeToElement(node) {
+ if (!node || typeof node !== 'object') return null;
+ return React.createElement(node.type, node.props);
+ }
+
+ elementToNode(element) {
+ return elementToTree(element);
+ }
+
+ nodeToHostNode(node) {
+ return ReactDOM.findDOMNode(node.instance);
+ }
+
+ isValidElement(element) {
+ return React.isValidElement(element);
+ }
+
+ createElement(...args) {
+ return React.createElement(...args);
+ }
+}
+
+module.exports = ReactFifteenAdapter;
diff --git a/src/adapters/ReactFifteenFourAdapter.js b/src/adapters/ReactFifteenFourAdapter.js
new file mode 100644
index 000000000..43c161868
--- /dev/null
+++ b/src/adapters/ReactFifteenFourAdapter.js
@@ -0,0 +1,213 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactDOMServer from 'react-dom/server';
+import TestUtils from 'react-addons-test-utils';
+import PropTypes from 'prop-types';
+import values from 'object.values';
+import EnzymeAdapter from './EnzymeAdapter';
+import elementToTree from './elementToTree';
+import {
+ mapNativeEventNames,
+ propFromEvent,
+ withSetStateAllowed,
+ assertDomAvailable,
+} from './Utils';
+
+function compositeTypeToNodeType(type) {
+ switch (type) {
+ case 0:
+ case 1: return 'class';
+ case 2: return 'function';
+ default:
+ throw new Error(`Enzyme Internal Error: unknown composite type ${type}`);
+ }
+}
+
+function instanceToTree(inst) {
+ if (!inst || typeof inst !== 'object') {
+ return inst;
+ }
+ const el = inst._currentElement;
+ if (!el) {
+ return null;
+ }
+ if (inst._renderedChildren) {
+ return {
+ nodeType: inst._hostNode ? 'host' : compositeTypeToNodeType(inst._compositeType),
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: values(inst._renderedChildren).map(instanceToTree),
+ };
+ }
+ if (inst._hostNode) {
+ if (typeof el !== 'object') {
+ return el;
+ }
+ const children = inst._renderedChildren || { '.0': el.props.children };
+ return {
+ nodeType: 'host',
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: values(children).map(instanceToTree),
+ };
+ }
+ if (inst._renderedComponent) {
+ return {
+ nodeType: compositeTypeToNodeType(inst._compositeType),
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: instanceToTree(inst._renderedComponent),
+ };
+ }
+ throw new Error('Enzyme Internal Error: unknown instance encountered');
+}
+
+class SimpleWrapper extends React.Component {
+ render() {
+ return this.props.node || null;
+ }
+}
+
+SimpleWrapper.propTypes = { node: PropTypes.node.isRequired };
+
+class ReactFifteenFourAdapter extends EnzymeAdapter {
+ createMountRenderer(options) {
+ assertDomAvailable('mount');
+ const domNode = options.attachTo || global.document.createElement('div');
+ let instance = null;
+ return {
+ render(el/* , context */) {
+ const wrappedEl = React.createElement(SimpleWrapper, {
+ node: el,
+ });
+ instance = ReactDOM.render(wrappedEl, domNode);
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(domNode);
+ },
+ getNode() {
+ return instanceToTree(instance._reactInternalInstance._renderedComponent);
+ },
+ simulateEvent(node, event, mock) {
+ const mappedEvent = mapNativeEventNames(event);
+ const eventFn = TestUtils.Simulate[mappedEvent];
+ if (!eventFn) {
+ throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
+ }
+ // eslint-disable-next-line react/no-find-dom-node
+ eventFn(ReactDOM.findDOMNode(node.instance), mock);
+ },
+ batchedUpdates(fn) {
+ return ReactDOM.unstable_batchedUpdates(fn);
+ },
+ };
+ }
+
+ createShallowRenderer(/* options */) {
+ const renderer = TestUtils.createRenderer();
+ let isDOM = false;
+ let cachedNode = null;
+ return {
+ render(el, context) {
+ cachedNode = el;
+ /* eslint consistent-return: 0 */
+ if (typeof el.type === 'string') {
+ isDOM = true;
+ } else {
+ isDOM = false;
+ return withSetStateAllowed(() => renderer.render(el, context));
+ }
+ },
+ unmount() {
+ renderer.unmount();
+ },
+ getNode() {
+ if (isDOM) {
+ return elementToTree(cachedNode);
+ }
+ const output = renderer.getRenderOutput();
+ return {
+ nodeType: 'class',
+ type: cachedNode.type,
+ props: cachedNode.props,
+ key: cachedNode.key,
+ ref: cachedNode.ref,
+ instance: renderer._instance._instance,
+ rendered: elementToTree(output),
+ };
+ },
+ simulateEvent(node, event, ...args) {
+ const handler = node.props[propFromEvent(event)];
+ if (handler) {
+ withSetStateAllowed(() => {
+ // TODO(lmr): create/use synthetic events
+ // TODO(lmr): emulate React's event propagation
+ ReactDOM.unstable_batchedUpdates(() => {
+ handler(...args);
+ });
+ });
+ }
+ },
+ batchedUpdates(fn) {
+ return withSetStateAllowed(() => ReactDOM.unstable_batchedUpdates(fn));
+ },
+ };
+ }
+
+ createStringRenderer(/* options */) {
+ return {
+ render(el /* , context */) {
+ return ReactDOMServer.renderToStaticMarkup(el);
+ },
+ };
+ }
+
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createRenderer(options) {
+ switch (options.mode) {
+ case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
+ case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
+ case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
+ default:
+ throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
+ }
+ }
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ nodeToElement(node) {
+ if (!node || typeof node !== 'object') return null;
+ return React.createElement(node.type, node.props);
+ }
+
+ elementToNode(element) {
+ return elementToTree(element);
+ }
+
+ nodeToHostNode(node) {
+ return ReactDOM.findDOMNode(node.instance);
+ }
+
+ isValidElement(element) {
+ return React.isValidElement(element);
+ }
+
+ createElement(...args) {
+ return React.createElement(...args);
+ }
+}
+
+module.exports = ReactFifteenFourAdapter;
diff --git a/src/adapters/ReactFourteenAdapter.js b/src/adapters/ReactFourteenAdapter.js
new file mode 100644
index 000000000..0a383b933
--- /dev/null
+++ b/src/adapters/ReactFourteenAdapter.js
@@ -0,0 +1,205 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactDOMServer from 'react-dom/server';
+import TestUtils from 'react-addons-test-utils';
+import PropTypes from 'prop-types';
+import values from 'object.values';
+import EnzymeAdapter from './EnzymeAdapter';
+import elementToTree from './elementToTree';
+import {
+ mapNativeEventNames,
+ propFromEvent,
+ withSetStateAllowed,
+ assertDomAvailable,
+} from './Utils';
+
+function typeToNodeType(type) {
+ if (typeof type === 'function') {
+ if (typeof type.prototype.render === 'function') {
+ return 'class';
+ }
+ return 'function';
+ }
+ return 'host';
+}
+
+function instanceToTree(inst) {
+ if (!inst || typeof inst !== 'object') {
+ return inst;
+ }
+ const el = inst._currentElement;
+ if (!el) {
+ return null;
+ }
+ if (typeof el !== 'object') {
+ return el;
+ }
+ if (inst._tag) {
+ if (typeof el !== 'object') {
+ return el;
+ }
+ const children = inst._renderedChildren || { '.0': el.props.children };
+ return {
+ nodeType: 'host',
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: ReactDOM.findDOMNode(inst.getPublicInstance()) || null,
+ rendered: values(children).map(instanceToTree),
+ };
+ }
+ if (inst._renderedComponent) {
+ return {
+ nodeType: typeToNodeType(el.type),
+ type: el.type,
+ props: el.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || null,
+ rendered: instanceToTree(inst._renderedComponent),
+ };
+ }
+ throw new Error('Enzyme Internal Error: unknown instance encountered');
+}
+
+class SimpleWrapper extends React.Component {
+ render() {
+ return this.props.node || null;
+ }
+}
+
+SimpleWrapper.propTypes = { node: PropTypes.node.isRequired };
+
+class ReactFifteenAdapter extends EnzymeAdapter {
+ createMountRenderer(options) {
+ assertDomAvailable('mount');
+ const domNode = options.attachTo || global.document.createElement('div');
+ let instance = null;
+ return {
+ render(el/* , context */) {
+ const wrappedEl = React.createElement(SimpleWrapper, {
+ node: el,
+ });
+ instance = ReactDOM.render(wrappedEl, domNode);
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(domNode);
+ },
+ getNode() {
+ return instanceToTree(instance._reactInternalInstance._renderedComponent);
+ },
+ simulateEvent(node, event, mock) {
+ const mappedEvent = mapNativeEventNames(event);
+ const eventFn = TestUtils.Simulate[mappedEvent];
+ if (!eventFn) {
+ throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
+ }
+ // eslint-disable-next-line react/no-find-dom-node
+ eventFn(ReactDOM.findDOMNode(node.instance), mock);
+ },
+ batchedUpdates(fn) {
+ return ReactDOM.unstable_batchedUpdates(fn);
+ },
+ };
+ }
+
+ createShallowRenderer(/* options */) {
+ const renderer = TestUtils.createRenderer();
+ let isDOM = false;
+ let cachedNode = null;
+ return {
+ render(el, context) {
+ cachedNode = el;
+ /* eslint consistent-return: 0 */
+ if (typeof el.type === 'string') {
+ isDOM = true;
+ } else {
+ isDOM = false;
+ return withSetStateAllowed(() => renderer.render(el, context));
+ }
+ },
+ unmount() {
+ renderer.unmount();
+ },
+ getNode() {
+ if (isDOM) {
+ return elementToTree(cachedNode);
+ }
+ const output = renderer.getRenderOutput();
+ return {
+ nodeType: 'class',
+ type: cachedNode.type,
+ props: cachedNode.props,
+ key: cachedNode.key,
+ ref: cachedNode.ref,
+ instance: renderer._instance._instance,
+ rendered: elementToTree(output),
+ };
+ },
+ simulateEvent(node, event, ...args) {
+ const handler = node.props[propFromEvent(event)];
+ if (handler) {
+ withSetStateAllowed(() => {
+ // TODO(lmr): create/use synthetic events
+ // TODO(lmr): emulate React's event propagation
+ ReactDOM.unstable_batchedUpdates(() => {
+ handler(...args);
+ });
+ });
+ }
+ },
+ batchedUpdates(fn) {
+ return withSetStateAllowed(() => ReactDOM.unstable_batchedUpdates(fn));
+ },
+ };
+ }
+
+ createStringRenderer(/* options */) {
+ return {
+ render(el /* , context */) {
+ return ReactDOMServer.renderToStaticMarkup(el);
+ },
+ };
+ }
+
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createRenderer(options) {
+ switch (options.mode) {
+ case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
+ case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
+ case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
+ default:
+ throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
+ }
+ }
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ nodeToElement(node) {
+ if (!node || typeof node !== 'object') return null;
+ return React.createElement(node.type, node.props);
+ }
+
+ elementToNode(element) {
+ return elementToTree(element);
+ }
+
+ nodeToHostNode(node) {
+ return ReactDOM.findDOMNode(node.instance);
+ }
+
+ isValidElement(element) {
+ return React.isValidElement(element);
+ }
+
+ createElement(...args) {
+ return React.createElement(...args);
+ }
+}
+
+module.exports = ReactFifteenAdapter;
diff --git a/src/adapters/ReactSixteenAdapter.js b/src/adapters/ReactSixteenAdapter.js
new file mode 100644
index 000000000..e40bb0d24
--- /dev/null
+++ b/src/adapters/ReactSixteenAdapter.js
@@ -0,0 +1,283 @@
+/* eslint no-use-before-define: 0 */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ReactDOMServer from 'react-dom/server';
+import ShallowRenderer from 'react-test-renderer/shallow';
+import TestUtils from 'react-dom/test-utils';
+import PropTypes from 'prop-types';
+import EnzymeAdapter from './EnzymeAdapter';
+import elementToTree from './elementToTree';
+import {
+ mapNativeEventNames,
+ propFromEvent,
+ assertDomAvailable,
+ withSetStateAllowed,
+} from './Utils';
+
+const HostRoot = 3;
+const ClassComponent = 2;
+const Fragment = 10;
+const FunctionalComponent = 1;
+const HostComponent = 5;
+const HostText = 6;
+
+function nodeAndSiblingsArray(nodeWithSibling) {
+ const array = [];
+ let node = nodeWithSibling;
+ while (node != null) {
+ array.push(node);
+ node = node.sibling;
+ }
+ return array;
+}
+
+function flatten(arr) {
+ const result = [];
+ const stack = [{ i: 0, array: arr }];
+ while (stack.length) {
+ const 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 toTree(vnode) {
+ if (vnode == null) {
+ return null;
+ }
+ // TODO(lmr): I'm not really sure I understand whether or not this is what
+ // i should be doing, or if this is a hack for something i'm doing wrong
+ // somewhere else. Should talk to sebastian about this perhaps
+ const node = vnode.alternate !== null ? vnode.alternate : vnode;
+ switch (node.tag) {
+ case HostRoot: // 3
+ return toTree(node.child);
+ case ClassComponent:
+ return {
+ nodeType: 'class',
+ type: node.type,
+ props: { ...vnode.memoizedProps },
+ key: node.key,
+ ref: node.ref,
+ instance: node.stateNode,
+ rendered: childrenToTree(node.child),
+ };
+ case Fragment: // 10
+ return childrenToTree(node.child);
+ case FunctionalComponent: // 1
+ return {
+ nodeType: 'function',
+ type: node.type,
+ props: { ...vnode.memoizedProps },
+ key: node.key,
+ ref: node.ref,
+ instance: null,
+ rendered: childrenToTree(node.child),
+ };
+ case HostComponent: { // 5
+ let renderedNodes = flatten(nodeAndSiblingsArray(node.child).map(toTree));
+ if (renderedNodes.length === 0) {
+ renderedNodes = [node.memoizedProps.children];
+ }
+ return {
+ nodeType: 'host',
+ type: node.type,
+ props: { ...node.memoizedProps },
+ key: node.key,
+ ref: node.ref,
+ instance: node.stateNode,
+ rendered: renderedNodes,
+ };
+ }
+ case HostText: // 6
+ return node.memoizedProps;
+ default:
+ throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`);
+ }
+}
+
+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]);
+ }
+ return flatten(children.map(toTree));
+}
+
+function nodeToHostNode(_node) {
+ // NOTE(lmr): node could be a function component
+ // which wont have an instance prop, but we can get the
+ // host node associated with its return value at that point.
+ // Although this breaks down if the return value is an array,
+ // as is possible with React 16.
+ let node = _node;
+ while (node && !Array.isArray(node) && node.instance === null) {
+ node = node.rendered;
+ }
+ if (Array.isArray(node)) {
+ // TODO(lmr): throw warning regarding not being able to get a host node here
+ throw new Error('Trying to get host node of an array');
+ }
+ // if the SFC returned null effectively, there is no host node.
+ if (!node) {
+ return null;
+ }
+ return ReactDOM.findDOMNode(node.instance);
+}
+
+class SimpleWrapper extends React.Component {
+ render() {
+ return this.props.node || null;
+ }
+}
+
+SimpleWrapper.propTypes = { node: PropTypes.node.isRequired };
+
+class ReactSixteenAdapter extends EnzymeAdapter {
+ createMountRenderer(options) {
+ assertDomAvailable('mount');
+ const domNode = options.attachTo || global.document.createElement('div');
+ let instance = null;
+ return {
+ render(el/* , context */) {
+ const wrappedEl = React.createElement(SimpleWrapper, {
+ node: el,
+ });
+ instance = ReactDOM.render(wrappedEl, domNode);
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(domNode);
+ },
+ getNode() {
+ return toTree(instance._reactInternalInstance.child);
+ },
+ simulateEvent(node, event, mock) {
+ const mappedEvent = mapNativeEventNames(event);
+ const eventFn = TestUtils.Simulate[mappedEvent];
+ if (!eventFn) {
+ throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
+ }
+ // eslint-disable-next-line react/no-find-dom-node
+ eventFn(nodeToHostNode(node), mock);
+ },
+ batchedUpdates(fn) {
+ return fn();
+ // return ReactDOM.unstable_batchedUpdates(fn);
+ },
+ };
+ }
+
+ createShallowRenderer(/* options */) {
+ const renderer = new ShallowRenderer();
+ let isDOM = false;
+ let cachedNode = null;
+ return {
+ render(el, context) {
+ cachedNode = el;
+ /* eslint consistent-return: 0 */
+ if (typeof el.type === 'string') {
+ isDOM = true;
+ } else {
+ isDOM = false;
+ return withSetStateAllowed(() => renderer.render(el, context));
+ }
+ },
+ unmount() {
+ renderer.unmount();
+ },
+ getNode() {
+ if (isDOM) {
+ return elementToTree(cachedNode);
+ }
+ const output = renderer.getRenderOutput();
+ return {
+ nodeType: 'class',
+ type: cachedNode.type,
+ props: cachedNode.props,
+ key: cachedNode.key,
+ ref: cachedNode.ref,
+ instance: renderer._instance,
+ rendered: elementToTree(output),
+ };
+ },
+ simulateEvent(node, event, ...args) {
+ const handler = node.props[propFromEvent(event)];
+ if (handler) {
+ withSetStateAllowed(() => {
+ // TODO(lmr): create/use synthetic events
+ // TODO(lmr): emulate React's event propagation
+ // ReactDOM.unstable_batchedUpdates(() => {
+ handler(...args);
+ // });
+ });
+ }
+ },
+ batchedUpdates(fn) {
+ return fn();
+ // return ReactDOM.unstable_batchedUpdates(fn);
+ },
+ };
+ }
+
+ createStringRenderer(/* options */) {
+ return {
+ render(el /* , context */) {
+ return ReactDOMServer.renderToStaticMarkup(el);
+ },
+ };
+ }
+
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createRenderer(options) {
+ switch (options.mode) {
+ case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
+ case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
+ case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
+ default:
+ throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
+ }
+ }
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ nodeToElement(node) {
+ if (!node || typeof node !== 'object') return null;
+ return React.createElement(node.type, node.props);
+ }
+
+ elementToNode(element) {
+ return elementToTree(element);
+ }
+
+ nodeToHostNode(node) {
+ return nodeToHostNode(node);
+ }
+
+ isValidElement(element) {
+ return React.isValidElement(element);
+ }
+
+ createElement(...args) {
+ return React.createElement(...args);
+ }
+}
+
+module.exports = ReactSixteenAdapter;
diff --git a/src/adapters/ReactThirteenAdapter.js b/src/adapters/ReactThirteenAdapter.js
new file mode 100644
index 000000000..a223f2167
--- /dev/null
+++ b/src/adapters/ReactThirteenAdapter.js
@@ -0,0 +1,233 @@
+import React from 'react';
+import ReactAddons from 'react/addons';
+import ReactContext from 'react/lib/ReactContext';
+import PropTypes from 'prop-types';
+import values from 'object.values';
+import EnzymeAdapter from './EnzymeAdapter';
+import elementToTree from './elementToTree';
+import mapNativeEventNames from './ReactThirteenMapNativeEventNames';
+import {
+ propFromEvent,
+ withSetStateAllowed,
+ assertDomAvailable,
+} from './Utils';
+
+// this fixes some issues in React 0.13 with setState and jsdom...
+// see issue: https://github.com/airbnb/enzyme/issues/27
+// eslint-disable-next-line import/no-unresolved
+require('react/lib/ExecutionEnvironment').canUseDOM = true;
+
+const { TestUtils, batchedUpdates } = ReactAddons.addons;
+
+const getEmptyElementType = (() => {
+ let EmptyElementType = null;
+ // eslint-disable-next-line react/prefer-stateless-function
+ class Foo extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ return () => {
+ if (EmptyElementType === null) {
+ const instance = TestUtils.renderIntoDocument(React.createElement(Foo));
+ EmptyElementType = instance._reactInternalInstance._renderedComponent._currentElement.type;
+ }
+ return EmptyElementType;
+ };
+})();
+
+const createShallowRenderer = function createRendererCompatible() {
+ const renderer = TestUtils.createRenderer();
+ renderer.render = (originalRender => function contextCompatibleRender(node, context = {}) {
+ ReactContext.current = context;
+ originalRender.call(this, React.createElement(node.type, node.props), context);
+ ReactContext.current = {};
+ return renderer.getRenderOutput();
+ })(renderer.render);
+ return renderer;
+};
+
+
+function instanceToTree(inst) {
+ if (typeof inst !== 'object') {
+ return inst;
+ }
+ const el = inst._currentElement;
+ if (!el) {
+ return null;
+ }
+ if (typeof el !== 'object') {
+ return el;
+ }
+ if (el.type === getEmptyElementType()) {
+ return null;
+ }
+ if (typeof el.type === 'string') {
+ const innerInst = inst._renderedComponent;
+ const children = innerInst._renderedChildren || { '.0': el._store.props.children };
+ return {
+ nodeType: 'host',
+ type: el.type,
+ props: el._store.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance.getDOMNode(),
+ rendered: values(children).map(instanceToTree),
+ };
+ }
+ if (inst._renderedComponent) {
+ return {
+ nodeType: 'class',
+ type: el.type,
+ props: el._store.props,
+ key: el.key,
+ ref: el.ref,
+ instance: inst._instance || inst._hostNode || null,
+ rendered: instanceToTree(inst._renderedComponent),
+ };
+ }
+ throw new Error('Enzyme Internal Error: unknown instance encountered');
+}
+
+class SimpleWrapper extends React.Component {
+ render() {
+ return this.props.node || null;
+ }
+}
+
+SimpleWrapper.propTypes = { node: PropTypes.node.isRequired };
+
+class ReactThirteenAdapter extends EnzymeAdapter {
+ createMountRenderer(options) {
+ assertDomAvailable('mount');
+ const domNode = options.attachTo || global.document.createElement('div');
+ let instance = null;
+ return {
+ render(el/* , context */) {
+ const wrappedEl = React.createElement(SimpleWrapper, {
+ node: el,
+ });
+ instance = React.render(wrappedEl, domNode);
+ },
+ unmount() {
+ React.unmountComponentAtNode(domNode);
+ },
+ getNode() {
+ return instanceToTree(instance._reactInternalInstance._renderedComponent);
+ },
+ simulateEvent(node, event, mock) {
+ const mappedEvent = mapNativeEventNames(event);
+ const eventFn = TestUtils.Simulate[mappedEvent];
+ if (!eventFn) {
+ throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
+ }
+ // eslint-disable-next-line react/no-find-dom-node
+ eventFn(React.findDOMNode(node.instance), mock);
+ },
+ batchedUpdates(fn) {
+ return batchedUpdates(fn);
+ },
+ };
+ }
+
+ createShallowRenderer(/* options */) {
+ const renderer = createShallowRenderer();
+ let isDOM = false;
+ let cachedNode = null;
+ return {
+ render(el, context) {
+ cachedNode = el;
+ /* eslint consistent-return: 0 */
+ if (typeof el.type === 'string') {
+ isDOM = true;
+ } else {
+ isDOM = false;
+ // return withSetStateAllowed(() => renderer.render(el, context));
+ return renderer.render(el, context);
+ }
+ },
+ unmount() {
+ renderer.unmount();
+ },
+ getNode() {
+ if (isDOM) {
+ return elementToTree(cachedNode);
+ }
+ const output = renderer.getRenderOutput();
+ return {
+ nodeType: 'class',
+ type: cachedNode.type,
+ props: cachedNode.props,
+ key: cachedNode.key,
+ ref: cachedNode.ref,
+ instance: renderer._instance._instance,
+ rendered: elementToTree(output),
+ };
+ },
+ simulateEvent(node, event, ...args) {
+ const handler = node.props[propFromEvent(event)];
+ if (handler) {
+ withSetStateAllowed(() => {
+ // TODO(lmr): create/use synthetic events
+ // TODO(lmr): emulate React's event propagation
+ batchedUpdates(() => {
+ handler(...args);
+ });
+ });
+ }
+ },
+ batchedUpdates(fn) {
+ return withSetStateAllowed(() => batchedUpdates(fn));
+ },
+ };
+ }
+
+ createStringRenderer(/* options */) {
+ return {
+ render(el /* , context */) {
+ return React.renderToStaticMarkup(el);
+ },
+ };
+ }
+
+ // Provided a bag of options, return an `EnzymeRenderer`. Some options can be implementation
+ // specific, like `attach` etc. for React, but not part of this interface explicitly.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ createRenderer(options) {
+ switch (options.mode) {
+ case EnzymeAdapter.MODES.MOUNT: return this.createMountRenderer(options);
+ case EnzymeAdapter.MODES.SHALLOW: return this.createShallowRenderer(options);
+ case EnzymeAdapter.MODES.STRING: return this.createStringRenderer(options);
+ default:
+ throw new Error(`Enzyme Internal Error: Unrecognized mode: ${options.mode}`);
+ }
+ }
+
+ // converts an RSTNode to the corresponding JSX Pragma Element. This will be needed
+ // in order to implement the `Wrapper.mount()` and `Wrapper.shallow()` methods, but should
+ // be pretty straightforward for people to implement.
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
+ nodeToElement(node) {
+ if (!node || typeof node !== 'object') return null;
+ return React.createElement(node.type, node.props);
+ }
+
+ elementToNode(element) {
+ return elementToTree(element);
+ }
+
+ nodeToHostNode(node) {
+ return React.findDOMNode(node.instance);
+ }
+
+ isValidElement(element) {
+ return React.isValidElement(element);
+ }
+
+ createElement(...args) {
+ return React.createElement(...args);
+ }
+}
+
+module.exports = ReactThirteenAdapter;
diff --git a/src/adapters/ReactThirteenMapNativeEventNames.js b/src/adapters/ReactThirteenMapNativeEventNames.js
new file mode 100644
index 000000000..b18f41528
--- /dev/null
+++ b/src/adapters/ReactThirteenMapNativeEventNames.js
@@ -0,0 +1,39 @@
+export default function mapNativeEventNames(event) {
+ const nativeToReactEventMap = {
+ compositionend: 'compositionEnd',
+ compositionstart: 'compositionStart',
+ compositionupdate: 'compositionUpdate',
+ keydown: 'keyDown',
+ keyup: 'keyUp',
+ keypress: 'keyPress',
+ contextmenu: 'contextMenu',
+ dblclick: 'doubleClick',
+ doubleclick: 'doubleClick', // kept for legacy. TODO: remove with next major.
+ dragend: 'dragEnd',
+ dragenter: 'dragEnter',
+ dragexist: 'dragExit',
+ dragleave: 'dragLeave',
+ dragover: 'dragOver',
+ dragstart: 'dragStart',
+ mousedown: 'mouseDown',
+ mousemove: 'mouseMove',
+ mouseout: 'mouseOut',
+ mouseover: 'mouseOver',
+ mouseup: 'mouseUp',
+ touchcancel: 'touchCancel',
+ touchend: 'touchEnd',
+ touchmove: 'touchMove',
+ touchstart: 'touchStart',
+ canplay: 'canPlay',
+ canplaythrough: 'canPlayThrough',
+ durationchange: 'durationChange',
+ loadeddata: 'loadedData',
+ loadedmetadata: 'loadedMetadata',
+ loadstart: 'loadStart',
+ ratechange: 'rateChange',
+ timeupdate: 'timeUpdate',
+ volumechange: 'volumeChange',
+ beforeinput: 'beforeInput',
+ };
+ return nativeToReactEventMap[event] || event;
+}
diff --git a/src/adapters/Utils.js b/src/adapters/Utils.js
new file mode 100644
index 000000000..302f9da54
--- /dev/null
+++ b/src/adapters/Utils.js
@@ -0,0 +1,76 @@
+export function mapNativeEventNames(event) {
+ const nativeToReactEventMap = {
+ compositionend: 'compositionEnd',
+ compositionstart: 'compositionStart',
+ compositionupdate: 'compositionUpdate',
+ keydown: 'keyDown',
+ keyup: 'keyUp',
+ keypress: 'keyPress',
+ contextmenu: 'contextMenu',
+ dblclick: 'doubleClick',
+ doubleclick: 'doubleClick', // kept for legacy. TODO: remove with next major.
+ dragend: 'dragEnd',
+ dragenter: 'dragEnter',
+ dragexist: 'dragExit',
+ dragleave: 'dragLeave',
+ dragover: 'dragOver',
+ dragstart: 'dragStart',
+ mousedown: 'mouseDown',
+ mousemove: 'mouseMove',
+ mouseout: 'mouseOut',
+ mouseover: 'mouseOver',
+ mouseup: 'mouseUp',
+ touchcancel: 'touchCancel',
+ touchend: 'touchEnd',
+ touchmove: 'touchMove',
+ touchstart: 'touchStart',
+ canplay: 'canPlay',
+ canplaythrough: 'canPlayThrough',
+ durationchange: 'durationChange',
+ loadeddata: 'loadedData',
+ loadedmetadata: 'loadedMetadata',
+ loadstart: 'loadStart',
+ ratechange: 'rateChange',
+ timeupdate: 'timeUpdate',
+ volumechange: 'volumeChange',
+ beforeinput: 'beforeInput',
+ mouseenter: 'mouseEnter',
+ mouseleave: 'mouseLeave',
+ };
+
+ return nativeToReactEventMap[event] || event;
+}
+
+// 'click' => 'onClick'
+// 'mouseEnter' => 'onMouseEnter'
+export function propFromEvent(event) {
+ const nativeEvent = mapNativeEventNames(event);
+ return `on${nativeEvent[0].toUpperCase()}${nativeEvent.slice(1)}`;
+}
+
+export function withSetStateAllowed(fn) {
+ // NOTE(lmr):
+ // this is currently here to circumvent a React bug where `setState()` is
+ // not allowed without global being defined.
+ let cleanup = false;
+ if (typeof global.document === 'undefined') {
+ cleanup = true;
+ global.document = {};
+ }
+ const result = fn();
+ if (cleanup) {
+ // This works around a bug in node/jest in that developers aren't able to
+ // delete things from global when running in a node vm.
+ global.document = undefined;
+ delete global.document;
+ }
+ return result;
+}
+
+export function assertDomAvailable(feature) {
+ if (!global || !global.document || !global.document.createElement) {
+ throw new Error(
+ `Enzyme's ${feature} expects a DOM environment to be loaded, but found none`,
+ );
+ }
+}
diff --git a/src/adapters/elementToTree.js b/src/adapters/elementToTree.js
new file mode 100644
index 000000000..b9c95cc00
--- /dev/null
+++ b/src/adapters/elementToTree.js
@@ -0,0 +1,38 @@
+import flatten from 'lodash/flatten';
+
+function nodeTypeFromType(type) {
+ if (typeof type === 'string') {
+ return 'host';
+ }
+ if (
+ type &&
+ type.prototype &&
+ (type.prototype.isReactComponent || typeof type.prototype.render === 'function')
+ ) {
+ return 'class';
+ }
+ return 'function';
+}
+
+export default function elementToTree(el) {
+ if (el === null || typeof el !== 'object' || !('type' in el)) {
+ return el;
+ }
+ const { type, props, key, ref } = el;
+ const { children } = props;
+ let rendered = null;
+ if (Array.isArray(children)) {
+ rendered = flatten(children, true).map(elementToTree);
+ } else if (children !== undefined) {
+ rendered = elementToTree(children);
+ }
+ return {
+ nodeType: nodeTypeFromType(type),
+ type,
+ props,
+ key,
+ ref,
+ instance: null,
+ rendered,
+ };
+}
diff --git a/src/configuration.js b/src/configuration.js
new file mode 100644
index 000000000..755c39504
--- /dev/null
+++ b/src/configuration.js
@@ -0,0 +1,13 @@
+import validateAdapter from './validateAdapter';
+
+const configuration = {};
+
+module.exports = {
+ get() { return { ...configuration }; },
+ merge(extra) {
+ if (extra.adapter) {
+ validateAdapter(extra.adapter);
+ }
+ Object.assign(configuration, extra);
+ },
+};
diff --git a/src/index.js b/src/index.js
index e5ef7ce8e..2c3a41b9b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,11 +4,13 @@ import ShallowWrapper from './ShallowWrapper';
import mount from './mount';
import shallow from './shallow';
import render from './render';
+import { merge as configure } from './configuration';
-export {
+module.exports = {
render,
shallow,
mount,
ShallowWrapper,
ReactWrapper,
+ configure,
};
diff --git a/src/react-compat.js b/src/react-compat.js
deleted file mode 100644
index fe1f0613d..000000000
--- a/src/react-compat.js
+++ /dev/null
@@ -1,208 +0,0 @@
-/* eslint
- global-require: 0,
- import/no-mutable-exports: 0,
- import/no-unresolved: 0,
- react/no-deprecated: 0,
- react/no-render-return-value: 0,
-*/
-
-import { REACT013, REACT155 } from './version';
-
-let TestUtils;
-let createShallowRenderer;
-let renderToStaticMarkup;
-let renderIntoDocument;
-let findDOMNode;
-let childrenToArray;
-let renderWithOptions;
-let unmountComponentAtNode;
-let batchedUpdates;
-let shallowRendererFactory;
-
-const React = require('react');
-
-if (REACT013) {
- renderToStaticMarkup = React.renderToStaticMarkup;
- /* eslint-disable react/no-deprecated */
- findDOMNode = React.findDOMNode;
- unmountComponentAtNode = React.unmountComponentAtNode;
- /* eslint-enable react/no-deprecated */
- TestUtils = require('react/addons').addons.TestUtils;
- batchedUpdates = require('react/addons').addons.batchedUpdates;
- const ReactContext = require('react/lib/ReactContext');
-
- // Shallow rendering in 0.13 did not properly support context. This function provides a shim
- // around `TestUtils.createRenderer` that instead returns a ShallowRenderer that actually
- // works with context. See https://github.com/facebook/react/issues/3721 for more details.
- createShallowRenderer = function createRendererCompatible() {
- const renderer = TestUtils.createRenderer();
- renderer.render = (originalRender => function contextCompatibleRender(node, context = {}) {
- ReactContext.current = context;
- originalRender.call(this, React.createElement(node.type, node.props), context);
- ReactContext.current = {};
- return renderer.getRenderOutput();
- })(renderer.render);
- return renderer;
- };
- renderIntoDocument = TestUtils.renderIntoDocument;
- // this fixes some issues in React 0.13 with setState and jsdom...
- // see issue: https://github.com/airbnb/enzyme/issues/27
- require('react/lib/ExecutionEnvironment').canUseDOM = true;
-
- // in 0.13, a Children.toArray function was not exported. Make our own instead.
- childrenToArray = (children) => {
- const results = [];
- if (children !== undefined && children !== null && children !== false) {
- React.Children.forEach(children, (el) => {
- if (el !== undefined && el !== null && el !== false) {
- results.push(el);
- }
- });
- }
- return results;
- };
-
- renderWithOptions = (node, options) => {
- if (options.attachTo) {
- return React.render(node, options.attachTo);
- }
- return TestUtils.renderIntoDocument(node);
- };
-} else {
- let ReactDOM;
-
- try {
- // eslint-disable-next-line import/no-extraneous-dependencies
- ReactDOM = require('react-dom');
- } catch (e) {
- throw new Error(
- 'react-dom is an implicit dependency in order to support react@0.13-14. ' +
- 'Please add the appropriate version to your devDependencies. ' +
- 'See https://github.com/airbnb/enzyme#installation',
- );
- }
-
- // eslint-disable-next-line import/no-extraneous-dependencies
- renderToStaticMarkup = require('react-dom/server').renderToStaticMarkup;
-
- findDOMNode = ReactDOM.findDOMNode;
- unmountComponentAtNode = ReactDOM.unmountComponentAtNode;
- batchedUpdates = ReactDOM.unstable_batchedUpdates;
- // We require the testutils, but they don't come with 0.14 out of the box, so we
- // require them here through this node module. The bummer is that we are not able
- // to list this as a dependency in package.json and have 0.13 work properly.
- // As a result, right now this is basically an implicit dependency.
- try {
- try {
- // This is for react v15.5 and up...
-
- // eslint-disable-next-line import/no-extraneous-dependencies
- TestUtils = require('react-dom/test-utils');
- // eslint-disable-next-line import/no-extraneous-dependencies
- shallowRendererFactory = require('react-test-renderer/shallow').createRenderer;
- } catch (e) {
- // This is for react < v15.5. Note that users who have `react^15.4.x` in their package.json
- // will arrive here, too. They need to upgrade. React will print a nice warning letting
- // them know they need to upgrade, though, so we're good. Also note we explicitly do not
- // use TestUtils from react-dom/test-utils here, mainly so the user still gets a warning for
- // requiring 'react-addons-test-utils', which lets them know there's action required.
-
- // eslint-disable-next-line import/no-extraneous-dependencies
- TestUtils = require('react-addons-test-utils');
- shallowRendererFactory = TestUtils.createRenderer;
- }
- } catch (e) {
- if (REACT155) {
- throw new Error(
- 'react-dom@15.5+ and react-test-renderer are implicit dependencies when using ' +
- 'react@15.5+ with enzyme. Please add the appropriate version to your ' +
- 'devDependencies. See https://github.com/airbnb/enzyme#installation',
- );
- } else {
- throw new Error(
- 'react-addons-test-utils is an implicit dependency in order to support react@0.13-14. ' +
- 'Please add the appropriate version to your devDependencies. ' +
- 'See https://github.com/airbnb/enzyme#installation',
- );
- }
- }
-
- // Shallow rendering changed from 0.13 => 0.14 in such a way that
- // 0.14 now does not allow shallow rendering of native DOM elements.
- // This is mainly because the result of such a call should not realistically
- // be any different than the JSX you passed in (result of `React.createElement`.
- // In order to maintain the same behavior across versions, this function
- // is essentially a replacement for `TestUtils.createRenderer` that doesn't use
- // shallow rendering when it's just a DOM element.
- createShallowRenderer = function createRendererCompatible() {
- const renderer = shallowRendererFactory();
- const originalRender = renderer.render;
- const originalRenderOutput = renderer.getRenderOutput;
- let isDOM = false;
- let cachedNode;
- return Object.assign(renderer, {
- render(node, context) {
- /* eslint consistent-return: 0 */
- if (typeof node.type === 'string') {
- isDOM = true;
- cachedNode = node;
- } else {
- isDOM = false;
- return originalRender.call(this, node, context);
- }
- },
- getRenderOutput() {
- if (isDOM) {
- return cachedNode;
- }
- return originalRenderOutput.call(this);
- },
- });
- };
- renderIntoDocument = TestUtils.renderIntoDocument;
- childrenToArray = React.Children.toArray;
-
- renderWithOptions = (node, options) => {
- if (options.attachTo) {
- return ReactDOM.render(node, options.attachTo);
- }
- return TestUtils.renderIntoDocument(node);
- };
-}
-
-function isDOMComponentElement(inst) {
- return React.isValidElement(inst) && typeof inst.type === 'string';
-}
-
-const {
- mockComponent,
- isElement,
- isElementOfType,
- isDOMComponent,
- isCompositeComponent,
- isCompositeComponentWithType,
- isCompositeComponentElement,
- Simulate,
- findAllInRenderedTree,
-} = TestUtils;
-
-export {
- createShallowRenderer,
- renderToStaticMarkup,
- renderIntoDocument,
- mockComponent,
- isElement,
- isElementOfType,
- isDOMComponent,
- isDOMComponentElement,
- isCompositeComponent,
- isCompositeComponentWithType,
- isCompositeComponentElement,
- Simulate,
- findDOMNode,
- findAllInRenderedTree,
- childrenToArray,
- renderWithOptions,
- unmountComponentAtNode,
- batchedUpdates,
-};
diff --git a/src/render.jsx b/src/render.jsx
index b0d25a64f..cd9bc0438 100644
--- a/src/render.jsx
+++ b/src/render.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import cheerio from 'cheerio';
-import { renderToStaticMarkup } from './react-compat';
+import { getAdapter } from './Utils';
/**
* Renders a react component into static HTML and provides a cheerio wrapper around it. This is
@@ -30,15 +30,17 @@ function createContextWrapperForNode(node, context, childContextTypes) {
}
export default function render(node, options = {}) {
+ const adapter = getAdapter(options);
+ const renderer = adapter.createRenderer({ mode: 'string', ...options });
if (options.context && (node.type.contextTypes || options.childContextTypes)) {
const childContextTypes = {
...(node.type.contextTypes || {}),
...options.childContextTypes,
};
const ContextWrapper = createContextWrapperForNode(node, options.context, childContextTypes);
- const html = renderToStaticMarkup( );
+ const html = renderer.render( );
return cheerio.load(html).root();
}
- const html = renderToStaticMarkup(node);
+ const html = renderer.render(node);
return cheerio.load(html).root();
}
diff --git a/src/validateAdapter.js b/src/validateAdapter.js
new file mode 100644
index 000000000..329a1b983
--- /dev/null
+++ b/src/validateAdapter.js
@@ -0,0 +1,22 @@
+import EnzymeAdapter from './adapters/EnzymeAdapter';
+
+export default function validateAdapter(adapter) {
+ if (!adapter) {
+ throw new Error(`
+ Enzyme Internal Error: Enzyme expects an adapter to be configured, but found none. To
+ configure an adapter, you should call \`Enzyme.configure({ adapter: new Adapter() })\`
+ before using any of Enzyme's top level APIs, where \`Adapter\` is the adapter
+ corresponding to the library currently being tested. For example:
+
+ import Adapter from 'enzyme-adapter-react-15';
+
+ To find out more about this, see http://airbnb.io/enzyme/docs/installation/index.html
+ `);
+ }
+ if (!(adapter instanceof EnzymeAdapter)) {
+ throw new Error(
+ 'Enzyme Internal Error: configured enzyme adapter did not inherit from the EnzymeAdapter ' +
+ 'base class',
+ );
+ }
+}
diff --git a/test/Adapter-spec.jsx b/test/Adapter-spec.jsx
new file mode 100644
index 000000000..a16e32daa
--- /dev/null
+++ b/test/Adapter-spec.jsx
@@ -0,0 +1,529 @@
+import './_helpers/setupAdapters';
+import React from 'react';
+import { expect } from 'chai';
+
+import { REACT013, REACT16 } from './_helpers/version';
+import configuration from '../src/configuration';
+import { itIf, describeWithDOM } from './_helpers';
+
+const { adapter } = configuration.get();
+
+const prettyFormat = o => JSON.stringify(o, null, 2);
+
+// Kind of hacky, but we nullify all the instances to test the tree structure
+// with jasmine's deep equality function, and test the instances separate. We
+// also delete children props because testing them is more annoying and not
+// really important to verify.
+function cleanNode(node) {
+ if (!node) {
+ return;
+ }
+ if (node && node.instance) {
+ node.instance = null;
+ }
+ if (node && node.props && node.props.children) {
+ // eslint-disable-next-line no-unused-vars
+ const { children, ...props } = node.props;
+ node.props = props;
+ }
+ if (Array.isArray(node.rendered)) {
+ node.rendered.forEach(cleanNode);
+ } else if (typeof node.rendered === 'object') {
+ cleanNode(node.rendered);
+ }
+}
+
+describe('Adapter', () => {
+ describeWithDOM('mounted render', () => {
+ it('treats mixed children correctlyf', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
+ hello{4}{'world'}
+ );
+ }
+ }
+
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ renderer.render( );
+
+ const node = renderer.getNode();
+
+ cleanNode(node);
+
+ expect(prettyFormat(node)).to.equal(prettyFormat({
+ nodeType: 'class',
+ type: Foo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'div',
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: [
+ 'hello',
+ REACT16 ? '4' : 4,
+ 'world',
+ ],
+ },
+ }));
+ });
+
+ it('treats null renders correctly', () => {
+ class Foo extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ renderer.render( );
+
+ const node = renderer.getNode();
+
+ cleanNode(node);
+
+ expect(prettyFormat(node)).to.equal(prettyFormat({
+ nodeType: 'class',
+ type: Foo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: null,
+ }));
+ });
+
+ itIf(!REACT013, 'renders simple components returning host components', () => {
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ const Qoo = () => Hello World! ;
+
+ renderer.render( );
+
+ const node = renderer.getNode();
+
+ cleanNode(node);
+
+ expect(prettyFormat(node)).to.equal(prettyFormat({
+ nodeType: 'function',
+ type: Qoo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'span',
+ props: { className: 'Qoo' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: ['Hello World!'],
+ },
+ }));
+ });
+
+ it('renders simple components returning host components', () => {
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ class Qoo extends React.Component {
+ render() {
+ return (
+ Hello World!
+ );
+ }
+ }
+
+ renderer.render( );
+
+ const node = renderer.getNode();
+
+ cleanNode(node);
+
+ expect(prettyFormat(node)).to.equal(prettyFormat({
+ nodeType: 'class',
+ type: Qoo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'span',
+ props: { className: 'Qoo' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: ['Hello World!'],
+ },
+ }));
+ });
+
+ it('handles null rendering components', () => {
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ class Foo extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ renderer.render( );
+
+ const node = renderer.getNode();
+
+ expect(node.instance).to.be.instanceof(Foo);
+
+ cleanNode(node);
+
+ expect(prettyFormat(node)).to.equal(prettyFormat({
+ nodeType: 'class',
+ type: Foo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: null,
+ }));
+ });
+
+
+ itIf(!REACT013, 'renders complicated trees of composites and hosts', () => {
+ // SFC returning host. no children props.
+ const Qoo = () => Hello World! ;
+
+ // SFC returning host. passes through children.
+ const Foo = ({ className, children }) => (
+
+ Literal
+ {children}
+
+ );
+
+ // class composite returning composite. passes through children.
+ class Bar extends React.Component {
+ render() {
+ const { special, children } = this.props;
+ return (
+
+ {children}
+
+ );
+ }
+ }
+
+ // class composite return composite. no children props.
+ class Bam extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ renderer.render( );
+
+ const tree = renderer.getNode();
+
+ // we test for the presence of instances before nulling them out
+ expect(tree.instance).to.be.instanceof(Bam);
+ expect(tree.rendered.instance).to.be.instanceof(Bar);
+
+ cleanNode(tree);
+
+ expect(prettyFormat(tree)).to.equal(
+ prettyFormat({
+ nodeType: 'class',
+ type: Bam,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'class',
+ type: Bar,
+ props: { special: true },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'function',
+ type: Foo,
+ props: { className: 'special' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'div',
+ props: { className: 'Foo special' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: [
+ {
+ nodeType: 'host',
+ type: 'span',
+ props: { className: 'Foo2' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: ['Literal'],
+ },
+ {
+ nodeType: 'function',
+ type: Qoo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'span',
+ props: { className: 'Qoo' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: ['Hello World!'],
+ },
+ },
+ ],
+ },
+ },
+ },
+ }),
+ );
+ });
+
+ it('renders complicated trees of composites and hosts', () => {
+ // class returning host. no children props.
+ class Qoo extends React.Component {
+ render() {
+ return (
+ Hello World!
+ );
+ }
+ }
+
+ class Foo extends React.Component {
+ render() {
+ const { className, children } = this.props;
+ return (
+
+ Literal
+ {children}
+
+ );
+ }
+ }
+
+ // class composite returning composite. passes through children.
+ class Bar extends React.Component {
+ render() {
+ const { special, children } = this.props;
+ return (
+
+ {children}
+
+ );
+ }
+ }
+
+ // class composite return composite. no children props.
+ class Bam extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ const options = { mode: 'mount' };
+ const renderer = adapter.createRenderer(options);
+
+ renderer.render( );
+
+ const tree = renderer.getNode();
+
+ // we test for the presence of instances before nulling them out
+ expect(tree.instance).to.be.instanceof(Bam);
+ expect(tree.rendered.instance).to.be.instanceof(Bar);
+
+ cleanNode(tree);
+
+ expect(prettyFormat(tree)).to.equal(
+ prettyFormat({
+ nodeType: 'class',
+ type: Bam,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'class',
+ type: Bar,
+ props: { special: true },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'class',
+ type: Foo,
+ props: { className: 'special' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'div',
+ props: { className: 'Foo special' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: [
+ {
+ nodeType: 'host',
+ type: 'span',
+ props: { className: 'Foo2' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: ['Literal'],
+ },
+ {
+ nodeType: 'class',
+ type: Qoo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'host',
+ type: 'span',
+ props: { className: 'Qoo' },
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: ['Hello World!'],
+ },
+ },
+ ],
+ },
+ },
+ },
+ }),
+ );
+ });
+ });
+
+ it('renders basic shallow as well', () => {
+ // eslint-disable-next-line react/require-render-return
+ class Bar extends React.Component {
+ constructor(props) {
+ super(props);
+ throw new Error('Bar constructor should not be called');
+ }
+ render() {
+ throw new Error('Bar render method should not be called');
+ }
+ }
+
+ // eslint-disable-next-line react/require-render-return
+ class Foo extends React.Component {
+ render() {
+ throw new Error('Foo render method should not be called');
+ }
+ }
+
+ // class composite return composite. no children props.
+ class Bam extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+ }
+
+ const options = { mode: 'shallow' };
+ const renderer = adapter.createRenderer(options);
+
+ renderer.render( );
+
+ const tree = renderer.getNode();
+
+ cleanNode(tree);
+
+ expect(prettyFormat(tree)).to.equal(
+ prettyFormat({
+ nodeType: 'class',
+ type: Bam,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: {
+ nodeType: 'class',
+ type: Bar,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: [
+ {
+ nodeType: 'class',
+ type: Foo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: null,
+ },
+ {
+ nodeType: 'class',
+ type: Foo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: null,
+ },
+ {
+ nodeType: 'class',
+ type: Foo,
+ props: {},
+ key: null,
+ ref: null,
+ instance: null,
+ rendered: null,
+ },
+ ],
+ },
+ }),
+ );
+ });
+
+});
diff --git a/test/ComplexSelector-spec.jsx b/test/ComplexSelector-spec.jsx
index b8a271d41..e0f6cb963 100644
--- a/test/ComplexSelector-spec.jsx
+++ b/test/ComplexSelector-spec.jsx
@@ -1,3 +1,4 @@
+import './_helpers/setupAdapters';
import React from 'react';
import { expect } from 'chai';
diff --git a/test/Debug-spec.jsx b/test/Debug-spec.jsx
index 7108e645f..6f63fc274 100644
--- a/test/Debug-spec.jsx
+++ b/test/Debug-spec.jsx
@@ -1,3 +1,4 @@
+import './_helpers/setupAdapters';
import { expect } from 'chai';
import React from 'react';
import {
@@ -12,7 +13,12 @@ import {
describeIf,
itIf,
} from './_helpers';
-import { REACT013 } from '../src/version';
+import { REACT013 } from './_helpers/version';
+import configuration from '../src/configuration';
+
+const { adapter } = configuration.get();
+
+const debugElement = element => debugNode(adapter.elementToNode(element));
describe('debug', () => {
describe('spaces(n)', () => {
@@ -37,11 +43,11 @@ describe('debug', () => {
describe('debugNode(node)', () => {
it('should render a node with no props or children as single single xml tag', () => {
- expect(debugNode(
)).to.equal('
');
+ expect(debugElement(
)).to.equal('
');
});
it('should render props inline inline', () => {
- expect(debugNode(
+ expect(debugElement(
,
)).to.equal(
'
',
@@ -49,7 +55,7 @@ describe('debug', () => {
});
it('should render children on newline and indented', () => {
- expect(debugNode(
+ expect(debugElement(
,
@@ -61,7 +67,7 @@ describe('debug', () => {
});
it('should render mixed children', () => {
- expect(debugNode(
+ expect(debugElement(
hello{'world'}
,
)).to.equal(
`
@@ -72,7 +78,7 @@ describe('debug', () => {
});
it('should render props on root and children', () => {
- expect(debugNode(
+ expect(debugElement(
,
@@ -84,7 +90,7 @@ describe('debug', () => {
});
it('should render text on new line and indented', () => {
- expect(debugNode(
+ expect(debugElement(
some text ,
)).to.equal(
`
@@ -99,7 +105,7 @@ describe('debug', () => {
}
Foo.displayName = 'Bar';
- expect(debugNode(
+ expect(debugElement(
,
@@ -116,7 +122,7 @@ describe('debug', () => {
render() { return
; }
}
- expect(debugNode(
+ expect(debugElement(
,
@@ -132,7 +138,7 @@ describe('debug', () => {
const Foo = () =>
;
- expect(debugNode(
+ expect(debugElement(
,
@@ -145,7 +151,7 @@ describe('debug', () => {
});
it('should render mapped children properly', () => {
- expect(debugNode(
+ expect(debugElement(
not in array
{['a', 'b', 'c']}
@@ -163,7 +169,7 @@ describe('debug', () => {
});
it('should render number children properly', () => {
- expect(debugNode(
+ expect(debugElement(
{-1}
{0}
@@ -179,7 +185,7 @@ describe('debug', () => {
});
it('renders html entities properly', () => {
- expect(debugNode(
+ expect(debugElement(
>
,
)).to.equal(
`
@@ -189,7 +195,7 @@ describe('debug', () => {
});
it('should not render falsy children ', () => {
- expect(debugNode(
+ expect(debugElement(
{false}
{null}
@@ -504,7 +510,7 @@ describe('debug', () => {
}
}
- expect(debugNodes(shallow(
).getNodes())).to.eql(
+ expect(debugNodes(shallow(
).getNodesInternal())).to.eql(
`
inside Foo
@@ -536,7 +542,7 @@ describe('debug', () => {
}
}
- expect(debugNodes(shallow( ).children().getNodes())).to.eql(
+ expect(debugNodes(shallow( ).children().getElements())).to.eql(
`
@@ -560,7 +566,7 @@ describe('debug', () => {
}
}
- expect(debugNodes(shallow( ).children().getNodes())).to.eql(
+ expect(debugNodes(shallow( ).children().getNodesInternal())).to.eql(
`
span1 text
diff --git a/test/ShallowTraversal-spec.jsx b/test/RSTTraversal-spec.jsx
similarity index 88%
rename from test/ShallowTraversal-spec.jsx
rename to test/RSTTraversal-spec.jsx
index a6398e283..a5656bbe1 100644
--- a/test/ShallowTraversal-spec.jsx
+++ b/test/RSTTraversal-spec.jsx
@@ -1,9 +1,11 @@
+import './_helpers/setupAdapters';
import React from 'react';
import sinon from 'sinon';
import { expect } from 'chai';
import {
splitSelector,
} from '../src/Utils';
+import elementToTree from '../src/adapters/elementToTree';
import {
hasClassName,
nodeHasProperty,
@@ -12,12 +14,13 @@ import {
pathToNode,
getTextFromNode,
buildPredicate,
-} from '../src/ShallowTraversal';
+} from '../src/RSTTraversal';
import { describeIf } from './_helpers';
-import { REACT013 } from '../src/version';
+import { REACT013 } from './_helpers/version';
-describe('ShallowTraversal', () => {
+const $ = elementToTree;
+describe('RSTTraversal', () => {
describe('splitSelector', () => {
const fn = splitSelector;
it('splits multiple class names', () => {
@@ -46,13 +49,13 @@ describe('ShallowTraversal', () => {
describe('hasClassName', () => {
it('should work for standalone classNames', () => {
- const node = (
);
+ const node = $(
);
expect(hasClassName(node, 'foo')).to.equal(true);
expect(hasClassName(node, 'bar')).to.equal(false);
});
it('should work for multiple classNames', () => {
- const node = (
);
+ const node = $(
);
expect(hasClassName(node, 'foo')).to.equal(true);
expect(hasClassName(node, 'bar')).to.equal(true);
expect(hasClassName(node, 'baz')).to.equal(true);
@@ -60,14 +63,14 @@ describe('ShallowTraversal', () => {
});
it('should also allow hyphens', () => {
- const node = (
);
+ const node = $(
);
expect(hasClassName(node, 'foo-bar')).to.equal(true);
});
it('should work if className has a function in toString property', () => {
function classes() {}
classes.toString = () => 'foo-bar';
- const node = (
);
+ const node = $(
);
expect(hasClassName(node, 'foo-bar')).to.equal(true);
});
});
@@ -76,32 +79,32 @@ describe('ShallowTraversal', () => {
it('should find properties', () => {
function noop() {}
- const node = (
);
+ const node = $(
);
expect(nodeHasProperty(node, 'onChange')).to.equal(true);
expect(nodeHasProperty(node, 'title', '"foo"')).to.equal(true);
});
it('should not match on html attributes', () => {
- const node = (
);
+ const node = $(
);
expect(nodeHasProperty(node, 'for', '"foo"')).to.equal(false);
});
it('should not find undefined properties', () => {
- const node = (
);
+ const node = $(
);
expect(nodeHasProperty(node, 'title')).to.equal(false);
});
it('should parse false as a literal', () => {
- const node = (
);
+ const node = $(
);
expect(nodeHasProperty(node, 'foo', 'false')).to.equal(true);
});
it('should parse false as a literal', () => {
- const node = (
);
+ const node = $(
);
expect(nodeHasProperty(node, 'foo', 'true')).to.equal(true);
});
@@ -154,7 +157,7 @@ describe('ShallowTraversal', () => {
});
it('should throw when un unquoted string is passed in', () => {
- const node = (
);
+ const node = $(
);
expect(() => nodeHasProperty(node, 'title', 'foo')).to.throw();
});
@@ -165,17 +168,17 @@ describe('ShallowTraversal', () => {
it('should be called once for a leaf node', () => {
const spy = sinon.spy();
- const node = (
);
+ const node = $(
);
treeForEach(node, spy);
expect(spy.calledOnce).to.equal(true);
});
it('should handle a single child', () => {
const spy = sinon.spy();
- const node = (
+ const node = $(
+ ,
);
treeForEach(node, spy);
expect(spy.callCount).to.equal(2);
@@ -183,11 +186,11 @@ describe('ShallowTraversal', () => {
it('should handle several children', () => {
const spy = sinon.spy();
- const node = (
+ const node = $(
+
,
);
treeForEach(node, spy);
expect(spy.callCount).to.equal(3);
@@ -195,13 +198,13 @@ describe('ShallowTraversal', () => {
it('should handle multiple hierarchies', () => {
const spy = sinon.spy();
- const node = (
+ const node = $(
+
,
);
treeForEach(node, spy);
expect(spy.callCount).to.equal(4);
@@ -209,10 +212,10 @@ describe('ShallowTraversal', () => {
it('should not get trapped from empty strings', () => {
const spy = sinon.spy();
- const node = (
+ const node = $(
+
,
);
treeForEach(node, spy);
expect(spy.callCount).to.equal(3);
@@ -220,13 +223,13 @@ describe('ShallowTraversal', () => {
it('should pass in the node', () => {
const spy = sinon.spy();
- const node = (
+ const node = $(
-
+
,
);
treeForEach(node, spy);
expect(spy.callCount).to.equal(4);
@@ -239,14 +242,14 @@ describe('ShallowTraversal', () => {
});
describe('treeFilter', () => {
- const tree = (
+ const tree = $(
-
+ ,
);
it('should return an empty array for falsy test', () => {
@@ -273,17 +276,18 @@ describe('ShallowTraversal', () => {
it('should return trees from the root node', () => {
const node = ;
- const tree = (
+ const tree = $(
{node}
-
+ ,
);
- const result = pathToNode(node, tree);
+ const nodeInTree = tree.rendered[1].rendered[0];
+ const result = pathToNode(nodeInTree, tree);
expect(result.length).to.equal(2);
expect(result[0].type).to.equal('div');
expect(result[1].type).to.equal('nav');
@@ -291,17 +295,18 @@ describe('ShallowTraversal', () => {
it('should return trees from the root node except the sibling node', () => {
const node =
bar
@@ -967,7 +999,7 @@ describeWithDOM('mount', () => {
expect(wrapper.props().d).to.equal('e');
});
- it('should throw if an exception occurs during render', () => {
+ itIf(!REACT16, 'should throw if an exception occurs during render', () => {
class Trainwreck extends React.Component {
render() {
const { user } = this.props;
@@ -1005,7 +1037,7 @@ describeWithDOM('mount', () => {
expect(setInvalidProps).to.throw(TypeError, similarException.message);
});
- it('should call the callback when setProps has completed', () => {
+ itIf(!REACT16, 'should call the callback when setProps has completed', () => {
class Foo extends React.Component {
render() {
return (
@@ -1018,7 +1050,7 @@ describeWithDOM('mount', () => {
const wrapper = mount(
);
expect(wrapper.find('.foo').length).to.equal(1);
- batchedUpdates(() => {
+ wrapper.renderer.batchedUpdates(() => {
wrapper.setProps({ id: 'bar', foo: 'bla' }, () => {
expect(wrapper.find('.bar').length).to.equal(1);
});
@@ -1058,7 +1090,7 @@ describeWithDOM('mount', () => {
expect(wrapper.props().d).to.equal('e');
});
- it('should throw if an exception occurs during render', () => {
+ itIf(!REACT16, 'should throw if an exception occurs during render', () => {
const Trainwreck = ({ user }) => (
{user.name.givenName}
@@ -1408,7 +1440,7 @@ describeWithDOM('mount', () => {
expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
});
- itIf(REACT15, 'should throw error when cb is not a function', () => {
+ itIf(is('>=15 || ^16.0.0-alpha'), 'should throw error when cb is not a function', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
@@ -1422,11 +1454,7 @@ describeWithDOM('mount', () => {
}
const wrapper = mount(
);
expect(wrapper.state()).to.eql({ id: 'foo' });
- expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(
- Error,
- 'setState(...): Expected the last optional `callback` argument ' +
- 'to be a function. Instead received: number.',
- );
+ expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
});
});
@@ -1492,7 +1520,7 @@ describeWithDOM('mount', () => {
expect(wrapper.isEmptyRender()).to.equal(false);
});
- describeIf(REACT15, 'stateless function components', () => {
+ describeIf(is('>=15 || ^16.0.0-alpha'), 'stateless function components', () => {
itWithData(emptyRenderValues, 'when a component returns: ', (data) => {
function Foo() {
return data.value;
@@ -1892,10 +1920,10 @@ describeWithDOM('mount', () => {
]}
/>,
);
- expect(wrapper.children().length).to.equal(3);
- expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
- expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
+ expect(wrapper.children().children().length).to.equal(3);
+ expect(wrapper.children().children().at(0).hasClass('foo')).to.equal(true);
+ expect(wrapper.children().children().at(1).hasClass('bar')).to.equal(true);
+ expect(wrapper.children().children().at(2).hasClass('baz')).to.equal(true);
});
it('should optionally allow a selector to filter by', () => {
@@ -1935,10 +1963,10 @@ describeWithDOM('mount', () => {
]}
/>,
);
- expect(wrapper.children().length).to.equal(3);
- expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
- expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
+ expect(wrapper.children().children().length).to.equal(3);
+ expect(wrapper.children().children().at(0).hasClass('foo')).to.equal(true);
+ expect(wrapper.children().children().at(1).hasClass('bar')).to.equal(true);
+ expect(wrapper.children().children().at(2).hasClass('baz')).to.equal(true);
});
});
});
@@ -2122,12 +2150,19 @@ describeWithDOM('mount', () => {
const Foo = () =>
;
const wrapper = mount(
);
- expect(wrapper.hasClass('foo')).to.equal(true);
- expect(wrapper.hasClass('bar')).to.equal(true);
- expect(wrapper.hasClass('baz')).to.equal(true);
- expect(wrapper.hasClass('some-long-string')).to.equal(true);
- expect(wrapper.hasClass('FoOo')).to.equal(true);
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('bar')).to.equal(false);
+ expect(wrapper.hasClass('baz')).to.equal(false);
+ expect(wrapper.hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.hasClass('FoOo')).to.equal(false);
expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+
+ expect(wrapper.children().hasClass('foo')).to.equal(true);
+ expect(wrapper.children().hasClass('bar')).to.equal(true);
+ expect(wrapper.children().hasClass('baz')).to.equal(true);
+ expect(wrapper.children().hasClass('some-long-string')).to.equal(true);
+ expect(wrapper.children().hasClass('FoOo')).to.equal(true);
+ expect(wrapper.children().hasClass('doesnt-exist')).to.equal(false);
});
});
@@ -2140,12 +2175,19 @@ describeWithDOM('mount', () => {
}
const wrapper = mount(
);
- expect(wrapper.hasClass('foo')).to.equal(true);
- expect(wrapper.hasClass('bar')).to.equal(true);
- expect(wrapper.hasClass('baz')).to.equal(true);
- expect(wrapper.hasClass('some-long-string')).to.equal(true);
- expect(wrapper.hasClass('FoOo')).to.equal(true);
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('bar')).to.equal(false);
+ expect(wrapper.hasClass('baz')).to.equal(false);
+ expect(wrapper.hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.hasClass('FoOo')).to.equal(false);
expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+
+ expect(wrapper.children().hasClass('foo')).to.equal(true);
+ expect(wrapper.children().hasClass('bar')).to.equal(true);
+ expect(wrapper.children().hasClass('baz')).to.equal(true);
+ expect(wrapper.children().hasClass('some-long-string')).to.equal(true);
+ expect(wrapper.children().hasClass('FoOo')).to.equal(true);
+ expect(wrapper.children().hasClass('doesnt-exist')).to.equal(false);
});
});
@@ -2163,12 +2205,21 @@ describeWithDOM('mount', () => {
}
const wrapper = mount(
);
- expect(wrapper.hasClass('foo')).to.equal(true);
- expect(wrapper.hasClass('bar')).to.equal(true);
- expect(wrapper.hasClass('baz')).to.equal(true);
- expect(wrapper.hasClass('some-long-string')).to.equal(true);
- expect(wrapper.hasClass('FoOo')).to.equal(true);
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('bar')).to.equal(false);
+ expect(wrapper.hasClass('baz')).to.equal(false);
+ expect(wrapper.hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.hasClass('FoOo')).to.equal(false);
expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+
+ // NOTE(lmr): the fact that this no longer works is a semantically
+ // meaningfull deviation in behavior. But this will be remedied with the ".root()" change
+ expect(wrapper.children().hasClass('foo')).to.equal(false);
+ expect(wrapper.children().hasClass('bar')).to.equal(false);
+ expect(wrapper.children().hasClass('baz')).to.equal(false);
+ expect(wrapper.children().hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.children().hasClass('FoOo')).to.equal(false);
+ expect(wrapper.children().hasClass('doesnt-exist')).to.equal(false);
});
});
@@ -2481,7 +2532,7 @@ describeWithDOM('mount', () => {
,
);
- const nodes = wrapper.find('.foo').flatMap(w => w.children().getNodes());
+ const nodes = wrapper.find('.foo').flatMap(w => w.children().getNodesInternal());
expect(nodes.length).to.equal(6);
expect(nodes.at(0).hasClass('bar')).to.equal(true);
@@ -2593,10 +2644,10 @@ describeWithDOM('mount', () => {
,
);
- expect(wrapper.find('.bar').get(0)).to.equal(wrapper.find('.foo').getNode());
- expect(wrapper.find('.bar').get(1)).to.equal(wrapper.find('.bax').getNode());
- expect(wrapper.find('.bar').get(2)).to.equal(wrapper.find('.bux').getNode());
- expect(wrapper.find('.bar').get(3)).to.equal(wrapper.find('.baz').getNode());
+ expect(wrapper.find('.bar').get(0)).to.deep.equal(wrapper.find('.foo').getElement());
+ expect(wrapper.find('.bar').get(1)).to.deep.equal(wrapper.find('.bax').getElement());
+ expect(wrapper.find('.bar').get(2)).to.deep.equal(wrapper.find('.bux').getElement());
+ expect(wrapper.find('.bar').get(3)).to.deep.equal(wrapper.find('.baz').getElement());
});
});
@@ -2615,8 +2666,16 @@ describeWithDOM('mount', () => {
}
}
const wrapper = mount(