diff --git a/package.json b/package.json index a05a491a..a4d0c662 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "@babel/runtime": "^7.4.5", "@sheerun/mutationobserver-shim": "^0.3.2", + "aria-query": "3.0.0", "pretty-format": "^24.8.0", "wait-for-expect": "^1.2.0" }, diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 561c0006..d1c41bd2 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -374,6 +374,61 @@ describe('query by test id', () => { }) }) +test('queryAllByRole returns semantic html elements', () => { + const {queryAllByRole} = render(` +
+

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5
+
Heading 6
+
    +
  1. +
  2. +
+ + + + + + + + + + + + + + + + +
+
+ +
+ `) + + expect(queryAllByRole(/table/i)).toHaveLength(1) + expect(queryAllByRole(/tabl/i, {exact: false})).toHaveLength(1) + expect(queryAllByRole(/columnheader/i)).toHaveLength(1) + expect(queryAllByRole(/rowheader/i)).toHaveLength(1) + expect(queryAllByRole(/grid/i)).toHaveLength(1) + expect(queryAllByRole(/form/i)).toHaveLength(1) + expect(queryAllByRole(/button/i)).toHaveLength(1) + expect(queryAllByRole(/heading/i)).toHaveLength(6) + expect(queryAllByRole('list')).toHaveLength(2) + expect(queryAllByRole(/listitem/i)).toHaveLength(3) + expect(queryAllByRole(/textbox/i)).toHaveLength(2) + expect(queryAllByRole(/checkbox/i)).toHaveLength(1) + expect(queryAllByRole(/radio/i)).toHaveLength(1) + expect(queryAllByRole('row')).toHaveLength(3) + expect(queryAllByRole(/rowgroup/i)).toHaveLength(2) + expect(queryAllByRole(/(table)|(textbox)/i)).toHaveLength(3) +}) + test('getAll* matchers return an array', () => { const { getAllByAltText, diff --git a/src/queries/role.js b/src/queries/role.js index c493ea92..62f0dde6 100644 --- a/src/queries/role.js +++ b/src/queries/role.js @@ -1,6 +1,74 @@ -import {queryAllByAttribute, buildQueries} from './all-utils' +import {buildQueries, fuzzyMatches, makeNormalizer, matches} from './all-utils' +import {elementRoles} from 'aria-query' -const queryAllByRole = queryAllByAttribute.bind(null, 'role') +function buildElementRoleList(elementRolesMap) { + function makeElementSelector({name, attributes = []}) { + return `${name}${attributes + .map(({name: attributeName, value}) => `[${attributeName}=${value}]`) + .join('')}` + } + + function getSelectorSpecificity({attributes = []}) { + return attributes.length + } + + function bySelectorSpecificity( + {specificity: leftSpecificity}, + {specificity: rightSpecificity}, + ) { + return rightSpecificity - leftSpecificity + } + + let result = [] + + for (const [element, roles] of elementRolesMap.entries()) { + result = [ + ...result, + { + selector: makeElementSelector(element), + roles: Array.from(roles), + specificity: getSelectorSpecificity(element), + }, + ] + } + + return result.sort(bySelectorSpecificity) +} + +const elementRoleList = buildElementRoleList(elementRoles) + +function queryAllByRole( + container, + role, + {exact = true, collapseWhitespace, trim, normalizer} = {}, +) { + const matcher = exact ? matches : fuzzyMatches + const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) + + function getImplicitAriaRole(currentNode) { + for (const {selector, roles} of elementRoleList) { + if (currentNode.matches(selector)) { + return [...roles] + } + } + + return [] + } + + return Array.from(container.querySelectorAll('*')).filter(node => { + const isRoleSpecifiedExplicitly = node.hasAttribute('role') + + if (isRoleSpecifiedExplicitly) { + return matcher(node.getAttribute('role'), node, role, matchNormalizer) + } + + const implicitRoles = getImplicitAriaRole(node) + + return implicitRoles.some(implicitRole => + matcher(implicitRole, node, role, matchNormalizer), + ) + }) +} const getMultipleError = (c, id) => `Found multiple elements by [role=${id}]` const getMissingError = (c, id) => `Unable to find an element by [role=${id}]`