diff --git a/src/__tests__/get-user-code-frame.js b/src/__tests__/get-user-code-frame.js
index 8d2bd058..6befcc78 100644
--- a/src/__tests__/get-user-code-frame.js
+++ b/src/__tests__/get-user-code-frame.js
@@ -39,13 +39,13 @@ test('it returns only user code frame when code frames from node_modules are fir
const userTrace = getUserCodeFrame(stack)
expect(userTrace).toMatchInlineSnapshot(`
- /sample-error/error-example.js:7:14
- 5 | document.createTextNode('Hello world')
- 6 | )
- > 7 | screen.debug()
- | ^
-
- `)
+"/sample-error/error-example.js:7:14
+ 5 | document.createTextNode('Hello world')
+ 6 | )
+> 7 | screen.debug()
+ | ^
+"
+`)
})
test('it returns only user code frame when node code frames are present afterwards', () => {
@@ -59,13 +59,13 @@ test('it returns only user code frame when node code frames are present afterwar
const userTrace = getUserCodeFrame()
expect(userTrace).toMatchInlineSnapshot(`
- /sample-error/error-example.js:7:14
- 5 | document.createTextNode('Hello world')
- 6 | )
- > 7 | screen.debug()
- | ^
-
- `)
+"/sample-error/error-example.js:7:14
+ 5 | document.createTextNode('Hello world')
+ 6 | )
+> 7 | screen.debug()
+ | ^
+"
+`)
})
test("it returns empty string if file from code frame can't be read", () => {
diff --git a/src/__tests__/role.js b/src/__tests__/role.js
index c7dc70c5..a63043e3 100644
--- a/src/__tests__/role.js
+++ b/src/__tests__/role.js
@@ -5,100 +5,100 @@ import {render, renderIntoDocument} from './helpers/test-utils'
test('by default logs accessible roles when it fails', () => {
const {getByRole} = render(`
Hi
`)
expect(() => getByRole('article')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "article"
+ "Unable to find an accessible element with the role "article"
-Here are the accessible roles:
+ Here are the accessible roles:
- heading:
+ heading:
- Name "Hi":
-
+ Name "Hi":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
- Hi
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+ Hi
+
+
"
+ `)
})
test('when hidden: true logs available roles when it fails', () => {
const {getByRole} = render(`
Hi
`)
expect(() => getByRole('article', {hidden: true}))
.toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an element with the role "article"
+ "Unable to find an element with the role "article"
-Here are the available roles:
+ Here are the available roles:
- heading:
+ heading:
- Name "Hi":
-
+ Name "Hi":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
-
- Hi
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+
+ Hi
+
+
+
"
+ `)
})
test('logs error when there are no accessible roles', () => {
const {getByRole} = render('')
expect(() => getByRole('article')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "article"
+ "Unable to find an accessible element with the role "article"
-There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
+ There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
-Ignored nodes: comments, ,
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+
"
+ `)
})
test('logs a different error if inaccessible roles should be included', () => {
const {getByRole} = render('')
expect(() => getByRole('article', {hidden: true}))
.toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an element with the role "article"
+ "Unable to find an element with the role "article"
-There are no available roles.
+ There are no available roles.
-Ignored nodes: comments, ,
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+
"
+ `)
})
test('by default excludes elements that have the html hidden attribute or any of their parents', () => {
const {getByRole} = render('
')
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "list"
-
-There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
-
-Ignored nodes: comments, ,
-
-
-
-
-
"
-`)
+ "Unable to find an accessible element with the role "list"
+
+ There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
+
+ Ignored nodes: comments, ,
+
+
+
+
+
"
+ `)
})
test('by default excludes elements which have display: none or any of their parents', () => {
@@ -107,21 +107,21 @@ test('by default excludes elements which have display: none or any of their pare
)
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "list"
-
-There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
-
-Ignored nodes: comments, ,
-
-
-
-
-
"
-`)
+ "Unable to find an accessible element with the role "list"
+
+ There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
+
+ Ignored nodes: comments, ,
+
+
+
+
+
"
+ `)
})
test('by default excludes elements which have visibility hidden', () => {
@@ -130,19 +130,19 @@ test('by default excludes elements which have visibility hidden', () => {
const {getByRole} = render('
')
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "list"
-
-There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
-
-Ignored nodes: comments, ,
-
-
-
-
-
"
-`)
+ "Unable to find an accessible element with the role "list"
+
+ There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
+
+ Ignored nodes: comments, ,
+
+
+
+
+
"
+ `)
})
test('by default excludes elements which have aria-hidden="true" or any of their parents', () => {
@@ -155,21 +155,21 @@ test('by default excludes elements which have aria-hidden="true" or any of their
)
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "list"
-
-There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
-
-Ignored nodes: comments, ,
-
-
-
-
-
"
-`)
+ "Unable to find an accessible element with the role "list"
+
+ There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the \`hidden\` option to \`true\`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole
+
+ Ignored nodes: comments, ,
+
+
+
+
+
"
+ `)
})
test('considers the computed visibility style not the parent', () => {
@@ -238,27 +238,27 @@ test('accessible name comparison is case sensitive', () => {
expect(() => getByRole('heading', {name: 'something that does not match'}))
.toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "heading" and name "something that does not match"
+ "Unable to find an accessible element with the role "heading" and name "something that does not match"
-Here are the accessible roles:
+ Here are the accessible roles:
- heading:
+ heading:
- Name "Sign up":
-
+ Name "Sign up":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
- Sign
-
- up
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+ Sign
+
+ up
+
+
+
"
+ `)
})
test('accessible name filter implements TextMatch', () => {
@@ -285,51 +285,51 @@ test('TextMatch serialization in error message', () => {
expect(() => getByRole('heading', {name: /something that does not match/}))
.toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "heading" and name \`/something that does not match/\`
+ "Unable to find an accessible element with the role "heading" and name \`/something that does not match/\`
-Here are the accessible roles:
+ Here are the accessible roles:
- heading:
+ heading:
- Name "Sign up":
-
+ Name "Sign up":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
- Sign
-
- up
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+ Sign
+
+ up
+
+
+
"
+ `)
expect(() => getByRole('heading', {name: () => false}))
.toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "heading" and name \`() => false\`
+ "Unable to find an accessible element with the role "heading" and name \`() => false\`
-Here are the accessible roles:
+ Here are the accessible roles:
- heading:
+ heading:
- Name "Sign up":
-
+ Name "Sign up":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
- Sign
-
- up
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+ Sign
+
+ up
+
+
+
"
+ `)
})
test('does not include the container in the queryable roles', () => {
@@ -337,22 +337,22 @@ test('does not include the container in the queryable roles', () => {
container: document.createElement('ul'),
})
expect(() => getByRole('list')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "list"
+ "Unable to find an accessible element with the role "list"
-Here are the accessible roles:
+ Here are the accessible roles:
- listitem:
+ listitem:
- Name "":
-
+ Name "":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
-
"
-`)
+ Ignored nodes: comments, ,
+
+
+
"
+ `)
})
test('has no useful error message in findBy', async () => {
@@ -369,28 +369,28 @@ test('explicit role is most specific', () => {
)
expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an accessible element with the role "button"
+ "Unable to find an accessible element with the role "button"
-Here are the accessible roles:
+ Here are the accessible roles:
- tab:
+ tab:
- Name "my-tab":
-
+ Name "my-tab":
+
- --------------------------------------------------
+ --------------------------------------------------
-Ignored nodes: comments, ,
-
-
-"
-`)
+ Ignored nodes: comments, ,
+
+
+ "
+ `)
})
test('accessible regex name in error message for multiple found', () => {
@@ -403,40 +403,40 @@ test('accessible regex name in error message for multiple found', () => {
expect(() => getByRole('button', {name: /value/i}))
.toThrowErrorMatchingInlineSnapshot(`
-"Found multiple elements with the role "button" and name \`/value/i\`
-
-Here are the matching elements:
-
-Ignored nodes: comments, ,
-
-
-Ignored nodes: comments, ,
-
-
-Ignored nodes: comments, ,
-
-
-(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).
-
-Ignored nodes: comments, ,
-
-
-
-
-
"
-`)
+ "Found multiple elements with the role "button" and name \`/value/i\`
+
+ Here are the matching elements:
+
+ Ignored nodes: comments, ,
+
+
+ Ignored nodes: comments, ,
+
+
+ Ignored nodes: comments, ,
+
+
+ (If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).
+
+ Ignored nodes: comments, ,
+
+
+
+
+
"
+ `)
})
test('accessible string name in error message for multiple found', () => {
@@ -449,40 +449,40 @@ test('accessible string name in error message for multiple found', () => {
expect(() => getByRole('button', {name: 'Submit'}))
.toThrowErrorMatchingInlineSnapshot(`
-"Found multiple elements with the role "button" and name "Submit"
-
-Here are the matching elements:
-
-Ignored nodes: comments, ,
-
-
-Ignored nodes: comments, ,
-
-
-Ignored nodes: comments, ,
-
-
-(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).
-
-Ignored nodes: comments, ,
-
-
-
-
-
"
-`)
+ "Found multiple elements with the role "button" and name "Submit"
+
+ Here are the matching elements:
+
+ Ignored nodes: comments, ,
+
+
+ Ignored nodes: comments, ,
+
+
+ Ignored nodes: comments, ,
+
+
+ (If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).
+
+ Ignored nodes: comments, ,
+
+
+
+
+
"
+ `)
})
test('matching elements in error for multiple found', () => {
@@ -496,38 +496,38 @@ test('matching elements in error for multiple found', () => {
expect(() => getByRole('button', {name: /value/i}))
.toThrowErrorMatchingInlineSnapshot(`
-"Found multiple elements with the role "button" and name \`/value/i\`
-
-Here are the matching elements:
-
-Ignored nodes: comments, ,
-
-
-Ignored nodes: comments, ,
-
-
-(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).
-
-Ignored nodes: comments, ,
-
-
-
-
- Wrong role
-
-
-
"
-`)
+ "Found multiple elements with the role "button" and name \`/value/i\`
+
+ Here are the matching elements:
+
+ Ignored nodes: comments, ,
+
+
+ Ignored nodes: comments, ,
+
+
+ (If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).
+
+ Ignored nodes: comments, ,
+
+
+
+
+ Wrong role
+
+
+
"
+ `)
})
describe('configuration', () => {
diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js
index 9e3255b3..86e6bfe1 100644
--- a/src/__tests__/suggestions.js
+++ b/src/__tests__/suggestions.js
@@ -107,19 +107,19 @@ 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 })
-
-
-Ignored nodes: comments, ,
-
-
-"
-`)
+ "A better query is available, try this:
+ getByRole('button', { name: /submit/i })
+
+
+ Ignored nodes: comments, ,
+
+
+ "
+ `)
})
test('should suggest getAllByRole when used with getAllByTestId', () => {
@@ -129,28 +129,28 @@ test('should suggest getAllByRole when used with getAllByTestId', () => {
expect(() => screen.getAllByTestId('foo'))
.toThrowErrorMatchingInlineSnapshot(`
-"A better query is available, try this:
-getAllByRole('button', { name: /submit/i })
-
-
-Ignored nodes: comments, ,
-
-
-
-
-
-
-
-"
-`)
+ "A better query is available, try this:
+ getAllByRole('button', { name: /submit/i })
+
+
+ Ignored nodes: comments, ,
+
+
+
+
+
+
+
+ "
+ `)
})
test('should suggest findByRole when used with findByTestId', async () => {
renderIntoDocument(`
diff --git a/src/queries/label-text.ts b/src/queries/label-text.ts
index 0b59d253..d7cff68d 100644
--- a/src/queries/label-text.ts
+++ b/src/queries/label-text.ts
@@ -100,9 +100,6 @@ const queryAllByLabelText: AllByText = (
return labelledElements
}, [])
.concat(
- // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
queryAllByAttribute('aria-label', container, text, {
exact,
normalizer: matchNormalizer,
diff --git a/src/queries/placeholder-text.ts b/src/queries/placeholder-text.ts
index 9c07c137..4a632afa 100644
--- a/src/queries/placeholder-text.ts
+++ b/src/queries/placeholder-text.ts
@@ -5,9 +5,6 @@ import {queryAllByAttribute, buildQueries} from './all-utils'
const queryAllByPlaceholderText: AllByBoundAttribute = (...args) => {
checkContainerType(args[0])
- // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
return queryAllByAttribute('placeholder', ...args)
}
const getMultipleError: GetErrorFunction = (c, text) =>
diff --git a/src/queries/test-id.ts b/src/queries/test-id.ts
index 6a9c9c81..b4cb9e2e 100644
--- a/src/queries/test-id.ts
+++ b/src/queries/test-id.ts
@@ -7,9 +7,6 @@ const getTestIdAttribute = () => getConfig().testIdAttribute
const queryAllByTestId: AllByBoundAttribute = (...args) => {
checkContainerType(args[0])
- // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
return queryAllByAttribute(getTestIdAttribute(), ...args)
}
diff --git a/src/query-helpers.js b/src/query-helpers.js
deleted file mode 100644
index 52b7888d..00000000
--- a/src/query-helpers.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import {getSuggestedQuery} from './suggestions'
-import {fuzzyMatches, matches, makeNormalizer} from './matches'
-import {waitFor} from './wait-for'
-import {getConfig} from './config'
-
-function getElementError(message, container) {
- return getConfig().getElementError(message, container)
-}
-
-function getMultipleElementsFoundError(message, container) {
- return getElementError(
- `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`,
- container,
- )
-}
-
-function queryAllByAttribute(
- attribute,
- container,
- text,
- {exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node =>
- matcher(node.getAttribute(attribute), node, text, matchNormalizer),
- )
-}
-
-function queryByAttribute(attribute, container, text, ...args) {
- const els = queryAllByAttribute(attribute, container, text, ...args)
- if (els.length > 1) {
- throw getMultipleElementsFoundError(
- `Found multiple elements by [${attribute}=${text}]`,
- container,
- )
- }
- return els[0] || null
-}
-
-// this accepts a query function and returns a function which throws an error
-// if more than one elements is returned, otherwise it returns the first
-// element or null
-function makeSingleQuery(allQuery, getMultipleError) {
- return (container, ...args) => {
- const els = allQuery(container, ...args)
- if (els.length > 1) {
- const elementStrings = els
- .map(element => getElementError(null, element).message)
- .join('\n\n')
-
- throw getMultipleElementsFoundError(
- `${getMultipleError(container, ...args)}
-
-Here are the matching elements:
-
-${elementStrings}`,
- container,
- )
- }
- return els[0] || null
- }
-}
-
-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) {
- return (container, ...args) => {
- const els = allQuery(container, ...args)
- if (!els.length) {
- throw getConfig().getElementError(
- getMissingError(container, ...args),
- container,
- )
- }
-
- return els
- }
-}
-
-// this accepts a getter query function and returns a function which calls
-// waitFor and passing a function which invokes the getter.
-function makeFindQuery(getter) {
- return (container, text, options, waitForOptions) => {
- return waitFor(
- () => {
- return getter(container, text, options)
- },
- {container, ...waitForOptions},
- )
- }
-}
-
-const wrapSingleQueryWithSuggestion =
- (query, queryAllByName, variant) =>
- (container, ...args) => {
- const element = query(container, ...args)
- const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1)
- if (element && 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 (els.length && 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 = wrapSingleQueryWithSuggestion(
- makeSingleQuery(queryAllBy, getMultipleError),
- queryAllBy.name,
- 'query',
- )
- const getAllBy = makeGetAllQuery(queryAllBy, getMissingError)
-
- const getBy = makeSingleQuery(getAllBy, getMultipleError)
- 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,
- getAllWithSuggestions,
- getByWithSuggestions,
- findAllBy,
- findBy,
- ]
-}
-
-export {
- getElementError,
- wrapAllByQueryWithSuggestion,
- wrapSingleQueryWithSuggestion,
- getMultipleElementsFoundError,
- queryAllByAttribute,
- queryByAttribute,
- makeSingleQuery,
- makeGetAllQuery,
- makeFindQuery,
- buildQueries,
-}
diff --git a/src/query-helpers.ts b/src/query-helpers.ts
new file mode 100644
index 00000000..9bca3289
--- /dev/null
+++ b/src/query-helpers.ts
@@ -0,0 +1,310 @@
+import type {
+ AllByAttribute,
+ BuiltQueryMethods,
+ FindAllBy,
+ FindBy,
+ GetAllBy,
+ GetBy,
+ getElementError as GetElementError,
+ GetErrorFunction,
+ Matcher,
+ MatcherOptions,
+ QueryBy,
+ QueryByAttribute,
+ Suggestion,
+ Variant,
+ waitForOptions as WaitForOptions,
+ WithSuggest,
+} from '../types'
+import {getConfig} from './config'
+import {fuzzyMatches, makeNormalizer, matches} from './matches'
+import {getSuggestedQuery} from './suggestions'
+import {waitFor} from './wait-for'
+
+const getElementError: typeof GetElementError = (
+ message: string | null,
+ container: HTMLElement,
+): Error => {
+ return getConfig().getElementError(message, container)
+}
+
+function getMultipleElementsFoundError(
+ message: string,
+ container: HTMLElement,
+) {
+ return getElementError(
+ `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`,
+ container,
+ )
+}
+
+const queryAllByAttribute: AllByAttribute = (
+ attribute: string,
+ container: HTMLElement,
+ text: Matcher,
+ options: MatcherOptions = {},
+): HTMLElement[] => {
+ const {exact = true, collapseWhitespace, trim, normalizer} = options
+ const matcher = exact ? matches : fuzzyMatches
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ return Array.from(
+ container.querySelectorAll(`[${attribute}]`),
+ ).filter(node =>
+ matcher(node.getAttribute(attribute), node, text, matchNormalizer),
+ )
+}
+
+const queryByAttribute: QueryByAttribute = (
+ attribute: string,
+ container: HTMLElement,
+ text: Matcher,
+ options?: MatcherOptions,
+): HTMLElement => {
+ const els = queryAllByAttribute(attribute, container, text, options)
+ if (els.length > 1) {
+ throw getMultipleElementsFoundError(
+ `Found multiple elements by [${attribute}=${text}]`,
+ container,
+ )
+ }
+ return els[0] || null
+}
+
+// this accepts a query function and returns a function which throws an error
+// if more than one elements is returned, otherwise it returns the first
+// element or null
+// Arguments is a tuple of unknown params
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function makeSingleQuery(
+ allQuery: {
+ (container: HTMLElement, ...args: Arguments): HTMLElement[]
+ },
+ getMultipleError: GetErrorFunction,
+) {
+ return (container: HTMLElement, ...args: Arguments) => {
+ const els = allQuery(container, ...args)
+ if (els.length > 1) {
+ const elementStrings = els
+ .map(element => getElementError(null, element).message)
+ .join('\n\n')
+
+ throw getMultipleElementsFoundError(
+ `${getMultipleError(container, ...args)}
+
+Here are the matching elements:
+
+${elementStrings}`,
+ container,
+ )
+ }
+ return els[0] || null
+ }
+}
+
+function getSuggestionError(suggestion: string, container: HTMLElement) {
+ 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
+// Arguments is a tuple of unknown params
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function makeGetAllQuery(
+ allQuery: (arg0: HTMLElement, ...args: Arguments) => HTMLElement[],
+ getMissingError: GetErrorFunction,
+): (container: HTMLElement, ...args: Arguments) => HTMLElement[] {
+ return (container: HTMLElement, ...args: Arguments) => {
+ const els = allQuery(container, ...args)
+ if (!els.length) {
+ throw getConfig().getElementError(
+ getMissingError(container, ...args),
+ container,
+ )
+ }
+
+ return els
+ }
+}
+
+// this accepts a getter query function and returns a function which calls
+// waitFor and passing a function which invokes the getter.
+// Arguments is a tuple of unknown params
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function makeFindQuery(
+ getter: (
+ container: HTMLElement,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ...args: Arguments
+ ) => HTMLElement[] | HTMLElement | undefined,
+) {
+ return (
+ container: HTMLElement,
+ text: Arguments[0],
+ options: Arguments[1],
+ waitForOptions?: WaitForOptions,
+ ) => {
+ return waitFor(
+ () => {
+ // Technically, this function receives a very general `getter` argument, whose signature
+ // may not match its usage in the next statement
+ // We could define getter more narrowly to accept the `text` and `options` params, but
+ // then that breaks this function's calls inside buildQueries,
+ // which pass functions with the general signatures alluded to above
+ // eslint-disable-next-line no-warning-comments
+ // FIXME: clarification from maintainers would be appreciated here
+ // @ts-expect-error explanation above
+ return getter(container, text, options)
+ },
+ {container, ...waitForOptions},
+ )
+ }
+}
+
+const wrapSingleQueryWithSuggestion =
+ ( // eslint-disable-line @typescript-eslint/no-explicit-any
+ query: {
+ (container: HTMLElement, ...args: Arguments): HTMLElement
+ },
+ queryAllByName: string,
+ variant: Variant | undefined,
+ ): ((container: HTMLElement, ...args: Arguments) => HTMLElement) =>
+ (container: HTMLElement, ...args: Arguments) => {
+ const element = query(container, ...args)
+ const [{suggest = getConfig().throwSuggestions} = {} as WithSuggest] =
+ args.slice(-1) as [WithSuggest]
+ if ((element as HTMLElement | undefined) && suggest) {
+ // eslint-disable-next-line no-warning-comments
+ // FIXME: When `src/suggestions.js` is converted to TS, remove this explicit type annotation, and the surrounding two eslint disable-lines
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ const suggestion = getSuggestedQuery(element, variant) as
+ | Suggestion
+ | undefined
+ if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) {
+ throw getSuggestionError(suggestion.toString(), container)
+ }
+ }
+
+ return element
+ }
+
+const wrapAllByQueryWithSuggestion =
+ ( // eslint-disable-line @typescript-eslint/no-explicit-any
+ query: {
+ (container: HTMLElement, ...args: Arguments): HTMLElement[]
+ },
+ queryAllByName: string,
+ variant: Variant | undefined,
+ ) =>
+ (container: HTMLElement, ...args: Arguments) => {
+ const els = query(container, ...args)
+
+ const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) as [
+ WithSuggest,
+ ]
+ if (els.length && 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 = [
+ ...Array.from(
+ 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 &&
+ // Is this actually nullish-safe? endsWith takes a string, but getSuggestedQuery() might return undefined
+ // So .queryName must be accessed via optional chain, and so itself might be undefined
+ // Is there a fallback that won't break the existing logic? Or can we be sure that it won't be undefined?
+ // eslint-disable-next-line no-warning-comments
+ // FIXME: clarification from maintainers would be appreciated here
+ !queryAllByName.endsWith(
+ // @ts-expect-error explanation/question above
+ // eslint-disable-next-line no-warning-comments
+ // FIXME: remove this type assertion + eslint-disable once `src/suggestions.js` is converted to TS
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ (getSuggestedQuery(els[0], variant) as Suggestion | undefined)
+ ?.queryName,
+ )
+ ) {
+ throw getSuggestionError(
+ uniqueSuggestionMessages[0] as string,
+ container,
+ )
+ }
+ }
+
+ return els
+ }
+
+// Arguments is a tuple of unknown params
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function buildQueries(
+ queryAllBy: GetAllBy,
+ getMultipleError: GetErrorFunction,
+ getMissingError: GetErrorFunction,
+): BuiltQueryMethods {
+ const queryBy: QueryBy = wrapSingleQueryWithSuggestion(
+ makeSingleQuery(
+ queryAllBy,
+ getMultipleError as unknown as GetErrorFunction,
+ ),
+ queryAllBy.name,
+ 'query',
+ )
+ const getAllBy: GetAllBy = makeGetAllQuery(
+ queryAllBy,
+ getMissingError as unknown as GetErrorFunction,
+ )
+
+ const getBy: GetBy = makeSingleQuery(
+ getAllBy,
+ getMultipleError as unknown as GetErrorFunction,
+ )
+ const getByWithSuggestions: GetBy = wrapSingleQueryWithSuggestion(
+ getBy,
+ queryAllBy.name,
+ 'get',
+ )
+ const getAllWithSuggestions: GetAllBy =
+ wrapAllByQueryWithSuggestion(
+ getAllBy,
+ queryAllBy.name.replace('query', 'get'),
+ 'getAll',
+ )
+
+ const findAllBy: FindAllBy = makeFindQuery(
+ wrapAllByQueryWithSuggestion(getAllBy, queryAllBy.name, 'findAll'),
+ )
+ const findBy: FindBy = makeFindQuery(
+ wrapSingleQueryWithSuggestion(getBy, queryAllBy.name, 'find'),
+ )
+
+ return [
+ queryBy,
+ getAllWithSuggestions,
+ getByWithSuggestions,
+ findAllBy,
+ findBy,
+ ]
+}
+
+export {
+ getElementError,
+ wrapAllByQueryWithSuggestion,
+ wrapSingleQueryWithSuggestion,
+ getMultipleElementsFoundError,
+ queryAllByAttribute,
+ queryByAttribute,
+ makeSingleQuery,
+ makeGetAllQuery,
+ makeFindQuery,
+ buildQueries,
+}
diff --git a/types/query-helpers.d.ts b/types/query-helpers.d.ts
index 28db0a31..2f5b9188 100644
--- a/types/query-helpers.d.ts
+++ b/types/query-helpers.d.ts
@@ -1,7 +1,12 @@
import {Matcher, MatcherOptions} from './matches'
import {waitForOptions} from './wait-for'
-export type GetErrorFunction = (c: Element | null, alt: string) => string
+export type WithSuggest = {suggest?: boolean}
+
+export type GetErrorFunction = (
+ c: Element | null,
+ ...args: Arguments
+) => string
export interface SelectorMatcherOptions extends MatcherOptions {
selector?: string
@@ -24,7 +29,10 @@ export type AllByAttribute = (
export const queryByAttribute: QueryByAttribute
export const queryAllByAttribute: AllByAttribute
-export function getElementError(message: string, container: HTMLElement): Error
+export function getElementError(
+ message: string | null,
+ container: HTMLElement,
+): Error
/**
* query methods have a common call signature. Only the return type differs.
@@ -58,8 +66,9 @@ export type BuiltQueryMethods = [
FindAllBy,
FindBy,
]
+
export function buildQueries(
- queryByAll: GetAllBy,
- getMultipleError: (container: HTMLElement, ...args: Arguments) => string,
- getMissingError: (container: HTMLElement, ...args: Arguments) => string,
+ queryAllBy: GetAllBy,
+ getMultipleError: GetErrorFunction,
+ getMissingError: GetErrorFunction,
): BuiltQueryMethods