Skip to content

Commit 860356b

Browse files
feat: option for queries to respect accessibility (#1064)
Co-authored-by: Maciej Jastrzebski <[email protected]>
1 parent b76f11a commit 860356b

32 files changed

+577
-243
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ test('form submits two answers', () => {
117117
fireEvent.press(screen.getByText('Submit'));
118118

119119
expect(mockFn).toBeCalledWith({
120-
'1': { q: 'q1', a: 'a1' },
121-
'2': { q: 'q2', a: 'a2' },
120+
1: { q: 'q1', a: 'a1' },
121+
2: { q: 'q2', a: 'a2' },
122122
});
123123
});
124124
```
@@ -173,4 +173,4 @@ Supported and used by [Rally Health](https://www.rallyhealth.com/careers-home).
173173
[callstack-badge]: https://callstack.com/images/callstack-badge.svg
174174
[callstack]: https://callstack.com/open-source/?utm_source=github.com&utm_medium=referral&utm_campaign=react-native-testing-library&utm_term=readme
175175
[codecov-badge]: https://codecov.io/gh/callstack/react-native-testing-library/branch/main/graph/badge.svg?token=tYVSWro1IP
176-
[codecov]: https://codecov.io/gh/callstack/react-native-testing-library
176+
[codecov]: https://codecov.io/gh/callstack/react-native-testing-library

src/__tests__/config.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ test('configure() overrides existing config values', () => {
1414
expect(getConfig()).toEqual({
1515
asyncUtilTimeout: 5000,
1616
defaultDebugOptions: { message: 'debug message' },
17+
defaultHidden: true,
1718
});
1819
});
1920

src/config.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ export type Config = {
44
/** Default timeout, in ms, for `waitFor` and `findBy*` queries. */
55
asyncUtilTimeout: number;
66

7+
/** Default hidden value for all queries */
8+
defaultHidden: boolean;
9+
710
/** Default options for `debug` helper. */
811
defaultDebugOptions?: Partial<DebugOptions>;
912
};
1013

1114
const defaultConfig: Config = {
1215
asyncUtilTimeout: 1000,
16+
defaultHidden: true,
1317
};
1418

15-
let config = {
16-
...defaultConfig,
17-
};
19+
let config = { ...defaultConfig };
1820

1921
export function configure(options: Partial<Config>) {
2022
config = {
@@ -24,7 +26,7 @@ export function configure(options: Partial<Config>) {
2426
}
2527

2628
export function resetToDefaults() {
27-
config = defaultConfig;
29+
config = { ...defaultConfig };
2830
}
2931

3032
export function getConfig() {

src/helpers/accessiblity.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { AccessibilityState, StyleSheet } from 'react-native';
22
import { ReactTestInstance } from 'react-test-renderer';
33
import { getHostSiblings } from './component-tree';
44

5+
type IsInaccessibleOptions = {
6+
cache?: WeakMap<ReactTestInstance, boolean>;
7+
};
8+
59
export type AccessibilityStateKey = keyof AccessibilityState;
610

711
export const accessibilityStateKeys: AccessibilityStateKey[] = [
@@ -12,14 +16,24 @@ export const accessibilityStateKeys: AccessibilityStateKey[] = [
1216
'expanded',
1317
];
1418

15-
export function isInaccessible(element: ReactTestInstance | null): boolean {
19+
export function isInaccessible(
20+
element: ReactTestInstance | null,
21+
{ cache }: IsInaccessibleOptions = {}
22+
): boolean {
1623
if (element == null) {
1724
return true;
1825
}
1926

2027
let current: ReactTestInstance | null = element;
2128
while (current) {
22-
if (isSubtreeInaccessible(current)) {
29+
let isCurrentSubtreeInaccessible = cache?.get(current);
30+
31+
if (isCurrentSubtreeInaccessible === undefined) {
32+
isCurrentSubtreeInaccessible = isSubtreeInaccessible(current);
33+
cache?.set(current, isCurrentSubtreeInaccessible);
34+
}
35+
36+
if (isCurrentSubtreeInaccessible) {
2337
return true;
2438
}
2539

@@ -29,7 +43,9 @@ export function isInaccessible(element: ReactTestInstance | null): boolean {
2943
return false;
3044
}
3145

32-
function isSubtreeInaccessible(element: ReactTestInstance | null): boolean {
46+
export function isSubtreeInaccessible(
47+
element: ReactTestInstance | null
48+
): boolean {
3349
if (element == null) {
3450
return true;
3551
}
@@ -46,7 +62,7 @@ function isSubtreeInaccessible(element: ReactTestInstance | null): boolean {
4662
return true;
4763
}
4864

49-
// Note that `opacity: 0` is not threated as inassessible on iOS
65+
// Note that `opacity: 0` is not treated as inaccessible on iOS
5066
const flatStyle = StyleSheet.flatten(element.props.style) ?? {};
5167
if (flatStyle.display === 'none') return true;
5268

src/helpers/findAll.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
import { getConfig } from '../config';
3+
import { isInaccessible } from './accessiblity';
4+
5+
interface FindAllOptions {
6+
hidden?: boolean;
7+
}
8+
9+
export function findAll(
10+
root: ReactTestInstance,
11+
predicate: (node: ReactTestInstance) => boolean,
12+
options?: FindAllOptions
13+
) {
14+
const results = root.findAll(predicate);
15+
16+
const hidden = options?.hidden ?? getConfig().defaultHidden;
17+
if (hidden) {
18+
return results;
19+
}
20+
21+
const cache = new WeakMap<ReactTestInstance>();
22+
return results.filter((element) => !isInaccessible(element, { cache }));
23+
}

src/queries/__tests__/a11yState.test.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,22 @@ test('*ByA11yState on TouchableOpacity with "disabled" prop', () => {
226226
expect(view.getByA11yState({ disabled: true })).toBeTruthy();
227227
expect(view.queryByA11yState({ disabled: false })).toBeFalsy();
228228
});
229+
230+
test('byA11yState queries support hidden option', () => {
231+
const { getByA11yState, queryByA11yState } = render(
232+
<Pressable
233+
accessibilityState={{ expanded: false }}
234+
style={{ display: 'none' }}
235+
>
236+
<Text>Hidden from accessibility</Text>
237+
</Pressable>
238+
);
239+
240+
expect(getByA11yState({ expanded: false })).toBeTruthy();
241+
expect(getByA11yState({ expanded: false }, { hidden: true })).toBeTruthy();
242+
243+
expect(queryByA11yState({ expanded: false }, { hidden: false })).toBeFalsy();
244+
expect(() =>
245+
getByA11yState({ expanded: false }, { hidden: false })
246+
).toThrow();
247+
});

src/queries/__tests__/a11yValue.test.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,17 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => {
9292
);
9393
await expect(findAllByA11yValue({ max: 60 })).resolves.toHaveLength(2);
9494
});
95+
96+
test('byA11yValue queries support hidden option', () => {
97+
const { getByA11yValue, queryByA11yValue } = render(
98+
<Text accessibilityValue={{ max: 10 }} style={{ display: 'none' }}>
99+
Hidden from accessibility
100+
</Text>
101+
);
102+
103+
expect(getByA11yValue({ max: 10 })).toBeTruthy();
104+
expect(getByA11yValue({ max: 10 }, { hidden: true })).toBeTruthy();
105+
106+
expect(queryByA11yValue({ max: 10 }, { hidden: false })).toBeFalsy();
107+
expect(() => getByA11yValue({ max: 10 }, { hidden: false })).toThrow();
108+
});

src/queries/__tests__/displayValue.test.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,15 @@ test('findBy queries work asynchronously', async () => {
9999
await expect(findByDisplayValue('Display Value')).resolves.toBeTruthy();
100100
await expect(findAllByDisplayValue('Display Value')).resolves.toHaveLength(1);
101101
}, 20000);
102+
103+
test('byDisplayValue queries support hidden option', () => {
104+
const { getByDisplayValue, queryByDisplayValue } = render(
105+
<TextInput value="hidden" style={{ display: 'none' }} />
106+
);
107+
108+
expect(getByDisplayValue('hidden')).toBeTruthy();
109+
expect(getByDisplayValue('hidden', { hidden: true })).toBeTruthy();
110+
111+
expect(queryByDisplayValue('hidden', { hidden: false })).toBeFalsy();
112+
expect(() => getByDisplayValue('hidden', { hidden: false })).toThrow();
113+
});

src/queries/__tests__/hintText.test.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,17 @@ test('getByHintText, getByHintText and exact = true', () => {
106106
expect(queryByHintText('id', { exact: true })).toBeNull();
107107
expect(getAllByHintText('test', { exact: true })).toHaveLength(1);
108108
});
109+
110+
test('byHintText queries support hidden option', () => {
111+
const { getByHintText, queryByHintText } = render(
112+
<Text accessibilityHint="hidden" style={{ display: 'none' }}>
113+
Hidden from accessiblity
114+
</Text>
115+
);
116+
117+
expect(getByHintText('hidden')).toBeTruthy();
118+
expect(getByHintText('hidden', { hidden: true })).toBeTruthy();
119+
120+
expect(queryByHintText('hidden', { hidden: false })).toBeFalsy();
121+
expect(() => getByHintText('hidden', { hidden: false })).toThrow();
122+
});

src/queries/__tests__/labelText.test.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,17 @@ describe('findBy options deprecations', () => {
143143
);
144144
}, 20000);
145145
});
146+
147+
test('byLabelText queries support hidden option', () => {
148+
const { getByLabelText, queryByLabelText } = render(
149+
<Text accessibilityLabel="hidden" style={{ display: 'none' }}>
150+
Hidden from accessibility
151+
</Text>
152+
);
153+
154+
expect(getByLabelText('hidden')).toBeTruthy();
155+
expect(getByLabelText('hidden', { hidden: true })).toBeTruthy();
156+
157+
expect(queryByLabelText('hidden', { hidden: false })).toBeFalsy();
158+
expect(() => getByLabelText('hidden', { hidden: false })).toThrow();
159+
});

src/queries/__tests__/placeholderText.test.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,15 @@ test('getAllByPlaceholderText, queryAllByPlaceholderText', () => {
5858
expect(queryAllByPlaceholderText(/fresh/i)).toEqual(inputs);
5959
expect(queryAllByPlaceholderText('no placeholder')).toHaveLength(0);
6060
});
61+
62+
test('byPlaceholderText queries support hidden option', () => {
63+
const { getByPlaceholderText, queryByPlaceholderText } = render(
64+
<TextInput placeholder="hidden" style={{ display: 'none' }} />
65+
);
66+
67+
expect(getByPlaceholderText('hidden')).toBeTruthy();
68+
expect(getByPlaceholderText('hidden', { hidden: true })).toBeTruthy();
69+
70+
expect(queryByPlaceholderText('hidden', { hidden: false })).toBeFalsy();
71+
expect(() => getByPlaceholderText('hidden', { hidden: false })).toThrow();
72+
});

src/queries/__tests__/role.test.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -697,3 +697,17 @@ describe('error messages', () => {
697697
);
698698
});
699699
});
700+
701+
test('byRole queries support hidden option', () => {
702+
const { getByRole, queryByRole } = render(
703+
<Pressable accessibilityRole="button" style={{ display: 'none' }}>
704+
<Text>Hidden from accessibility</Text>
705+
</Pressable>
706+
);
707+
708+
expect(getByRole('button')).toBeTruthy();
709+
expect(getByRole('button', { hidden: true })).toBeTruthy();
710+
711+
expect(queryByRole('button', { hidden: false })).toBeFalsy();
712+
expect(() => getByRole('button', { hidden: false })).toThrow();
713+
});

src/queries/__tests__/testId.test.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,17 @@ test('findByTestId and findAllByTestId work asynchronously', async () => {
132132
await expect(findByTestId('aTestId')).resolves.toBeTruthy();
133133
await expect(findAllByTestId('aTestId')).resolves.toHaveLength(1);
134134
}, 20000);
135+
136+
test('byTestId queries support hidden option', () => {
137+
const { getByTestId, queryByTestId } = render(
138+
<Text style={{ display: 'none' }} testID="hidden">
139+
Hidden from accessibility
140+
</Text>
141+
);
142+
143+
expect(getByTestId('hidden')).toBeTruthy();
144+
expect(getByTestId('hidden', { hidden: true })).toBeTruthy();
145+
146+
expect(queryByTestId('hidden', { hidden: false })).toBeFalsy();
147+
expect(() => getByTestId('hidden', { hidden: false })).toThrow();
148+
});

src/queries/__tests__/text.test.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,15 @@ test('getByText searches for text within self host element', () => {
466466
const textNode = within(getByTestId('subject'));
467467
expect(textNode.getByText('Hello')).toBeTruthy();
468468
});
469+
470+
test('byText support hidden option', () => {
471+
const { getByText, queryByText } = render(
472+
<Text style={{ display: 'none' }}>Hidden from accessibility</Text>
473+
);
474+
475+
expect(getByText(/hidden/i)).toBeTruthy();
476+
expect(getByText(/hidden/i, { hidden: true })).toBeTruthy();
477+
478+
expect(queryByText(/hidden/i, { hidden: false })).toBeFalsy();
479+
expect(() => getByText(/hidden/i, { hidden: false })).toThrow();
480+
});

src/queries/a11yState.ts

+36-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
2-
import type { AccessibilityState } from 'react-native';
2+
import { AccessibilityState } from 'react-native';
33
import { accessibilityStateKeys } from '../helpers/accessiblity';
4+
import { findAll } from '../helpers/findAll';
45
import { matchAccessibilityState } from '../helpers/matchers/accessibilityState';
56
import { makeQueries } from './makeQueries';
67
import type {
@@ -11,14 +12,20 @@ import type {
1112
QueryAllByQuery,
1213
QueryByQuery,
1314
} from './makeQueries';
15+
import { CommonQueryOptions } from './options';
1416

1517
const queryAllByA11yState = (
1618
instance: ReactTestInstance
17-
): ((matcher: AccessibilityState) => Array<ReactTestInstance>) =>
18-
function queryAllByA11yStateFn(matcher) {
19-
return instance.findAll(
19+
): ((
20+
matcher: AccessibilityState,
21+
queryOptions?: CommonQueryOptions
22+
) => Array<ReactTestInstance>) =>
23+
function queryAllByA11yStateFn(matcher, queryOptions) {
24+
return findAll(
25+
instance,
2026
(node) =>
21-
typeof node.type === 'string' && matchAccessibilityState(node, matcher)
27+
typeof node.type === 'string' && matchAccessibilityState(node, matcher),
28+
queryOptions
2229
);
2330
};
2431

@@ -47,19 +54,31 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
4754
);
4855

4956
export type ByA11yStateQueries = {
50-
getByA11yState: GetByQuery<AccessibilityState>;
51-
getAllByA11yState: GetAllByQuery<AccessibilityState>;
52-
queryByA11yState: QueryByQuery<AccessibilityState>;
53-
queryAllByA11yState: QueryAllByQuery<AccessibilityState>;
54-
findByA11yState: FindByQuery<AccessibilityState>;
55-
findAllByA11yState: FindAllByQuery<AccessibilityState>;
57+
getByA11yState: GetByQuery<AccessibilityState, CommonQueryOptions>;
58+
getAllByA11yState: GetAllByQuery<AccessibilityState, CommonQueryOptions>;
59+
queryByA11yState: QueryByQuery<AccessibilityState, CommonQueryOptions>;
60+
queryAllByA11yState: QueryAllByQuery<AccessibilityState, CommonQueryOptions>;
61+
findByA11yState: FindByQuery<AccessibilityState, CommonQueryOptions>;
62+
findAllByA11yState: FindAllByQuery<AccessibilityState, CommonQueryOptions>;
5663

57-
getByAccessibilityState: GetByQuery<AccessibilityState>;
58-
getAllByAccessibilityState: GetAllByQuery<AccessibilityState>;
59-
queryByAccessibilityState: QueryByQuery<AccessibilityState>;
60-
queryAllByAccessibilityState: QueryAllByQuery<AccessibilityState>;
61-
findByAccessibilityState: FindByQuery<AccessibilityState>;
62-
findAllByAccessibilityState: FindAllByQuery<AccessibilityState>;
64+
getByAccessibilityState: GetByQuery<AccessibilityState, CommonQueryOptions>;
65+
getAllByAccessibilityState: GetAllByQuery<
66+
AccessibilityState,
67+
CommonQueryOptions
68+
>;
69+
queryByAccessibilityState: QueryByQuery<
70+
AccessibilityState,
71+
CommonQueryOptions
72+
>;
73+
queryAllByAccessibilityState: QueryAllByQuery<
74+
AccessibilityState,
75+
CommonQueryOptions
76+
>;
77+
findByAccessibilityState: FindByQuery<AccessibilityState, CommonQueryOptions>;
78+
findAllByAccessibilityState: FindAllByQuery<
79+
AccessibilityState,
80+
CommonQueryOptions
81+
>;
6382
};
6483

6584
export const bindByA11yStateQueries = (

0 commit comments

Comments
 (0)