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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 6 additions & 4 deletions
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

Lines changed: 20 additions & 4 deletions
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

Lines changed: 23 additions & 0 deletions
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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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+
});

0 commit comments

Comments
 (0)