Skip to content

Commit 28c8729

Browse files
Implement toHaveAccessibilityValue matcher (#1496)
* feat: add toHaveAccessibilityValue matcher * refactor: clenup * refactor: clean up tests * refactor: self review --------- Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
1 parent 798100b commit 28c8729

7 files changed

+232
-21
lines changed

src/helpers/format-default.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { StyleSheet, ViewStyle } from 'react-native';
2+
import { removeUndefinedKeys } from './object';
23

34
const propsToDisplay = [
45
'accessible',
@@ -64,27 +65,6 @@ export function defaultMapProps(
6465
return result;
6566
}
6667

67-
function isObject(value: unknown): value is Record<string, unknown> {
68-
return typeof value === 'object' && value !== null && !Array.isArray(value);
69-
}
70-
71-
function removeUndefinedKeys(prop: unknown) {
72-
if (!isObject(prop)) {
73-
return prop;
74-
}
75-
76-
let hasKeys = false;
77-
const result: Record<string, unknown> = {};
78-
Object.entries(prop).forEach(([key, value]) => {
79-
if (value !== undefined) {
80-
result[key] = value;
81-
hasKeys = true;
82-
}
83-
});
84-
85-
return hasKeys ? result : undefined;
86-
}
87-
8868
function extractStyle(style: ViewStyle | undefined) {
8969
if (style == null) {
9070
return undefined;

src/helpers/object.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,24 @@ export function pick<T extends {}>(object: T, keys: (keyof T)[]): Partial<T> {
88

99
return result;
1010
}
11+
12+
function isObject(value: unknown): value is Record<string, unknown> {
13+
return value !== null && typeof value === 'object' && !Array.isArray(value);
14+
}
15+
16+
export function removeUndefinedKeys(prop: unknown) {
17+
if (!isObject(prop)) {
18+
return prop;
19+
}
20+
21+
let hasKeys = false;
22+
const result: Record<string, unknown> = {};
23+
Object.entries(prop).forEach(([key, value]) => {
24+
if (value !== undefined) {
25+
result[key] = value;
26+
hasKeys = true;
27+
}
28+
});
29+
30+
return hasKeys ? result : undefined;
31+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import * as React from 'react';
2+
import { View } from 'react-native';
3+
import { render, screen } from '../..';
4+
import '../extend-expect';
5+
6+
describe('toHaveAccessibilityValue', () => {
7+
it('supports "accessibilityValue.min"', () => {
8+
render(<View accessibilityValue={{ min: 0 }} />);
9+
expect(screen.root).toHaveAccessibilityValue({ min: 0 });
10+
expect(screen.root).not.toHaveAccessibilityValue({ min: 1 });
11+
});
12+
13+
it('supports "accessibilityValue.max"', () => {
14+
render(<View accessibilityValue={{ max: 100 }} />);
15+
expect(screen.root).toHaveAccessibilityValue({ max: 100 });
16+
expect(screen.root).not.toHaveAccessibilityValue({ max: 99 });
17+
});
18+
19+
it('supports "accessibilityValue.now"', () => {
20+
render(<View accessibilityValue={{ now: 33 }} />);
21+
expect(screen.root).toHaveAccessibilityValue({ now: 33 });
22+
expect(screen.root).not.toHaveAccessibilityValue({ now: 34 });
23+
});
24+
25+
it('supports "accessibilityValue.text"', () => {
26+
render(<View testID="view" accessibilityValue={{ text: 'Hello' }} />);
27+
expect(screen.root).toHaveAccessibilityValue({ text: 'Hello' });
28+
expect(screen.root).toHaveAccessibilityValue({ text: /He/ });
29+
expect(screen.root).not.toHaveAccessibilityValue({ text: 'Hi' });
30+
expect(screen.root).not.toHaveAccessibilityValue({ text: /Hi/ });
31+
});
32+
33+
it('supports "aria-valuemin"', () => {
34+
render(<View testID="view" aria-valuemin={0} />);
35+
expect(screen.root).toHaveAccessibilityValue({ min: 0 });
36+
expect(screen.root).not.toHaveAccessibilityValue({ min: 1 });
37+
});
38+
39+
it('supports "aria-valuemax"', () => {
40+
render(<View testID="view" aria-valuemax={100} />);
41+
expect(screen.root).toHaveAccessibilityValue({ max: 100 });
42+
expect(screen.root).not.toHaveAccessibilityValue({ max: 99 });
43+
});
44+
45+
it('supports "aria-valuenow"', () => {
46+
render(<View testID="view" aria-valuenow={33} />);
47+
expect(screen.root).toHaveAccessibilityValue({ now: 33 });
48+
expect(screen.root).not.toHaveAccessibilityValue({ now: 34 });
49+
});
50+
51+
it('supports "aria-valuetext"', () => {
52+
render(<View testID="view" aria-valuetext="Hello" />);
53+
expect(screen.root).toHaveAccessibilityValue({ text: 'Hello' });
54+
expect(screen.root).toHaveAccessibilityValue({ text: /He/ });
55+
expect(screen.root).not.toHaveAccessibilityValue({ text: 'Hi' });
56+
expect(screen.root).not.toHaveAccessibilityValue({ text: /Hi/ });
57+
});
58+
59+
it('supports multi-argument matching', () => {
60+
render(
61+
<View accessibilityValue={{ min: 1, max: 10, now: 5, text: '5/10' }} />
62+
);
63+
64+
expect(screen.root).toHaveAccessibilityValue({ now: 5 });
65+
expect(screen.root).toHaveAccessibilityValue({ now: 5, min: 1 });
66+
expect(screen.root).toHaveAccessibilityValue({ now: 5, max: 10 });
67+
expect(screen.root).toHaveAccessibilityValue({ now: 5, min: 1, max: 10 });
68+
expect(screen.root).toHaveAccessibilityValue({ text: '5/10' });
69+
expect(screen.root).toHaveAccessibilityValue({ now: 5, text: '5/10' });
70+
expect(screen.root).toHaveAccessibilityValue({
71+
now: 5,
72+
min: 1,
73+
max: 10,
74+
text: '5/10',
75+
});
76+
77+
expect(screen.root).not.toHaveAccessibilityValue({ now: 6 });
78+
expect(screen.root).not.toHaveAccessibilityValue({ now: 5, min: 0 });
79+
expect(screen.root).not.toHaveAccessibilityValue({ now: 5, max: 9 });
80+
expect(screen.root).not.toHaveAccessibilityValue({
81+
now: 5,
82+
min: 1,
83+
max: 10,
84+
text: '5 of 10',
85+
});
86+
});
87+
88+
it('gives precedence to ARIA values', () => {
89+
render(
90+
<View
91+
testID="view"
92+
aria-valuemin={0}
93+
aria-valuemax={100}
94+
aria-valuenow={33}
95+
aria-valuetext="Hello"
96+
accessibilityValue={{ min: 10, max: 90, now: 30, text: 'Hi' }}
97+
/>
98+
);
99+
100+
expect(screen.root).toHaveAccessibilityValue({ min: 0 });
101+
expect(screen.root).toHaveAccessibilityValue({ max: 100 });
102+
expect(screen.root).toHaveAccessibilityValue({ now: 33 });
103+
expect(screen.root).toHaveAccessibilityValue({ text: 'Hello' });
104+
105+
expect(screen.root).not.toHaveAccessibilityValue({ min: 10 });
106+
expect(screen.root).not.toHaveAccessibilityValue({ max: 90 });
107+
expect(screen.root).not.toHaveAccessibilityValue({ now: 30 });
108+
expect(screen.root).not.toHaveAccessibilityValue({ text: 'Hi' });
109+
});
110+
111+
it('shows errors in expected format', () => {
112+
render(
113+
<View
114+
testID="view"
115+
aria-valuemin={0}
116+
aria-valuemax={100}
117+
aria-valuenow={33}
118+
aria-valuetext="Hello"
119+
/>
120+
);
121+
122+
expect(() => expect(screen.root).toHaveAccessibilityValue({ min: 10 }))
123+
.toThrowErrorMatchingInlineSnapshot(`
124+
"expect(element).toHaveAccessibilityValue({"min": 10})
125+
126+
Expected the element to have accessibility value:
127+
{"min": 10}
128+
Received element with accessibility value:
129+
{"max": 100, "min": 0, "now": 33, "text": "Hello"}"
130+
`);
131+
132+
expect(() => expect(screen.root).not.toHaveAccessibilityValue({ min: 0 }))
133+
.toThrowErrorMatchingInlineSnapshot(`
134+
"expect(element).not.toHaveAccessibilityValue({"min": 0})
135+
136+
Expected the element not to have accessibility value:
137+
{"min": 0}
138+
Received element with accessibility value:
139+
{"max": 100, "min": 0, "now": 33, "text": "Hello"}"
140+
`);
141+
});
142+
143+
it('shows errors in expected format with partial value', () => {
144+
render(<View testID="view" aria-valuenow={33} aria-valuetext="Hello" />);
145+
146+
expect(() => expect(screen.root).toHaveAccessibilityValue({ min: 30 }))
147+
.toThrowErrorMatchingInlineSnapshot(`
148+
"expect(element).toHaveAccessibilityValue({"min": 30})
149+
150+
Expected the element to have accessibility value:
151+
{"min": 30}
152+
Received element with accessibility value:
153+
{"now": 33, "text": "Hello"}"
154+
`);
155+
156+
expect(() => expect(screen.root).not.toHaveAccessibilityValue({ now: 33 }))
157+
.toThrowErrorMatchingInlineSnapshot(`
158+
"expect(element).not.toHaveAccessibilityValue({"now": 33})
159+
160+
Expected the element not to have accessibility value:
161+
{"now": 33}
162+
Received element with accessibility value:
163+
{"now": 33, "text": "Hello"}"
164+
`);
165+
});
166+
});

src/matchers/extend-expect.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { StyleProp } from 'react-native';
22
import type { ReactTestInstance } from 'react-test-renderer';
33
import type { TextMatch, TextMatchOptions } from '../matches';
4+
import type { AccessibilityValueMatcher } from '../helpers/matchers/accessibilityValue';
45
import type { Style } from './to-have-style';
56

67
export interface JestNativeMatchers<R> {
@@ -16,6 +17,7 @@ export interface JestNativeMatchers<R> {
1617
toBeSelected(): R;
1718
toBeVisible(): R;
1819
toContainElement(element: ReactTestInstance | null): R;
20+
toHaveAccessibilityValue(expectedValue: AccessibilityValueMatcher): R;
1921
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
2022
toHaveProp(name: string, expectedValue?: unknown): R;
2123
toHaveStyle(style: StyleProp<Style>): R;

src/matchers/extend-expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { toBePartiallyChecked } from './to-be-partially-checked';
1111
import { toBeSelected } from './to-be-selected';
1212
import { toBeVisible } from './to-be-visible';
1313
import { toContainElement } from './to-contain-element';
14+
import { toHaveAccessibilityValue } from './to-have-accessibility-value';
1415
import { toHaveDisplayValue } from './to-have-display-value';
1516
import { toHaveProp } from './to-have-prop';
1617
import { toHaveStyle } from './to-have-style';
@@ -29,6 +30,7 @@ expect.extend({
2930
toBeSelected,
3031
toBeVisible,
3132
toContainElement,
33+
toHaveAccessibilityValue,
3234
toHaveDisplayValue,
3335
toHaveProp,
3436
toHaveStyle,

src/matchers/index.tsx renamed to src/matchers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { toBePartiallyChecked } from './to-be-partially-checked';
99
export { toBeSelected } from './to-be-selected';
1010
export { toBeVisible } from './to-be-visible';
1111
export { toContainElement } from './to-contain-element';
12+
export { toHaveAccessibilityValue } from './to-have-accessibility-value';
1213
export { toHaveDisplayValue } from './to-have-display-value';
1314
export { toHaveProp } from './to-have-prop';
1415
export { toHaveStyle } from './to-have-style';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { ReactTestInstance } from 'react-test-renderer';
2+
import { matcherHint, stringify } from 'jest-matcher-utils';
3+
import { getAccessibilityValue } from '../helpers/accessiblity';
4+
import {
5+
AccessibilityValueMatcher,
6+
matchAccessibilityValue,
7+
} from '../helpers/matchers/accessibilityValue';
8+
import { removeUndefinedKeys } from '../helpers/object';
9+
import { checkHostElement, formatMessage } from './utils';
10+
11+
export function toHaveAccessibilityValue(
12+
this: jest.MatcherContext,
13+
element: ReactTestInstance,
14+
expectedValue: AccessibilityValueMatcher
15+
) {
16+
checkHostElement(element, toHaveAccessibilityValue, this);
17+
18+
const receivedValue = getAccessibilityValue(element);
19+
20+
return {
21+
pass: matchAccessibilityValue(element, expectedValue),
22+
message: () => {
23+
const matcher = matcherHint(
24+
`${this.isNot ? '.not' : ''}.toHaveAccessibilityValue`,
25+
'element',
26+
stringify(expectedValue)
27+
);
28+
return formatMessage(
29+
matcher,
30+
`Expected the element ${
31+
this.isNot ? 'not to' : 'to'
32+
} have accessibility value`,
33+
stringify(expectedValue),
34+
'Received element with accessibility value',
35+
stringify(removeUndefinedKeys(receivedValue))
36+
);
37+
},
38+
};
39+
}

0 commit comments

Comments
 (0)