jest-dom icon indicating copy to clipboard operation
jest-dom copied to clipboard

Accessibility-related matchers

Open gnapse opened this issue 5 years ago • 10 comments

Describe the feature you'd like:

In the light of projects like Reach UI and react-beautiful-dnd and others like them, that have a focus on accessibility, I was thinking that it could be useful to test that stuff in the Dom have some features that help with accessibility.

I'm not sure though, if it'd be enough to test for aria- attributes with toHaveAttribute, or if there's something else we could do with custom matchers that would help even more. It does not help that I my self am fairly new to being aware and worried about a11y in my apps (shame on me!) but I'm catching up, and wanted to raise the issue here in hope for others to chime in and determine if there could be something jest-dom could do about it.

Suggested implementation:

Not sure yet. Not even sure if it's needed and kind of matchers we would need.

Describe alternatives you've considered:

Not sure of any. If you know of any, let me know in the comments.

Teachability, Documentation, Adoption, Migration Strategy:

TODO

gnapse avatar Sep 04 '18 18:09 gnapse

Prior art: jest-axe 👍

kentcdodds avatar Sep 04 '18 19:09 kentcdodds

This sounds incredible!

@gnapse, since jest-dom is scoped to DOM elements toHaveAttribute does work for quite a bit.

*One prime area that is missing through is anything where it deals with more than one element: like aria-labelledby, aria-describedby, aria-controls!

smacpherson64 avatar Sep 04 '18 19:09 smacpherson64

@gnapse, since jest-dom is scoped to DOM elements toHaveAttribute does work for quite a bit.

Yes, the drawback is that in addition to tests being too verbose, people would need to know what to test for in order to test that a UI is "accessible". I was kinda hoping to provide something akin to .toBeAccessible() 😄 that would take of everything for me, and in the process of creating it, we would learn a lot. And users using it would learn too.

Apparently jest-axe takes care of that, or rather axe, which powers it (didn't know about it either). Taking a loot at it right now. Maybe that's the answer and we do not need to do anything. Though at the very least I'd like to link to the from the README. I'm going to play with it a bit to see how it goes.

gnapse avatar Sep 04 '18 19:09 gnapse

Hahaha, kk, It would be really amazing to have the ability to have an in-depth automated accessibility test that was really accurate and walked you through how to resolve the issues.

I have been using axe (chrome plugin) and Lighthouse (in chrome dev tools audits tab) to find issues and they seem to work really well.

I was really excited to see something like this!

HTML from MDN

/* Assuming below
<div id="dialog" role="dialog" aria-labelledby="dialogheader">
    <h2 id="dialogheader">Choose a File</h2>
    ...Dialog contents
</div>
*/

expect(document.querySelector('#dialog')).toBeLabelled('Choose a File')

smacpherson64 avatar Sep 04 '18 19:09 smacpherson64

That's something, yeah. I wonder if dom-testing-library's getByLabelText gives you the dialog.

Another possibility is to test, for instance, that you're not applying onClick handlers to elements that are not interactive. Like <div onClick={...}> ... </div> without a role="button" in the div.

I assume these are the kinds of things that axe takes care of. Let's see.

gnapse avatar Sep 04 '18 19:09 gnapse

Hrm, the core of axe is really cool! https://github.com/dequelabs/axe-core/blob/develop/lib/rules/label.json#L18-L29

For example if we did make a toBeLabelled matcher it should be able to handle:

Explicit Label

<label for="explicit">Label</label>
<input id="explicit" type="text">

Implicit Label

<label><input id="implicit" type="radio">Label</label>

Labelledby

<span id="labelledby">Label</span>
<input type="text" aria-labelledby="labelledby">

Direct Label

<input type="text" aria-label="Label">

Title

<input type="text" title="Label">

Also the following rules must be true: The explicit label should be visible, there should only be one explicit label, and the help text (E.G.: Title and Label) should not be same exact text.

I am wondering if it makes sense for any of these matchers to directly use axe validation (on a per matcher basis - so axe would be configured per matcher to only look for one thing). So for example toBeLabelled would use axe to make sure rules match valid labels, then do normal matcher actions.

We would need to be careful though about scoping (to a specific set of elements) and JSDOM:

https://github.com/dequelabs/axe-core There is limited support for JSDOM. We will attempt to make all rules compatible with JSDOM but where this is not possible, we recommend turning those rules off. Currently the color-contrast rule is known not to work with JSDOM.

smacpherson64 avatar Sep 11 '18 23:09 smacpherson64

WRT to isAccessible: We now automatically exclude inaccessible elements in @testing-library/dom#getByRole with https://github.com/testing-library/dom-testing-library/blob/dbbea6ee514399d0b37690ce5c56bb21f5ae2cb3/src/role-helpers.js#L17

I think it's not public yet though. I'll start working on a chai matcher and report back how that worked.

eps1lon avatar Sep 24 '19 07:09 eps1lon

Hi,

On a professional project i needed a matcher to check if the form fields have an 'aria' description. I created this:

const matches = (textToMatch: string, matcher: string | RegExp) => {
  if (matcher instanceof RegExp) {
    return matcher.test(textToMatch);
  }

  return textToMatch.includes(matcher);
};

const getFieldName = (formField: HTMLElement) => {
  return formField.id || formField.getAttribute('name') || 'element';
};

export function toHaveDescriptionText(this: jest.MatcherContext, formField: HTMLElement, text: string | RegExp): jest.CustomMatcherResult {
  const ariaDescribedBy: string = formField.getAttribute('aria-describedby') || '';
  const elementIds: string[] = ariaDescribedBy.split(' ').filter(Boolean);
  const hasDescription = elementIds
    .map((id: string) => document.getElementById(id))
    .some((element) => element != null && element.textContent && matches(element.textContent, text));

  return {
    pass: hasDescription,
    message: () => {
      return matcherHint(`${this.isNot ? '.not' : ''}.toHaveDescriptionText`, getFieldName(formField), String(text));
    },
  };
}

Here is an example:

expect(getByLabelText('Account name')).toHaveDescriptionText('This field is required');

Would that be useful to add it into testing-library/jest-dom? i could provide a PR.

Regards

ghostd avatar Apr 22 '20 15:04 ghostd

@ghostd Thanks for offering help. Eventually dom-accessibility-api should get a computeAccessibleDescription following the accname spec. So any effort should go into that package which has proper infra to test that the accessible description is correct.

eps1lon avatar Apr 22 '20 15:04 eps1lon

Thanks for the pointers. I didn't know this specification.

ghostd avatar Apr 22 '20 16:04 ghostd