Skip to content

Commit 25c531d

Browse files
committed
Add getDefaultNormalizer() and move existing options
This commit moves the implementation of `trim` + `collapseWhitespace`, making them just another normalizer. It also exposes a `getDefaultNormalizer()` function which provides the default normalization and allows for the configuration of it. Removed `matches` and `fuzzyMatches` from being publicly exposed. Updated tests, added new documentation for normalizer and getDefaultNormalizer, and removed documentation for the previous top-level `trim` and `collapseWhitespace` options.
1 parent ab3a203 commit 25c531d

File tree

8 files changed

+186
-131
lines changed

8 files changed

+186
-131
lines changed

README.md

+40-26
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ when a real user uses it.
9898
- [Using other assertion libraries](#using-other-assertion-libraries)
9999
- [`TextMatch`](#textmatch)
100100
- [Precision](#precision)
101+
- [Normalization](#normalization)
101102
- [TextMatch Examples](#textmatch-examples)
102103
- [`query` APIs](#query-apis)
103104
- [`queryAll` and `getAll` APIs](#queryall-and-getall-apis)
@@ -207,8 +208,6 @@ getByLabelText(
207208
options?: {
208209
selector?: string = '*',
209210
exact?: boolean = true,
210-
collapseWhitespace?: boolean = true,
211-
trim?: boolean = true,
212211
normalizer?: NormalizerFn,
213212
}): HTMLElement
214213
```
@@ -260,8 +259,6 @@ getByPlaceholderText(
260259
text: TextMatch,
261260
options?: {
262261
exact?: boolean = true,
263-
collapseWhitespace?: boolean = false,
264-
trim?: boolean = true,
265262
normalizer?: NormalizerFn,
266263
}): HTMLElement
267264
```
@@ -285,8 +282,6 @@ getBySelectText(
285282
text: TextMatch,
286283
options?: {
287284
exact?: boolean = true,
288-
collapseWhitespace?: boolean = true,
289-
trim?: boolean = true,
290285
normalizer?: NormalizerFn,
291286
}): HTMLElement
292287
```
@@ -316,8 +311,6 @@ getByText(
316311
options?: {
317312
selector?: string = '*',
318313
exact?: boolean = true,
319-
collapseWhitespace?: boolean = true,
320-
trim?: boolean = true,
321314
ignore?: string|boolean = 'script, style',
322315
normalizer?: NormalizerFn,
323316
}): HTMLElement
@@ -349,8 +342,6 @@ getByAltText(
349342
text: TextMatch,
350343
options?: {
351344
exact?: boolean = true,
352-
collapseWhitespace?: boolean = false,
353-
trim?: boolean = true,
354345
normalizer?: NormalizerFn,
355346
}): HTMLElement
356347
```
@@ -375,8 +366,6 @@ getByTitle(
375366
title: TextMatch,
376367
options?: {
377368
exact?: boolean = true,
378-
collapseWhitespace?: boolean = false,
379-
trim?: boolean = true,
380369
normalizer?: NormalizerFn,
381370
}): HTMLElement
382371
```
@@ -403,8 +392,6 @@ getByValue(
403392
value: TextMatch,
404393
options?: {
405394
exact?: boolean = true,
406-
collapseWhitespace?: boolean = false,
407-
trim?: boolean = true,
408395
normalizer?: NormalizerFn,
409396
}): HTMLElement
410397
```
@@ -424,8 +411,6 @@ getByRole(
424411
text: TextMatch,
425412
options?: {
426413
exact?: boolean = true,
427-
collapseWhitespace?: boolean = false,
428-
trim?: boolean = true,
429414
normalizer?: NormalizerFn,
430415
}): HTMLElement
431416
```
@@ -446,8 +431,6 @@ getByTestId(
446431
text: TextMatch,
447432
options?: {
448433
exact?: boolean = true,
449-
collapseWhitespace?: boolean = false,
450-
trim?: boolean = true,
451434
normalizer?: NormalizerFn,
452435
}): HTMLElement
453436
```
@@ -811,15 +794,46 @@ affect the precision of string matching:
811794
- `exact` has no effect on `regex` or `function` arguments.
812795
- In most cases using a regex instead of a string gives you more control over
813796
fuzzy matching and should be preferred over `{ exact: false }`.
814-
- `trim`: Defaults to `true`. Trims leading and trailing whitespace.
797+
- `normalizer`: An optional function which overrides normalization behavior.
798+
See [`Normalization`](#normalization).
799+
800+
### Normalization
801+
802+
Before running any matching logic against text in the DOM, `dom-testing-library`
803+
automatically normalizes that text. By default, normalization consists of
804+
trimming whitespace from the start and end of text, and collapsing multiple
805+
adjacent whitespace characters into a single space.
806+
807+
If you want to prevent that normalization, or provide alternative
808+
normalization (e.g. to remove Unicode control characters), you can provide a
809+
`normalizer` function in the options object. This function will be given
810+
a string and is expected to return a normalized version of that string.
811+
812+
Note: Specifying a value for `normalizer` _replaces_ the built-in normalization, but
813+
you can call `getDefaultNormalizer` to obtain a built-in normalizer, either
814+
to adjust that normalization or to call it from your own normalizer.
815+
816+
`getDefaultNormalizer` takes an options object which allows the selection of behaviour:
817+
818+
- `trim`: Defaults to `true`. Trims leading and trailing whitespace
815819
- `collapseWhitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space.
816-
- `normalizer`: Defaults to `undefined`. Specifies a custom function which will be called to normalize the text (after applying any `trim` or
817-
`collapseWhitespace` behaviour). An example use of this might be to remove Unicode control characters before applying matching behavior, e.g.
818-
```javascript
819-
{
820-
normalizer: str => str.replace(/[\u200E-\u200F]*/g, '')
821-
}
822-
```
820+
821+
#### Normalization Examples
822+
823+
To perform a match against text without trimming:
824+
825+
```javascript
826+
getByText(node, 'text', {normalizer: getDefaultNormalizer({trim: false})})
827+
```
828+
829+
To override normalization to remove some Unicode characters whilst keeping some (but not all) of the built-in normalization behavior:
830+
831+
```javascript
832+
getByText(node, 'text', {
833+
normalizer: str =>
834+
getDefaultNormalizer({trim: false})(str).replace(/[\u200E-\u200F]*/g, ''),
835+
})
836+
```
823837

824838
### TextMatch Examples
825839

src/__tests__/matches.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
import {fuzzyMatches, matches} from '../'
1+
import {fuzzyMatches, matches} from '../matches'
22

33
// unit tests for text match utils
44

55
const node = null
6+
const normalizer = str => str
67

78
test('matchers accept strings', () => {
8-
expect(matches('ABC', node, 'ABC')).toBe(true)
9-
expect(fuzzyMatches('ABC', node, 'ABC')).toBe(true)
9+
expect(matches('ABC', node, 'ABC', normalizer)).toBe(true)
10+
expect(fuzzyMatches('ABC', node, 'ABC', normalizer)).toBe(true)
1011
})
1112

1213
test('matchers accept regex', () => {
13-
expect(matches('ABC', node, /ABC/)).toBe(true)
14-
expect(fuzzyMatches('ABC', node, /ABC/)).toBe(true)
14+
expect(matches('ABC', node, /ABC/, normalizer)).toBe(true)
15+
expect(fuzzyMatches('ABC', node, /ABC/, normalizer)).toBe(true)
1516
})
1617

1718
test('matchers accept functions', () => {
18-
expect(matches('ABC', node, text => text === 'ABC')).toBe(true)
19-
expect(fuzzyMatches('ABC', node, text => text === 'ABC')).toBe(true)
19+
expect(matches('ABC', node, text => text === 'ABC', normalizer)).toBe(true)
20+
expect(fuzzyMatches('ABC', node, text => text === 'ABC', normalizer)).toBe(
21+
true,
22+
)
2023
})
2124

2225
test('matchers return false if text to match is not a string', () => {
23-
expect(matches(null, node, 'ABC')).toBe(false)
24-
expect(fuzzyMatches(null, node, 'ABC')).toBe(false)
26+
expect(matches(null, node, 'ABC', normalizer)).toBe(false)
27+
expect(fuzzyMatches(null, node, 'ABC', normalizer)).toBe(false)
2528
})

src/__tests__/text-matchers.js

+45-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'jest-dom/extend-expect'
22
import cases from 'jest-in-case'
3+
4+
import {getDefaultNormalizer} from '../'
35
import {render} from './helpers/test-utils'
46

57
cases(
@@ -68,7 +70,12 @@ cases(
6870
const queries = render(dom)
6971
expect(queries[queryFn](query)).toHaveLength(1)
7072
expect(
71-
queries[queryFn](query, {collapseWhitespace: false, trim: false}),
73+
queries[queryFn](query, {
74+
normalizer: getDefaultNormalizer({
75+
collapseWhitespace: false,
76+
trim: false,
77+
}),
78+
}),
7279
).toHaveLength(0)
7380
},
7481
{
@@ -271,39 +278,47 @@ test('normalizer works with both exact and non-exact matching', () => {
271278
expect(queryAllByText('MiXeD CaSe', {exact: true})).toHaveLength(0)
272279
})
273280

274-
test('normalizer runs after trim and collapseWhitespace options', () => {
275-
// Our test text has leading and trailing spaces, and multiple
276-
// spaces in the middle; we'll make use of that fact to test
277-
// the execution order of trim/collapseWhitespace and the custom
278-
// normalizer
281+
test('top-level trim and collapseWhitespace options are not supported if normalizer is specified', () => {
279282
const {queryAllByText} = render('<div> abc def </div>')
283+
const normalizer = str => str
280284

281-
// Double-check the normal trim + collapseWhitespace behavior
282-
expect(
283-
queryAllByText('abc def', {trim: true, collapseWhitespace: true}),
284-
).toHaveLength(1)
285+
expect(() => queryAllByText('abc', {trim: false, normalizer})).toThrow()
286+
expect(() => queryAllByText('abc', {trim: true, normalizer})).toThrow()
287+
expect(() =>
288+
queryAllByText('abc', {collapseWhitespace: false, normalizer}),
289+
).toThrow()
290+
expect(() =>
291+
queryAllByText('abc', {collapseWhitespace: true, normalizer}),
292+
).toThrow()
293+
})
294+
295+
test('getDefaultNormalizer returns a normalizer that supports trim and collapseWhitespace', () => {
296+
// Default is trim: true and collapseWhitespace: true
297+
expect(getDefaultNormalizer()(' abc def ')).toEqual('abc def')
285298

286-
// Test that again, but with a normalizer that replaces double
287-
// spaces with 'X' characters. If that runs before trim/collapseWhitespace,
288-
// it'll prevent successful matching
299+
// Turning off trimming should not turn off whitespace collapsing
300+
expect(getDefaultNormalizer({trim: false})(' abc def ')).toEqual(
301+
' abc def ',
302+
)
303+
304+
// Turning off whitespace collapsing should not turn off trimming
289305
expect(
290-
queryAllByText('abc def', {
291-
trim: true,
292-
collapseWhitespace: true,
293-
normalizer: str => str.replace(/ {2}/g, 'X'),
294-
}),
295-
).toHaveLength(1)
306+
getDefaultNormalizer({collapseWhitespace: false})(' abc def '),
307+
).toEqual('abc def')
296308

297-
// Test that, if we turn off trim + collapse, that the normalizer does
298-
// then get to see the double whitespace, and we should now be able
299-
// to match the Xs
309+
// Whilst it's rather pointless, we should be able to turn both off
300310
expect(
301-
queryAllByText('XabcXdefX', {
302-
trim: false,
303-
collapseWhitespace: false,
304-
// With the whitespace left in, this will add Xs which will
305-
// prevent matching
306-
normalizer: str => str.replace(/ {2}/g, 'X'),
307-
}),
308-
).toHaveLength(1)
311+
getDefaultNormalizer({trim: false, collapseWhitespace: false})(
312+
' abc def ',
313+
),
314+
).toEqual(' abc def ')
315+
})
316+
317+
test('we support an older API with trim and collapseWhitespace instead of a normalizer', () => {
318+
const {queryAllByText} = render('<div> x y </div>')
319+
expect(queryAllByText('x y')).toHaveLength(1)
320+
expect(queryAllByText('x y', {trim: false})).toHaveLength(0)
321+
expect(queryAllByText(' x y ', {trim: false})).toHaveLength(1)
322+
expect(queryAllByText('x y', {collapseWhitespace: false})).toHaveLength(0)
323+
expect(queryAllByText('x y', {collapseWhitespace: false})).toHaveLength(1)
309324
})

src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export * from './queries'
66
export * from './wait'
77
export * from './wait-for-element'
88
export * from './wait-for-dom-change'
9-
export * from './matches'
9+
export {getDefaultNormalizer} from './matches'
1010
export * from './get-node-text'
1111
export * from './events'
1212
export * from './get-queries-for-element'

src/matches.js

+47-31
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
function fuzzyMatches(
2-
textToMatch,
3-
node,
4-
matcher,
5-
{collapseWhitespace = true, trim = true, normalizer} = {},
6-
) {
1+
function fuzzyMatches(textToMatch, node, matcher, normalizer) {
72
if (typeof textToMatch !== 'string') {
83
return false
94
}
10-
const normalizedText = normalize(textToMatch, {
11-
trim,
12-
collapseWhitespace,
13-
normalizer,
14-
})
5+
6+
const normalizedText = normalizer(textToMatch)
157
if (typeof matcher === 'string') {
168
return normalizedText.toLowerCase().includes(matcher.toLowerCase())
179
} else if (typeof matcher === 'function') {
@@ -21,20 +13,12 @@ function fuzzyMatches(
2113
}
2214
}
2315

24-
function matches(
25-
textToMatch,
26-
node,
27-
matcher,
28-
{collapseWhitespace = true, trim = true, normalizer} = {},
29-
) {
16+
function matches(textToMatch, node, matcher, normalizer) {
3017
if (typeof textToMatch !== 'string') {
3118
return false
3219
}
33-
const normalizedText = normalize(textToMatch, {
34-
trim,
35-
collapseWhitespace,
36-
normalizer,
37-
})
20+
21+
const normalizedText = normalizer(textToMatch)
3822
if (typeof matcher === 'string') {
3923
return normalizedText === matcher
4024
} else if (typeof matcher === 'function') {
@@ -44,14 +28,46 @@ function matches(
4428
}
4529
}
4630

47-
function normalize(text, {trim, collapseWhitespace, normalizer}) {
48-
let normalizedText = text
49-
normalizedText = trim ? normalizedText.trim() : normalizedText
50-
normalizedText = collapseWhitespace
51-
? normalizedText.replace(/\s+/g, ' ')
52-
: normalizedText
53-
normalizedText = normalizer ? normalizer(normalizedText) : normalizedText
54-
return normalizedText
31+
function getDefaultNormalizer({trim = true, collapseWhitespace = true} = {}) {
32+
return text => {
33+
let normalizedText = text
34+
normalizedText = trim ? normalizedText.trim() : normalizedText
35+
normalizedText = collapseWhitespace
36+
? normalizedText.replace(/\s+/g, ' ')
37+
: normalizedText
38+
return normalizedText
39+
}
40+
}
41+
42+
/**
43+
* Constructs a normalizer to pass to functions in matches.js
44+
* @param {boolean|undefined} trim The user-specified value for `trim`, without
45+
* any defaulting having been applied
46+
* @param {boolean|undefined} collapseWhitespace The user-specified value for
47+
* `collapseWhitespace`, without any defaulting having been applied
48+
* @param {Function|undefined} normalizer The user-specified normalizer
49+
* @returns {Function} A normalizer
50+
*/
51+
function makeNormalizer({trim, collapseWhitespace, normalizer}) {
52+
if (normalizer) {
53+
// User has specified a custom normalizer
54+
if (
55+
typeof trim !== 'undefined' ||
56+
typeof collapseWhitespace !== 'undefined'
57+
) {
58+
// They've also specified a value for trim or collapseWhitespace
59+
throw new Error(
60+
'trim and collapseWhitespace are not supported with a normalizer. ' +
61+
'If you want to use the default trim and collapseWhitespace logic in your normalizer, ' +
62+
'use "getDefaultNormalizer({trim, collapseWhitespace})" and compose that into your normalizer',
63+
)
64+
}
65+
66+
return normalizer
67+
} else {
68+
// No custom normalizer specified. Just use default.
69+
return getDefaultNormalizer({trim, collapseWhitespace})
70+
}
5571
}
5672

57-
export {fuzzyMatches, matches}
73+
export {fuzzyMatches, matches, getDefaultNormalizer, makeNormalizer}

0 commit comments

Comments
 (0)