Skip to content

Commit 68d9b29

Browse files
committed
feature: a11y value option for *ByRole queries
1 parent a58bd2a commit 68d9b29

File tree

10 files changed

+235
-56
lines changed

10 files changed

+235
-56
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 = (node.props.accessibilityValue ?? {}) as 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: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@ 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
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as React from 'react';
2+
import { View } 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+
});

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', () => {

src/queries/a11yValue.ts

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
2+
import { accessiblityValueKeys } from '../helpers/accessiblity';
23
import { findAll } from '../helpers/findAll';
3-
import { matchObjectProp } from '../helpers/matchers/matchObjectProp';
4+
import {
5+
AccessibilityValueMatcher,
6+
matchAccessibilityValue,
7+
} from '../helpers/matchers/accessibilityValue';
48
import { makeQueries } from './makeQueries';
59
import type {
610
FindAllByQuery,
@@ -12,33 +16,38 @@ import type {
1216
} from './makeQueries';
1317
import { CommonQueryOptions } from './options';
1418

15-
type A11yValue = {
16-
min?: number;
17-
max?: number;
18-
now?: number;
19-
text?: string;
20-
};
21-
2219
const queryAllByA11yValue = (
2320
instance: ReactTestInstance
2421
): ((
25-
value: A11yValue,
22+
value: AccessibilityValueMatcher,
2623
queryOptions?: CommonQueryOptions
2724
) => Array<ReactTestInstance>) =>
2825
function queryAllByA11yValueFn(value, queryOptions) {
2926
return findAll(
3027
instance,
3128
(node) =>
32-
typeof node.type === 'string' &&
33-
matchObjectProp(node.props.accessibilityValue, value),
29+
typeof node.type === 'string' && matchAccessibilityValue(node, value),
3430
queryOptions
3531
);
3632
};
3733

38-
const getMultipleError = (value: A11yValue) =>
39-
`Found multiple elements with accessibilityValue: ${JSON.stringify(value)} `;
40-
const getMissingError = (value: A11yValue) =>
41-
`Unable to find an element with accessibilityValue: ${JSON.stringify(value)}`;
34+
const buildErrorMessage = (value: AccessibilityValueMatcher) => {
35+
const errors: string[] = [];
36+
37+
accessiblityValueKeys.forEach((valueKey) => {
38+
if (value[valueKey] !== undefined) {
39+
errors.push(`${valueKey} value: ${value[valueKey]}`);
40+
}
41+
});
42+
43+
return errors.join(', ');
44+
};
45+
46+
const getMultipleError = (value: AccessibilityValueMatcher) =>
47+
`Found multiple elements with ${buildErrorMessage(value)}`;
48+
49+
const getMissingError = (value: AccessibilityValueMatcher) =>
50+
`Unable to find an element with ${buildErrorMessage(value)}`;
4251

4352
const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
4453
queryAllByA11yValue,
@@ -47,19 +56,46 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
4756
);
4857

4958
export type ByA11yValueQueries = {
50-
getByA11yValue: GetByQuery<A11yValue, CommonQueryOptions>;
51-
getAllByA11yValue: GetAllByQuery<A11yValue, CommonQueryOptions>;
52-
queryByA11yValue: QueryByQuery<A11yValue, CommonQueryOptions>;
53-
queryAllByA11yValue: QueryAllByQuery<A11yValue, CommonQueryOptions>;
54-
findByA11yValue: FindByQuery<A11yValue, CommonQueryOptions>;
55-
findAllByA11yValue: FindAllByQuery<A11yValue, CommonQueryOptions>;
59+
getByA11yValue: GetByQuery<AccessibilityValueMatcher, CommonQueryOptions>;
60+
getAllByA11yValue: GetAllByQuery<
61+
AccessibilityValueMatcher,
62+
CommonQueryOptions
63+
>;
64+
queryByA11yValue: QueryByQuery<AccessibilityValueMatcher, CommonQueryOptions>;
65+
queryAllByA11yValue: QueryAllByQuery<
66+
AccessibilityValueMatcher,
67+
CommonQueryOptions
68+
>;
69+
findByA11yValue: FindByQuery<AccessibilityValueMatcher, CommonQueryOptions>;
70+
findAllByA11yValue: FindAllByQuery<
71+
AccessibilityValueMatcher,
72+
CommonQueryOptions
73+
>;
5674

57-
getByAccessibilityValue: GetByQuery<A11yValue, CommonQueryOptions>;
58-
getAllByAccessibilityValue: GetAllByQuery<A11yValue, CommonQueryOptions>;
59-
queryByAccessibilityValue: QueryByQuery<A11yValue, CommonQueryOptions>;
60-
queryAllByAccessibilityValue: QueryAllByQuery<A11yValue, CommonQueryOptions>;
61-
findByAccessibilityValue: FindByQuery<A11yValue, CommonQueryOptions>;
62-
findAllByAccessibilityValue: FindAllByQuery<A11yValue, CommonQueryOptions>;
75+
getByAccessibilityValue: GetByQuery<
76+
AccessibilityValueMatcher,
77+
CommonQueryOptions
78+
>;
79+
getAllByAccessibilityValue: GetAllByQuery<
80+
AccessibilityValueMatcher,
81+
CommonQueryOptions
82+
>;
83+
queryByAccessibilityValue: QueryByQuery<
84+
AccessibilityValueMatcher,
85+
CommonQueryOptions
86+
>;
87+
queryAllByAccessibilityValue: QueryAllByQuery<
88+
AccessibilityValueMatcher,
89+
CommonQueryOptions
90+
>;
91+
findByAccessibilityValue: FindByQuery<
92+
AccessibilityValueMatcher,
93+
CommonQueryOptions
94+
>;
95+
findAllByAccessibilityValue: FindAllByQuery<
96+
AccessibilityValueMatcher,
97+
CommonQueryOptions
98+
>;
6399
};
64100

65101
export const bindByA11yValueQueries = (

0 commit comments

Comments
 (0)