Skip to content

Commit 527ea62

Browse files
feat: *ByRole a11y value option (#1210)
1 parent f746a86 commit 527ea62

File tree

10 files changed

+308
-65
lines changed

10 files changed

+308
-65
lines changed

src/helpers/__tests__/accessiblity.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ test('returns false for accessible elements', () => {
2222
).toBe(false);
2323
});
2424

25-
test('returns true for hidden elements', () => {
25+
test('returns true for null elements', () => {
2626
expect(isHiddenFromAccessibility(null)).toBe(true);
2727
});
2828

src/helpers/accessiblity.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
1-
import { AccessibilityState, StyleSheet } from 'react-native';
1+
import {
2+
AccessibilityState,
3+
AccessibilityValue,
4+
StyleSheet,
5+
} from 'react-native';
26
import { ReactTestInstance } from 'react-test-renderer';
37
import { getHostSiblings } from './component-tree';
48

59
type IsInaccessibleOptions = {
610
cache?: WeakMap<ReactTestInstance, boolean>;
711
};
812

9-
export type AccessibilityStateKey = keyof AccessibilityState;
10-
11-
export const accessibilityStateKeys: AccessibilityStateKey[] = [
13+
export const accessibilityStateKeys: (keyof AccessibilityState)[] = [
1214
'disabled',
1315
'selected',
1416
'checked',
1517
'busy',
1618
'expanded',
1719
];
1820

21+
export const accessiblityValueKeys: (keyof AccessibilityValue)[] = [
22+
'min',
23+
'max',
24+
'now',
25+
'text',
26+
];
27+
1928
export function isHiddenFromAccessibility(
2029
element: ReactTestInstance | null,
2130
{ cache }: IsInaccessibleOptions = {}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { AccessibilityValue } from 'react-native';
2+
import { ReactTestInstance } from 'react-test-renderer';
3+
import { TextMatch } from '../../matches';
4+
import { matchStringProp } from './matchStringProp';
5+
6+
export interface AccessibilityValueMatcher {
7+
min?: number;
8+
max?: number;
9+
now?: number;
10+
text?: TextMatch;
11+
}
12+
13+
export function matchAccessibilityValue(
14+
node: ReactTestInstance,
15+
matcher: AccessibilityValueMatcher
16+
): boolean {
17+
const value: AccessibilityValue = node.props.accessibilityValue ?? {};
18+
return (
19+
(matcher.min === undefined || matcher.min === value.min) &&
20+
(matcher.max === undefined || matcher.max === value.max) &&
21+
(matcher.now === undefined || matcher.now === value.now) &&
22+
(matcher.text === undefined || matchStringProp(value.text, matcher.text))
23+
);
24+
}

src/queries/__tests__/a11yValue.test.tsx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
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 TEXT_LABEL = 'cool text';
66

7-
const getMultipleInstancesFoundMessage = (value: string) => {
8-
return `Found multiple elements with accessibilityValue: ${value}`;
9-
};
10-
11-
const getNoInstancesFoundMessage = (value: string) => {
12-
return `Unable to find an element with accessibilityValue: ${value}`;
13-
};
14-
157
const Typography = ({ children, ...rest }: any) => {
168
return <Text {...rest}>{children}</Text>;
179
};
@@ -46,15 +38,15 @@ test('getByA11yValue, queryByA11yValue, findByA11yValue', async () => {
4638
});
4739

4840
expect(() => getByA11yValue({ min: 50 })).toThrow(
49-
getNoInstancesFoundMessage('{"min":50}')
41+
'Unable to find an element with min value: 50'
5042
);
5143
expect(queryByA11yValue({ min: 50 })).toEqual(null);
5244

5345
expect(() => getByA11yValue({ max: 60 })).toThrow(
54-
getMultipleInstancesFoundMessage('{"max":60}')
46+
'Found multiple elements with max value: 60'
5547
);
5648
expect(() => queryByA11yValue({ max: 60 })).toThrow(
57-
getMultipleInstancesFoundMessage('{"max":60}')
49+
'Found multiple elements with max value: 60'
5850
);
5951

6052
const asyncElement = await findByA11yValue({ min: 40 });
@@ -63,10 +55,10 @@ test('getByA11yValue, queryByA11yValue, findByA11yValue', async () => {
6355
max: 60,
6456
});
6557
await expect(findByA11yValue({ min: 50 })).rejects.toThrow(
66-
getNoInstancesFoundMessage('{"min":50}')
58+
'Unable to find an element with min value: 50'
6759
);
6860
await expect(findByA11yValue({ max: 60 })).rejects.toThrow(
69-
getMultipleInstancesFoundMessage('{"max":60}')
61+
'Found multiple elements with max value: 60'
7062
);
7163
});
7264

@@ -79,7 +71,7 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => {
7971
expect(queryAllByA11yValue({ min: 40 })).toHaveLength(1);
8072

8173
expect(() => getAllByA11yValue({ min: 50 })).toThrow(
82-
getNoInstancesFoundMessage('{"min":50}')
74+
'Unable to find an element with min value: 50'
8375
);
8476
expect(queryAllByA11yValue({ min: 50 })).toEqual([]);
8577

@@ -88,7 +80,7 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => {
8880

8981
await expect(findAllByA11yValue({ min: 40 })).resolves.toHaveLength(1);
9082
await expect(findAllByA11yValue({ min: 50 })).rejects.toThrow(
91-
getNoInstancesFoundMessage('{"min":50}')
83+
'Unable to find an element with min value: 50'
9284
);
9385
await expect(findAllByA11yValue({ max: 60 })).resolves.toHaveLength(2);
9486
});
@@ -111,6 +103,30 @@ test('byA11yValue queries support hidden option', () => {
111103
expect(() =>
112104
getByA11yValue({ max: 10 }, { includeHiddenElements: false })
113105
).toThrowErrorMatchingInlineSnapshot(
114-
`"Unable to find an element with accessibilityValue: {"max":10}"`
106+
`"Unable to find an element with max value: 10"`
107+
);
108+
});
109+
110+
test('byA11yValue error messages', () => {
111+
const { getByA11yValue } = render(<View />);
112+
expect(() =>
113+
getByA11yValue({ min: 10, max: 10 })
114+
).toThrowErrorMatchingInlineSnapshot(
115+
`"Unable to find an element with min value: 10, max value: 10"`
116+
);
117+
expect(() =>
118+
getByA11yValue({ max: 20, now: 5 })
119+
).toThrowErrorMatchingInlineSnapshot(
120+
`"Unable to find an element with max value: 20, now value: 5"`
121+
);
122+
expect(() =>
123+
getByA11yValue({ min: 1, max: 2, now: 3 })
124+
).toThrowErrorMatchingInlineSnapshot(
125+
`"Unable to find an element with min value: 1, max value: 2, now value: 3"`
126+
);
127+
expect(() =>
128+
getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i })
129+
).toThrowErrorMatchingInlineSnapshot(
130+
`"Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i"`
115131
);
116132
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as React from 'react';
2+
import { View, Text } from 'react-native';
3+
import { render } from '../..';
4+
5+
describe('accessibility value', () => {
6+
test('matches using all value props', () => {
7+
const { getByRole, queryByRole } = render(
8+
<View
9+
accessibilityRole="adjustable"
10+
accessibilityValue={{ min: 0, max: 100, now: 50, text: '50%' }}
11+
/>
12+
);
13+
14+
expect(
15+
getByRole('adjustable', {
16+
value: { min: 0, max: 100, now: 50, text: '50%' },
17+
})
18+
).toBeTruthy();
19+
expect(
20+
queryByRole('adjustable', {
21+
value: { min: 1, max: 100, now: 50, text: '50%' },
22+
})
23+
).toBeFalsy();
24+
expect(
25+
queryByRole('adjustable', {
26+
value: { min: 0, max: 99, now: 50, text: '50%' },
27+
})
28+
).toBeFalsy();
29+
expect(
30+
queryByRole('adjustable', {
31+
value: { min: 0, max: 100, now: 45, text: '50%' },
32+
})
33+
).toBeFalsy();
34+
expect(
35+
queryByRole('adjustable', {
36+
value: { min: 0, max: 100, now: 50, text: '55%' },
37+
})
38+
).toBeFalsy();
39+
});
40+
41+
test('matches using single value', () => {
42+
const { getByRole, queryByRole } = render(
43+
<View
44+
accessibilityRole="adjustable"
45+
accessibilityValue={{ min: 10, max: 20, now: 12, text: 'Hello' }}
46+
/>
47+
);
48+
49+
expect(getByRole('adjustable', { value: { min: 10 } })).toBeTruthy();
50+
expect(getByRole('adjustable', { value: { max: 20 } })).toBeTruthy();
51+
expect(getByRole('adjustable', { value: { now: 12 } })).toBeTruthy();
52+
expect(getByRole('adjustable', { value: { text: 'Hello' } })).toBeTruthy();
53+
expect(getByRole('adjustable', { value: { text: /hello/i } })).toBeTruthy();
54+
55+
expect(queryByRole('adjustable', { value: { min: 11 } })).toBeFalsy();
56+
expect(queryByRole('adjustable', { value: { max: 19 } })).toBeFalsy();
57+
expect(queryByRole('adjustable', { value: { now: 15 } })).toBeFalsy();
58+
expect(queryByRole('adjustable', { value: { text: 'No' } })).toBeFalsy();
59+
expect(queryByRole('adjustable', { value: { text: /no/ } })).toBeFalsy();
60+
});
61+
62+
test('matches using single value and other options', () => {
63+
const { getByRole } = render(
64+
<Text
65+
accessibilityRole="adjustable"
66+
accessibilityState={{ disabled: true }}
67+
accessibilityValue={{ min: 10, max: 20, now: 12, text: 'Hello' }}
68+
>
69+
Hello
70+
</Text>
71+
);
72+
73+
expect(
74+
getByRole('adjustable', { name: 'Hello', value: { min: 10 } })
75+
).toBeTruthy();
76+
expect(
77+
getByRole('adjustable', { disabled: true, value: { min: 10 } })
78+
).toBeTruthy();
79+
80+
expect(() =>
81+
getByRole('adjustable', { name: 'Hello', value: { min: 5 } })
82+
).toThrowErrorMatchingInlineSnapshot(
83+
`"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"`
84+
);
85+
expect(() =>
86+
getByRole('adjustable', { name: 'World', value: { min: 10 } })
87+
).toThrowErrorMatchingInlineSnapshot(
88+
`"Unable to find an element with role: "adjustable", name: "World", min value: 10"`
89+
);
90+
expect(() =>
91+
getByRole('adjustable', { name: 'Hello', value: { min: 5 } })
92+
).toThrowErrorMatchingInlineSnapshot(
93+
`"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"`
94+
);
95+
expect(() =>
96+
getByRole('adjustable', { selected: true, value: { min: 10 } })
97+
).toThrowErrorMatchingInlineSnapshot(
98+
`"Unable to find an element with role: "adjustable", selected state: true, min value: 10"`
99+
);
100+
});
101+
});

src/queries/__tests__/role.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,24 @@ describe('error messages', () => {
696696
`"Unable to find an element with role: "button", disabled state: true"`
697697
);
698698
});
699+
700+
test('gives a descriptive error message when querying with a role and an accessibility value', () => {
701+
const { getByRole } = render(<View />);
702+
703+
expect(() =>
704+
getByRole('adjustable', { value: { min: 1 } })
705+
).toThrowErrorMatchingInlineSnapshot(
706+
`"Unable to find an element with role: "adjustable", min value: 1"`
707+
);
708+
709+
expect(() =>
710+
getByRole('adjustable', {
711+
value: { min: 1, max: 2, now: 1, text: /hello/ },
712+
})
713+
).toThrowErrorMatchingInlineSnapshot(
714+
`"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/"`
715+
);
716+
});
699717
});
700718

701719
test('byRole queries support hidden option', () => {

0 commit comments

Comments
 (0)