diff --git a/README.md b/README.md
index 8ba1662..3e9b3ea 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@
- [`toHaveTextContent`](#tohavetextcontent)
- [`toHaveStyle`](#tohavestyle)
- [`toBeVisible`](#tobevisible)
+ - [`toHaveAccessibilityState`](#tohaveaccessibilitystate)
- [Inspiration](#inspiration)
- [Other solutions](#other-solutions)
- [Contributors](#contributors)
@@ -226,7 +227,7 @@ expect(parent).not.toContainElement(grandparent);
toHaveProp(prop: string, value?: any);
```
-Check that an element has a given prop.
+Check that the element has a given prop.
You can optionally check that the attribute has a specific expected value.
@@ -431,10 +432,72 @@ const { getByTestId } = render(
expect(getByTestId('test')).not.toBeVisible();
```
+### `toHaveAccessibilityState`
+
+```ts
+toHaveAccessibilityState(state: {
+ disabled?: boolean;
+ selected?: boolean;
+ checked?: boolean | 'mixed';
+ busy?: boolean;
+ expanded?: boolean;
+});
+```
+
+Check that the element has given accessibility state entries.
+
+This check is based on `accessibilityState` prop but also takes into account the default entries
+which have been found by experimenting with accessibility inspector and screen readers on both iOS
+and Android.
+
+Some state entries behave as if explicit `false` value is the same as not having given state entry,
+so their default value is `false`:
+
+- `disabled`
+- `selected`
+- `busy`
+
+The remaining state entries behave as if explicit `false` value is different than not having given
+state entry, so their default value is `undefined`:
+
+- `checked`
+- `expanded`
+
+This matcher is compatible with `*ByRole` and `*ByA11State` queries from React Native Testing
+Library.
+
+#### Examples
+
+```js
+render();
+
+// Single value match
+expect(screen.getByTestId('view')).toHaveAccessibilityState({ expanded: true });
+expect(screen.getByTestId('view')).toHaveAccessibilityState({ checked: true });
+
+// Can match multiple entries
+expect(screen.getByTestId('view')).toHaveAccessibilityState({ expanded: true, checked: true });
+```
+
+Default values handling:
+
+```js
+render();
+
+// Matching states where default value is `false`
+expect(screen.getByTestId('view')).toHaveAccessibilityState({ disabled: false });
+expect(screen.getByTestId('view')).toHaveAccessibilityState({ selected: false });
+expect(screen.getByTestId('view')).toHaveAccessibilityState({ busy: false });
+
+// Matching states where default value is `undefined`
+expect(screen.getByTestId('view')).not.toHaveAccessibilityState({ checked: false });
+expect(screen.getByTestId('view')).not.toHaveAccessibilityState({ expanded: false });
+```
+
## Inspiration
This library was made to be a companion for
-[RNTL](https://github.com/callstack/react-native-testing-library).
+[React Native Testing Library](https://github.com/callstack/react-native-testing-library).
It was inspired by [jest-dom](https://github.com/gnapse/jest-dom/), the companion library for
[DTL](https://github.com/kentcdodds/dom-testing-library/). We emulated as many of those helpers as
diff --git a/extend-expect.d.ts b/extend-expect.d.ts
index d75154a..17d3c0b 100644
--- a/extend-expect.d.ts
+++ b/extend-expect.d.ts
@@ -1,4 +1,4 @@
-import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
+import type { AccessibilityState, ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
import type { ReactTestInstance } from 'react-test-renderer';
declare global {
@@ -16,6 +16,8 @@ declare global {
/** @deprecated This function has been renamed to `toBeEmptyElement`. */
toBeEmpty(): R;
toBeVisible(): R;
+
+ toHaveAccessibilityState(state: AccessibilityState): R;
}
}
}
diff --git a/src/__tests__/to-have-accessibility-state.tsx b/src/__tests__/to-have-accessibility-state.tsx
new file mode 100644
index 0000000..868c2b0
--- /dev/null
+++ b/src/__tests__/to-have-accessibility-state.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react';
+import { View } from 'react-native';
+import { render } from '@testing-library/react-native';
+
+test('.toHaveAccessibilityState to handle explicit state', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ expect(getByTestId('disabled')).toHaveAccessibilityState({ disabled: true });
+ expect(getByTestId('disabled')).not.toHaveAccessibilityState({ disabled: false });
+ expect(() => expect(getByTestId('disabled')).toHaveAccessibilityState({ disabled: false }))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(element).toHaveAccessibilityState({"disabled": false})
+
+ Expected the element to have accessibility state:
+ {"disabled": false}
+ Received element with implied accessibility state:
+ {"busy": false, "disabled": true, "selected": false}"
+ `);
+
+ expect(getByTestId('selected')).toHaveAccessibilityState({ selected: true });
+ expect(getByTestId('selected')).not.toHaveAccessibilityState({ selected: false });
+ expect(() => expect(getByTestId('selected')).not.toHaveAccessibilityState({ selected: true }))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "expect(element).not.toHaveAccessibilityState({"selected": true})
+
+ Expected the element not to have accessibility state:
+ {"selected": true}
+ Received element with implied accessibility state:
+ {"busy": false, "disabled": false, "selected": true}"
+ `);
+
+ expect(getByTestId('busy')).toHaveAccessibilityState({ busy: true });
+ expect(getByTestId('busy')).not.toHaveAccessibilityState({ busy: false });
+
+ expect(getByTestId('checked-true')).toHaveAccessibilityState({ checked: true });
+ expect(getByTestId('checked-true')).not.toHaveAccessibilityState({ checked: 'mixed' });
+ expect(getByTestId('checked-true')).not.toHaveAccessibilityState({ checked: false });
+
+ expect(getByTestId('checked-mixed')).toHaveAccessibilityState({ checked: 'mixed' });
+ expect(getByTestId('checked-mixed')).not.toHaveAccessibilityState({ checked: true });
+ expect(getByTestId('checked-mixed')).not.toHaveAccessibilityState({ checked: false });
+
+ expect(getByTestId('checked-false')).toHaveAccessibilityState({ checked: false });
+ expect(getByTestId('checked-false')).not.toHaveAccessibilityState({ checked: true });
+ expect(getByTestId('checked-false')).not.toHaveAccessibilityState({ checked: 'mixed' });
+
+ expect(getByTestId('expanded-true')).toHaveAccessibilityState({ expanded: true });
+ expect(getByTestId('expanded-true')).not.toHaveAccessibilityState({ expanded: false });
+
+ expect(getByTestId('expanded-false')).toHaveAccessibilityState({ expanded: false });
+ expect(getByTestId('expanded-false')).not.toHaveAccessibilityState({ expanded: true });
+
+ expect(getByTestId('disabled-selected')).toHaveAccessibilityState({
+ disabled: true,
+ selected: true,
+ });
+ expect(getByTestId('disabled-selected')).not.toHaveAccessibilityState({
+ disabled: false,
+ selected: true,
+ });
+ expect(getByTestId('disabled-selected')).not.toHaveAccessibilityState({
+ disabled: true,
+ selected: false,
+ });
+});
+
+test('.toHaveAccessibilityState to handle implicit state', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('subject')).toHaveAccessibilityState({ disabled: false });
+ expect(getByTestId('subject')).toHaveAccessibilityState({ selected: false });
+ expect(getByTestId('subject')).toHaveAccessibilityState({ busy: false });
+
+ expect(getByTestId('subject')).not.toHaveAccessibilityState({ checked: false });
+ expect(getByTestId('subject')).not.toHaveAccessibilityState({ expanded: false });
+});
diff --git a/src/extend-expect.ts b/src/extend-expect.ts
index 0a0e5be..33f1c36 100644
--- a/src/extend-expect.ts
+++ b/src/extend-expect.ts
@@ -5,6 +5,7 @@ import { toHaveProp } from './to-have-prop';
import { toHaveStyle } from './to-have-style';
import { toHaveTextContent } from './to-have-text-content';
import { toBeVisible } from './to-be-visible';
+import { toHaveAccessibilityState } from './to-have-accessibility-state';
expect.extend({
toBeDisabled,
@@ -16,4 +17,5 @@ expect.extend({
toHaveStyle,
toHaveTextContent,
toBeVisible,
+ toHaveAccessibilityState,
});
diff --git a/src/to-have-accessibility-state.ts b/src/to-have-accessibility-state.ts
new file mode 100644
index 0000000..3392e2f
--- /dev/null
+++ b/src/to-have-accessibility-state.ts
@@ -0,0 +1,71 @@
+import type { AccessibilityState } from 'react-native';
+import type { ReactTestInstance } from 'react-test-renderer';
+import { matcherHint, stringify } from 'jest-matcher-utils';
+import { checkReactElement, getMessage } from './utils';
+
+export function toHaveAccessibilityState(
+ this: jest.MatcherContext,
+ element: ReactTestInstance,
+ expectedState: AccessibilityState,
+) {
+ checkReactElement(element, toHaveAccessibilityState, this);
+
+ const impliedState = getAccessibilityState(element);
+ return {
+ pass: matchAccessibilityState(element, expectedState),
+ message: () => {
+ const matcher = matcherHint(
+ `${this.isNot ? '.not' : ''}.toHaveAccessibilityState`,
+ 'element',
+ stringify(expectedState),
+ );
+ return getMessage(
+ matcher,
+ `Expected the element ${this.isNot ? 'not to' : 'to'} have accessibility state`,
+ stringify(expectedState),
+ 'Received element with implied accessibility state',
+ stringify(impliedState),
+ );
+ },
+ };
+}
+
+/**
+ * Default accessibility state values based on experiments using accessibility
+ * inspector/screen reader on iOS and Android.
+ *
+ * @see https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State
+ */
+const defaultState: AccessibilityState = {
+ disabled: false,
+ selected: false,
+ busy: false,
+};
+
+const getAccessibilityState = (element: ReactTestInstance) => {
+ return {
+ ...defaultState,
+ ...element.props.accessibilityState,
+ };
+};
+
+const accessibilityStateKeys: (keyof AccessibilityState)[] = [
+ 'disabled',
+ 'selected',
+ 'checked',
+ 'busy',
+ 'expanded',
+];
+
+function matchAccessibilityState(element: ReactTestInstance, matcher: AccessibilityState) {
+ const state = getAccessibilityState(element);
+ return accessibilityStateKeys.every((key) => matchStateEntry(state, matcher, key));
+}
+
+function matchStateEntry(
+ state: AccessibilityState,
+ matcher: AccessibilityState,
+ key: keyof AccessibilityState,
+) {
+ return matcher[key] === undefined || matcher[key] === state[key];
+}