testing-library / jest-dom

:owl: Custom jest matchers to test the state of the DOM

Home Page:https://testing-library.com/docs/ecosystem-jest-dom

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

add aria-disabled in the `toBeDisabled` matcher

chrismatheson opened this issue · comments

Describe the feature you'd like:

i have custom buttons (SVG's with click handlers) which i want to ensure are disabled under certain circumstances. Since these elements have disabled styling I've been using the aria-disabled attrubtue for this purpose (two birds one stone)

so i would like to be able to test as
expect(myCustomSvgButton).toBeDisabled()

Describe alternatives you've considered:

obiviously this is doable just now using .toHaveAttribute but i feel like this reduces the clarity of what's being checked and focuses on implementation details making the test brittle.

on a side note, maybe encouraging the use of aria-* attrs for communication of more complex state in the DOM is not a bad thing?

I wasn't aware of this issue so I want to summarize some concerns that I raised when the implementation was proposed:

The aria-disabled attribute (proposed) and the disabled property (current) assert different things with a very small overlap. I do not know everything disabled implies but of the top of my hat the following are implicitly expected when disabled:

  • not focusable
  • not clickable
  • certain events not firing (e.g. mouseEnter)
  • implicit aria-disabled="true"

So while I do see the usefulness of an toBeAriaDisabled matcher (either having aria-disabled="true" or disabled) I do not think it is a good idea to check for aria-disabled in toBeDisabled.

@eps1lon thanks for the input, i suppose i was thinking (maybe incorrectly) that this would be inline with the testing-library philosophy, that tests shouldn't necessary be testing implementation details (which exact attributes has a value) but should be testing what a user observes as the behaviour.

for the latter case, to a screenreader client disabled & aria-dissabled would indicate the same thing ?

however for my own self interest, would there be a better way to indicate disabled SVG buttons/Clickable elements that would be testable using toBeDissabled ?

for the latter case, to a screenreader client disabled & aria-dissabled would indicate the same thing ?

Partially. But you're not matching toBeAriaDisabled but toBeDisabled. The latter one implies a variety of other UX properties that are not implied by aria-disabled.

So to summarize:

  • toBeAriaDisabled matches aria-disabled="true" or disabled
  • toBeDisabled matches the disabled property

If you want that toBeDisabled also matches aria-disabled="true" you have to match all the other UX behaviors implied by disabled.

Also want to address

that tests shouldn't necessary be testing implementation details

checking that a disabled element is not focusable is not an implementation detail. This is very much observable both by users with and without assistive technology.

So to summarize, @eps1lon, what you're proposing is that we leave toBeDisabled as is, and we instead add a new .toBeAriaDisabled custom matcher. Is that it?

@gnapse 👍 That seems like it would adress the original issue and be safe for UX testing.

If .toBeAriaDisabled is added then should we add .toBeAriaHidden or .toBeAriaExpanded or .toBeAria*?

If you want that toBeDisabled also matches aria-disabled="true" you have to match all the other UX behaviors implied by disabled.

It doesn't look like .toBeInvalid is matching all other UX behaviours implied by invalid. It looks like .toBeInvalid checks for aria-invalid="true" and toBeChecked checks for aria-checked="true" so why would .toBeDisabled work differently?

I think it makes more sense to add the aria-disabled check to .toBeDisabled so that's it's intuitive to the user and is consistent with the other matchers.

If .toBeAriaDisabled is added then should we add .toBeAriaHidden or .toBeAriaExpanded or .toBeAria*?

Yep although I would make sure there aren't other libraries out there that already reflect aria properties.

I think it makes more sense to add the aria-disabled check to .toBeDisabled so that's it's intuitive to the user and is consistent with the other matchers.

If toBeInvalid has similar false assumptions then we should fix this instead of applying this bug everywhere else.

It does feel like toBeAriaDisabled is implying the underlying implementation, I'm not sure how you would convey that the extra recommended UX behaviours have been met by just by using a different matcher. Unfortunately the aria spec is a bit light on the details of the 'UX behaviours' associated with aria-disabled so I'm not sure what toBeAriaDisabled will give you that toBeDisabled won't.

Also wouldn't the user of jest-dom now have to make a decision on which matcher to use based on their implementation? I guess another solution would be that toBeDisabled also asserts all the behaviours associated with an element being disabled, regardless of aria-disabled="true" or disabled? Or jest-dom makes no assumption on the behaviour of a disabled element and only asserts that the correct attributes have been applied?

https://www.w3.org/TR/wai-aria-1.1/#aria-disabled

Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable. See related aria-hidden and aria-readonly.

For example, irrelevant options in a radio group may be disabled. Disabled elements might not receive focus from the tab order. For some disabled elements, applications might choose not to support navigation to descendants. In addition to setting the aria-disabled attribute, authors SHOULD change the appearance (grayed out, etc.) to indicate that the item has been disabled.

The state of being disabled applies to the current element and all focusable descendant elements of the element on which the aria-disabled attribute is applied.

Also wouldn't the user of jest-dom now have to make a decision on which matcher to use based on their implementation?

Yes. I can't stress this enough: Using aria-* attributes is not an implementation detail. They are very much observable by the user of your website. There is a big difference between <button disabled /> and <button aria-disabled="true" /> so your test should reflect that. When we talk about implementation details we usually mean the details of your renderer e.g. react or vue or svelte. For some APIs of the DOM this is also true but we currently don't mimic a browser with the testing-library (and doing so would be out of scope).

aria-* attributes (as the spec even words it) are only indicators for assistive technology. These map these attributes and the original DOM attributes/properties to their accessibility API which then puts this out as speech or receivable commands to the user. With aria-* attributes you communicate with assistive technology. With the other attributes you communicate with assistive technology and users not using assistive technology. Even if you are writing software only usable by assistive technology then both matchers then these matchers wouldn't do the same. <button aria-disabled /> is still focusable (which is perfectly valid) while <button disabled /> is not. This is pretty significant for users of assistive technology as well.

I understand that it is tempting to short-circuit writing tests. If you have considered all the implications of considering aria-disabled and disabled equal then you can always use custom matchers in your code base.

Yep that makes perfect sense, thanks for the detailed explanation! 👍

If toBeInvalid has similar false assumptions then we should fix this instead of applying this bug everywhere else.

I know I added aria-checked into toBeChecked, so that will also need to be split out into 2 matchers. I'm happy to dig into this and put in some PRs for that and other matchers, if that's alright?

I know I added aria-checked into toBeChecked, so that will also need to be split out into 2 matchers. I'm happy to dig into this and put in some PRs for that and other matchers, if that's alright?

Sure. It's mostly digging through specifications though I'd imagine. I think the biggest difference is that something like checked has an impact when submitting a form to the server. aria-checked doesn't

just to ask ... would it be un-feasable to modify toBeDisabled to test for the behaviour implied by disabled attr?

i.e. something would be disabled if it were not focusable && not clickable && certain events not firing (e.g. mouseEnter) regardless of the presence of a disabled attr ?

i.e. something would be disabled if it were not focusable && not clickable && certain events not firing (e.g. mouseEnter) regardless of the presence of a disabled attr ?

You would have to find this out by reading specifications and researching if some behavior is defined by specifications or by vendor implementations etc. I'd imagine this would take some time.

Wait wait, I very much agree with what's being said about disabled vs aria-disabled, but I do not think that's reason enough to extrapolate the same issue to toBeChecked with respect to the checked and aria-checked attributes. Yes, the latter does not imply that the thing will be submitted in a form, but that's about it. And for that we have toHaveFormValues which really does strictly follow the form spec. But if we continue down this path, then no matcher will be able to cover both the regular vs the aria version of declaring that something is under some state.

With respect to add a toBeAriaDisabled custom matcher, I'm not sure I agree with doing it. The equivalent check .toHaveAttribute('aria-disabled', 'true') is simple enough to not merit a custom matcher.

I kind of agree with @gnapse. To be honest I'd never use something like toBeDisabled if it only asserts on a single property.

I wasn't aware of toHaveFormValues. Do the docs warn about using toBeChecked for form submission assertions?

So as an attempt to come to a bit of a conclusion (maybe only for myself)

There is a big difference between <button disabled /> and <button aria-disabled="true" /> so your test should reflect that. ... we currently don't mimic a browser with the testing-library

As a user (and fan) of this library & the testing-library ecosystem as a whole, i did think that "mimic a browser" was the intent. I was under the impression (possibly incorrectly) that the philosophy behind the testing-library ecosystem was to encourage tests that mimic what a user would see and do. So following that logic i thought rolling these two together would be a sensible thing.

This conversation has been really useful in showing that this thinking wasn't quite accurate, so instead would it be acceptable to add a new toBeAriaDissabled which would do the same matching as toBeDissabled but also match elements with aria-disabled="true" ?

So following that logic i thought rolling these two together would be a sensible thing.

I seem to have failed communicating that doing that would be the exact opposite. If we would do that someone would need to put time and effort into compiling what .disabled implies in the browser. Just matching the aria-disabled attribute would not be sufficient. I'm not opposed to doing that but I just wanted to make it clear that this isn't something you're done with in 30 minutes or handwave why you added some expectations.

This conversation has been really useful in showing that this thinking wasn't quite accurate, so instead would it be acceptable to add a new toBeAriaDissabled which would do the same matching as toBeDissabled but also match elements with aria-disabled="true" ?

This sounds perfectly fine. This way you can't accidentally introduce (potentially ) breaking changes by switching from disabled to aria-disabled. I think the important part here is documenting that this only asserts what assistive technology announces when reading that element and explain disabled vs aria-dsiabled and focus, events etc.

@eps1lon sorry, I've been reading back right to my initial query, i might have confused matters by not being very clear originally. (if not just ignore this message 😄 ) My proposal was never to remove any of the existing behaviour / matching semantics of the current toBeDisabled, only to add to them so that all the following examples would match:

<input disabled/>
<input aria-disabled="true"/>
<input disabled aria-disabled="true"/>
<div disabled="true"/>
<div aria-disabled="true"/>
<div disabled aria-disabled="true"/>
<form disabled/>
<form aria-disabled="true"/>
<form disabled aria-disabled="true"/>

obviously there would be the potential for ambiguous

<form disabled aria-disabled="false"/>
<input disabled="false" aria-disabled="true"/>
<input disabled aria-disabled="false"/>

which could be detected and throw an error or some description explaining the ambiguity.

this is exactly what id propose goes into a new toBeAriaDissabled

It is not the original topic, but very similar issue. What is the recommendation for .toBeVisible() but for aria-hidden? I have a carousel with copied fake slides (for infinite scrolling) and I use aria-hidden in them.

<!-- DOM -->
<div id="carousel">
  <div class="slide fake-slide" aria-hidden="true">
    <span>Text Slide #1</span>
  </div>
  <div class="slide">
    <span>Text Slide #1</span>
  </div>
  <div class="slide fake-slide" aria-hidden="true">
    <span>Text Slide #1</span>
  </div>
</div>
// test
const slidesDOM = getAllByText(container, 'Text Slide #1')
expect(slidesDOM[0]).not.toBeVisible();
expect(slidesDOM[1]).toBeVisible();
expect(slidesDOM[2]).not.toBeVisible();

See that the slidesDOM array are not the .slide elements, but their span children. The tests rely on their parents being visible/not visible, so I cannot use the .toHaveAttribute('aria-hidden', "true") check.

Even though they are rendered in the dom nodes, they are not visible for most users. They are only visible if they scroll very far in a single swipe, after which they remove the hidden attribute.

I can't use the hidden attribute in them. The parents should be visible in the case of a long swipe, but I can't test that they are hidden for screen readers users.

@andre-matulionis-ifood byRole only include accessible elements by default. That might help

@andre-matulionis-ifood we may need a custom matcher for that because the semantics of aria-hidden are fundamentally different than that of hidden. Can you convert that comment of yours into a new issue?

Hi folks,

Thanks for all your input.

I don't think we're going to do this (adding support for the presence of aria-disabled to toBeDisabled). The semantics are different, as discussed above. We're probably not going to add a toBeAriaDisabled either. The workaround is .toHaveAttribute('aria-disabled', 'true').

Good luck!

The workaround is .toHaveAttribute('aria-disabled', 'true').

Isn't this testing for the explicit attribute? Whereas the proposal above should return true whether it has has aria-disabled or disabled (because the latter implies aria-disabled).

Surely this is exactly the use case for the convenience methods such as .toBeDisabled()? If not, then all the above arguments should have already been applied and .toBeDisabled() would never have been added. If you explicitly want something with the disabled attribute, the check is .toHaveAttribute('disabled'), and this is literally the first example in the docs for that method:
https://github.com/testing-library/jest-dom#tohaveattribute

the proposal above should return true whether it has has aria-disabled or disabled (because the latter implies aria-disabled).

The problem with aria-disabled is that it does not really tell you that the element is disabled. For instance, having a button like this:

<button aria-disabled="true">
  Click me
</button>

That button is still clickable and focusable. You can reach it when tabbing around the page, and click it with the mouse and with the keyboard. So having .toBeDisabled() pass when it has aria-disabled="true" and nothing else is misleading.

then all the above arguments should have already been applied and .toBeDisabled() would never have been added. If you explicitly want something with the disabled attribute, the check is .toHaveAttribute('disabled'),

This is not so simple. The toBeDisabled() custom matcher still does more than that. An element is also disabled if it's inside a parent form or fieldset that is disabled. So even if an element does not have the disabled attribute, the custom matcher may still pass the test.

For instance:

<fieldset disabled="true">
    <button>
      Click me
    </button>
</fieldset>

The test below passes:

const buttonElement = screen.getByRole('button', { name: 'Click me' })
expect(buttonElement).toBeDisabled()

The same cannot be said of aria-disabled, which is not only not really disabling anything, but it is also not inherited down the DOM tree in this way.