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

Rewrite DOMPropertyOperations-test in terms of public API #10281

Merged
merged 3 commits into from
Aug 7, 2017

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Jul 26, 2017

This removes a dependency on Stack-only ReactDOMComponentTree.precacheNode so that we can remove it when Stack is gone. For the most part the tests were easy to convert but I’ll point out a few things I wasn’t sure about.

expect(stubNode.hasAttribute('data-foo')).toBe(false);
});

it('should use mutation method where applicable', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed this test. It seems like the only mutation method we use is value, and AFAIK there are plenty tests about it specifically. Internally at FB we don’t inject custom properties with mutation methods either.

Copy link
Contributor

Choose a reason for hiding this comment

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

👍. Long term, I believe we can remove mutation method all together if we circle back to #10150

expect(stubNode.hasAttribute('hidden')).toBe(false);
});

it('should remove property properly even with different name', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed this test. I couldn’t find any cases where property name would differ in practice—neither in master nor internally at FB. If we add such case we should add an integration test for that specific property instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this doesn't happen for any prop that must be assigned as a DOM property. The only thing we alias are a few HTML attribute names and a bunch of SVG attribute names.

expect(stubNode.value).toBe('');
});

it('should not leave all options selected when deleting multiple', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I couldn’t figure out how to write this test in terms of public API. I tried rendering <select multiple> with both value and defaultValue, and then rendering <select> without multiple but couldn’t get this test to pass. If I need to bring it back, can somebody please explain how? Or maybe this is better suited for DOM fixtures.

Copy link
Collaborator Author

@gaearon gaearon Jul 26, 2017

Choose a reason for hiding this comment

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

cc @syranide who wrote this test originally in #1510

Copy link
Contributor

Choose a reason for hiding this comment

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

Not quite sure what you're asking. The key thing is that multiple individual options will still have selected set to true in the DOM when removing multiple as an attribute in some browsers. So you need to render with multiple and multiple options selected, then remove multiple and verify that only one option is set in the DOM. The logical way would probably be using an uncontrolled select (because that also avoids controlled behavior potentially fixing it).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm. I tried rendering uncontrolled select and then rendering it without multiple, and these assertions failed. I'll try again.

Copy link
Contributor

@syranide syranide Jul 26, 2017

Choose a reason for hiding this comment

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

@gaearon Doesn't that mean the current implementation is broken then? (EDIT: if we care about fixing it)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately I don't know 🤷‍♀️ We have the old unit test but it's not clear to me whether it is testing the right thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

The key thing is that multiple individual options will still have selected set to true in the DOM when removing multiple as an attribute in some browsers.

If this behavior is inconsistent, and JSDOM does not exhibit it, do we need to make this a DOM Test Fixture and explore the surface area of browsers that demonstrate it?

DOMPropertyOperations.deleteValueForProperty(stubNode, 'value');
// JSDOM does not behave correctly for attributes/properties
//expect(stubNode.getAttribute('value')).toBe('foo');
expect(stubNode.value).toBe('');
Copy link
Collaborator Author

@gaearon gaearon Jul 26, 2017

Choose a reason for hiding this comment

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

The diff got a bit messed up. The new test is:

     var container = document.createElement('div');
      spyOn(console, 'error');
      ReactDOM.render(
        <input type="text" value="foo" onChange={function() {}} />,
        container,
      );
      ReactDOM.render(
        <input type="text" onChange={function() {}} />,
        container,
      );
      expect(container.firstChild.getAttribute('value')).toBe('foo');
      expect(console.error.calls.count()).toBe(1);
      expect(console.error.calls.argsFor(0)[0]).toContain(
        'A component is changing a controlled input of type text to be uncontrolled',
        );

But I’m not sure this is right. As you can see, the last line in the old test:

      // JSDOM does not behave correctly for attributes/properties
      //expect(stubNode.getAttribute('value')).toBe('foo');
      expect(stubNode.value).toBe('');

was actually not true anymore. However uncommented line works:

expect(container.firstChild.getAttribute('value')).toBe('foo');

I’m not sure what’s going on here, and whether my test reproduces this case, or not.
What is this really testing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Answering to myself: this test was written in #6228, seemingly to fix #6219. Perhaps I can use fiddle from that issue to recreate the original problem.

Copy link
Contributor

Choose a reason for hiding this comment

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

The new test seems wrong to me, the second render should be <input ... value="" /> to be truthful to the original test-case (it's about preserving the initial attribute value). But AFAIK this changed way back when you made the attribute always reflect the property for input value? So this should fail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why did the test pass then, if it’s wrong? Is it because we were testing internals rather than public API?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In other words, I think my test is not faithful to the original test, but is faithful to the actual behavior in 15.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this test might be a victim of changes in how value is assigned over the churn trying to fix bugs in controlled inputs. Your change is a good one, but I wonder it is covered by the round of tests starting at:

https://github.com/facebook/react/blob/master/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js#L873

@gaearon
Copy link
Collaborator Author

gaearon commented Jul 26, 2017

@syranide I'd appreciate if you could help with the two cases above. I’m trying to figure out how to write correct tests for them that only use ReactDOM.render but haven’t managed so far.

@syranide
Copy link
Contributor

syranide commented Aug 2, 2017

Sorry. As far as I understand and remember it all; in some browsers <select multiple> when removing multiple will keep all options still selected despite not allowing multiple. So the question is what we care about. Do we care incorrect options being selected in the DOM for uncontrolled selects? Do we care about incorrect options in DOM being selected for controlled selects? Can this break uncontrolled selects if not normalized (reporting wrong value to onChange)? Can this break controlled selects (showing wrong value, reporting wrong value to onChange)? So one thing is that we need to settle on expected behavior, from last time I was involved in this and related stuff I'm strongly in-favor of treating <select> and <select multiple> as effectively separate elements, so that switching between them means internally recreating them (but I'm not sure if this was cemented or not). In that case then this entire problem goes away and all you need to test for is that selects are recreated when toggling multiple, same goes for toggling controlled (and same for changing input type, but that is separate).

As for the input-test I'm not sure what to do, I know what it wants to test. But AFAIK it should not hold anymore (because you intentionally changed that behavior), BUT IIRC you guys decided to backtrack on the logic for syncing properties and attributes in some way. So yeah, I'm not 100% what to do about it.

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

The thing I'm struggling with about the <select> test is I can’t seem to get wrong behavior even in old version. For example, even React 0.10 seems to “deselect” multiple options: https://jsfiddle.net/cs3twpz2/.

If it works just due to the browser (as opposed to React being smart) then I don’t understand what is the value in keeping this test (since it effectively tests jsdom).

@nhunzaker
Copy link
Contributor

nhunzaker commented Aug 5, 2017

@gaearon I noticed a difference in select behavior as well. It might be related. I have a PR out for it in #10142

Sorry. I might have misunderstood.

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

Note I didn’t see a difference—but I probably was testing the wrong thing. All I’m trying to figure out in this thread is whether the test was worth keeping. For now I settled on removing it because I don’t understand how exactly to replicate it with public API. And I’d rather have no test than test that codifies wrong or useless behavior since it could mislead people in the future.

I don't understand what the jsdom comments are about since they pass.
@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

@syranide @aweary @nhunzaker @jquense

Can you please help me out with this? I’d really like to get this test rewrite in. We need to start getting rid of those internal tests that only verify private APIs and don’t test the whole picture.

I tried to faithfully reproduce the current behavior. I had to change the thing <input> test was verifying (since behavior seems to have changed). I removed the <select> test because it wasn’t clear to me what it was testing.

Can we get it in like this? Or please help me understand how to verify current behavior for these tests.

expect(stubNode.title).toBe('Tip!');
var container = document.createElement('div');
ReactDOM.render(<div title="Tip!" />, container);
expect(container.firstChild.title).toBe('Tip!');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the advantage to using ReactDOM.render over TestUtils.renderIntoDocument?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

None. Just seems a bit more flexible.

var container = document.createElement('div');
ReactDOM.render(<div role="#" />, container);
expect(container.firstChild.getAttribute('role')).toBe('#');
expect(container.firstChild.role).toBeUndefined();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Just thinking: I didn't see a test that covers attributes that require property assignment. I think it would be covered by a test that uses multiple with a select, but the behavior is probably covered by ReactDOMSelect-test.

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

To be clear my intention is not to find best solutions for these cases.

It is to ensure the existing behavior is still codified in tests when rewritten via public API. So that we don't accidentally regress.

ReactDOM.render(<div disabled={true} />, container);
expect(container.firstChild.getAttribute('disabled')).toBe('');
ReactDOM.render(<div disabled={false} />, container);
expect(container.firstChild.getAttribute('disabled')).toBe(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this test cover null too? Is that overkill?

var container = document.createElement('div');
ReactDOM.render(<div title="foo" />, container);
expect(container.firstChild.getAttribute('title')).toBe('foo');
expect(container.firstChild.title).toBe('foo');
Copy link
Contributor

Choose a reason for hiding this comment

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

This is existing test behavior, so take it or leave it, but I think line 143 is essentially testing that JSDOM syncs up the title property with the title attribute. Could line 143 go away?

Copy link
Contributor

Choose a reason for hiding this comment

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

Same deal with line 146.

expect(container.firstChild.value).toBe('foo');
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'A component is changing a controlled input of type text to be uncontrolled',
);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

If this test is giving us trouble via unit testing, I think we should drop it and consider writing a DOM fixture.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, please lump this into the chat above about selects.

Copy link
Contributor

@nhunzaker nhunzaker left a comment

Choose a reason for hiding this comment

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

I think this is accurate. I'm curious if the input value is covered by ReactDOMInput-test, and if the select test should be a fixture, but otherwise 👍

expect(container.firstChild.value).toBe('foo');
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'A component is changing a controlled input of type text to be uncontrolled',
);
Copy link
Contributor

Choose a reason for hiding this comment

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

is this providing any additional coverage over the uncontrolled/controlled switch tests in `ReactDOMInput?

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

To be clear I deleted the select test. Do you think I should bring it back in some form?

@nhunzaker
Copy link
Contributor

No. I think deleting it is the right call.

@syranide
Copy link
Contributor

syranide commented Aug 5, 2017

The thing I'm struggling with about the <select> test is I can’t seem to get wrong behavior even in old version. For example, even React 0.10 seems to “deselect” multiple options: https://jsfiddle.net/cs3twpz2/.

It's not a React-bug, it's a browser bug. IIRC only older IE, possibly IE9-IE10.

@syranide
Copy link
Contributor

syranide commented Aug 5, 2017

EDIT: But I think testing that the defaultValue is correctly set when toggling multiple is a good replacement.

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

But is it possible to write a test using jsdom and public API to verify React is able to work around this browser bug, even though jsdom does not exhibit it? For example maybe we can set some mocks on some DOM properties to verify React somehow forces them to be right?

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 5, 2017

Thanks. Can you elaborate on what it should be set to?

@syranide
Copy link
Contributor

syranide commented Aug 5, 2017

But is it possible to write a test using jsdom and public API to verify React is able to work around this browser bug, even though jsdom does not exhibit it? For example maybe we can set some mocks on some DOM properties to verify React somehow forces them to be right?

If we can decide on "toggling multiple is equal to recreating the component", then the bug goes away and you also get a consistent logical behavior IMHO. This can be tested.

Thanks. Can you elaborate on what it should be set to?

As above, as the component should be recreated when toggling multipleand the selected options (in the DOM) afterwards should always be equal to defaultValue (should not be necessary to test controlled components) regardless of what was selected before the change. Bug goes away, consistent behavior across all browsers too (browsers are not consistent in which option is selected when going from multiple to single).

I'm not sure if ReactDOM currently has this behavior, but IMHO it makes sense and how it should be. But I think it does...

Makes sense?

@gaearon gaearon merged commit 4e4653d into facebook:master Aug 7, 2017
@gaearon
Copy link
Collaborator Author

gaearon commented Aug 7, 2017

I’m going to land this as is for now. I’ll file a task to follow up with the <select multiple> test.

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.

4 participants