diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js
index 7073fee4..99372a4d 100644
--- a/src/__tests__/element-queries.js
+++ b/src/__tests__/element-queries.js
@@ -86,7 +86,7 @@ test('get throws a useful error message', () => {
[36m[39m"
`)
expect(() => getByRole('LucyRicardo')).toThrowErrorMatchingInlineSnapshot(`
-"Unable to find an element by role=LucyRicardo
+"Unable to find an element by [role=LucyRicardo]
[36m
[39m
[36m
[39m
diff --git a/src/__tests__/get-by-errors.js b/src/__tests__/get-by-errors.js
new file mode 100644
index 00000000..335ccef5
--- /dev/null
+++ b/src/__tests__/get-by-errors.js
@@ -0,0 +1,86 @@
+import cases from 'jest-in-case'
+import {render} from './helpers/test-utils'
+
+cases(
+ 'getBy* queries throw an error when there are multiple elements returned',
+ ({name, query, html}) => {
+ const utils = render(html)
+ expect(() => utils[name](query)).toThrow(/multiple elements/i)
+ },
+ {
+ getByLabelText: {
+ query: /his/,
+ html: `
`,
+ },
+ getByPlaceholderText: {
+ query: /his/,
+ html: `
`,
+ },
+ getByText: {
+ query: /his/,
+ html: `
his
history
`,
+ },
+ getByAltText: {
+ query: /his/,
+ html: `
`,
+ },
+ getByTitle: {
+ query: /his/,
+ html: `
`,
+ },
+ getByDisplayValue: {
+ query: /his/,
+ html: `
history `,
+ },
+ getByRole: {
+ query: /his/,
+ html: `
`,
+ },
+ getByTestId: {
+ query: /his/,
+ html: `
`,
+ },
+ },
+)
+
+cases(
+ 'queryBy* queries throw an error when there are multiple elements returned',
+ ({name, query, html}) => {
+ const utils = render(html)
+ expect(() => utils[name](query)).toThrow(/multiple elements/i)
+ },
+ {
+ queryByLabelText: {
+ query: /his/,
+ html: `
`,
+ },
+ queryByPlaceholderText: {
+ query: /his/,
+ html: `
`,
+ },
+ queryByText: {
+ query: /his/,
+ html: `
his
history
`,
+ },
+ queryByAltText: {
+ query: /his/,
+ html: `
`,
+ },
+ queryByTitle: {
+ query: /his/,
+ html: `
`,
+ },
+ queryByDisplayValue: {
+ query: /his/,
+ html: `
history `,
+ },
+ queryByRole: {
+ query: /his/,
+ html: `
`,
+ },
+ queryByTestId: {
+ query: /his/,
+ html: `
`,
+ },
+ },
+)
diff --git a/src/__tests__/misc.js b/src/__tests__/misc.js
new file mode 100644
index 00000000..62b7eb9c
--- /dev/null
+++ b/src/__tests__/misc.js
@@ -0,0 +1,15 @@
+import {render} from './helpers/test-utils'
+import {queryByAttribute} from '..'
+
+// we used to use queryByAttribute internally, but we don't anymore. Some people
+// use it as an undocumented part of the API, so we'll keep it around.
+test('queryByAttribute', () => {
+ const {container} = render(
+ '
',
+ )
+ expect(queryByAttribute('data-foo', container, 'bar')).not.toBeNull()
+ expect(queryByAttribute('blah', container, 'sup')).toBeNull()
+ expect(() => queryByAttribute('data-foo', container, /bar/)).toThrow(
+ /multiple/,
+ )
+})
diff --git a/src/queries.js b/src/queries.js
deleted file mode 100644
index 182baf96..00000000
--- a/src/queries.js
+++ /dev/null
@@ -1,393 +0,0 @@
-import {fuzzyMatches, matches, makeNormalizer} from './matches'
-import {getNodeText} from './get-node-text'
-import {
- getElementError,
- firstResultOrNull,
- queryAllByAttribute,
- queryByAttribute,
-} from './query-helpers'
-import {waitForElement} from './wait-for-element'
-import {getConfig} from './config'
-
-// Here are the queries for the library.
-// The queries here should only be things that are accessible to both users who are using a screen reader
-// and those who are not using a screen reader (with the exception of the data-testid attribute query).
-
-function queryAllLabelsByText(
- container,
- text,
- {exact = true, trim, collapseWhitespace, normalizer} = {},
-) {
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll('label')).filter(label =>
- matcher(label.textContent, label, text, matchNormalizer),
- )
-}
-
-function queryAllByLabelText(
- container,
- text,
- {selector = '*', exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- const labels = queryAllLabelsByText(container, text, {
- exact,
- normalizer: matchNormalizer,
- })
- const labelledElements = labels
- .map(label => {
- if (label.control) {
- return label.control
- }
- /* istanbul ignore if */
- if (label.getAttribute('for')) {
- // 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
- //
text
-
- // .control support has landed in jsdom (https://github.com/jsdom/jsdom/issues/2175)
- return container.querySelector(`[id="${label.getAttribute('for')}"]`)
- }
- if (label.getAttribute('id')) {
- //
text
- return container.querySelector(
- `[aria-labelledby~="${label.getAttribute('id')}"]`,
- )
- }
- if (label.childNodes.length) {
- //
text:
- return label.querySelector(selector)
- }
- return null
- })
- .filter(label => label !== null)
- .concat(queryAllByAttribute('aria-label', container, text, {exact}))
-
- const possibleAriaLabelElements = queryAllByText(container, text, {
- exact,
- normalizer: matchNormalizer,
- }).filter(el => el.tagName !== 'LABEL') // don't reprocess labels
-
- const ariaLabelledElements = possibleAriaLabelElements.reduce(
- (allLabelledElements, nextLabelElement) => {
- const labelId = nextLabelElement.getAttribute('id')
-
- if (!labelId) return allLabelledElements
-
- // ARIA labels can label multiple elements
- const labelledNodes = Array.from(
- container.querySelectorAll(`[aria-labelledby~="${labelId}"]`),
- )
-
- return allLabelledElements.concat(labelledNodes)
- },
- [],
- )
-
- return Array.from(new Set([...labelledElements, ...ariaLabelledElements]))
-}
-
-function queryByLabelText(...args) {
- return firstResultOrNull(queryAllByLabelText, ...args)
-}
-
-function queryAllByText(
- container,
- text,
- {
- selector = '*',
- exact = true,
- collapseWhitespace,
- trim,
- ignore = 'script, style',
- normalizer,
- } = {},
-) {
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- let baseArray = []
- if (typeof container.matches === 'function' && container.matches(selector)) {
- baseArray = [container]
- }
- return [...baseArray, ...Array.from(container.querySelectorAll(selector))]
- .filter(node => !ignore || !node.matches(ignore))
- .filter(node => matcher(getNodeText(node), node, text, matchNormalizer))
-}
-
-function queryByText(...args) {
- return firstResultOrNull(queryAllByText, ...args)
-}
-
-function queryAllByTitle(
- container,
- text,
- {exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll('[title], svg > title')).filter(
- node =>
- matcher(node.getAttribute('title'), node, text, matchNormalizer) ||
- matcher(getNodeText(node), node, text, matchNormalizer),
- )
-}
-
-function queryByTitle(...args) {
- return firstResultOrNull(queryAllByTitle, ...args)
-}
-
-function getTestIdAttribute() {
- return getConfig().testIdAttribute
-}
-
-const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder')
-const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder')
-const queryByTestId = (...args) =>
- queryByAttribute(getTestIdAttribute(), ...args)
-const queryAllByTestId = (...args) =>
- queryAllByAttribute(getTestIdAttribute(), ...args)
-const queryByRole = queryByAttribute.bind(null, 'role')
-const queryAllByRole = queryAllByAttribute.bind(null, 'role')
-
-function queryAllByAltText(
- container,
- alt,
- {exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll('img,input,area')).filter(node =>
- matcher(node.getAttribute('alt'), node, alt, matchNormalizer),
- )
-}
-
-function queryByAltText(...args) {
- return firstResultOrNull(queryAllByAltText, ...args)
-}
-
-function queryAllByDisplayValue(
- container,
- value,
- {exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
- const matcher = exact ? matches : fuzzyMatches
- const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll(`input,textarea,select`)).filter(
- node => {
- if (node.tagName === 'SELECT') {
- const selectedOptions = Array.from(node.options).filter(
- option => option.selected,
- )
- return selectedOptions.some(optionNode =>
- matcher(getNodeText(optionNode), optionNode, value, matchNormalizer),
- )
- } else {
- return matcher(node.value, node, value, matchNormalizer)
- }
- },
- )
-}
-
-function queryByDisplayValue(...args) {
- return firstResultOrNull(queryAllByDisplayValue, ...args)
-}
-
-// getters
-// the reason we're not dynamically generating these functions that look so similar:
-// 1. The error messages are specific to each one and depend on arguments
-// 2. The stack trace will look better because it'll have a helpful method name.
-
-function getAllByTestId(container, id, ...rest) {
- const els = queryAllByTestId(container, id, ...rest)
- if (!els.length) {
- throw getElementError(
- `Unable to find an element by: [${getTestIdAttribute()}="${id}"]`,
- container,
- )
- }
- return els
-}
-
-function getByTestId(...args) {
- return firstResultOrNull(getAllByTestId, ...args)
-}
-
-function getAllByTitle(container, title, ...rest) {
- const els = queryAllByTitle(container, title, ...rest)
- if (!els.length) {
- throw getElementError(
- `Unable to find an element with the title: ${title}.`,
- container,
- )
- }
- return els
-}
-
-function getByTitle(...args) {
- return firstResultOrNull(getAllByTitle, ...args)
-}
-
-function getAllByPlaceholderText(container, text, ...rest) {
- const els = queryAllByPlaceholderText(container, text, ...rest)
- if (!els.length) {
- throw getElementError(
- `Unable to find an element with the placeholder text of: ${text}`,
- container,
- )
- }
- return els
-}
-
-function getByPlaceholderText(...args) {
- return firstResultOrNull(getAllByPlaceholderText, ...args)
-}
-
-function getAllByLabelText(container, text, ...rest) {
- const els = queryAllByLabelText(container, text, ...rest)
- if (!els.length) {
- const labels = queryAllLabelsByText(container, text, ...rest)
- if (labels.length) {
- throw getElementError(
- `Found a label with the text of: ${text}, however no form control was found associated to that label. Make sure you're using the "for" attribute or "aria-labelledby" attribute correctly.`,
- container,
- )
- } else {
- throw getElementError(
- `Unable to find a label with the text of: ${text}`,
- container,
- )
- }
- }
- return els
-}
-
-function getByLabelText(...args) {
- return firstResultOrNull(getAllByLabelText, ...args)
-}
-
-function getAllByText(container, text, ...rest) {
- const els = queryAllByText(container, text, ...rest)
- if (!els.length) {
- throw getElementError(
- `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.`,
- container,
- )
- }
- return els
-}
-
-function getByText(...args) {
- return firstResultOrNull(getAllByText, ...args)
-}
-
-function getAllByAltText(container, alt, ...rest) {
- const els = queryAllByAltText(container, alt, ...rest)
- if (!els.length) {
- throw getElementError(
- `Unable to find an element with the alt text: ${alt}`,
- container,
- )
- }
- return els
-}
-
-function getByAltText(...args) {
- return firstResultOrNull(getAllByAltText, ...args)
-}
-
-function getAllByRole(container, id, ...rest) {
- const els = queryAllByRole(container, id, ...rest)
- if (!els.length) {
- throw getElementError(`Unable to find an element by role=${id}`, container)
- }
- return els
-}
-
-function getByRole(...args) {
- return firstResultOrNull(getAllByRole, ...args)
-}
-
-function getAllByDisplayValue(container, value, ...rest) {
- const els = queryAllByDisplayValue(container, value, ...rest)
- if (!els.length) {
- throw getElementError(
- `Unable to find an element with the value: ${value}.`,
- container,
- )
- }
- return els
-}
-
-function getByDisplayValue(...args) {
- return firstResultOrNull(getAllByDisplayValue, ...args)
-}
-
-function makeFinder(getter) {
- return (container, text, options, waitForElementOptions) =>
- waitForElement(
- () => getter(container, text, options),
- waitForElementOptions,
- )
-}
-
-export const findByLabelText = makeFinder(getByLabelText)
-export const findAllByLabelText = makeFinder(getAllByLabelText)
-
-export const findByPlaceholderText = makeFinder(getByPlaceholderText)
-export const findAllByPlaceholderText = makeFinder(getAllByPlaceholderText)
-
-export const findByText = makeFinder(getByText)
-export const findAllByText = makeFinder(getAllByText)
-
-export const findByAltText = makeFinder(getByAltText)
-export const findAllByAltText = makeFinder(getAllByAltText)
-
-export const findByTitle = makeFinder(getByTitle)
-export const findAllByTitle = makeFinder(getAllByTitle)
-
-export const findByDisplayValue = makeFinder(getByDisplayValue)
-export const findAllByDisplayValue = makeFinder(getAllByDisplayValue)
-
-export const findByRole = makeFinder(getByRole)
-export const findAllByRole = makeFinder(getAllByRole)
-
-export const findByTestId = makeFinder(getByTestId)
-export const findAllByTestId = makeFinder(getAllByTestId)
-
-export {
- queryByPlaceholderText,
- queryAllByPlaceholderText,
- getByPlaceholderText,
- getAllByPlaceholderText,
- queryByText,
- queryAllByText,
- getByText,
- getAllByText,
- queryByLabelText,
- queryAllByLabelText,
- getByLabelText,
- getAllByLabelText,
- queryByAltText,
- queryAllByAltText,
- getByAltText,
- getAllByAltText,
- queryByTestId,
- queryAllByTestId,
- getByTestId,
- getAllByTestId,
- queryByTitle,
- queryAllByTitle,
- getByTitle,
- getAllByTitle,
- queryByDisplayValue,
- queryAllByDisplayValue,
- getByDisplayValue,
- getAllByDisplayValue,
- queryByRole,
- queryAllByRole,
- getAllByRole,
- getByRole,
-}
-
-/* eslint complexity:["error", 14] */
diff --git a/src/queries/all-utils.js b/src/queries/all-utils.js
new file mode 100644
index 00000000..c4ce9c43
--- /dev/null
+++ b/src/queries/all-utils.js
@@ -0,0 +1,4 @@
+export * from '../matches'
+export * from '../get-node-text'
+export * from '../query-helpers'
+export * from '../config'
diff --git a/src/queries/alt-text.js b/src/queries/alt-text.js
new file mode 100644
index 00000000..debb2b33
--- /dev/null
+++ b/src/queries/alt-text.js
@@ -0,0 +1,34 @@
+import {matches, fuzzyMatches, makeNormalizer, buildQueries} from './all-utils'
+
+function queryAllByAltText(
+ container,
+ alt,
+ {exact = true, collapseWhitespace, trim, normalizer} = {},
+) {
+ const matcher = exact ? matches : fuzzyMatches
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ return Array.from(container.querySelectorAll('img,input,area')).filter(node =>
+ matcher(node.getAttribute('alt'), node, alt, matchNormalizer),
+ )
+}
+
+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 [
+ queryByAltText,
+ getAllByAltText,
+ getByAltText,
+ findAllByAltText,
+ findByAltText,
+] = buildQueries(queryAllByAltText, getMultipleError, getMissingError)
+
+export {
+ queryByAltText,
+ queryAllByAltText,
+ getByAltText,
+ getAllByAltText,
+ findAllByAltText,
+ findByAltText,
+}
diff --git a/src/queries/display-value.js b/src/queries/display-value.js
new file mode 100644
index 00000000..01a95a96
--- /dev/null
+++ b/src/queries/display-value.js
@@ -0,0 +1,51 @@
+import {
+ getNodeText,
+ matches,
+ fuzzyMatches,
+ makeNormalizer,
+ buildQueries,
+} from './all-utils'
+
+function queryAllByDisplayValue(
+ container,
+ value,
+ {exact = true, collapseWhitespace, trim, normalizer} = {},
+) {
+ const matcher = exact ? matches : fuzzyMatches
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ return Array.from(container.querySelectorAll(`input,textarea,select`)).filter(
+ node => {
+ if (node.tagName === 'SELECT') {
+ const selectedOptions = Array.from(node.options).filter(
+ option => option.selected,
+ )
+ return selectedOptions.some(optionNode =>
+ matcher(getNodeText(optionNode), optionNode, value, matchNormalizer),
+ )
+ } else {
+ return matcher(node.value, node, value, matchNormalizer)
+ }
+ },
+ )
+}
+
+const getMultipleError = (c, value) =>
+ `Found multiple elements with the value: ${value}.`
+const getMissingError = (c, value) =>
+ `Unable to find an element with the value: ${value}.`
+const [
+ queryByDisplayValue,
+ getAllByDisplayValue,
+ getByDisplayValue,
+ findAllByDisplayValue,
+ findByDisplayValue,
+] = buildQueries(queryAllByDisplayValue, getMultipleError, getMissingError)
+
+export {
+ queryByDisplayValue,
+ queryAllByDisplayValue,
+ getByDisplayValue,
+ getAllByDisplayValue,
+ findAllByDisplayValue,
+ findByDisplayValue,
+}
diff --git a/src/queries/index.js b/src/queries/index.js
new file mode 100644
index 00000000..56b283fa
--- /dev/null
+++ b/src/queries/index.js
@@ -0,0 +1,8 @@
+export * from './label-text'
+export * from './placeholder-text'
+export * from './text'
+export * from './display-value'
+export * from './alt-text'
+export * from './title'
+export * from './role'
+export * from './test-id'
diff --git a/src/queries/label-text.js b/src/queries/label-text.js
new file mode 100644
index 00000000..7df781cd
--- /dev/null
+++ b/src/queries/label-text.js
@@ -0,0 +1,129 @@
+import {
+ fuzzyMatches,
+ matches,
+ makeNormalizer,
+ getElementError,
+ queryAllByAttribute,
+ makeFindQuery,
+ makeSingleQuery,
+} from './all-utils'
+import {queryAllByText} from './text'
+
+function queryAllLabelsByText(
+ container,
+ text,
+ {exact = true, trim, collapseWhitespace, normalizer} = {},
+) {
+ const matcher = exact ? matches : fuzzyMatches
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ return Array.from(container.querySelectorAll('label')).filter(label =>
+ matcher(label.textContent, label, text, matchNormalizer),
+ )
+}
+
+function queryAllByLabelText(
+ container,
+ text,
+ {selector = '*', exact = true, collapseWhitespace, trim, normalizer} = {},
+) {
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ const labels = queryAllLabelsByText(container, text, {
+ exact,
+ normalizer: matchNormalizer,
+ })
+ const labelledElements = labels
+ .map(label => {
+ if (label.control) {
+ return label.control
+ }
+ /* istanbul ignore if */
+ if (label.getAttribute('for')) {
+ // 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
+ //
text
+
+ // .control support has landed in jsdom (https://github.com/jsdom/jsdom/issues/2175)
+ return container.querySelector(`[id="${label.getAttribute('for')}"]`)
+ }
+ if (label.getAttribute('id')) {
+ //
text
+ return container.querySelector(
+ `[aria-labelledby~="${label.getAttribute('id')}"]`,
+ )
+ }
+ if (label.childNodes.length) {
+ //
text:
+ return label.querySelector(selector)
+ }
+ return null
+ })
+ .filter(label => label !== null)
+ .concat(queryAllByAttribute('aria-label', container, text, {exact}))
+
+ const possibleAriaLabelElements = queryAllByText(container, text, {
+ exact,
+ normalizer: matchNormalizer,
+ }).filter(el => el.tagName !== 'LABEL') // don't reprocess labels
+
+ const ariaLabelledElements = possibleAriaLabelElements.reduce(
+ (allLabelledElements, nextLabelElement) => {
+ const labelId = nextLabelElement.getAttribute('id')
+
+ if (!labelId) return allLabelledElements
+
+ // ARIA labels can label multiple elements
+ const labelledNodes = Array.from(
+ container.querySelectorAll(`[aria-labelledby~="${labelId}"]`),
+ )
+
+ return allLabelledElements.concat(labelledNodes)
+ },
+ [],
+ )
+
+ return Array.from(new Set([...labelledElements, ...ariaLabelledElements]))
+}
+
+// the getAll* query would normally look like this:
+// const getAllByLabelText = makeGetAllQuery(
+// queryAllByLabelText,
+// (c, text) => `Unable to find a label with the text of: ${text}`,
+// )
+// 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 els = queryAllByLabelText(container, text, ...rest)
+ if (!els.length) {
+ const labels = queryAllLabelsByText(container, text, ...rest)
+ if (labels.length) {
+ throw getElementError(
+ `Found a label with the text of: ${text}, however no form control was found associated to that label. Make sure you're using the "for" attribute or "aria-labelledby" attribute correctly.`,
+ container,
+ )
+ } else {
+ throw getElementError(
+ `Unable to find a label with the text of: ${text}`,
+ container,
+ )
+ }
+ }
+ return els
+}
+
+// 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 getByLabelText = makeSingleQuery(getAllByLabelText, getMultipleError)
+
+const findAllByLabelText = makeFindQuery(getAllByLabelText)
+const findByLabelText = makeFindQuery(getByLabelText)
+
+export {
+ queryAllByLabelText,
+ queryByLabelText,
+ getAllByLabelText,
+ getByLabelText,
+ findAllByLabelText,
+ findByLabelText,
+}
diff --git a/src/queries/placeholder-text.js b/src/queries/placeholder-text.js
new file mode 100644
index 00000000..8e58ab5f
--- /dev/null
+++ b/src/queries/placeholder-text.js
@@ -0,0 +1,25 @@
+import {queryAllByAttribute, buildQueries} from './all-utils'
+
+const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder')
+
+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 [
+ queryByPlaceholderText,
+ getAllByPlaceholderText,
+ getByPlaceholderText,
+ findAllByPlaceholderText,
+ findByPlaceholderText,
+] = buildQueries(queryAllByPlaceholderText, getMultipleError, getMissingError)
+
+export {
+ queryByPlaceholderText,
+ queryAllByPlaceholderText,
+ getByPlaceholderText,
+ getAllByPlaceholderText,
+ findAllByPlaceholderText,
+ findByPlaceholderText,
+}
diff --git a/src/queries/role.js b/src/queries/role.js
new file mode 100644
index 00000000..c493ea92
--- /dev/null
+++ b/src/queries/role.js
@@ -0,0 +1,23 @@
+import {queryAllByAttribute, buildQueries} from './all-utils'
+
+const queryAllByRole = queryAllByAttribute.bind(null, 'role')
+
+const getMultipleError = (c, id) => `Found multiple elements by [role=${id}]`
+const getMissingError = (c, id) => `Unable to find an element by [role=${id}]`
+
+const [
+ queryByRole,
+ getAllByRole,
+ getByRole,
+ findAllByRole,
+ findByRole,
+] = buildQueries(queryAllByRole, getMultipleError, getMissingError)
+
+export {
+ queryByRole,
+ queryAllByRole,
+ getAllByRole,
+ getByRole,
+ findAllByRole,
+ findByRole,
+}
diff --git a/src/queries/test-id.js b/src/queries/test-id.js
new file mode 100644
index 00000000..6d36c164
--- /dev/null
+++ b/src/queries/test-id.js
@@ -0,0 +1,28 @@
+import {queryAllByAttribute, getConfig, buildQueries} from './all-utils'
+
+const getTestIdAttribute = () => getConfig().testIdAttribute
+
+const queryAllByTestId = (...args) =>
+ 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 [
+ queryByTestId,
+ getAllByTestId,
+ getByTestId,
+ findAllByTestId,
+ findByTestId,
+] = buildQueries(queryAllByTestId, getMultipleError, getMissingError)
+
+export {
+ queryByTestId,
+ queryAllByTestId,
+ getByTestId,
+ getAllByTestId,
+ findAllByTestId,
+ findByTestId,
+}
diff --git a/src/queries/text.js b/src/queries/text.js
new file mode 100644
index 00000000..18cf9cb7
--- /dev/null
+++ b/src/queries/text.js
@@ -0,0 +1,52 @@
+import {
+ fuzzyMatches,
+ matches,
+ makeNormalizer,
+ getNodeText,
+ buildQueries,
+} from './all-utils'
+
+function queryAllByText(
+ container,
+ text,
+ {
+ selector = '*',
+ exact = true,
+ collapseWhitespace,
+ trim,
+ ignore = 'script, style',
+ normalizer,
+ } = {},
+) {
+ const matcher = exact ? matches : fuzzyMatches
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ let baseArray = []
+ if (typeof container.matches === 'function' && container.matches(selector)) {
+ baseArray = [container]
+ }
+ return [...baseArray, ...Array.from(container.querySelectorAll(selector))]
+ .filter(node => !ignore || !node.matches(ignore))
+ .filter(node => matcher(getNodeText(node), node, text, matchNormalizer))
+}
+
+const getMultipleError = (c, text) =>
+ `Found multiple elements with the text: ${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 [
+ queryByText,
+ getAllByText,
+ getByText,
+ findAllByText,
+ findByText,
+] = buildQueries(queryAllByText, getMultipleError, getMissingError)
+
+export {
+ queryByText,
+ queryAllByText,
+ getByText,
+ getAllByText,
+ findAllByText,
+ findByText,
+}
diff --git a/src/queries/title.js b/src/queries/title.js
new file mode 100644
index 00000000..c696572f
--- /dev/null
+++ b/src/queries/title.js
@@ -0,0 +1,43 @@
+import {
+ fuzzyMatches,
+ matches,
+ makeNormalizer,
+ getNodeText,
+ buildQueries,
+} from './all-utils'
+
+function queryAllByTitle(
+ container,
+ text,
+ {exact = true, collapseWhitespace, trim, normalizer} = {},
+) {
+ const matcher = exact ? matches : fuzzyMatches
+ const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
+ return Array.from(container.querySelectorAll('[title], svg > title')).filter(
+ node =>
+ matcher(node.getAttribute('title'), node, text, matchNormalizer) ||
+ matcher(getNodeText(node), node, text, matchNormalizer),
+ )
+}
+
+const getMultipleError = (c, title) =>
+ `Found multiple elements with the title: ${title}.`
+const getMissingError = (c, title) =>
+ `Unable to find an element with the title: ${title}.`
+
+const [
+ queryByTitle,
+ getAllByTitle,
+ getByTitle,
+ findAllByTitle,
+ findByTitle,
+] = buildQueries(queryAllByTitle, getMultipleError, getMissingError)
+
+export {
+ queryByTitle,
+ queryAllByTitle,
+ getByTitle,
+ getAllByTitle,
+ findAllByTitle,
+ findByTitle,
+}
diff --git a/src/query-helpers.js b/src/query-helpers.js
index f3b62e61..f91ba209 100644
--- a/src/query-helpers.js
+++ b/src/query-helpers.js
@@ -1,5 +1,6 @@
import {prettyDOM} from './pretty-dom'
import {fuzzyMatches, matches, makeNormalizer} from './matches'
+import {waitForElement} from './wait-for-element'
/* eslint-disable complexity */
function debugDOM(htmlElement) {
@@ -30,10 +31,11 @@ function getElementError(message, container) {
return new Error([message, debugDOM(container)].filter(Boolean).join('\n\n'))
}
-function firstResultOrNull(queryFunction, ...args) {
- const result = queryFunction(...args)
- if (result.length === 0) return null
- return result[0]
+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(
@@ -49,14 +51,73 @@ function queryAllByAttribute(
)
}
-function queryByAttribute(...args) {
- return firstResultOrNull(queryAllByAttribute, ...args)
+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) {
+ throw getMultipleElementsFoundError(
+ getMultipleError(container, ...args),
+ container,
+ )
+ }
+ return els[0] || null
+ }
+}
+
+// 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 getElementError(getMissingError(container, ...args), container)
+ }
+ return els
+ }
+}
+
+// this accepts a getter query function and returns a function which calls
+// waitForElement and passing a function which invokes the getter.
+function makeFindQuery(getter) {
+ return (container, text, options, waitForElementOptions) =>
+ waitForElement(
+ () => getter(container, text, options),
+ waitForElementOptions,
+ )
+}
+
+function buildQueries(queryAllBy, getMultipleError, getMissingError) {
+ const queryBy = makeSingleQuery(queryAllBy, getMultipleError)
+ const getAllBy = makeGetAllQuery(queryAllBy, getMissingError)
+ const getBy = makeSingleQuery(getAllBy, getMultipleError)
+ const findAllBy = makeFindQuery(getAllBy)
+ const findBy = makeFindQuery(getBy)
+
+ return [queryBy, getAllBy, getBy, findAllBy, findBy]
}
export {
debugDOM,
getElementError,
- firstResultOrNull,
+ getMultipleElementsFoundError,
queryAllByAttribute,
queryByAttribute,
+ makeSingleQuery,
+ makeGetAllQuery,
+ makeFindQuery,
+ buildQueries,
}
diff --git a/typings/query-helpers.d.ts b/typings/query-helpers.d.ts
index a4bff63f..b9e86b5d 100644
--- a/typings/query-helpers.d.ts
+++ b/typings/query-helpers.d.ts
@@ -20,11 +20,5 @@ export type AllByAttribute = (
export const queryByAttribute: QueryByAttribute
export const queryAllByAttribute: AllByAttribute
-export const firstResultOrNull: (
- fn: AllByAttribute,
- container?: HTMLElement,
- id?: Matcher,
- options?: MatcherOptions,
-) => HTMLElement | null
export const debugDOM: (htmlElement: HTMLElement) => string
export const getElementError: (message: string, container: HTMLElement) => Error