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

fix(keyboard): dispatch change event on blur #703

Merged
merged 3 commits into from
Jul 25, 2021
Merged

fix(keyboard): dispatch change event on blur #703

merged 3 commits into from
Jul 25, 2021

Conversation

ph-fritsche
Copy link
Member

What:
Closes #682

Why:
The browser dispatches a change event when an element with changed value loses focus.
When we alter the element value/textContent, we dispatch an input event but no native change event.

How:
When setting the value/textContent an event handler is added to the capture phase of the blur event on the window.
If the value/textContent on our element is different than the initial one, we dispatch a change event.

Checklist:

  • [N/A] Documentation
  • Tests
  • Ready to be merged

Constraints:

  • We can not determine if an element was changed between gaining focus and our first edit.
    We could (also) set the initial value on the focus event, but for this we would either:
    1. add it on the document as side-effect of the import (only works if the document is global)
    2. add it per additional setup API (and/or implicitly when calling one of our other APIs)
  • There is no cross-platform solution for adding an event handler to the bottom of the stack.
    Therefore other event handlers on blur (that are added on the capture phase on window) might be executed before we dispatch the change event.

Sorry, something went wrong.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jul 20, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit d9c9f54:

Sandbox Source
userEvent-PR-template Configuration
lucid-smoke-vodzj Issue #682

@codecov
Copy link

codecov bot commented Jul 20, 2021

Codecov Report

Merging #703 (d9c9f54) into main (e5e78af) will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #703   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           50        50           
  Lines          935       947   +12     
  Branches       369       372    +3     
=========================================
+ Hits           935       947   +12     
Impacted Files Coverage Δ
src/keyboard/shared/fireInputEvent.ts 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e5e78af...d9c9f54. Read the comment docs.

@ph-fritsche
Copy link
Member Author

@nickmccurdy What do you think about adding a setup API?
If we extend the keyboard approach of an internal state (options.keyboardState) to potential new APIs (pointer #638 (comment)), we could allow the user to start his test at any state of the input devices (e.g. element is added while ControlLeft and left mouse button are pressed).

@nickserv
Copy link
Member

What would usage look like? And this is unrelated from our internal setup function for rendering HTML right?

@ph-fritsche
Copy link
Member Author

For the problem at hand - i.e. keeping track of non-userevent changes:

it('test something', () => {
  // ... render empty text field ...
  element.focus()
  element.value = 'foo'
  userEvent.keyboard('bar')
  userEvent.keyboard('{Backspace}{Backspace}{Backspace}')
  element.blur() // this triggers no change event but it should
})
it('test again with setup', () => {
  // ... render empty text field ...
  userEvent.setup({document: element.ownerDocument})
  element.focus()
  element.value = 'foo'
  userEvent.keyboard('bar')
  userEvent.keyboard('{Backspace}{Backspace}{Backspace}')
  element.blur() // does trigger a change event because we tracked all changes since `focus()`
})

For keyboardState and proposed pointerState:

it('act on some dialog that opened on [ctrl]+[mouseLeft]', () => {
  // ... render ...
  userEvent.setup({
    keyboardState: userEvent.createKeyboardState('[ControlLeft>]'),
    pointerState: userEvent.createPointerState('[MouseLeft>]'), // tbd how this API will exactly look like
  })

  // possibly some rudimentary pointer movement
  // will not be accurate in jsdom but could be good enough for many cases
  // e.g. testing that drag&drop handlers work in principle
  userEvent.pointerMove(element, {x: 15, y: 20})

  userEvent.pointer(element, '[/MouseLeft]') // trigger pointerUp, mouseUp, click with ctrl=true
})

@nickserv
Copy link
Member

nickserv commented Jul 20, 2021

I may be missing something, but I don't see an advantage to using setup over calling render with a custom element and some custom function calls after:

function renderWithStates(element) {
  render(element, { element: element.ownerDocument})
  userEvent.keyboard('[ControlLeft>]')
  userEvent.pointer('[MouseLeft>]')
}

A more specific pointer API could help for testing edge cases, though I'm not sure how commonly that would be needed in JSDOM.

@ph-fritsche
Copy link
Member Author

For our blind spot regarding changes that happen before our API call the users would have to reimplement our workarounds for the change event in their render function.
And they would need to be aware of internals of DOM and possibly some framework (React).
I think the problem and the solution are non-obvious.
Pointing the users towards an easy solution like "if you programmatically alter the value before changing it with userEvent (e.g. inside onFocus) please call userEvent.setup after creating the document" sounds better to me.


For the keyboardState: Currently the state ist considered internal and returned from keyboard as a black box.

let keyboardState = userEvent.keyboard('[ControlLeft>]')
// ... do something ...
keyboardState = userEvent.keyboard('abc', {keyboardState}) // continue key events with ctrl=true
// ... do something else ...
userEvent.keyboard('[/ControlLeft]', {keyboardState}) // release control key

But currently it is not (officially) possible to start off with some state without triggering the extra events that come with it. If that should happen in a setup function or right on the keyboard call or both would be to be determined. Just adding the note that it could be used for this.

@ph-fritsche ph-fritsche merged commit 9600abb into main Jul 25, 2021
@ph-fritsche ph-fritsche deleted the fix-682 branch July 25, 2021 13:57
@github-actions
Copy link

🎉 This PR is included in version 13.2.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Successfully merging this pull request may close these issues.

userEvent.type does not dispatch change events on input with @testing-library/dom
2 participants