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

feat(custom normalizer): allow custom control of normalization #172

Merged
merged 6 commits into from
Dec 12, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ getByLabelText(
exact?: boolean = true,
collapseWhitespace?: boolean = true,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand Down Expand Up @@ -261,6 +262,7 @@ getByPlaceholderText(
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand All @@ -285,6 +287,7 @@ getBySelectText(
exact?: boolean = true,
collapseWhitespace?: boolean = true,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand Down Expand Up @@ -315,7 +318,8 @@ getByText(
exact?: boolean = true,
collapseWhitespace?: boolean = true,
trim?: boolean = true,
ignore?: string|boolean = 'script, style'
ignore?: string|boolean = 'script, style',
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand Down Expand Up @@ -347,6 +351,7 @@ getByAltText(
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand All @@ -372,6 +377,7 @@ getByTitle(
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand Down Expand Up @@ -399,6 +405,7 @@ getByValue(
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand All @@ -419,6 +426,7 @@ getByRole(
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
```

Expand All @@ -440,7 +448,8 @@ getByTestId(
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement`
normalizer?: NormalizerFn,
}): HTMLElement
```

A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` (and it
Expand Down Expand Up @@ -802,8 +811,15 @@ affect the precision of string matching:
- `exact` has no effect on `regex` or `function` arguments.
- In most cases using a regex instead of a string gives you more control over
fuzzy matching and should be preferred over `{ exact: false }`.
- `trim`: Defaults to `true`; trim leading and trailing whitespace.
- `trim`: Defaults to `true`. Trims leading and trailing whitespace.
- `collapseWhitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space.
- `normalizer`: Defaults to `undefined`. Specifies a custom function which will be called to normalize the text (after applying any `trim` or
`collapseWhitespace` behaviour). An example use of this might be to remove Unicode control characters before applying matching behavior, e.g.
```javascript
{
normalizer: str => str.replace(/[\u200E-\u200F]*/g, '')
}
```

### TextMatch Examples

Expand Down
113 changes: 113 additions & 0 deletions src/__tests__/text-matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,116 @@ cases(
},
},
)

// A good use case for a custom normalizer is stripping
// out Unicode control characters such as LRM (left-right-mark)
// before matching
const LRM = '\u200e'
function removeUCC(str) {
return str.replace(/[\u200e]/g, '')
}

cases(
'{ normalizer } option allows custom pre-match normalization',
({dom, queryFn}) => {
const queries = render(dom)

const query = queries[queryFn]

// With the correct normalizer, we should match
expect(query(/user n.me/i, {normalizer: removeUCC})).toHaveLength(1)
expect(query('User name', {normalizer: removeUCC})).toHaveLength(1)

// Without the normalizer, we shouldn't
expect(query(/user n.me/i)).toHaveLength(0)
expect(query('User name')).toHaveLength(0)
},
{
queryAllByLabelText: {
dom: `
<label for="username">User ${LRM}name</label>
<input id="username" />`,
queryFn: 'queryAllByLabelText',
},
queryAllByPlaceholderText: {
dom: `<input placeholder="User ${LRM}name" />`,
queryFn: 'queryAllByPlaceholderText',
},
queryAllBySelectText: {
dom: `<select><option>User ${LRM}name</option></select>`,
queryFn: 'queryAllBySelectText',
},
queryAllByText: {
dom: `<div>User ${LRM}name</div>`,
queryFn: 'queryAllByText',
},
queryAllByAltText: {
dom: `<img alt="User ${LRM}name" src="username.jpg" />`,
queryFn: 'queryAllByAltText',
},
queryAllByTitle: {
dom: `<div title="User ${LRM}name" />`,
queryFn: 'queryAllByTitle',
},
queryAllByValue: {
dom: `<input value="User ${LRM}name" />`,
queryFn: 'queryAllByValue',
},
queryAllByRole: {
dom: `<input role="User ${LRM}name" />`,
queryFn: 'queryAllByRole',
},
},
)

test('normalizer works with both exact and non-exact matching', () => {
const {queryAllByText} = render(`<div>MiXeD ${LRM}CaSe</div>`)

expect(
queryAllByText('mixed case', {exact: false, normalizer: removeUCC}),
).toHaveLength(1)
expect(
queryAllByText('mixed case', {exact: true, normalizer: removeUCC}),
).toHaveLength(0)
expect(
queryAllByText('MiXeD CaSe', {exact: true, normalizer: removeUCC}),
).toHaveLength(1)
expect(queryAllByText('MiXeD CaSe', {exact: true})).toHaveLength(0)
})

test('normalizer runs after trim and collapseWhitespace options', () => {
// Our test text has leading and trailing spaces, and multiple
// spaces in the middle; we'll make use of that fact to test
// the execution order of trim/collapseWhitespace and the custom
// normalizer
const {queryAllByText} = render('<div> abc def </div>')

// Double-check the normal trim + collapseWhitespace behavior
expect(
queryAllByText('abc def', {trim: true, collapseWhitespace: true}),
).toHaveLength(1)

// Test that again, but with a normalizer that replaces double
// spaces with 'X' characters. If that runs before trim/collapseWhitespace,
// it'll prevent successful matching
expect(
queryAllByText('abc def', {
trim: true,
collapseWhitespace: true,
normalizer: str => str.replace(/ {2}/g, 'X'),
}),
).toHaveLength(1)

// Test that, if we turn off trim + collapse, that the normalizer does
// then get to see the double whitespace, and we should now be able
// to match the Xs
expect(
queryAllByText('XabcXdefX', {
trim: false,
collapseWhitespace: false,
// With the whitespace left in, this will add Xs which will
// prevent matching
normalizer: str => str.replace(/ {2}/g, 'X'),
}),
).toHaveLength(1)
})
19 changes: 14 additions & 5 deletions src/matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ function fuzzyMatches(
textToMatch,
node,
matcher,
{collapseWhitespace = true, trim = true} = {},
{collapseWhitespace = true, trim = true, normalizer} = {},
alexkrolick marked this conversation as resolved.
Show resolved Hide resolved
) {
if (typeof textToMatch !== 'string') {
return false
}
const normalizedText = normalize(textToMatch, {trim, collapseWhitespace})
const normalizedText = normalize(textToMatch, {
trim,
collapseWhitespace,
normalizer,
})
if (typeof matcher === 'string') {
return normalizedText.toLowerCase().includes(matcher.toLowerCase())
} else if (typeof matcher === 'function') {
Expand All @@ -21,12 +25,16 @@ function matches(
textToMatch,
node,
matcher,
{collapseWhitespace = true, trim = true} = {},
{collapseWhitespace = true, trim = true, normalizer} = {},
) {
if (typeof textToMatch !== 'string') {
return false
}
const normalizedText = normalize(textToMatch, {trim, collapseWhitespace})
const normalizedText = normalize(textToMatch, {
trim,
collapseWhitespace,
normalizer,
})
if (typeof matcher === 'string') {
return normalizedText === matcher
} else if (typeof matcher === 'function') {
Expand All @@ -36,12 +44,13 @@ function matches(
}
}

function normalize(text, {trim, collapseWhitespace}) {
function normalize(text, {trim, collapseWhitespace, normalizer}) {
let normalizedText = text
normalizedText = trim ? normalizedText.trim() : normalizedText
normalizedText = collapseWhitespace
? normalizedText.replace(/\s+/g, ' ')
: normalizedText
normalizedText = normalizer ? normalizer(normalizedText) : normalizedText
return normalizedText
}

Expand Down
29 changes: 18 additions & 11 deletions src/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import {getConfig} from './config'
function queryAllLabelsByText(
container,
text,
{exact = true, trim = true, collapseWhitespace = true} = {},
{exact = true, trim = true, collapseWhitespace = true, normalizer} = {},
) {
const matcher = exact ? matches : fuzzyMatches
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
return Array.from(container.querySelectorAll('label')).filter(label =>
matcher(label.textContent, label, text, matchOpts),
)
Expand All @@ -27,9 +27,15 @@ function queryAllLabelsByText(
function queryAllByLabelText(
container,
text,
{selector = '*', exact = true, collapseWhitespace = true, trim = true} = {},
{
selector = '*',
exact = true,
collapseWhitespace = true,
trim = true,
normalizer,
} = {},
) {
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
const labels = queryAllLabelsByText(container, text, {exact, ...matchOpts})
const labelledElements = labels
.map(label => {
Expand Down Expand Up @@ -97,10 +103,11 @@ function queryAllByText(
collapseWhitespace = true,
trim = true,
ignore = 'script, style',
normalizer,
} = {},
) {
const matcher = exact ? matches : fuzzyMatches
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
return Array.from(container.querySelectorAll(selector))
.filter(node => !ignore || !node.matches(ignore))
.filter(node => matcher(getNodeText(node), node, text, matchOpts))
Expand All @@ -113,10 +120,10 @@ function queryByText(...args) {
function queryAllByTitle(
container,
text,
{exact = true, collapseWhitespace = true, trim = true} = {},
{exact = true, collapseWhitespace = true, trim = true, normalizer} = {},
) {
const matcher = exact ? matches : fuzzyMatches
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
return Array.from(container.querySelectorAll('[title], svg > title')).filter(
node =>
matcher(node.getAttribute('title'), node, text, matchOpts) ||
Expand All @@ -131,10 +138,10 @@ function queryByTitle(...args) {
function queryAllBySelectText(
container,
text,
{exact = true, collapseWhitespace = true, trim = true} = {},
{exact = true, collapseWhitespace = true, trim = true, normalizer} = {},
) {
const matcher = exact ? matches : fuzzyMatches
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
return Array.from(container.querySelectorAll('select')).filter(selectNode => {
const selectedOptions = Array.from(selectNode.options).filter(
option => option.selected,
Expand Down Expand Up @@ -167,10 +174,10 @@ const queryAllByRole = queryAllByAttribute.bind(null, 'role')
function queryAllByAltText(
container,
alt,
{exact = true, collapseWhitespace = true, trim = true} = {},
{exact = true, collapseWhitespace = true, trim = true, normalizer} = {},
) {
const matcher = exact ? matches : fuzzyMatches
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
return Array.from(container.querySelectorAll('img,input,area')).filter(node =>
matcher(node.getAttribute('alt'), node, alt, matchOpts),
)
Expand Down
4 changes: 2 additions & 2 deletions src/query-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ function queryAllByAttribute(
attribute,
container,
text,
{exact = true, collapseWhitespace = true, trim = true} = {},
{exact = true, collapseWhitespace = true, trim = true, normalizer} = {},
) {
const matcher = exact ? matches : fuzzyMatches
const matchOpts = {collapseWhitespace, trim}
const matchOpts = {collapseWhitespace, trim, normalizer}
return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node =>
matcher(node.getAttribute(attribute), node, text, matchOpts),
)
Expand Down
4 changes: 4 additions & 0 deletions typings/matches.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
export type MatcherFunction = (content: string, element: HTMLElement) => boolean
export type Matcher = string | RegExp | MatcherFunction

export type NormalizerFn = (text: string) => string

export interface MatcherOptions {
exact?: boolean
trim?: boolean
collapseWhitespace?: boolean
normalizer?: NormalizerFn
}

export type Match = (
Expand Down
4 changes: 1 addition & 3 deletions typings/queries.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {Matcher, MatcherOptions} from './matches'
import {
SelectorMatcherOptions,
} from './query-helpers'
import {SelectorMatcherOptions} from './query-helpers'

export type QueryByBoundAttribute = (
container: HTMLElement,
Expand Down