From e8193d8d08f36e8d03a79bf6339920479e608752 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Fri, 29 Nov 2019 13:01:36 +0100 Subject: [PATCH 01/10] feat(byRole): Add `name` filter --- package.json | 1 + src/__tests__/role.js | 70 ++++++++++++++++++++++++++++++++++++++++++- src/queries/role.js | 18 +++++++++-- src/role-helpers.js | 10 +++++-- 4 files changed, 93 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 55fd2640..1965470e 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@sheerun/mutationobserver-shim": "^0.3.2", "@types/testing-library__dom": "^6.0.0", "aria-query": "3.0.0", + "dom-accessibility-api": "^0.2.0", "pretty-format": "^24.9.0", "wait-for-expect": "^3.0.0" }, diff --git a/src/__tests__/role.js b/src/__tests__/role.js index 565fe605..e4f48e1c 100644 --- a/src/__tests__/role.js +++ b/src/__tests__/role.js @@ -1,5 +1,6 @@ import {configure, getConfig} from '../config' -import {render} from './helpers/test-utils' +import {render, renderIntoDocument} from './helpers/test-utils' +import {getQueriesForElement} from '../get-queries-for-element' test('by default logs accessible roles when it fails', () => { const {getByRole} = render(`

Hi

`) @@ -183,6 +184,73 @@ test('can include inaccessible roles', () => { expect(getByRole('list', {hidden: true})).not.toBeNull() }) +test('can be filtered by accessible name', () => { + const {getByRole} = renderIntoDocument( + ` +
+

Order

+

Delivery Adress

+
+ + +
+

Invoice Adress

+
+ + +
+
`, + ) + + const deliveryForm = getByRole('form', {name: 'Delivery Adress'}) + expect(deliveryForm).not.toBeNull() + + expect( + // TODO: upstream bug in `aria-query`; should be `button` role + getQueriesForElement(deliveryForm).getByRole('textbox', {name: 'Submit'}), + ).not.toBeNull() + + const invoiceForm = getByRole('form', {name: 'Delivery Adress'}) + expect(invoiceForm).not.toBeNull() + + expect( + getQueriesForElement(invoiceForm).getByRole('textbox', {name: 'Street'}), + ).not.toBeNull() +}) + +test('includes accesible names in error message', () => { + const {getByRole} = render(`

Sign up

`) + + expect(() => getByRole('heading', {name: 'Sign Up'})) + .toThrowErrorMatchingInlineSnapshot(` +"Unable to find an accessible element with the role "heading" and name "Sign Up" + +Here are the accessible roles: + + heading: + + Name "Sign up": +

+ + -------------------------------------------------- + +
+

+ Sign + + up + +

+
" +`) +}) + describe('configuration', () => { let originalConfig beforeEach(() => { diff --git a/src/queries/role.js b/src/queries/role.js index 55e50e10..4026454d 100644 --- a/src/queries/role.js +++ b/src/queries/role.js @@ -1,3 +1,4 @@ +import {computeAccessibleName} from 'dom-accessibility-api' import { getImplicitAriaRoles, prettyRoles, @@ -19,6 +20,7 @@ function queryAllByRole( exact = true, collapseWhitespace, hidden = getConfig().defaultHidden, + name, trim, normalizer, queryFallbacks = false, @@ -70,6 +72,11 @@ function queryAllByRole( }) === false : true }) + .filter(element => { + return typeof name === 'string' + ? computeAccessibleName(element) === name + : true + }) } const getMultipleError = (c, role) => @@ -78,9 +85,12 @@ const getMultipleError = (c, role) => const getMissingError = ( container, role, - {hidden = getConfig().defaultHidden} = {}, + {hidden = getConfig().defaultHidden, name} = {}, ) => { - const roles = prettyRoles(container, {hidden}) + const roles = prettyRoles(container, { + hidden, + includeName: typeof name === 'string', + }) let roleMessage if (roles.length === 0) { @@ -103,7 +113,9 @@ Here are the ${hidden === false ? 'accessible' : 'available'} roles: return ` Unable to find an ${ hidden === false ? 'accessible ' : '' - }element with the role "${role}" + }element with the role "${role}"${ + typeof name === 'string' ? ` and name "${name}"` : '' + } ${roleMessage}`.trim() } diff --git a/src/role-helpers.js b/src/role-helpers.js index 6218461d..592567fb 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -1,5 +1,6 @@ import {elementRoles} from 'aria-query' import {prettyDOM} from './pretty-dom' +import {computeAccessibleName} from 'dom-accessibility-api' const elementRoleList = buildElementRoleList(elementRoles) @@ -138,14 +139,19 @@ function getRoles(container, {hidden = false} = {}) { }, {}) } -function prettyRoles(dom, {hidden}) { +function prettyRoles(dom, {hidden, includeName = false}) { const roles = getRoles(dom, {hidden}) return Object.entries(roles) .map(([role, elements]) => { const delimiterBar = '-'.repeat(50) const elementsString = elements - .map(el => prettyDOM(el.cloneNode(false))) + .map( + el => + `${ + includeName === true ? `Name "${computeAccessibleName(el)}":\n` : '' + }${prettyDOM(el.cloneNode(false))}`, + ) .join('\n\n') return `${role}:\n\n${elementsString}\n\n${delimiterBar}` From a70ed309bc612e60a3482c1834fb03bdb70a5912 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Jan 2020 11:34:00 +0100 Subject: [PATCH 02/10] Use common place to define defaults --- src/role-helpers.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/role-helpers.js b/src/role-helpers.js index 592567fb..f56abda9 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -139,7 +139,7 @@ function getRoles(container, {hidden = false} = {}) { }, {}) } -function prettyRoles(dom, {hidden, includeName = false}) { +function prettyRoles(dom, {hidden, includeName}) { const roles = getRoles(dom, {hidden}) return Object.entries(roles) @@ -149,7 +149,9 @@ function prettyRoles(dom, {hidden, includeName = false}) { .map( el => `${ - includeName === true ? `Name "${computeAccessibleName(el)}":\n` : '' + includeName === true + ? `Name "${computeAccessibleName(el)}":\n` + : '' }${prettyDOM(el.cloneNode(false))}`, ) .join('\n\n') @@ -159,8 +161,8 @@ function prettyRoles(dom, {hidden, includeName = false}) { .join('\n') } -const logRoles = (dom, {hidden = false} = {}) => - console.log(prettyRoles(dom, {hidden})) +const logRoles = (dom, {hidden = false, includeName = false} = {}) => + console.log(prettyRoles(dom, {hidden, includeName})) export { getRoles, From 5c41d96c56b640aa8a15bbaafc266c70cb1b9bfa Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Jan 2020 12:11:21 +0100 Subject: [PATCH 03/10] feat(byRole): Implement TextMatch for `name` option --- src/__tests__/role.js | 65 +++++++++++++++++++++++++++++++++++++++++++ src/queries/role.js | 31 ++++++++++++++++----- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/__tests__/role.js b/src/__tests__/role.js index e4f48e1c..f777872a 100644 --- a/src/__tests__/role.js +++ b/src/__tests__/role.js @@ -224,6 +224,25 @@ test('can be filtered by accessible name', () => { ).not.toBeNull() }) +test('accessible name filter implements TextMatch', () => { + const {getByRole} = render( + `

Sign up

Details

Your Signature

`, + ) + + // subset via regex + expect(getByRole('heading', {name: /gn u/})).not.toBeNull() + // regex + expect(getByRole('heading', {name: /^sign/i})).not.toBeNull() + // function + expect( + getByRole('heading', { + name: (name, element) => { + return element.nodeName === 'H2' && name === 'Your Signature' + }, + }), + ).not.toBeNull() +}) + test('includes accesible names in error message', () => { const {getByRole} = render(`

Sign up

`) @@ -231,6 +250,52 @@ test('includes accesible names in error message', () => { .toThrowErrorMatchingInlineSnapshot(` "Unable to find an accessible element with the role "heading" and name "Sign Up" +Here are the accessible roles: + + heading: + + Name "Sign up": +

+ + -------------------------------------------------- + +
+

+ Sign + + up + +

+
" +`) + + expect(() => getByRole('heading', {name: /Login/})) + .toThrowErrorMatchingInlineSnapshot(` +"Unable to find an accessible element with the role "heading" and name \`/Login/\` + +Here are the accessible roles: + + heading: + + Name "Sign up": +

+ + -------------------------------------------------- + +
+

+ Sign + + up + +

+
" +`) + + expect(() => getByRole('heading', {name: () => false})) + .toThrowErrorMatchingInlineSnapshot(` +"Unable to find an accessible element with the role "heading" and name \`() => false\` + Here are the accessible roles: heading: diff --git a/src/queries/role.js b/src/queries/role.js index 4026454d..3d93a48b 100644 --- a/src/queries/role.js +++ b/src/queries/role.js @@ -73,9 +73,19 @@ function queryAllByRole( : true }) .filter(element => { - return typeof name === 'string' - ? computeAccessibleName(element) === name - : true + if (name === undefined) { + // Don't care + return true + } + + const accessibleName = computeAccessibleName(element) + if (typeof name === 'string') { + return name === accessibleName + } + if (typeof name === 'function') { + return name(accessibleName, element) + } + return name.test(accessibleName) }) } @@ -89,7 +99,7 @@ const getMissingError = ( ) => { const roles = prettyRoles(container, { hidden, - includeName: typeof name === 'string', + includeName: name !== undefined, }) let roleMessage @@ -110,12 +120,19 @@ Here are the ${hidden === false ? 'accessible' : 'available'} roles: `.trim() } + let nameHint = '' + if (name === undefined) { + nameHint = '' + } else if (typeof name === 'string') { + nameHint = ` and name "${name}"` + } else { + nameHint = ` and name \`${name}\`` + } + return ` Unable to find an ${ hidden === false ? 'accessible ' : '' - }element with the role "${role}"${ - typeof name === 'string' ? ` and name "${name}"` : '' - } + }element with the role "${role}"${nameHint} ${roleMessage}`.trim() } From 225887a05431cbb14b0194e4346525fde5a59110 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 27 Jan 2020 12:15:15 +0100 Subject: [PATCH 04/10] chore(lint): fix warnings --- src/__tests__/role.js | 2 +- src/role-helpers.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/role.js b/src/__tests__/role.js index f777872a..446c9bbc 100644 --- a/src/__tests__/role.js +++ b/src/__tests__/role.js @@ -1,6 +1,6 @@ import {configure, getConfig} from '../config' -import {render, renderIntoDocument} from './helpers/test-utils' import {getQueriesForElement} from '../get-queries-for-element' +import {render, renderIntoDocument} from './helpers/test-utils' test('by default logs accessible roles when it fails', () => { const {getByRole} = render(`

Hi

`) diff --git a/src/role-helpers.js b/src/role-helpers.js index f56abda9..ab21bb26 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -1,6 +1,6 @@ import {elementRoles} from 'aria-query' -import {prettyDOM} from './pretty-dom' import {computeAccessibleName} from 'dom-accessibility-api' +import {prettyDOM} from './pretty-dom' const elementRoleList = buildElementRoleList(elementRoles) From 608fd2f603d4ecca859e93d5855139d8a1014be4 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 1 Feb 2020 21:44:58 +0100 Subject: [PATCH 05/10] Final dap review --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1965470e..5cadd2b5 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@sheerun/mutationobserver-shim": "^0.3.2", "@types/testing-library__dom": "^6.0.0", "aria-query": "3.0.0", - "dom-accessibility-api": "^0.2.0", + "dom-accessibility-api": "^0.3.0", "pretty-format": "^24.9.0", "wait-for-expect": "^3.0.0" }, From 3e683d713f6f7aff0c53c3c4f650ee9033c5896d Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 4 Feb 2020 18:05:01 +0100 Subject: [PATCH 06/10] describe test better --- src/__tests__/role.js | 46 ++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/__tests__/role.js b/src/__tests__/role.js index 446c9bbc..33ec4c6f 100644 --- a/src/__tests__/role.js +++ b/src/__tests__/role.js @@ -224,28 +224,11 @@ test('can be filtered by accessible name', () => { ).not.toBeNull() }) -test('accessible name filter implements TextMatch', () => { - const {getByRole} = render( - `

Sign up

Details

Your Signature

`, - ) - - // subset via regex - expect(getByRole('heading', {name: /gn u/})).not.toBeNull() - // regex - expect(getByRole('heading', {name: /^sign/i})).not.toBeNull() - // function - expect( - getByRole('heading', { - name: (name, element) => { - return element.nodeName === 'H2' && name === 'Your Signature' - }, - }), - ).not.toBeNull() -}) - -test('includes accesible names in error message', () => { +test('accessible name comparison is case sensitive', () => { const {getByRole} = render(`

Sign up

`) + // actual: "Sign up", + // queried: "Sign Up" expect(() => getByRole('heading', {name: 'Sign Up'})) .toThrowErrorMatchingInlineSnapshot(` "Unable to find an accessible element with the role "heading" and name "Sign Up" @@ -268,6 +251,29 @@ Here are the accessible roles:

" `) +}) + +test('accessible name filter implements TextMatch', () => { + const {getByRole} = render( + `

Sign up

Details

Your Signature

`, + ) + + // subset via regex + expect(getByRole('heading', {name: /gn u/})).not.toBeNull() + // regex + expect(getByRole('heading', {name: /^sign/i})).not.toBeNull() + // function + expect( + getByRole('heading', { + name: (name, element) => { + return element.nodeName === 'H2' && name === 'Your Signature' + }, + }), + ).not.toBeNull() +}) + +test('TextMatch serialization in error message', () => { + const {getByRole} = render(`

Sign up

`) expect(() => getByRole('heading', {name: /Login/})) .toThrowErrorMatchingInlineSnapshot(` From cfce8d9bde0c845c82c629747559ac1dfc008e2d Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 4 Feb 2020 18:09:54 +0100 Subject: [PATCH 07/10] Reuse matches in name filter --- src/queries/role.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/queries/role.js b/src/queries/role.js index 3d93a48b..a442bfd9 100644 --- a/src/queries/role.js +++ b/src/queries/role.js @@ -78,14 +78,12 @@ function queryAllByRole( return true } - const accessibleName = computeAccessibleName(element) - if (typeof name === 'string') { - return name === accessibleName - } - if (typeof name === 'function') { - return name(accessibleName, element) - } - return name.test(accessibleName) + return matches( + computeAccessibleName(element), + element, + name, + text => text, + ) }) } From d6ac51850160a4f5b03e0d329a2da39e606e0621 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 4 Feb 2020 21:28:17 +0100 Subject: [PATCH 08/10] Include accname by default in logRoles --- .../__snapshots__/role-helpers.js.snap | 29 +++++++++++++++++++ src/role-helpers.js | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/__tests__/__snapshots__/role-helpers.js.snap b/src/__tests__/__snapshots__/role-helpers.js.snap index ef5746b2..2e5ce3d7 100644 --- a/src/__tests__/__snapshots__/role-helpers.js.snap +++ b/src/__tests__/__snapshots__/role-helpers.js.snap @@ -3,6 +3,7 @@ exports[`logRoles calls console.log with output from prettyRoles 1`] = ` "region: +Name "":
@@ -10,6 +11,7 @@ exports[`logRoles calls console.log with output from prettyRoles 1`] = ` -------------------------------------------------- link: +Name "link": @@ -25,14 +28,17 @@ navigation: -------------------------------------------------- heading: +Name "Main Heading":

+Name "Sub Heading":

+Name "Tertiary Heading":

@@ -40,6 +46,7 @@ heading: -------------------------------------------------- article: +Name "":
@@ -47,10 +54,12 @@ article: -------------------------------------------------- command: +Name "": +Name "": @@ -58,10 +67,12 @@ command: -------------------------------------------------- menuitem: +Name "": +Name "": @@ -69,10 +80,12 @@ menuitem: -------------------------------------------------- list: +Name "":
    +Name "":
      @@ -80,18 +93,22 @@ list: -------------------------------------------------- listitem: +Name "":
    • +Name "":
    • +Name "":
    • +Name "":
    • @@ -99,6 +116,7 @@ listitem: -------------------------------------------------- table: +Name "": @@ -106,6 +124,7 @@ table: -------------------------------------------------- rowgroup: +Name "": @@ -113,6 +132,7 @@ rowgroup: -------------------------------------------------- row: +Name "Cell 1 Cell 2 Cell 3": @@ -120,14 +140,17 @@ row: -------------------------------------------------- cell: +Name "Cell 1":
      +Name "Cell 2": +Name "Cell 3": @@ -135,6 +158,7 @@ cell: -------------------------------------------------- form: +Name "":
      @@ -142,11 +166,13 @@ form: -------------------------------------------------- radio: +Name "": +Name "": +Name "": +Name "":