diff --git a/.all-contributorsrc b/.all-contributorsrc index f382cca7..5d5a66b6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -191,6 +191,15 @@ "code", "test" ] + }, + { + "login": "alexkrolick", + "name": "Alex Krolick", + "avatar_url": "https://avatars3.githubusercontent.com/u/1571667?v=4", + "profile": "https://alexkrolick.com", + "contributions": [ + "code" + ] } ] } diff --git a/README.md b/README.md index 8672f018..eb672608 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![downloads][downloads-badge]][npmtrends] [![MIT License][license-badge]][license] -[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -83,6 +83,7 @@ when a real user uses it. * [Using other assertion libraries](#using-other-assertion-libraries) * [`TextMatch`](#textmatch) * [`query` APIs](#query-apis) +* [`queryAll` and `getAll` APIs](#queryall-and-getall-apis) * [`bindElementToQueries`](#bindelementtoqueries) * [Debugging](#debugging) * [`prettyDOM`](#prettydom) @@ -106,6 +107,8 @@ npm install --save-dev dom-testing-library ## Usage +Note: each of the `get` APIs below have a matching [`getAll`](#queryall-and-getall-apis) API that returns all elements instead of just the first one, and [`query`](#query-apis)/[`getAll`](#queryall-and-getall-apis) that return `null`/`[]` instead of throwing an error. + ```javascript // src/__tests__/example.js // query utilities: @@ -489,6 +492,16 @@ expect(submitButton).toBeNull() // it doesn't exist expect(submitButton).not.toBeInTheDOM() ``` +## `queryAll` and `getAll` APIs + +Each of the `query` APIs have a corresponsing `queryAll` version that always returns an Array of matching nodes. `getAll` is the same but throws when the array has a length of 0. + +```javascript +const submitButtons = queryAllByText(container, 'submit') +expect(submitButtons).toHaveLength(3) // expect 3 elements +expect(submitButtons[0]).toBeInTheDOM() +``` + ## `bindElementToQueries` `bindElementToQueries` takes a DOM element and binds it to the raw query functions, allowing them @@ -711,7 +724,7 @@ Thanks goes to these people ([emoji key][emojis]): | [
Kent C. Dodds](https://kentcdodds.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Documentation") [πŸš‡](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Tests") | [
Ryan Castner](http://audiolion.github.io)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=dnlsandiego "Code") | [
PaweΕ‚ MikoΕ‚ajczyk](https://github.com/Miklet)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=Miklet "Code") | [
Alejandro ÑÑñez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=alejandronanez "Documentation") | [
Matt Parrish](https://github.com/pbomb)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Apbomb "Bug reports") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Tests") | [
Justin Hall](https://github.com/wKovacs64)
[πŸ“¦](#platform-wKovacs64 "Packaging/porting to new platform") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | [
Anto Aravinth](https://github.com/antoaravinth)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [
Łukasz Gandecki](http://team.thebrain.pro)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [πŸ€”](#ideas-sompylasar "Ideas, Planning, & Feedback") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Documentation") | [
Jesse Day](https://github.com/jday3)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [
Ernesto GarcΓ­a](http://gnapse.github.io)
[πŸ’¬](#question-gnapse "Answering Questions") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") | [
Josef Maxx Blake](http://jomaxx.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jomaxx "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=jomaxx "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=jomaxx "Tests") | -| [
Alex Cook](https://github.com/alecook)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=alecook "Documentation") [πŸ’‘](#example-alecook "Examples") | [
Daniel Cook](https://github.com/dfcook)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=dfcook "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=dfcook "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=dfcook "Tests") | [
Thomas Chia](https://github.com/thchia)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Athchia "Bug reports") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=thchia "Code") | [
Tim Deschryver](https://github.com/tdeschryver)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=tdeschryver "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=tdeschryver "Tests") | +| [
Alex Cook](https://github.com/alecook)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=alecook "Documentation") [πŸ’‘](#example-alecook "Examples") | [
Daniel Cook](https://github.com/dfcook)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=dfcook "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=dfcook "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=dfcook "Tests") | [
Thomas Chia](https://github.com/thchia)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Athchia "Bug reports") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=thchia "Code") | [
Tim Deschryver](https://github.com/tdeschryver)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=tdeschryver "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=tdeschryver "Tests") | [
Alex Krolick](https://alexkrolick.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=alexkrolick "Code") | diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 30601751..1cf4e915 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -117,6 +117,78 @@ test('get element by its alt text', () => { expect(getByAltText(/fin.*nem.*poster$/i).src).toBe('/finding-nemo.png') }) +test('getAll* matchers return an array', () => { + const { + getAllByAltText, + getAllByTestId, + getAllByLabelText, + getAllByPlaceholderText, + getAllByText, + } = render(` +
+ finding nemo poster + finding dory poster + jumanji poster +

Where to next?

+ + +
, + `) + expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2) + expect(getAllByAltText('jumanji')).toHaveLength(1) + expect(getAllByTestId('poster')).toHaveLength(3) + expect(getAllByPlaceholderText(/The Rock/)).toHaveLength(1) + expect(getAllByLabelText('User Name')).toHaveLength(1) + expect(getAllByText('where')).toHaveLength(1) +}) + +test('getAll* matchers throw for 0 matches', () => { + const { + getAllByAltText, + getAllByTestId, + getAllByLabelText, + getAllByPlaceholderText, + getAllByText, + } = render(` +
+ +
, + `) + expect(() => getAllByTestId('nope')).toThrow() + expect(() => getAllByAltText('nope')).toThrow() + expect(() => getAllByLabelText('nope')).toThrow() + expect(() => getAllByLabelText('no matches please')).toThrow() + expect(() => getAllByPlaceholderText('nope')).toThrow() + expect(() => getAllByText('nope')).toThrow() +}) + +test('queryAll* matchers return an array for 0 matches', () => { + const { + queryAllByAltText, + queryAllByTestId, + queryAllByLabelText, + queryAllByPlaceholderText, + queryAllByText, + } = render(` +
+
, + `) + expect(queryAllByTestId('nope')).toHaveLength(0) + expect(queryAllByAltText('nope')).toHaveLength(0) + expect(queryAllByLabelText('nope')).toHaveLength(0) + expect(queryAllByPlaceholderText('nope')).toHaveLength(0) + expect(queryAllByText('nope')).toHaveLength(0) +}) + test('using jest helpers to assert element states', () => { const {queryByTestId} = render(`2`) diff --git a/src/queries.js b/src/queries.js index a846754e..49d14223 100644 --- a/src/queries.js +++ b/src/queries.js @@ -10,97 +10,135 @@ function debugDOM(htmlElement) { // 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 queryLabelByText(container, text) { - return ( - Array.from(container.querySelectorAll('label')).find(label => - matches(label.textContent, label, text), - ) || null +function firstResultOrNull(queryFunction, ...args) { + const result = queryFunction(...args) + if (result.length === 0) return null + return result[0] +} + +function queryAllLabelsByText(container, text) { + return Array.from(container.querySelectorAll('label')).filter(label => + matches(label.textContent, label, text), ) } -function queryByLabelText(container, text, {selector = '*'} = {}) { - const label = queryLabelByText(container, text) - if (!label) { - return queryByAttribute('aria-label', container, text) - } - /* istanbul ignore if */ - if (label.control) { - // appears to be unsupported in jsdom: https://github.com/jsdom/jsdom/issues/2175 - // but this would be the proper way to do things - return label.control - } else 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 - // - return container.querySelector(`[id="${label.getAttribute('for')}"]`) - } else if (label.getAttribute('id')) { - // - return container.querySelector( - `[aria-labelledby="${label.getAttribute('id')}"]`, - ) - } else if (label.childNodes.length) { - // - return label.querySelector(selector) - } else { - return null - } +function queryAllByLabelText(container, text, {selector = '*'} = {}) { + const labels = queryAllLabelsByText(container, text) + const labelledElements = labels + .map(label => { + /* istanbul ignore if */ + if (label.control) { + // appears to be unsupported in jsdom: https://github.com/jsdom/jsdom/issues/2175 + // but this would be the proper way to do things + return label.control + } else 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 + // + return container.querySelector(`[id="${label.getAttribute('for')}"]`) + } else if (label.getAttribute('id')) { + // + return container.querySelector( + `[aria-labelledby="${label.getAttribute('id')}"]`, + ) + } else if (label.childNodes.length) { + // + return label.querySelector(selector) + } else { + return null + } + }) + .filter(label => label !== null) + .concat(queryAllByAttribute('aria-label', container, text)) + + return labelledElements +} + +function queryByLabelText(container, text, opts) { + return firstResultOrNull(queryAllByLabelText, container, text, opts) +} + +function queryAllByText(container, text, {selector = '*'} = {}) { + return Array.from(container.querySelectorAll(selector)).filter(node => + matches(getNodeText(node), node, text), + ) +} + +function queryByText(container, text, opts) { + return firstResultOrNull(queryAllByText, container, text, opts) } -function queryByText(container, text, {selector = '*'} = {}) { - return ( - Array.from(container.querySelectorAll(selector)).find(node => - matches(getNodeText(node), node, text), - ) || null +// this is just a utility and not an exposed query. +// There are no plans to expose this. +function queryAllByAttribute(attribute, container, text) { + return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node => + matches(node.getAttribute(attribute), node, text), ) } // this is just a utility and not an exposed query. // There are no plans to expose this. function queryByAttribute(attribute, container, text) { - return ( - Array.from(container.querySelectorAll(`[${attribute}]`)).find(node => - matches(node.getAttribute(attribute), node, text), - ) || null - ) + return firstResultOrNull(queryAllByAttribute, attribute, container, text) } const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder') +const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder') const queryByTestId = queryByAttribute.bind(null, 'data-testid') +const queryAllByTestId = queryAllByAttribute.bind(null, 'data-testid') + +function queryAllByAltText(container, alt) { + return Array.from(container.querySelectorAll('img,input,area')).filter(node => + matches(node.getAttribute('alt'), node, alt), + ) +} + +function queryByAltText(container, alt) { + return firstResultOrNull(queryAllByAltText, container, alt) +} // 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 getByTestId(container, id, ...rest) { - const el = queryByTestId(container, id, ...rest) - if (!el) { +function getAllByTestId(container, id, ...rest) { + const els = queryAllByTestId(container, id, ...rest) + if (!els.length) { throw new Error( `Unable to find an element by: [data-testid="${id}"] \n\n${debugDOM( container, )}`, ) } - return el + return els +} + +function getByTestId(...args) { + return firstResultOrNull(getAllByTestId, ...args) } -function getByPlaceholderText(container, text, ...rest) { - const el = queryByPlaceholderText(container, text, ...rest) - if (!el) { +function getAllByPlaceholderText(container, text, ...rest) { + const els = queryAllByPlaceholderText(container, text, ...rest) + if (!els.length) { throw new Error( `Unable to find an element with the placeholder text of: ${text} \n\n${debugDOM( container, )}`, ) } - return el + return els } -function getByLabelText(container, text, ...rest) { - const el = queryByLabelText(container, text, ...rest) - if (!el) { - const label = queryLabelByText(container, text) - if (label) { +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) + if (labels.length) { throw new Error( `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. \n\n${debugDOM( container, @@ -114,52 +152,66 @@ function getByLabelText(container, text, ...rest) { ) } } - return el + return els +} + +function getByLabelText(...args) { + return firstResultOrNull(getAllByLabelText, ...args) } -function getByText(container, text, ...rest) { - const el = queryByText(container, text, ...rest) - if (!el) { +function getAllByText(container, text, ...rest) { + const els = queryAllByText(container, text, ...rest) + if (!els.length) { throw new Error( `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. \n\n${debugDOM( container, )}`, ) } - return el + return els } -function queryByAltText(container, alt) { - return ( - Array.from(container.querySelectorAll('img,input,area')).find(node => - matches(node.getAttribute('alt'), node, alt), - ) || null - ) +function getByText(...args) { + return firstResultOrNull(getAllByText, ...args) } -function getByAltText(container, alt) { - const el = queryByAltText(container, alt) - if (!el) { +function getAllByAltText(container, alt) { + const els = queryAllByAltText(container, alt) + if (!els.length) { throw new Error( `Unable to find an element with the alt text: ${alt} \n\n${debugDOM( container, )}`, ) } - return el + return els +} + +function getByAltText(...args) { + return firstResultOrNull(getAllByAltText, ...args) } export { queryByPlaceholderText, + queryAllByPlaceholderText, getByPlaceholderText, + getAllByPlaceholderText, queryByText, + queryAllByText, getByText, + getAllByText, queryByLabelText, + queryAllByLabelText, getByLabelText, + getAllByLabelText, queryByAltText, + queryAllByAltText, getByAltText, + getAllByAltText, queryByTestId, + queryAllByTestId, getByTestId, + getAllByTestId, } /* eslint complexity:["error", 14] */