-
Notifications
You must be signed in to change notification settings - Fork 411
Implement .toBeLabelled custom matcher #112
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
Changes from all commits
67029d7
0c04f17
605d798
ea4969c
3eb11ee
ec5dce1
ad22c63
f71d858
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import {render} from './helpers/test-utils' | ||
|
|
||
| test('.toBeLabelled', () => { | ||
| const {queryByTestId} = render(` | ||
| <div> | ||
| <img data-testid="img-alt" src="" alt="Test alt" /> | ||
| <img data-testid="img-label" src="" alt="" aria-label="Test alt" /> | ||
| <img data-testid="img-labelledby" src="" alt="" aria-labelledby="testId" /> | ||
| <img data-testid="img-empty-alt" src="" alt="" /> | ||
| <svg data-testid="svg-title"><title>Test title</title></svg> | ||
| <button><img data-testid="img-text-sibling" src="" alt="" /><span>Test content</span></button> | ||
| <button data-testid="button-img-alt"><img src="" alt="Test" /></button> | ||
| <object data-testid="object" data="companylogo.gif" type="image/gif"><p>Company Name</p></object> | ||
| <p><img data-testid="img-paragraph" src="" alt="" />Test content</p> | ||
| <button data-testid="svg-button"><svg><title>Test</title></svg></p> | ||
| <div><svg data-testid="svg-without-title"></svg></div> | ||
| <input data-testid="input-title" title="test" /> | ||
| </div> | ||
| `) | ||
|
|
||
| expect(queryByTestId('img-alt')).toBeLabelled() | ||
| expect(queryByTestId('img-label')).toBeLabelled() | ||
| expect(queryByTestId('img-labelledby')).toBeLabelled() | ||
| expect(queryByTestId('img-empty-alt')).not.toBeLabelled() | ||
| expect(queryByTestId('svg-title')).toBeLabelled() | ||
| expect(queryByTestId('img-text-sibling')).toBeLabelled() | ||
| expect(queryByTestId('button-img-alt')).toBeLabelled() | ||
| expect(queryByTestId('object')).toBeLabelled() | ||
| expect(queryByTestId('img-paragraph')).not.toBeLabelled() | ||
| expect(queryByTestId('svg-button')).toBeLabelled() | ||
| expect(queryByTestId('svg-without-title')).not.toBeLabelled() | ||
| expect(queryByTestId('input-title')).toBeLabelled() | ||
|
|
||
| expect(() => | ||
| expect(queryByTestId('img-alt')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('img-label')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('img-labelledby')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('img-empty-alt')).toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('svg-title')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('img-text-sibling')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('button-img-alt')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('object')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('img-paragraph')).toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('svg-button')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('svg-without-title')).toBeLabelled(), | ||
| ).toThrowError() | ||
| expect(() => | ||
| expect(queryByTestId('input-title')).not.toBeLabelled(), | ||
| ).toThrowError() | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import {matcherHint, printReceived} from 'jest-matcher-utils' | ||
| import {checkHtmlElement, getTag} from './utils' | ||
|
|
||
| /* | ||
| * Lists all tags accepting alt as an attribute | ||
| * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes | ||
| */ | ||
| const TAGS_WITH_ALT = ['applet', 'area', 'img'] | ||
|
|
||
| const TAGS_WITH_TITLE = ['svg'] | ||
|
|
||
| const TAGS_WITH_TITLE_ATTR = ['input', 'select', 'textarea', 'abbr'] | ||
|
|
||
| const TAGS_IMAGES = ['img', 'svg'] | ||
|
|
||
| const TAGS_WITH_TEXT_CONTENT = ['a', 'button', 'object'] | ||
|
|
||
| /* | ||
| * Checks if the element is labelled by `aria-label` | ||
| * | ||
| * e.g. `<button aria-label="Add new item"></button>` | ||
| * | ||
| * @link https://www.w3.org/TR/WCAG20-TECHS/ARIA6.html | ||
| */ | ||
| function isHavingARIALabel(element) { | ||
| return element.hasAttribute('aria-label') | ||
| } | ||
|
|
||
| /* | ||
| * Checks if the element is labelled by `aria-labelledby` | ||
| * | ||
| * e.g. `<svg aria-labelledby="refId"></svg>` | ||
| * | ||
| * @link https://www.w3.org/TR/WCAG20-TECHS/ARIA10.html | ||
| */ | ||
| function isHavingARIALabelledBy(element) { | ||
| return element.hasAttribute('aria-labelledby') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about this checking if the referred element really exist? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I though about that a bit but I do not know if we should do that all the time. But at the same time, Should this check be an option? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel that this should indeed do the additional check. If users merely wanted to check for the presence of the If a UI library that you're testing has components that receive the render(
<div>
<h1 id="my-component-title">Hello world</h1>
<MyComponent aria-labelledby="my-component-title" />
</div>
)There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This would be really useful - you can add this before doing 'label' queries so that you can tell what is going wrong - the label or the reference. Also ensures the aria relationships don't get stale or broken over time. |
||
| } | ||
|
|
||
| /* | ||
| * Checks if meaningful images are having an alt attribute | ||
| * | ||
| * e.g. `<img src="" alt="Text alternative" />` | ||
| * | ||
| * @link https://www.w3.org/TR/WCAG20-TECHS/H37.html | ||
| */ | ||
| function isHavingMeaningfulAlt(element) { | ||
| return ( | ||
| TAGS_WITH_ALT.includes(getTag(element)) && | ||
| element.hasAttribute('alt') && | ||
| element.getAttribute('alt') !== '' | ||
| ) | ||
| } | ||
|
|
||
| /* | ||
| * Checks if there is a title tag as a child | ||
| * | ||
| * e.g. `<svg><title>Title for this non-text content</title></svg>` | ||
| * | ||
| * @link https://www.w3.org/TR/WCAG20-TECHS/H65.html | ||
| */ | ||
| function isHavingMeaningfulTitle(element) { | ||
| const titleTag = element.querySelector('title') | ||
|
|
||
| return ( | ||
| TAGS_WITH_TITLE.includes(getTag(element)) && | ||
| titleTag !== null && | ||
| titleTag.textContent !== null | ||
| ) | ||
| } | ||
|
|
||
| /* | ||
| * Checks if there is a title attribute | ||
| * | ||
| * e.g. `<abbr title="United Nations">UN</abbr>` | ||
| */ | ||
| function isHavingMeaningfulTitleAttribute(element) { | ||
| return ( | ||
| TAGS_WITH_TITLE_ATTR.includes(getTag(element)) && | ||
| element.hasAttribute('title') && | ||
| element.getAttribute('title') !== null | ||
| ) | ||
| } | ||
|
|
||
| /* | ||
| * Checks if there is an adjacent image and text | ||
| * | ||
| * e.g. `<button><img src="" alt="">Add new item</button>` | ||
| * | ||
| * @link https://www.w3.org/TR/WCAG20-TECHS/G95.html | ||
hiwelo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| function isHavingASiblingLabel(element) { | ||
| return ( | ||
| TAGS_IMAGES.includes(getTag(element)) && | ||
| TAGS_WITH_TEXT_CONTENT.includes(getTag(element.parentElement)) && | ||
| element.parentElement.textContent !== '' | ||
| ) | ||
| } | ||
|
|
||
| /* | ||
| * Checks if the image is having a meaningful content with ARIA, an alt attribute or a title attribute | ||
| * | ||
| * @link https://www.w3.org/TR/WCAG20-TECHS/H2.html | ||
hiwelo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| function isHavingAContentText(element) { | ||
| const isHavingLabelledImg = | ||
| element.querySelector('img') && | ||
| (isHavingARIALabel(element.querySelector('img')) || | ||
| isHavingARIALabelledBy(element.querySelector('img')) || | ||
| isHavingMeaningfulAlt(element.querySelector('img'))) | ||
|
|
||
| const isHavingLabelledSVG = | ||
| element.querySelector('svg') && | ||
| (isHavingARIALabel(element.querySelector('svg')) || | ||
| isHavingARIALabelledBy(element.querySelector('svg')) || | ||
| isHavingMeaningfulTitle(element.querySelector('svg'))) | ||
|
|
||
| return ( | ||
| TAGS_WITH_TEXT_CONTENT.includes(getTag(element)) && | ||
| (element.textContent !== '' || isHavingLabelledImg || isHavingLabelledSVG) | ||
| ) | ||
| } | ||
|
|
||
| export function toBeLabelled(element) { | ||
gnapse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| checkHtmlElement(element, toBeLabelled, this) | ||
|
|
||
| const isLabelled = | ||
| isHavingARIALabel(element) || | ||
| isHavingARIALabelledBy(element) || | ||
| isHavingMeaningfulAlt(element) || | ||
| isHavingMeaningfulTitle(element) || | ||
| isHavingMeaningfulTitleAttribute(element) || | ||
| isHavingASiblingLabel(element) || | ||
| isHavingAContentText(element) | ||
|
|
||
| return { | ||
| pass: isLabelled, | ||
| message: () => { | ||
| const is = isLabelled ? 'is' : 'is not' | ||
| return [ | ||
| matcherHint(`${this.isNot ? '.not' : ''}.toBeLabelled`, 'element', ''), | ||
| '', | ||
| `Received element ${is} labelled:`, | ||
| ` ${printReceived(element.cloneNode(false))}`, | ||
| ].join('\n') | ||
| }, | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.