diff --git a/README.md b/README.md index 57fe9766..59650138 100644 --- a/README.md +++ b/README.md @@ -72,19 +72,20 @@ when a real user uses it. * [Installation](#installation) * [Usage](#usage) - * [`getByLabelText(container: HTMLElement, text: TextMatch, options: {selector: string = '*'}): HTMLElement`](#getbylabeltextcontainer-htmlelement-text-textmatch-options-selector-string---htmlelement) - * [`getByPlaceholderText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyplaceholdertextcontainer-htmlelement-text-textmatch-htmlelement) - * [`getByText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbytextcontainer-htmlelement-text-textmatch-htmlelement) - * [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement) - * [`getByTitle(container: HTMLElement, title: ExactTextMatch): HTMLElement`](#getbytitlecontainer-htmlelement-title-exacttextmatch-htmlelement) - * [`getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`](#getbytestidcontainer-htmlelement-text-exacttextmatch-htmlelement) + * [`getByLabelText`](#getbylabeltext) + * [`getByPlaceholderText`](#getbyplaceholdertext) + * [`getByText`](#getbytext) + * [`getByAltText`](#getbyalttext) + * [`getByTitle`](#getbytitle) + * [`getByTestId`](#getbytestid) * [`wait`](#wait) * [`waitForElement`](#waitforelement) - * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) + * [`fireEvent`](#fireevent) * [Custom Jest Matchers](#custom-jest-matchers) * [Using other assertion libraries](#using-other-assertion-libraries) * [`TextMatch`](#textmatch) - * [ExactTextMatch](#exacttextmatch) + * [Precision](#precision) + * [TextMatch Examples](#textmatch-examples) * [`query` APIs](#query-apis) * [`queryAll` and `getAll` APIs](#queryall-and-getall-apis) * [`bindElementToQueries`](#bindelementtoqueries) @@ -110,7 +111,10 @@ 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. +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. +* See [TextMatch](#textmatch) for details on the `exact`, `trim`, and `collapseWhitespace` options. ```javascript // src/__tests__/example.js @@ -179,7 +183,19 @@ test('examples of some things', async () => { }) ``` -### `getByLabelText(container: HTMLElement, text: TextMatch, options: {selector: string = '*'}): HTMLElement` +### `getByLabelText` + +```typescript +getByLabelText( + container: HTMLElement, + text: TextMatch, + options?: { + selector?: string = '*', + exact?: boolean = true, + collapseWhitespace?: boolean = true, + trim?: boolean = true, + }): HTMLElement +``` This will search for the label that matches the given [`TextMatch`](#textmatch), then find the element associated with that label. @@ -214,7 +230,18 @@ const inputNode = getByLabelText(container, 'username', {selector: 'input'}) > want this behavior (for example you wish to assert that it doesn't exist), > then use `queryByLabelText` instead. -### `getByPlaceholderText(container: HTMLElement, text: TextMatch): HTMLElement` +### `getByPlaceholderText` + +```typescript +getByPlaceholderText( + container: HTMLElement, + text: TextMatch, + options?: { + exact?: boolean = true, + collapseWhitespace?: boolean = false, + trim?: boolean = true, + }): HTMLElement +``` This will search for all elements with a placeholder attribute and find one that matches the given [`TextMatch`](#textmatch). @@ -227,7 +254,18 @@ const inputNode = getByPlaceholderText(container, 'Username') > NOTE: a placeholder is not a good substitute for a label so you should > generally use `getByLabelText` instead. -### `getByText(container: HTMLElement, text: TextMatch): HTMLElement` +### `getByText` + +```typescript +getByText( + container: HTMLElement, + text: TextMatch, + options?: { + exact?: boolean = true, + collapseWhitespace?: boolean = true, + trim?: boolean = true, + }): HTMLElement +``` This will search for all elements that have a text node with `textContent` matching the given [`TextMatch`](#textmatch). @@ -237,7 +275,18 @@ matching the given [`TextMatch`](#textmatch). const aboutAnchorNode = getByText(container, 'about') ``` -### `getByAltText(container: HTMLElement, text: TextMatch): HTMLElement` +### `getByAltText` + +```typescript +getByAltText( + container: HTMLElement, + text: TextMatch, + options?: { + exact?: boolean = true, + collapseWhitespace?: boolean = false, + trim?: boolean = true, + }): HTMLElement +``` This will return the element (normally an ``) that has the given `alt` text. Note that it only supports elements which accept an `alt` attribute: @@ -251,19 +300,41 @@ and [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area) const incrediblesPosterImg = getByAltText(container, /incredibles.*poster$/i) ``` -### `getByTitle(container: HTMLElement, title: ExactTextMatch): HTMLElement` +### `getByTitle` -This will return the element that has the matching `title` attribute. +```typescript +getByTitle( + container: HTMLElement, + title: TextMatch, + options?: { + exact?: boolean = true, + collapseWhitespace?: boolean = false, + trim?: boolean = true, + }): HTMLElement +``` + +Returns the element that has the matching `title` attribute. ```javascript // const deleteElement = getByTitle(container, 'Delete') ``` -### `getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement` +### `getByTestId` + +```typescript +getByTestId( + container: HTMLElement, + text: TextMatch, + options?: { + exact?: boolean = true, + collapseWhitespace?: boolean = false, + trim?: boolean = true, + }): HTMLElement` +``` A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` (and it -also accepts an [`ExactTextMatch`](#exacttextmatch)). +also accepts a [`TextMatch`](#textmatch)). ```javascript // @@ -280,8 +351,6 @@ const usernameInputElement = getByTestId(container, 'username-input') ### `wait` -Defined as: - ```typescript function wait( callback?: () => void, @@ -323,8 +392,6 @@ intervals. ### `waitForElement` -Defined as: - ```typescript function waitForElement( callback?: () => T | null | undefined, @@ -383,7 +450,11 @@ The default `timeout` is `4500ms` which will keep you under additions and removals of child elements (including text nodes) in the `container` and any of its descendants. It won't detect attribute changes unless you add `attributes: true` to the options. -### `fireEvent(node: HTMLElement, event: Event)` +### `fireEvent` + +```typescript +fireEvent(node: HTMLElement, event: Event) +``` Fire DOM events. @@ -398,7 +469,11 @@ fireEvent( ) ``` -#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)` +#### `fireEvent[eventName]` + +```typescript +fireEvent[eventName](node: HTMLElement, eventProperties: Object) +``` Convenience methods for firing DOM events. Check out [src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js) @@ -411,7 +486,11 @@ fireEvent.click(getElementByText('Submit'), rightClick) // default `button` property for click events is set to `0` which is a left click. ``` -#### `getNodeText(node: HTMLElement)` +#### `getNodeText` + +```typescript +getNodeText(node: HTMLElement) +``` Returns the complete text content of a html element, removing any extra whitespace. The intention is to treat text in nodes exactly as how it is @@ -469,43 +548,50 @@ and add it here! Several APIs accept a `TextMatch` which can be a `string`, `regex` or a `function` which returns `true` for a match and `false` for a mismatch. -Here's an example +### Precision + +Some APIs accept an object as the final argument that can contain options that +affect the precision of string matching: + +* `exact`: Defaults to `true`; matches full strings, case-sensitive. When false, + matches substrings and is not case-sensitive. + * `exact` has no effect on `regex` or `function` arguments. + * In most cases using a regex instead of a string gives you more control over + fuzzy matching and should be preferred over `{ exact: false }`. +* `trim`: Defaults to `true`; trim leading and trailing whitespace. +* `collapseWhitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space. + +### TextMatch Examples ```javascript -//
Hello World
-// all of the following will find the div -getByText(container, 'Hello World') // full match -getByText(container, 'llo worl') // substring match -getByText(container, 'hello world') // strings ignore case -getByText(container, /Hello W?oRlD/i) // regex -getByText(container, (content, element) => content.startsWith('Hello')) // function - -// all of the following will NOT find the div -getByText(container, 'Goodbye World') // non-string match -getByText(container, /hello world/) // case-sensitive regex with different case -// function looking for a span when it's actually a div -getByText(container, (content, element) => { - return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello') -}) -``` +//
+// Hello World +//
+ +// WILL find the div: -### ExactTextMatch +// Matching a string: +getByText(container, 'Hello World') // full string match +getByText(container, 'llo Worl'), {exact: false} // substring match +getByText(container, 'hello world', {exact: false}) // ignore case -Some APIs use ExactTextMatch, which is the same as TextMatch but case-sensitive -and does not match substrings; however, regexes and functions are also accepted -for custom matching. +// Matching a regex: +getByText(container, /World/) // substring match +getByText(container, /world/i) // substring match, ignore case +getByText(container, /^hello world$/i) // full string match, ignore case +getByText(container, /Hello W?oRlD/i) // advanced regex -```js -// +// Matching with a custom function: +getByText(container, (content, element) => content.startsWith('Hello')) -// all of the following will find the button -getByTestId(container, 'submit-button') // exact match -getByTestId(container, /submit*/) // regex match -getByTestId(container, content => content.startsWith('submit')) // function +// WILL NOT find the div: -// all of the following will NOT find the button -getByTestId(container, 'submit-') // no substrings -getByTestId(container, 'Submit-Button') // case-sensitive +getByText(container, 'Goodbye World') // full string does not match +getByText(container, /hello world/) // case-sensitive regex with different case +// function looking for a span when it's actually a div: +getByText(container, (content, element) => { + return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello') +}) ``` ## `query` APIs diff --git a/src/__tests__/__snapshots__/element-queries.js.snap b/src/__tests__/__snapshots__/element-queries.js.snap index 67c2c6a9..825e4813 100644 --- a/src/__tests__/__snapshots__/element-queries.js.snap +++ b/src/__tests__/__snapshots__/element-queries.js.snap @@ -49,7 +49,7 @@ exports[`get throws a useful error message 6`] = ` `; exports[`label with no form control 1`] = ` -"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly. +"Found a label with the text of: /alone/, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly. 
 
, `) expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2) - expect(getAllByAltText('jumanji')).toHaveLength(1) + 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(getAllByText(/^where/i)).toHaveLength(1) }) test('getAll* matchers throw for 0 matches', () => { @@ -188,8 +188,6 @@ test('getAll* matchers throw for 0 matches', () => { } = render(`
-
-
, `) expect(() => getAllByTestId('nope')).toThrow() diff --git a/src/__tests__/matches.js b/src/__tests__/matches.js index abef386d..8296a54f 100644 --- a/src/__tests__/matches.js +++ b/src/__tests__/matches.js @@ -1,26 +1,25 @@ -import {matches, matchesExact} from '../' +import {fuzzyMatches, matches} from '../' // unit tests for text match utils const node = null -test('matches should get fuzzy matches', () => { - // should not match - expect(matchesExact(null, node, 'abc')).toBe(false) - expect(matchesExact('', node, 'abc')).toBe(false) - // should match - expect(matches('ABC', node, 'abc')).toBe(true) +test('matchers accept strings', () => { expect(matches('ABC', node, 'ABC')).toBe(true) + expect(fuzzyMatches('ABC', node, 'ABC')).toBe(true) }) -test('matchesExact should only get exact matches', () => { - // should not match - expect(matchesExact(null, node, null)).toBe(false) - expect(matchesExact(null, node, 'abc')).toBe(false) - expect(matchesExact('', node, 'abc')).toBe(false) - expect(matchesExact('ABC', node, 'abc')).toBe(false) - expect(matchesExact('ABC', node, 'A')).toBe(false) - expect(matchesExact('ABC', node, 'ABCD')).toBe(false) - // should match - expect(matchesExact('ABC', node, 'ABC')).toBe(true) +test('matchers accept regex', () => { + expect(matches('ABC', node, /ABC/)).toBe(true) + expect(fuzzyMatches('ABC', node, /ABC/)).toBe(true) +}) + +test('matchers accept functions', () => { + expect(matches('ABC', node, text => text === 'ABC')).toBe(true) + expect(fuzzyMatches('ABC', node, text => text === 'ABC')).toBe(true) +}) + +test('matchers return false if text to match is not a string', () => { + expect(matches(null, node, 'ABC')).toBe(false) + expect(fuzzyMatches(null, node, 'ABC')).toBe(false) }) diff --git a/src/__tests__/text-matchers.js b/src/__tests__/text-matchers.js index 36abc98a..4cc66303 100644 --- a/src/__tests__/text-matchers.js +++ b/src/__tests__/text-matchers.js @@ -1,22 +1,169 @@ +import 'jest-dom/extend-expect' import cases from 'jest-in-case' import {render} from './helpers/test-utils' cases( - 'text matchers', - opts => { - const {getByText} = render(` - About - `) - expect(getByText(opts.textMatch).id).toBe('anchor') + 'matches find case-sensitive full strings by default', + ({dom, query, queryFn}) => { + const queries = render(dom) + + const queryString = query + const queryRegex = new RegExp(query) + const queryFunc = text => text === query + + expect(queries[queryFn](queryString)).toHaveLength(1) + expect(queries[queryFn](queryRegex)).toHaveLength(1) + expect(queries[queryFn](queryFunc)).toHaveLength(1) + + expect(queries[queryFn](query.toUpperCase())).toHaveLength(0) // case + expect(queries[queryFn](query.slice(0, 1))).toHaveLength(0) // substring + }, + { + queryAllByTestId: { + dom: `Link`, + query: `link`, + queryFn: `queryAllByTestId`, + }, + queryAllByAltText: { + dom: ` + Finding Nemo poster`, + query: `Finding Nemo poster`, + queryFn: `queryAllByAltText`, + }, + queryAllByPlaceholderText: { + dom: ``, + query: `Dwayne 'The Rock' Johnson`, + queryFn: `queryAllByPlaceholderText`, + }, + queryAllByText: { + dom: `

Some content

`, + query: `Some content`, + queryFn: `queryAllByText`, + }, + queryAllByLabelText: { + dom: ` + + `, + query: `User Name`, + queryFn: `queryAllByLabelText`, + }, + }, +) + +cases( + 'queries trim leading, trailing & inner whitespace by default', + ({dom, query, queryFn}) => { + const queries = render(dom) + expect(queries[queryFn](query)).toHaveLength(1) + expect( + queries[queryFn](query, {collapseWhitespace: false, trim: false}), + ).toHaveLength(0) + }, + { + queryAllByTestId: { + dom: `Link`, + query: /^link$/, + queryFn: `queryAllByTestId`, + }, + queryAllByAltText: { + dom: ` + 
+            Finding Nemo poster `, + query: /^Finding Nemo poster$/, + queryFn: `queryAllByAltText`, + }, + queryAllByPlaceholderText: { + dom: ` + `, + query: /^Dwayne/, + queryFn: `queryAllByPlaceholderText`, + }, + queryAllByText: { + dom: ` +

+ Content + with + linebreaks + is + ok +

`, + query: `Content with linebreaks is ok`, + queryFn: `queryAllByText`, + }, + queryAllByLabelText: { + dom: ` + + `, + query: `User Name`, + queryFn: `queryAllByLabelText`, + }, + }, +) + +cases( + '{ exact } option toggles case-insensitive partial matches', + ({dom, query, queryFn}) => { + const queries = render(dom) + + const queryString = query + const queryRegex = new RegExp(query) + const queryFunc = text => text === query + + expect(queries[queryFn](query)).toHaveLength(1) + + expect(queries[queryFn](queryString, {exact: false})).toHaveLength(1) + expect(queries[queryFn](queryRegex, {exact: false})).toHaveLength(1) + expect(queries[queryFn](queryFunc, {exact: false})).toHaveLength(1) + + expect(queries[queryFn](query.split(' ')[0], {exact: false})).toHaveLength( + 1, + ) + expect(queries[queryFn](query.toLowerCase(), {exact: false})).toHaveLength( + 1, + ) + }, + { + queryAllByPlaceholderText: { + dom: ``, + query: `Dwayne 'The Rock' Johnson`, + queryFn: `queryAllByPlaceholderText`, + }, + queryAllByLabelText: { + dom: ` + + `, + query: `User Name`, + queryFn: `queryAllByLabelText`, + }, + queryAllByText: { + dom: ` +

+ Content + with + linebreaks + is + ok +

`, + query: `Content with linebreaks is ok`, + queryFn: `queryAllByText`, + }, + queryAllByAltText: { + dom: ` + Finding Nemo poster`, + query: `Finding Nemo poster`, + queryFn: `queryAllByAltText`, + }, }, - [ - {name: 'string match', textMatch: 'About'}, - {name: 'case insensitive', textMatch: 'about'}, - {name: 'regex', textMatch: /^about$/i}, - { - name: 'function', - textMatch: (text, element) => - element.tagName === 'A' && text.includes('out'), - }, - ], ) diff --git a/src/get-node-text.js b/src/get-node-text.js index dc1c8a67..77db5dea 100644 --- a/src/get-node-text.js +++ b/src/get-node-text.js @@ -5,8 +5,6 @@ function getNodeText(node) { ) .map(c => c.textContent) .join(' ') - .trim() - .replace(/\s+/g, ' ') } export {getNodeText} diff --git a/src/matches.js b/src/matches.js index 96c8f2b3..1a79cb4f 100644 --- a/src/matches.js +++ b/src/matches.js @@ -1,8 +1,13 @@ -function matches(textToMatch, node, matcher) { +function fuzzyMatches( + textToMatch, + node, + matcher, + {collapseWhitespace = true, trim = true} = {}, +) { if (typeof textToMatch !== 'string') { return false } - const normalizedText = textToMatch.trim().replace(/\s+/g, ' ') + const normalizedText = normalize(textToMatch, {trim, collapseWhitespace}) if (typeof matcher === 'string') { return normalizedText.toLowerCase().includes(matcher.toLowerCase()) } else if (typeof matcher === 'function') { @@ -12,17 +17,32 @@ function matches(textToMatch, node, matcher) { } } -function matchesExact(textToMatch, node, matcher) { +function matches( + textToMatch, + node, + matcher, + {collapseWhitespace = true, trim = true} = {}, +) { if (typeof textToMatch !== 'string') { return false } + const normalizedText = normalize(textToMatch, {trim, collapseWhitespace}) if (typeof matcher === 'string') { - return textToMatch === matcher + return normalizedText === matcher } else if (typeof matcher === 'function') { - return matcher(textToMatch, node) + return matcher(normalizedText, node) } else { - return matcher.test(textToMatch) + return matcher.test(normalizedText) } } -export {matches, matchesExact} +function normalize(text, {trim, collapseWhitespace}) { + let normalizedText = text + normalizedText = trim ? normalizedText.trim() : normalizedText + normalizedText = collapseWhitespace + ? normalizedText.replace(/\s+/g, ' ') + : normalizedText + return normalizedText +} + +export {fuzzyMatches, matches} diff --git a/src/queries.js b/src/queries.js index 7e666549..f7b8e806 100644 --- a/src/queries.js +++ b/src/queries.js @@ -1,4 +1,4 @@ -import {matches, matchesExact} from './matches' +import {fuzzyMatches, matches} from './matches' import {getNodeText} from './get-node-text' import {prettyDOM} from './pretty-dom' @@ -16,14 +16,25 @@ function firstResultOrNull(queryFunction, ...args) { return result[0] } -function queryAllLabelsByText(container, text) { +function queryAllLabelsByText( + container, + text, + {exact = true, trim = true, collapseWhitespace = true} = {}, +) { + const matcher = exact ? matches : fuzzyMatches + const matchOpts = {collapseWhitespace, trim} return Array.from(container.querySelectorAll('label')).filter(label => - matches(label.textContent, label, text), + matcher(label.textContent, label, text, matchOpts), ) } -function queryAllByLabelText(container, text, {selector = '*'} = {}) { - const labels = queryAllLabelsByText(container, text) +function queryAllByLabelText( + container, + text, + {selector = '*', exact = true, collapseWhitespace = true, trim = true} = {}, +) { + const matchOpts = {collapseWhitespace, trim} + const labels = queryAllLabelsByText(container, text, {exact, ...matchOpts}) const labelledElements = labels .map(label => { /* istanbul ignore if */ @@ -49,53 +60,43 @@ function queryAllByLabelText(container, text, {selector = '*'} = {}) { } }) .filter(label => label !== null) - .concat(queryAllByAttribute('aria-label', container, text)) + .concat(queryAllByAttribute('aria-label', container, text, {exact})) return labelledElements } -function queryByLabelText(container, text, opts) { - return firstResultOrNull(queryAllByLabelText, container, text, opts) +function queryByLabelText(...args) { + return firstResultOrNull(queryAllByLabelText, ...args) } -function queryAllByText(container, text, {selector = '*'} = {}) { +function queryAllByText( + container, + text, + {selector = '*', exact = true, collapseWhitespace = true, trim = true} = {}, +) { + const matcher = exact ? matches : fuzzyMatches + const matchOpts = {collapseWhitespace, trim} return Array.from(container.querySelectorAll(selector)).filter(node => - matches(getNodeText(node), node, text), + matcher(getNodeText(node), node, text, matchOpts), ) } -function queryByText(container, text, opts) { - return firstResultOrNull(queryAllByText, container, text, opts) -} - -const queryAllByTitle = (...args) => - queryAllByAttribute('title', ...args, {exact: true}) - -const queryByTitle = (...args) => - queryByAttribute('title', ...args, {exact: true}) - -function getAllByTitle(container, title, ...rest) { - const els = queryAllByTitle(container, title, ...rest) - if (!els.length) { - throw new Error( - `Unable to find an element with the title: ${title}. \n\n${debugDOM( - container, - )}`, - ) - } - return els -} - -function getByTitle(...args) { - return firstResultOrNull(getAllByTitle, ...args) +function queryByText(...args) { + return firstResultOrNull(queryAllByText, ...args) } // this is just a utility and not an exposed query. // There are no plans to expose this. -function queryAllByAttribute(attribute, container, text, {exact = false} = {}) { - const matcher = exact ? matchesExact : matches +function queryAllByAttribute( + attribute, + container, + text, + {exact = true, collapseWhitespace = true, trim = true} = {}, +) { + const matcher = exact ? matches : fuzzyMatches + const matchOpts = {collapseWhitespace, trim} return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node => - matcher(node.getAttribute(attribute), node, text), + matcher(node.getAttribute(attribute), node, text, matchOpts), ) } @@ -107,19 +108,25 @@ function queryByAttribute(...args) { const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder') const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder') -const queryByTestId = (...args) => - queryByAttribute('data-testid', ...args, {exact: true}) -const queryAllByTestId = (...args) => - queryAllByAttribute('data-testid', ...args, {exact: true}) +const queryByTestId = queryByAttribute.bind(null, 'data-testid') +const queryAllByTestId = queryAllByAttribute.bind(null, 'data-testid') +const queryByTitle = queryByAttribute.bind(null, 'title') +const queryAllByTitle = queryAllByAttribute.bind(null, 'title') -function queryAllByAltText(container, alt) { +function queryAllByAltText( + container, + alt, + {exact = true, collapseWhitespace = true, trim = true} = {}, +) { + const matcher = exact ? matches : fuzzyMatches + const matchOpts = {collapseWhitespace, trim} return Array.from(container.querySelectorAll('img,input,area')).filter(node => - matches(node.getAttribute('alt'), node, alt), + matcher(node.getAttribute('alt'), node, alt, matchOpts), ) } -function queryByAltText(container, alt) { - return firstResultOrNull(queryAllByAltText, container, alt) +function queryByAltText(...args) { + return firstResultOrNull(queryAllByAltText, ...args) } // getters @@ -143,6 +150,22 @@ function getByTestId(...args) { return firstResultOrNull(getAllByTestId, ...args) } +function getAllByTitle(container, title, ...rest) { + const els = queryAllByTitle(container, title, ...rest) + if (!els.length) { + throw new Error( + `Unable to find an element with the title: ${title}. \n\n${debugDOM( + container, + )}`, + ) + } + return els +} + +function getByTitle(...args) { + return firstResultOrNull(getAllByTitle, ...args) +} + function getAllByPlaceholderText(container, text, ...rest) { const els = queryAllByPlaceholderText(container, text, ...rest) if (!els.length) { @@ -162,7 +185,7 @@ function getByPlaceholderText(...args) { function getAllByLabelText(container, text, ...rest) { const els = queryAllByLabelText(container, text, ...rest) if (!els.length) { - const labels = queryAllLabelsByText(container, text) + const labels = queryAllLabelsByText(container, text, ...rest) 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( @@ -200,8 +223,8 @@ function getByText(...args) { return firstResultOrNull(getAllByText, ...args) } -function getAllByAltText(container, alt) { - const els = queryAllByAltText(container, alt) +function getAllByAltText(container, alt, ...rest) { + const els = queryAllByAltText(container, alt, ...rest) if (!els.length) { throw new Error( `Unable to find an element with the alt text: ${alt} \n\n${debugDOM(