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

Use only public API for ChangeEventPlugin-test.js #11333

Merged
merged 13 commits into from
Nov 24, 2017

Conversation

Ethan-Arrowood
Copy link
Contributor

Ref #11299

Do not merge this! I need help understanding the ChangeEventPlugin._isInputEventSupported method on line 172.

@jquense
Copy link
Contributor

jquense commented Oct 24, 2017

It exists to toggle the plugin into the polyfill mode in order to test certain paths, since the jsdom environment always supports the input event. The "public api" way to address this would be to defeat the feature test in the plugin by stunning out document.mode or something

var setUntrackedValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if this will work since we stub this in the input tracking code...or is that only the instance property that's stubbed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only prototype method I could find that would grant me access to this kind of info. If the value is untracked it won't show up in the Object.getOwnPropertyDescriptor() call. (Hence why I am using the .set method).

// Do not understand this method. Please advise
// if (!ChangeEventPlugin._isInputEventSupported) {
// return;
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

Essentially if the input event isn't supported this test won't test anything

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 do you check if an input is valid?

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 as mutated a bit over time. This guard should be fine to just remove at this point. onInput will always be supported in the unit test environment so this wouldn't ever get hit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay then I'll clean up my code and push. Should be good to go at this point.

@gaearon
Copy link
Collaborator

gaearon commented Oct 26, 2017

Is this ready for review, or is there more work planned?

@Ethan-Arrowood
Copy link
Contributor Author

@gaearon I think its ready to be reviewed

@gaearon
Copy link
Collaborator

gaearon commented Oct 27, 2017

Can we also try to get rid of ReactTestUtils.SimulateNative calls? Like in #11309.

@Ethan-Arrowood
Copy link
Contributor Author

@gaearon quick git question. I noticed that you merged master into this PR. How do I make sure my local branch is up to date now? Should I use git pull --rebase upstream master from my open-test-to-api local branch?

@gaearon
Copy link
Collaborator

gaearon commented Oct 31, 2017

I think you can just git pull on it. I pushed to your branch, so you can pull from your branch.

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

Let's avoid SimulateNative too.

@Ethan-Arrowood
Copy link
Contributor Author

Could use some help. I can't for the life of me get the onChange callback function to run:
screen shot 2017-10-31 at 2 42 06 pm Any tips? @gaearon

@jquense
Copy link
Contributor

jquense commented Oct 31, 2017

@Ethan-Arrowood checkboxes don't listen to change or input events oddly enough. They actually respond to 'click' events. check out the original code a bit closer, you'll see it (it's easy to miss)

@Ethan-Arrowood
Copy link
Contributor Author

@jquense okay changing 'change' to 'click' works. The checked value is now changing as expected; however, no onChange or onClick is being executed. Any idea?

@gaearon
Copy link
Collaborator

gaearon commented Nov 3, 2017

ping @jquense :-)

@jquense
Copy link
Contributor

jquense commented Nov 6, 2017

Sorry, just pulled the branch and the tests all seem to pass for checkboxes. Is there some specific spot they aren't working or something i missed? Otherwise this seems gtg

@Ethan-Arrowood
Copy link
Contributor Author

@jquense my orig edits work but @gaearon wants me to remove ReactTestUtils.SimulateNative

@jquense
Copy link
Contributor

jquense commented Nov 7, 2017

Ah yes I am hunk I know why. So all "artificial" events are going to be inadvertently silenced because to he dedupe logic. SimulateNative bypasses by tagging the event logic with a "simulated" flag. Check out the test utils code to see what I mean.

The east way to deal with it is to mimic SimulateNative by dispatching an event with the flag. Maybe let's see if that fixes the issue? Folks may want something a little less dependent on the implementation details but maybe let's confirm that's the issue

@gaearon
Copy link
Collaborator

gaearon commented Nov 8, 2017

The east way to deal with it is to mimic SimulateNative by dispatching an event with the flag.

This doesn't actually test the same code path as the browser goes through, does it? In that case I'd prefer we don't use the flag, and do something that resembles the browser code path.

@gaearon
Copy link
Collaborator

gaearon commented Nov 8, 2017

TODO: before merging, verify cases in #11337 are also covered.

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

Let's try to avoid the simulated flag. Any ideas?

@jquense
Copy link
Contributor

jquense commented Nov 8, 2017

I'm taking a closer look...I'm not sure the simulated thing is actually the cause here.

@jquense
Copy link
Contributor

jquense commented Nov 8, 2017

so from what I can tell using:

function dispatchEventOnNode(node, type) {
  node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
}

to dispatch events just doesn't fire anything in the React event stack. It's not the ChangeEvent, ReactDOMEventListener.dispatch event isn't seeing anything either

@Ethan-Arrowood
Copy link
Contributor Author

Sorry for not updating @jquense and @gaearon I've had a busy week full of internship interviews. I'll have some time to revisit this on Saturday.

@Ethan-Arrowood
Copy link
Contributor Author

I've figured out how to get click callback to work. Seems to be a potential bug with the change event. I found a unanswered Stackoverflow question that actually links to one of your comments @jquense https://stackoverflow.com/questions/47169131/how-to-use-js-or-jquery-to-trigger-simulated-change-and-input-events-for-react-1

@Ethan-Arrowood
Copy link
Contributor Author

Check out this CodeSandbox I made demonstrating this potential bug:
Edit ReactChangeEventExample

Open the console and watch as you enter characters into the input. A change event is dispatched as expected. But when we call dispatchChange no change is logged to the screen. Does this have to do with the fact that the input value is actually not changing?

@Ethan-Arrowood
Copy link
Contributor Author

Experimental but could be useful in the future once completed: https://w3c.github.io/input-events/index.html#interface-InputEvent

@Ethan-Arrowood
Copy link
Contributor Author

I can give it another shot, but I'm a little confused about the behavior of the onChange handler

@gaearon
Copy link
Collaborator

gaearon commented Nov 24, 2017

I wouldn't expect triggering change event to work.

If you open ChangeEventPlugin and read its source, it says: should use change? not for inputs (unless they're file inputs). The purpose of ChangeEventPlugin is to mostly turn other events into React onChange events. That's what @jquense also said in #11333 (comment).

Also, if I search for "checkbox" in that file, I find an explanation for why this is the case and how to trigger the event:

function shouldUseClickEvent(elem) {
// Use the `click` event to detect changes to checkbox and radio inputs.
// This approach works across all browsers, whereas `change` does not fire
// until `blur` in IE8.

So trying to get dispatching change to work was the original problem. It wouldn't.

@gaearon
Copy link
Collaborator

gaearon commented Nov 24, 2017

Interestingly it seems that dispatching click in jsdom actually toggles the checked value.


expect(called).toBe(1);
var node = ReactDOM.findDOMNode(stub);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Tip: there is no "stub" :-) Old tests are bad, don't copy them.
ReactDOM.render() already returns a DOM node.

);
var input = ReactDOM.findDOMNode(ref);
setUntrackedValue.call(input, 'bar');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are we setting untracked value here? I thought this test verifies that it catches setting value programmatically (and thus through the tracked value property like in the original test).

@@ -128,9 +145,9 @@ describe('ChangeEventPlugin', () => {
var input = ReactTestUtils.renderIntoDocument(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This won't work because it's not actually rendering into document (yes, the naming of that method is pretty bad). That's why reviews in other similar tests added setting up a container and adding it to the body before every test.

@gaearon
Copy link
Collaborator

gaearon commented Nov 24, 2017

Here's how I tried to write some of these tests based on your code:

describe('ChangeEventPlugin', () => {
  var container;

  beforeEach(() => {
    React = require('react');
    ReactDOM = require('react-dom');

    // The container has to be attached for events to fire.
    container = document.createElement('div');
    document.body.appendChild(container);
  });

  afterEach(() => {
    document.body.removeChild(container);
    container = null;
  });

  fit('should fire change for checkbox input', () => {
    var called = 0;

    function cb(e) {
      called++;
      expect(e.type).toBe('change');
    }

    var node = ReactDOM.render(
      <input type="checkbox" onChange={cb} />,
      container,
    );

    // We want `setUntrackedChecked.call(node, true)` but jsdom incorrectly
    // changes `checked` on `click` event dispatch so we set it the opposite value.
    setUntrackedChecked.call(node, false);
    node.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
    expect(called).toBe(1);
    expect(node.checked).toBe(true);

    // We want `setUntrackedChecked.call(node, false)` but jsdom incorrectly
    // changes `checked` on `click` event dispatch so we set it the opposite value.
    setUntrackedChecked.call(node, true);
    node.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
    expect(called).toBe(2);
    expect(node.checked).toBe(false);
  });

  fit('should not fire change setting the value programmatically', () => {
    var called = 0;

    function cb(e) {
      called++;
      expect(e.type).toBe('change');
    }

    var input = ReactDOM.render(
      <input type="text" defaultValue="foo" onChange={cb} />,
      container,
    );

    // Set to a value programmatically...
    input.value = 'bar';
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    expect(called).toBe(0);

    // ...but then change it back as if the user did it.
    setUntrackedValue.call(input, 'foo');
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));

    // Deduplication logic should understand it's not a 'foo' -> 'foo' noop, but
    // 'bar' -> 'foo' change, and therefore the onChange event should be fired.
    expect(called).toBe(1);
    expect(input.value).toBe('foo');

    // For good measure, check that firing an input without an actual change
    // is deduplicated.
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    expect(called).toBe(1);
  });

  xit('should not fire change setting checked programmatically', () => {
    var called = 0;

    function cb(e) {
      called++;
      expect(e.type).toBe('change');
    }

    var input = ReactDOM.render(
      <input type="checkbox" onChange={cb} defaultChecked={true} />,
      container,
    );

    // Set to a value programmatically...
    input.checked = false;
    input.dispatchEvent(new Event('click'));
    expect(called).toBe(0);

    // ...but then change it back as if the user did it.
    setUntrackedValue.call(input, 'foo');
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));

    // Deduplication logic should understand it's not a 'foo' -> 'foo' noop, but
    // 'bar' -> 'foo' change, and therefore the onChange event should be fired.
    expect(called).toBe(1);
    expect(input.value).toBe('foo');

    // For good measure, check that firing an input without an actual change
    // is deduplicated.
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    expect(called).toBe(1);
  });

  fit('should not fire change when setting checked programmatically', () => {
    var called = 0;

    function cb(e) {
      called++;
      expect(e.type).toBe('change');
    }

    var input = ReactDOM.render(
      <input type="checkbox" onChange={cb} defaultChecked={true} />,
      container
    );

    // Set to a value programmatically...
    input.checked = true;
    // We want the real checked value to stay `true` but jsdom incorrectly
    // changes `checked` on `click` event dispatch so we set it to the opposite.
    setUntrackedChecked.call(input, false);
    input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
    expect(called).toBe(0);

    // ...but then change it back as if the user did it.
    // We want `setUntrackedChecked.call(input, false)` but jsdom incorrectly
    // changes `checked` on `click` event dispatch so we set it to the opposite.
    setUntrackedChecked.call(input, true);
    input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));

    expect(called).toBe(1);
  });

@Ethan-Arrowood
Copy link
Contributor Author

Thanks for the help @gaearon! Let me see what I can come up with now.

@Ethan-Arrowood
Copy link
Contributor Author

@gaearon alright I've update everything. I tried my best to understand everything and I hope my tests are covering what they are supposed to. Let me know how I did and if there are any changes to be made.

container = null;
});

fit('should fire change for checkbox input', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wrote "fit" in my example to force only a few tests to execute. It should not be used in committed code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should it just be it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes.

ReactTestUtils.SimulateNative.click(input);

setUntrackedChecked.call(node, false);
node.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
Copy link
Collaborator

@gaearon gaearon Nov 24, 2017

Choose a reason for hiding this comment

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

We should leave a comment about jsdom click misbehaving. It's not obvious and doesn't happen in the browser (it's also possible jsdom has fixed this in new versions so we need to make it clear in case somebody later tries to update it, and the test will fail).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you please elaborate on what a 'common' is?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

node.dispatchEvent(new Event('click'));
expect(called).toBe(0);

Is this what you mean?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, autocorrect. Meant "we should leave a comment". Like in the sample I wrote above.


expect(called).toBe(1);
});

it('should unmount', () => {
var container = document.createElement('div');
fit('should catch setting the value programmatically', () => {
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 understand what this test is trying to test. What is the thinking behind it?

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 it should test that the tracked value updates when you set it from js. The test should normally set the value then trigger and event and assert that the event doesn't fire

Copy link
Collaborator

@gaearon gaearon Nov 24, 2017

Choose a reason for hiding this comment

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

Sorry, I meant that I don't understand what the new (rewritten) test is doing (since it's not testing setting the field at all).

@Ethan-Arrowood
Copy link
Contributor Author

Forgot to prettify. Pushing now

@gaearon
Copy link
Collaborator

gaearon commented Nov 24, 2017

Thanks. I think I can take it from here as I want to make some more changes.

@Ethan-Arrowood
Copy link
Contributor Author

Okay! Glad I could contribute 😃

[
<input type="text" onChange={cb} />,
<input type="number" onChange={cb} />,
<input type="range" onChange={cb} />,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like it was important to do this for all types of inputs, and not just 'text'?

@gaearon
Copy link
Collaborator

gaearon commented Nov 24, 2017

I rewrote some of the more complex tests but really appreciate you starting this! That was a tough one 😄

@gaearon gaearon merged commit a67757e into facebook:master Nov 24, 2017
@Ethan-Arrowood
Copy link
Contributor Author

Thank you very much! Love the challenge :) Thanks for all your help.

@gaearon
Copy link
Collaborator

gaearon commented Nov 25, 2017

This was a great discussion. I feel like I actually understand this now. Here's a follow up #11654

HeroProtagonist pushed a commit to HeroProtagonist/react that referenced this pull request Nov 26, 2017
* Use only public API for ChangeEventPlugin-test.js

* precommit commands complete

* Removed comments

* Improving event dispatchers

* Updated tests

* Fixed for revisions

* Prettified

* Add more details and fixes to tests

* Not internal anymore

* Remove unused code
raphamorim pushed a commit to raphamorim/react that referenced this pull request Nov 27, 2017
* Use only public API for ChangeEventPlugin-test.js

* precommit commands complete

* Removed comments

* Improving event dispatchers

* Updated tests

* Fixed for revisions

* Prettified

* Add more details and fixes to tests

* Not internal anymore

* Remove unused code
Ethan-Arrowood added a commit to Ethan-Arrowood/react that referenced this pull request Dec 8, 2017
* Use only public API for ChangeEventPlugin-test.js

* precommit commands complete

* Removed comments

* Improving event dispatchers

* Updated tests

* Fixed for revisions

* Prettified

* Add more details and fixes to tests

* Not internal anymore

* Remove unused code
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