From 0cf9fd9a8f3793240980c7cbafba273342ddc016 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 00:26:48 -0700 Subject: [PATCH 01/30] feat: suggestions for which query to use --- src/__tests__/suggestions-on-queries.js | 61 +++++++++++++ src/__tests__/suggestions.js | 110 ++++++++++++++++++++++++ src/queries/placeholder-text.js | 5 +- src/queries/test-id.js | 5 +- src/query-helpers.js | 35 +++++++- src/suggestions.js | 88 +++++++++++++++++++ 6 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/suggestions-on-queries.js create mode 100644 src/__tests__/suggestions.js create mode 100644 src/suggestions.js diff --git a/src/__tests__/suggestions-on-queries.js b/src/__tests__/suggestions-on-queries.js new file mode 100644 index 00000000..87f1719a --- /dev/null +++ b/src/__tests__/suggestions-on-queries.js @@ -0,0 +1,61 @@ +import {configure} from '../config' +import {renderIntoDocument} from './helpers/test-utils' +import {screen} from '..' + +beforeEach(() => { + configure({showSuggestions: true}) +}) + +test('should not show getByRole when using getByRole', () => { + renderIntoDocument(``) + + expect(() => screen.getByRole('button', {name: /submit/})).not.toThrowError() +}) + +test('should suggest getByRole when used with getBy', () => { + renderIntoDocument(``) + + expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(` +"A better query is available, try this: +*ByRole("button", {name:/submit/}) + + + + +" +`) +}) + +test('should suggest *ByRole when used with getAllBy', () => { + renderIntoDocument(` + + `) + + expect(() => screen.getAllByTestId('foo')) + .toThrowErrorMatchingInlineSnapshot(` +"A better query is available, try this: +*ByRole("button", {name:/submit/}) + + + + + + + + + +" +`) +}) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js new file mode 100644 index 00000000..ca0bfb83 --- /dev/null +++ b/src/__tests__/suggestions.js @@ -0,0 +1,110 @@ +import {screen} from '../' +import {getSuggestedQuery} from '../suggestions' +import {renderIntoDocument} from './helpers/test-utils' + +it('should not recommend anything when not possible', () => { + const {container} = renderIntoDocument(``) + + const element = container.firstChild + expect(getSuggestedQuery({element})).not.toBeDefined() +}) + +describe('Role', () => { + it('should recommend role for element with text', () => { + renderIntoDocument(``) + + const element = screen.getByTestId('foo') //omg the hypocrisy + const results = getSuggestedQuery({element}) + expect(results.toString()).toBe(`Role("button", {name:/submit/})`) + + expect(results).toEqual( + expect.objectContaining({ + queryName: 'Role', + role: 'button', + textContent: 'submit', + }), + ) + }) + + it('should recommend role on element without text', () => { + renderIntoDocument(``) + + const element = screen.getByRole('checkbox') + const results = getSuggestedQuery({element}) + expect(results.toString()).toBe(`Role("checkbox")`) + + expect(results).toEqual( + expect.objectContaining({ + queryName: 'Role', + role: 'checkbox', + textContent: '', + }), + ) + }) +}) + +describe('Form Fields (role not present)', () => { + it('should recommend LabelText on input', () => { + renderIntoDocument( + ``, + ) + + const element = screen.getByLabelText('Username') + + const results = getSuggestedQuery({element}) + expect(results.toString()).toBe(`LabelText("Username")`) + expect(results).toEqual( + expect.objectContaining({ + queryName: 'LabelText', + textContent: 'Username', + }), + ) + }) + + it('should recommend LabelText on nested input', () => { + renderIntoDocument(``) + + const element = screen.getByLabelText('Username') + + const results = getSuggestedQuery({element}) + expect(results.toString()).toBe(`LabelText("Username")`) + expect(results).toEqual( + expect.objectContaining({ + queryName: 'LabelText', + textContent: 'Username', + }), + ) + }) + + it('should recommend PlaceholderText on nested input', () => { + renderIntoDocument(``) + + const element = screen.getByPlaceholderText('Username') + + const results = getSuggestedQuery({element}) + expect(results.toString()).toBe(`PlaceholderText("Username")`) + expect(results).toEqual( + expect.objectContaining({ + queryName: 'PlaceholderText', + textContent: 'Username', + }), + ) + }) +}) + +describe('Text', () => { + it('should recommend Text', () => { + renderIntoDocument(`
hello there
`) + + const element = screen.getByText('hello there') + const results = getSuggestedQuery({element}) + + expect(results.toString()).toBe(`Text("hello there")`) + expect(results).toEqual( + expect.objectContaining({ + queryName: 'Text', + textContent: 'hello there', + }), + ) + }) +}) diff --git a/src/queries/placeholder-text.js b/src/queries/placeholder-text.js index 8e58ab5f..031cfcad 100644 --- a/src/queries/placeholder-text.js +++ b/src/queries/placeholder-text.js @@ -1,7 +1,8 @@ import {queryAllByAttribute, buildQueries} from './all-utils' -const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder') - +function queryAllByPlaceholderText(...args) { + return queryAllByAttribute('placeholder', ...args) +} const getMultipleError = (c, text) => `Found multiple elements with the placeholder text of: ${text}` const getMissingError = (c, text) => diff --git a/src/queries/test-id.js b/src/queries/test-id.js index 6d36c164..d4e28089 100644 --- a/src/queries/test-id.js +++ b/src/queries/test-id.js @@ -2,8 +2,9 @@ import {queryAllByAttribute, getConfig, buildQueries} from './all-utils' const getTestIdAttribute = () => getConfig().testIdAttribute -const queryAllByTestId = (...args) => - queryAllByAttribute(getTestIdAttribute(), ...args) +function queryAllByTestId(...args) { + return queryAllByAttribute(getTestIdAttribute(), ...args) +} const getMultipleError = (c, id) => `Found multiple elements by: [${getTestIdAttribute()}="${id}"]` diff --git a/src/query-helpers.js b/src/query-helpers.js index 7fcc6bd9..73512a4d 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -1,3 +1,4 @@ +import {getSuggestedQuery} from './suggestions' import {fuzzyMatches, matches, makeNormalizer} from './matches' import {waitFor} from './wait-for' import {getConfig} from './config' @@ -49,10 +50,29 @@ function makeSingleQuery(allQuery, getMultipleError) { container, ) } - return els[0] || null + const element = els[0] || null + + if (getConfig().showSuggestions) { + const suggestion = getSuggestedQuery({element}) + + if (allQuery.name.endsWith(suggestion.queryName)) { + throw getSuggestionError(suggestion, container) + } + } + + return element } } +function getSuggestionError(suggestion, container) { + return getConfig().getElementError( + `A better query is available, try this: +*By${suggestion.toString()} +`, + container, + ) +} + // this accepts a query function and returns a function which throws an error // if an empty list of elements is returned function makeGetAllQuery(allQuery, getMissingError) { @@ -64,6 +84,19 @@ function makeGetAllQuery(allQuery, getMissingError) { container, ) } + if (getConfig().showSuggestions) { + const rawSuggestions = els.map(element => getSuggestedQuery({element})) + const suggestions = [ + ...new Set(els.map(element => getSuggestedQuery({element}).toString())), + ] + + if ( + suggestions.length === 1 && + !allQuery.name.endsWith(rawSuggestions[0].queryName) + ) { + throw getSuggestionError(suggestions[0], container) + } + } return els } } diff --git a/src/suggestions.js b/src/suggestions.js new file mode 100644 index 00000000..66fc3d1a --- /dev/null +++ b/src/suggestions.js @@ -0,0 +1,88 @@ +/* eslint-disable babel/new-cap */ +import {getRoles} from './role-helpers' +const FORM_ELEMENTS = [ + 'button', + 'input', + 'meter', + 'output', + 'progress', + 'select', + 'textarea', +] + +function getSerializer(queryName, primaryMatch, secondaryMatch) { + return () => { + const options = secondaryMatch ? `, {name:/${secondaryMatch}/}` : '' + return `${queryName}("${primaryMatch}"${options})` + } +} + +// eslint-disable-next-line consistent-return +export function getSuggestedQuery({element}) { + const roles = getRoles(element) + + let queryName + const roleNames = Object.keys(roles) + let {textContent} = element + if (roleNames.length) { + queryName = 'Role' + const [role] = roleNames + + return { + queryName, + role, + textContent, + toString: getSerializer(queryName, role, textContent), + } + } + + if (FORM_ELEMENTS.includes(element.tagName.toLowerCase())) { + if (element.hasAttribute('id')) { + const label = document.querySelector( + `label[for="${element.getAttribute('id')}"]`, + ) + ;({textContent} = label) + + queryName = 'LabelText' + return { + queryName, + textContent, + toString: getSerializer(queryName, textContent), + } + } + + const allLabels = Array.from(document.querySelectorAll('label')) + + const labelWithControl = allLabels.find(label => label.control === element) + + if (labelWithControl) { + ;({textContent} = labelWithControl) + + queryName = 'LabelText' + return { + queryName, + textContent, + toString: getSerializer(queryName, textContent), + } + } + + if (element.hasAttribute('placeholder')) { + textContent = element.getAttribute('placeholder') + queryName = 'PlaceholderText' + return { + queryName, + textContent, + toString: getSerializer(queryName, textContent), + } + } + } + + if (textContent) { + queryName = 'Text' + return { + queryName, + textContent, + toString: getSerializer(queryName, textContent), + } + } +} From 7cb52ba8e037a75895fc12e0c7b196d44fd73cb0 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 00:48:18 -0700 Subject: [PATCH 02/30] coverage bumped --- src/__tests__/suggestions.js | 44 +++++++++++++++++++++--------------- src/query-helpers.js | 12 +--------- src/suggestions.js | 5 ++-- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index ca0bfb83..0dbd7ef9 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -2,11 +2,21 @@ import {screen} from '../' import {getSuggestedQuery} from '../suggestions' import {renderIntoDocument} from './helpers/test-utils' -it('should not recommend anything when not possible', () => { - const {container} = renderIntoDocument(``) +describe('Unable to suggest', () => { + it('should not recommend anything on empty span', () => { + const {container} = renderIntoDocument(``) - const element = container.firstChild - expect(getSuggestedQuery({element})).not.toBeDefined() + const element = container.firstChild + expect(getSuggestedQuery({element})).not.toBeDefined() + }) + + it('should not recommend PlaceholderText on input with empty placeholder', () => { + renderIntoDocument(``) + + const element = screen.getByTestId('foo') + + expect(getSuggestedQuery({element})).not.toBeDefined() + }) }) describe('Role', () => { @@ -76,7 +86,7 @@ describe('Form Fields (role not present)', () => { ) }) - it('should recommend PlaceholderText on nested input', () => { + it('should recommend PlaceholderText on input', () => { renderIntoDocument(``) const element = screen.getByPlaceholderText('Username') @@ -92,19 +102,17 @@ describe('Form Fields (role not present)', () => { }) }) -describe('Text', () => { - it('should recommend Text', () => { - renderIntoDocument(`
hello there
`) +it('should recommend Text', () => { + renderIntoDocument(`
hello there
`) - const element = screen.getByText('hello there') - const results = getSuggestedQuery({element}) + const element = screen.getByText('hello there') + const results = getSuggestedQuery({element}) - expect(results.toString()).toBe(`Text("hello there")`) - expect(results).toEqual( - expect.objectContaining({ - queryName: 'Text', - textContent: 'hello there', - }), - ) - }) + expect(results.toString()).toBe(`Text("hello there")`) + expect(results).toEqual( + expect.objectContaining({ + queryName: 'Text', + textContent: 'hello there', + }), + ) }) diff --git a/src/query-helpers.js b/src/query-helpers.js index 73512a4d..f9810adb 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -50,17 +50,7 @@ function makeSingleQuery(allQuery, getMultipleError) { container, ) } - const element = els[0] || null - - if (getConfig().showSuggestions) { - const suggestion = getSuggestedQuery({element}) - - if (allQuery.name.endsWith(suggestion.queryName)) { - throw getSuggestionError(suggestion, container) - } - } - - return element + return els[0] || null } } diff --git a/src/suggestions.js b/src/suggestions.js index 66fc3d1a..17a19753 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -66,8 +66,9 @@ export function getSuggestedQuery({element}) { } } - if (element.hasAttribute('placeholder')) { - textContent = element.getAttribute('placeholder') + const placeholderText = element.getAttribute('placeholder') + if (placeholderText) { + textContent = placeholderText queryName = 'PlaceholderText' return { queryName, From 75c492c2971e9dc860b51d61917b5ddd1c63bb6f Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 11:27:37 -0700 Subject: [PATCH 03/30] more tests, label text working --- src/__tests__/suggestions-on-queries.js | 22 +++++++++++++++++++- src/__tests__/suggestions.js | 18 ++++++++--------- src/config.js | 2 ++ src/query-helpers.js | 27 +++++++++++++++++-------- src/suggestions.js | 23 ++++++++++++++------- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/__tests__/suggestions-on-queries.js b/src/__tests__/suggestions-on-queries.js index 87f1719a..0609cbb0 100644 --- a/src/__tests__/suggestions-on-queries.js +++ b/src/__tests__/suggestions-on-queries.js @@ -6,12 +6,18 @@ beforeEach(() => { configure({showSuggestions: true}) }) -test('should not show getByRole when using getByRole', () => { +it('should not suggest when using getByRole', () => { renderIntoDocument(``) expect(() => screen.getByRole('button', {name: /submit/})).not.toThrowError() }) +test('should not suggest when nothing available', () => { + renderIntoDocument(``) + + expect(() => screen.queryByTestId('foo')).not.toThrowError() +}) + test('should suggest getByRole when used with getBy', () => { renderIntoDocument(``) @@ -59,3 +65,17 @@ test('should suggest *ByRole when used with getAllBy', () => { " `) }) + +test('should suggest *ByLabelText when no role available', () => { + renderIntoDocument(``) +}) + +it('should suggest *ByRole when even if label is available', () => { + renderIntoDocument( + ``, + ) + + expect(() => screen.getByLabelText('Username')).toThrowError( + /\*ByRole\("textbox", \{name:\/Username\/\}\)/g, + ) +}) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index 0dbd7ef9..341c88d6 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -7,7 +7,7 @@ describe('Unable to suggest', () => { const {container} = renderIntoDocument(``) const element = container.firstChild - expect(getSuggestedQuery({element})).not.toBeDefined() + expect(getSuggestedQuery(element)).not.toBeDefined() }) it('should not recommend PlaceholderText on input with empty placeholder', () => { @@ -15,7 +15,7 @@ describe('Unable to suggest', () => { const element = screen.getByTestId('foo') - expect(getSuggestedQuery({element})).not.toBeDefined() + expect(getSuggestedQuery(element)).not.toBeDefined() }) }) @@ -24,7 +24,7 @@ describe('Role', () => { renderIntoDocument(``) const element = screen.getByTestId('foo') //omg the hypocrisy - const results = getSuggestedQuery({element}) + const results = getSuggestedQuery(element) expect(results.toString()).toBe(`Role("button", {name:/submit/})`) expect(results).toEqual( @@ -40,14 +40,14 @@ describe('Role', () => { renderIntoDocument(``) const element = screen.getByRole('checkbox') - const results = getSuggestedQuery({element}) + const results = getSuggestedQuery(element) expect(results.toString()).toBe(`Role("checkbox")`) expect(results).toEqual( expect.objectContaining({ queryName: 'Role', role: 'checkbox', - textContent: '', + textContent: undefined, }), ) }) @@ -61,7 +61,7 @@ describe('Form Fields (role not present)', () => { const element = screen.getByLabelText('Username') - const results = getSuggestedQuery({element}) + const results = getSuggestedQuery(element) expect(results.toString()).toBe(`LabelText("Username")`) expect(results).toEqual( expect.objectContaining({ @@ -76,7 +76,7 @@ describe('Form Fields (role not present)', () => { const element = screen.getByLabelText('Username') - const results = getSuggestedQuery({element}) + const results = getSuggestedQuery(element) expect(results.toString()).toBe(`LabelText("Username")`) expect(results).toEqual( expect.objectContaining({ @@ -91,7 +91,7 @@ describe('Form Fields (role not present)', () => { const element = screen.getByPlaceholderText('Username') - const results = getSuggestedQuery({element}) + const results = getSuggestedQuery(element) expect(results.toString()).toBe(`PlaceholderText("Username")`) expect(results).toEqual( expect.objectContaining({ @@ -106,7 +106,7 @@ it('should recommend Text', () => { renderIntoDocument(`
hello there
`) const element = screen.getByText('hello there') - const results = getSuggestedQuery({element}) + const results = getSuggestedQuery(element) expect(results.toString()).toBe(`Text("hello there")`) expect(results).toEqual( diff --git a/src/config.js b/src/config.js index 248c9008..2b32441e 100644 --- a/src/config.js +++ b/src/config.js @@ -19,6 +19,8 @@ let config = { //showOriginalStackTrace flag to show the full error stack traces for async errors showOriginalStackTrace: false, + showSuggestions: false, + // called when getBy* queries fail. (message, container) => Error getElementError(message, container) { const error = new Error( diff --git a/src/query-helpers.js b/src/query-helpers.js index f9810adb..5a770c5b 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -50,7 +50,15 @@ function makeSingleQuery(allQuery, getMultipleError) { container, ) } - return els[0] || null + const element = els[0] || null + + if (getConfig().showSuggestions) { + const suggestion = getSuggestedQuery(element) + if (suggestion && !allQuery.name.endsWith(suggestion.queryName)) { + throw getSuggestionError(suggestion.toString(), container) + } + } + return element } } @@ -75,16 +83,15 @@ function makeGetAllQuery(allQuery, getMissingError) { ) } if (getConfig().showSuggestions) { - const rawSuggestions = els.map(element => getSuggestedQuery({element})) - const suggestions = [ - ...new Set(els.map(element => getSuggestedQuery({element}).toString())), + const suggestionMessages = [ + ...new Set(els.map(element => getSuggestedQuery(element).toString())), ] - if ( - suggestions.length === 1 && - !allQuery.name.endsWith(rawSuggestions[0].queryName) + // only want to suggest if all the els have the same suggestion. + suggestionMessages.length === 1 && + !allQuery.name.endsWith(getSuggestedQuery(els[0]).queryName) ) { - throw getSuggestionError(suggestions[0], container) + throw getSuggestionError(suggestionMessages[0], container) } } return els @@ -101,6 +108,10 @@ function makeFindQuery(getter) { function buildQueries(queryAllBy, getMultipleError, getMissingError) { const queryBy = makeSingleQuery(queryAllBy, getMultipleError) const getAllBy = makeGetAllQuery(queryAllBy, getMissingError) + // Suggestions need to know how they're being used, so need to set the name of the allQuery + Object.defineProperty(getAllBy, 'name', { + value: queryAllBy.name.replace('query', 'get'), + }) const getBy = makeSingleQuery(getAllBy, getMultipleError) const findAllBy = makeFindQuery(getAllBy) const findBy = makeFindQuery(getBy) diff --git a/src/suggestions.js b/src/suggestions.js index 17a19753..3d79e166 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -1,4 +1,3 @@ -/* eslint-disable babel/new-cap */ import {getRoles} from './role-helpers' const FORM_ELEMENTS = [ 'button', @@ -18,7 +17,7 @@ function getSerializer(queryName, primaryMatch, secondaryMatch) { } // eslint-disable-next-line consistent-return -export function getSuggestedQuery({element}) { +export function getSuggestedQuery(element) { const roles = getRoles(element) let queryName @@ -27,7 +26,9 @@ export function getSuggestedQuery({element}) { if (roleNames.length) { queryName = 'Role' const [role] = roleNames - + if (!textContent) { + textContent = getLabelTextFor(element) + } return { queryName, role, @@ -38,10 +39,7 @@ export function getSuggestedQuery({element}) { if (FORM_ELEMENTS.includes(element.tagName.toLowerCase())) { if (element.hasAttribute('id')) { - const label = document.querySelector( - `label[for="${element.getAttribute('id')}"]`, - ) - ;({textContent} = label) + textContent = getLabelTextFor(element) queryName = 'LabelText' return { @@ -87,3 +85,14 @@ export function getSuggestedQuery({element}) { } } } +function getLabelTextFor(element) { + const label = document.querySelector( + `label[for="${element.getAttribute('id')}"]`, + ) + + if (label) { + return label.textContent + } + + return undefined +} From 51c377c0961426020e438957ec6d369c4ca2b3cb Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 14:04:32 -0700 Subject: [PATCH 04/30] more cases for labelText --- src/__tests__/suggestions-on-queries.js | 38 +++++++-- src/suggestions.js | 102 ++++++++++++------------ 2 files changed, 85 insertions(+), 55 deletions(-) diff --git a/src/__tests__/suggestions-on-queries.js b/src/__tests__/suggestions-on-queries.js index 0609cbb0..d64600a8 100644 --- a/src/__tests__/suggestions-on-queries.js +++ b/src/__tests__/suggestions-on-queries.js @@ -67,15 +67,43 @@ test('should suggest *ByRole when used with getAllBy', () => { }) test('should suggest *ByLabelText when no role available', () => { - renderIntoDocument(``) + renderIntoDocument( + ``, + ) + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByLabelText\("Username"\)/, + ) }) -it('should suggest *ByRole when even if label is available', () => { - renderIntoDocument( - ``, +test(`should suggest *ByLabel on non form elements`, () => { + renderIntoDocument(` +
+ Section One +

some content

+
+ `) + + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByLabelText\("Section One"\)/, ) +}) + +test.each([ + ``, + ``, + ``, +])('should suggest *ByRole over label %s', html => { + renderIntoDocument(html) expect(() => screen.getByLabelText('Username')).toThrowError( - /\*ByRole\("textbox", \{name:\/Username\/\}\)/g, + /\*ByRole\("textbox", \{name:\/Username\/\}\)/, + ) +}) + +test(`should recommend *ByText for simple elements`, () => { + renderIntoDocument(`
hello there
`) + + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByText\("hello there"\)/, ) }) diff --git a/src/suggestions.js b/src/suggestions.js index 3d79e166..7d3e61f9 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -1,13 +1,28 @@ import {getRoles} from './role-helpers' -const FORM_ELEMENTS = [ - 'button', - 'input', - 'meter', - 'output', - 'progress', - 'select', - 'textarea', -] + +function getLabelTextFor(element) { + let label + + const allLabels = Array.from(document.querySelectorAll('label')) + + label = allLabels.find(lbl => lbl.control === element) + + if (!label) { + const ariaLabelledBy = element.getAttribute('aria-labelledby') + + if (ariaLabelledBy) { + // 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 + label = document.querySelector(`[id=${ariaLabelledBy}]`) + } + } + + if (label) { + return label.textContent + } + + return undefined +} function getSerializer(queryName, primaryMatch, secondaryMatch) { return () => { @@ -37,45 +52,43 @@ export function getSuggestedQuery(element) { } } - if (FORM_ELEMENTS.includes(element.tagName.toLowerCase())) { - if (element.hasAttribute('id')) { - textContent = getLabelTextFor(element) - - queryName = 'LabelText' - return { - queryName, - textContent, - toString: getSerializer(queryName, textContent), - } + textContent = getLabelTextFor(element) + if (textContent) { + queryName = 'LabelText' + return { + queryName, + textContent, + toString: getSerializer(queryName, textContent), } + } - const allLabels = Array.from(document.querySelectorAll('label')) + // const allLabels = Array.from(document.querySelectorAll('label')) - const labelWithControl = allLabels.find(label => label.control === element) + // const labelWithControl = allLabels.find(label => label.control === element) - if (labelWithControl) { - ;({textContent} = labelWithControl) + // if (labelWithControl) { + // ; ({ textContent } = labelWithControl) - queryName = 'LabelText' - return { - queryName, - textContent, - toString: getSerializer(queryName, textContent), - } - } + // queryName = 'LabelText' + // return { + // queryName, + // textContent, + // toString: getSerializer(queryName, textContent), + // } + // } - const placeholderText = element.getAttribute('placeholder') - if (placeholderText) { - textContent = placeholderText - queryName = 'PlaceholderText' - return { - queryName, - textContent, - toString: getSerializer(queryName, textContent), - } + const placeholderText = element.getAttribute('placeholder') + if (placeholderText) { + textContent = placeholderText + queryName = 'PlaceholderText' + return { + queryName, + textContent, + toString: getSerializer(queryName, textContent), } } + ;({textContent} = element) if (textContent) { queryName = 'Text' return { @@ -85,14 +98,3 @@ export function getSuggestedQuery(element) { } } } -function getLabelTextFor(element) { - const label = document.querySelector( - `label[for="${element.getAttribute('id')}"]`, - ) - - if (label) { - return label.textContent - } - - return undefined -} From be480adcfc923e3bdf06523e398b11eaa379af59 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 14:05:20 -0700 Subject: [PATCH 05/30] removed commented out code --- src/suggestions.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/suggestions.js b/src/suggestions.js index 7d3e61f9..fefd1f52 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -62,21 +62,6 @@ export function getSuggestedQuery(element) { } } - // const allLabels = Array.from(document.querySelectorAll('label')) - - // const labelWithControl = allLabels.find(label => label.control === element) - - // if (labelWithControl) { - // ; ({ textContent } = labelWithControl) - - // queryName = 'LabelText' - // return { - // queryName, - // textContent, - // toString: getSerializer(queryName, textContent), - // } - // } - const placeholderText = element.getAttribute('placeholder') if (placeholderText) { textContent = placeholderText From 63b26cb2ff67940728df6b40e33a0c4f5c12f707 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 15:37:06 -0700 Subject: [PATCH 06/30] added *ByDisplayValue --- src/__tests__/suggestions-on-queries.js | 20 +++++++++++++++++++- src/query-helpers.js | 11 +++++++---- src/suggestions.js | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/__tests__/suggestions-on-queries.js b/src/__tests__/suggestions-on-queries.js index d64600a8..2601a31f 100644 --- a/src/__tests__/suggestions-on-queries.js +++ b/src/__tests__/suggestions-on-queries.js @@ -18,6 +18,14 @@ test('should not suggest when nothing available', () => { expect(() => screen.queryByTestId('foo')).not.toThrowError() }) +test(`should not suggest if the suggestion would give different results`, () => { + renderIntoDocument(` + + `) + + expect(() => screen.getAllByTestId('foo')).not.toThrowError() +}) + test('should suggest getByRole when used with getBy', () => { renderIntoDocument(``) @@ -100,10 +108,20 @@ test.each([ ) }) -test(`should recommend *ByText for simple elements`, () => { +test(`should suggest *ByText for simple elements`, () => { renderIntoDocument(`
hello there
`) expect(() => screen.getByTestId('foo')).toThrowError( /\*ByText\("hello there"\)/, ) }) + +test(`should suggest *ByDisplayValue`, () => { + renderIntoDocument(``) + + document.getElementById('lastName').value = 'Prine' // RIP John Prine + + expect(() => screen.getByTestId('lastName')).toThrowError( + /\*ByDisplayValue\("Prine"\)/, + ) +}) diff --git a/src/query-helpers.js b/src/query-helpers.js index 5a770c5b..4981dc95 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -83,15 +83,18 @@ function makeGetAllQuery(allQuery, getMissingError) { ) } if (getConfig().showSuggestions) { - const suggestionMessages = [ - ...new Set(els.map(element => getSuggestedQuery(element).toString())), + //get a unique list of all suggestion messages. We are only going to make a suggestion if + // all the suggestions are the same + const uniqueSuggestionMessages = [ + ...new Set(els.map(element => getSuggestedQuery(element)?.toString())), ] + if ( // only want to suggest if all the els have the same suggestion. - suggestionMessages.length === 1 && + uniqueSuggestionMessages.length === 1 && !allQuery.name.endsWith(getSuggestedQuery(els[0]).queryName) ) { - throw getSuggestionError(suggestionMessages[0], container) + throw getSuggestionError(uniqueSuggestionMessages[0], container) } } return els diff --git a/src/suggestions.js b/src/suggestions.js index fefd1f52..ac073644 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -1,4 +1,7 @@ import {getRoles} from './role-helpers' +import {getDefaultNormalizer} from './queries/all-utils' + +const normalize = getDefaultNormalizer() function getLabelTextFor(element) { let label @@ -74,6 +77,7 @@ export function getSuggestedQuery(element) { } ;({textContent} = element) + textContent = normalize(textContent) if (textContent) { queryName = 'Text' return { @@ -82,4 +86,16 @@ export function getSuggestedQuery(element) { toString: getSerializer(queryName, textContent), } } + + if (element.value) { + queryName = 'DisplayValue' + + textContent = element.value + return { + queryName, + textContent, + + toString: getSerializer(queryName, textContent), + } + } } From a0dc8f9fa2f6c70a396460a3d72f950f7fd73af0 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 25 May 2020 22:27:46 -0700 Subject: [PATCH 07/30] all queries supported now --- src/__tests__/suggestions-on-queries.js | 127 ------------ src/__tests__/suggestions.js | 261 +++++++++++++++--------- src/suggestions.js | 72 +++---- 3 files changed, 186 insertions(+), 274 deletions(-) delete mode 100644 src/__tests__/suggestions-on-queries.js diff --git a/src/__tests__/suggestions-on-queries.js b/src/__tests__/suggestions-on-queries.js deleted file mode 100644 index 2601a31f..00000000 --- a/src/__tests__/suggestions-on-queries.js +++ /dev/null @@ -1,127 +0,0 @@ -import {configure} from '../config' -import {renderIntoDocument} from './helpers/test-utils' -import {screen} from '..' - -beforeEach(() => { - configure({showSuggestions: true}) -}) - -it('should not suggest when using getByRole', () => { - renderIntoDocument(``) - - expect(() => screen.getByRole('button', {name: /submit/})).not.toThrowError() -}) - -test('should not suggest when nothing available', () => { - renderIntoDocument(``) - - expect(() => screen.queryByTestId('foo')).not.toThrowError() -}) - -test(`should not suggest if the suggestion would give different results`, () => { - renderIntoDocument(` - - `) - - expect(() => screen.getAllByTestId('foo')).not.toThrowError() -}) - -test('should suggest getByRole when used with getBy', () => { - renderIntoDocument(``) - - expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(` -"A better query is available, try this: -*ByRole("button", {name:/submit/}) - - - - -" -`) -}) - -test('should suggest *ByRole when used with getAllBy', () => { - renderIntoDocument(` - - `) - - expect(() => screen.getAllByTestId('foo')) - .toThrowErrorMatchingInlineSnapshot(` -"A better query is available, try this: -*ByRole("button", {name:/submit/}) - - - - - - - - - -" -`) -}) - -test('should suggest *ByLabelText when no role available', () => { - renderIntoDocument( - ``, - ) - expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByLabelText\("Username"\)/, - ) -}) - -test(`should suggest *ByLabel on non form elements`, () => { - renderIntoDocument(` -
- Section One -

some content

-
- `) - - expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByLabelText\("Section One"\)/, - ) -}) - -test.each([ - ``, - ``, - ``, -])('should suggest *ByRole over label %s', html => { - renderIntoDocument(html) - - expect(() => screen.getByLabelText('Username')).toThrowError( - /\*ByRole\("textbox", \{name:\/Username\/\}\)/, - ) -}) - -test(`should suggest *ByText for simple elements`, () => { - renderIntoDocument(`
hello there
`) - - expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByText\("hello there"\)/, - ) -}) - -test(`should suggest *ByDisplayValue`, () => { - renderIntoDocument(``) - - document.getElementById('lastName').value = 'Prine' // RIP John Prine - - expect(() => screen.getByTestId('lastName')).toThrowError( - /\*ByDisplayValue\("Prine"\)/, - ) -}) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index 341c88d6..f0e58aed 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -1,118 +1,175 @@ -import {screen} from '../' -import {getSuggestedQuery} from '../suggestions' +import {configure} from '../config' import {renderIntoDocument} from './helpers/test-utils' +import {screen} from '..' -describe('Unable to suggest', () => { - it('should not recommend anything on empty span', () => { - const {container} = renderIntoDocument(``) +beforeEach(() => { + configure({showSuggestions: true}) +}) +it('should not suggest when using getByRole', () => { + renderIntoDocument(``) + + expect(() => screen.getByRole('button', {name: /submit/})).not.toThrowError() +}) - const element = container.firstChild - expect(getSuggestedQuery(element)).not.toBeDefined() - }) +test('should not suggest when nothing available', () => { + renderIntoDocument(``) - it('should not recommend PlaceholderText on input with empty placeholder', () => { - renderIntoDocument(``) + expect(() => screen.queryByTestId('foo')).not.toThrowError() +}) - const element = screen.getByTestId('foo') +test(`should not suggest if the suggestion would give different results`, () => { + renderIntoDocument(` + + `) - expect(getSuggestedQuery(element)).not.toBeDefined() - }) + expect(() => screen.getAllByTestId('foo')).not.toThrowError() }) -describe('Role', () => { - it('should recommend role for element with text', () => { - renderIntoDocument(``) - - const element = screen.getByTestId('foo') //omg the hypocrisy - const results = getSuggestedQuery(element) - expect(results.toString()).toBe(`Role("button", {name:/submit/})`) - - expect(results).toEqual( - expect.objectContaining({ - queryName: 'Role', - role: 'button', - textContent: 'submit', - }), - ) - }) - - it('should recommend role on element without text', () => { - renderIntoDocument(``) - - const element = screen.getByRole('checkbox') - const results = getSuggestedQuery(element) - expect(results.toString()).toBe(`Role("checkbox")`) - - expect(results).toEqual( - expect.objectContaining({ - queryName: 'Role', - role: 'checkbox', - textContent: undefined, - }), - ) - }) +test('should suggest getByRole when used with getBy', () => { + renderIntoDocument(``) + + expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(` +"A better query is available, try this: +*ByRole("button", {name:/submit/}) + + + + +" +`) }) -describe('Form Fields (role not present)', () => { - it('should recommend LabelText on input', () => { - renderIntoDocument( - ``, - ) - - const element = screen.getByLabelText('Username') - - const results = getSuggestedQuery(element) - expect(results.toString()).toBe(`LabelText("Username")`) - expect(results).toEqual( - expect.objectContaining({ - queryName: 'LabelText', - textContent: 'Username', - }), - ) - }) - - it('should recommend LabelText on nested input', () => { - renderIntoDocument(``) - - const element = screen.getByLabelText('Username') - - const results = getSuggestedQuery(element) - expect(results.toString()).toBe(`LabelText("Username")`) - expect(results).toEqual( - expect.objectContaining({ - queryName: 'LabelText', - textContent: 'Username', - }), - ) - }) - - it('should recommend PlaceholderText on input', () => { - renderIntoDocument(``) - - const element = screen.getByPlaceholderText('Username') - - const results = getSuggestedQuery(element) - expect(results.toString()).toBe(`PlaceholderText("Username")`) - expect(results).toEqual( - expect.objectContaining({ - queryName: 'PlaceholderText', - textContent: 'Username', - }), - ) - }) +test('should suggest *ByRole when used with getAllBy', () => { + renderIntoDocument(` + + `) + + expect(() => screen.getAllByTestId('foo')) + .toThrowErrorMatchingInlineSnapshot(` +"A better query is available, try this: +*ByRole("button", {name:/submit/}) + + + + + + + + + +" +`) }) -it('should recommend Text', () => { - renderIntoDocument(`
hello there
`) +test('should suggest img role w/ alt text', () => { + renderIntoDocument(`Incredibles 2 Poster`) - const element = screen.getByText('hello there') - const results = getSuggestedQuery(element) + expect(() => screen.getByAltText('Incredibles 2 Poster')).toThrowError( + /\*ByRole\("img", \{name:\/Incredibles 2 Poster\/\}\)/, + ) +}) - expect(results.toString()).toBe(`Text("hello there")`) - expect(results).toEqual( - expect.objectContaining({ - queryName: 'Text', - textContent: 'hello there', - }), +test('should suggest *ByLabelText when no role available', () => { + renderIntoDocument( + ``, ) + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByLabelText\("Username"\)/, + ) +}) + +test(`should suggest *ByLabel on non form elements`, () => { + renderIntoDocument(` +
+ Section One +

some content

+
+ `) + + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByLabelText\("Section One"\)/, + ) +}) + +test.each([ + ``, + ``, + ``, +])('should suggest *ByRole over label %s', html => { + renderIntoDocument(html) + + expect(() => screen.getByLabelText('Username')).toThrowError( + /\*ByRole\("textbox", \{name:\/Username\/\}\)/, + ) +}) + +test(`should suggest *ByPlaceholderText`, () => { + renderIntoDocument(``) + + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByPlaceholderText\("Username"\)/, + ) +}) + +test(`should suggest *ByText for simple elements`, () => { + renderIntoDocument(`
hello there
`) + + expect(() => screen.getByTestId('foo')).toThrowError( + /\*ByText\("hello there"\)/, + ) +}) + +test(`should suggest *ByDisplayValue`, () => { + renderIntoDocument(``) + + document.getElementById('lastName').value = 'Prine' // RIP John Prine + + expect(() => screen.getByTestId('lastName')).toThrowError( + /\*ByDisplayValue\("Prine"\)/, + ) +}) + +test(`should suggest *ByAltText`, () => { + renderIntoDocument(` + + + Computer + + `) + + expect(() => screen.getByTestId('input')).toThrowError( + /\*ByAltText\("last name"\)/, + ) + expect(() => screen.getByTestId('area')).toThrowError( + /\*ByAltText\("Computer"\)/, + ) +}) + +test(`should suggest *ByTitle`, () => { + renderIntoDocument(` + + + Close + + `) + + expect(() => screen.getByTestId('delete')).toThrowError( + /\*ByTitle\("Delete"\)/, + ) + + //Since `ByTitle` and `ByText` will both return the element + //`getByText` will always be the suggested query as it is higher up the list. + expect(() => screen.getByTestId('svg')).toThrowError(/\*ByText\("Close"\)/) }) diff --git a/src/suggestions.js b/src/suggestions.js index ac073644..582ab02f 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -1,5 +1,6 @@ import {getRoles} from './role-helpers' import {getDefaultNormalizer} from './queries/all-utils' +import {computeAccessibleName} from 'dom-accessibility-api' const normalize = getDefaultNormalizer() @@ -27,10 +28,14 @@ function getLabelTextFor(element) { return undefined } -function getSerializer(queryName, primaryMatch, secondaryMatch) { - return () => { - const options = secondaryMatch ? `, {name:/${secondaryMatch}/}` : '' - return `${queryName}("${primaryMatch}"${options})` +function makeSuggestion(queryName, content, name) { + return { + queryName, + content, + toString() { + const options = name ? `, {name:/${name}/}` : '' + return `${queryName}("${content}"${options})` + }, } } @@ -38,64 +43,41 @@ function getSerializer(queryName, primaryMatch, secondaryMatch) { export function getSuggestedQuery(element) { const roles = getRoles(element) - let queryName const roleNames = Object.keys(roles) - let {textContent} = element + const name = computeAccessibleName(element) if (roleNames.length) { - queryName = 'Role' const [role] = roleNames - if (!textContent) { - textContent = getLabelTextFor(element) - } - return { - queryName, - role, - textContent, - toString: getSerializer(queryName, role, textContent), - } + return makeSuggestion('Role', role, name) } - textContent = getLabelTextFor(element) - if (textContent) { - queryName = 'LabelText' - return { - queryName, - textContent, - toString: getSerializer(queryName, textContent), - } + const labelText = getLabelTextFor(element) + if (labelText) { + return makeSuggestion('LabelText', labelText) } const placeholderText = element.getAttribute('placeholder') if (placeholderText) { - textContent = placeholderText - queryName = 'PlaceholderText' - return { - queryName, - textContent, - toString: getSerializer(queryName, textContent), - } + return makeSuggestion('PlaceholderText', placeholderText) } - ;({textContent} = element) + let {textContent} = element textContent = normalize(textContent) if (textContent) { - queryName = 'Text' - return { - queryName, - textContent, - toString: getSerializer(queryName, textContent), - } + return makeSuggestion('Text', textContent) } if (element.value) { - queryName = 'DisplayValue' + return makeSuggestion('DisplayValue', normalize(element.value)) + } - textContent = element.value - return { - queryName, - textContent, + const alt = element.getAttribute('alt') + if (alt) { + return makeSuggestion('AltText', alt) + } - toString: getSerializer(queryName, textContent), - } + const title = element.getAttribute('title') + + if (title) { + return makeSuggestion('Title', title) } } From 5ee96493cbfda878a068e20007ceae1a73f1e980 Mon Sep 17 00:00:00 2001 From: Ben Monro <benjamin.monro@walmartlabs.com> Date: Mon, 25 May 2020 22:48:10 -0700 Subject: [PATCH 08/30] cleanup --- src/config.js | 1 + src/suggestions.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/config.js b/src/config.js index 2b32441e..cd9bc986 100644 --- a/src/config.js +++ b/src/config.js @@ -19,6 +19,7 @@ let config = { //showOriginalStackTrace flag to show the full error stack traces for async errors showOriginalStackTrace: false, + //throw errors w/ suggestions for better queries. opt in so off by default. showSuggestions: false, // called when getBy* queries fail. (message, container) => Error diff --git a/src/suggestions.js b/src/suggestions.js index 582ab02f..641e4744 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -31,7 +31,6 @@ function getLabelTextFor(element) { function makeSuggestion(queryName, content, name) { return { queryName, - content, toString() { const options = name ? `, {name:/${name}/}` : '' return `${queryName}("${content}"${options})` @@ -39,7 +38,6 @@ function makeSuggestion(queryName, content, name) { } } -// eslint-disable-next-line consistent-return export function getSuggestedQuery(element) { const roles = getRoles(element) @@ -80,4 +78,6 @@ export function getSuggestedQuery(element) { if (title) { return makeSuggestion('Title', title) } + + return undefined } From 625ce158540788c85014a3e8b3875fac5eb83c5a Mon Sep 17 00:00:00 2001 From: Ben Monro <benjamin.monro@walmartlabs.com> Date: Mon, 25 May 2020 22:49:32 -0700 Subject: [PATCH 09/30] added types for suggestions --- types/suggestions.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 types/suggestions.d.ts diff --git a/types/suggestions.d.ts b/types/suggestions.d.ts new file mode 100644 index 00000000..f574f344 --- /dev/null +++ b/types/suggestions.d.ts @@ -0,0 +1,6 @@ +export interface Suggestion { + queryName: string + toString(): string +} + +export function getSuggestedQuery(element: HTMLElement): Suggestion | undefined From 1db13e6cc191c575a5e92f3989e7283e1787d0b5 Mon Sep 17 00:00:00 2001 From: Ben Monro <benjamin.monro@walmartlabs.com> Date: Mon, 25 May 2020 23:01:19 -0700 Subject: [PATCH 10/30] export suggestions from index --- src/index.js | 1 + types/index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index aff9388d..e5888b99 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,7 @@ export * from './query-helpers' export {getRoles, logRoles, isInaccessible} from './role-helpers' export * from './pretty-dom' export {configure} from './config' +export * from './suggestions' export { // "within" reads better in user-code diff --git a/types/index.d.ts b/types/index.d.ts index 406db91c..5b199dcf 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -22,3 +22,4 @@ export * from './get-queries-for-element'; export * from './pretty-dom'; export * from './role-helpers'; export * from './config'; +export * from './suggestions'; From 30e64e2689f5ad7fd14bdc512c59267504fbb413 Mon Sep 17 00:00:00 2001 From: Ben Monro <benjamin.monro@walmartlabs.com> Date: Mon, 25 May 2020 23:08:41 -0700 Subject: [PATCH 11/30] fixed a couple lint warnings --- src/__tests__/suggestions.js | 2 +- src/suggestions.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index f0e58aed..bd6f83ad 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -1,6 +1,6 @@ import {configure} from '../config' -import {renderIntoDocument} from './helpers/test-utils' import {screen} from '..' +import {renderIntoDocument} from './helpers/test-utils' beforeEach(() => { configure({showSuggestions: true}) diff --git a/src/suggestions.js b/src/suggestions.js index 641e4744..6668d259 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -1,6 +1,6 @@ -import {getRoles} from './role-helpers' -import {getDefaultNormalizer} from './queries/all-utils' import {computeAccessibleName} from 'dom-accessibility-api' +import {getRoles} from './role-helpers' +import {getDefaultNormalizer} from './matches' const normalize = getDefaultNormalizer() From e3bdcff32eba36b4530f467e977f296c270461e2 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 17:03:06 -0700 Subject: [PATCH 12/30] Update src/__tests__/suggestions.js Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> --- src/__tests__/suggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index bd6f83ad..780f2f54 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -5,7 +5,7 @@ import {renderIntoDocument} from './helpers/test-utils' beforeEach(() => { configure({showSuggestions: true}) }) -it('should not suggest when using getByRole', () => { +test('does not suggest when using getByRole', () => { renderIntoDocument(`<button data-testid="foo">submit</button>`) expect(() => screen.getByRole('button', {name: /submit/})).not.toThrowError() From a9394b0b1d13f680aa25f2fbb74d8665b770dbfb Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 17:07:23 -0700 Subject: [PATCH 13/30] Update src/config.js haha Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index cd9bc986..205875a4 100644 --- a/src/config.js +++ b/src/config.js @@ -19,7 +19,7 @@ let config = { //showOriginalStackTrace flag to show the full error stack traces for async errors showOriginalStackTrace: false, - //throw errors w/ suggestions for better queries. opt in so off by default. + // throw errors w/ suggestions for better queries. Opt in so off by default. showSuggestions: false, // called when getBy* queries fail. (message, container) => Error From b496f09115b1f0a9e16146d9903e41b1d9ab66cc Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 17:08:32 -0700 Subject: [PATCH 14/30] Update src/__tests__/suggestions.js Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> --- src/__tests__/suggestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index 780f2f54..f6c31397 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -169,7 +169,7 @@ test(`should suggest *ByTitle`, () => { /\*ByTitle\("Delete"\)/, ) - //Since `ByTitle` and `ByText` will both return the <title> element - //`getByText` will always be the suggested query as it is higher up the list. + // Since `ByTitle` and `ByText` will both return the <title> element + // `getByText` will always be the suggested query as it is higher up the list. expect(() => screen.getByTestId('svg')).toThrowError(/\*ByText\("Close"\)/) }) From 32883e74c8b50f0a91c8381b1295a65702aceba4 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 17:08:48 -0700 Subject: [PATCH 15/30] Update src/config.js Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 205875a4..f682b9e8 100644 --- a/src/config.js +++ b/src/config.js @@ -16,7 +16,7 @@ let config = { asyncWrapper: cb => cb(), // default value for the `hidden` option in `ByRole` queries defaultHidden: false, - //showOriginalStackTrace flag to show the full error stack traces for async errors + // showOriginalStackTrace flag to show the full error stack traces for async errors showOriginalStackTrace: false, // throw errors w/ suggestions for better queries. Opt in so off by default. From 7354bc7917174c5869fd19995e271916f2a5fae2 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 17:18:13 -0700 Subject: [PATCH 16/30] Update src/suggestions.js Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com> --- src/suggestions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/suggestions.js b/src/suggestions.js index 6668d259..54f50753 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -42,10 +42,9 @@ export function getSuggestedQuery(element) { const roles = getRoles(element) const roleNames = Object.keys(roles) - const name = computeAccessibleName(element) if (roleNames.length) { const [role] = roleNames - return makeSuggestion('Role', role, name) + return makeSuggestion('Role', role, computeAccessibleName(element)) } const labelText = getLabelTextFor(element) From effdd927dddaa85f09fea2d8a38f2fd04ed200e2 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 18:09:42 -0700 Subject: [PATCH 17/30] Update src/query-helpers.js --- src/query-helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/query-helpers.js b/src/query-helpers.js index 4981dc95..3b2858b4 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -83,7 +83,7 @@ function makeGetAllQuery(allQuery, getMissingError) { ) } if (getConfig().showSuggestions) { - //get a unique list of all suggestion messages. We are only going to make a suggestion if + // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same const uniqueSuggestionMessages = [ ...new Set(els.map(element => getSuggestedQuery(element)?.toString())), From 0d5c6e463ce525e0fe47ebcd31ef61f488aa8d64 Mon Sep 17 00:00:00 2001 From: Ben Monro <benjamin.monro@walmartlabs.com> Date: Thu, 28 May 2020 19:16:49 -0700 Subject: [PATCH 18/30] PR feedback --- src/suggestions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/suggestions.js b/src/suggestions.js index 6668d259..eb0ee597 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -58,8 +58,7 @@ export function getSuggestedQuery(element) { return makeSuggestion('PlaceholderText', placeholderText) } - let {textContent} = element - textContent = normalize(textContent) + const textContent = normalize(element.textContent) if (textContent) { return makeSuggestion('Text', textContent) } From 7ec23c2f9186aeb9cebba2ed01892411ba022223 Mon Sep 17 00:00:00 2001 From: Ben Monro <benjamin.monro@walmartlabs.com> Date: Thu, 28 May 2020 19:35:27 -0700 Subject: [PATCH 19/30] refactor to getLabelFor --- src/suggestions.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/suggestions.js b/src/suggestions.js index eb0ee597..bb30473e 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -5,15 +5,13 @@ import {getDefaultNormalizer} from './matches' const normalize = getDefaultNormalizer() function getLabelTextFor(element) { - let label - - const allLabels = Array.from(document.querySelectorAll('label')) - - label = allLabels.find(lbl => lbl.control === element) + let label = + element.labels && + Array.from(element.labels).find(el => Boolean(normalize(el.textContent))) + // non form elements that are using aria-labelledby won't be included in `element.labels` if (!label) { const ariaLabelledBy = element.getAttribute('aria-labelledby') - if (ariaLabelledBy) { // 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 From cf7861055c3f55319f2365acfae8c96623f9027d Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 19:39:57 -0700 Subject: [PATCH 20/30] formatting --- src/suggestions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/suggestions.js b/src/suggestions.js index 72da1cac..93029a32 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -22,7 +22,6 @@ function getLabelTextFor(element) { if (label) { return label.textContent } - return undefined } From 042bd93303a911546b147cefea5840b5f8070f88 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 20:35:02 -0700 Subject: [PATCH 21/30] added support for suggest:false --- src/__tests__/suggestions.js | 22 ++++++++++++++++++++-- src/query-helpers.js | 7 +++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index f6c31397..fa0d1557 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -2,9 +2,14 @@ import {configure} from '../config' import {screen} from '..' import {renderIntoDocument} from './helpers/test-utils' -beforeEach(() => { +beforeAll(() => { configure({showSuggestions: true}) }) + +afterAll(() => { + configure({showSuggestions: false}) +}) + test('does not suggest when using getByRole', () => { renderIntoDocument(`<button data-testid="foo">submit</button>`) @@ -22,7 +27,20 @@ test(`should not suggest if the suggestion would give different results`, () => <input type="text" data-testid="foo" /><span data-testid="foo" /> `) - expect(() => screen.getAllByTestId('foo')).not.toThrowError() + expect(() => + screen.getAllByTestId('foo', {suggest: false}), + ).not.toThrowError() +}) + +test('should not suggest when suggest is turned off for a query', () => { + renderIntoDocument(` + <button data-testid="foo">submit</button> + <button data-testid="foot">another</button>`) + + expect(() => screen.getByTestId('foo', {suggest: false})).not.toThrowError() + expect(() => + screen.getAllByTestId(/foo/, {suggest: false}), + ).not.toThrowError() }) test('should suggest getByRole when used with getBy', () => { diff --git a/src/query-helpers.js b/src/query-helpers.js index 3b2858b4..f7a8efa7 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -52,7 +52,9 @@ function makeSingleQuery(allQuery, getMultipleError) { } const element = els[0] || null - if (getConfig().showSuggestions) { + const [{suggest = true} = {}] = args.slice(-1) + + if (getConfig().showSuggestions && suggest) { const suggestion = getSuggestedQuery(element) if (suggestion && !allQuery.name.endsWith(suggestion.queryName)) { throw getSuggestionError(suggestion.toString(), container) @@ -82,7 +84,8 @@ function makeGetAllQuery(allQuery, getMissingError) { container, ) } - if (getConfig().showSuggestions) { + const [{suggest = true} = {}] = args.slice(-1) + if (suggest && getConfig().showSuggestions) { // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same const uniqueSuggestionMessages = [ From e536d05745dffad2a351ae483426911ef2d47d96 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 22:19:06 -0700 Subject: [PATCH 22/30] case ignored regex --- src/__tests__/suggestions.js | 10 +++++----- src/suggestions.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index fa0d1557..c14850e8 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -13,7 +13,7 @@ afterAll(() => { test('does not suggest when using getByRole', () => { renderIntoDocument(`<button data-testid="foo">submit</button>`) - expect(() => screen.getByRole('button', {name: /submit/})).not.toThrowError() + expect(() => screen.getByRole('button', {name: /submit/i})).not.toThrowError() }) test('should not suggest when nothing available', () => { @@ -48,7 +48,7 @@ test('should suggest getByRole when used with getBy', () => { expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(` "A better query is available, try this: -*ByRole("button", {name:/submit/}) +*ByRole("button", {name: /submit/i}) <body> @@ -69,7 +69,7 @@ test('should suggest *ByRole when used with getAllBy', () => { expect(() => screen.getAllByTestId('foo')) .toThrowErrorMatchingInlineSnapshot(` "A better query is available, try this: -*ByRole("button", {name:/submit/}) +*ByRole("button", {name: /submit/i}) <body> @@ -95,7 +95,7 @@ test('should suggest img role w/ alt text', () => { renderIntoDocument(`<img data-testid="img" alt="Incredibles 2 Poster" />`) expect(() => screen.getByAltText('Incredibles 2 Poster')).toThrowError( - /\*ByRole\("img", \{name:\/Incredibles 2 Poster\/\}\)/, + /\*ByRole\("img", \{name: \/incredibles 2 poster\/i\}\)/, ) }) @@ -129,7 +129,7 @@ test.each([ renderIntoDocument(html) expect(() => screen.getByLabelText('Username')).toThrowError( - /\*ByRole\("textbox", \{name:\/Username\/\}\)/, + /\*ByRole\("textbox", \{name: \/username\/i\}\)/, ) }) diff --git a/src/suggestions.js b/src/suggestions.js index 93029a32..4b2de687 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -29,7 +29,7 @@ function makeSuggestion(queryName, content, name) { return { queryName, toString() { - const options = name ? `, {name:/${name}/}` : '' + const options = name ? `, {name: /${name.toLowerCase()}/i}` : '' return `${queryName}("${content}"${options})` }, } From 8fb28da33e6028858a23fafc06494c67a8f125c2 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Thu, 28 May 2020 23:44:21 -0700 Subject: [PATCH 23/30] using full query name for get & getAll & query --- src/__tests__/suggestions.js | 52 ++++++++++-------- src/queries/label-text.js | 7 ++- src/query-helpers.js | 102 +++++++++++++++++++++++------------ src/suggestions.js | 23 ++++---- 4 files changed, 116 insertions(+), 68 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index c14850e8..4b2efb13 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -32,6 +32,14 @@ test(`should not suggest if the suggestion would give different results`, () => ).not.toThrowError() }) +test('should not suggest if there would be mixed suggestions', () => { + renderIntoDocument(` + <button data-testid="foo">submit</button> + <label for="foo">Username</label><input data-testid="foo" id="foo" />`) + + expect(() => screen.getAllByTestId('foo')).not.toThrowError() +}) + test('should not suggest when suggest is turned off for a query', () => { renderIntoDocument(` <button data-testid="foo">submit</button> @@ -48,7 +56,7 @@ test('should suggest getByRole when used with getBy', () => { expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(` "A better query is available, try this: -*ByRole("button", {name: /submit/i}) +getByRole("button", {name: /submit/i}) <body> @@ -61,7 +69,7 @@ test('should suggest getByRole when used with getBy', () => { `) }) -test('should suggest *ByRole when used with getAllBy', () => { +test('should suggest getAllByRole when used with getAllByTestId', () => { renderIntoDocument(` <button data-testid="foo">submit</button> <button data-testid="foo">submit</button>`) @@ -69,7 +77,7 @@ test('should suggest *ByRole when used with getAllBy', () => { expect(() => screen.getAllByTestId('foo')) .toThrowErrorMatchingInlineSnapshot(` "A better query is available, try this: -*ByRole("button", {name: /submit/i}) +getAllByRole("button", {name: /submit/i}) <body> @@ -95,20 +103,20 @@ test('should suggest img role w/ alt text', () => { renderIntoDocument(`<img data-testid="img" alt="Incredibles 2 Poster" />`) expect(() => screen.getByAltText('Incredibles 2 Poster')).toThrowError( - /\*ByRole\("img", \{name: \/incredibles 2 poster\/i\}\)/, + /getByRole\("img", \{name: \/incredibles 2 poster\/i\}\)/, ) }) -test('should suggest *ByLabelText when no role available', () => { +test('should suggest getByLabelText when no role available', () => { renderIntoDocument( `<label for="foo">Username</label><input data-testid="foo" id="foo" />`, ) expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByLabelText\("Username"\)/, + /getByLabelText\("Username"\)/, ) }) -test(`should suggest *ByLabel on non form elements`, () => { +test(`should suggest getByLabel on non form elements`, () => { renderIntoDocument(` <div data-testid="foo" aria-labelledby="section-one-header"> <span id="section-one-header">Section One</span> @@ -117,7 +125,7 @@ test(`should suggest *ByLabel on non form elements`, () => { `) expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByLabelText\("Section One"\)/, + /getByLabelText\("Section One"\)/, ) }) @@ -125,41 +133,41 @@ test.each([ `<label id="username-label">Username</label><input aria-labelledby="username-label" type="text" />`, `<label><span>Username</span><input type="text" /></label>`, `<label for="foo">Username</label><input id="foo" type="text" />`, -])('should suggest *ByRole over label %s', html => { +])('should suggest getByRole over label %s', html => { renderIntoDocument(html) expect(() => screen.getByLabelText('Username')).toThrowError( - /\*ByRole\("textbox", \{name: \/username\/i\}\)/, + /getByRole\("textbox", \{name: \/username\/i\}\)/, ) }) -test(`should suggest *ByPlaceholderText`, () => { +test(`should suggest getByPlaceholderText`, () => { renderIntoDocument(`<input data-testid="foo" placeholder="Username" />`) expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByPlaceholderText\("Username"\)/, + /getByPlaceholderText\("Username"\)/, ) }) -test(`should suggest *ByText for simple elements`, () => { +test(`should suggest getByText for simple elements`, () => { renderIntoDocument(`<div data-testid="foo">hello there</div>`) expect(() => screen.getByTestId('foo')).toThrowError( - /\*ByText\("hello there"\)/, + /getByText\("hello there"\)/, ) }) -test(`should suggest *ByDisplayValue`, () => { +test(`should suggest getByDisplayValue`, () => { renderIntoDocument(`<input id="lastName" data-testid="lastName" />`) document.getElementById('lastName').value = 'Prine' // RIP John Prine expect(() => screen.getByTestId('lastName')).toThrowError( - /\*ByDisplayValue\("Prine"\)/, + /getByDisplayValue\("Prine"\)/, ) }) -test(`should suggest *ByAltText`, () => { +test(`should suggest getByAltText`, () => { renderIntoDocument(` <input data-testid="input" alt="last name" /> <map name="workmap"> @@ -168,14 +176,14 @@ test(`should suggest *ByAltText`, () => { `) expect(() => screen.getByTestId('input')).toThrowError( - /\*ByAltText\("last name"\)/, + /getByAltText\("last name"\)/, ) expect(() => screen.getByTestId('area')).toThrowError( - /\*ByAltText\("Computer"\)/, + /getByAltText\("Computer"\)/, ) }) -test(`should suggest *ByTitle`, () => { +test(`should suggest getByTitle`, () => { renderIntoDocument(` <span title="Delete" data-testid="delete"></span> <svg> @@ -184,10 +192,10 @@ test(`should suggest *ByTitle`, () => { </svg>`) expect(() => screen.getByTestId('delete')).toThrowError( - /\*ByTitle\("Delete"\)/, + /getByTitle\("Delete"\)/, ) // Since `ByTitle` and `ByText` will both return the <title> element // `getByText` will always be the suggested query as it is higher up the list. - expect(() => screen.getByTestId('svg')).toThrowError(/\*ByText\("Close"\)/) + expect(() => screen.getByTestId('svg')).toThrowError(/getByText\("Close"\)/) }) diff --git a/src/queries/label-text.js b/src/queries/label-text.js index 94eeed79..d0e13651 100644 --- a/src/queries/label-text.js +++ b/src/queries/label-text.js @@ -6,6 +6,7 @@ import { queryAllByAttribute, makeFindQuery, makeSingleQuery, + wrapSingleQueryWithSuggestion, } from './all-utils' import {queryAllByText} from './text' @@ -140,7 +141,11 @@ function getAllByLabelText(container, text, ...rest) { const getMultipleError = (c, text) => `Found multiple elements with the text of: ${text}` const queryByLabelText = makeSingleQuery(queryAllByLabelText, getMultipleError) -const getByLabelText = makeSingleQuery(getAllByLabelText, getMultipleError) +const getByLabelText = wrapSingleQueryWithSuggestion( + makeSingleQuery(getAllByLabelText, getMultipleError), + getAllByLabelText.name, + 'get', +) const findAllByLabelText = makeFindQuery(getAllByLabelText) const findByLabelText = makeFindQuery(getByLabelText) diff --git a/src/query-helpers.js b/src/query-helpers.js index f7a8efa7..df8dd266 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -50,24 +50,14 @@ function makeSingleQuery(allQuery, getMultipleError) { container, ) } - const element = els[0] || null - - const [{suggest = true} = {}] = args.slice(-1) - - if (getConfig().showSuggestions && suggest) { - const suggestion = getSuggestedQuery(element) - if (suggestion && !allQuery.name.endsWith(suggestion.queryName)) { - throw getSuggestionError(suggestion.toString(), container) - } - } - return element + return els[0] || null } } function getSuggestionError(suggestion, container) { return getConfig().getElementError( `A better query is available, try this: -*By${suggestion.toString()} +${suggestion.toString()} `, container, ) @@ -84,22 +74,7 @@ function makeGetAllQuery(allQuery, getMissingError) { container, ) } - const [{suggest = true} = {}] = args.slice(-1) - if (suggest && getConfig().showSuggestions) { - // get a unique list of all suggestion messages. We are only going to make a suggestion if - // all the suggestions are the same - const uniqueSuggestionMessages = [ - ...new Set(els.map(element => getSuggestedQuery(element)?.toString())), - ] - - if ( - // only want to suggest if all the els have the same suggestion. - uniqueSuggestionMessages.length === 1 && - !allQuery.name.endsWith(getSuggestedQuery(els[0]).queryName) - ) { - throw getSuggestionError(uniqueSuggestionMessages[0], container) - } - } + return els } } @@ -111,22 +86,79 @@ function makeFindQuery(getter) { waitFor(() => getter(container, text, options), waitForOptions) } +const wrapSingleQueryWithSuggestion = (query, queryAllByName, variant) => ( + container, + ...args +) => { + const element = query(container, ...args) + const [{suggest = true} = {}] = args.slice(-1) + + if (getConfig().showSuggestions && suggest) { + const suggestion = getSuggestedQuery(element, variant) + if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) { + throw getSuggestionError(suggestion.toString(), container) + } + } + + return element +} + +const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( + container, + ...args +) => { + const els = query(container, ...args) + + const [{suggest = true}] = args.slice(-1) + if (suggest && getConfig().showSuggestions) { + // get a unique list of all suggestion messages. We are only going to make a suggestion if + // all the suggestions are the same + const uniqueSuggestionMessages = [ + ...new Set( + els.map(element => getSuggestedQuery(element, variant)?.toString()), + ), + ] + + if ( + // only want to suggest if all the els have the same suggestion. + uniqueSuggestionMessages.length === 1 && + !queryAllByName.endsWith(getSuggestedQuery(els[0], variant).queryName) + ) { + throw getSuggestionError(uniqueSuggestionMessages[0], container) + } + } + + return els +} + function buildQueries(queryAllBy, getMultipleError, getMissingError) { - const queryBy = makeSingleQuery(queryAllBy, getMultipleError) + const queryBy = wrapSingleQueryWithSuggestion( + makeSingleQuery(queryAllBy, getMultipleError), + ) const getAllBy = makeGetAllQuery(queryAllBy, getMissingError) - // Suggestions need to know how they're being used, so need to set the name of the allQuery - Object.defineProperty(getAllBy, 'name', { - value: queryAllBy.name.replace('query', 'get'), - }) - const getBy = makeSingleQuery(getAllBy, getMultipleError) + + const getBy = wrapSingleQueryWithSuggestion( + makeSingleQuery(getAllBy, getMultipleError), + queryAllBy.name, + 'get', + ) + + const getAllWithSuggestions = wrapAllByQueryWithSuggestion( + getAllBy, + queryAllBy.name.replace('query', 'get'), + 'getAll', + ) + const findAllBy = makeFindQuery(getAllBy) const findBy = makeFindQuery(getBy) - return [queryBy, getAllBy, getBy, findAllBy, findBy] + return [queryBy, getAllWithSuggestions, getBy, findAllBy, findBy] } export { getElementError, + wrapAllByQueryWithSuggestion, + wrapSingleQueryWithSuggestion, getMultipleElementsFoundError, queryAllByAttribute, queryByAttribute, diff --git a/src/suggestions.js b/src/suggestions.js index 4b2de687..7d109bd3 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -25,53 +25,56 @@ function getLabelTextFor(element) { return undefined } -function makeSuggestion(queryName, content, name) { +function makeSuggestion(queryName, content, {variant, name}) { return { queryName, toString() { const options = name ? `, {name: /${name.toLowerCase()}/i}` : '' - return `${queryName}("${content}"${options})` + return `${variant}By${queryName}("${content}"${options})` }, } } -export function getSuggestedQuery(element) { +export function getSuggestedQuery(element, variant) { const roles = getRoles(element) const roleNames = Object.keys(roles) if (roleNames.length) { const [role] = roleNames - return makeSuggestion('Role', role, computeAccessibleName(element)) + return makeSuggestion('Role', role, { + variant, + name: computeAccessibleName(element), + }) } const labelText = getLabelTextFor(element) if (labelText) { - return makeSuggestion('LabelText', labelText) + return makeSuggestion('LabelText', labelText, {variant}) } const placeholderText = element.getAttribute('placeholder') if (placeholderText) { - return makeSuggestion('PlaceholderText', placeholderText) + return makeSuggestion('PlaceholderText', placeholderText, {variant}) } const textContent = normalize(element.textContent) if (textContent) { - return makeSuggestion('Text', textContent) + return makeSuggestion('Text', textContent, {variant}) } if (element.value) { - return makeSuggestion('DisplayValue', normalize(element.value)) + return makeSuggestion('DisplayValue', normalize(element.value), {variant}) } const alt = element.getAttribute('alt') if (alt) { - return makeSuggestion('AltText', alt) + return makeSuggestion('AltText', alt, {variant}) } const title = element.getAttribute('title') if (title) { - return makeSuggestion('Title', title) + return makeSuggestion('Title', title, {variant}) } return undefined From 18b1813ea44e7efc4f6dfe5a7b57188652e1ab0d Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 08:01:44 -0700 Subject: [PATCH 24/30] suggest on labeltext --- src/__tests__/suggestions.js | 32 ++++++++++++++++++++++++- src/queries/label-text.js | 45 ++++++++++++++++++++++++++++-------- src/query-helpers.js | 29 +++++++++++++++-------- src/wait-for.js | 1 + 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index 4b2efb13..7c54c2ea 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -98,6 +98,19 @@ getAllByRole("button", {name: /submit/i}) </body>" `) }) +test('should suggest findByRole when used with findByTestId', async () => { + renderIntoDocument(` + <button data-testid="foo">submit</button> + <button data-testid="foot">submit</button> + `) + + await expect(screen.findByTestId('foo')).rejects.toThrowError( + /findByRole\("button", \{name: \/submit\/i\}\)/, + ) + await expect(screen.findAllByTestId(/foo/)).rejects.toThrowError( + /findAllByRole\("button", \{name: \/submit\/i\}\)/, + ) +}) test('should suggest img role w/ alt text', () => { renderIntoDocument(`<img data-testid="img" alt="Incredibles 2 Poster" />`) @@ -133,12 +146,29 @@ test.each([ `<label id="username-label">Username</label><input aria-labelledby="username-label" type="text" />`, `<label><span>Username</span><input type="text" /></label>`, `<label for="foo">Username</label><input id="foo" type="text" />`, -])('should suggest getByRole over label %s', html => { +])('%s\nshould suggest getByRole over', async html => { renderIntoDocument(html) expect(() => screen.getByLabelText('Username')).toThrowError( /getByRole\("textbox", \{name: \/username\/i\}\)/, ) + expect(() => screen.getAllByLabelText('Username')).toThrowError( + /getAllByRole\("textbox", \{name: \/username\/i\}\)/, + ) + + expect(() => screen.queryByLabelText('Username')).toThrowError( + /queryByRole\("textbox", \{name: \/username\/i\}\)/, + ) + expect(() => screen.queryAllByLabelText('Username')).toThrowError( + /queryAllByRole\("textbox", \{name: \/username\/i\}\)/, + ) + + await expect(screen.findByLabelText('Username')).rejects.toThrowError( + /findByRole\("textbox", \{name: \/username\/i\}\)/, + ) + await expect(screen.findAllByLabelText(/Username/)).rejects.toThrowError( + /findAllByRole\("textbox", \{name: \/username\/i\}\)/, + ) }) test(`should suggest getByPlaceholderText`, () => { diff --git a/src/queries/label-text.js b/src/queries/label-text.js index d0e13651..9e900025 100644 --- a/src/queries/label-text.js +++ b/src/queries/label-text.js @@ -6,6 +6,7 @@ import { queryAllByAttribute, makeFindQuery, makeSingleQuery, + wrapAllByQueryWithSuggestion, wrapSingleQueryWithSuggestion, } from './all-utils' import {queryAllByText} from './text' @@ -118,7 +119,7 @@ function queryAllByLabelText( // ) // 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 getAllByLabelText = (container, text, ...rest) => { const els = queryAllByLabelText(container, text, ...rest) if (!els.length) { const labels = queryAllLabelsByText(container, text, ...rest) @@ -140,21 +141,45 @@ function getAllByLabelText(container, text, ...rest) { // 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 = wrapSingleQueryWithSuggestion( - makeSingleQuery(getAllByLabelText, getMultipleError), +const queryByLabelText = wrapSingleQueryWithSuggestion( + makeSingleQuery(queryAllByLabelText, getMultipleError), + queryAllByLabelText.name, + 'query', +) +const getByLabelText = makeSingleQuery(getAllByLabelText, getMultipleError) + +const findAllByLabelText = makeFindQuery( + wrapAllByQueryWithSuggestion( + getAllByLabelText, + getAllByLabelText.name, + 'findAll', + ), +) +const findByLabelText = makeFindQuery( + wrapSingleQueryWithSuggestion(getByLabelText, getByLabelText.name, 'find'), +) + +const getAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion( + getAllByLabelText, + getAllByLabelText.name, + 'getAll', +) +const getByLabelTextWithSuggestions = wrapSingleQueryWithSuggestion( + getByLabelText, getAllByLabelText.name, 'get', ) -const findAllByLabelText = makeFindQuery(getAllByLabelText) -const findByLabelText = makeFindQuery(getByLabelText) - -export { +const queryAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion( queryAllByLabelText, + queryAllByLabelText.name, + 'queryAll', +) +export { + queryAllByLabelTextWithSuggestions as queryAllByLabelText, queryByLabelText, - getAllByLabelText, - getByLabelText, + getAllByLabelTextWithSuggestions as getAllByLabelText, + getByLabelTextWithSuggestions as getByLabelText, findAllByLabelText, findByLabelText, } diff --git a/src/query-helpers.js b/src/query-helpers.js index df8dd266..64264178 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -83,7 +83,9 @@ function makeGetAllQuery(allQuery, getMissingError) { // waitFor and passing a function which invokes the getter. function makeFindQuery(getter) { return (container, text, options, waitForOptions) => - waitFor(() => getter(container, text, options), waitForOptions) + waitFor(() => { + return getter(container, text, options) + }, waitForOptions) } const wrapSingleQueryWithSuggestion = (query, queryAllByName, variant) => ( @@ -92,7 +94,6 @@ const wrapSingleQueryWithSuggestion = (query, queryAllByName, variant) => ( ) => { const element = query(container, ...args) const [{suggest = true} = {}] = args.slice(-1) - if (getConfig().showSuggestions && suggest) { const suggestion = getSuggestedQuery(element, variant) if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) { @@ -109,7 +110,7 @@ const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( ) => { const els = query(container, ...args) - const [{suggest = true}] = args.slice(-1) + const [{suggest = true} = {}] = args.slice(-1) if (suggest && getConfig().showSuggestions) { // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same @@ -137,22 +138,32 @@ function buildQueries(queryAllBy, getMultipleError, getMissingError) { ) const getAllBy = makeGetAllQuery(queryAllBy, getMissingError) - const getBy = wrapSingleQueryWithSuggestion( - makeSingleQuery(getAllBy, getMultipleError), + const getBy = makeSingleQuery(getAllBy, getMultipleError) + const getByWithSuggestions = wrapSingleQueryWithSuggestion( + getBy, queryAllBy.name, 'get', ) - const getAllWithSuggestions = wrapAllByQueryWithSuggestion( getAllBy, queryAllBy.name.replace('query', 'get'), 'getAll', ) - const findAllBy = makeFindQuery(getAllBy) - const findBy = makeFindQuery(getBy) + const findAllBy = makeFindQuery( + wrapAllByQueryWithSuggestion(getAllBy, queryAllBy.name, 'findAll'), + ) + const findBy = makeFindQuery( + wrapSingleQueryWithSuggestion(getBy, queryAllBy.name, 'find'), + ) - return [queryBy, getAllWithSuggestions, getBy, findAllBy, findBy] + return [ + queryBy, + getAllWithSuggestions, + getByWithSuggestions, + findAllBy, + findBy, + ] } export { diff --git a/src/wait-for.js b/src/wait-for.js index 931a01a7..1dfbbbff 100644 --- a/src/wait-for.js +++ b/src/wait-for.js @@ -51,6 +51,7 @@ function waitFor( clearTimeout(overallTimeoutTimer) clearInterval(intervalId) setImmediate(() => observer.disconnect()) + if (error) { reject(error) } else { From 2a741e988dec502493a8973d817a28494611922f Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 08:08:56 -0700 Subject: [PATCH 25/30] suggest on queryAllBy --- src/queries/alt-text.js | 9 ++++++++- src/queries/display-value.js | 10 +++++++++- src/queries/placeholder-text.js | 9 ++++++++- src/queries/role.js | 9 +++++++-- src/queries/test-id.js | 9 ++++++++- src/queries/text.js | 9 ++++++++- src/queries/title.js | 9 ++++++++- 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/queries/alt-text.js b/src/queries/alt-text.js index debb2b33..aebda550 100644 --- a/src/queries/alt-text.js +++ b/src/queries/alt-text.js @@ -1,4 +1,5 @@ import {matches, fuzzyMatches, makeNormalizer, buildQueries} from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' function queryAllByAltText( container, @@ -16,6 +17,12 @@ 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 queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByAltText, + queryAllByAltText.name, + 'queryAll', +) const [ queryByAltText, getAllByAltText, @@ -26,7 +33,7 @@ const [ export { queryByAltText, - queryAllByAltText, + queryAllByAltTextWithSuggestions as queryAllByAltText, getByAltText, getAllByAltText, findAllByAltText, diff --git a/src/queries/display-value.js b/src/queries/display-value.js index d53df31a..a90966d5 100644 --- a/src/queries/display-value.js +++ b/src/queries/display-value.js @@ -5,6 +5,7 @@ import { makeNormalizer, buildQueries, } from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' function queryAllByDisplayValue( container, @@ -33,6 +34,13 @@ const getMultipleError = (c, value) => `Found multiple elements with the display value: ${value}.` const getMissingError = (c, value) => `Unable to find an element with the display value: ${value}.` + +const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByDisplayValue, + queryAllByDisplayValue.name, + 'queryAll', +) + const [ queryByDisplayValue, getAllByDisplayValue, @@ -43,7 +51,7 @@ const [ export { queryByDisplayValue, - queryAllByDisplayValue, + queryAllByDisplayValueWithSuggestions as queryAllByDisplayValue, getByDisplayValue, getAllByDisplayValue, findAllByDisplayValue, diff --git a/src/queries/placeholder-text.js b/src/queries/placeholder-text.js index 031cfcad..7f2bc62b 100644 --- a/src/queries/placeholder-text.js +++ b/src/queries/placeholder-text.js @@ -1,4 +1,5 @@ import {queryAllByAttribute, buildQueries} from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' function queryAllByPlaceholderText(...args) { return queryAllByAttribute('placeholder', ...args) @@ -8,6 +9,12 @@ const getMultipleError = (c, text) => const getMissingError = (c, text) => `Unable to find an element with the placeholder text of: ${text}` +const queryAllByPlaceholderTextWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByPlaceholderText, + queryAllByPlaceholderText.name, + 'queryAll', +) + const [ queryByPlaceholderText, getAllByPlaceholderText, @@ -18,7 +25,7 @@ const [ export { queryByPlaceholderText, - queryAllByPlaceholderText, + queryAllByPlaceholderTextWithSuggestions as queryAllByPlaceholderText, getByPlaceholderText, getAllByPlaceholderText, findAllByPlaceholderText, diff --git a/src/queries/role.js b/src/queries/role.js index b0e4c4f5..95b42273 100644 --- a/src/queries/role.js +++ b/src/queries/role.js @@ -14,6 +14,7 @@ import { makeNormalizer, matches, } from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' function queryAllByRole( container, @@ -154,7 +155,11 @@ Unable to find an ${ ${roleMessage}`.trim() } - +const queryAllByRoleWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByRole, + queryAllByRole.name, + 'queryAll', +) const [ queryByRole, getAllByRole, @@ -165,7 +170,7 @@ const [ export { queryByRole, - queryAllByRole, + queryAllByRoleWithSuggestions as queryAllByRole, getAllByRole, getByRole, findAllByRole, diff --git a/src/queries/test-id.js b/src/queries/test-id.js index d4e28089..5f5047da 100644 --- a/src/queries/test-id.js +++ b/src/queries/test-id.js @@ -1,4 +1,5 @@ import {queryAllByAttribute, getConfig, buildQueries} from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' const getTestIdAttribute = () => getConfig().testIdAttribute @@ -11,6 +12,12 @@ const getMultipleError = (c, id) => const getMissingError = (c, id) => `Unable to find an element by: [${getTestIdAttribute()}="${id}"]` +const queryAllByTestIdWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByTestId, + queryAllByTestId.name, + 'queryAll', +) + const [ queryByTestId, getAllByTestId, @@ -21,7 +28,7 @@ const [ export { queryByTestId, - queryAllByTestId, + queryAllByTestIdWithSuggestions as queryAllByTestId, getByTestId, getAllByTestId, findAllByTestId, diff --git a/src/queries/text.js b/src/queries/text.js index 18cf9cb7..aa7419d9 100644 --- a/src/queries/text.js +++ b/src/queries/text.js @@ -5,6 +5,7 @@ import { getNodeText, buildQueries, } from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' function queryAllByText( container, @@ -34,6 +35,12 @@ const getMultipleError = (c, 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 queryAllByTextWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByText, + queryAllByText.name, + 'queryAll', +) + const [ queryByText, getAllByText, @@ -44,7 +51,7 @@ const [ export { queryByText, - queryAllByText, + queryAllByTextWithSuggestions as queryAllByText, getByText, getAllByText, findAllByText, diff --git a/src/queries/title.js b/src/queries/title.js index c696572f..93c2cf5b 100644 --- a/src/queries/title.js +++ b/src/queries/title.js @@ -5,6 +5,7 @@ import { getNodeText, buildQueries, } from './all-utils' +import {wrapAllByQueryWithSuggestion} from '../query-helpers' function queryAllByTitle( container, @@ -25,6 +26,12 @@ const getMultipleError = (c, title) => const getMissingError = (c, title) => `Unable to find an element with the title: ${title}.` +const queryAllByTitleWithSuggestions = wrapAllByQueryWithSuggestion( + queryAllByTitle, + queryAllByTitle.name, + 'queryAll', +) + const [ queryByTitle, getAllByTitle, @@ -35,7 +42,7 @@ const [ export { queryByTitle, - queryAllByTitle, + queryAllByTitleWithSuggestions as queryAllByTitle, getByTitle, getAllByTitle, findAllByTitle, From ac9e9dfa73d6c4e78fe05805233dc42a20f6e74b Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 08:23:50 -0700 Subject: [PATCH 26/30] more tests --- src/__tests__/suggestions.js | 25 +++++++++++++++++++++++++ src/query-helpers.js | 2 ++ 2 files changed, 27 insertions(+) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index 7c54c2ea..c5db1594 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -171,6 +171,16 @@ test.each([ ) }) +test(`should suggest label over placeholder text`, () => { + renderIntoDocument( + `<label for="foo">Username</label><input id="foo" data-testid="foo" placeholder="Username" />`, + ) + + expect(() => screen.getByPlaceholderText('Username')).toThrowError( + /getByLabelText\("Username"\)/, + ) +}) + test(`should suggest getByPlaceholderText`, () => { renderIntoDocument(`<input data-testid="foo" placeholder="Username" />`) @@ -224,6 +234,21 @@ test(`should suggest getByTitle`, () => { expect(() => screen.getByTestId('delete')).toThrowError( /getByTitle\("Delete"\)/, ) + expect(() => screen.getAllByTestId('delete')).toThrowError( + /getAllByTitle\("Delete"\)/, + ) + expect(() => screen.queryByTestId('delete')).toThrowError( + /queryByTitle\("Delete"\)/, + ) + expect(() => screen.queryAllByTestId('delete')).toThrowError( + /queryAllByTitle\("Delete"\)/, + ) + expect(() => screen.queryAllByTestId('delete')).toThrowError( + /queryAllByTitle\("Delete"\)/, + ) + expect(() => screen.queryAllByTestId('delete')).toThrowError( + /queryAllByTitle\("Delete"\)/, + ) // Since `ByTitle` and `ByText` will both return the <title> element // `getByText` will always be the suggested query as it is higher up the list. diff --git a/src/query-helpers.js b/src/query-helpers.js index 64264178..1c490128 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -135,6 +135,8 @@ const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( function buildQueries(queryAllBy, getMultipleError, getMissingError) { const queryBy = wrapSingleQueryWithSuggestion( makeSingleQuery(queryAllBy, getMultipleError), + queryAllBy.name, + 'query', ) const getAllBy = makeGetAllQuery(queryAllBy, getMissingError) From 53460df4dcc98a127864fc14b3889b92baeb79e2 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 08:47:43 -0700 Subject: [PATCH 27/30] rename showSuggs to throwSuggs --- src/__tests__/suggestions.js | 4 ++-- src/config.js | 2 +- src/query-helpers.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/__tests__/suggestions.js b/src/__tests__/suggestions.js index c5db1594..75d6f90e 100644 --- a/src/__tests__/suggestions.js +++ b/src/__tests__/suggestions.js @@ -3,11 +3,11 @@ import {screen} from '..' import {renderIntoDocument} from './helpers/test-utils' beforeAll(() => { - configure({showSuggestions: true}) + configure({throwSuggestions: true}) }) afterAll(() => { - configure({showSuggestions: false}) + configure({throwSuggestions: false}) }) test('does not suggest when using getByRole', () => { diff --git a/src/config.js b/src/config.js index f682b9e8..ddc1deb6 100644 --- a/src/config.js +++ b/src/config.js @@ -20,7 +20,7 @@ let config = { showOriginalStackTrace: false, // throw errors w/ suggestions for better queries. Opt in so off by default. - showSuggestions: false, + throwSuggestions: false, // called when getBy* queries fail. (message, container) => Error getElementError(message, container) { diff --git a/src/query-helpers.js b/src/query-helpers.js index 1c490128..477744c1 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -94,7 +94,7 @@ const wrapSingleQueryWithSuggestion = (query, queryAllByName, variant) => ( ) => { const element = query(container, ...args) const [{suggest = true} = {}] = args.slice(-1) - if (getConfig().showSuggestions && suggest) { + if (getConfig().throwSuggestions && suggest) { const suggestion = getSuggestedQuery(element, variant) if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) { throw getSuggestionError(suggestion.toString(), container) @@ -111,7 +111,7 @@ const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( const els = query(container, ...args) const [{suggest = true} = {}] = args.slice(-1) - if (suggest && getConfig().showSuggestions) { + if (suggest && getConfig().throwSuggestions) { // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same const uniqueSuggestionMessages = [ From fa69912f861f3c7d27b28f1c7b5193ce6671d8f9 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 08:48:46 -0700 Subject: [PATCH 28/30] PR feedback --- src/query-helpers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/query-helpers.js b/src/query-helpers.js index 477744c1..0b7793f0 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.js @@ -93,8 +93,8 @@ const wrapSingleQueryWithSuggestion = (query, queryAllByName, variant) => ( ...args ) => { const element = query(container, ...args) - const [{suggest = true} = {}] = args.slice(-1) - if (getConfig().throwSuggestions && suggest) { + const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) + if (suggest) { const suggestion = getSuggestedQuery(element, variant) if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) { throw getSuggestionError(suggestion.toString(), container) @@ -110,8 +110,8 @@ const wrapAllByQueryWithSuggestion = (query, queryAllByName, variant) => ( ) => { const els = query(container, ...args) - const [{suggest = true} = {}] = args.slice(-1) - if (suggest && getConfig().throwSuggestions) { + const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) + if (suggest) { // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same const uniqueSuggestionMessages = [ From 7ec0b97abaa24f24ce22e7816a921a915cd97718 Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 11:14:58 -0700 Subject: [PATCH 29/30] matches.d.ts --- types/matches.d.ts | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/types/matches.d.ts b/types/matches.d.ts index 0b8dad4d..0af83547 100644 --- a/types/matches.d.ts +++ b/types/matches.d.ts @@ -1,29 +1,33 @@ -export type MatcherFunction = (content: string, element: HTMLElement) => boolean; -export type Matcher = string | RegExp | MatcherFunction; +export type MatcherFunction = (content: string, element: HTMLElement) => boolean +export type Matcher = string | RegExp | MatcherFunction -export type NormalizerFn = (text: string) => string; +export type NormalizerFn = (text: string) => string export interface MatcherOptions { - exact?: boolean; - /** Use normalizer with getDefaultNormalizer instead */ - trim?: boolean; - /** Use normalizer with getDefaultNormalizer instead */ - collapseWhitespace?: boolean; - normalizer?: NormalizerFn; + exact?: boolean + /** Use normalizer with getDefaultNormalizer instead */ + trim?: boolean + /** Use normalizer with getDefaultNormalizer instead */ + collapseWhitespace?: boolean + normalizer?: NormalizerFn + /** set query suppress suggestions */ + suggest?: boolean } export type Match = ( - textToMatch: string, - node: HTMLElement | null, - matcher: Matcher, - options?: MatcherOptions, -) => boolean; + textToMatch: string, + node: HTMLElement | null, + matcher: Matcher, + options?: MatcherOptions, +) => boolean export interface DefaultNormalizerOptions { - trim?: boolean; - collapseWhitespace?: boolean; + trim?: boolean + collapseWhitespace?: boolean } -export function getDefaultNormalizer(options?: DefaultNormalizerOptions): NormalizerFn; +export function getDefaultNormalizer( + options?: DefaultNormalizerOptions, +): NormalizerFn // N.B. Don't expose fuzzyMatches + matches here: they're not public API From 9cec77331476214d9d190770a905b40b0c3344ca Mon Sep 17 00:00:00 2001 From: Ben Monro <ben.monro@gmail.com> Date: Fri, 29 May 2020 11:43:07 -0700 Subject: [PATCH 30/30] Update types/matches.d.ts --- types/matches.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/matches.d.ts b/types/matches.d.ts index 0af83547..6454c86a 100644 --- a/types/matches.d.ts +++ b/types/matches.d.ts @@ -10,7 +10,7 @@ export interface MatcherOptions { /** Use normalizer with getDefaultNormalizer instead */ collapseWhitespace?: boolean normalizer?: NormalizerFn - /** set query suppress suggestions */ + /** suppress suggestions for a specific query */ suggest?: boolean }