Skip to content

Commit 125c233

Browse files
committed
feat: support *ByRole queries
1 parent 70dd329 commit 125c233

File tree

5 files changed

+56
-23
lines changed

5 files changed

+56
-23
lines changed

src/helpers/accessibility.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole
103103
return false;
104104
}
105105

106+
if (isHostImage(element) && element.props.alt) {
107+
return true;
108+
}
109+
106110
if (element.props.accessible !== undefined) {
107111
return element.props.accessible;
108112
}
@@ -131,16 +135,28 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole
131135
export function getRole(element: ReactTestInstance): Role | AccessibilityRole {
132136
const explicitRole = element.props.role ?? element.props.accessibilityRole;
133137
if (explicitRole) {
134-
return explicitRole;
138+
return normalizeRole(explicitRole);
135139
}
136140

137141
if (isHostText(element)) {
138142
return 'text';
139143
}
140144

145+
if (isHostImage(element) && element.props.alt) {
146+
return 'img';
147+
}
148+
141149
return 'none';
142150
}
143151

152+
export function normalizeRole(role: string): Role | AccessibilityRole {
153+
if (role === 'image') {
154+
return 'img';
155+
}
156+
157+
return role as Role | AccessibilityRole;
158+
}
159+
144160
export function computeAriaModal(element: ReactTestInstance): boolean | undefined {
145161
return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal;
146162
}

src/helpers/format-default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const propsToDisplay = [
99
'accessibilityLabelledBy',
1010
'accessibilityRole',
1111
'accessibilityViewIsModal',
12+
'alt',
1213
'aria-busy',
1314
'aria-checked',
1415
'aria-disabled',

src/queries/__tests__/role-value.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('accessibility value', () => {
7777

7878
expect(() => screen.getByRole('adjustable', { name: 'Hello', value: { min: 5 } }))
7979
.toThrowErrorMatchingInlineSnapshot(`
80-
"Unable to find an element with role: "adjustable", name: "Hello", min value: 5
80+
"Unable to find an element with role: adjustable, name: Hello, min value: 5
8181
8282
<Text
8383
accessibilityRole="adjustable"
@@ -100,7 +100,7 @@ describe('accessibility value', () => {
100100
`);
101101
expect(() => screen.getByRole('adjustable', { name: 'World', value: { min: 10 } }))
102102
.toThrowErrorMatchingInlineSnapshot(`
103-
"Unable to find an element with role: "adjustable", name: "World", min value: 10
103+
"Unable to find an element with role: adjustable, name: World, min value: 10
104104
105105
<Text
106106
accessibilityRole="adjustable"
@@ -123,7 +123,7 @@ describe('accessibility value', () => {
123123
`);
124124
expect(() => screen.getByRole('adjustable', { name: 'Hello', value: { min: 5 } }))
125125
.toThrowErrorMatchingInlineSnapshot(`
126-
"Unable to find an element with role: "adjustable", name: "Hello", min value: 5
126+
"Unable to find an element with role: adjustable, name: Hello, min value: 5
127127
128128
<Text
129129
accessibilityRole="adjustable"
@@ -146,7 +146,7 @@ describe('accessibility value', () => {
146146
`);
147147
expect(() => screen.getByRole('adjustable', { selected: true, value: { min: 10 } }))
148148
.toThrowErrorMatchingInlineSnapshot(`
149-
"Unable to find an element with role: "adjustable", selected state: true, min value: 10
149+
"Unable to find an element with role: adjustable, selected state: true, min value: 10
150150
151151
<Text
152152
accessibilityRole="adjustable"

src/queries/__tests__/role.test.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Pressable,
55
Text,
66
TextInput,
7+
Image,
78
TouchableOpacity,
89
TouchableWithoutFeedback,
910
View,
@@ -17,11 +18,11 @@ const TEXT_LABEL = 'cool text';
1718
const NO_MATCHES_TEXT: any = 'not-existent-element';
1819

1920
const getMultipleInstancesFoundMessage = (value: string) => {
20-
return `Found multiple elements with role: "${value}"`;
21+
return `Found multiple elements with role: ${value}`;
2122
};
2223

2324
const getNoInstancesFoundMessage = (value: string) => {
24-
return `Unable to find an element with role: "${value}"`;
25+
return `Unable to find an element with role: ${value}`;
2526
};
2627

2728
const Typography = ({ children, ...rest }: any) => {
@@ -224,6 +225,15 @@ describe('supports name option', () => {
224225
expect(screen.getByRole('header', { name: 'About' })).toBe(screen.getByTestId('target-header'));
225226
expect(screen.getByRole('header', { name: 'About' }).props.testID).toBe('target-header');
226227
});
228+
229+
test('supports host Image element with "alt" prop', () => {
230+
render(<Image alt="An elephant" testID="image" />);
231+
232+
const expectedElement = screen.getByTestId('image');
233+
expect(screen.getByRole('img', { name: 'An elephant' })).toBe(expectedElement);
234+
expect(screen.getByRole('image', { name: 'An elephant' })).toBe(expectedElement);
235+
expect(screen.getByRole(/img/, { name: 'An elephant' })).toBe(expectedElement);
236+
});
227237
});
228238

229239
describe('supports accessibility states', () => {
@@ -768,7 +778,7 @@ describe('error messages', () => {
768778
render(<View />);
769779

770780
expect(() => screen.getByRole('button')).toThrowErrorMatchingInlineSnapshot(`
771-
"Unable to find an element with role: "button"
781+
"Unable to find an element with role: button
772782
773783
<View />"
774784
`);
@@ -778,7 +788,7 @@ describe('error messages', () => {
778788
render(<View />);
779789

780790
expect(() => screen.getByRole('button', { name: 'Save' })).toThrowErrorMatchingInlineSnapshot(`
781-
"Unable to find an element with role: "button", name: "Save"
791+
"Unable to find an element with role: button, name: Save
782792
783793
<View />"
784794
`);
@@ -789,7 +799,7 @@ describe('error messages', () => {
789799

790800
expect(() => screen.getByRole('button', { name: 'Save', disabled: true }))
791801
.toThrowErrorMatchingInlineSnapshot(`
792-
"Unable to find an element with role: "button", name: "Save", disabled state: true
802+
"Unable to find an element with role: button, name: Save, disabled state: true
793803
794804
<View />"
795805
`);
@@ -800,7 +810,7 @@ describe('error messages', () => {
800810

801811
expect(() => screen.getByRole('button', { name: 'Save', disabled: true, selected: true }))
802812
.toThrowErrorMatchingInlineSnapshot(`
803-
"Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true
813+
"Unable to find an element with role: button, name: Save, disabled state: true, selected state: true
804814
805815
<View />"
806816
`);
@@ -811,7 +821,7 @@ describe('error messages', () => {
811821

812822
expect(() => screen.getByRole('button', { disabled: true }))
813823
.toThrowErrorMatchingInlineSnapshot(`
814-
"Unable to find an element with role: "button", disabled state: true
824+
"Unable to find an element with role: button, disabled state: true
815825
816826
<View />"
817827
`);
@@ -822,7 +832,7 @@ describe('error messages', () => {
822832

823833
expect(() => screen.getByRole('adjustable', { value: { min: 1 } }))
824834
.toThrowErrorMatchingInlineSnapshot(`
825-
"Unable to find an element with role: "adjustable", min value: 1
835+
"Unable to find an element with role: adjustable, min value: 1
826836
827837
<View />"
828838
`);
@@ -832,7 +842,7 @@ describe('error messages', () => {
832842
value: { min: 1, max: 2, now: 1, text: /hello/ },
833843
}),
834844
).toThrowErrorMatchingInlineSnapshot(`
835-
"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/
845+
"Unable to find an element with role: adjustable, min value: 1, max value: 2, now value: 1, text value: /hello/
836846
837847
<View />"
838848
`);
@@ -852,7 +862,7 @@ test('byRole queries support hidden option', () => {
852862
expect(screen.queryByRole('button', { includeHiddenElements: false })).toBeFalsy();
853863
expect(() => screen.getByRole('button', { includeHiddenElements: false }))
854864
.toThrowErrorMatchingInlineSnapshot(`
855-
"Unable to find an element with role: "button"
865+
"Unable to find an element with role: button
856866
857867
<View
858868
accessibilityRole="button"
@@ -889,7 +899,7 @@ describe('matches only accessible elements', () => {
889899
expect(screen.queryByRole('button', { name: 'Action' })).toBeFalsy();
890900
});
891901

892-
test('ignores elements with accessible={undefined} and that are implicitely not accessible', () => {
902+
test('ignores elements with accessible={undefined} and that are implicitly not accessible', () => {
893903
render(
894904
<View accessibilityRole="menu">
895905
<Text>Action</Text>
@@ -903,31 +913,31 @@ test('error message renders the element tree, preserving only helpful props', as
903913
render(<View accessibilityRole="button" key="3" />);
904914

905915
expect(() => screen.getByRole('link')).toThrowErrorMatchingInlineSnapshot(`
906-
"Unable to find an element with role: "link"
916+
"Unable to find an element with role: link
907917
908918
<View
909919
accessibilityRole="button"
910920
/>"
911921
`);
912922

913923
expect(() => screen.getAllByRole('link')).toThrowErrorMatchingInlineSnapshot(`
914-
"Unable to find an element with role: "link"
924+
"Unable to find an element with role: link
915925
916926
<View
917927
accessibilityRole="button"
918928
/>"
919929
`);
920930

921931
await expect(screen.findByRole('link')).rejects.toThrowErrorMatchingInlineSnapshot(`
922-
"Unable to find an element with role: "link"
932+
"Unable to find an element with role: link
923933
924934
<View
925935
accessibilityRole="button"
926936
/>"
927937
`);
928938

929939
await expect(screen.findAllByRole('link')).rejects.toThrowErrorMatchingInlineSnapshot(`
930-
"Unable to find an element with role: "link"
940+
"Unable to find an element with role: link
931941
932942
<View
933943
accessibilityRole="button"

src/queries/role.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
accessibilityValueKeys,
66
getRole,
77
isAccessibilityElement,
8+
normalizeRole,
89
} from '../helpers/accessibility';
910
import { findAll } from '../helpers/find-all';
1011
import {
@@ -38,6 +39,10 @@ export type ByRoleOptions = CommonQueryOptions &
3839
value?: AccessibilityValueMatcher;
3940
};
4041

42+
const matchRole = (node: ReactTestInstance, role: ByRoleMatcher) => {
43+
return matchStringProp(getRole(node), role);
44+
};
45+
4146
const matchAccessibleNameIfNeeded = (node: ReactTestInstance, name?: TextMatch) => {
4247
if (name == null) return true;
4348

@@ -60,12 +65,13 @@ const queryAllByRole = (
6065
instance: ReactTestInstance,
6166
): QueryAllByQuery<ByRoleMatcher, ByRoleOptions> =>
6267
function queryAllByRoleFn(role, options) {
68+
const normalizedRole = typeof role === 'string' ? normalizeRole(role) : role;
6369
return findAll(
6470
instance,
6571
(node) =>
6672
// run the cheapest checks first, and early exit to avoid unneeded computations
6773
isAccessibilityElement(node) &&
68-
matchStringProp(getRole(node), role) &&
74+
matchRole(node, normalizedRole) &&
6975
matchAccessibleStateIfNeeded(node, options) &&
7076
matchAccessibilityValueIfNeeded(node, options?.value) &&
7177
matchAccessibleNameIfNeeded(node, options?.name),
@@ -74,10 +80,10 @@ const queryAllByRole = (
7480
};
7581

7682
const formatQueryParams = (role: TextMatch, options: ByRoleOptions = {}) => {
77-
const params = [`role: "${String(role)}"`];
83+
const params = [`role: ${String(role)}`];
7884

7985
if (options.name) {
80-
params.push(`name: "${String(options.name)}"`);
86+
params.push(`name: ${String(options.name)}`);
8187
}
8288

8389
accessibilityStateKeys.forEach((stateKey) => {

0 commit comments

Comments
 (0)