From ee51139b3357498d00552075dc5241847ef86075 Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Sun, 22 Apr 2018 11:17:18 -0700 Subject: [PATCH 1/8] Add queryAll/getAll methods Adds "all" methods for: - byAltText - byTestId --- src/__tests__/element-queries.js | 21 ++++++++++++++++++++ src/queries.js | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 30601751..75b8877c 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -117,6 +117,27 @@ 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} = render(` +
+ finding nemo poster + finding dory poster + jumanji poster +
, + `) + expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2) + expect(getAllByTestId('poster')).toHaveLength(3) +}) + test('using jest helpers to assert element states', () => { const {queryByTestId} = render(`2`) diff --git a/src/queries.js b/src/queries.js index a846754e..7656029f 100644 --- a/src/queries.js +++ b/src/queries.js @@ -64,8 +64,19 @@ function queryByAttribute(attribute, container, text) { ) } +// 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), + ) || null + ) +} + const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder') const queryByTestId = queryByAttribute.bind(null, 'data-testid') +const queryAllByTestId = queryAllByAttribute.bind(null, 'data-testid') // getters // the reason we're not dynamically generating these functions that look so similar: @@ -84,6 +95,11 @@ function getByTestId(container, id, ...rest) { return el } +function getAllByTestId(container, id, ...rest) { + const matched = queryAllByTestId(container, id, ...rest) + return matched +} + function getByPlaceholderText(container, text, ...rest) { const el = queryByPlaceholderText(container, text, ...rest) if (!el) { @@ -137,6 +153,14 @@ function queryByAltText(container, alt) { ) } +function queryAllByAltText(container, alt) { + return ( + Array.from(container.querySelectorAll('img,input,area')).filter(node => + matches(node.getAttribute('alt'), node, alt), + ) || null + ) +} + function getByAltText(container, alt) { const el = queryByAltText(container, alt) if (!el) { @@ -149,6 +173,11 @@ function getByAltText(container, alt) { return el } +function getAllByAltText(container, alt) { + const matched = queryAllByAltText(container, alt) + return matched +} + export { queryByPlaceholderText, getByPlaceholderText, @@ -157,9 +186,13 @@ export { queryByLabelText, getByLabelText, queryByAltText, + queryAllByAltText, getByAltText, + getAllByAltText, queryByTestId, + queryAllByTestId, getByTestId, + getAllByTestId, } /* eslint complexity:["error", 14] */ From 60ed4a6fb3c84bc81105398a332bcba1a28432ed Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Sun, 22 Apr 2018 11:23:39 -0700 Subject: [PATCH 2/8] Add tests for empty matches that return [] --- src/__tests__/element-queries.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 75b8877c..afd59591 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -135,7 +135,11 @@ test('getAll* matchers return an array', () => { , `) expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2) + expect(getAllByAltText('jumanji')).toHaveLength(1) expect(getAllByTestId('poster')).toHaveLength(3) + + expect(getAllByTestId('uhoh')).toHaveLength(0) + expect(getAllByAltText('nope')).toHaveLength(0) }) test('using jest helpers to assert element states', () => { From 893964f4e948c0370e7e55dcb113bb41d92bdee1 Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Sun, 22 Apr 2018 11:25:14 -0700 Subject: [PATCH 3/8] Add contrib --- .all-contributorsrc | 9 +++++++++ README.md | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) 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..d65fa660 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] @@ -711,7 +711,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") | From 46e18ac54c6644a010955c3842107b04279e37ff Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Tue, 24 Apr 2018 00:00:26 -0700 Subject: [PATCH 4/8] Add rest of getAll queries --- src/__tests__/element-queries.js | 31 ++++++- src/queries.js | 147 +++++++++++++++++-------------- 2 files changed, 109 insertions(+), 69 deletions(-) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index afd59591..5f45abc8 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -118,7 +118,13 @@ test('get element by its alt text', () => { }) test('getAll* matchers return an array', () => { - const {getAllByAltText, getAllByTestId} = render(` + const { + getAllByAltText, + getAllByTestId, + getAllByLabelText, + getAllByPlaceholderText, + getAllByText, + } = render(`
{ data-testid="poster" alt="jumanji poster" src="/jumanji.png" /> +

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) +}) - expect(getAllByTestId('uhoh')).toHaveLength(0) +test('getAll* matchers return an array for 0 matches', () => { + const { + getAllByAltText, + getAllByTestId, + getAllByLabelText, + getAllByPlaceholderText, + getAllByText, + } = render(` +
+
, + `) + expect(getAllByTestId('nope')).toHaveLength(0) expect(getAllByAltText('nope')).toHaveLength(0) + expect(getAllByLabelText('nope')).toHaveLength(0) + expect(getAllByPlaceholderText('nope')).toHaveLength(0) + expect(getAllByText('nope')).toHaveLength(0) }) test('using jest helpers to assert element states', () => { diff --git a/src/queries.js b/src/queries.js index 7656029f..f1366970 100644 --- a/src/queries.js +++ b/src/queries.js @@ -10,71 +10,84 @@ 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 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 queryAllLabelByText(container, text) { + return Array.from(container.querySelectorAll('label')).filter(label => + matches(label.textContent, label, text), + ) } -function queryByText(container, text, {selector = '*'} = {}) { - return ( - Array.from(container.querySelectorAll(selector)).find(node => - matches(getNodeText(node), node, text), - ) || null +function queryLabelByText(container, text) { + return firstResultOrNull(queryAllLabelByText, container, text) +} + +function queryAllByLabelText(container, text, {selector = '*'} = {}) { + const labels = queryAllLabelByText(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) +} + // 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 +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 queryAllByAttribute(attribute, container, text) { - return ( - Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node => - matches(node.getAttribute(attribute), node, text), - ) || null - ) +function queryByAttribute(attribute, container, text) { + 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') @@ -95,10 +108,7 @@ function getByTestId(container, id, ...rest) { return el } -function getAllByTestId(container, id, ...rest) { - const matched = queryAllByTestId(container, id, ...rest) - return matched -} +const getAllByTestId = queryAllByTestId function getByPlaceholderText(container, text, ...rest) { const el = queryByPlaceholderText(container, text, ...rest) @@ -112,6 +122,8 @@ function getByPlaceholderText(container, text, ...rest) { return el } +const getAllByPlaceholderText = queryAllByPlaceholderText + function getByLabelText(container, text, ...rest) { const el = queryByLabelText(container, text, ...rest) if (!el) { @@ -133,6 +145,8 @@ function getByLabelText(container, text, ...rest) { return el } +const getAllByLabelText = queryAllByLabelText + function getByText(container, text, ...rest) { const el = queryByText(container, text, ...rest) if (!el) { @@ -145,22 +159,18 @@ function getByText(container, text, ...rest) { return el } -function queryByAltText(container, alt) { - return ( - Array.from(container.querySelectorAll('img,input,area')).find(node => - matches(node.getAttribute('alt'), node, alt), - ) || null - ) -} +const getAllByText = queryAllByText function queryAllByAltText(container, alt) { - return ( - Array.from(container.querySelectorAll('img,input,area')).filter(node => - matches(node.getAttribute('alt'), node, alt), - ) || null + 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) +} + function getByAltText(container, alt) { const el = queryByAltText(container, alt) if (!el) { @@ -173,18 +183,21 @@ function getByAltText(container, alt) { return el } -function getAllByAltText(container, alt) { - const matched = queryAllByAltText(container, alt) - return matched -} +const getAllByAltText = queryAllByAltText export { queryByPlaceholderText, + queryAllByPlaceholderText, getByPlaceholderText, + getAllByPlaceholderText, queryByText, + queryAllByText, getByText, + getAllByText, queryByLabelText, + queryAllByLabelText, getByLabelText, + getAllByLabelText, queryByAltText, queryAllByAltText, getByAltText, From 9e04efee7edf0ccf4da22eb76f3ab4159d2df683 Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Tue, 24 Apr 2018 00:16:59 -0700 Subject: [PATCH 5/8] Document queryAll and getAll --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index d65fa660..eadc95dc 100644 --- a/README.md +++ b/README.md @@ -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 a [`query`](#query-apis) API that returns `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` APIs are included for completeness; however, they are just aliases for the `queryAll` methods and do not throw errors like the `get` methods. + +```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 From 17fc1f467297aaa26386e5a954cb11e7c34bb1d0 Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Tue, 24 Apr 2018 09:42:14 -0700 Subject: [PATCH 6/8] Make getAll throw when nothing matches --- src/__tests__/element-queries.js | 32 +++++-- src/queries.js | 143 ++++++++++++++++++++++--------- 2 files changed, 127 insertions(+), 48 deletions(-) diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js index 5f45abc8..1cf4e915 100644 --- a/src/__tests__/element-queries.js +++ b/src/__tests__/element-queries.js @@ -151,7 +151,7 @@ test('getAll* matchers return an array', () => { expect(getAllByText('where')).toHaveLength(1) }) -test('getAll* matchers return an array for 0 matches', () => { +test('getAll* matchers throw for 0 matches', () => { const { getAllByAltText, getAllByTestId, @@ -160,13 +160,33 @@ test('getAll* matchers return an array for 0 matches', () => { getAllByText, } = render(`
+
, `) - expect(getAllByTestId('nope')).toHaveLength(0) - expect(getAllByAltText('nope')).toHaveLength(0) - expect(getAllByLabelText('nope')).toHaveLength(0) - expect(getAllByPlaceholderText('nope')).toHaveLength(0) - expect(getAllByText('nope')).toHaveLength(0) + 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', () => { diff --git a/src/queries.js b/src/queries.js index f1366970..f3ad1b89 100644 --- a/src/queries.js +++ b/src/queries.js @@ -91,99 +91,158 @@ 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 noMatchingTestId(container, id) { + return new Error( + `Unable to find an element by: [data-testid="${id}"] \n\n${debugDOM( + container, + )}`, + ) +} + function getByTestId(container, id, ...rest) { const el = queryByTestId(container, id, ...rest) if (!el) { - throw new Error( - `Unable to find an element by: [data-testid="${id}"] \n\n${debugDOM( - container, - )}`, - ) + throw noMatchingTestId(container, id) } return el } -const getAllByTestId = queryAllByTestId +function getAllByTestId(container, id, ...rest) { + const els = queryAllByTestId(container, id, ...rest) + if (!els.length) { + throw noMatchingTestId(container, id) + } + return els +} + +function noMatchingPlaceholder(container, text) { + return new Error( + `Unable to find an element with the placeholder text of: ${text} \n\n${debugDOM( + container, + )}`, + ) +} function getByPlaceholderText(container, text, ...rest) { const el = queryByPlaceholderText(container, text, ...rest) if (!el) { - throw new Error( - `Unable to find an element with the placeholder text of: ${text} \n\n${debugDOM( - container, - )}`, - ) + throw noMatchingPlaceholder(container, text) } return el } -const getAllByPlaceholderText = queryAllByPlaceholderText +function getAllByPlaceholderText(container, text, ...rest) { + const els = queryAllByPlaceholderText(container, text, ...rest) + if (!els.length) { + throw noMatchingPlaceholder(container, text) + } + return els +} + +function noMatchingFormControl(container, text) { + return 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, + )}`, + ) +} + +function noMatchingLabel(container, text) { + return new Error( + `Unable to find a label with the text of: ${text} \n\n${debugDOM( + container, + )}`, + ) +} function getByLabelText(container, text, ...rest) { const el = queryByLabelText(container, text, ...rest) if (!el) { const label = queryLabelByText(container, text) if (label) { - 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, - )}`, - ) + throw noMatchingFormControl(container, text) } else { - throw new Error( - `Unable to find a label with the text of: ${text} \n\n${debugDOM( - container, - )}`, - ) + throw noMatchingLabel(container, text) } } return el } -const getAllByLabelText = queryAllByLabelText +function getAllByLabelText(container, text, ...rest) { + const els = queryAllByLabelText(container, text, ...rest) + if (!els.length) { + const labels = queryAllLabelByText(container, text) + if (labels.length) { + throw noMatchingFormControl(container, text) + } else { + throw noMatchingLabel(container, text) + } + } + return els +} + +function noMatchingText(container, text) { + return 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, + )}`, + ) +} function getByText(container, text, ...rest) { const el = queryByText(container, text, ...rest) if (!el) { - 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, - )}`, - ) + throw noMatchingText(container, text) } return el } -const getAllByText = queryAllByText - -function queryAllByAltText(container, alt) { - return Array.from(container.querySelectorAll('img,input,area')).filter(node => - matches(node.getAttribute('alt'), node, alt), - ) +function getAllByText(container, text, ...rest) { + const els = queryAllByText(container, text, ...rest) + if (!els.length) { + throw noMatchingText(container, text) + } + return els } -function queryByAltText(container, alt) { - return firstResultOrNull(queryAllByAltText, container, alt) +function noMatchingAltText(container, alt) { + return new Error( + `Unable to find an element with the alt text: ${alt} \n\n${debugDOM( + container, + )}`, + ) } function getByAltText(container, alt) { const el = queryByAltText(container, alt) if (!el) { - throw new Error( - `Unable to find an element with the alt text: ${alt} \n\n${debugDOM( - container, - )}`, - ) + throw noMatchingAltText(container, alt) } return el } -const getAllByAltText = queryAllByAltText +function getAllByAltText(container, alt) { + const els = queryAllByAltText(container, alt) + if (!els.length) { + throw noMatchingAltText(container, alt) + } + return els +} export { queryByPlaceholderText, From d156d128e07251ef43d9d41e147e10e3b805d517 Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Tue, 24 Apr 2018 09:46:19 -0700 Subject: [PATCH 7/8] Update docs on queryAll/getAll --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eadc95dc..eb672608 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ 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 a [`query`](#query-apis) API that returns `null` instead of throwing an error. +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 @@ -494,7 +494,7 @@ 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` APIs are included for completeness; however, they are just aliases for the `queryAll` methods and do not throw errors like the `get` methods. +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') From 682a9ac8590d1406ab0f500828e2aada7137dd7a Mon Sep 17 00:00:00 2001 From: Alex Krolick Date: Tue, 24 Apr 2018 11:46:31 -0700 Subject: [PATCH 8/8] Refactor get* to use getAll* --- src/queries.js | 143 ++++++++++++++++--------------------------------- 1 file changed, 45 insertions(+), 98 deletions(-) diff --git a/src/queries.js b/src/queries.js index f3ad1b89..49d14223 100644 --- a/src/queries.js +++ b/src/queries.js @@ -16,18 +16,14 @@ function firstResultOrNull(queryFunction, ...args) { return result[0] } -function queryAllLabelByText(container, text) { +function queryAllLabelsByText(container, text) { return Array.from(container.querySelectorAll('label')).filter(label => matches(label.textContent, label, text), ) } -function queryLabelByText(container, text) { - return firstResultOrNull(queryAllLabelByText, container, text) -} - function queryAllByLabelText(container, text, {selector = '*'} = {}) { - const labels = queryAllLabelByText(container, text) + const labels = queryAllLabelsByText(container, text) const labelledElements = labels .map(label => { /* istanbul ignore if */ @@ -106,144 +102,95 @@ function queryByAltText(container, alt) { // 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 noMatchingTestId(container, id) { - return new Error( - `Unable to find an element by: [data-testid="${id}"] \n\n${debugDOM( - container, - )}`, - ) -} - -function getByTestId(container, id, ...rest) { - const el = queryByTestId(container, id, ...rest) - if (!el) { - throw noMatchingTestId(container, id) - } - return el -} - function getAllByTestId(container, id, ...rest) { const els = queryAllByTestId(container, id, ...rest) if (!els.length) { - throw noMatchingTestId(container, id) + throw new Error( + `Unable to find an element by: [data-testid="${id}"] \n\n${debugDOM( + container, + )}`, + ) } return els } -function noMatchingPlaceholder(container, text) { - return new Error( - `Unable to find an element with the placeholder text of: ${text} \n\n${debugDOM( - container, - )}`, - ) -} - -function getByPlaceholderText(container, text, ...rest) { - const el = queryByPlaceholderText(container, text, ...rest) - if (!el) { - throw noMatchingPlaceholder(container, text) - } - return el +function getByTestId(...args) { + return firstResultOrNull(getAllByTestId, ...args) } function getAllByPlaceholderText(container, text, ...rest) { const els = queryAllByPlaceholderText(container, text, ...rest) if (!els.length) { - throw noMatchingPlaceholder(container, text) + throw new Error( + `Unable to find an element with the placeholder text of: ${text} \n\n${debugDOM( + container, + )}`, + ) } return els } -function noMatchingFormControl(container, text) { - return 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, - )}`, - ) -} - -function noMatchingLabel(container, text) { - return new Error( - `Unable to find a label with the text of: ${text} \n\n${debugDOM( - container, - )}`, - ) -} - -function getByLabelText(container, text, ...rest) { - const el = queryByLabelText(container, text, ...rest) - if (!el) { - const label = queryLabelByText(container, text) - if (label) { - throw noMatchingFormControl(container, text) - } else { - throw noMatchingLabel(container, text) - } - } - return el +function getByPlaceholderText(...args) { + return firstResultOrNull(getAllByPlaceholderText, ...args) } function getAllByLabelText(container, text, ...rest) { const els = queryAllByLabelText(container, text, ...rest) if (!els.length) { - const labels = queryAllLabelByText(container, text) + const labels = queryAllLabelsByText(container, text) if (labels.length) { - throw noMatchingFormControl(container, text) + 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, + )}`, + ) } else { - throw noMatchingLabel(container, text) + throw new Error( + `Unable to find a label with the text of: ${text} \n\n${debugDOM( + container, + )}`, + ) } } return els } -function noMatchingText(container, text) { - return 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, - )}`, - ) -} - -function getByText(container, text, ...rest) { - const el = queryByText(container, text, ...rest) - if (!el) { - throw noMatchingText(container, text) - } - return el +function getByLabelText(...args) { + return firstResultOrNull(getAllByLabelText, ...args) } function getAllByText(container, text, ...rest) { const els = queryAllByText(container, text, ...rest) if (!els.length) { - throw noMatchingText(container, text) + 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 els } -function noMatchingAltText(container, alt) { - return new Error( - `Unable to find an element with the alt text: ${alt} \n\n${debugDOM( - container, - )}`, - ) -} - -function getByAltText(container, alt) { - const el = queryByAltText(container, alt) - if (!el) { - throw noMatchingAltText(container, alt) - } - return el +function getByText(...args) { + return firstResultOrNull(getAllByText, ...args) } function getAllByAltText(container, alt) { const els = queryAllByAltText(container, alt) if (!els.length) { - throw noMatchingAltText(container, alt) + throw new Error( + `Unable to find an element with the alt text: ${alt} \n\n${debugDOM( + container, + )}`, + ) } return els } +function getByAltText(...args) { + return firstResultOrNull(getAllByAltText, ...args) +} + export { queryByPlaceholderText, queryAllByPlaceholderText,