From 637529e3892611092671f30c135587c130fbf92d Mon Sep 17 00:00:00 2001 From: Nick McCurdy Date: Fri, 23 Apr 2021 07:38:06 -0400 Subject: [PATCH 1/3] chore: continue testing on Node 15 (#360) --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3f0c609f..e6b270ac 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -16,7 +16,7 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [10.14, 12, 14, 16] + node: [10.14, 12, 14, 15, 16] runs-on: ubuntu-latest steps: - name: ⬇️ Checkout repo From c816955ce5101d1ac3ee10b3d9fb649649a055c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Sat, 15 May 2021 13:11:37 -0400 Subject: [PATCH 2/3] doc: Better documentation for toContaintHTML (#363) * doc: Better documentation for toContaintHTML --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94b8a5af..e69ece0f 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ toContainHTML(htmlText: string) ``` Assert whether a string representing a HTML element is contained in another -element: +element. The string should contain valid html, and not any incomplete html. #### Examples @@ -500,7 +500,15 @@ element: ``` ```javascript +// These are valid uses expect(getByTestId('parent')).toContainHTML('') +expect(getByTestId('parent')).toContainHTML('') +expect(getByTestId('parent')).not.toContainHTML('
') + +// These won't work +expect(getByTestId('parent')).toContainHTML('data-testid="child"') +expect(getByTestId('parent')).toContainHTML('data-testid') +expect(getByTestId('parent')).toContainHTML('
') ``` > Chances are you probably do not need to use this matcher. We encourage testing From 217fdcc2377bc24bfdbd2e121289704128048fa9 Mon Sep 17 00:00:00 2001 From: Doma Date: Fri, 4 Jun 2021 01:02:34 +0800 Subject: [PATCH 3/3] feat: Add `toHaveErrorMessage` matcher (#370) * feat: add toHaveErrorMessage matcher * docs: add docs for toHaveErrorMessage update test cases to match example --- README.md | 53 +++++++ src/__tests__/to-have-errormessage.js | 206 ++++++++++++++++++++++++++ src/matchers.js | 2 + src/to-have-errormessage.js | 70 +++++++++ 4 files changed, 331 insertions(+) create mode 100644 src/__tests__/to-have-errormessage.js create mode 100644 src/to-have-errormessage.js diff --git a/README.md b/README.md index e69ece0f..86a2267a 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ clear to read and to maintain. - [`toBeChecked`](#tobechecked) - [`toBePartiallyChecked`](#tobepartiallychecked) - [`toHaveDescription`](#tohavedescription) + - [`toHaveErrorMessage`](#tohaveerrormessage) - [Deprecated matchers](#deprecated-matchers) - [`toBeInTheDOM`](#tobeinthedom) - [Inspiration](#inspiration) @@ -1042,6 +1043,58 @@ expect(deleteButton).not.toHaveDescription() expect(deleteButton).toHaveDescription('') // Missing or empty description always becomes a blank string ``` +### `toHaveErrorMessage` + +```typescript +toHaveErrorMessage(text: string | RegExp) +``` + +This allows you to check whether the given element has an +[ARIA error message](https://www.w3.org/TR/wai-aria/#aria-errormessage) or not. + +Use the `aria-errormessage` attribute to reference another element that contains +custom error message text. Multiple ids is **NOT** allowed. Authors MUST use +`aria-invalid` in conjunction with `aria-errormessage`. Leran more from +[`aria-errormessage` spec](https://www.w3.org/TR/wai-aria/#aria-errormessage). + +Whitespace is normalized. + +When a `string` argument is passed through, it will perform a whole +case-sensitive match to the error message text. + +To perform a case-insensitive match, you can use a `RegExp` with the `/i` +modifier. + +To perform a partial match, you can pass a `RegExp` or use +`expect.stringContaining("partial string")`. + +#### Examples + +```html + + + + Invalid time: the time must be between 9:00 AM and 5:00 PM" + +``` + +```javascript +const timeInput = getByLabel('startTime') + +expect(timeInput).toHaveErrorMessage( + 'Invalid time: the time must be between 9:00 AM and 5:00 PM', +) +expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match +expect(timeInput).toHaveErrorMessage(expect.stringContaining('Invalid time')) // to partially match +expect(timeInput).not.toHaveErrorMessage('Pikachu!') +``` + ## Deprecated matchers ### `toBeInTheDOM` diff --git a/src/__tests__/to-have-errormessage.js b/src/__tests__/to-have-errormessage.js new file mode 100644 index 00000000..68817653 --- /dev/null +++ b/src/__tests__/to-have-errormessage.js @@ -0,0 +1,206 @@ +import {render} from './helpers/test-utils' + +// eslint-disable-next-line max-lines-per-function +describe('.toHaveErrorMessage', () => { + test('resolves for object with correct aria-errormessage reference', () => { + const {queryByTestId} = render(` + + + Invalid time: the time must be between 9:00 AM and 5:00 PM + `) + + const timeInput = queryByTestId('startTime') + + expect(timeInput).toHaveErrorMessage( + 'Invalid time: the time must be between 9:00 AM and 5:00 PM', + ) + expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match + expect(timeInput).toHaveErrorMessage( + expect.stringContaining('Invalid time'), + ) // to partially match + expect(timeInput).not.toHaveErrorMessage('Pikachu!') + }) + + test('works correctly on implicit invalid element', () => { + const {queryByTestId} = render(` + + + Invalid time: the time must be between 9:00 AM and 5:00 PM + `) + + const timeInput = queryByTestId('startTime') + + expect(timeInput).toHaveErrorMessage( + 'Invalid time: the time must be between 9:00 AM and 5:00 PM', + ) + expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match + expect(timeInput).toHaveErrorMessage( + expect.stringContaining('Invalid time'), + ) // to partially match + expect(timeInput).not.toHaveErrorMessage('Pikachu!') + }) + + test('rejects for valid object', () => { + const {queryByTestId} = render(` +
The errormessage
+
+
+ `) + + expect(queryByTestId('valid')).not.toHaveErrorMessage('The errormessage') + expect(() => { + expect(queryByTestId('valid')).toHaveErrorMessage('The errormessage') + }).toThrowError() + + expect(queryByTestId('explicitly_valid')).not.toHaveErrorMessage( + 'The errormessage', + ) + expect(() => { + expect(queryByTestId('explicitly_valid')).toHaveErrorMessage( + 'The errormessage', + ) + }).toThrowError() + }) + + test('rejects for object with incorrect aria-errormessage reference', () => { + const {queryByTestId} = render(` +
The errormessage
+
+ `) + + expect(queryByTestId('invalid_id')).not.toHaveErrorMessage() + expect(queryByTestId('invalid_id')).toHaveErrorMessage('') + }) + + test('handles invalid element without aria-errormessage', () => { + const {queryByTestId} = render(` +
The errormessage
+
+ `) + + expect(queryByTestId('without')).not.toHaveErrorMessage() + expect(queryByTestId('without')).toHaveErrorMessage('') + }) + + test('handles valid element without aria-errormessage', () => { + const {queryByTestId} = render(` +
The errormessage
+
+ `) + + expect(queryByTestId('without')).not.toHaveErrorMessage() + expect(() => { + expect(queryByTestId('without')).toHaveErrorMessage() + }).toThrowError() + + expect(queryByTestId('without')).not.toHaveErrorMessage('') + expect(() => { + expect(queryByTestId('without')).toHaveErrorMessage('') + }).toThrowError() + }) + + test('handles multiple ids', () => { + const {queryByTestId} = render(` +
First errormessage
+
Second errormessage
+
Third errormessage
+
+ `) + + expect(queryByTestId('multiple')).toHaveErrorMessage( + 'First errormessage Second errormessage Third errormessage', + ) + expect(queryByTestId('multiple')).toHaveErrorMessage( + /Second errormessage Third/, + ) + expect(queryByTestId('multiple')).toHaveErrorMessage( + expect.stringContaining('Second errormessage Third'), + ) + expect(queryByTestId('multiple')).toHaveErrorMessage( + expect.stringMatching(/Second errormessage Third/), + ) + expect(queryByTestId('multiple')).not.toHaveErrorMessage('Something else') + expect(queryByTestId('multiple')).not.toHaveErrorMessage('First') + }) + + test('handles negative test cases', () => { + const {queryByTestId} = render(` +
The errormessage
+
+ `) + + expect(() => + expect(queryByTestId('other')).toHaveErrorMessage('The errormessage'), + ).toThrowError() + + expect(() => + expect(queryByTestId('target')).toHaveErrorMessage('Something else'), + ).toThrowError() + + expect(() => + expect(queryByTestId('target')).not.toHaveErrorMessage( + 'The errormessage', + ), + ).toThrowError() + }) + + test('normalizes whitespace', () => { + const {queryByTestId} = render(` +
+ Step + 1 + of + 4 +
+
+ And + extra + errormessage +
+
+ `) + + expect(queryByTestId('target')).toHaveErrorMessage( + 'Step 1 of 4 And extra errormessage', + ) + }) + + test('can handle multiple levels with content spread across decendants', () => { + const {queryByTestId} = render(` + + Step + 1 + of + 4 + +
+ `) + + expect(queryByTestId('target')).toHaveErrorMessage('Step 1 of 4') + }) + + test('handles extra whitespace with multiple ids', () => { + const {queryByTestId} = render(` +
First errormessage
+
Second errormessage
+
Third errormessage
+
+ `) + + expect(queryByTestId('multiple')).toHaveErrorMessage( + 'First errormessage Second errormessage Third errormessage', + ) + }) + + test('is case-sensitive', () => { + const {queryByTestId} = render(` + Sensitive text +
+ `) + + expect(queryByTestId('target')).toHaveErrorMessage('Sensitive text') + expect(queryByTestId('target')).not.toHaveErrorMessage('sensitive text') + }) +}) diff --git a/src/matchers.js b/src/matchers.js index 36896d07..1dbbec77 100644 --- a/src/matchers.js +++ b/src/matchers.js @@ -19,6 +19,7 @@ import {toHaveDisplayValue} from './to-have-display-value' import {toBeChecked} from './to-be-checked' import {toBePartiallyChecked} from './to-be-partially-checked' import {toHaveDescription} from './to-have-description' +import {toHaveErrorMessage} from './to-have-errormessage' export { toBeInTheDOM, @@ -44,4 +45,5 @@ export { toBeChecked, toBePartiallyChecked, toHaveDescription, + toHaveErrorMessage, } diff --git a/src/to-have-errormessage.js b/src/to-have-errormessage.js new file mode 100644 index 00000000..a253b390 --- /dev/null +++ b/src/to-have-errormessage.js @@ -0,0 +1,70 @@ +import {checkHtmlElement, getMessage, normalize} from './utils' + +// See aria-errormessage spec https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage +export function toHaveErrorMessage(htmlElement, checkWith) { + checkHtmlElement(htmlElement, toHaveErrorMessage, this) + + if ( + !htmlElement.hasAttribute('aria-invalid') || + htmlElement.getAttribute('aria-invalid') === 'false' + ) { + const not = this.isNot ? '.not' : '' + + return { + pass: false, + message: () => { + return getMessage( + this, + this.utils.matcherHint(`${not}.toHaveErrorMessage`, 'element', ''), + `Expected the element to have invalid state indicated by`, + 'aria-invalid="true"', + 'Received', + htmlElement.hasAttribute('aria-invalid') + ? `aria-invalid="${htmlElement.getAttribute('aria-invalid')}"` + : this.utils.printReceived(''), + ) + }, + } + } + + const expectsErrorMessage = checkWith !== undefined + + const errormessageIDRaw = htmlElement.getAttribute('aria-errormessage') || '' + const errormessageIDs = errormessageIDRaw.split(/\s+/).filter(Boolean) + + let errormessage = '' + if (errormessageIDs.length > 0) { + const document = htmlElement.ownerDocument + + const errormessageEls = errormessageIDs + .map(errormessageID => document.getElementById(errormessageID)) + .filter(Boolean) + + errormessage = normalize( + errormessageEls.map(el => el.textContent).join(' '), + ) + } + + return { + pass: expectsErrorMessage + ? checkWith instanceof RegExp + ? checkWith.test(errormessage) + : this.equals(errormessage, checkWith) + : Boolean(errormessage), + message: () => { + const to = this.isNot ? 'not to' : 'to' + return getMessage( + this, + this.utils.matcherHint( + `${this.isNot ? '.not' : ''}.toHaveErrorMessage`, + 'element', + '', + ), + `Expected the element ${to} have error message`, + this.utils.printExpected(checkWith), + 'Received', + this.utils.printReceived(errormessage), + ) + }, + } +}