Skip to content

Commit

Permalink
Implement toHaveAccessibilityValue matcher (#1496)
Browse files Browse the repository at this point in the history
* feat: add toHaveAccessibilityValue matcher

* refactor: clenup

* refactor: clean up tests

* refactor: self review

---------

Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
  • Loading branch information
tarunrajput and mdjastrzebski authored Oct 25, 2023
1 parent 798100b commit 28c8729
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 21 deletions.
22 changes: 1 addition & 21 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StyleSheet, ViewStyle } from 'react-native';
import { removeUndefinedKeys } from './object';

const propsToDisplay = [
'accessible',
Expand Down Expand Up @@ -64,27 +65,6 @@ export function defaultMapProps(
return result;
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function removeUndefinedKeys(prop: unknown) {
if (!isObject(prop)) {
return prop;
}

let hasKeys = false;
const result: Record<string, unknown> = {};
Object.entries(prop).forEach(([key, value]) => {
if (value !== undefined) {
result[key] = value;
hasKeys = true;
}
});

return hasKeys ? result : undefined;
}

function extractStyle(style: ViewStyle | undefined) {
if (style == null) {
return undefined;
Expand Down
21 changes: 21 additions & 0 deletions src/helpers/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,24 @@ export function pick<T extends {}>(object: T, keys: (keyof T)[]): Partial<T> {

return result;
}

function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

export function removeUndefinedKeys(prop: unknown) {
if (!isObject(prop)) {
return prop;
}

let hasKeys = false;
const result: Record<string, unknown> = {};
Object.entries(prop).forEach(([key, value]) => {
if (value !== undefined) {
result[key] = value;
hasKeys = true;
}
});

return hasKeys ? result : undefined;
}
166 changes: 166 additions & 0 deletions src/matchers/__tests__/to-have-accessibility-value.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as React from 'react';
import { View } from 'react-native';
import { render, screen } from '../..';
import '../extend-expect';

describe('toHaveAccessibilityValue', () => {
it('supports "accessibilityValue.min"', () => {
render(<View accessibilityValue={{ min: 0 }} />);
expect(screen.root).toHaveAccessibilityValue({ min: 0 });
expect(screen.root).not.toHaveAccessibilityValue({ min: 1 });
});

it('supports "accessibilityValue.max"', () => {
render(<View accessibilityValue={{ max: 100 }} />);
expect(screen.root).toHaveAccessibilityValue({ max: 100 });
expect(screen.root).not.toHaveAccessibilityValue({ max: 99 });
});

it('supports "accessibilityValue.now"', () => {
render(<View accessibilityValue={{ now: 33 }} />);
expect(screen.root).toHaveAccessibilityValue({ now: 33 });
expect(screen.root).not.toHaveAccessibilityValue({ now: 34 });
});

it('supports "accessibilityValue.text"', () => {
render(<View testID="view" accessibilityValue={{ text: 'Hello' }} />);
expect(screen.root).toHaveAccessibilityValue({ text: 'Hello' });
expect(screen.root).toHaveAccessibilityValue({ text: /He/ });
expect(screen.root).not.toHaveAccessibilityValue({ text: 'Hi' });
expect(screen.root).not.toHaveAccessibilityValue({ text: /Hi/ });
});

it('supports "aria-valuemin"', () => {
render(<View testID="view" aria-valuemin={0} />);
expect(screen.root).toHaveAccessibilityValue({ min: 0 });
expect(screen.root).not.toHaveAccessibilityValue({ min: 1 });
});

it('supports "aria-valuemax"', () => {
render(<View testID="view" aria-valuemax={100} />);
expect(screen.root).toHaveAccessibilityValue({ max: 100 });
expect(screen.root).not.toHaveAccessibilityValue({ max: 99 });
});

it('supports "aria-valuenow"', () => {
render(<View testID="view" aria-valuenow={33} />);
expect(screen.root).toHaveAccessibilityValue({ now: 33 });
expect(screen.root).not.toHaveAccessibilityValue({ now: 34 });
});

it('supports "aria-valuetext"', () => {
render(<View testID="view" aria-valuetext="Hello" />);
expect(screen.root).toHaveAccessibilityValue({ text: 'Hello' });
expect(screen.root).toHaveAccessibilityValue({ text: /He/ });
expect(screen.root).not.toHaveAccessibilityValue({ text: 'Hi' });
expect(screen.root).not.toHaveAccessibilityValue({ text: /Hi/ });
});

it('supports multi-argument matching', () => {
render(
<View accessibilityValue={{ min: 1, max: 10, now: 5, text: '5/10' }} />
);

expect(screen.root).toHaveAccessibilityValue({ now: 5 });
expect(screen.root).toHaveAccessibilityValue({ now: 5, min: 1 });
expect(screen.root).toHaveAccessibilityValue({ now: 5, max: 10 });
expect(screen.root).toHaveAccessibilityValue({ now: 5, min: 1, max: 10 });
expect(screen.root).toHaveAccessibilityValue({ text: '5/10' });
expect(screen.root).toHaveAccessibilityValue({ now: 5, text: '5/10' });
expect(screen.root).toHaveAccessibilityValue({
now: 5,
min: 1,
max: 10,
text: '5/10',
});

expect(screen.root).not.toHaveAccessibilityValue({ now: 6 });
expect(screen.root).not.toHaveAccessibilityValue({ now: 5, min: 0 });
expect(screen.root).not.toHaveAccessibilityValue({ now: 5, max: 9 });
expect(screen.root).not.toHaveAccessibilityValue({
now: 5,
min: 1,
max: 10,
text: '5 of 10',
});
});

it('gives precedence to ARIA values', () => {
render(
<View
testID="view"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={33}
aria-valuetext="Hello"
accessibilityValue={{ min: 10, max: 90, now: 30, text: 'Hi' }}
/>
);

expect(screen.root).toHaveAccessibilityValue({ min: 0 });
expect(screen.root).toHaveAccessibilityValue({ max: 100 });
expect(screen.root).toHaveAccessibilityValue({ now: 33 });
expect(screen.root).toHaveAccessibilityValue({ text: 'Hello' });

expect(screen.root).not.toHaveAccessibilityValue({ min: 10 });
expect(screen.root).not.toHaveAccessibilityValue({ max: 90 });
expect(screen.root).not.toHaveAccessibilityValue({ now: 30 });
expect(screen.root).not.toHaveAccessibilityValue({ text: 'Hi' });
});

it('shows errors in expected format', () => {
render(
<View
testID="view"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={33}
aria-valuetext="Hello"
/>
);

expect(() => expect(screen.root).toHaveAccessibilityValue({ min: 10 }))
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toHaveAccessibilityValue({"min": 10})
Expected the element to have accessibility value:
{"min": 10}
Received element with accessibility value:
{"max": 100, "min": 0, "now": 33, "text": "Hello"}"
`);

expect(() => expect(screen.root).not.toHaveAccessibilityValue({ min: 0 }))
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toHaveAccessibilityValue({"min": 0})
Expected the element not to have accessibility value:
{"min": 0}
Received element with accessibility value:
{"max": 100, "min": 0, "now": 33, "text": "Hello"}"
`);
});

it('shows errors in expected format with partial value', () => {
render(<View testID="view" aria-valuenow={33} aria-valuetext="Hello" />);

expect(() => expect(screen.root).toHaveAccessibilityValue({ min: 30 }))
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).toHaveAccessibilityValue({"min": 30})
Expected the element to have accessibility value:
{"min": 30}
Received element with accessibility value:
{"now": 33, "text": "Hello"}"
`);

expect(() => expect(screen.root).not.toHaveAccessibilityValue({ now: 33 }))
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toHaveAccessibilityValue({"now": 33})
Expected the element not to have accessibility value:
{"now": 33}
Received element with accessibility value:
{"now": 33, "text": "Hello"}"
`);
});
});
2 changes: 2 additions & 0 deletions src/matchers/extend-expect.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { StyleProp } from 'react-native';
import type { ReactTestInstance } from 'react-test-renderer';
import type { TextMatch, TextMatchOptions } from '../matches';
import type { AccessibilityValueMatcher } from '../helpers/matchers/accessibilityValue';
import type { Style } from './to-have-style';

export interface JestNativeMatchers<R> {
Expand All @@ -16,6 +17,7 @@ export interface JestNativeMatchers<R> {
toBeSelected(): R;
toBeVisible(): R;
toContainElement(element: ReactTestInstance | null): R;
toHaveAccessibilityValue(expectedValue: AccessibilityValueMatcher): R;
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
toHaveProp(name: string, expectedValue?: unknown): R;
toHaveStyle(style: StyleProp<Style>): R;
Expand Down
2 changes: 2 additions & 0 deletions src/matchers/extend-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { toBePartiallyChecked } from './to-be-partially-checked';
import { toBeSelected } from './to-be-selected';
import { toBeVisible } from './to-be-visible';
import { toContainElement } from './to-contain-element';
import { toHaveAccessibilityValue } from './to-have-accessibility-value';
import { toHaveDisplayValue } from './to-have-display-value';
import { toHaveProp } from './to-have-prop';
import { toHaveStyle } from './to-have-style';
Expand All @@ -29,6 +30,7 @@ expect.extend({
toBeSelected,
toBeVisible,
toContainElement,
toHaveAccessibilityValue,
toHaveDisplayValue,
toHaveProp,
toHaveStyle,
Expand Down
1 change: 1 addition & 0 deletions src/matchers/index.tsx → src/matchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { toBePartiallyChecked } from './to-be-partially-checked';
export { toBeSelected } from './to-be-selected';
export { toBeVisible } from './to-be-visible';
export { toContainElement } from './to-contain-element';
export { toHaveAccessibilityValue } from './to-have-accessibility-value';
export { toHaveDisplayValue } from './to-have-display-value';
export { toHaveProp } from './to-have-prop';
export { toHaveStyle } from './to-have-style';
Expand Down
39 changes: 39 additions & 0 deletions src/matchers/to-have-accessibility-value.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, stringify } from 'jest-matcher-utils';
import { getAccessibilityValue } from '../helpers/accessiblity';
import {
AccessibilityValueMatcher,
matchAccessibilityValue,
} from '../helpers/matchers/accessibilityValue';
import { removeUndefinedKeys } from '../helpers/object';
import { checkHostElement, formatMessage } from './utils';

export function toHaveAccessibilityValue(
this: jest.MatcherContext,
element: ReactTestInstance,
expectedValue: AccessibilityValueMatcher
) {
checkHostElement(element, toHaveAccessibilityValue, this);

const receivedValue = getAccessibilityValue(element);

return {
pass: matchAccessibilityValue(element, expectedValue),
message: () => {
const matcher = matcherHint(
`${this.isNot ? '.not' : ''}.toHaveAccessibilityValue`,
'element',
stringify(expectedValue)
);
return formatMessage(
matcher,
`Expected the element ${
this.isNot ? 'not to' : 'to'
} have accessibility value`,
stringify(expectedValue),
'Received element with accessibility value',
stringify(removeUndefinedKeys(receivedValue))
);
},
};
}

0 comments on commit 28c8729

Please sign in to comment.