-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Enzyme Adapter + React Standard Tree Proposal #742
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
# Enzyme Adapter & Compatibility Proposal | ||
|
||
|
||
## Motivation | ||
|
||
This proposal is attempting to address a hand full of pain points that Enzyme has been | ||
suspect to for quite a while. This proposal has resulted mostly [#715](https://github.com/airbnb/enzyme/issues/715), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'd thought of COM - Component Object Model - but this is probably better :-p There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do like subbing React for Component as it isn't tied to React but just the idea of a Component. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CST already means "Concrete Syntax Tree" though (a superset of AST) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically it's not components, it's elements - Element Something Tree, or Something Element Tree? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Expanded" Component Tree? 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gaearon yes but those aren't components anymore - a component is what you use to create that plain object (the react element). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right! I mean that those are neither components nor elements really. Colloquially we often say "component tree" though. "Element tree" sounds more technical to me, and misleading for this reason: element tree is exactly what is being returned from render. Anyway, I don't really care. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point too - I'd rather invent a new name than pick a name that isn't accurate, or could be misleading. lots of 🚲 🏠 on this one, for sure. "Concrete Render Tree"? i dunno There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RAT: Reactish Abstract Tree. It's a tree of abstraction over the elements produced by reactish environments |
||
|
||
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. | ||
|
||
```js | ||
// 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 | ||
|} | ||
|
||
// An RSTNode has this specific shape | ||
type RSTNode = {| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggest adding nodeType to the shape that can be used to differentiate between non-element nodes like text nodes and fragment nodes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lelandrichardson something like nodeType is not necessarily specific to the DOM. Inferno uses flags to represent something similar for better performance inline with monomorphism. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. It's possible i'm not quite understanding what you mean then. What would the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, thanks for weighing in! I hope i'm not dismissing a good idea too quickly! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that we plan to support more node types (e.g. Portals and Coroutines) in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I think i see what you are saying now @thysultan , your link to the MDN docs was throwing me off at first. I think this could be a reasonable way to define There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would all possible There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose all possible generic nodes could be defined or at least the possibility to add new nodes could be left open with a dedicated flag, and any library that implements such a nodeType can opt in to use them to define a node. i.e fragments, not all libs implement them but it's a generic enough node that could be used to define a possible node representation, the same could be said for Coroutines. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aweary I agree. I'm not sure how I feel about the node types that don't seem to apply to enzyme... but I need to understand them more. If they become a part of react and enzyme needs to know about them in order to provide useful testing APIs, we might still need them. |
||
// 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; | ||
|
||
// Whether or not this node is a "Host" node. | ||
host: boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For supporting coroutines and portals, you might want to add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is good feedback, thanks. What do you think about also having a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Fiber, we only have string literals at the renderer level, numbers get converted to strings before that. I don't see a benefit in wrapping all literals into nodes. Let's just always use strings. Technically we do create fibers for literals when there are several of them but it's an implementation detail and depends on the renderer. For example, DOM renderer inlines a single text child, but test renderer may choose not to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, regarding the above comment: I meant you probably want to replace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gaearon understood. thanks. I'll make some updates to use |
||
|
||
// The props object passed to the node, which will include `children` in its raw form, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about the context being passed to the node? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hesitate to provide any sort of standardization around context, a feature that is very likely to change, and seems more of a react-implementation-specific thing. I think enzyme will continue to provide the ability to utilize context through the renderer options (which will be shallow/mount options passed through to the renderer), but we shouldn't make it part of this proposal. Enzyme will continue using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is it likely to change? Do Fiber, Inferno, or Preact not implement it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, since v13, 14, and 15 all use context, I don't think we can just pretend it doesn't exist. Even if v16 removed it (which I doubt) we'd still need this spec to support it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its often mentioned that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For what it's worth, context support was added to React fiber. I don't think they're going away anytime soon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I write a component that relies on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as this spec is currently written, both I think we shouldn't add things to this spec that aren't needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I wasn't under the impression that this spec was just for traversal - I was thinking it would be a way to render the entire tree with 100% fidelity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we want to iron down things about context, that we can do so by specifying it in the I could see more APIs being put on I think the tree structure (ie, |
||
// 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; | ||
|} | ||
``` | ||
|
||
Thoughts: | ||
|
||
- If an `RSTNode` has `host: true`, `rendered` will always be an `Array` or `null`. (though this | ||
might change with Fiber I guess). | ||
- `rendered` is a better property name than `children`, as the meaning of `children` is ambiguous | ||
here since it is defined on props/elements etc. | ||
- If a node has `host: true`, loosely speaking that means that an instance of that component | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this thought got cut off? |
||
- For "host" nodes, can `instance` ever be non-null? Could it be the DOM element? Yes/No? | ||
|
||
|
||
### 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` | ||
|
||
|
||
```js | ||
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: | ||
|
||
```js | ||
import Enzyme from 'enzyme'; | ||
import ThirdPartyEnzymeAdapter from 'third-party-enzyme-adapter'; | ||
|
||
Enzyme.configure({ adapter: ThirdPartyEnzymeAdapter }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we did a "magical" approach similar to babel, where we look for a node module with a matching standardized prefix and autoloaded that. So say the standard was With that we'd have to have a way to handle multiple enzyme-adapters being found in node_modules. Maybe also have this manual api but first try and do it automatically? Just thinking about lessening burden and confusion to users. I find most users don't understand how to set up a testing engine and just want to write their tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the explicit API is best, I'm not a huge fan of "magical" auto-configuration approaches like that. What happens if they update from React 14 to 15 and forget to remove Maybe that could be a long-term goal once we're more familiar with actual use patterns. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm more partial to the explicit API as well. It also makes it really clear how to use different adapters in the same test suite. Though I am kind of bummed we will be joining the ranks of tools that don't "just work" with one npm install :/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also prefer the explicit API. |
||
|
||
``` | ||
|
||
Additionally, each wrapper Enzyme exposes will allow for an overriding `adapter` option that will use a | ||
given adapter for just that wrapper: | ||
|
||
```jsx | ||
import { shallow } from 'enzyme'; | ||
import ThirdPartyEnzymeAdapter from 'third-party-enzyme-adapter'; | ||
|
||
shallow(<Foo />, { adapter: ThirdPartyEnzymeAdapter }); | ||
``` | ||
|
||
By default, Enzyme will ship with adapters for all major versions of React since React 0.13: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if React 16 comes out before this is done, then we'd probably support 0.14 - 16 instead? Perhaps more accurate to state "the latest, and previous two, major versions of React"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actually maybe rethinking this. Perhaps enzyme shouldn't ship with any adapter? Those are all added via a second dependency and import? As for supporting react 0.13. Structuring the project this way should make supporting react 0.13 much much easier. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also think adapters should not be shipped automatically. For users of inferno/preact it becomes bloat. Even for users of React on a specific version, it becomes bloat getting adapters for other versions. I'm thinking we have a specific There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I'm fine not shipping any adapter directly, but we'd have to be very very clear with instructions and error messages so people know how to add one. |
||
|
||
```js | ||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/hand full/handful