Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fiber test renderer #8628

Merged
merged 33 commits into from
Jan 11, 2017
Merged

Fiber test renderer #8628

merged 33 commits into from
Jan 11, 2017

Conversation

iamdustan
Copy link
Contributor

@iamdustan iamdustan commented Dec 22, 2016

This is the start of reimplementing ReactTestRenderer on Fiber. Current tests status:

  • renders a simple component (2ms)
  • renders a top-level empty component
  • exposes a type flag (2ms)
  • can render a composite component (1ms)
  • renders some basics with an update
  • exposes the instance (1ms)
  • updates types (1ms)
  • updates children
  • does the full lifecycle (1ms)
  • gives a ref to native components (2ms)
  • warns correctly for refs on SFCs
  • allows an optional createNodeMock function
  • supports unmounting when using refs
  • supports unmounting inner instances (3ms)
  • supports updates when using refs
  • supports error boundaries
  • compatibility: add support for unstable_batchedUpdates

A good chunk of those tests have to do with refs and the ability to provide a createNodeMock function. I believe that ReactFiberReconciler will need to be updated with a getPublicInstance method to handle this.

Ignore all changes in ReactTestRenderer-test.js. The first is to import the fiber renderer instead of stack (actually, is there a better way to do this? should I copy the ReactTestRenderer-test for ReactTestFiberRenderer?), the rest are testing/skip/etc and will be backed out when done.

cc @spicyj

@aweary
Copy link
Contributor

aweary commented Dec 22, 2016

should I copy the ReactTestRenderer-test for ReactTestFiberRenderer?

👍 I think we'll want to keep the tests for the stack implementation of ReactTestRenderer for now


var instanceCounter = 0;

var ReactTestFiberComponent = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note: the separation between ReactDOMFiber and ReactDOMFiberComponent is just an artifact of how it was forked from ReactDOMComponent and that we still want to keep them in sync when possible. So feel free to abandon that convention (e.g. see ReactNoop.js)

@sophiebits
Copy link
Collaborator

should I copy the ReactTestRenderer-test for ReactTestFiberRenderer?

It would be fine to branch on renderer in the test file like the ReactART tests do.

@iamdustan
Copy link
Contributor Author

Thanks for the feedback thus far.

Regarding refs I think I can reuse the Renderer.findHostInstance that already exists, though I don’t think I understand the value it is returning. For example:

    class Component extends React.Component {
      render() {
        return (
          <div className="purple">
            <div />
            <Child />
          </div>
        );
      }
    }

    class Child extends React.Component {
      render() {
        return (
          <div className="green" />
        );
      }
    }
    
    ReactTestRenderer.create(<Component />);
    const hostInstance = TestRenderer.findHostInstance(root);
    /* hostInstance ==>
{ type: 'div',
      children:
       [ { '$$typeof': Symbol(react.element),
           type: 'div',
           key: null,
           ref: null,
           props: {},
           _owner: [Object],
           _store: {} },
         { '$$typeof': Symbol(react.element),
           type: [Function: Child],
           key: null,
           ref: null,
           props: {},
           _owner: [Object],
           _store: {} },
         { type: 'div', children: null, props: {} },
         { type: 'div', children: null, props: [Object] } ],
      props: { className: 'purple' } }
*/

Is that expected? Why?

@iamdustan
Copy link
Contributor Author

Second issue is if I try to call findHostInstance on the (currently passed) Fiber to a ref callback I always get it as in an unmounted state.

    class Component extends React.Component {
      render() {
        return (
          <div className="purple">
            <div />
            {/* this callback ref will throw */}
            <Child ref={fiber => console.log(ReactTestRenderer.findHostInstance(fiber)} />
          </div>
        );
      }
    }

    class Child extends React.Component {
      render() {
        return (
          <div className="green" />
        );
      }
    }
    ReactTestRenderer.create(<Component />);

// ref calback throws:
// Invariant Violation: Unable to find node on an unmounted component.

I would expect that to be mounted by the time the ref callback is called...

@iamdustan
Copy link
Contributor Author

Actually, ignore everything I’ve said. I think I see a path forward.

@iamdustan
Copy link
Contributor Author

Woohoo! Only 2 failing tests now!

I have ReactTestRenderer-test branching on the ReactDOMFeatureFlags useFiber switch now.

To support the createNodeMock function I needed to add a method to ReactFiberHostContext to support support hijacking the attachRef call in ReactFiberCommitWork. I’m a bit iffy about adding that since it seems intentional in the renderer API that that instances a renderer creates are exactly what the user gets in a ref.

With the two failing tests, I am curious if these are failing because the feature is incomplete or because behavior has changed subtly.

warns correctly for refs on SFCs

Is this warning still to be implemented? If yes I would suspect it to “just work” here without any additional effort.

supports updates when using refs

The ref callback is not being called yet on unmount. This is another situation where I would expect this to just work. Is this not yet implemented or do I need to track down a bug in this renderer?

supports error boundaries

The order of ilfecycles is different here. Based on watching the error boundary work between @acdlite and @gaearon I suspect that this may be expected.

Before

        'Boundary render',
        'Angry render',
        'Boundary render',
        'Boundary componentDidMount',

After

        'Boundary render',
        'Angry render',
        'Boundary componentDidMount',
        'Boundary render',

@iamdustan
Copy link
Contributor Author

Just checked master. It looks like warns correctly for refs on SFCs is not implemented yet. That answers one of my questions.

@gaearon
Copy link
Collaborator

gaearon commented Dec 24, 2016

@iamdustan Want to help with implementing that warning in master as a separate PR?

@@ -86,7 +86,11 @@ module.exports = function<T, P, I, TI, C, CX>(
function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) {
const ref = finishedWork.ref;
if (ref && (!current || current.ref !== ref)) {
ref(instance);
if (typeof config.getPublicInstance === 'function') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please destructure this from config earlier. We don't want to read that value every time. I think we could also make this required to keep contract strict.

parentHostContext : HostContext,
type : string,
) : HostContext {
return {};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please return the same object (emptyObject like in ReactNoop) so it doesn't push stack unnecessarily.

@gaearon
Copy link
Collaborator

gaearon commented Jan 9, 2017

Can you please update this now that the SFC warning is in?

@iamdustan iamdustan force-pushed the fiber-test-renderer branch 2 times, most recently from 595a489 to a63c10a Compare January 10, 2017 03:19
@iamdustan
Copy link
Contributor Author

The only failing test remaining is that it supports updates when using refs. I just copied that test to refs-test locally to see if the behavior already works in ReactDOMFiber with a slight change to account for null being passed on unmount (rather than the same node).

The test expects the ref to be called with ['div', 'div', 'span'] (the second div being on unmount), whilst the current behavior is to only be called with ['div', 'span'].

My biggest concern is that being called twice wasn’t necessarily an intentional decision before, but the test was added to assert the behavior and not regress accidentally. I’m not certain how much effort should be put into maintaining this because I think it would require changing ReactFiberCommitWorks detachRefIfNeeded to call the createNodeMock method in some capacity.

@spicyj what thoughts or advise do you have since you wrote the original implementation?

parentInstance.removeChild(child);
},

scheduleAnimationCallback: window.requestAnimationFrame,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this will throw in non-jsdom environment. Can you make it setTimeout? It seems like it wouldn't be used anyway, but at least we shouldn't throw.

rootContainerInstance : Container,
) : boolean {
// console.log('finalizeInitialChildren');
// setInitialProperties(testElement, type, props, rootContainerInstance);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please clean up the comments.

if (typeof child.toJSON === 'function') {
childrenJSON.push(child.toJSON());
} else if (typeof child.text !== 'undefined') {
childrenJSON.push();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are pushing undefined? Why?

}

toJSON() {
// eslint-disable ignore the children
Copy link
Collaborator

Choose a reason for hiding this comment

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

What are we ignoring specifically? Generally I'd like to avoid adding eslint ignores.

}

update(type, props) {
// console.log('update', type, props, this.children);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Comments

text : text,
id: instanceCounter++,
rootContainerInstance,
toJSON: () => isNaN(+inst.text) ? inst.text : +inst.text,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this is right. For example even "2" would become 2. Maybe this is unobservable implementation detail and we should just always use strings? Since that's what they get coerced to anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point. this was to make one of the tests pass, but I think to be correct I need to capture the type when creating the TextInstance and only coerce back to a number if it came in as a number.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think you'd actually get a number though in createTextInstance? As far as I can see it's already coerced by Fiber code by then. I think it actually makes more sense than exposing this to the renderer, and I'm fine with divergent behavior here (always treating text as strings).

@@ -317,6 +343,7 @@ describe('ReactTestRenderer', () => {
]);
});

// this is only passing in Fiber because refs aren't actually working
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this comment outdated? It’s not telling me much: are refs broken in Fiber? Are they broken specifically for test renderer? What is our plan to fix them? Is this within the scope of this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

outdated comment.

createNodeMock: Function;

constructor(rootID, createNodeMock) {
this.rootID = rootID;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see rootID being used anywhere. Is it needed?

}
var container = new TestContainer('<default>', createNodeMock);
var root = TestRenderer.createContainer(container);
if (root) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When is it null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don’t think ever, but flow was complaining that TestRenderer.createContainer may return null

Copy link
Collaborator

Choose a reason for hiding this comment

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

You'd need to see why other renderers didn't need this. It probably inferred that OpaqueNode type can be null, maybe from some other method definition.

},

/* eslint-disable camelcase */
unstable_batchedUpdates() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you just omit it? I don't think we want to expose it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah, forgot about this one. The stack-based ReactTestRenderer exposes this currently..

Copy link
Collaborator

Choose a reason for hiding this comment

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

Exposing but throwing isn't much better. :-)
Let's either make it work or remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😇

...adding this to the PR checklist.

}
ReactDOM.render(<Foo useDiv={true} />, container);
ReactDOM.render(<Foo useDiv={false} />, container);
expect(log).toEqual(['div', null, 'span']);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ideally also unmount at the end and check we get another null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh whoops, didn’t mean to actually commit this. This was testing the behavior difference for refs between the test-renderer and DOM renderer re #8628 (comment)

@gaearon
Copy link
Collaborator

gaearon commented Jan 10, 2017

I think the existing test for updates might be checking accidental implementation details.

It's not testing refs themselves, it's testing calls of createNodeMock.
It was added by @aweary in e43aaab, not by @spicyj.

In particular, I don't see why it calls createNodeMock for div twice. It might just be a bug/inefficiency in the Stack version. Why would you need to mock a node for something you're unmounting?

@aweary
Copy link
Contributor

aweary commented Jan 10, 2017

Right now createNodeMock is always called when getPublicInstance is called, and when unmounting ReactOwner.removeComponentAsRefFrom calls it to see if the cached ref is equal to the current public instance.

We can try caching the mock node on the instance and only calling createNodeMock when the instance is being mounted/updated.

@iamdustan
Copy link
Contributor Author

iamdustan commented Jan 10, 2017

thanks @gaearon and @aweary. I added the caching and changed the test case accordingly in 087f1f6. All tests now pass.

Only remaining task is the PI public instance type parameter...

@iamdustan iamdustan changed the title WIP: Fiber test renderer Fiber test renderer Jan 10, 2017
@@ -463,7 +463,7 @@ module.exports = function<T, P, I, TI, C, CX, CI>(
}
const ref = finishedWork.ref;
if (ref) {
const instance = getPublicInstance(finishedWork.stateNode);
const instance = (getPublicInstance(finishedWork.stateNode) : any);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the flow type for instance on master is any. Whilst adding the PI parameter does end up saying instance is PI, that fails flow due to ref expecting:

ref: null | (((handle : ?Object) => void) & { _stringRef: ?string }),

I suspect this could be improved, but for time constraints at the moment I simply dropped it back to equal the current inference.


constructor(element: ReactElement) {
this._currentElement = element;
this._renderedChildren = null;
this._topLevelWrapper = null;
this._hostContainerInfo = null;
this._nodeMock = UNSET;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we just assign it here instead? We already know the element.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh, would this need to be invalidated on receiveComponent?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, that's trickier. Probably not since nodes are supposed to be kept intact. I'm actually not sure the API is right now, maybe we should've just passed the type instead of the element. But what's done is done?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

welp, I’ve updated it to on mountComponent because that is the earliest point we can do so (that is when hostContainerInfo is passed in)

@gaearon
Copy link
Collaborator

gaearon commented Jan 10, 2017

Ugh, GH collapsed my comment. Here it is: #8628 (comment).

@gaearon
Copy link
Collaborator

gaearon commented Jan 10, 2017

This is currently breaking react-test-renderer built package:

screen shot 2017-01-10 at 18 30 33

(No, we don't have automated tests for them.)

This is because ReactDOMFeatureFlags is in dom folder so doesn't become part of the build.

@iamdustan
Copy link
Contributor Author

First two thoughts on how to fix this are:

  • add ReactDOMFeatureFlags to the src array of reactTestRenderer in the gulpfile
  • Create a react-test-renderer/fiber.js file for output and do some finagling to continue using ReactDOMFeatureFlags.useFiber for repo unit tests, but not break the build

@@ -28,7 +32,8 @@ type ReactTestRendererJSON = {|

type Container = {|
children: Array<Instance | TextInstance>,
createNodeMock: Function
createNodeMock: Function,
$$typeof: typeof CONTAINER_TYPE,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this could be just $$typeof: CONTAINER_TYPE?
Same below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

without the typeof I get the following:

src/renderers/testing/ReactTestRendererFiber.js:36
 36:   $$typeof: CONTAINER_TYPE,
                 ^^^^^^^^^^^^^^ string. Ineligible value used in/as type annotation (did you forget 'typeof'?)
 36:   $$typeof: CONTAINER_TYPE,
                 ^^^^^^^^^^^^^^ CONTAINER_TYPE

@gaearon
Copy link
Collaborator

gaearon commented Jan 11, 2017

This looks good to me. Thanks for sticking with it. 😄
I’ll merge later today.

) : void {
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this.children?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a bug. should be parentInstance.children.

@gaearon gaearon merged commit 2da35fc into facebook:master Jan 11, 2017
@gaearon
Copy link
Collaborator

gaearon commented Jan 11, 2017

Congratulations. 😄

@iamdustan iamdustan deleted the fiber-test-renderer branch January 11, 2017 19:29
@sebmarkbage
Copy link
Collaborator

@@ -462,7 +463,7 @@ module.exports = function<T, P, I, TI, C, CX>(
}
const ref = finishedWork.ref;
if (ref) {
const instance = finishedWork.stateNode;
const instance = getPublicInstance(finishedWork.stateNode);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not type safe for the case finishedWork.tag === ClassComponent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how so? what can I do to rectify that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

finishedWork.stateNode on a a class component will be the instance of the class. That doesn't match the generic types I | TI. I think it only accidentally works on runtime now because you assume they won't have a .tag field with the string INSTANCE so they fallback.

You can make separate branches by switching on finishedWork.tag and only use getPublicInstance on the branch on HostComponent.


useSyncScheduling: true,

getPublicInstance(inst) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm. It's strange that you're allowed to avoid the type annotation here. It probably leads to some overly optimistic analysis by Flow. We should add a type annotation here. It's important to do in the renderers since otherwise the generic arguments can be inferred to be very weak.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Related, how can I type the createNodeMock function so PI is properly inferred?

iamdustan added a commit to iamdustan/react that referenced this pull request Jan 11, 2017
iamdustan added a commit to iamdustan/react that referenced this pull request Jan 11, 2017
iamdustan added a commit to iamdustan/react that referenced this pull request Jan 17, 2017
iamdustan added a commit to iamdustan/react that referenced this pull request Jan 17, 2017
iamdustan added a commit to iamdustan/react that referenced this pull request Jan 21, 2017
gaearon pushed a commit that referenced this pull request May 30, 2017
* Fix ReactFiberReconciler annotation to include `PI`

#8628 (comment)

* Make getPublicInstance type safe

* attempting to trigger the issue @sebmarkbage described

#8751 (comment)

* Unify renderer-specific getPublicInstance with getRootInstance

* Switch on fiber type HostComponent for getPublicRootInstance

* Fix test that was too dynamic (and failing)

* Use PI in reconciler public API

* Prettier
NMinhNguyen referenced this pull request in enzymejs/react-shallow-renderer Jan 29, 2020
* ReactTestRenderer move current impl to stack dir

* ReactTestRenderer on fiber: commence!

* ReactTestRenderer: most non-ref/non-public-instance tests are passing

* Move ReactTestFiberComponent functions from Renderer to Component file

* test renderer: get rid of private root containers and root Maps

* TestRenderer: switch impl based on ReactDOMFeatureFlag.useFiber

* ReactTestRenderer: inline component creation

* ReactTestRenderer: return to pristine original glory (+ Fiber for error order difference)

* TestRendererFiber: use a simple class as TestComponentInstances

* Add `getPublicInstance` to support TestRenderer `createNodeMock`

* Rename files to end. Update for `mountContainer->createContainer` change

* test renderer return same object to prevent unnecessary context pushing/popping

* Fiber HostConfig add getPublicInstance. This should be the identity fn everywhere except the test renderer

* appease flow

* Initial cleanup from sleepy work

* unstable_batchedUpdates

* Stack test renderer: cache nodeMock to not call on unmount

* add public instance type parameter to the reconciler

* test renderer: set _nodeMock when mounted

* More cleanup

* Add test cases for root fragments and (maybe?) root text nodes

* Fix the npm package build

Explicitly require the Stack version by default.
Add a separate entry point for Fiber.

We don't add fiber.js to the package yet since it's considered internal until React 16.

* Relax the ref type from Object to mixed

This seems like the most straightforward way to support getPublicInstance for test renderer.

* Remove accidental newline

* test renderer: unify TestComponent and TestContainer, handle root updates

* Remove string/number serialization attempts since Fiber ensures all textInstances are strings

* Return full fragments in toJSON

* Test Renderer remove TestComponent instances for simple objects

* Update babylon for exact object type syntax

* Use $$typeof because clarity > punching ducks.

* Minor Flow annotation tweaks

* Tweak style, types, and naming

* Fix typo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants