Jest-dom: Add toHaveRole

Created on 17 Oct 2019  路  10Comments  路  Source: testing-library/jest-dom

Describe the feature you'd like:

A toHaveRole matcher that uses a similar implementation to @testing-library's described here. In other words, checking implicit roles in addition to aria roles (i.e. using aria-query).

Motivation

The use case that prompted this was a page that had a button (example text "Create Issue") that was supposed to navigate to a page with a heading with the same text ("Create Issue").

Ideally, the test would look like this:

const createIssueBtn = getByText(/create issue/i)
expect(createIssueBtn).toHaveRole('button')
fireEvent.click(createIssueBtn)

const createIssueHeader = getByText(/create issue/i)
expect(createIssueHeader).toHaveRole('header')

Instead, the opposite can only be done (i.e. getByRole, then assert with toHaveTextContent). This is a little more fickle, since it relies on these elements being the first of the specified role on the page, which may not always be the case. Querying by text first is much more specific, and hopefully more robust.

Concretely, here's what I had to implement as an alternative:

    const utils = render(<App />)
    const createIssueBtn = utils.getByText(/create issue/i)
    // Since the heading on the page we're going to also has the text "create issue", we need to be a little dirty and make sure this element is a button
    expect(createIssueBtn.tagName).toBe('BUTTON')

    fireEvent.click(createIssueButton)

    // Wait for the "create issue" element to be a heading instead of a button -- necessary since the click won't trigger the page change immediately
    await wait(() =>
      expect(utils.getByRole('heading')).toHaveTextContent(/create issue/i)
    )
    const createIssueHeader = utils.getByText(/create issue/i)
    // Again, just being paranoid and making sure this element isn't just the button that's on the home page
    expect(examSchedulerHeader.tagName).not.toBe('BUTTON')
needs discussion

All 10 comments

It feels like it might be simpler to assert that the user navigated to the correct URL after clicking button, getting the history object in a similar way to: https://testing-library.com/docs/example-react-router#reducing-boilerplate

const utils = render(<App />)

const createIssueBtn = utils.getByText(/create issue/i)
fireEvent.click(createIssueBtn)

expect(history.location.pathname).toBe('/create-issue-page');

Or would it be possible to use waitForDomChange after you click the button?

const utils = render(<App />)

const createIssueBtn = utils.getByText(/create issue/i)
fireEvent.click(createIssueBtn)

await waitForDomChange()

const createIssueHeader = utils.getByText(/create issue/i)
...

A toHaveRole matcher that uses a similar implementation to @testing-library's described here

Yes, it seems like every query should have a corresponding jest-dom method


off topic

@connormeredith I think we still want this for general usage even if there is an alternative way for this particular example

This exact flow is really common, where a button opens a modal or page with the same text in a heading.

Some Page...

[New Message]
New Message

To:      [                ]
Subject: [                ]

@connormeredith

It feels like it might be simpler to assert that the user navigated to the correct URL after clicking button

I could do that in addition to my test, but the content on the page changing to what it should is more representative of what I'm trying to test/what the user cares about IMO.

Or would it be possible to use waitForDomChange after you click the button?

Yeah, I definitely considered this. I've actually never used waitForDomChange before -- at first glance my instinct was that it might be brittle (i.e. a future implementation on the page is something else on the page is changing that's unrelated).

Good suggestions -- thanks! 馃檪

I've actually never used waitForDomChange before -- at first glance my instinct was that it might be brittle

Yes, wrapping the specific assertion with wait or using the await findBy... variant is probably preferably in most cases

Can you take the part of the discussion about this specific scenario to https://spectrum.chat/testing-library? The feature request for toHaveRole has been logged 馃槃

Sure thing 馃檪

One last thing in response that I think might still be relevant -- the findBy... method you suggested also doesn't work in this case -- the button you clicked will be found immediately, instead of waiting for the new heading you're trying to find.

-    await wait(() =>
-      expect(utils.getByRole('heading')).toHaveTextContent(/create issue/i)
-    )
+ expect(await utils.findByRole('heading')).toHaveTextContent(/create issue/i)

Yeah -- so in that solution utils.findByRole('heading') will find any heading that was on the original page, and not even run after the DOM changes

I'm confused as to what the conclusion was in the above discussion? Do we think we need this matcher? Isn't it .toHaveAttribute('role', 'dialog') enough? (maybe it is not, if we want to support the implicit roles of some elements, such as lis having an implicit listitem role).

(maybe it is not, if we want to support the implicit roles of some elements, such as lis having an implicit listitem role)

This is the meat of what I'm getting at.

An alternative solution is similar to add a role option similar to the selector option that some queries in @testing-libary/dom has.

// This would fail if an element that's a heading with the 'create issue' text doesn't exist
const createIssueButton = getByText(/create issue/i, { role: 'heading' })

An alternative solution is similar to add a role option similar to the selector option that some queries in @testing-libary/dom has.

@babramczyk With https://github.com/testing-library/dom-testing-library/pull/408 you'll be able to use byRole('heading', { name: /create issue/i })

Was this page helpful?
0 / 5 - 0 ratings

Related issues

BurntCaramel picture BurntCaramel  路  5Comments

kentcdodds picture kentcdodds  路  4Comments

srittau picture srittau  路  5Comments

gnapse picture gnapse  路  3Comments

thomaslombart picture thomaslombart  路  6Comments