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

strange toHaveFocus() behaviour #53

Closed
artooras opened this issue Sep 3, 2018 · 12 comments
Closed

strange toHaveFocus() behaviour #53

artooras opened this issue Sep 3, 2018 · 12 comments

Comments

@artooras
Copy link

artooras commented Sep 3, 2018

Sorry for posting the question here, but I have tried spectrum.chat and SO without much success so far...

I'm having trouble understanding how toHaveFocus() works exactly. Here's my setup:

MyComponent.js

import React from 'react'
import styled from 'styled-components'

import TextArea from './TextArea'


const Container = styled.div`
  flex: 1;
  height: 100%;
  padding: ${props => props.theme.size};
`

const Title = styled(TextArea)`
  font-weight: bold;
  font-size: ${props => props.theme.sizeLarger};
  margin-left: ${props => props.theme.sizeSmall};
`

class MyComponent extends React.Component {

  handleTitleChange = e => {
    this.props.onTitleChange(e.target.value)
  }

  handleTitleLostFocus = () => {
    this.props.onChangeComplete()
  }

  render () {
    return (
      <Container>
        <Title 
          value={this.props.item.title || ''}
          onChange={this.handleTitleChange}
          onBlur={this.handleTitleLostFocus}
        />
      </Container>
    )
  }
}

export default MyComponent

MyComponent.test.js

import React from 'react'
import {render, fireEvent, prettyDOM} from 'react-testing-library'

import MyComponent from '../MyComponent'


describe('MyComponent', () => {
  it('handles title changes', () => {
    const title = 'title'
    const handleTitleChangeMock = jest.fn()
    const {getByText} = render(
      <MyComponent
        item={{
          title: title
        }}
        onTitleChange={handleTitleChangeMock}
        onChangeComplete={() => {}}
      />
    )
    const titleInput = getByText(title)
    console.log(prettyDOM(titleInput))
    fireEvent.click(getByText(title))
    expect(getByText(title)).toHaveFocus()
    fireEvent.change(getByText(title), {target: {value: title.slice(0, -1)}})
    expect(handleTitleChangeMock).toHaveBeenCalledTimes(1)
    expect(handleTitleChangeMock).toHaveBeenCalledWith(title.slice(0, -1))
  })
})

When I do this:

const titleInput = getByText(title)
console.log(prettyDOM(titleInput))

The console logs the following:

<textarea
    class="sc-htoDjs dLjZCT sc-bxivhb jdLTBU"
    style="height: 0px;"
  >
    title
  </textarea>

That textarea element is the one I am targeting. But then, when I do this:

fireEvent.click(titleInput)
expect(titleInput).toHaveFocus()

I get this error:

Received:
  <body><textarea style="min-height: 0 !important; max-height: none !important; height: 0px !important; visibility: hidden !important; overflow: hidden !important; position: absolute !important; z-index: -1000 !important; top: 0px !important; right: 0px;" /><div><div class="sc-bZQynM ePHCfO"><textarea class="sc-htoDjs dLjZCT sc-bxivhb jdLTBU" style="height: 0px;">title</textarea></div></div></body>

  at Object.it (src/__tests__/MyComponent.test.js:84:30)
      at new Promise (<anonymous>)
  at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
      at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:188:7)

I don't quite understand why when I'm trying to assert a textarea element toHaveFocus(), I'm receiving an error that references the entire DOM tree under body...

@gnapse
Copy link
Member

gnapse commented Sep 3, 2018

Hi @artooras, sorry for the lack of attention in SO.

I'll be able to take a deeper look at this tomorrow. At first sight I'm not sure what it is. It'd be good if you can provide a small reproducible example in codesandbox.io.

Although one final note: You do not need to simulate the click and the focus before fireEvent.change. If all you're trying to do is test that the component properly reacts to attempts to change the texture value, the fireEvent.change is enough.

This of course does not solve the underlying problem, but my point it, you can go forward with the test without those steps. Unless they're there for some reason. Are they?

@smacpherson64
Copy link
Collaborator

Hi @artooras, the toHaveFocus matcher checks the element against document.activeElement. By default, and when no element is focused, document.activeElement is set to document.body.

I believe fireEvent.click is not focusing the element in this scenario:
https://codesandbox.io/s/l9j2zzny9l

@artooras
Copy link
Author

Thanks @smacpherson64. Why doesn't fireEvent.click focus the textarea though? I suppose testing for a click would be more correct than setting focus programmatically via fireEvent.focus as I want to reproduce an actual user action, right?

@gnapse, as for the change event, I'm not sure I need it TBO. Ideally I would like to follow the exact steps a user would, e.g. fireEvent.focus(titleInput) and then fireEvent.keyDown(titleInput, {key: 'a', keyCode: 65, which: 65}). Would this trigger a change?

@smacpherson64
Copy link
Collaborator

Hey @artooras,

I did some testing on this and think it is because a manual click in the browser couples a focus event with it, where firing the click event only fires a click event:

Exploration using about:blank

document.body.appendChild(document.createElement('input'))
document.addEventListener('focusin', console.log)
document.addEventListener('click', console.log)
const input = document.querySelector('input')
  • When clicking on the element manually both the focusin and click event occur.
  • When using events (input.click()) to click only the 'click' event occurs.

@smacpherson64
Copy link
Collaborator

Hahaha, sorry @artooras I didn't fully answer the question. Since the tooling has different behavior than the browser it is necessary in this case to work with the tooling (or change it for the better :-)!). I think you will run into the same with the change event as well. I do not believe, react-testing-library currently binds multiple events together like a browser does.

I think you bring up really good points, the points may have already been discussed but I am not sure. A good avenue for this would be to create a bug report on react-testing-library describing the discrepancies you are experiencing between the browser and react-testing-library. That would bring to light what the correct way to accomplish what you are aiming for when it comes to react-testing-library.

I hope this helps!

@gnapse
Copy link
Member

gnapse commented Sep 16, 2018

@artooras I think the problem is related to the fact that when an element looses focus, the next element to acquire it actually gets it on the next tick, and briefly, during a tick, the focus is placed on the document body. Source: https://stackoverflow.com/a/11299876

Can you please wrap your expectation toHaveFocus inside wait?

// import `wait` from react-testing-library
import { render, fireEvent, cleanup, wait } from 'react-testing-library';

// ...

fireEvent.click(titleInput)
await wait(() => {
  expect(titleInput).toHaveFocus()
});

When you programmatically give it focus directly, instead of with a click, I think it happens immediately. But with a click (or when acquired via pressing the tab key in a web page) it happens in this manner described above. Agreed is weird, because even if it's async, I would expect to pass focus directly from the current active element to the new one, without briefly giving focus to the body. But that's how it is, apparently.

Let us know if this helps.

Update: BTW, this is not a jsdom thing. I actually realized this was the trouble you might be facing, when I encountered it precisely today in a UI I've been working on. It happens in the browser too. That's where I encountered first, not in tests.

@smacpherson64
Copy link
Collaborator

smacpherson64 commented Sep 18, 2018

Hey @gnapse,

I’ve updated the exploration in codesandbox to use wait but the test is failing: (The wait takes about 5 seconds to fail because of the timeout.)

https://codesandbox.io/s/l9j2zzny9l

Can you check to make sure It matches what you said above or explain further what you mean (if you have a shareable example that would be awesome!). I have not heard of that behavior before!

@gnapse
Copy link
Member

gnapse commented Sep 18, 2018

Ok, I'll dig up a little bit more.

I did not know about this behavior I commented before. I just found out recently when it actually helped me solve a problem unrelated to jest-dom. I thought it could explain this too.

@gnapse
Copy link
Member

gnapse commented Sep 18, 2018

Hmmm, I wonder if dom-testing-library's fireEvent.focus(...) or fireEvent.click(...) merely dispatch the event so that registered event handlers are called, or if it also generates the side effect of giving focus to the element.

A couple of tests I'm doing seem like it's just about triggering event handlers. But if what I suspect is wrong, then I'm not sure what are we (me and @artooras who raised this issue above) doing wrong. The code for fireEvent also does not seem to taking care of this.

I checked our own tests of toHaveFocus, and of course we're giving focus to an element directly by calling element.focus(), because we do not depend on or use dom-testing-library.

@kentcdodds care to chime in?

@gnapse
Copy link
Member

gnapse commented Sep 19, 2018

The best I can come up with is that you'd need to call element.focus() in addition to fireEvent.focus(element), if you not just want to test the event handlers, but also have the actual element receive focus.

At most, it may be conceivable (?) to suggest dom-testing-library to do this ad-hoc action specifically for the fireEvent.focus() call. What do you think @kentcdodds? Though I'd understand if this is not accepted.

@kentcdodds
Copy link
Member

Huh... Interesting. I think that between this and #59, we're starting to see that what people want is something more like a user object which I've considered before. Whereas fireEvent simply dispatches events on DOM nodes, a user object would fire all events necessary to do an action.

For example, this would focus the input and dispatch input events for each character:

user.type(inputNode, 'hello')

Why don't we open something up in dom-testing-library to discuss this.

@gnapse
Copy link
Member

gnapse commented Sep 20, 2018

Done testing-library/dom-testing-library#107

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

No branches or pull requests

4 participants