diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 141d45ead..227a47a65 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -7,7 +7,12 @@ import { } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; import { getHostSiblings, getUnsafeRootElement } from './component-tree'; -import { getHostComponentNames, isHostText, isHostTextInput } from './host-component-names'; +import { + getHostComponentNames, + isHostSwitch, + isHostText, + isHostTextInput, +} from './host-component-names'; import { getTextContent } from './text-content'; import { isTextInputEditable } from './text-input'; @@ -154,12 +159,17 @@ export function computeAriaBusy({ props }: ReactTestInstance): boolean { // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#checked-state export function computeAriaChecked(element: ReactTestInstance): AccessibilityState['checked'] { + const { props } = element; + + if (isHostSwitch(element)) { + return props.value; + } + const role = getRole(element); - if (role !== 'checkbox' && role !== 'radio') { + if (!rolesSupportingCheckedState[role]) { return undefined; } - const props = element.props; return props['aria-checked'] ?? props.accessibilityState?.checked; } @@ -217,3 +227,11 @@ export function computeAccessibleName(element: ReactTestInstance): string | unde return getTextContent(element); } + +type RoleSupportMap = Partial>; + +export const rolesSupportingCheckedState: RoleSupportMap = { + checkbox: true, + radio: true, + switch: true, +}; diff --git a/src/matchers/__tests__/to-be-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx index 872a08ae8..85dc39aa7 100644 --- a/src/matchers/__tests__/to-be-checked.test.tsx +++ b/src/matchers/__tests__/to-be-checked.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { type AccessibilityRole, View } from 'react-native'; +import { type AccessibilityRole, Switch, View } from 'react-native'; import render from '../../render'; import { screen } from '../../screen'; import '../extend-expect'; @@ -30,7 +30,56 @@ function renderViewsWithRole(role: AccessibilityRole) { ); } -test('toBeCheck() with checkbox role', () => { +test('toBeCheck() with Switch', () => { + render( + <> + + + + , + ); + + const checked = screen.getByTestId('checked'); + const unchecked = screen.getByTestId('unchecked'); + const defaultView = screen.getByTestId('default'); + + expect(checked).toBeChecked(); + expect(unchecked).not.toBeChecked(); + expect(defaultView).not.toBeChecked(); + + expect(() => expect(checked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeChecked() + + Received element is checked: + " + `); + expect(() => expect(unchecked).toBeChecked()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeChecked() + + Received element is not checked: + " + `); + expect(() => expect(defaultView).toBeChecked()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeChecked() + + Received element is not checked: + " + `); +}); + +test('toBeCheck() with "checkbox" role', () => { renderViewsWithRole('checkbox'); const checked = screen.getByTestId('checkbox-checked'); @@ -100,7 +149,7 @@ test('toBeCheck() with checkbox role', () => { `); }); -test('toBeCheck() with radio role', () => { +test('toBeCheck() with "radio" role', () => { renderViewsWithRole('radio'); const checked = screen.getByTestId('radio-checked'); @@ -153,6 +202,59 @@ test('toBeCheck() with radio role', () => { `); }); +test('toBeCheck() with "switch" role', () => { + renderViewsWithRole('switch'); + + const checked = screen.getByTestId('switch-checked'); + const unchecked = screen.getByTestId('switch-unchecked'); + const defaultView = screen.getByTestId('switch-default'); + + expect(checked).toBeChecked(); + expect(unchecked).not.toBeChecked(); + expect(defaultView).not.toBeChecked(); + + expect(() => expect(checked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toBeChecked() + + Received element is checked: + " + `); + expect(() => expect(unchecked).toBeChecked()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeChecked() + + Received element is not checked: + " + `); + expect(() => expect(defaultView).toBeChecked()).toThrowErrorMatchingInlineSnapshot(` + "expect(element).toBeChecked() + + Received element is not checked: + " + `); +}); + test('throws error for invalid role', () => { renderViewsWithRole('adjustable'); @@ -160,10 +262,10 @@ test('throws error for invalid role', () => { const unchecked = screen.getByTestId('adjustable-unchecked'); expect(() => expect(checked).toBeChecked()).toThrowErrorMatchingInlineSnapshot( - `"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`, + `"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`, ); expect(() => expect(unchecked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot( - `"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`, + `"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`, ); }); @@ -172,6 +274,6 @@ test('throws error for non-accessibility element', () => { const view = screen.getByTestId('test'); expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot( - `"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`, + `"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`, ); }); diff --git a/src/matchers/to-be-checked.tsx b/src/matchers/to-be-checked.tsx index 57defac15..fb9be3927 100644 --- a/src/matchers/to-be-checked.tsx +++ b/src/matchers/to-be-checked.tsx @@ -1,15 +1,21 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; -import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility'; +import { + computeAriaChecked, + getRole, + isAccessibilityElement, + rolesSupportingCheckedState, +} from '../helpers/accessibility'; import { ErrorWithStack } from '../helpers/errors'; +import { isHostSwitch } from '../helpers/host-component-names'; import { checkHostElement, formatElement } from './utils'; export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstance) { checkHostElement(element, toBeChecked, this); - if (!hasValidAccessibilityRole(element)) { + if (!isHostSwitch(element) && !isSupportedAccessibilityElement(element)) { throw new ErrorWithStack( - `toBeChecked() works only on accessibility elements with "checkbox" or "radio" role.`, + `toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role.`, toBeChecked, ); } @@ -28,11 +34,11 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc }; } -function hasValidAccessibilityRole(element: ReactTestInstance) { +function isSupportedAccessibilityElement(element: ReactTestInstance) { if (!isAccessibilityElement(element)) { return false; } const role = getRole(element); - return role === 'checkbox' || role === 'radio'; + return rolesSupportingCheckedState[role]; } diff --git a/src/queries/__tests__/accessibility-state.test.tsx b/src/queries/__tests__/accessibility-state.test.tsx index 58cbc9525..26a2b61de 100644 --- a/src/queries/__tests__/accessibility-state.test.tsx +++ b/src/queries/__tests__/accessibility-state.test.tsx @@ -477,14 +477,14 @@ describe('aria-checked prop', () => { }); test('supports aria-checked="mixed" prop', () => { - render(); + render(); expect(screen.getByAccessibilityState({ checked: 'mixed' })).toBeTruthy(); expect(screen.queryByAccessibilityState({ checked: true })).toBeNull(); expect(screen.queryByAccessibilityState({ checked: false })).toBeNull(); }); test('supports default aria-checked prop', () => { - render(); + render(); expect(screen.getByAccessibilityState({})).toBeTruthy(); expect(screen.queryByAccessibilityState({ checked: true })).toBeNull(); expect(screen.queryByAccessibilityState({ checked: false })).toBeNull(); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index bcb6d65d1..c052ecc9a 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -7,6 +7,7 @@ import { TouchableOpacity, TouchableWithoutFeedback, View, + Switch, } from 'react-native'; import { render, screen } from '../..'; @@ -426,7 +427,7 @@ describe('supports accessibility states', () => { expect(screen.queryByRole('checkbox', { checked: 'mixed' })).toBe(null); }); - it('returns `mixed` checkboxes', () => { + test('returns `mixed` checkboxes', () => { render( , ); @@ -508,6 +509,14 @@ describe('supports accessibility states', () => { expect(screen.queryByRole('checkbox', { checked: false })).toBe(null); }); + test('supports "Switch" component', () => { + render(); + + expect(screen.getByRole('switch', { checked: true })).toBeTruthy(); + expect(screen.queryByRole('switch', { checked: false })).toBe(null); + expect(screen.queryByRole('switch', { checked: 'mixed' })).toBe(null); + }); + test('supports aria-checked={true} prop', () => { render(); expect(screen.getByRole('checkbox', { checked: true })).toBeTruthy(); diff --git a/website/docs/12.x/docs/api/jest-matchers.mdx b/website/docs/12.x/docs/api/jest-matchers.mdx index d013c6b97..26255ada9 100644 --- a/website/docs/12.x/docs/api/jest-matchers.mdx +++ b/website/docs/12.x/docs/api/jest-matchers.mdx @@ -140,8 +140,8 @@ These allow you to assert whether the given element is checked or partially chec :::note -- `toBeChecked()` matcher works only on elements with the `checkbox` or `radio` role. -- `toBePartiallyChecked()` matcher works only on elements with the `checkbox` role. +- `toBeChecked()` matcher works only on `Switch` host elements and accessibility elements with `checkbox`, `radio` or `switch` role. +- `toBePartiallyChecked()` matcher works only on elements with `checkbox` role. ::: diff --git a/website/docs/12.x/docs/migration/jest-matchers.mdx b/website/docs/12.x/docs/migration/jest-matchers.mdx index d81367e46..1ab9e05aa 100644 --- a/website/docs/12.x/docs/migration/jest-matchers.mdx +++ b/website/docs/12.x/docs/migration/jest-matchers.mdx @@ -71,5 +71,5 @@ New [`toHaveAccessibleName()`](docs/api/jest-matchers#tohaveaccessiblename) has You should be aware of the following details: - [`toBeEnabled()` / `toBeDisabled()`](docs/api/jest-matchers#tobeenabled) matchers also check the disabled state for the element's ancestors and not only the element itself. This is the same as in legacy Jest Native matchers of the same name but differs from the removed `toHaveAccessibilityState()` matcher. -- [`toBeChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with a `checkbox` or `radio` role +- [`toBeChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with a `checkbox`, `radio` and 'switch' role - [`toBePartiallyChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with `checkbox` role