From 21527d5e50ad64372d049c13c1c16f2a1bff635e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sun, 9 Oct 2022 22:09:20 +0200 Subject: [PATCH 1/8] fix: *ByA11yState default value false value for busy, disabled & selected state --- src/queries/__tests__/a11yState.test.tsx | 140 ++++++++++++++++++++++- src/queries/a11yState.ts | 26 +++-- 2 files changed, 158 insertions(+), 8 deletions(-) diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index c15c078b4..e08f1cbc1 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { TouchableOpacity, Text } from 'react-native'; +import { View, Text, Pressable, TouchableOpacity } from 'react-native'; import { render } from '../..'; const TEXT_LABEL = 'cool text'; @@ -96,3 +96,141 @@ test('getAllByA11yState, queryAllByA11yState, findAllByA11yState', async () => { 2 ); }); + +describe('checked state matching', () => { + it('handles true', () => { + const view = render(); + + expect(view.getByA11yState({ checked: true })).toBeTruthy(); + expect(view.queryByA11yState({ checked: 'mixed' })).toBeFalsy(); + expect(view.queryByA11yState({ checked: false })).toBeFalsy(); + }); + + it('handles mixed', () => { + const view = render(); + + expect(view.getByA11yState({ checked: 'mixed' })).toBeTruthy(); + expect(view.queryByA11yState({ checked: true })).toBeFalsy(); + expect(view.queryByA11yState({ checked: false })).toBeFalsy(); + }); + + it('handles false', () => { + const view = render(); + + expect(view.getByA11yState({ checked: false })).toBeTruthy(); + expect(view.queryByA11yState({ checked: true })).toBeFalsy(); + expect(view.queryByA11yState({ checked: 'mixed' })).toBeFalsy(); + }); + + it('handles default', () => { + const view = render(); + + expect(view.queryByA11yState({ checked: false })).toBeFalsy(); + expect(view.queryByA11yState({ checked: true })).toBeFalsy(); + expect(view.queryByA11yState({ checked: 'mixed' })).toBeFalsy(); + }); +}); + +describe('expanded state matching', () => { + it('handles true', () => { + const view = render(); + + expect(view.getByA11yState({ expanded: true })).toBeTruthy(); + expect(view.queryByA11yState({ expanded: false })).toBeFalsy(); + }); + + it('handles false', () => { + const view = render(); + + expect(view.getByA11yState({ expanded: false })).toBeTruthy(); + expect(view.queryByA11yState({ expanded: true })).toBeFalsy(); + }); + + it('handles default', () => { + const view = render(); + + expect(view.queryByA11yState({ expanded: false })).toBeFalsy(); + expect(view.queryByA11yState({ expanded: true })).toBeFalsy(); + }); +}); + +describe('disabled state matching', () => { + it('handles true', () => { + const view = render(); + + expect(view.getByA11yState({ disabled: true })).toBeTruthy(); + expect(view.queryByA11yState({ disabled: false })).toBeFalsy(); + }); + + it('handles false', () => { + const view = render(); + + expect(view.getByA11yState({ disabled: false })).toBeTruthy(); + expect(view.queryByA11yState({ disabled: true })).toBeFalsy(); + }); + + it('handles default', () => { + const view = render(); + + expect(view.getByA11yState({ disabled: false })).toBeTruthy(); + expect(view.queryByA11yState({ disabled: true })).toBeFalsy(); + }); +}); + +describe('busy state matching', () => { + it('handles true', () => { + const view = render(); + + expect(view.getByA11yState({ busy: true })).toBeTruthy(); + expect(view.queryByA11yState({ busy: false })).toBeFalsy(); + }); + + it('handles false', () => { + const view = render(); + + expect(view.getByA11yState({ busy: false })).toBeTruthy(); + expect(view.queryByA11yState({ busy: true })).toBeFalsy(); + }); + + it('handles default', () => { + const view = render(); + + expect(view.getByA11yState({ busy: false })).toBeTruthy(); + expect(view.queryByA11yState({ busy: true })).toBeFalsy(); + }); +}); + +describe('selected state matching', () => { + it('handles true', () => { + const view = render(); + + expect(view.getByA11yState({ selected: true })).toBeTruthy(); + expect(view.queryByA11yState({ selected: false })).toBeFalsy(); + }); + + it('handles false', () => { + const view = render(); + + expect(view.getByA11yState({ selected: false })).toBeTruthy(); + expect(view.queryByA11yState({ selected: true })).toBeFalsy(); + }); + + it('handles default', () => { + const view = render(); + + expect(view.getByA11yState({ selected: false })).toBeTruthy(); + expect(view.queryByA11yState({ selected: true })).toBeFalsy(); + }); +}); + +test('*ByA11yState on Pressable with "disabled" prop', () => { + const view = render(); + expect(view.getByA11yState({ disabled: true })).toBeTruthy(); + expect(view.queryByA11yState({ disabled: false })).toBeFalsy(); +}); + +test('*ByA11yState on TouchableOpacity with "disabled" prop', () => { + const view = render(); + expect(view.getByA11yState({ disabled: true })).toBeTruthy(); + expect(view.queryByA11yState({ disabled: false })).toBeFalsy(); +}); diff --git a/src/queries/a11yState.ts b/src/queries/a11yState.ts index 4d740367d..eaffeca01 100644 --- a/src/queries/a11yState.ts +++ b/src/queries/a11yState.ts @@ -1,6 +1,5 @@ import type { ReactTestInstance } from 'react-test-renderer'; import type { AccessibilityState } from 'react-native'; -import { matchObjectProp } from '../helpers/matchers/matchObjectProp'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -11,15 +10,28 @@ import type { QueryByQuery, } from './makeQueries'; +function matchState(value: unknown, matcher: unknown) { + return matcher === undefined || value === matcher; +} + const queryAllByA11yState = ( instance: ReactTestInstance -): ((state: AccessibilityState) => Array) => - function queryAllByA11yStateFn(state) { - return instance.findAll( - (node) => +): ((matcher: AccessibilityState) => Array) => + function queryAllByA11yStateFn(matcher) { + return instance.findAll((node) => { + const stateProp = node.props.accessibilityState; + + // busy, disabled & selected states default to false, + // while checked & expended states treat false and default as sepatate values + return ( typeof node.type === 'string' && - matchObjectProp(node.props.accessibilityState, state) - ); + matchState(stateProp?.busy ?? false, matcher.busy) && + matchState(stateProp?.disabled ?? false, matcher.disabled) && + matchState(stateProp?.selected ?? false, matcher.selected) && + matchState(stateProp?.checked, matcher.checked) && + matchState(stateProp?.expanded, matcher.expanded) + ); + }); }; const getMultipleError = (state: AccessibilityState) => From be3af1322c7898932799b28451b6754c82a12390 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 15 Oct 2022 23:17:38 +0200 Subject: [PATCH 2/8] refactor: self code review --- src/queries/a11yState.ts | 28 ++++++++++++++++++---------- website/docs/Queries.md | 26 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/queries/a11yState.ts b/src/queries/a11yState.ts index eaffeca01..05a3efa72 100644 --- a/src/queries/a11yState.ts +++ b/src/queries/a11yState.ts @@ -10,6 +10,23 @@ import type { QueryByQuery, } from './makeQueries'; +export function matchAccessibilityState( + node: ReactTestInstance, + matcher: AccessibilityState +) { + const stateProp = node.props.accessibilityState; + + // busy, disabled & selected states default to false, + // while checked & expended states treat false and default as sepatate values + return ( + matchState(stateProp?.busy ?? false, matcher.busy) && + matchState(stateProp?.disabled ?? false, matcher.disabled) && + matchState(stateProp?.selected ?? false, matcher.selected) && + matchState(stateProp?.checked, matcher.checked) && + matchState(stateProp?.expanded, matcher.expanded) + ); +} + function matchState(value: unknown, matcher: unknown) { return matcher === undefined || value === matcher; } @@ -19,17 +36,8 @@ const queryAllByA11yState = ( ): ((matcher: AccessibilityState) => Array) => function queryAllByA11yStateFn(matcher) { return instance.findAll((node) => { - const stateProp = node.props.accessibilityState; - - // busy, disabled & selected states default to false, - // while checked & expended states treat false and default as sepatate values return ( - typeof node.type === 'string' && - matchState(stateProp?.busy ?? false, matcher.busy) && - matchState(stateProp?.disabled ?? false, matcher.disabled) && - matchState(stateProp?.selected ?? false, matcher.selected) && - matchState(stateProp?.checked, matcher.checked) && - matchState(stateProp?.expanded, matcher.expanded) + typeof node.type === 'string' && matchAccessibilityState(node, matcher) ); }); }; diff --git a/website/docs/Queries.md b/website/docs/Queries.md index cc9c4b2a6..2b39059af 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -23,6 +23,8 @@ title: Queries - [`ByRole`](#byrole) - [Options](#options-1) - [`ByA11yState`, `ByAccessibilityState`](#bya11ystate-byaccessibilitystate) + - [Default state for: `disabled`, `selected`, and `busy` keys](#default-state-for-disabled-selected-and-busy-keys) + - [Default state for: `checked` and `expanded` keys](#default-state-for-checked-and-expanded-keys) - [`ByA11Value`, `ByAccessibilityValue`](#bya11value-byaccessibilityvalue) - [TextMatch](#textmatch) - [Examples](#examples) @@ -296,6 +298,30 @@ render(); const element = screen.getByA11yState({ disabled: true }); ``` +:::note + +#### Default state for: `disabled`, `selected`, and `busy` keys + +Passing `false` matcher value will match both elements with explicit `false` state value and without explicit state value. + +For instance, `getByA11yState({ disabled: false })` will match elements with following props: +* `accessibilityState={{ disabled: false, ... }}` +* no `disabled` key under `accessibilityState` prop, e.g. `accessibilityState={{}}` +* no `accessibilityState` prop at all + +#### Default state for: `checked` and `expanded` keys +Passing `false` matcher value will only match elements with explicit `false` state value. + +For instance, `getByA11yState({ checked: false })` will only match elements with: +* `accessibilityState={{ checked: false, ... }}` + +but will not match elements with following props: +* no `checked` key under `accessibilityState` prop, e.g. `accessibilityState={{}}` +* no `accessibilityState` prop at all + +The difference in handling default values is made to reflect observed accessibility behaviour on iOS and Android platforms. +::: + ### `ByA11Value`, `ByAccessibilityValue` > getByA11yValue, getAllByA11yValue, queryByA11yValue, queryAllByA11yValue, findByA11yValue, findAllByA11yValue From 864528c2f7232dc7f8489334e7d815ec2f0101ac Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 19 Oct 2022 22:32:35 +0200 Subject: [PATCH 3/8] refactor: code review changes --- src/queries/a11yState.ts | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/queries/a11yState.ts b/src/queries/a11yState.ts index 05a3efa72..5e6b82bc4 100644 --- a/src/queries/a11yState.ts +++ b/src/queries/a11yState.ts @@ -10,25 +10,41 @@ import type { QueryByQuery, } from './makeQueries'; +/** + * 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, + checked: undefined, + busy: false, + expanded: undefined, +}; + export function matchAccessibilityState( node: ReactTestInstance, matcher: AccessibilityState ) { - const stateProp = node.props.accessibilityState; - - // busy, disabled & selected states default to false, - // while checked & expended states treat false and default as sepatate values + const state = node.props.accessibilityState; return ( - matchState(stateProp?.busy ?? false, matcher.busy) && - matchState(stateProp?.disabled ?? false, matcher.disabled) && - matchState(stateProp?.selected ?? false, matcher.selected) && - matchState(stateProp?.checked, matcher.checked) && - matchState(stateProp?.expanded, matcher.expanded) + matchState(state, matcher, 'disabled') && + matchState(state, matcher, 'selected') && + matchState(state, matcher, 'checked') && + matchState(state, matcher, 'busy') && + matchState(state, matcher, 'expanded') ); } -function matchState(value: unknown, matcher: unknown) { - return matcher === undefined || value === matcher; +function matchState( + value: AccessibilityState, + matcher: AccessibilityState, + key: keyof AccessibilityState +) { + const valueWithDefault = value?.[key] ?? defaultState[key]; + return matcher[key] === undefined || matcher[key] === valueWithDefault; } const queryAllByA11yState = ( From d80d2a91704074317ec4e6887af2f601df100647 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 20 Oct 2022 12:04:31 +0200 Subject: [PATCH 4/8] refactor: reuse `matchAccessibilityState` in `*ByRole` queries --- src/queries/role.ts | 66 +++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/src/queries/role.ts b/src/queries/role.ts index cb77a22aa..706f3972a 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -12,6 +12,7 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; +import { matchAccessibilityState } from './a11yState'; type ByRoleOptions = { name?: TextMatch; @@ -19,6 +20,14 @@ type ByRoleOptions = { type AccessibilityStateKey = keyof AccessibilityState; +const accessibilityStateKeys: AccessibilityStateKey[] = [ + 'disabled', + 'selected', + 'checked', + 'busy', + 'expanded', +]; + const matchAccessibleNameIfNeeded = ( node: ReactTestInstance, name?: TextMatch @@ -31,42 +40,16 @@ const matchAccessibleNameIfNeeded = ( ); }; -// disabled:undefined is equivalent to disabled:false, same for selected. busy not, but it makes -// sense from a testing/voice-over perspective. checked and expanded do behave differently -const implicityFalseState: AccessibilityStateKey[] = [ - 'disabled', - 'selected', - 'busy', -]; - const matchAccessibleStateIfNeeded = ( node: ReactTestInstance, options?: ByRoleOptions -) => - accessibilityStates.every((accessibilityState) => { - const queriedState = options?.[accessibilityState]; - - if (typeof queriedState !== 'undefined') { - // Some accessibilityState properties have implicit value (when not set) - const defaultState = implicityFalseState.includes(accessibilityState) - ? false - : undefined; - return ( - queriedState === - (node.props.accessibilityState?.[accessibilityState] ?? defaultState) - ); - } else { - return true; - } - }); +) => { + if (!options) { + return true; + } -const accessibilityStates: AccessibilityStateKey[] = [ - 'disabled', - 'selected', - 'checked', - 'busy', - 'expanded', -]; + return matchAccessibilityState(node, options); +}; const queryAllByRole = ( instance: ReactTestInstance @@ -90,22 +73,15 @@ const buildErrorMessage = (role: TextMatch, options: ByRoleOptions = {}) => { errors.push(`name: "${String(options.name)}"`); } - if ( - accessibilityStates.some( - (accessibilityState) => typeof options[accessibilityState] !== 'undefined' - ) - ) { - accessibilityStates.forEach((accessibilityState) => { - if (options[accessibilityState]) { - errors.push( - `${accessibilityState} state: ${options[accessibilityState]}` - ); - } - }); - } + accessibilityStateKeys.forEach((stateKey) => { + if (options[stateKey]) { + errors.push(`${stateKey} state: ${options[stateKey]}`); + } + }); return errors.join(', '); }; + const getMultipleError = (role: TextMatch, options?: ByRoleOptions) => `Found multiple elements with ${buildErrorMessage(role, options)}`; const getMissingError = (role: TextMatch, options?: ByRoleOptions) => From 0aa7d544653ef27ed78c87736117c7f4440a3c9c Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 20 Oct 2022 12:12:18 +0200 Subject: [PATCH 5/8] refactor: tweak --- src/queries/role.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/queries/role.ts b/src/queries/role.ts index 706f3972a..fe5f7319b 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -44,11 +44,7 @@ const matchAccessibleStateIfNeeded = ( node: ReactTestInstance, options?: ByRoleOptions ) => { - if (!options) { - return true; - } - - return matchAccessibilityState(node, options); + return options != null ? matchAccessibilityState(node, options) : true; }; const queryAllByRole = ( From 50a12d74d0574e96540b22d69736893d2fef1c13 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 20 Oct 2022 13:56:51 +0200 Subject: [PATCH 6/8] refactor: code review changes --- src/helpers/accessiblity.ts | 12 ++++- src/helpers/matchers/accessibilityState.ts | 39 ++++++++++++++ src/queries/__tests__/a11yState.test.tsx | 22 +++----- src/queries/a11yState.ts | 63 +++++++--------------- src/queries/role.ts | 18 ++----- 5 files changed, 82 insertions(+), 72 deletions(-) create mode 100644 src/helpers/matchers/accessibilityState.ts diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index a4dc1885f..0cccb7b6b 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -1,7 +1,17 @@ -import { StyleSheet } from 'react-native'; +import { AccessibilityState, StyleSheet } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; import { getHostSiblings } from './component-tree'; +export type AccessibilityStateKey = keyof AccessibilityState; + +export const accessibilityStateKeys: AccessibilityStateKey[] = [ + 'disabled', + 'selected', + 'checked', + 'busy', + 'expanded', +]; + export function isInaccessible(element: ReactTestInstance | null): boolean { if (element == null) { return true; diff --git a/src/helpers/matchers/accessibilityState.ts b/src/helpers/matchers/accessibilityState.ts new file mode 100644 index 000000000..61b2af064 --- /dev/null +++ b/src/helpers/matchers/accessibilityState.ts @@ -0,0 +1,39 @@ +import { AccessibilityState } from 'react-native'; +import { ReactTestInstance } from 'react-test-renderer'; + +/** + * 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, + checked: undefined, + busy: false, + expanded: undefined, +}; + +export function matchAccessibilityState( + node: ReactTestInstance, + matcher: AccessibilityState +) { + const state = node.props.accessibilityState; + return ( + matchState(state, matcher, 'disabled') && + matchState(state, matcher, 'selected') && + matchState(state, matcher, 'checked') && + matchState(state, matcher, 'busy') && + matchState(state, matcher, 'expanded') + ); +} + +function matchState( + value: AccessibilityState, + matcher: AccessibilityState, + key: keyof AccessibilityState +) { + const valueWithDefault = value?.[key] ?? defaultState[key]; + return matcher[key] === undefined || matcher[key] === valueWithDefault; +} diff --git a/src/queries/__tests__/a11yState.test.tsx b/src/queries/__tests__/a11yState.test.tsx index e08f1cbc1..c97ad70cb 100644 --- a/src/queries/__tests__/a11yState.test.tsx +++ b/src/queries/__tests__/a11yState.test.tsx @@ -4,14 +4,6 @@ import { render } from '../..'; const TEXT_LABEL = 'cool text'; -const getMultipleInstancesFoundMessage = (value: string) => { - return `Found multiple elements with accessibilityState: ${value}`; -}; - -const getNoInstancesFoundMessage = (value: string) => { - return `Unable to find an element with accessibilityState: ${value}`; -}; - const Typography = ({ children, ...rest }: any) => { return {children}; }; @@ -48,15 +40,15 @@ test('getByA11yState, queryByA11yState, findByA11yState', async () => { }); expect(() => getByA11yState({ disabled: true })).toThrow( - getNoInstancesFoundMessage('{"disabled":true}') + 'Unable to find an element with disabled state: true' ); expect(queryByA11yState({ disabled: true })).toEqual(null); expect(() => getByA11yState({ expanded: false })).toThrow( - getMultipleInstancesFoundMessage('{"expanded":false}') + 'Found multiple elements with expanded state: false' ); expect(() => queryByA11yState({ expanded: false })).toThrow( - getMultipleInstancesFoundMessage('{"expanded":false}') + 'Found multiple elements with expanded state: false' ); const asyncButton = await findByA11yState({ selected: true }); @@ -65,10 +57,10 @@ test('getByA11yState, queryByA11yState, findByA11yState', async () => { expanded: false, }); await expect(findByA11yState({ disabled: true })).rejects.toThrow( - getNoInstancesFoundMessage('{"disabled":true}') + 'Unable to find an element with disabled state: true' ); await expect(findByA11yState({ expanded: false })).rejects.toThrow( - getMultipleInstancesFoundMessage('{"expanded":false}') + 'Found multiple elements with expanded state: false' ); }); @@ -81,7 +73,7 @@ test('getAllByA11yState, queryAllByA11yState, findAllByA11yState', async () => { expect(queryAllByA11yState({ selected: true })).toHaveLength(1); expect(() => getAllByA11yState({ disabled: true })).toThrow( - getNoInstancesFoundMessage('{"disabled":true}') + 'Unable to find an element with disabled state: true' ); expect(queryAllByA11yState({ disabled: true })).toEqual([]); @@ -90,7 +82,7 @@ test('getAllByA11yState, queryAllByA11yState, findAllByA11yState', async () => { await expect(findAllByA11yState({ selected: true })).resolves.toHaveLength(1); await expect(findAllByA11yState({ disabled: true })).rejects.toThrow( - getNoInstancesFoundMessage('{"disabled":true}') + 'Unable to find an element with disabled state: true' ); await expect(findAllByA11yState({ expanded: false })).resolves.toHaveLength( 2 diff --git a/src/queries/a11yState.ts b/src/queries/a11yState.ts index 5e6b82bc4..e93fa5d9e 100644 --- a/src/queries/a11yState.ts +++ b/src/queries/a11yState.ts @@ -1,5 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import type { AccessibilityState } from 'react-native'; +import { matchAccessibilityState } from '../helpers/matchers/accessibilityState'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -9,59 +10,35 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; - -/** - * 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, - checked: undefined, - busy: false, - expanded: undefined, -}; - -export function matchAccessibilityState( - node: ReactTestInstance, - matcher: AccessibilityState -) { - const state = node.props.accessibilityState; - return ( - matchState(state, matcher, 'disabled') && - matchState(state, matcher, 'selected') && - matchState(state, matcher, 'checked') && - matchState(state, matcher, 'busy') && - matchState(state, matcher, 'expanded') - ); -} - -function matchState( - value: AccessibilityState, - matcher: AccessibilityState, - key: keyof AccessibilityState -) { - const valueWithDefault = value?.[key] ?? defaultState[key]; - return matcher[key] === undefined || matcher[key] === valueWithDefault; -} +import { accessibilityStateKeys } from '../helpers/accessiblity'; const queryAllByA11yState = ( instance: ReactTestInstance ): ((matcher: AccessibilityState) => Array) => function queryAllByA11yStateFn(matcher) { - return instance.findAll((node) => { - return ( + return instance.findAll( + (node) => typeof node.type === 'string' && matchAccessibilityState(node, matcher) - ); - }); + ); }; +const buildErrorMessage = (state: AccessibilityState = {}) => { + const errors: string[] = []; + + accessibilityStateKeys.forEach((stateKey) => { + if (state[stateKey] !== undefined) { + errors.push(`${stateKey} state: ${state[stateKey]}`); + } + }); + + return errors.join(', '); +}; + const getMultipleError = (state: AccessibilityState) => - `Found multiple elements with accessibilityState: ${JSON.stringify(state)}`; + `Found multiple elements with ${buildErrorMessage(state)}`; + const getMissingError = (state: AccessibilityState) => - `Unable to find an element with accessibilityState: ${JSON.stringify(state)}`; + `Unable to find an element with ${buildErrorMessage(state)}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( queryAllByA11yState, diff --git a/src/queries/role.ts b/src/queries/role.ts index fe5f7319b..00d42b5e0 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -1,7 +1,9 @@ import { type AccessibilityState } from 'react-native'; import type { ReactTestInstance } from 'react-test-renderer'; +import { accessibilityStateKeys } from '../helpers/accessiblity'; +import { matchAccessibilityState } from '../helpers/matchers/accessibilityState'; import { matchStringProp } from '../helpers/matchers/matchStringProp'; -import { TextMatch } from '../matches'; +import type { TextMatch } from '../matches'; import { getQueriesForElement } from '../within'; import { makeQueries } from './makeQueries'; import type { @@ -12,22 +14,11 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import { matchAccessibilityState } from './a11yState'; type ByRoleOptions = { name?: TextMatch; } & AccessibilityState; -type AccessibilityStateKey = keyof AccessibilityState; - -const accessibilityStateKeys: AccessibilityStateKey[] = [ - 'disabled', - 'selected', - 'checked', - 'busy', - 'expanded', -]; - const matchAccessibleNameIfNeeded = ( node: ReactTestInstance, name?: TextMatch @@ -70,7 +61,7 @@ const buildErrorMessage = (role: TextMatch, options: ByRoleOptions = {}) => { } accessibilityStateKeys.forEach((stateKey) => { - if (options[stateKey]) { + if (options[stateKey] !== undefined) { errors.push(`${stateKey} state: ${options[stateKey]}`); } }); @@ -80,6 +71,7 @@ const buildErrorMessage = (role: TextMatch, options: ByRoleOptions = {}) => { const getMultipleError = (role: TextMatch, options?: ByRoleOptions) => `Found multiple elements with ${buildErrorMessage(role, options)}`; + const getMissingError = (role: TextMatch, options?: ByRoleOptions) => `Unable to find an element with ${buildErrorMessage(role, options)}`; From bb416b97bcc9b1929db90166ef424f472cfc6c6d Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 20 Oct 2022 13:59:40 +0200 Subject: [PATCH 7/8] fix: lint --- src/queries/a11yState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/a11yState.ts b/src/queries/a11yState.ts index e93fa5d9e..7bbb415f7 100644 --- a/src/queries/a11yState.ts +++ b/src/queries/a11yState.ts @@ -1,5 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import type { AccessibilityState } from 'react-native'; +import { accessibilityStateKeys } from '../helpers/accessiblity'; import { matchAccessibilityState } from '../helpers/matchers/accessibilityState'; import { makeQueries } from './makeQueries'; import type { @@ -10,7 +11,6 @@ import type { QueryAllByQuery, QueryByQuery, } from './makeQueries'; -import { accessibilityStateKeys } from '../helpers/accessiblity'; const queryAllByA11yState = ( instance: ReactTestInstance From 5acbd6718d826071a079b14f73df485c8e2ab211 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 21 Oct 2022 10:03:09 +0200 Subject: [PATCH 8/8] refactor: code review changes --- src/helpers/matchers/accessibilityState.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/helpers/matchers/accessibilityState.ts b/src/helpers/matchers/accessibilityState.ts index 61b2af064..b068353e4 100644 --- a/src/helpers/matchers/accessibilityState.ts +++ b/src/helpers/matchers/accessibilityState.ts @@ -1,5 +1,6 @@ import { AccessibilityState } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; +import { accessibilityStateKeys } from '../accessiblity'; /** * Default accessibility state values based on experiments using accessibility @@ -20,20 +21,16 @@ export function matchAccessibilityState( matcher: AccessibilityState ) { const state = node.props.accessibilityState; - return ( - matchState(state, matcher, 'disabled') && - matchState(state, matcher, 'selected') && - matchState(state, matcher, 'checked') && - matchState(state, matcher, 'busy') && - matchState(state, matcher, 'expanded') - ); + return accessibilityStateKeys.every((key) => matchState(state, matcher, key)); } function matchState( - value: AccessibilityState, + state: AccessibilityState, matcher: AccessibilityState, key: keyof AccessibilityState ) { - const valueWithDefault = value?.[key] ?? defaultState[key]; - return matcher[key] === undefined || matcher[key] === valueWithDefault; + return ( + matcher[key] === undefined || + matcher[key] === (state?.[key] ?? defaultState[key]) + ); }