Skip to content

Commit 52222b4

Browse files
Add queryOptions to labelText and hintText (#1193)
Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
1 parent 380ff32 commit 52222b4

File tree

8 files changed

+258
-69
lines changed

8 files changed

+258
-69
lines changed

src/matches.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export function matches(
1818
? normalizedText === normalizedMatcher
1919
: normalizedText.toLowerCase().includes(normalizedMatcher.toLowerCase());
2020
} else {
21+
// Reset state for global regexes: https://stackoverflow.com/a/1520839/484499
22+
matcher.lastIndex = 0;
2123
return matcher.test(normalizedText);
2224
}
2325
}

src/queries/__tests__/hintText.test.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { TouchableOpacity, Text } from 'react-native';
2+
import { TouchableOpacity, Text, View } from 'react-native';
33
import { render } from '../..';
44

55
const BUTTON_HINT = 'click this button';
@@ -84,3 +84,25 @@ test('getAllByA11yHint, queryAllByA11yHint, findAllByA11yHint', async () => {
8484
getNoInstancesFoundMessage(NO_MATCHES_TEXT)
8585
);
8686
});
87+
88+
test('getByHintText, getByHintText', () => {
89+
const { getByHintText, getAllByHintText } = render(
90+
<View>
91+
<View accessibilityHint="test" />
92+
<View accessibilityHint="tests id" />
93+
</View>
94+
);
95+
expect(getByHintText('id', { exact: false })).toBeTruthy();
96+
expect(getAllByHintText('test', { exact: false })).toHaveLength(2);
97+
});
98+
99+
test('getByHintText, getByHintText and exact = true', () => {
100+
const { queryByHintText, getAllByHintText } = render(
101+
<View>
102+
<View accessibilityHint="test" />
103+
<View accessibilityHint="tests id" />
104+
</View>
105+
);
106+
expect(queryByHintText('id', { exact: true })).toBeNull();
107+
expect(getAllByHintText('test', { exact: true })).toHaveLength(1);
108+
});

src/queries/__tests__/labelText.test.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { TouchableOpacity, Text } from 'react-native';
2+
import { View, Text, TouchableOpacity } from 'react-native';
33
import { render } from '../..';
44

55
const BUTTON_LABEL = 'cool button';
@@ -93,3 +93,53 @@ test('getAllByLabelText, queryAllByLabelText, findAllByLabelText', async () => {
9393
getNoInstancesFoundMessage(NO_MATCHES_TEXT)
9494
);
9595
});
96+
97+
test('getAllByLabelText, queryAllByLabelText, findAllByLabelText with exact as false', async () => {
98+
const { getAllByLabelText, queryAllByLabelText, findAllByLabelText } = render(
99+
<Section />
100+
);
101+
102+
expect(getAllByLabelText(TEXT_LABEL, { exact: false })).toHaveLength(2);
103+
expect(queryAllByLabelText(/cool/g, { exact: false })).toHaveLength(3);
104+
105+
expect(() => getAllByLabelText(NO_MATCHES_TEXT, { exact: false })).toThrow(
106+
getNoInstancesFoundMessage(NO_MATCHES_TEXT)
107+
);
108+
expect(queryAllByLabelText(NO_MATCHES_TEXT, { exact: false })).toEqual([]);
109+
110+
await expect(
111+
findAllByLabelText(TEXT_LABEL, { exact: false })
112+
).resolves.toHaveLength(2);
113+
await expect(
114+
findAllByLabelText(NO_MATCHES_TEXT, { exact: false })
115+
).rejects.toThrow(getNoInstancesFoundMessage(NO_MATCHES_TEXT));
116+
});
117+
118+
describe('findBy options deprecations', () => {
119+
let warnSpy: jest.SpyInstance;
120+
beforeEach(() => {
121+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
122+
});
123+
afterEach(() => {
124+
warnSpy.mockRestore();
125+
});
126+
127+
test('findByText queries warn on deprecated use of WaitForOptions', async () => {
128+
const options = { timeout: 10 };
129+
// mock implementation to avoid warning in the test suite
130+
const view = render(<View />);
131+
await expect(
132+
view.findByLabelText('Some Text', options)
133+
).rejects.toBeTruthy();
134+
135+
setTimeout(
136+
() => view.rerender(<View accessibilityLabel="Some Text" />),
137+
20
138+
);
139+
await expect(view.findByLabelText('Some Text')).resolves.toBeTruthy();
140+
141+
expect(warnSpy).toHaveBeenCalledWith(
142+
expect.stringContaining('Use of option "timeout"')
143+
);
144+
}, 20000);
145+
});

src/queries/hintText.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
2-
import { TextMatch } from '../matches';
3-
import { matchStringProp } from '../helpers/matchers/matchStringProp';
2+
import { matches, TextMatch } from '../matches';
43
import { makeQueries } from './makeQueries';
54
import type {
65
FindAllByQuery,
@@ -10,15 +9,28 @@ import type {
109
QueryAllByQuery,
1110
QueryByQuery,
1211
} from './makeQueries';
12+
import { TextMatchOptions } from './text';
13+
14+
const getNodeByHintText = (
15+
node: ReactTestInstance,
16+
text: TextMatch,
17+
options: TextMatchOptions = {}
18+
) => {
19+
const { exact, normalizer } = options;
20+
return matches(text, node.props.accessibilityHint, normalizer, exact);
21+
};
1322

1423
const queryAllByHintText = (
1524
instance: ReactTestInstance
16-
): ((hint: TextMatch) => Array<ReactTestInstance>) =>
17-
function queryAllByA11yHintFn(hint) {
25+
): ((
26+
hint: TextMatch,
27+
queryOptions?: TextMatchOptions
28+
) => Array<ReactTestInstance>) =>
29+
function queryAllByA11yHintFn(hint, queryOptions) {
1830
return instance.findAll(
1931
(node) =>
2032
typeof node.type === 'string' &&
21-
matchStringProp(node.props.accessibilityHint, hint)
33+
getNodeByHintText(node, hint, queryOptions)
2234
);
2335
};
2436

@@ -34,28 +46,28 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
3446
);
3547

3648
export type ByHintTextQueries = {
37-
getByHintText: GetByQuery<TextMatch>;
38-
getAllByHintText: GetAllByQuery<TextMatch>;
39-
queryByHintText: QueryByQuery<TextMatch>;
40-
queryAllByHintText: QueryAllByQuery<TextMatch>;
41-
findByHintText: FindByQuery<TextMatch>;
42-
findAllByHintText: FindAllByQuery<TextMatch>;
49+
getByHintText: GetByQuery<TextMatch, TextMatchOptions>;
50+
getAllByHintText: GetAllByQuery<TextMatch, TextMatchOptions>;
51+
queryByHintText: QueryByQuery<TextMatch, TextMatchOptions>;
52+
queryAllByHintText: QueryAllByQuery<TextMatch, TextMatchOptions>;
53+
findByHintText: FindByQuery<TextMatch, TextMatchOptions>;
54+
findAllByHintText: FindAllByQuery<TextMatch, TextMatchOptions>;
4355

4456
// a11yHint aliases
45-
getByA11yHint: GetByQuery<TextMatch>;
46-
getAllByA11yHint: GetAllByQuery<TextMatch>;
47-
queryByA11yHint: QueryByQuery<TextMatch>;
48-
queryAllByA11yHint: QueryAllByQuery<TextMatch>;
49-
findByA11yHint: FindByQuery<TextMatch>;
50-
findAllByA11yHint: FindAllByQuery<TextMatch>;
57+
getByA11yHint: GetByQuery<TextMatch, TextMatchOptions>;
58+
getAllByA11yHint: GetAllByQuery<TextMatch, TextMatchOptions>;
59+
queryByA11yHint: QueryByQuery<TextMatch, TextMatchOptions>;
60+
queryAllByA11yHint: QueryAllByQuery<TextMatch, TextMatchOptions>;
61+
findByA11yHint: FindByQuery<TextMatch, TextMatchOptions>;
62+
findAllByA11yHint: FindAllByQuery<TextMatch, TextMatchOptions>;
5163

5264
// accessibilityHint aliases
53-
getByAccessibilityHint: GetByQuery<TextMatch>;
54-
getAllByAccessibilityHint: GetAllByQuery<TextMatch>;
55-
queryByAccessibilityHint: QueryByQuery<TextMatch>;
56-
queryAllByAccessibilityHint: QueryAllByQuery<TextMatch>;
57-
findByAccessibilityHint: FindByQuery<TextMatch>;
58-
findAllByAccessibilityHint: FindAllByQuery<TextMatch>;
65+
getByAccessibilityHint: GetByQuery<TextMatch, TextMatchOptions>;
66+
getAllByAccessibilityHint: GetAllByQuery<TextMatch, TextMatchOptions>;
67+
queryByAccessibilityHint: QueryByQuery<TextMatch, TextMatchOptions>;
68+
queryAllByAccessibilityHint: QueryAllByQuery<TextMatch, TextMatchOptions>;
69+
findByAccessibilityHint: FindByQuery<TextMatch, TextMatchOptions>;
70+
findAllByAccessibilityHint: FindAllByQuery<TextMatch, TextMatchOptions>;
5971
};
6072

6173
export const bindByHintTextQueries = (

src/queries/labelText.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
2-
import { TextMatch } from '../matches';
3-
import { matchStringProp } from '../helpers/matchers/matchStringProp';
2+
import { matches, TextMatch } from '../matches';
43
import { makeQueries } from './makeQueries';
54
import type {
65
FindAllByQuery,
@@ -10,15 +9,28 @@ import type {
109
QueryAllByQuery,
1110
QueryByQuery,
1211
} from './makeQueries';
12+
import { TextMatchOptions } from './text';
13+
14+
const getNodeByLabelText = (
15+
node: ReactTestInstance,
16+
text: TextMatch,
17+
options: TextMatchOptions = {}
18+
) => {
19+
const { exact, normalizer } = options;
20+
return matches(text, node.props.accessibilityLabel, normalizer, exact);
21+
};
1322

1423
const queryAllByLabelText = (
1524
instance: ReactTestInstance
16-
): ((text: TextMatch) => Array<ReactTestInstance>) =>
17-
function queryAllByLabelTextFn(text) {
25+
): ((
26+
text: TextMatch,
27+
queryOptions?: TextMatchOptions
28+
) => Array<ReactTestInstance>) =>
29+
function queryAllByLabelTextFn(text, queryOptions?: TextMatchOptions) {
1830
return instance.findAll(
1931
(node) =>
2032
typeof node.type === 'string' &&
21-
matchStringProp(node.props.accessibilityLabel, text)
33+
getNodeByLabelText(node, text, queryOptions)
2234
);
2335
};
2436

@@ -34,12 +46,12 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
3446
);
3547

3648
export type ByLabelTextQueries = {
37-
getByLabelText: GetByQuery<TextMatch>;
38-
getAllByLabelText: GetAllByQuery<TextMatch>;
39-
queryByLabelText: QueryByQuery<TextMatch>;
40-
queryAllByLabelText: QueryAllByQuery<TextMatch>;
41-
findByLabelText: FindByQuery<TextMatch>;
42-
findAllByLabelText: FindAllByQuery<TextMatch>;
49+
getByLabelText: GetByQuery<TextMatch, TextMatchOptions>;
50+
getAllByLabelText: GetAllByQuery<TextMatch, TextMatchOptions>;
51+
queryByLabelText: QueryByQuery<TextMatch, TextMatchOptions>;
52+
queryAllByLabelText: QueryAllByQuery<TextMatch, TextMatchOptions>;
53+
findByLabelText: FindByQuery<TextMatch, TextMatchOptions>;
54+
findAllByLabelText: FindAllByQuery<TextMatch, TextMatchOptions>;
4355
};
4456

4557
export const bindByLabelTextQueries = (

src/queries/makeQueries.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ export type QueryAllByQuery<Predicate, Options = void> = (
2525

2626
export type FindByQuery<Predicate, Options = void> = (
2727
predicate: Predicate,
28-
options?: Options,
28+
// Remove `& WaitForOptions` when all queries have been migrated to support 2nd arg query options.
29+
options?: Options & WaitForOptions,
2930
waitForOptions?: WaitForOptions
3031
) => Promise<ReactTestInstance>;
3132

3233
export type FindAllByQuery<Predicate, Options = void> = (
3334
predicate: Predicate,
34-
options?: Options,
35+
// Remove `& WaitForOptions` when all queries have been migrated to support 2nd arg query options.
36+
options?: Options & WaitForOptions,
3537
waitForOptions?: WaitForOptions
3638
) => Promise<ReactTestInstance[]>;
3739

@@ -46,6 +48,41 @@ export type UnboundQueries<Predicate, Options> = {
4648
findAllBy: UnboundQuery<FindAllByQuery<Predicate, Options>>;
4749
};
4850

51+
const deprecatedKeys: (keyof WaitForOptions)[] = [
52+
'timeout',
53+
'interval',
54+
'stackTraceError',
55+
];
56+
57+
// The WaitForOptions has been moved to the second option param of findBy* methods with the adding of TextMatchOptions
58+
// To make the migration easier and avoid a breaking change, keep reading this options from the first param but warn
59+
function extractDeprecatedWaitForOptions(options?: WaitForOptions) {
60+
if (!options) {
61+
return undefined;
62+
}
63+
64+
const waitForOptions: WaitForOptions = {
65+
timeout: options.timeout,
66+
interval: options.interval,
67+
stackTraceError: options.stackTraceError,
68+
};
69+
70+
deprecatedKeys.forEach((key) => {
71+
const option = options[key];
72+
if (option) {
73+
// eslint-disable-next-line no-console
74+
console.warn(
75+
`Use of option "${key}" in a findBy* query options (2nd parameter) is deprecated. Please pass this option in the waitForOptions (3rd parameter).
76+
Example:
77+
78+
findByText(text, {}, { ${key}: ${option.toString()} })`
79+
);
80+
}
81+
});
82+
83+
return waitForOptions;
84+
}
85+
4986
export function makeQueries<Predicate, Options>(
5087
queryAllByQuery: UnboundQuery<QueryAllByQuery<Predicate, Options>>,
5188
getMissingError: (predicate: Predicate, options?: Options) => string,
@@ -101,26 +138,30 @@ export function makeQueries<Predicate, Options>(
101138
function findAllByQuery(instance: ReactTestInstance) {
102139
return function findAllFn(
103140
predicate: Predicate,
104-
queryOptions?: Options,
105-
waitForOptions?: WaitForOptions
141+
queryOptions?: Options & WaitForOptions,
142+
waitForOptions: WaitForOptions = {}
106143
) {
107-
return waitFor(
108-
() => getAllByQuery(instance)(predicate, queryOptions),
109-
waitForOptions
110-
);
144+
const deprecatedWaitForOptions =
145+
extractDeprecatedWaitForOptions(queryOptions);
146+
return waitFor(() => getAllByQuery(instance)(predicate, queryOptions), {
147+
...deprecatedWaitForOptions,
148+
...waitForOptions,
149+
});
111150
};
112151
}
113152

114153
function findByQuery(instance: ReactTestInstance) {
115154
return function findFn(
116155
predicate: Predicate,
117-
queryOptions?: Options,
118-
waitForOptions?: WaitForOptions
156+
queryOptions?: Options & WaitForOptions,
157+
waitForOptions: WaitForOptions = {}
119158
) {
120-
return waitFor(
121-
() => getByQuery(instance)(predicate, queryOptions),
122-
waitForOptions
123-
);
159+
const deprecatedWaitForOptions =
160+
extractDeprecatedWaitForOptions(queryOptions);
161+
return waitFor(() => getByQuery(instance)(predicate, queryOptions), {
162+
...deprecatedWaitForOptions,
163+
...waitForOptions,
164+
});
124165
};
125166
}
126167

0 commit comments

Comments
 (0)