two`,
},
getByTestId: {
query: /his/,
@@ -87,8 +87,8 @@ cases(
html: `
two`,
},
queryByTestId: {
query: /his/,
diff --git a/src/__tests__/role.js b/src/__tests__/role.js
index c709d82e..63872cda 100644
--- a/src/__tests__/role.js
+++ b/src/__tests__/role.js
@@ -274,6 +274,7 @@ test('accessible name filter implements TextMatch', () => {
expect(
getByRole('heading', {
name: (name, element) => {
+ // eslint-disable-next-line jest/no-conditional-in-test -- false-positive
return element.nodeName === 'H2' && name === 'Your Signature'
},
}),
@@ -564,11 +565,11 @@ describe('configuration', () => {
queryByRole('button', {name: 'Hello, Dave!'})
expect(console.error).toHaveBeenCalledTimes(2)
- expect(console.error.mock.calls[0][0]).toMatch(
- 'Error: Not implemented: window.computedStyle(elt, pseudoElt)',
+ expect(console.error.mock.calls[0][0].message).toMatch(
+ 'Not implemented: window.computedStyle(elt, pseudoElt)',
)
- expect(console.error.mock.calls[1][0]).toMatch(
- 'Error: Not implemented: window.computedStyle(elt, pseudoElt)',
+ expect(console.error.mock.calls[1][0].message).toMatch(
+ 'Not implemented: window.computedStyle(elt, pseudoElt)',
)
} finally {
jest.restoreAllMocks()
diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js
index 79a12821..08ce2f7c 100644
--- a/src/__tests__/suggestions.js
+++ b/src/__tests__/suggestions.js
@@ -612,8 +612,8 @@ test('should suggest hidden option if element is not in the accessibility tree',
suggestion.toString()
expect(console.warn.mock.calls).toMatchInlineSnapshot(`
- Array [
- Array [
+ [
+ [
Element is inaccessible. This means that the element and all its children are invisible to screen readers.
If you are using the aria-hidden prop, make sure this is the right choice for your case.
,
@@ -649,9 +649,9 @@ test('should find label text using the aria-labelledby', () => {
warning: '',
},
`
- Object {
- queryArgs: Array [
- Object {},
+ {
+ queryArgs: [
+ {},
],
queryMethod: getByLabelText,
queryName: LabelText,
diff --git a/src/__tests__/text-matchers.js b/src/__tests__/text-matchers.js
index d272f15a..05d096f7 100644
--- a/src/__tests__/text-matchers.js
+++ b/src/__tests__/text-matchers.js
@@ -291,10 +291,6 @@ cases(
dom: `
`,
queryFn: 'queryAllByDisplayValue',
},
- queryAllByRole: {
- dom: `
`,
- queryFn: 'queryAllByRole',
- },
},
)
diff --git a/src/__tests__/wait-for-element-to-be-removed.js b/src/__tests__/wait-for-element-to-be-removed.js
index 775b8b82..6ea0a091 100644
--- a/src/__tests__/wait-for-element-to-be-removed.js
+++ b/src/__tests__/wait-for-element-to-be-removed.js
@@ -98,6 +98,7 @@ test('rethrows non-testing-lib errors', () => {
const error = new Error('my own error')
return expect(
waitForElementToBeRemoved(() => {
+ // eslint-disable-next-line jest/no-conditional-in-test -- false-positive
if (throwIt) {
throw error
}
diff --git a/src/__tests__/wait-for.js b/src/__tests__/wait-for.js
index a80b9fe4..03814020 100644
--- a/src/__tests__/wait-for.js
+++ b/src/__tests__/wait-for.js
@@ -275,7 +275,7 @@ test('does not work after it resolves', async () => {
context = 'act'
try {
const result = callback()
- // eslint-disable-next-line jest/no-if
+ // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive
if (typeof result?.then === 'function') {
const thenable = result
return {
@@ -319,6 +319,7 @@ test('does not work after it resolves', async () => {
await waitFor(
() => {
+ // eslint-disable-next-line jest/no-conditional-in-test -- false-positive
if (data === null) {
throw new Error('not found')
}
diff --git a/src/helpers.ts b/src/helpers.ts
index 5a068300..77d30d6a 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -10,6 +10,7 @@ function jestFakeTimersAreEnabled() {
// legacy timers
(setTimeout as any)._isMockFunction === true ||
// modern timers
+ // eslint-disable-next-line prefer-object-has-own -- not supported by our support matrix
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
)
}
diff --git a/src/queries/label-text.ts b/src/queries/label-text.ts
index 39e766d5..33eaba6b 100644
--- a/src/queries/label-text.ts
+++ b/src/queries/label-text.ts
@@ -71,20 +71,23 @@ const queryAllByLabelText: AllByText = (
if (
matcher(label.content, label.formControl, text, matchNormalizer) &&
label.formControl
- )
+ ) {
labelledElements.push(label.formControl)
+ }
})
const labelsValue = labelList
.filter(label => Boolean(label.content))
.map(label => label.content)
if (
matcher(labelsValue.join(' '), labelledElement, text, matchNormalizer)
- )
+ ) {
labelledElements.push(labelledElement)
+ }
if (labelsValue.length > 1) {
labelsValue.forEach((labelValue, index) => {
- if (matcher(labelValue, labelledElement, text, matchNormalizer))
+ if (matcher(labelValue, labelledElement, text, matchNormalizer)) {
labelledElements.push(labelledElement)
+ }
const labelsFiltered = [...labelsValue]
labelsFiltered.splice(index, 1)
@@ -97,8 +100,9 @@ const queryAllByLabelText: AllByText = (
text,
matchNormalizer,
)
- )
+ ) {
labelledElements.push(labelledElement)
+ }
}
})
}
diff --git a/src/queries/role.js b/src/queries/role.ts
similarity index 73%
rename from src/queries/role.js
rename to src/queries/role.ts
index a46d8a2b..f8e976a4 100644
--- a/src/queries/role.js
+++ b/src/queries/role.ts
@@ -2,7 +2,11 @@ import {
computeAccessibleDescription,
computeAccessibleName,
} from 'dom-accessibility-api'
-import {roles as allRoles, roleElements} from 'aria-query'
+import {
+ roles as allRoles,
+ roleElements,
+ ARIARoleDefinitionKey,
+} from 'aria-query'
import {
computeAriaSelected,
computeAriaChecked,
@@ -18,24 +22,24 @@ import {
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
import {
- buildQueries,
- fuzzyMatches,
- getConfig,
- makeNormalizer,
- matches,
-} from './all-utils'
+ AllByRole,
+ ByRoleMatcher,
+ ByRoleOptions,
+ GetErrorFunction,
+ Matcher,
+ MatcherFunction,
+ MatcherOptions,
+} from '../../types'
+
+import {buildQueries, getConfig, matches} from './all-utils'
-function queryAllByRole(
+const queryAllByRole: AllByRole = (
container,
role,
{
- exact = true,
- collapseWhitespace,
hidden = getConfig().defaultHidden,
name,
description,
- trim,
- normalizer,
queryFallbacks = false,
selected,
checked,
@@ -44,28 +48,35 @@ function queryAllByRole(
level,
expanded,
} = {},
-) {
+) => {
checkContainerType(container)
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
if (selected !== undefined) {
// guard against unknown roles
- if (allRoles.get(role)?.props['aria-selected'] === undefined) {
+ if (
+ allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-selected'] ===
+ undefined
+ ) {
throw new Error(`"aria-selected" is not supported on role "${role}".`)
}
}
if (checked !== undefined) {
// guard against unknown roles
- if (allRoles.get(role)?.props['aria-checked'] === undefined) {
+ if (
+ allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-checked'] ===
+ undefined
+ ) {
throw new Error(`"aria-checked" is not supported on role "${role}".`)
}
}
if (pressed !== undefined) {
// guard against unknown roles
- if (allRoles.get(role)?.props['aria-pressed'] === undefined) {
+ if (
+ allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-pressed'] ===
+ undefined
+ ) {
throw new Error(`"aria-pressed" is not supported on role "${role}".`)
}
}
@@ -75,7 +86,10 @@ function queryAllByRole(
// guard against unknown roles
// All currently released ARIA versions support `aria-current` on all roles.
// Leaving this for symetry and forward compatibility
- if (allRoles.get(role)?.props['aria-current'] === undefined) {
+ if (
+ allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-current'] ===
+ undefined
+ ) {
throw new Error(`"aria-current" is not supported on role "${role}".`)
}
}
@@ -89,51 +103,50 @@ function queryAllByRole(
if (expanded !== undefined) {
// guard against unknown roles
- if (allRoles.get(role)?.props['aria-expanded'] === undefined) {
+ if (
+ allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-expanded'] ===
+ undefined
+ ) {
throw new Error(`"aria-expanded" is not supported on role "${role}".`)
}
}
- const subtreeIsInaccessibleCache = new WeakMap()
- function cachedIsSubtreeInaccessible(element) {
+ const subtreeIsInaccessibleCache = new WeakMap
()
+ function cachedIsSubtreeInaccessible(element: Element) {
if (!subtreeIsInaccessibleCache.has(element)) {
subtreeIsInaccessibleCache.set(element, isSubtreeInaccessible(element))
}
- return subtreeIsInaccessibleCache.get(element)
+ return subtreeIsInaccessibleCache.get(element) as boolean
}
return Array.from(
- container.querySelectorAll(
+ container.querySelectorAll(
// Only query elements that can be matched by the following filters
- makeRoleSelector(role, exact, normalizer ? matchNormalizer : undefined),
+ makeRoleSelector(role),
),
)
.filter(node => {
const isRoleSpecifiedExplicitly = node.hasAttribute('role')
if (isRoleSpecifiedExplicitly) {
- const roleValue = node.getAttribute('role')
+ const roleValue = node.getAttribute('role') as string
if (queryFallbacks) {
return roleValue
.split(' ')
.filter(Boolean)
- .some(text => matcher(text, node, role, matchNormalizer))
- }
- // if a custom normalizer is passed then let normalizer handle the role value
- if (normalizer) {
- return matcher(roleValue, node, role, matchNormalizer)
+ .some(roleAttributeToken => roleAttributeToken === role)
}
- // other wise only send the first word to match
- const [firstWord] = roleValue.split(' ')
- return matcher(firstWord, node, role, matchNormalizer)
+ // other wise only send the first token to match
+ const [firstRoleAttributeToken] = roleValue.split(' ')
+ return firstRoleAttributeToken === role
}
- const implicitRoles = getImplicitAriaRoles(node)
+ const implicitRoles = getImplicitAriaRoles(node) as string[]
- return implicitRoles.some(implicitRole =>
- matcher(implicitRole, node, role, matchNormalizer),
- )
+ return implicitRoles.some(implicitRole => {
+ return implicitRole === role
+ })
})
.filter(element => {
if (selected !== undefined) {
@@ -169,7 +182,7 @@ function queryAllByRole(
getConfig().computedStyleSupportsPseudoElements,
}),
element,
- name,
+ name as MatcherFunction,
text => text,
)
})
@@ -185,7 +198,7 @@ function queryAllByRole(
getConfig().computedStyleSupportsPseudoElements,
}),
element,
- description,
+ description as Matcher,
text => text,
)
})
@@ -198,16 +211,11 @@ function queryAllByRole(
})
}
-function makeRoleSelector(role, exact, customNormalizer) {
- if (typeof role !== 'string') {
- // For non-string role parameters we can not determine the implicitRoleSelectors.
- return '*'
- }
-
- const explicitRoleSelector =
- exact && !customNormalizer ? `*[role~="${role}"]` : '*[role]'
+function makeRoleSelector(role: ByRoleMatcher) {
+ const explicitRoleSelector = `*[role~="${role}"]`
- const roleRelations = roleElements.get(role) ?? new Set()
+ const roleRelations =
+ roleElements.get(role as ARIARoleDefinitionKey) ?? new Set()
const implicitRoleSelectors = new Set(
Array.from(roleRelations).map(({name}) => name),
)
@@ -220,7 +228,7 @@ function makeRoleSelector(role, exact, customNormalizer) {
.join(',')
}
-const getNameHint = name => {
+const getNameHint = (name: ByRoleOptions['name']): string => {
let nameHint = ''
if (name === undefined) {
nameHint = ''
@@ -233,11 +241,15 @@ const getNameHint = name => {
return nameHint
}
-const getMultipleError = (c, role, {name} = {}) => {
+const getMultipleError: GetErrorFunction<
+ [matcher: ByRoleMatcher, options: ByRoleOptions]
+> = (c, role, {name} = {}) => {
return `Found multiple elements with the role "${role}"${getNameHint(name)}`
}
-const getMissingError = (
+const getMissingError: GetErrorFunction<
+ [matcher: ByRoleMatcher, options: ByRoleOptions]
+> = (
container,
role,
{hidden = getConfig().defaultHidden, name, description} = {},
@@ -247,7 +259,7 @@ const getMissingError = (
}
let roles = ''
- Array.from(container.children).forEach(childElement => {
+ Array.from((container as Element).children).forEach(childElement => {
roles += prettyRoles(childElement, {
hidden,
includeDescription: description !== undefined,
@@ -297,11 +309,10 @@ Unable to find an ${
${roleMessage}`.trim()
}
-const queryAllByRoleWithSuggestions = wrapAllByQueryWithSuggestion(
- queryAllByRole,
- queryAllByRole.name,
- 'queryAll',
-)
+const queryAllByRoleWithSuggestions = wrapAllByQueryWithSuggestion<
+ // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
+ [labelText: Matcher, options?: MatcherOptions]
+>(queryAllByRole, queryAllByRole.name, 'queryAll')
const [queryByRole, getAllByRole, getByRole, findAllByRole, findByRole] =
buildQueries(queryAllByRole, getMultipleError, getMissingError)
diff --git a/src/query-helpers.ts b/src/query-helpers.ts
index 155210e1..44328e69 100644
--- a/src/query-helpers.ts
+++ b/src/query-helpers.ts
@@ -1,11 +1,11 @@
-import type {
- GetErrorFunction,
- Matcher,
- MatcherOptions,
- QueryMethod,
- Variant,
- waitForOptions as WaitForOptions,
- WithSuggest,
+import {
+ type GetErrorFunction,
+ type Matcher,
+ type MatcherOptions,
+ type QueryMethod,
+ type Variant,
+ type waitForOptions as WaitForOptions,
+ type WithSuggest,
} from '../types'
import {getSuggestedQuery} from './suggestions'
import {fuzzyMatches, matches, makeNormalizer} from './matches'
@@ -117,16 +117,16 @@ function makeGetAllQuery(
// this accepts a getter query function and returns a function which calls
// waitFor and passing a function which invokes the getter.
-function makeFindQuery(
+function makeFindQuery(
getter: (
container: HTMLElement,
- text: Matcher,
+ text: QueryMatcher,
options: MatcherOptions,
) => QueryFor,
) {
return (
container: HTMLElement,
- text: Matcher,
+ text: QueryMatcher,
options: MatcherOptions,
waitForOptions: WaitForOptions,
) => {
@@ -209,16 +209,16 @@ const wrapAllByQueryWithSuggestion =
// TODO: This deviates from the published declarations
// However, the implementation always required a dyadic (after `container`) not variadic `queryAllBy` considering the implementation of `makeFindQuery`
// This is at least statically true and can be verified by accepting `QueryMethod`
-function buildQueries(
+function buildQueries(
queryAllBy: QueryMethod<
- [matcher: Matcher, options: MatcherOptions],
+ [matcher: QueryMatcher, options: MatcherOptions],
HTMLElement[]
>,
getMultipleError: GetErrorFunction<
- [matcher: Matcher, options: MatcherOptions]
+ [matcher: QueryMatcher, options: MatcherOptions]
>,
getMissingError: GetErrorFunction<
- [matcher: Matcher, options: MatcherOptions]
+ [matcher: QueryMatcher, options: MatcherOptions]
>,
) {
const queryBy = wrapSingleQueryWithSuggestion(
diff --git a/src/screen.ts b/src/screen.ts
index 9034b159..fbe372ff 100644
--- a/src/screen.ts
+++ b/src/screen.ts
@@ -1,7 +1,7 @@
// WARNING: `lz-string` only has a default export but statically we assume named exports are allowd
// TODO: Statically verify we don't rely on NodeJS implicit named imports.
import lzString from 'lz-string'
-import type {OptionsReceived} from 'pretty-format'
+import {type OptionsReceived} from 'pretty-format'
import {getQueriesForElement} from './get-queries-for-element'
import {getDocument} from './helpers'
import {logDOM} from './pretty-dom'
diff --git a/tests/jest.config.dom.js b/tests/jest.config.dom.js
index 72b3b24b..d67874d9 100644
--- a/tests/jest.config.dom.js
+++ b/tests/jest.config.dom.js
@@ -1,7 +1,12 @@
const path = require('path')
-const baseConfig = require('kcd-scripts/jest')
+const {
+ // global config options that would trigger warnings in project configs
+ collectCoverageFrom,
+ watchPlugins,
+ ...baseConfig
+} = require('kcd-scripts/jest')
-module.exports = {
+const projectConfig = {
...baseConfig,
rootDir: path.join(__dirname, '..'),
displayName: 'dom',
@@ -12,3 +17,5 @@ module.exports = {
],
testEnvironment: 'jest-environment-jsdom',
}
+
+module.exports = projectConfig
diff --git a/tests/jest.config.node.js b/tests/jest.config.node.js
index bf37b60b..e387de9a 100644
--- a/tests/jest.config.node.js
+++ b/tests/jest.config.node.js
@@ -1,7 +1,12 @@
const path = require('path')
-const baseConfig = require('kcd-scripts/jest')
+const {
+ // global config options that would trigger warnings in project configs
+ collectCoverageFrom,
+ watchPlugins,
+ ...baseConfig
+} = require('kcd-scripts/jest')
-module.exports = {
+const projectConfig = {
...baseConfig,
rootDir: path.join(__dirname, '..'),
displayName: 'node',
@@ -13,3 +18,5 @@ module.exports = {
],
testMatch: ['**/__node_tests__/**.js'],
}
+
+module.exports = projectConfig
diff --git a/types/__tests__/type-tests.ts b/types/__tests__/type-tests.ts
index 7a3212cf..4e1a4fb2 100644
--- a/types/__tests__/type-tests.ts
+++ b/types/__tests__/type-tests.ts
@@ -182,11 +182,8 @@ export async function testByRole() {
}) === null,
)
- // allow to query for a role that isn't included in the types
console.assert(queryByRole(element, 'foo') === null)
- console.assert(queryByRole(element, /foo/) === null)
console.assert(screen.queryByRole('foo') === null)
- console.assert(screen.queryByRole(/foo/) === null)
}
export function testA11yHelper() {
diff --git a/types/matches.d.ts b/types/matches.d.ts
index 13fa3692..9df51680 100644
--- a/types/matches.d.ts
+++ b/types/matches.d.ts
@@ -7,8 +7,8 @@ export type MatcherFunction = (
export type Matcher = MatcherFunction | RegExp | number | string
// Get autocomplete for ARIARole union types, while still supporting another string
-// Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-505826972
-export type ByRoleMatcher = ARIARole | MatcherFunction | {}
+// Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939
+export type ByRoleMatcher = ARIARole | (string & {})
export type NormalizerFn = (text: string) => string
diff --git a/types/queries.d.ts b/types/queries.d.ts
index 1fb66b7d..cea02365 100644
--- a/types/queries.d.ts
+++ b/types/queries.d.ts
@@ -66,7 +66,9 @@ export type FindByText = (
waitForElementOptions?: waitForOptions,
) => Promise
-export interface ByRoleOptions extends MatcherOptions {
+export interface ByRoleOptions {
+ /** suppress suggestions for a specific query */
+ suggest?: boolean
/**
* If true includes elements in the query set that are usually excluded from
* the accessibility tree. `role="none"` or `role="presentation"` are included