Dom-testing-library: Enhance experience while writing tests with waitForElement

Created on 13 Oct 2018  路  14Comments  路  Source: testing-library/dom-testing-library

Describe the feature you'd like:

Currently I am working on improve the test coverage of my project and I am trying to use waitForElement to detect if some specific element appears in the DOM. Because you can't visually see the changes which happen after triggering a input change or click. I think it would be nice to be somehow have a debug log in the waitForElement and its companions so you can see which changes occurred in the DOM.

It's a bit of a black box at the moment as you can't really compare it with manually doing the actions in the browser. E.g. for example I want to test if some message is being shown while the user leaves focus of an input field e.g. the password strength message. Basically I want to give an input field focus type something and leave the field so onBlur gets called.

Suggested implementation:

I think an easy way to ease the experience would be to add new argument to waitForElement (etc) methods so it enabled logging and then all the changed nodes will be logged.

E.g. new node added with .... I am not sure if you want to only log node mutations or also mutations to attributes of nodes

Describe alternatives you've considered:

I have been messing around with breakpoints

Teachability, Documentation, Adoption, Migration Strategy:

Users would be able to temporary enable the logging argument flag and see all the changes occurred within the container of rendered component and after ensured the expected change occurred they could disabled it again.

Most helpful comment

I personally prefer more explicitness in my tests when possible, so I would write this test like so:

    test('ensure field is rendered with error', async () => {
        const handleChange = jest.fn()
        const handleValidate = jest.fn().mockImplementation(values => ({name: 'Error message'}))
        const {container, getByText, getByLabelText, debug} = renderWithFormik(
            <FormField name="name" label="Name" />,
            { validate: handleValidate, validateOnChange: true, validateOnBlur: true }
        )
        const nameInput = getByLabelText('Name')
        expect(nameInput).not.toBeNull()
        userEvent.type(nameInput, 'Firstname')
        expect(nameInput.value).toBe('Firstname')
        userEvent.type(nameInput, '', { allAtOnce: true })
        expect(nameInput.value).toBe('')
        fireEvent.blur(nameInput) // Note: This is needed for Formik to touch the field so the error is shown
        await wait(() => expect(getByText('Error message')).not.toBeNull())
        debug(nameInput)
    })

If that fails, then the error message should print out the state of the DOM so you can see if your query is wrong.

All 14 comments

I think logging would also have helped with catching this issue mentioned in an earlier ticket: https://github.com/kentcdodds/dom-testing-library/issues/72

So something like:

const el = waitForElement(() => getByText(container, 'hi'), {logChanges: true})

And any time there's a mutation, we'll basically call prettyDOM on the container and console.log? Hmmm... I think it may be better to do:

What if you do:

const el = waitForElement(() => {
  console.log(prettyDOM(container))
  return getByText(container, 'hi')
})

or if you're using react-testing-library:

const el = waitForElement(() => {
  debug()
  return getByText('hi')
})

I think we should document that example rather than add a fancy option. What do you think?

I think a clear explanation in the docs of what is happening in the code @kentcdodds posted will not only help people in this specific situation, but will also give a better understanding of what waitForElement is doing. It is literally just a function that calls the return of the callback when the dom changes, and that code clearly shows that.

Oh cool, I totally wasn't aware it worked like that. What if we only wanted the changes printed out?

I suppose we could pass the arguments our onMutation handler receives to your callbacks here and you could log that, but I don't know if a good way to show only the changes in a way that is at all helpful... Lots of changes can happen. I feel like trying to print out only what changed would be pretty complex, but an interesting problem if anyone wants to take a swing at it... But for this issue, I think someone should just add the example we have to the README and move on. Anyone want to do that?

I will have a stab at it, probably not today, though

What about using debug to scope verbose logging? It would also be useful to do full-DOM printouts in prettyDOM when there is an error instead of truncated.

DEBUG=dom-testing:mutations yarn test
DEBUG=dom-testing:queries yarn test
DEBUG=dom-testing:* yarn test

I think it's ok. But I feel like that kind of feature is always so obscure that nobody ever uses it...

Well, maybe. But every major CLI app implements it. It's also nice because you don't have to change test code to add/remove verbose debug output.

True, but you do have to stop the test script and start it up again to make any changes. Trade-offs.

I have been trying to make use of it but not much mutation is happening with my component. I am attempting to rewrite with react-testing-library from the get go.

I have been able to make it work with the new helper method waitForDomChange never got it working with waitForElement I tried it like this for that method: await waitForElement(() => expect(getByText('Error message')).not.toBeNull())

    test('ensure field is rendered with error', async () => {
        const handleChange = jest.fn()
        const handleValidate = jest.fn().mockImplementation(values => ({name: 'Error message'}))
        const {container, getByText, getByLabelText, debug} = renderWithFormik(
            <FormField name="name" label="Name" />,
            { validate: handleValidate, validateOnChange: true, validateOnBlur: true }
        )
        const nameInput = getByLabelText('Name')
        expect(nameInput).not.toBeNull()
        userEvent.type(nameInput, 'Firstname')
        expect(nameInput.value).toBe('Firstname')
        userEvent.type(nameInput, '', { allAtOnce: true })
        expect(nameInput.value).toBe('')
        fireEvent.blur(nameInput) // Note: This is needed for Formik to touch the field so the error is shown
        await waitForDomChange() // Wait for a change in the DOM 
        expect(getByText('Error message')).not.toBeNull()
        debug(nameInput)
    })

I personally prefer more explicitness in my tests when possible, so I would write this test like so:

    test('ensure field is rendered with error', async () => {
        const handleChange = jest.fn()
        const handleValidate = jest.fn().mockImplementation(values => ({name: 'Error message'}))
        const {container, getByText, getByLabelText, debug} = renderWithFormik(
            <FormField name="name" label="Name" />,
            { validate: handleValidate, validateOnChange: true, validateOnBlur: true }
        )
        const nameInput = getByLabelText('Name')
        expect(nameInput).not.toBeNull()
        userEvent.type(nameInput, 'Firstname')
        expect(nameInput.value).toBe('Firstname')
        userEvent.type(nameInput, '', { allAtOnce: true })
        expect(nameInput.value).toBe('')
        fireEvent.blur(nameInput) // Note: This is needed for Formik to touch the field so the error is shown
        await wait(() => expect(getByText('Error message')).not.toBeNull())
        debug(nameInput)
    })

If that fails, then the error message should print out the state of the DOM so you can see if your query is wrong.

Thank you, Kent. Appreciated :)

Was this page helpful?
0 / 5 - 0 ratings