Skip to content

Commit 31ef0b9

Browse files
AugustinLFRafikiTikithymikeemdjastrzebskiDamian Sznajder
authored
feat(breaking): add text match options a.k.a string precision API (#554)
* Add TextMatch options to getByAPI * Add TextMatch options to queryByAPI * Add TextMatch options to findByAPI * Add tests covering new TextMatch options * Add documentation and examples for TextMatch options * Implement text match normalization customization * Implement tests for normalization options * Add text normalization docs * Refactor typings * Add export for getDefaultNormalizer function * Remove undocumented config for normalizer function * Update website/docs/Queries.md Co-authored-by: Maciej Jastrzebski <[email protected]> * BREAKING: make findBy* queries arguments order compatible with RTL * Fix tests for findBy queries after changing their API * Fix matching nested Text components with queryOptions exact set to true * Remove recursion matching for *byText APIs * Reuse getAllByText in getByText * Add Textmatch options to *byTestId * fix: types for getDefaultNormalizer * refactor: remove makeNormalizer and use getDefaultNormalizer() instead * refactor: change params position and description for matches * refactor: move & remove duplicated tests after merge * Make waitForOptions param change non breaking change * chore: cleanup and type fixups * tests: update snapshots * chore: use default arg value for normalizer * Restore mock after use * fix: wrong snapshot update with outdated deps Co-authored-by: Rafał Zakrzewski <[email protected]> Co-authored-by: Michał Pierzchała <[email protected]> Co-authored-by: Maciej Jastrzebski <[email protected]> Co-authored-by: Damian Sznajder <[email protected]>
1 parent 0ede617 commit 31ef0b9

17 files changed

+693
-149
lines changed

Diff for: src/__tests__/byDisplayValue.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ test('findBy queries work asynchronously', async () => {
8181
);
8282

8383
await expect(
84-
findByDisplayValue('Display Value', options)
84+
findByDisplayValue('Display Value', {}, options)
8585
).rejects.toBeTruthy();
8686
await expect(
87-
findAllByDisplayValue('Display Value', options)
87+
findAllByDisplayValue('Display Value', {}, options)
8888
).rejects.toBeTruthy();
8989

9090
setTimeout(

Diff for: src/__tests__/byTestId.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ test('getAllByTestId, queryAllByTestId', () => {
115115
test('findByTestId and findAllByTestId work asynchronously', async () => {
116116
const options = { timeout: 10 }; // Short timeout so that this test runs quickly
117117
const { rerender, findByTestId, findAllByTestId } = render(<View />);
118-
await expect(findByTestId('aTestId', options)).rejects.toBeTruthy();
119-
await expect(findAllByTestId('aTestId', options)).rejects.toBeTruthy();
118+
await expect(findByTestId('aTestId', {}, options)).rejects.toBeTruthy();
119+
await expect(findAllByTestId('aTestId', {}, options)).rejects.toBeTruthy();
120120

121121
setTimeout(
122122
() =>

Diff for: src/__tests__/byText.test.js

+210-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
// @flow
22
import * as React from 'react';
3-
import { View, Text, TouchableOpacity, Image } from 'react-native';
4-
import { render } from '..';
3+
import {
4+
View,
5+
Text,
6+
TouchableOpacity,
7+
Image,
8+
Button,
9+
TextInput,
10+
} from 'react-native';
11+
import { render, getDefaultNormalizer } from '..';
512

613
const MyButton = ({ children, onPress }) => (
714
<TouchableOpacity onPress={onPress}>
@@ -88,8 +95,8 @@ test('getAllByText, queryAllByText', () => {
8895
test('findByText queries work asynchronously', async () => {
8996
const options = { timeout: 10 }; // Short timeout so that this test runs quickly
9097
const { rerender, findByText, findAllByText } = render(<View />);
91-
await expect(findByText('Some Text', options)).rejects.toBeTruthy();
92-
await expect(findAllByText('Some Text', options)).rejects.toBeTruthy();
98+
await expect(findByText('Some Text', {}, options)).rejects.toBeTruthy();
99+
await expect(findAllByText('Some Text', {}, options)).rejects.toBeTruthy();
93100

94101
setTimeout(
95102
() =>
@@ -105,6 +112,39 @@ test('findByText queries work asynchronously', async () => {
105112
await expect(findAllByText('Some Text')).resolves.toHaveLength(1);
106113
}, 20000);
107114

115+
describe('findBy options deprecations', () => {
116+
let warnSpy;
117+
beforeEach(() => {
118+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
119+
});
120+
afterEach(() => {
121+
warnSpy.mockRestore();
122+
});
123+
124+
test('findByText queries warn on deprecated use of WaitForOptions', async () => {
125+
const options = { timeout: 10 };
126+
// mock implementation to avoid warning in the test suite
127+
const { rerender, findByText } = render(<View />);
128+
await expect(findByText('Some Text', options)).rejects.toBeTruthy();
129+
130+
setTimeout(
131+
() =>
132+
rerender(
133+
<View>
134+
<Text>Some Text</Text>
135+
</View>
136+
),
137+
20
138+
);
139+
140+
await expect(findByText('Some Text')).resolves.toBeTruthy();
141+
142+
expect(warnSpy).toHaveBeenCalledWith(
143+
expect.stringContaining('Use of option "timeout"')
144+
);
145+
}, 20000);
146+
});
147+
108148
test.skip('getByText works properly with custom text component', () => {
109149
function BoldText({ children }) {
110150
return <Text>{children}</Text>;
@@ -181,7 +221,7 @@ test('queryByText not found', () => {
181221
).toBeFalsy();
182222
});
183223

184-
test('queryByText nested text across multiple <Text> in <Text>', () => {
224+
test('queryByText does not match nested text across multiple <Text> in <Text>', () => {
185225
const { queryByText } = render(
186226
<Text nativeID="1">
187227
Hello{' '}
@@ -192,7 +232,7 @@ test('queryByText nested text across multiple <Text> in <Text>', () => {
192232
</Text>
193233
);
194234

195-
expect(queryByText('Hello World!')?.props.nativeID).toBe('1');
235+
expect(queryByText('Hello World!')).toBe(null);
196236
});
197237

198238
test('queryByText with nested Text components return the closest Text', () => {
@@ -204,7 +244,7 @@ test('queryByText with nested Text components return the closest Text', () => {
204244

205245
const { queryByText } = render(<NestedTexts />);
206246

207-
expect(queryByText('My text')?.props.nativeID).toBe('2');
247+
expect(queryByText('My text', { exact: false })?.props.nativeID).toBe('2');
208248
});
209249

210250
test('queryByText with nested Text components each with text return the lowest one', () => {
@@ -217,33 +257,182 @@ test('queryByText with nested Text components each with text return the lowest o
217257

218258
const { queryByText } = render(<NestedTexts />);
219259

220-
expect(queryByText('My text')?.props.nativeID).toBe('2');
260+
expect(queryByText('My text', { exact: false })?.props.nativeID).toBe('2');
221261
});
222262

223-
test('queryByText nested <CustomText> in <Text>', () => {
263+
test('queryByText nested deep <CustomText> in <Text>', () => {
224264
const CustomText = ({ children }) => {
225265
return <Text>{children}</Text>;
226266
};
227267

228268
expect(
229269
render(
230270
<Text>
231-
Hello <CustomText>World!</CustomText>
271+
<CustomText>Hello</CustomText> <CustomText>World!</CustomText>
232272
</Text>
233273
).queryByText('Hello World!')
234-
).toBeTruthy();
274+
).toBe(null);
235275
});
236276

237-
test('queryByText nested deep <CustomText> in <Text>', () => {
238-
const CustomText = ({ children }) => {
239-
return <Text>{children}</Text>;
240-
};
277+
test('queryByText with nested Text components: not-exact text match returns the most deeply nested common component', () => {
278+
const { queryByText: queryByTextFirstCase } = render(
279+
<Text nativeID="1">
280+
bob
281+
<Text nativeID="2">My </Text>
282+
<Text nativeID="3">text</Text>
283+
</Text>
284+
);
285+
286+
const { queryByText: queryByTextSecondCase } = render(
287+
<Text nativeID="1">
288+
bob
289+
<Text nativeID="2">My text for test</Text>
290+
</Text>
291+
);
241292

293+
expect(queryByTextFirstCase('My text')).toBe(null);
242294
expect(
243-
render(
244-
<Text>
245-
<CustomText>Hello</CustomText> <CustomText>World!</CustomText>
246-
</Text>
247-
).queryByText('Hello World!')
248-
).toBeTruthy();
295+
queryByTextSecondCase('My text', { exact: false })?.props.nativeID
296+
).toBe('2');
297+
});
298+
299+
test('queryAllByText does not match several times the same text', () => {
300+
const allMatched = render(
301+
<Text nativeID="1">
302+
Start
303+
<Text nativeID="2">This is a long text</Text>
304+
</Text>
305+
).queryAllByText('long text', { exact: false });
306+
expect(allMatched.length).toBe(1);
307+
expect(allMatched[0].props.nativeID).toBe('2');
308+
});
309+
310+
test('queryAllByText matches all the matching nodes', () => {
311+
const allMatched = render(
312+
<Text nativeID="1">
313+
Start
314+
<Text nativeID="2">This is a long text</Text>
315+
<Text nativeID="3">This is another long text</Text>
316+
</Text>
317+
).queryAllByText('long text', { exact: false });
318+
expect(allMatched.length).toBe(2);
319+
expect(allMatched.map((node) => node.props.nativeID)).toEqual(['2', '3']);
320+
});
321+
322+
describe('supports TextMatch options', () => {
323+
test('getByText, getAllByText', () => {
324+
const { getByText, getAllByText } = render(
325+
<View>
326+
<Text testID="text">Text and details</Text>
327+
<Button
328+
testID="button"
329+
title="Button and a detail"
330+
onPress={jest.fn()}
331+
/>
332+
</View>
333+
);
334+
335+
expect(getByText('details', { exact: false })).toBeTruthy();
336+
expect(getAllByText('detail', { exact: false })).toHaveLength(2);
337+
});
338+
339+
test('getByPlaceholderText, getAllByPlaceholderText', () => {
340+
const { getByPlaceholderText, getAllByPlaceholderText } = render(
341+
<View>
342+
<TextInput placeholder={'Placeholder with details'} />
343+
<TextInput placeholder={'Placeholder with a DETAIL'} />
344+
</View>
345+
);
346+
347+
expect(getByPlaceholderText('details', { exact: false })).toBeTruthy();
348+
expect(getAllByPlaceholderText('detail', { exact: false })).toHaveLength(2);
349+
});
350+
351+
test('getByDisplayValue, getAllByDisplayValue', () => {
352+
const { getByDisplayValue, getAllByDisplayValue } = render(
353+
<View>
354+
<TextInput value={'Value with details'} />
355+
<TextInput value={'Value with a detail'} />
356+
</View>
357+
);
358+
359+
expect(getByDisplayValue('details', { exact: false })).toBeTruthy();
360+
expect(getAllByDisplayValue('detail', { exact: false })).toHaveLength(2);
361+
});
362+
363+
test('getByTestId, getAllByTestId', () => {
364+
const { getByTestId, getAllByTestId } = render(
365+
<View>
366+
<View testID="test" />
367+
<View testID="tests id" />
368+
</View>
369+
);
370+
expect(getByTestId('id', { exact: false })).toBeTruthy();
371+
expect(getAllByTestId('test', { exact: false })).toHaveLength(2);
372+
});
373+
374+
test('with TextMatch option exact === false text search is NOT case sensitive', () => {
375+
const { getByText, getAllByText } = render(
376+
<View>
377+
<Text testID="text">Text and details</Text>
378+
<Button
379+
testID="button"
380+
title="Button and a DeTAil"
381+
onPress={jest.fn()}
382+
/>
383+
</View>
384+
);
385+
386+
expect(getByText('DeTaIlS', { exact: false })).toBeTruthy();
387+
expect(getAllByText('detail', { exact: false })).toHaveLength(2);
388+
});
389+
});
390+
391+
describe('Supports normalization', () => {
392+
test('trims and collapses whitespace by default', () => {
393+
const { getByText } = render(
394+
<View>
395+
<Text>{` Text and
396+
397+
398+
whitespace`}</Text>
399+
</View>
400+
);
401+
402+
expect(getByText('Text and whitespace')).toBeTruthy();
403+
});
404+
405+
test('trim and collapseWhitespace is customizable by getDefaultNormalizer param', () => {
406+
const testTextWithWhitespace = ` Text and
407+
408+
409+
whitespace`;
410+
const { getByText } = render(
411+
<View>
412+
<Text>{testTextWithWhitespace}</Text>
413+
</View>
414+
);
415+
416+
expect(
417+
getByText(testTextWithWhitespace, {
418+
normalizer: getDefaultNormalizer({
419+
trim: false,
420+
collapseWhitespace: false,
421+
}),
422+
})
423+
).toBeTruthy();
424+
});
425+
426+
test('normalizer function is customisable', () => {
427+
const testText = 'A TO REMOVE text';
428+
const normalizerFn = (textToNormalize) =>
429+
textToNormalize.replace('TO REMOVE ', '');
430+
const { getByText } = render(
431+
<View>
432+
<Text>{testText}</Text>
433+
</View>
434+
);
435+
436+
expect(getByText('A text', { normalizer: normalizerFn })).toBeTruthy();
437+
});
249438
});

Diff for: src/helpers/byDisplayValue.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
// @flow
2+
import { matches } from '../matches';
23
import { makeQueries } from './makeQueries';
34
import type { Queries } from './makeQueries';
45
import { filterNodeByType } from './filterNodeByType';
56
import { createLibraryNotSupportedError } from './errors';
7+
import type { TextMatchOptions } from './byText';
68

7-
const getTextInputNodeByDisplayValue = (node, value) => {
9+
const getTextInputNodeByDisplayValue = (
10+
node,
11+
value,
12+
options?: TextMatchOptions = {}
13+
) => {
814
try {
915
const { TextInput } = require('react-native');
16+
const { exact, normalizer } = options;
1017
const nodeValue =
1118
node.props.value !== undefined
1219
? node.props.value
1320
: node.props.defaultValue;
1421
return (
1522
filterNodeByType(node, TextInput) &&
16-
(typeof value === 'string' ? value === nodeValue : value.test(nodeValue))
23+
matches(value, nodeValue, normalizer, exact)
1724
);
1825
} catch (error) {
1926
throw createLibraryNotSupportedError(error);
@@ -22,10 +29,13 @@ const getTextInputNodeByDisplayValue = (node, value) => {
2229

2330
const queryAllByDisplayValue = (
2431
instance: ReactTestInstance
25-
): ((displayValue: string | RegExp) => Array<ReactTestInstance>) =>
26-
function queryAllByDisplayValueFn(displayValue) {
32+
): ((
33+
displayValue: string | RegExp,
34+
queryOptions?: TextMatchOptions
35+
) => Array<ReactTestInstance>) =>
36+
function queryAllByDisplayValueFn(displayValue, queryOptions) {
2737
return instance.findAll((node) =>
28-
getTextInputNodeByDisplayValue(node, displayValue)
38+
getTextInputNodeByDisplayValue(node, displayValue, queryOptions)
2939
);
3040
};
3141

0 commit comments

Comments
 (0)