diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js
new file mode 100644
index 00000000..75d6f90e
--- /dev/null
+++ b/src/__tests__/suggestions.js
@@ -0,0 +1,256 @@
+import {configure} from '../config'
+import {screen} from '..'
+import {renderIntoDocument} from './helpers/test-utils'
+
+beforeAll(() => {
+ configure({throwSuggestions: true})
+})
+
+afterAll(() => {
+ configure({throwSuggestions: false})
+})
+
+test('does not suggest when using getByRole', () => {
+ renderIntoDocument(``)
+
+ expect(() => screen.getByRole('button', {name: /submit/i})).not.toThrowError()
+})
+
+test('should not suggest when nothing available', () => {
+ renderIntoDocument(``)
+
+ expect(() => screen.queryByTestId('foo')).not.toThrowError()
+})
+
+test(`should not suggest if the suggestion would give different results`, () => {
+ renderIntoDocument(`
+
+ `)
+
+ expect(() =>
+ screen.getAllByTestId('foo', {suggest: false}),
+ ).not.toThrowError()
+})
+
+test('should not suggest if there would be mixed suggestions', () => {
+ renderIntoDocument(`
+
+ `)
+
+ expect(() => screen.getAllByTestId('foo')).not.toThrowError()
+})
+
+test('should not suggest when suggest is turned off for a query', () => {
+ renderIntoDocument(`
+
+ `)
+
+ expect(() => screen.getByTestId('foo', {suggest: false})).not.toThrowError()
+ expect(() =>
+ screen.getAllByTestId(/foo/, {suggest: false}),
+ ).not.toThrowError()
+})
+
+test('should suggest getByRole when used with getBy', () => {
+ renderIntoDocument(``)
+
+ expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(`
+"A better query is available, try this:
+getByRole("button", {name: /submit/i})
+
+
+
+
+"
+`)
+})
+
+test('should suggest getAllByRole when used with getAllByTestId', () => {
+ renderIntoDocument(`
+
+ `)
+
+ expect(() => screen.getAllByTestId('foo'))
+ .toThrowErrorMatchingInlineSnapshot(`
+"A better query is available, try this:
+getAllByRole("button", {name: /submit/i})
+
+
+
+
+
+
+
+
+
+"
+`)
+})
+test('should suggest findByRole when used with findByTestId', async () => {
+ renderIntoDocument(`
+
+
+ `)
+
+ await expect(screen.findByTestId('foo')).rejects.toThrowError(
+ /findByRole\("button", \{name: \/submit\/i\}\)/,
+ )
+ await expect(screen.findAllByTestId(/foo/)).rejects.toThrowError(
+ /findAllByRole\("button", \{name: \/submit\/i\}\)/,
+ )
+})
+
+test('should suggest img role w/ alt text', () => {
+ renderIntoDocument(``)
+
+ expect(() => screen.getByAltText('Incredibles 2 Poster')).toThrowError(
+ /getByRole\("img", \{name: \/incredibles 2 poster\/i\}\)/,
+ )
+})
+
+test('should suggest getByLabelText when no role available', () => {
+ renderIntoDocument(
+ ``,
+ )
+ expect(() => screen.getByTestId('foo')).toThrowError(
+ /getByLabelText\("Username"\)/,
+ )
+})
+
+test(`should suggest getByLabel on non form elements`, () => {
+ renderIntoDocument(`
+
+ `)
+
+ expect(() => screen.getByTestId('foo')).toThrowError(
+ /getByLabelText\("Section One"\)/,
+ )
+})
+
+test.each([
+ ``,
+ ``,
+ ``,
+])('%s\nshould suggest getByRole over', async html => {
+ renderIntoDocument(html)
+
+ expect(() => screen.getByLabelText('Username')).toThrowError(
+ /getByRole\("textbox", \{name: \/username\/i\}\)/,
+ )
+ expect(() => screen.getAllByLabelText('Username')).toThrowError(
+ /getAllByRole\("textbox", \{name: \/username\/i\}\)/,
+ )
+
+ expect(() => screen.queryByLabelText('Username')).toThrowError(
+ /queryByRole\("textbox", \{name: \/username\/i\}\)/,
+ )
+ expect(() => screen.queryAllByLabelText('Username')).toThrowError(
+ /queryAllByRole\("textbox", \{name: \/username\/i\}\)/,
+ )
+
+ await expect(screen.findByLabelText('Username')).rejects.toThrowError(
+ /findByRole\("textbox", \{name: \/username\/i\}\)/,
+ )
+ await expect(screen.findAllByLabelText(/Username/)).rejects.toThrowError(
+ /findAllByRole\("textbox", \{name: \/username\/i\}\)/,
+ )
+})
+
+test(`should suggest label over placeholder text`, () => {
+ renderIntoDocument(
+ ``,
+ )
+
+ expect(() => screen.getByPlaceholderText('Username')).toThrowError(
+ /getByLabelText\("Username"\)/,
+ )
+})
+
+test(`should suggest getByPlaceholderText`, () => {
+ renderIntoDocument(``)
+
+ expect(() => screen.getByTestId('foo')).toThrowError(
+ /getByPlaceholderText\("Username"\)/,
+ )
+})
+
+test(`should suggest getByText for simple elements`, () => {
+ renderIntoDocument(`hello there
`)
+
+ expect(() => screen.getByTestId('foo')).toThrowError(
+ /getByText\("hello there"\)/,
+ )
+})
+
+test(`should suggest getByDisplayValue`, () => {
+ renderIntoDocument(``)
+
+ document.getElementById('lastName').value = 'Prine' // RIP John Prine
+
+ expect(() => screen.getByTestId('lastName')).toThrowError(
+ /getByDisplayValue\("Prine"\)/,
+ )
+})
+
+test(`should suggest getByAltText`, () => {
+ renderIntoDocument(`
+
+
+ `)
+
+ expect(() => screen.getByTestId('input')).toThrowError(
+ /getByAltText\("last name"\)/,
+ )
+ expect(() => screen.getByTestId('area')).toThrowError(
+ /getByAltText\("Computer"\)/,
+ )
+})
+
+test(`should suggest getByTitle`, () => {
+ renderIntoDocument(`
+
+ `)
+
+ expect(() => screen.getByTestId('delete')).toThrowError(
+ /getByTitle\("Delete"\)/,
+ )
+ expect(() => screen.getAllByTestId('delete')).toThrowError(
+ /getAllByTitle\("Delete"\)/,
+ )
+ expect(() => screen.queryByTestId('delete')).toThrowError(
+ /queryByTitle\("Delete"\)/,
+ )
+ expect(() => screen.queryAllByTestId('delete')).toThrowError(
+ /queryAllByTitle\("Delete"\)/,
+ )
+ expect(() => screen.queryAllByTestId('delete')).toThrowError(
+ /queryAllByTitle\("Delete"\)/,
+ )
+ expect(() => screen.queryAllByTestId('delete')).toThrowError(
+ /queryAllByTitle\("Delete"\)/,
+ )
+
+ // Since `ByTitle` and `ByText` will both return the element
+ // `getByText` will always be the suggested query as it is higher up the list.
+ expect(() => screen.getByTestId('svg')).toThrowError(/getByText\("Close"\)/)
+})
diff --git a/src/config.js b/src/config.js
index c93ad92e..bce4d959 100644
--- a/src/config.js
+++ b/src/config.js
@@ -16,9 +16,12 @@ let config = {
asyncWrapper: cb => cb(),
// default value for the `hidden` option in `ByRole` queries
defaultHidden: false,
- //showOriginalStackTrace flag to show the full error stack traces for async errors
+ // showOriginalStackTrace flag to show the full error stack traces for async errors
showOriginalStackTrace: false,
+ // throw errors w/ suggestions for better queries. Opt in so off by default.
+ throwSuggestions: false,
+
// called when getBy* queries fail. (message, container) => Error
getElementError(message, container) {
const error = new Error(
diff --git a/src/index.js b/src/index.js
index aff9388d..e5888b99 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,6 +16,7 @@ export * from './query-helpers'
export {getRoles, logRoles, isInaccessible} from './role-helpers'
export * from './pretty-dom'
export {configure} from './config'
+export * from './suggestions'
export {
// "within" reads better in user-code
diff --git a/src/queries/alt-text.js b/src/queries/alt-text.js
index debb2b33..aebda550 100644
--- a/src/queries/alt-text.js
+++ b/src/queries/alt-text.js
@@ -1,4 +1,5 @@
import {matches, fuzzyMatches, makeNormalizer, buildQueries} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
function queryAllByAltText(
container,
@@ -16,6 +17,12 @@ const getMultipleError = (c, alt) =>
`Found multiple elements with the alt text: ${alt}`
const getMissingError = (c, alt) =>
`Unable to find an element with the alt text: ${alt}`
+
+const queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByAltText,
+ queryAllByAltText.name,
+ 'queryAll',
+)
const [
queryByAltText,
getAllByAltText,
@@ -26,7 +33,7 @@ const [
export {
queryByAltText,
- queryAllByAltText,
+ queryAllByAltTextWithSuggestions as queryAllByAltText,
getByAltText,
getAllByAltText,
findAllByAltText,
diff --git a/src/queries/display-value.js b/src/queries/display-value.js
index d53df31a..a90966d5 100644
--- a/src/queries/display-value.js
+++ b/src/queries/display-value.js
@@ -5,6 +5,7 @@ import {
makeNormalizer,
buildQueries,
} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
function queryAllByDisplayValue(
container,
@@ -33,6 +34,13 @@ const getMultipleError = (c, value) =>
`Found multiple elements with the display value: ${value}.`
const getMissingError = (c, value) =>
`Unable to find an element with the display value: ${value}.`
+
+const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByDisplayValue,
+ queryAllByDisplayValue.name,
+ 'queryAll',
+)
+
const [
queryByDisplayValue,
getAllByDisplayValue,
@@ -43,7 +51,7 @@ const [
export {
queryByDisplayValue,
- queryAllByDisplayValue,
+ queryAllByDisplayValueWithSuggestions as queryAllByDisplayValue,
getByDisplayValue,
getAllByDisplayValue,
findAllByDisplayValue,
diff --git a/src/queries/label-text.js b/src/queries/label-text.js
index 94eeed79..9e900025 100644
--- a/src/queries/label-text.js
+++ b/src/queries/label-text.js
@@ -6,6 +6,8 @@ import {
queryAllByAttribute,
makeFindQuery,
makeSingleQuery,
+ wrapAllByQueryWithSuggestion,
+ wrapSingleQueryWithSuggestion,
} from './all-utils'
import {queryAllByText} from './text'
@@ -117,7 +119,7 @@ function queryAllByLabelText(
// )
// however, we can give a more helpful error message than the generic one,
// so we're writing this one out by hand.
-function getAllByLabelText(container, text, ...rest) {
+const getAllByLabelText = (container, text, ...rest) => {
const els = queryAllByLabelText(container, text, ...rest)
if (!els.length) {
const labels = queryAllLabelsByText(container, text, ...rest)
@@ -139,17 +141,45 @@ function getAllByLabelText(container, text, ...rest) {
// the reason mentioned above is the same reason we're not using buildQueries
const getMultipleError = (c, text) =>
`Found multiple elements with the text of: ${text}`
-const queryByLabelText = makeSingleQuery(queryAllByLabelText, getMultipleError)
+const queryByLabelText = wrapSingleQueryWithSuggestion(
+ makeSingleQuery(queryAllByLabelText, getMultipleError),
+ queryAllByLabelText.name,
+ 'query',
+)
const getByLabelText = makeSingleQuery(getAllByLabelText, getMultipleError)
-const findAllByLabelText = makeFindQuery(getAllByLabelText)
-const findByLabelText = makeFindQuery(getByLabelText)
+const findAllByLabelText = makeFindQuery(
+ wrapAllByQueryWithSuggestion(
+ getAllByLabelText,
+ getAllByLabelText.name,
+ 'findAll',
+ ),
+)
+const findByLabelText = makeFindQuery(
+ wrapSingleQueryWithSuggestion(getByLabelText, getByLabelText.name, 'find'),
+)
+
+const getAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion(
+ getAllByLabelText,
+ getAllByLabelText.name,
+ 'getAll',
+)
+const getByLabelTextWithSuggestions = wrapSingleQueryWithSuggestion(
+ getByLabelText,
+ getAllByLabelText.name,
+ 'get',
+)
-export {
+const queryAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion(
queryAllByLabelText,
+ queryAllByLabelText.name,
+ 'queryAll',
+)
+export {
+ queryAllByLabelTextWithSuggestions as queryAllByLabelText,
queryByLabelText,
- getAllByLabelText,
- getByLabelText,
+ getAllByLabelTextWithSuggestions as getAllByLabelText,
+ getByLabelTextWithSuggestions as getByLabelText,
findAllByLabelText,
findByLabelText,
}
diff --git a/src/queries/placeholder-text.js b/src/queries/placeholder-text.js
index 8e58ab5f..7f2bc62b 100644
--- a/src/queries/placeholder-text.js
+++ b/src/queries/placeholder-text.js
@@ -1,12 +1,20 @@
import {queryAllByAttribute, buildQueries} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
-const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder')
-
+function queryAllByPlaceholderText(...args) {
+ return queryAllByAttribute('placeholder', ...args)
+}
const getMultipleError = (c, text) =>
`Found multiple elements with the placeholder text of: ${text}`
const getMissingError = (c, text) =>
`Unable to find an element with the placeholder text of: ${text}`
+const queryAllByPlaceholderTextWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByPlaceholderText,
+ queryAllByPlaceholderText.name,
+ 'queryAll',
+)
+
const [
queryByPlaceholderText,
getAllByPlaceholderText,
@@ -17,7 +25,7 @@ const [
export {
queryByPlaceholderText,
- queryAllByPlaceholderText,
+ queryAllByPlaceholderTextWithSuggestions as queryAllByPlaceholderText,
getByPlaceholderText,
getAllByPlaceholderText,
findAllByPlaceholderText,
diff --git a/src/queries/role.js b/src/queries/role.js
index f7eeff18..54aec533 100644
--- a/src/queries/role.js
+++ b/src/queries/role.js
@@ -14,6 +14,7 @@ import {
makeNormalizer,
matches,
} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
function queryAllByRole(
container,
@@ -158,7 +159,11 @@ Unable to find an ${
${roleMessage}`.trim()
}
-
+const queryAllByRoleWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByRole,
+ queryAllByRole.name,
+ 'queryAll',
+)
const [
queryByRole,
getAllByRole,
@@ -169,7 +174,7 @@ const [
export {
queryByRole,
- queryAllByRole,
+ queryAllByRoleWithSuggestions as queryAllByRole,
getAllByRole,
getByRole,
findAllByRole,
diff --git a/src/queries/test-id.js b/src/queries/test-id.js
index 6d36c164..5f5047da 100644
--- a/src/queries/test-id.js
+++ b/src/queries/test-id.js
@@ -1,15 +1,23 @@
import {queryAllByAttribute, getConfig, buildQueries} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
const getTestIdAttribute = () => getConfig().testIdAttribute
-const queryAllByTestId = (...args) =>
- queryAllByAttribute(getTestIdAttribute(), ...args)
+function queryAllByTestId(...args) {
+ return queryAllByAttribute(getTestIdAttribute(), ...args)
+}
const getMultipleError = (c, id) =>
`Found multiple elements by: [${getTestIdAttribute()}="${id}"]`
const getMissingError = (c, id) =>
`Unable to find an element by: [${getTestIdAttribute()}="${id}"]`
+const queryAllByTestIdWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByTestId,
+ queryAllByTestId.name,
+ 'queryAll',
+)
+
const [
queryByTestId,
getAllByTestId,
@@ -20,7 +28,7 @@ const [
export {
queryByTestId,
- queryAllByTestId,
+ queryAllByTestIdWithSuggestions as queryAllByTestId,
getByTestId,
getAllByTestId,
findAllByTestId,
diff --git a/src/queries/text.js b/src/queries/text.js
index 18cf9cb7..aa7419d9 100644
--- a/src/queries/text.js
+++ b/src/queries/text.js
@@ -5,6 +5,7 @@ import {
getNodeText,
buildQueries,
} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
function queryAllByText(
container,
@@ -34,6 +35,12 @@ const getMultipleError = (c, text) =>
const getMissingError = (c, text) =>
`Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`
+const queryAllByTextWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByText,
+ queryAllByText.name,
+ 'queryAll',
+)
+
const [
queryByText,
getAllByText,
@@ -44,7 +51,7 @@ const [
export {
queryByText,
- queryAllByText,
+ queryAllByTextWithSuggestions as queryAllByText,
getByText,
getAllByText,
findAllByText,
diff --git a/src/queries/title.js b/src/queries/title.js
index c696572f..93c2cf5b 100644
--- a/src/queries/title.js
+++ b/src/queries/title.js
@@ -5,6 +5,7 @@ import {
getNodeText,
buildQueries,
} from './all-utils'
+import {wrapAllByQueryWithSuggestion} from '../query-helpers'
function queryAllByTitle(
container,
@@ -25,6 +26,12 @@ const getMultipleError = (c, title) =>
const getMissingError = (c, title) =>
`Unable to find an element with the title: ${title}.`
+const queryAllByTitleWithSuggestions = wrapAllByQueryWithSuggestion(
+ queryAllByTitle,
+ queryAllByTitle.name,
+ 'queryAll',
+)
+
const [
queryByTitle,
getAllByTitle,
@@ -35,7 +42,7 @@ const [
export {
queryByTitle,
- queryAllByTitle,
+ queryAllByTitleWithSuggestions as queryAllByTitle,
getByTitle,
getAllByTitle,
findAllByTitle,
diff --git a/src/query-helpers.js b/src/query-helpers.js
index 7fcc6bd9..0b7793f0 100644
--- a/src/query-helpers.js
+++ b/src/query-helpers.js
@@ -1,3 +1,4 @@
+import {getSuggestedQuery} from './suggestions'
import {fuzzyMatches, matches, makeNormalizer} from './matches'
import {waitFor} from './wait-for'
import {getConfig} from './config'
@@ -53,6 +54,15 @@ function makeSingleQuery(allQuery, getMultipleError) {
}
}
+function getSuggestionError(suggestion, container) {
+ return getConfig().getElementError(
+ `A better query is available, try this:
+${suggestion.toString()}
+`,
+ container,
+ )
+}
+
// this accepts a query function and returns a function which throws an error
// if an empty list of elements is returned
function makeGetAllQuery(allQuery, getMissingError) {
@@ -64,6 +74,7 @@ function makeGetAllQuery(allQuery, getMissingError) {
container,
)
}
+
return els
}
}
@@ -72,21 +83,95 @@ function makeGetAllQuery(allQuery, getMissingError) {
// waitFor and passing a function which invokes the getter.
function makeFindQuery(getter) {
return (container, text, options, waitForOptions) =>
- waitFor(() => getter(container, text, options), waitForOptions)
+ waitFor(() => {
+ return getter(container, text, options)
+ }, waitForOptions)
+}
+
+const wrapSingleQueryWithSuggestion = (query, queryAllByName, variant) => (
+ container,
+ ...args
+) => {
+ const element = query(container, ...args)
+ const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1)
+ if (suggest) {
+ const suggestion = getSuggestedQuery(element, variant)
+ if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) {
+ throw getSuggestionError(suggestion.toString(), container)
+ }
+ }
+
+ return element
+}
+
+const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => (
+ container,
+ ...args
+) => {
+ const els = query(container, ...args)
+
+ const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1)
+ if (suggest) {
+ // get a unique list of all suggestion messages. We are only going to make a suggestion if
+ // all the suggestions are the same
+ const uniqueSuggestionMessages = [
+ ...new Set(
+ els.map(element => getSuggestedQuery(element, variant)?.toString()),
+ ),
+ ]
+
+ if (
+ // only want to suggest if all the els have the same suggestion.
+ uniqueSuggestionMessages.length === 1 &&
+ !queryAllByName.endsWith(getSuggestedQuery(els[0], variant).queryName)
+ ) {
+ throw getSuggestionError(uniqueSuggestionMessages[0], container)
+ }
+ }
+
+ return els
}
function buildQueries(queryAllBy, getMultipleError, getMissingError) {
- const queryBy = makeSingleQuery(queryAllBy, getMultipleError)
+ const queryBy = wrapSingleQueryWithSuggestion(
+ makeSingleQuery(queryAllBy, getMultipleError),
+ queryAllBy.name,
+ 'query',
+ )
const getAllBy = makeGetAllQuery(queryAllBy, getMissingError)
+
const getBy = makeSingleQuery(getAllBy, getMultipleError)
- const findAllBy = makeFindQuery(getAllBy)
- const findBy = makeFindQuery(getBy)
+ const getByWithSuggestions = wrapSingleQueryWithSuggestion(
+ getBy,
+ queryAllBy.name,
+ 'get',
+ )
+ const getAllWithSuggestions = wrapAllByQueryWithSuggestion(
+ getAllBy,
+ queryAllBy.name.replace('query', 'get'),
+ 'getAll',
+ )
+
+ const findAllBy = makeFindQuery(
+ wrapAllByQueryWithSuggestion(getAllBy, queryAllBy.name, 'findAll'),
+ )
+ const findBy = makeFindQuery(
+ wrapSingleQueryWithSuggestion(getBy, queryAllBy.name, 'find'),
+ )
- return [queryBy, getAllBy, getBy, findAllBy, findBy]
+ return [
+ queryBy,
+ getAllWithSuggestions,
+ getByWithSuggestions,
+ findAllBy,
+ findBy,
+ ]
}
export {
getElementError,
+ wrapAllByQueryWithSuggestion,
+ wrapSingleQueryWithSuggestion,
getMultipleElementsFoundError,
queryAllByAttribute,
queryByAttribute,
diff --git a/src/suggestions.js b/src/suggestions.js
new file mode 100644
index 00000000..7d109bd3
--- /dev/null
+++ b/src/suggestions.js
@@ -0,0 +1,81 @@
+import {computeAccessibleName} from 'dom-accessibility-api'
+import {getRoles} from './role-helpers'
+import {getDefaultNormalizer} from './matches'
+
+const normalize = getDefaultNormalizer()
+
+function getLabelTextFor(element) {
+ let label =
+ element.labels &&
+ Array.from(element.labels).find(el => Boolean(normalize(el.textContent)))
+
+ // non form elements that are using aria-labelledby won't be included in `element.labels`
+ if (!label) {
+ const ariaLabelledBy = element.getAttribute('aria-labelledby')
+ if (ariaLabelledBy) {
+ // we're using this notation because with the # selector we would have to escape special characters e.g. user.name
+ // see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector#Escaping_special_characters
+ label = document.querySelector(`[id=${ariaLabelledBy}]`)
+ }
+ }
+
+ if (label) {
+ return label.textContent
+ }
+ return undefined
+}
+
+function makeSuggestion(queryName, content, {variant, name}) {
+ return {
+ queryName,
+ toString() {
+ const options = name ? `, {name: /${name.toLowerCase()}/i}` : ''
+ return `${variant}By${queryName}("${content}"${options})`
+ },
+ }
+}
+
+export function getSuggestedQuery(element, variant) {
+ const roles = getRoles(element)
+
+ const roleNames = Object.keys(roles)
+ if (roleNames.length) {
+ const [role] = roleNames
+ return makeSuggestion('Role', role, {
+ variant,
+ name: computeAccessibleName(element),
+ })
+ }
+
+ const labelText = getLabelTextFor(element)
+ if (labelText) {
+ return makeSuggestion('LabelText', labelText, {variant})
+ }
+
+ const placeholderText = element.getAttribute('placeholder')
+ if (placeholderText) {
+ return makeSuggestion('PlaceholderText', placeholderText, {variant})
+ }
+
+ const textContent = normalize(element.textContent)
+ if (textContent) {
+ return makeSuggestion('Text', textContent, {variant})
+ }
+
+ if (element.value) {
+ return makeSuggestion('DisplayValue', normalize(element.value), {variant})
+ }
+
+ const alt = element.getAttribute('alt')
+ if (alt) {
+ return makeSuggestion('AltText', alt, {variant})
+ }
+
+ const title = element.getAttribute('title')
+
+ if (title) {
+ return makeSuggestion('Title', title, {variant})
+ }
+
+ return undefined
+}
diff --git a/src/wait-for.js b/src/wait-for.js
index ea99eae6..b271a00d 100644
--- a/src/wait-for.js
+++ b/src/wait-for.js
@@ -51,6 +51,7 @@ function waitFor(
clearTimeout(overallTimeoutTimer)
clearInterval(intervalId)
setImmediate(() => observer.disconnect())
+
if (error) {
reject(error)
} else {
diff --git a/types/index.d.ts b/types/index.d.ts
index 406db91c..5b199dcf 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -22,3 +22,4 @@ export * from './get-queries-for-element';
export * from './pretty-dom';
export * from './role-helpers';
export * from './config';
+export * from './suggestions';
diff --git a/types/matches.d.ts b/types/matches.d.ts
index 0b8dad4d..6454c86a 100644
--- a/types/matches.d.ts
+++ b/types/matches.d.ts
@@ -1,29 +1,33 @@
-export type MatcherFunction = (content: string, element: HTMLElement) => boolean;
-export type Matcher = string | RegExp | MatcherFunction;
+export type MatcherFunction = (content: string, element: HTMLElement) => boolean
+export type Matcher = string | RegExp | MatcherFunction
-export type NormalizerFn = (text: string) => string;
+export type NormalizerFn = (text: string) => string
export interface MatcherOptions {
- exact?: boolean;
- /** Use normalizer with getDefaultNormalizer instead */
- trim?: boolean;
- /** Use normalizer with getDefaultNormalizer instead */
- collapseWhitespace?: boolean;
- normalizer?: NormalizerFn;
+ exact?: boolean
+ /** Use normalizer with getDefaultNormalizer instead */
+ trim?: boolean
+ /** Use normalizer with getDefaultNormalizer instead */
+ collapseWhitespace?: boolean
+ normalizer?: NormalizerFn
+ /** suppress suggestions for a specific query */
+ suggest?: boolean
}
export type Match = (
- textToMatch: string,
- node: HTMLElement | null,
- matcher: Matcher,
- options?: MatcherOptions,
-) => boolean;
+ textToMatch: string,
+ node: HTMLElement | null,
+ matcher: Matcher,
+ options?: MatcherOptions,
+) => boolean
export interface DefaultNormalizerOptions {
- trim?: boolean;
- collapseWhitespace?: boolean;
+ trim?: boolean
+ collapseWhitespace?: boolean
}
-export function getDefaultNormalizer(options?: DefaultNormalizerOptions): NormalizerFn;
+export function getDefaultNormalizer(
+ options?: DefaultNormalizerOptions,
+): NormalizerFn
// N.B. Don't expose fuzzyMatches + matches here: they're not public API
diff --git a/types/suggestions.d.ts b/types/suggestions.d.ts
new file mode 100644
index 00000000..f574f344
--- /dev/null
+++ b/types/suggestions.d.ts
@@ -0,0 +1,6 @@
+export interface Suggestion {
+ queryName: string
+ toString(): string
+}
+
+export function getSuggestedQuery(element: HTMLElement): Suggestion | undefined