Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conform toHaveErrorMessage to Spec and Rename to toHaveAccessibleErrorMessage #503

Merged
merged 2 commits into from Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Expand Up @@ -64,6 +64,7 @@ clear to read and to maintain.
- [`toContainElement`](#tocontainelement)
- [`toContainHTML`](#tocontainhtml)
- [`toHaveAccessibleDescription`](#tohaveaccessibledescription)
- [`toHaveAccessibleErrorMessage`](#tohaveaccessibleerrormessage)
- [`toHaveAccessibleName`](#tohaveaccessiblename)
- [`toHaveAttribute`](#tohaveattribute)
- [`toHaveClass`](#tohaveclass)
Expand Down Expand Up @@ -561,6 +562,63 @@ expect(getByTestId('logo')).toHaveAccessibleDescription(

<hr />

### `toHaveAccessibleErrorMessage`

```typescript
toHaveAccessibleErrorMessage(expectedAccessibleErrorMessage?: string | RegExp)
```

This allows you to assert that an element has the expected
[accessible error message](https://w3c.github.io/aria/#aria-errormessage).

You can pass the exact string of the expected accessible error message.
Alternatively, you can perform a partial match by passing a regular expression
or by using
[expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring)/[expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp).

#### Examples

```html
<input
aria-label="Has Error"
aria-invalid="true"
aria-errormessage="error-message"
/>
<div id="error-message" role="alert">This field is invalid</div>

<input aria-label="No Error Attributes" />
<input
aria-label="Not Invalid"
aria-invalid="false"
aria-errormessage="error-message"
/>
```

```js
// Inputs with Valid Error Messages
expect(getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage()
expect(getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage(
'This field is invalid',
)
expect(getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage(
/invalid/i,
)
expect(
getByRole('textbox', {name: 'Has Error'}),
).not.toHaveAccessibleErrorMessage('This field is absolutely correct!')

// Inputs without Valid Error Messages
expect(
getByRole('textbox', {name: 'No Error Attributes'}),
).not.toHaveAccessibleErrorMessage()

expect(
getByRole('textbox', {name: 'Not Invalid'}),
).not.toHaveAccessibleErrorMessage()
```

<hr />

### `toHaveAccessibleName`

```typescript
Expand Down Expand Up @@ -1069,6 +1127,10 @@ expect(inputCheckboxIndeterminate).toBePartiallyChecked()

### `toHaveErrorMessage`

> This custom matcher is deprecated. Prefer
> [`toHaveAccessibleErrorMessage`](#tohaveaccessibleerrormessage) instead, which
> is more comprehensive in implementing the official spec.

```typescript
toHaveErrorMessage(text: string | RegExp)
```
Expand Down
277 changes: 277 additions & 0 deletions src/__tests__/to-have-accessible-errormessage.js
@@ -0,0 +1,277 @@
import {render} from './helpers/test-utils'

describe('.toHaveAccessibleErrorMessage', () => {
const input = 'input'
const errorId = 'error-id'
const error = 'This field is invalid'
const strings = {true: String(true), false: String(false)}

describe('Positive Test Cases', () => {
it("Fails the test if an invalid `id` is provided for the target element's `aria-errormessage`", () => {
const secondId = 'id2'
const secondError = 'LISTEN TO ME!!!'

const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId} ${secondId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>
<div data-testid="${secondId}" id="${secondId}" role="alert">${secondError}</div>
</div>
`)

const field = queryByTestId('input')
expect(() => expect(field).toHaveAccessibleErrorMessage())
.toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element's \`aria-errormessage\` attribute to be empty or a single, valid ID:

Received:
<red> aria-errormessage="error-id id2"</>
`)

// Assume the remaining error messages are the EXACT same as above
expect(() =>
expect(field).toHaveAccessibleErrorMessage(new RegExp(error[0])),
).toThrow()

expect(() => expect(field).toHaveAccessibleErrorMessage(error)).toThrow()
expect(() =>
expect(field).toHaveAccessibleErrorMessage(secondError),
).toThrow()

expect(() =>
expect(field).toHaveAccessibleErrorMessage(new RegExp(secondError[0])),
).toThrow()
})

it('Fails the test if the target element is valid according to the WAI-ARIA spec', () => {
const noAriaInvalidAttribute = 'no-aria-invalid-attribute'
const validFieldState = 'false'
const invalidFieldStates = [
'true',
'',
'grammar',
'spelling',
'asfdafbasdfasa',
]

function renderFieldWithState(state) {
return render(`
<div>
<${input} data-testid="${input}" aria-invalid="${state}" aria-errormessage="${errorId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>

<input data-testid="${noAriaInvalidAttribute}" aria-errormessage="${errorId}" />
</div>
`)
}

// Success Cases
invalidFieldStates.forEach(invalidState => {
const {queryByTestId} = renderFieldWithState(invalidState)
const field = queryByTestId('input')

expect(field).toHaveAccessibleErrorMessage()
expect(field).toHaveAccessibleErrorMessage(error)
})

// Failure Case
const {queryByTestId} = renderFieldWithState(validFieldState)
const field = queryByTestId('input')
const fieldWithoutAttribute = queryByTestId(noAriaInvalidAttribute)

expect(() => expect(fieldWithoutAttribute).toHaveAccessibleErrorMessage())
.toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element to be marked as invalid with attribute:
<green> aria-invalid="true"</>
Received:
<red> null</>
`)

expect(() => expect(field).toHaveAccessibleErrorMessage())
.toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element to be marked as invalid with attribute:
<green> aria-invalid="true"</>
Received:
<red> aria-invalid="false</>
`)

// Assume the remaining error messages are the EXACT same as above
expect(() => expect(field).toHaveAccessibleErrorMessage(error)).toThrow()
expect(() =>
expect(field).toHaveAccessibleErrorMessage(new RegExp(error, 'i')),
).toThrow()
})

it('Passes the test if the target element has ANY recognized, non-empty error message', () => {
const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>
</div>
`)

const field = queryByTestId(input)
expect(field).toHaveAccessibleErrorMessage()
})

it('Fails the test if NO recognized, non-empty error message was found for the target element', () => {
const empty = 'empty'
const emptyErrorId = 'empty-error'
const missing = 'missing'

const {queryByTestId} = render(`
<div>
<input data-testid="${empty}" aria-invalid="${strings.true}" aria-errormessage="${emptyErrorId}" />
<div data-testid="${emptyErrorId}" id="${emptyErrorId}" role="alert"></div>

<input data-testid="${missing}" aria-invalid="${strings.true}" aria-errormessage="${missing}-error" />
</div>
`)

const fieldWithEmptyError = queryByTestId(empty)
const fieldMissingError = queryByTestId(missing)

expect(() => expect(fieldWithEmptyError).toHaveAccessibleErrorMessage())
.toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element to have accessible error message:

Received:

`)

expect(() => expect(fieldMissingError).toHaveAccessibleErrorMessage())
.toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element to have accessible error message:

Received:

`)
})

it('Passes the test if the target element has the error message that was SPECIFIED', () => {
const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>
</div>
`)

const field = queryByTestId(input)
const halfOfError = error.slice(0, Math.floor(error.length * 0.5))

expect(field).toHaveAccessibleErrorMessage(error)
expect(field).toHaveAccessibleErrorMessage(new RegExp(halfOfError), 'i')
expect(field).toHaveAccessibleErrorMessage(
expect.stringContaining(halfOfError),
)
expect(field).toHaveAccessibleErrorMessage(
expect.stringMatching(new RegExp(halfOfError), 'i'),
)
})

it('Fails the test if the target element DOES NOT have the error message that was SPECIFIED', () => {
const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>
</div>
`)

const field = queryByTestId(input)
const msg = 'asdflkje2984fguyvb bnafdsasfa;lj'

expect(() => expect(field).toHaveAccessibleErrorMessage(''))
.toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element to have accessible error message:

Received:
<red> This field is invalid</>
`)

// Assume this error is SIMILAR to the error above
expect(() => expect(field).toHaveAccessibleErrorMessage(msg)).toThrow()
expect(() =>
expect(field).toHaveAccessibleErrorMessage(
error.slice(0, Math.floor(error.length * 0.5)),
),
).toThrow()

expect(() =>
expect(field).toHaveAccessibleErrorMessage(new RegExp(msg), 'i'),
).toThrowErrorMatchingInlineSnapshot(`
<dim>expect(</><red>element</><dim>).toHaveAccessibleErrorMessage(</><green>expected</><dim>)</>

Expected element to have accessible error message:
<green> /asdflkje2984fguyvb bnafdsasfa;lj/</>
Received:
<red> This field is invalid</>
`)
})

it('Normalizes the whitespace of the received error message', () => {
const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">
Step
1
of
9000
</div>
</div>
`)

const field = queryByTestId(input)
expect(field).toHaveAccessibleErrorMessage('Step 1 of 9000')
})
})

// These tests for the `.not` use cases will help us cover our bases and complete test coverage
describe('Negated Test Cases', () => {
it("Passes the test if an invalid `id` is provided for the target element's `aria-errormessage`", () => {
const secondId = 'id2'
const secondError = 'LISTEN TO ME!!!'

const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId} ${secondId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>
<div data-testid="${secondId}" id="${secondId}" role="alert">${secondError}</div>
</div>
`)

const field = queryByTestId('input')
expect(field).not.toHaveAccessibleErrorMessage()
expect(field).not.toHaveAccessibleErrorMessage(error)
expect(field).not.toHaveAccessibleErrorMessage(new RegExp(error[0]))
expect(field).not.toHaveAccessibleErrorMessage(secondError)
expect(field).not.toHaveAccessibleErrorMessage(new RegExp(secondError[0]))
})

it('Passes the test if the target element is valid according to the WAI-ARIA spec', () => {
const {queryByTestId} = render(`
<div>
<${input} data-testid="${input}" aria-errormessage="${errorId}" />
<div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div>
</div>
`)

const field = queryByTestId(input)
expect(field).not.toHaveAccessibleErrorMessage()
expect(field).not.toHaveAccessibleErrorMessage(error)
expect(field).not.toHaveAccessibleErrorMessage(new RegExp(error[0]))
})
})
})