Skip to content

Commit

Permalink
fix: *ByA11yState default value false value for busy, disabled & sele…
Browse files Browse the repository at this point in the history
…cted state (#1166)
  • Loading branch information
mdjastrzebski authored Oct 21, 2022
1 parent 96bf63a commit bd5ddd7
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 73 deletions.
12 changes: 11 additions & 1 deletion src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
36 changes: 36 additions & 0 deletions src/helpers/matchers/accessibilityState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AccessibilityState } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
import { accessibilityStateKeys } from '../accessiblity';

/**
* 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 accessibilityStateKeys.every((key) => matchState(state, matcher, key));
}

function matchState(
state: AccessibilityState,
matcher: AccessibilityState,
key: keyof AccessibilityState
) {
return (
matcher[key] === undefined ||
matcher[key] === (state?.[key] ?? defaultState[key])
);
}
162 changes: 146 additions & 16 deletions src/queries/__tests__/a11yState.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
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';

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 <Text {...rest}>{children}</Text>;
};
Expand Down Expand Up @@ -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 });
Expand All @@ -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'
);
});

Expand All @@ -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([]);

Expand All @@ -90,9 +82,147 @@ 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
);
});

describe('checked state matching', () => {
it('handles true', () => {
const view = render(<View accessibilityState={{ checked: true }} />);

expect(view.getByA11yState({ checked: true })).toBeTruthy();
expect(view.queryByA11yState({ checked: 'mixed' })).toBeFalsy();
expect(view.queryByA11yState({ checked: false })).toBeFalsy();
});

it('handles mixed', () => {
const view = render(<View accessibilityState={{ checked: 'mixed' }} />);

expect(view.getByA11yState({ checked: 'mixed' })).toBeTruthy();
expect(view.queryByA11yState({ checked: true })).toBeFalsy();
expect(view.queryByA11yState({ checked: false })).toBeFalsy();
});

it('handles false', () => {
const view = render(<View accessibilityState={{ checked: false }} />);

expect(view.getByA11yState({ checked: false })).toBeTruthy();
expect(view.queryByA11yState({ checked: true })).toBeFalsy();
expect(view.queryByA11yState({ checked: 'mixed' })).toBeFalsy();
});

it('handles default', () => {
const view = render(<View accessibilityState={{}} />);

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(<View accessibilityState={{ expanded: true }} />);

expect(view.getByA11yState({ expanded: true })).toBeTruthy();
expect(view.queryByA11yState({ expanded: false })).toBeFalsy();
});

it('handles false', () => {
const view = render(<View accessibilityState={{ expanded: false }} />);

expect(view.getByA11yState({ expanded: false })).toBeTruthy();
expect(view.queryByA11yState({ expanded: true })).toBeFalsy();
});

it('handles default', () => {
const view = render(<View accessibilityState={{}} />);

expect(view.queryByA11yState({ expanded: false })).toBeFalsy();
expect(view.queryByA11yState({ expanded: true })).toBeFalsy();
});
});

describe('disabled state matching', () => {
it('handles true', () => {
const view = render(<View accessibilityState={{ disabled: true }} />);

expect(view.getByA11yState({ disabled: true })).toBeTruthy();
expect(view.queryByA11yState({ disabled: false })).toBeFalsy();
});

it('handles false', () => {
const view = render(<View accessibilityState={{ disabled: false }} />);

expect(view.getByA11yState({ disabled: false })).toBeTruthy();
expect(view.queryByA11yState({ disabled: true })).toBeFalsy();
});

it('handles default', () => {
const view = render(<View accessibilityState={{}} />);

expect(view.getByA11yState({ disabled: false })).toBeTruthy();
expect(view.queryByA11yState({ disabled: true })).toBeFalsy();
});
});

describe('busy state matching', () => {
it('handles true', () => {
const view = render(<View accessibilityState={{ busy: true }} />);

expect(view.getByA11yState({ busy: true })).toBeTruthy();
expect(view.queryByA11yState({ busy: false })).toBeFalsy();
});

it('handles false', () => {
const view = render(<View accessibilityState={{ busy: false }} />);

expect(view.getByA11yState({ busy: false })).toBeTruthy();
expect(view.queryByA11yState({ busy: true })).toBeFalsy();
});

it('handles default', () => {
const view = render(<View accessibilityState={{}} />);

expect(view.getByA11yState({ busy: false })).toBeTruthy();
expect(view.queryByA11yState({ busy: true })).toBeFalsy();
});
});

describe('selected state matching', () => {
it('handles true', () => {
const view = render(<View accessibilityState={{ selected: true }} />);

expect(view.getByA11yState({ selected: true })).toBeTruthy();
expect(view.queryByA11yState({ selected: false })).toBeFalsy();
});

it('handles false', () => {
const view = render(<View accessibilityState={{ selected: false }} />);

expect(view.getByA11yState({ selected: false })).toBeTruthy();
expect(view.queryByA11yState({ selected: true })).toBeFalsy();
});

it('handles default', () => {
const view = render(<View accessibilityState={{}} />);

expect(view.getByA11yState({ selected: false })).toBeTruthy();
expect(view.queryByA11yState({ selected: true })).toBeFalsy();
});
});

test('*ByA11yState on Pressable with "disabled" prop', () => {
const view = render(<Pressable disabled />);
expect(view.getByA11yState({ disabled: true })).toBeTruthy();
expect(view.queryByA11yState({ disabled: false })).toBeFalsy();
});

test('*ByA11yState on TouchableOpacity with "disabled" prop', () => {
const view = render(<TouchableOpacity disabled />);
expect(view.getByA11yState({ disabled: true })).toBeTruthy();
expect(view.queryByA11yState({ disabled: false })).toBeFalsy();
});
27 changes: 20 additions & 7 deletions src/queries/a11yState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactTestInstance } from 'react-test-renderer';
import type { AccessibilityState } from 'react-native';
import { matchObjectProp } from '../helpers/matchers/matchObjectProp';
import { accessibilityStateKeys } from '../helpers/accessiblity';
import { matchAccessibilityState } from '../helpers/matchers/accessibilityState';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -13,19 +14,31 @@ import type {

const queryAllByA11yState = (
instance: ReactTestInstance
): ((state: AccessibilityState) => Array<ReactTestInstance>) =>
function queryAllByA11yStateFn(state) {
): ((matcher: AccessibilityState) => Array<ReactTestInstance>) =>
function queryAllByA11yStateFn(matcher) {
return instance.findAll(
(node) =>
typeof node.type === 'string' &&
matchObjectProp(node.props.accessibilityState, state)
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,
Expand Down
Loading

0 comments on commit bd5ddd7

Please sign in to comment.