Skip to content

feat(byRole): Add name filter #408

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

Merged
merged 10 commits into from
Mar 4, 2020
Merged

Conversation

eps1lon
Copy link
Member

@eps1lon eps1lon commented Nov 29, 2019

What:
Add a name option to ByRole queries. If a name is provided then the error messages will include the accessible name of each element with a role.

Why:

Better alternative to byText (especially for buttons) or byLabelText when aria-label or aria-labelledby is used (e.g. for custom Select implementations) or byTestId for "semantic" elements e.g. menuitem.

There's a bit more to write in https://testing-library.com/docs/guide-which-query to explain why you still might want to use byLabelText('street') instead of byRole('textbox', { name: 'street' }).

How:

Using https://github.com/eps1lon/dom-accessibility-api#readme which implements https://www.w3.org/TR/accname-1.2/ and is tested against web-platform-tests

Checklist:

I would hope that (after review and testing phase) we can move that package under the testing-library umbrella and move isInaccessible into dom-accessibility-api. In the end I'd like to add a single configuration that allows mocking window.getComputedStyle for the a11y context. That operation is quite expensive and I think allowing to pretend that every element is visible is sufficient for unit testing. The rationale being that we don't exclude certain groups of people by (accidentally) implementing inaccessible component but instead exclude everyone if we assert on elements that are actually visually hidden.

@eps1lon eps1lon added the enhancement New feature or request label Nov 29, 2019
@codesandbox-ci
Copy link

codesandbox-ci bot commented Nov 29, 2019

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 7a53e33:

Sandbox Source
inspiring-morning-z3ptt Configuration

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very very cool. Really looking forward to this feature!

@kentcdodds
Copy link
Member

Also, I completely agree with your comment about moving things around a bit 👍

@alexkrolick
Copy link
Collaborator

alexkrolick commented Dec 2, 2019

One thing I am wondering is why this shouldn't support combining the other matchers as well:

// combine with displayValue
getByRole('textbox', {displayValue: 'username'})

// combine with aria-labelledby or htmlFor
getByRole('dialog', {label: 'uh oh'})

// Inverted? Anything that takes a `selector` option could take `role`?
// This actually seems like a more natural modifier - it increases specificity
// of content-based queries.
getByLabelText('uh oh', {role: 'dialog'})

Seems like a one off when we should be figuring out how to intersect queries in general. There have been a few proposals on how to do that.

@eps1lon
Copy link
Member Author

eps1lon commented Dec 3, 2019

getByLabelText('uh oh', {role: 'dialog'})

Not sure what this is supposed to query. A dialog doesn't have a label. It's important to note that getByLabelText('street') is not equivalent to getByRole('textbox', { name: 'street' }) because the <label> element has other behavior as well: Clicking a label will focus the control it is associated with.

getByRole('textbox', {displayValue: 'username'})

I would have to look into this more. I don't know how display value is handled currently but we need consider that one might include the actual value attribute which is sent to the server/form library while e.g. <div role="textbox">value</div> does no such thing necessarily.

But this will definitely be part of the docs since it's apparently not obvious when to use what.

@alexkrolick
Copy link
Collaborator

That was just an example, but this is from MDN:

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role

<div role="dialog" aria-labelledby="dialog1Title" aria-describedby="dialog1Desc">
  <h2 id="dialog1Title">Your personal details were successfully updated</h2>
  <p id="dialog1Desc">You can change your details at any time in the user account section.</p>
  <button>Close</button>
</div>

@eps1lon
Copy link
Member Author

eps1lon commented Dec 4, 2019

That was just an example, but this is from MDN:

Sure. But be careful to not mix a <label> with the accessible name. They share some features but also have distinct features (e.g. clicking a <label> focuses the associated input).

If you used getByLabelText for the accessible name only then you can switch. If your test implied the click-focus feature then you shouldn't (or if you do add another test for click-focus).

@eps1lon eps1lon mentioned this pull request Jan 15, 2020
@eps1lon
Copy link
Member Author

eps1lon commented Jan 15, 2020

Plan for this in the next week:

@codecov
Copy link

codecov bot commented Jan 25, 2020

Codecov Report

Merging #408 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #408   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          22     22           
  Lines         385    397   +12     
  Branches       91     94    +3     
=====================================
+ Hits          385    397   +12
Impacted Files Coverage Δ
src/role-helpers.js 100% <100%> (ø) ⬆️
src/queries/role.js 100% <100%> (ø) ⬆️

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 ed36447...7a53e33. Read the comment docs.

@kentcdodds
Copy link
Member

Awesome! Let us know when you're ready for a review. Thanks a bunch!

@bowernite
Copy link

I'm also curious about the inversion that @alexkrolick mentioned. Maybe the example he gave wasn't applicable (finding a dialog by label text), but others would be.

getByText(/my website/i, { role: 'heading' })

This seems a little more expressive, at least for this use case than the query added in this PR:

getByRole('heading', { name: /my website/i })

I want to get something with specific text, and the fact that it's a heading is supplementary to that. I concede that this is nitpicky, though.

@eps1lon
Copy link
Member Author

eps1lon commented Feb 2, 2020

I want to get something with specific text, and the fact that it's a heading is supplementary to that. I concede that this is nitpicky, though.

I wouldn't agree with that since roles might be button, dialog, heading etc. These are not secondary at all.

The point is to encourage writing out what kind of element you're dealing with. It's more important that the element is e.g. a button than that it reads "log in". The first one is part of the UI/UX. The second one is subject to localization.

@bowernite
Copy link

Fair enough. I honestly can see scenarios in which either the role or the text are the primary thing you're concerned with.

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking awesome. I'm excited for this one :)

expect(deliveryForm).not.toBeNull()

expect(
// TODO: upstream bug in `aria-query`; should be `button` role
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we contribute a fix to aria-query before merging this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. They released a new major. Opening a separate PR that bumps aria-query and then we'll see if that worked.


heading:

Name "Sign up":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference between this and the query is the u is not capitalized. Could we make this difference more stark so this makes more sense why it failed to find an element please? Thanks!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had trouble understanding it as well. I would like to keep the test to make sure that { name: string } is always case-sensitive. I'll add a comment to the test explaining the issue.

</div>"
`)

expect(() => getByRole('heading', {name: /Login/}))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it's necessary to have all three of these because they're all testing the same codepath and their differences are covered in other tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more about showcasing how the name argument is serialized. It may be too verbose in which case the codepath should change. And overall: Isn't that testing implementation details 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(() => getByRole('heading', {name: /Login/}))
expect(() => getByRole('heading', {name: /something that does not match/}))

I think this makes it more clear that it's not just a typo...

.join('\n\n')

return `${role}:\n\n${elementsString}\n\n${delimiterBar}`
})
.join('\n')
}

const logRoles = (dom, {hidden = false} = {}) =>
console.log(prettyRoles(dom, {hidden}))
const logRoles = (dom, {hidden = false, includeName = false} = {}) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we not want to always include the name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't thought much about this. Just wanted to not change existing behavior.
Since this is only used by library consumers it might be a good idea to include the name by default to promote that we now support accessible name queries?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, since it's debug output, it seems fine to change.

@eps1lon
Copy link
Member Author

eps1lon commented Feb 4, 2020

Addressed #408 (comment) and #408 (comment) in 3c7dd0e.

@eps1lon eps1lon mentioned this pull request Feb 4, 2020
2 tasks
Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool cool! Looking good :)

Comment on lines 149 to 156
.map(
el =>
`${
includeName === true
? `Name "${computeAccessibleName(el)}":\n`
: ''
}${prettyDOM(el.cloneNode(false))}`,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prettier makes this look funny. Why don't we do this:

Suggested change
.map(
el =>
`${
includeName === true
? `Name "${computeAccessibleName(el)}":\n`
: ''
}${prettyDOM(el.cloneNode(false))}`,
)
.map(el =>
[
includeName === true
? `Name "${computeAccessibleName(el)}":\n`
: '',
prettyDOM(el.cloneNode(false)),
].join(''),
)

.join('\n\n')

return `${role}:\n\n${elementsString}\n\n${delimiterBar}`
})
.join('\n')
}

const logRoles = (dom, {hidden = false} = {}) =>
console.log(prettyRoles(dom, {hidden}))
const logRoles = (dom, {hidden = false, includeName = true} = {}) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we provide the option to not include the name?

@kentcdodds
Copy link
Member

I'm working on this now. I'll go ahead and remove the includeName feature because I think it's easier to add it later if we really need it than to include it if we don't really have a use case. Thanks!

@kentcdodds kentcdodds merged commit a98dc9f into testing-library:master Mar 4, 2020
@kentcdodds
Copy link
Member

🎉 This PR is included in version 6.14.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@eps1lon
Copy link
Member Author

eps1lon commented Mar 4, 2020

Agreed with everything. Thanks for finishing this up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request released
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants