-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Conversation
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; |
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.
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?
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.
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; | ||
// } |
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.
Essentially if the input event isn't supported this test won't test anything
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.
how do you check if an input is valid?
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.
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.
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.
Okay then I'll clean up my code and push. Should be good to go at this point.
Is this ready for review, or is there more work planned? |
@gaearon I think its ready to be reviewed |
Can we also try to get rid of |
@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 |
I think you can just |
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.
Let's avoid SimulateNative
too.
Could use some help. I can't for the life of me get the onChange callback function to run: |
@Ethan-Arrowood checkboxes don't listen to change or input events oddly enough. They actually respond to |
@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? |
ping @jquense :-) |
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 |
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 |
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. |
TODO: before merging, verify cases in #11337 are also covered. |
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.
Let's try to avoid the simulated
flag. Any ideas?
I'm taking a closer look...I'm not sure the simulated thing is actually the cause here. |
so from what I can tell using:
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 |
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 |
Experimental but could be useful in the future once completed: https://w3c.github.io/input-events/index.html#interface-InputEvent |
I can give it another shot, but I'm a little confused about the behavior of the onChange handler |
I wouldn't expect triggering If you open 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: react/packages/react-dom/src/events/ChangeEventPlugin.js Lines 200 to 203 in 46dd197
So trying to get dispatching |
Interestingly it seems that dispatching |
|
||
expect(called).toBe(1); | ||
var node = ReactDOM.findDOMNode(stub); |
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.
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'); |
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.
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( |
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.
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.
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);
}); |
Thanks for the help @gaearon! Let me see what I can come up with now. |
@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', () => { |
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.
I wrote "fit" in my example to force only a few tests to execute. It should not be used in committed code.
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.
Should it just be it
?
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.
Yes.
ReactTestUtils.SimulateNative.click(input); | ||
|
||
setUntrackedChecked.call(node, false); | ||
node.dispatchEvent(new Event('click', {bubbles: true, cancelable: true})); |
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.
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).
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.
Can you please elaborate on what a 'common' is?
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.
node.dispatchEvent(new Event('click'));
expect(called).toBe(0);
Is this what you mean?
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.
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', () => { |
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.
I don't understand what this test is trying to test. What is the thinking behind it?
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.
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
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.
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).
Forgot to prettify. Pushing now |
Thanks. I think I can take it from here as I want to make some more changes. |
Okay! Glad I could contribute 😃 |
[ | ||
<input type="text" onChange={cb} />, | ||
<input type="number" onChange={cb} />, | ||
<input type="range" onChange={cb} />, |
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.
Seems like it was important to do this for all types of inputs, and not just 'text
'?
I rewrote some of the more complex tests but really appreciate you starting this! That was a tough one 😄 |
Thank you very much! Love the challenge :) Thanks for all your help. |
This was a great discussion. I feel like I actually understand this now. Here's a follow up #11654 |
* 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
* 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
* 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
Ref #11299
Do not merge this! I need help understanding the
ChangeEventPlugin._isInputEventSupported
method on line 172.