Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error messages #1185

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/__tests__/element-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,40 @@ test('get throws a useful error message', () => {
<div />
</div>
`)
expect(() =>
getByText(function LucyRicardo() {
return false
}),
).toThrowErrorMatchingInlineSnapshot(`
Unable to find an element that match the custom matcher: LucyRicardo. 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.

Ignored nodes: comments, script, style
<div>
<div />
</div>
`)
expect(() => getByText(() => false)).toThrowErrorMatchingInlineSnapshot(`
Unable to find an element that match the custom matcher: [anonymous function]. 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.

Ignored nodes: comments, script, style
<div>
<div />
</div>
`)

function something() {
return false
}
something.customMatcherText = 'Lucy and Ricardo'

expect(() => getByText(something)).toThrowErrorMatchingInlineSnapshot(`
Unable to find an element that match the custom matcher: Lucy and Ricardo. 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.

Ignored nodes: comments, script, style
<div>
<div />
</div>
`)
expect(() => getByText('LucyRicardo', {selector: 'span'}))
.toThrowErrorMatchingInlineSnapshot(`
Unable to find an element with the text: LucyRicardo, which matches selector 'span'. 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.
Expand Down Expand Up @@ -1117,7 +1151,7 @@ test('the default value for `ignore` is used in errors', () => {
const {getByText} = render('<div>Hello</div>')

expect(() => getByText(/hello/i)).toThrowErrorMatchingInlineSnapshot(`
Unable to find an element with the text: /hello/i. 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.
Unable to find an element that its text match the regex: /hello/i. 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.

Ignored nodes: comments, div
<div />
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/get-by-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,15 @@ describe('*ByDisplayValue queries throw an error when there are multiple element
`<input value="his" /><select><option value="history">history</option></select>`,
)
expect(() => getByDisplayValue(/his/)).toThrow(
/multiple elements with the display value:/i,
/multiple elements that its display value match the regex:/i,
)
})
test('queryByDisplayValue', () => {
const {queryByDisplayValue} = render(
`<input value="his" /><select><option value="history">history</option></select>`,
)
expect(() => queryByDisplayValue(/his/)).toThrow(
/multiple elements with the display value:/i,
/multiple elements that its display value match the regex:/i,
)
})
})
2 changes: 1 addition & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function getWindowFromNode(node: any) {
throw new Error(
`It looks like the window object is not available for the provided node.`,
)
} else if (node.then instanceof Function) {
} else if (typeof node.then === 'function') {
throw new Error(
`It looks like you passed a Promise object instead of a DOM node. Did you do something like \`fireEvent.click(screen.findBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`, or await the findBy query \`fireEvent.click(await screen.findBy...\`?`,
)
Expand Down
22 changes: 22 additions & 0 deletions src/hints-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Matcher} from '../types'

function getMatcherHint(matcher: Matcher, prefixForRegex: string) {
if (matcher instanceof RegExp) {
return `${prefixForRegex} match the regex: ${matcher}`
}

if (typeof matcher === 'function') {
let customMatcherText: string = matcher.name || '[anonymous function]'

if (
matcher.customMatcherText &&
typeof matcher.customMatcherText === 'string'
) {
customMatcherText = matcher.customMatcherText
}

return `that match the custom matcher: ${customMatcherText}`
}
}

export {getMatcherHint}
2 changes: 1 addition & 1 deletion src/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function matches(
assertNotNullOrUndefined(matcher)

const normalizedText = normalizer(textToMatch)
if (matcher instanceof Function) {
if (typeof matcher === 'function') {
return matcher(normalizedText, node)
} else if (matcher instanceof RegExp) {
return matchRegExp(matcher, normalizedText)
Expand Down
43 changes: 38 additions & 5 deletions src/queries/alt-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {checkContainerType} from '../helpers'
import {
AllByBoundAttribute,
GetErrorFunction,
Matcher,
MatcherOptions,
SelectorMatcherOptions,
} from '../../types'
import {buildQueries} from './all-utils'
import {getMatcherHint} from '../hints-helpers'
import {buildQueries, makeNormalizer} from './all-utils'

// Valid tags are img, input, area and custom elements
const VALID_TAG_REGEXP = /^(img|input|area|.+-.+)$/i
Expand All @@ -24,10 +27,40 @@ const queryAllByAltText: AllByBoundAttribute = (
)
}

const getMultipleError: GetErrorFunction<[unknown]> = (c, alt) =>
`Found multiple elements with the alt text: ${alt}`
const getMissingError: GetErrorFunction<[unknown]> = (c, alt) =>
`Unable to find an element with the alt text: ${alt}`
const getMultipleError: GetErrorFunction<[Matcher, MatcherOptions]> = (
c,
alt,
options,
) => {
return `Found multiple elements ${getMatcherHintOrDefault(alt, options)}`
}
const getMissingError: GetErrorFunction<[Matcher, MatcherOptions]> = (
c,
alt,
options,
) => `Unable to find an element ${getMatcherHintOrDefault(alt, options)}`

function getMatcherHintOrDefault(
matcher: Matcher,
options: MatcherOptions = {},
) {
const matcherHint = getMatcherHint(matcher, 'that its alt text')

if (matcherHint) {
return matcherHint
}

const {normalizer} = options
const matchNormalizer = makeNormalizer({normalizer})
const normalizedText = matchNormalizer(matcher.toString())
const isNormalizedDifferent = normalizedText !== matcher.toString()

return `with the alt text: ${
isNormalizedDifferent
? `${normalizedText} (normalized from '${matcher}')`
: matcher
}`
}

const queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
Expand Down
37 changes: 33 additions & 4 deletions src/queries/display-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Matcher,
MatcherOptions,
} from '../../types'
import {getMatcherHint} from '../hints-helpers'
import {
getNodeText,
matches,
Expand Down Expand Up @@ -43,10 +44,38 @@ const queryAllByDisplayValue: AllByBoundAttribute = (
})
}

const getMultipleError: GetErrorFunction<[unknown]> = (c, value) =>
`Found multiple elements with the display value: ${value}.`
const getMissingError: GetErrorFunction<[unknown]> = (c, value) =>
`Unable to find an element with the display value: ${value}.`
const getMultipleError: GetErrorFunction<[Matcher, MatcherOptions]> = (
c,
value,
options = {},
) => `Found multiple elements ${getMatcherHintOrDefault(value, options)}.`
const getMissingError: GetErrorFunction<[Matcher, MatcherOptions]> = (
c,
value,
options = {},
) => `Unable to find an element ${getMatcherHintOrDefault(value, options)}.`

function getMatcherHintOrDefault(
matcher: Matcher,
options: MatcherOptions = {},
) {
const matcherHint = getMatcherHint(matcher, 'that its display value')

if (matcherHint) {
return matcherHint
}

const {normalizer} = options
const matchNormalizer = makeNormalizer({normalizer})
const normalizedText = matchNormalizer(matcher.toString())
const isNormalizedDifferent = normalizedText !== matcher.toString()

return `with the display value: ${
isNormalizedDifferent
? `${normalizedText} (normalized from '${matcher}')`
: matcher
}`
}

const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
Expand Down
46 changes: 40 additions & 6 deletions src/queries/placeholder-text.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
import {AllByBoundAttribute, GetErrorFunction} from '../../types'
import {queryAllByAttribute, buildQueries} from './all-utils'
import {
AllByBoundAttribute,
GetErrorFunction,
Matcher,
MatcherOptions,
} from '../../types'
import {getMatcherHint} from '../hints-helpers'
import {queryAllByAttribute, buildQueries, makeNormalizer} from './all-utils'

const queryAllByPlaceholderText: AllByBoundAttribute = (...args) => {
checkContainerType(args[0])
return queryAllByAttribute('placeholder', ...args)
}
const getMultipleError: GetErrorFunction<[unknown]> = (c, text) =>
`Found multiple elements with the placeholder text of: ${text}`
const getMissingError: GetErrorFunction<[unknown]> = (c, text) =>
`Unable to find an element with the placeholder text of: ${text}`
const getMultipleError: GetErrorFunction<[Matcher, MatcherOptions]> = (
c,
text,
options = {},
) => `Found multiple elements ${getMatcherHintOrDefault(text, options)}`
const getMissingError: GetErrorFunction<[Matcher, MatcherOptions]> = (
c,
text,
options = {},
) => `Unable to find an element ${getMatcherHintOrDefault(text, options)}`

function getMatcherHintOrDefault(
matcher: Matcher,
options: MatcherOptions = {},
) {
const matcherHint = getMatcherHint(matcher, 'that its placeholder text')

if (matcherHint) {
return matcherHint
}

const {normalizer} = options
const matchNormalizer = makeNormalizer({normalizer})
const normalizedText = matchNormalizer(matcher.toString())
const isNormalizedDifferent = normalizedText !== matcher.toString()

return `with the placeholder text of: ${
isNormalizedDifferent
? `${normalizedText} (normalized from '${matcher}')`
: matcher
}`
}

const queryAllByPlaceholderTextWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
Expand Down
51 changes: 39 additions & 12 deletions src/queries/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SelectorMatcherOptions,
Matcher,
} from '../../types'
import {getMatcherHint} from '../hints-helpers'
import {
fuzzyMatches,
matches,
Expand Down Expand Up @@ -45,27 +46,53 @@ const queryAllByText: AllByText = (
)
}

const getMultipleError: GetErrorFunction<[unknown]> = (c, text) =>
`Found multiple elements with the text: ${text}`
const getMultipleError: GetErrorFunction<[Matcher, SelectorMatcherOptions]> = (
c,
text,
options = {},
) => {
const {selector} = options
const isCustomSelector = (options.selector ?? '*') !== '*'

return `Found multiple elements ${getMatcherHintOrDefault(text, options)}${
isCustomSelector ? `, which matches selector '${selector}'` : ''
}`
}
const getMissingError: GetErrorFunction<[Matcher, SelectorMatcherOptions]> = (
c,
text,
options = {},
) => {
const {collapseWhitespace, trim, normalizer, selector} = options
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
const normalizedText = matchNormalizer(text.toString())
const isNormalizedDifferent = normalizedText !== text.toString()
const isCustomSelector = (selector ?? '*') !== '*'
return `Unable to find an element with the text: ${
isNormalizedDifferent
? `${normalizedText} (normalized from '${text}')`
: text
}${
const {selector} = options
const isCustomSelector = (options.selector ?? '*') !== '*'

return `Unable to find an element ${getMatcherHintOrDefault(text, options)}${
isCustomSelector ? `, which matches selector '${selector}'` : ''
}. 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.`
}

function getMatcherHintOrDefault(
matcher: Matcher,
options: SelectorMatcherOptions = {},
) {
const matcherHint = getMatcherHint(matcher, 'that its text')

if (matcherHint) {
return matcherHint
}

const {collapseWhitespace, trim, normalizer} = options
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
const normalizedText = matchNormalizer(matcher.toString())
const isNormalizedDifferent = normalizedText !== matcher.toString()

return `with the text: ${
isNormalizedDifferent
? `${normalizedText} (normalized from '${matcher}')`
: matcher
}`
}

const queryAllByTextWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
[text: Matcher, options?: MatcherOptions]
Expand Down