From cdb149283de8fba9c65b4dfde26fa8578b34ef1c Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 2 Sep 2024 21:46:14 +0200 Subject: [PATCH 1/2] feat: implement support for Switch toBeChecked refactor: tweaks refactor: update tests chore: add Switch test chore: more tests refactor: self code review --- src/helpers/accessibility.ts | 11 +++- src/matchers/__tests__/to-be-checked.test.tsx | 57 +++++++++++++++++-- src/matchers/to-be-checked.tsx | 7 ++- .../__tests__/accessibility-state.test.tsx | 4 +- src/queries/__tests__/role.test.tsx | 11 +++- 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 141d45ead..bdc27daad 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,6 +159,10 @@ 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'] { + if (isHostSwitch(element)) { + return element.props.value; + } + const role = getRole(element); if (role !== 'checkbox' && role !== 'radio') { return undefined; diff --git a/src/matchers/__tests__/to-be-checked.test.tsx b/src/matchers/__tests__/to-be-checked.test.tsx index 872a08ae8..216919de9 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,6 +30,55 @@ function renderViewsWithRole(role: AccessibilityRole) { ); } +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'); @@ -160,10 +209,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 "Switch" elements or accessibility elements with "checkbox" or "radio" role."`, ); expect(() => expect(unchecked).not.toBeChecked()).toThrowErrorMatchingInlineSnapshot( - `"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`, + `"toBeChecked() works only on "Switch" elements or accessibility elements with "checkbox" or "radio" role."`, ); }); @@ -172,6 +221,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 "Switch" elements or accessibility elements with "checkbox" or "radio" role."`, ); }); diff --git a/src/matchers/to-be-checked.tsx b/src/matchers/to-be-checked.tsx index 57defac15..83edf6f02 100644 --- a/src/matchers/to-be-checked.tsx +++ b/src/matchers/to-be-checked.tsx @@ -2,14 +2,15 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint } from 'jest-matcher-utils'; import { computeAriaChecked, getRole, isAccessibilityElement } 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 "Switch" elements or accessibility elements with "checkbox" or "radio" role.`, toBeChecked, ); } @@ -28,7 +29,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc }; } -function hasValidAccessibilityRole(element: ReactTestInstance) { +function isSupportedAccessibilityElement(element: ReactTestInstance) { if (!isAccessibilityElement(element)) { return false; } 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(); From 3f32966b380521dbec4a4e4cfb053114d939c5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 3 Sep 2024 22:17:31 +0200 Subject: [PATCH 2/2] refactor: support "switch" role --- src/helpers/accessibility.ts | 15 ++++- src/matchers/__tests__/to-be-checked.test.tsx | 63 +++++++++++++++++-- src/matchers/to-be-checked.tsx | 11 +++- website/docs/12.x/docs/api/jest-matchers.mdx | 4 +- .../12.x/docs/migration/jest-matchers.mdx | 2 +- 5 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index bdc27daad..227a47a65 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -159,16 +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 element.props.value; + 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; } @@ -226,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 216919de9..85dc39aa7 100644 --- a/src/matchers/__tests__/to-be-checked.test.tsx +++ b/src/matchers/__tests__/to-be-checked.test.tsx @@ -79,7 +79,7 @@ test('toBeCheck() with Switch', () => { `); }); -test('toBeCheck() with checkbox role', () => { +test('toBeCheck() with "checkbox" role', () => { renderViewsWithRole('checkbox'); const checked = screen.getByTestId('checkbox-checked'); @@ -149,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'); @@ -202,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'); @@ -209,10 +262,10 @@ test('throws error for invalid role', () => { const unchecked = screen.getByTestId('adjustable-unchecked'); expect(() => expect(checked).toBeChecked()).toThrowErrorMatchingInlineSnapshot( - `"toBeChecked() works only on "Switch" elements or 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 "Switch" elements or accessibility elements with "checkbox" or "radio" role."`, + `"toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role."`, ); }); @@ -221,6 +274,6 @@ test('throws error for non-accessibility element', () => { const view = screen.getByTestId('test'); expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot( - `"toBeChecked() works only on "Switch" elements or 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 83edf6f02..fb9be3927 100644 --- a/src/matchers/to-be-checked.tsx +++ b/src/matchers/to-be-checked.tsx @@ -1,6 +1,11 @@ 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'; @@ -10,7 +15,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc if (!isHostSwitch(element) && !isSupportedAccessibilityElement(element)) { throw new ErrorWithStack( - `toBeChecked() works only on "Switch" elements or accessibility elements with "checkbox" or "radio" role.`, + `toBeChecked() works only on host "Switch" elements or accessibility elements with "checkbox", "radio" or "switch" role.`, toBeChecked, ); } @@ -35,5 +40,5 @@ function isSupportedAccessibilityElement(element: ReactTestInstance) { } const role = getRole(element); - return role === 'checkbox' || role === 'radio'; + return rolesSupportingCheckedState[role]; } 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