Skip to content

Commit 4445c33

Browse files
anishamaldemdjastrzebski
authored andcommitted
feat: toHaveAccessibleName matcher
1 parent 66e540d commit 4445c33

File tree

7 files changed

+167
-40
lines changed

7 files changed

+167
-40
lines changed

src/helpers/accessiblity.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
StyleSheet,
55
} from 'react-native';
66
import { ReactTestInstance } from 'react-test-renderer';
7-
import { getHostSiblings, getHostParent } from './component-tree';
7+
import { getHostSiblings } from './component-tree';
88
import { getHostComponentNames } from './host-component-names';
99

1010
type IsInaccessibleOptions = {
@@ -233,34 +233,3 @@ export function isElementSelected(
233233
const { accessibilityState, 'aria-selected': ariaSelected } = element.props;
234234
return ariaSelected ?? accessibilityState?.selected ?? false;
235235
}
236-
237-
export function getAccessibleName(
238-
element: ReactTestInstance
239-
): string | undefined {
240-
const labelTextFromLabel = getAccessibilityLabel(element);
241-
const labelTextFromLabelledBy = getAccessibilityLabelledBy(element);
242-
243-
if (labelTextFromLabelledBy) {
244-
const parentElement = getHostParent(element);
245-
246-
if (parentElement) {
247-
const labelElement = parentElement.findByProps({
248-
nativeID: labelTextFromLabelledBy,
249-
});
250-
251-
if (labelElement) {
252-
// Extract and return the text content of the label element
253-
const label = labelElement.props.children;
254-
return label;
255-
}
256-
}
257-
}
258-
259-
if (labelTextFromLabel) {
260-
return labelTextFromLabel;
261-
}
262-
263-
const ownText = element?.children?.join(' ');
264-
265-
return ownText || undefined;
266-
}

src/helpers/matchers/accessibilityName.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReactTestInstance } from 'react-test-renderer';
2-
import { getAccessibleName } from '../accessiblity';
2+
import { getAccessibleName } from '../../matchers/to-have-accessible-name';
33
import { TextMatchOptions, matches, TextMatch } from '../../matches';
44

55
export function matchAccessibleName(

src/matchers/__tests__/to-have-accessible-name.test.tsx

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,24 @@ test('toHaveAccessibleName() on view with accessibilityLabel prop', () => {
99
expect(element).toHaveAccessibleName('Test Label');
1010
});
1111

12+
test('not.toHaveAccessibleName() on view with accessibilityLabel prop', () => {
13+
render(<View testID="accessibility-label" accessibilityLabel="Test Label" />);
14+
const element = screen.getByTestId('accessibility-label');
15+
expect(element).not.toHaveAccessibleName('Not Test Label');
16+
});
17+
1218
test('toHaveAccessibleName() on view with aria-label prop', () => {
1319
render(<View testID="aria-label" aria-label="Aria Test Label" />);
1420
const element = screen.getByTestId('aria-label');
1521
expect(element).toHaveAccessibleName('Aria Test Label');
1622
});
1723

24+
test('not.toHaveAccessibleName() on view with aria-label prop', () => {
25+
render(<View testID="aria-label" aria-label="Aria Test Label" />);
26+
const element = screen.getByTestId('aria-label');
27+
expect(element).not.toHaveAccessibleName('Not Aria Test Label');
28+
});
29+
1830
test('toHaveAccessibleName() on view with accessibilityLabel prop with no expectedName', () => {
1931
render(<View testID="no-expectName-label" accessibilityLabel="Test Label" />);
2032
const element = screen.getByTestId('no-expectName-label');
@@ -27,6 +39,12 @@ test('toHaveAccessibleName() on view with no accessibility props', () => {
2739
expect(element).toHaveAccessibleName('Text');
2840
});
2941

42+
test('not.toHaveAccessibleName() on view with no accessibility props', () => {
43+
render(<Text testID="accessibility-label">Text</Text>);
44+
const element = screen.getByTestId('accessibility-label');
45+
expect(element).not.toHaveAccessibleName('Not the expected Text');
46+
});
47+
3048
test('toHaveAccessibleName() on view with that does not have the expected accessible name', () => {
3149
render(<View testID="wrong-label" accessibilityLabel="The actual label" />);
3250
const element = screen.getByTestId('wrong-label');
@@ -56,6 +74,13 @@ test('toHaveAccessibleName() on view that doesnt have accessible name defined',
5674
`);
5775
});
5876

77+
test('not.toHaveAccessibleName() on view that doesnt have accessible name defined', () => {
78+
render(<View testID="no-accessibile-name" />);
79+
const element = screen.getByTestId('no-accessibile-name');
80+
81+
expect(element).not.toHaveAccessibleName();
82+
});
83+
5984
it('toHaveAccessibleName() on a non-host element', () => {
6085
const nonElement = 'This is not a ReactTestInstance';
6186
expect(() => expect(nonElement).toHaveAccessibleName())
@@ -68,6 +93,18 @@ it('toHaveAccessibleName() on a non-host element', () => {
6893
`);
6994
});
7095

96+
it('not.toHaveAccessibleName() on a non-host element', () => {
97+
const nonElement = 'This is not a ReactTestInstance';
98+
expect(() => expect(nonElement).not.toHaveAccessibleName())
99+
.toThrowErrorMatchingInlineSnapshot(`
100+
"expect(received).not.toHaveAccessibleName()
101+
102+
received value must be a host element.
103+
Received has type: string
104+
Received has value: "This is not a ReactTestInstance""
105+
`);
106+
});
107+
71108
test('toHaveAccessibleName() on view with accessibilityLabelledBy prop', async () => {
72109
render(
73110
<View>
@@ -83,6 +120,63 @@ test('toHaveAccessibleName() on view with accessibilityLabelledBy prop', async (
83120
expect(element).toHaveAccessibleName('Accessibility LabelledBy');
84121
});
85122

123+
test('not.toHaveAccessibleName() on view with accessibilityLabelledBy prop', async () => {
124+
render(
125+
<View>
126+
<Text nativeID="formLabel">Accessibility LabelledBy</Text>
127+
<TextInput
128+
testID="accessibility-labelledby"
129+
accessibilityLabelledBy="formLabel"
130+
/>
131+
</View>
132+
);
133+
134+
const element = screen.getByTestId('accessibility-labelledby');
135+
expect(element).not.toHaveAccessibleName('Not Accessibility LabelledBy');
136+
});
137+
138+
test('getByLabelText supports nested accessibilityLabelledBy', async () => {
139+
render(
140+
<>
141+
<View nativeID="label">
142+
<Text>Nested Accessibility LabelledBy</Text>
143+
</View>
144+
<TextInput testID="text-input" accessibilityLabelledBy="label" />
145+
</>
146+
);
147+
148+
const element = screen.getByTestId('text-input');
149+
expect(element).toHaveAccessibleName('Nested Accessibility LabelledBy');
150+
});
151+
152+
test('not.toHaveAccessibleName() on view with nested accessibilityLabelledBy', async () => {
153+
render(
154+
<>
155+
<View nativeID="label">
156+
<Text>Nested Aria LabelledBy</Text>
157+
</View>
158+
<TextInput testID="text-input" accessibilityLabelledBy="label" />
159+
</>
160+
);
161+
162+
const element = screen.getByTestId('text-input');
163+
expect(element).not.toHaveAccessibleName('Not Nested Aria LabelledBy');
164+
});
165+
166+
test('not.toHaveAccessibleName() on view with nested accessibilityLabelledBy with no text', async () => {
167+
render(
168+
<>
169+
<View nativeID="label">
170+
<View />
171+
</View>
172+
<TextInput testID="text-input" accessibilityLabelledBy="label" />
173+
</>
174+
);
175+
176+
const element = screen.getByTestId('text-input');
177+
expect(element).not.toHaveAccessibleName();
178+
});
179+
86180
test('toHaveAccessibleName() on view with ariaLabelledBy prop', async () => {
87181
render(
88182
<View>
@@ -95,9 +189,20 @@ test('toHaveAccessibleName() on view with ariaLabelledBy prop', async () => {
95189
expect(element).toHaveAccessibleName('Aria LabelledBy');
96190
});
97191

98-
//TODO: This fails as expected as I am unsure on how to extract the Text from the nested Element
192+
test('not.toHaveAccessibleName() on view with ariaLabelledBy prop', async () => {
193+
render(
194+
<View>
195+
<Text nativeID="formLabel">Aria LabelledBy</Text>
196+
<TextInput testID="aria-labelledby" aria-labelledby="formLabel" />
197+
</View>
198+
);
199+
200+
const element = screen.getByTestId('aria-labelledby');
201+
expect(element).not.toHaveAccessibleName('Not Aria LabelledBy');
202+
});
203+
99204
test('getByLabelText supports nested aria-labelledby', async () => {
100-
const screen = render(
205+
render(
101206
<>
102207
<View nativeID="label">
103208
<Text>Nested Aria LabelledBy</Text>
@@ -109,3 +214,31 @@ test('getByLabelText supports nested aria-labelledby', async () => {
109214
const element = screen.getByTestId('text-input');
110215
expect(element).toHaveAccessibleName('Nested Aria LabelledBy');
111216
});
217+
218+
test('not.toHaveAccessibleName() on view with nested aria-labelledby', async () => {
219+
render(
220+
<>
221+
<View nativeID="label">
222+
<Text>Nested Aria LabelledBy</Text>
223+
</View>
224+
<TextInput testID="text-input" aria-labelledby="label" />
225+
</>
226+
);
227+
228+
const element = screen.getByTestId('text-input');
229+
expect(element).not.toHaveAccessibleName('Not Nested Aria LabelledBy');
230+
});
231+
232+
test('not.toHaveAccessibleName() on view with nested aria-labelledby with no text', async () => {
233+
render(
234+
<>
235+
<View nativeID="label">
236+
<View />
237+
</View>
238+
<TextInput testID="text-input" aria-labelledby="label" />
239+
</>
240+
);
241+
242+
const element = screen.getByTestId('text-input');
243+
expect(element).not.toHaveAccessibleName();
244+
});

src/matchers/extend-expect.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ export interface JestNativeMatchers<R> {
1818
toBeVisible(): R;
1919
toContainElement(element: ReactTestInstance | null): R;
2020
toHaveAccessibilityValue(expectedValue: AccessibilityValueMatcher): R;
21+
toHaveAccessibleName(expectedName?: TextMatch, options?: TextMatchOptions): R;
2122
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
2223
toHaveProp(name: string, expectedValue?: unknown): R;
2324
toHaveStyle(style: StyleProp<Style>): R;
2425
toHaveTextContent(expectedText: TextMatch, options?: TextMatchOptions): R;
25-
toHaveAccessibleName(expectedName?: TextMatch, options?: TextMatchOptions): R;
2626
}
2727

2828
// Implicit Jest global `expect`.

src/matchers/extend-expect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import { toBeSelected } from './to-be-selected';
1212
import { toBeVisible } from './to-be-visible';
1313
import { toContainElement } from './to-contain-element';
1414
import { toHaveAccessibilityValue } from './to-have-accessibility-value';
15+
import { toHaveAccessibleName } from './to-have-accessible-name';
1516
import { toHaveDisplayValue } from './to-have-display-value';
1617
import { toHaveProp } from './to-have-prop';
1718
import { toHaveStyle } from './to-have-style';
1819
import { toHaveTextContent } from './to-have-text-content';
19-
import { toHaveAccessibleName } from './to-have-accessible-name';
2020

2121
expect.extend({
2222
toBeOnTheScreen,
@@ -32,9 +32,9 @@ expect.extend({
3232
toBeVisible,
3333
toContainElement,
3434
toHaveAccessibilityValue,
35+
toHaveAccessibleName,
3536
toHaveDisplayValue,
3637
toHaveProp,
3738
toHaveStyle,
3839
toHaveTextContent,
39-
toHaveAccessibleName,
4040
});

src/matchers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export { toBeSelected } from './to-be-selected';
1010
export { toBeVisible } from './to-be-visible';
1111
export { toContainElement } from './to-contain-element';
1212
export { toHaveAccessibilityValue } from './to-have-accessibility-value';
13+
export { toHaveAccessibleName } from './to-have-accessible-name';
1314
export { toHaveDisplayValue } from './to-have-display-value';
1415
export { toHaveProp } from './to-have-prop';
1516
export { toHaveStyle } from './to-have-style';
1617
export { toHaveTextContent } from './to-have-text-content';
17-
export { toHaveAccessibleName } from './to-have-accessible-name';

src/matchers/to-have-accessible-name.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
33
import { matchAccessibleName } from '../helpers/matchers/accessibilityName';
4-
import { getAccessibleName } from '../helpers/accessiblity';
4+
import {
5+
getAccessibilityLabel,
6+
getAccessibilityLabelledBy,
7+
} from '../helpers/accessiblity';
58
import { TextMatch, TextMatchOptions } from '../matches';
9+
import { getTextContent } from '../helpers/text-content';
10+
import { getUnsafeRootElement } from '../helpers/component-tree';
611
import { checkHostElement, formatMessage } from './utils';
712

13+
export function getAccessibleName(
14+
element: ReactTestInstance
15+
): string | undefined {
16+
const labelTextFromLabel = getAccessibilityLabel(element);
17+
const labelTextFromLabelledBy = getAccessibilityLabelledBy(element);
18+
const rootElement = getUnsafeRootElement(element);
19+
20+
const labelledByElement = labelTextFromLabelledBy
21+
? rootElement?.findByProps({
22+
nativeID: labelTextFromLabelledBy,
23+
})
24+
: undefined;
25+
26+
const nameFromLabel = labelTextFromLabelledBy
27+
? labelledByElement && getTextContent(labelledByElement)
28+
: labelTextFromLabel;
29+
30+
return nameFromLabel || getTextContent(element) || undefined;
31+
}
32+
833
export function toHaveAccessibleName(
934
this: jest.MatcherContext,
1035
element: ReactTestInstance,

0 commit comments

Comments
 (0)