diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 141d45ea..227a47a6 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 872a08ae..85dc39aa 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 57defac1..fb9be392 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 58cbc952..26a2b61d 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 bcb6d65d..c052ecc9 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 d013c6b9..26255ada 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 d81367e4..1ab9e05a 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