Skip to content

feat: byA11yState matcher #260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions src/__tests__/a11yAPI.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const BUTTON_ROLE = 'button';
const TEXT_LABEL = 'cool text';
const TEXT_HINT = 'static text';
const TEXT_ROLE = 'link';
const NO_MATCHES_TEXT = 'not-existent-element';
// Little hack to make all the methods happy with type
const NO_MATCHES_TEXT: any = 'not-existent-element';
const NO_INSTANCES_FOUND = 'No instances found';
const FOUND_TWO_INSTANCES = 'Expected 1 but found 2 instances';

const Typography = ({ children, ...rest }) => {
const Typography = ({ children, ...rest }: any) => {
return <Text {...rest}>{children}</Text>;
};

Expand All @@ -31,6 +32,7 @@ class Button extends React.Component<any> {
accessibilityLabel={TEXT_LABEL}
accessibilityRole={TEXT_ROLE}
accessibilityStates={['selected']}
accessibilityState={{ expanded: false, selected: true }}
>
{this.props.children}
</Typography>
Expand All @@ -47,6 +49,7 @@ function Section() {
accessibilityLabel={TEXT_LABEL}
accessibilityRole={TEXT_ROLE}
accessibilityStates={['selected', 'disabled']}
accessibilityState={{ expanded: false }}
>
Title
</Typography>
Expand Down Expand Up @@ -159,3 +162,43 @@ test('getAllByA11yStates, queryAllByA11yStates', () => {
expect(() => getAllByA11yStates([])).toThrow(NO_INSTANCES_FOUND);
expect(queryAllByA11yStates(NO_MATCHES_TEXT)).toEqual([]);
});

test('getByA11yState, queryByA11yState', () => {
const { getByA11yState, queryByA11yState } = render(<Section />);

expect(getByA11yState({ selected: true }).props.accessibilityState).toEqual({
selected: true,
expanded: false,
});
expect(
queryByA11yState({ selected: true })?.props.accessibilityState
).toEqual({
selected: true,
expanded: false,
});

expect(() => getByA11yState({ disabled: true })).toThrow(NO_INSTANCES_FOUND);
expect(queryByA11yState({ disabled: true })).toEqual(null);

expect(() => getByA11yState({ expanded: false })).toThrow(
FOUND_TWO_INSTANCES
);
expect(() => queryByA11yState({ expanded: false })).toThrow(
FOUND_TWO_INSTANCES
);
});

test('getAllByA11yState, queryAllByA11yState', () => {
const { getAllByA11yState, queryAllByA11yState } = render(<Section />);

expect(getAllByA11yState({ selected: true }).length).toEqual(1);
expect(queryAllByA11yState({ selected: true }).length).toEqual(1);

expect(() => getAllByA11yState({ disabled: true })).toThrow(
NO_INSTANCES_FOUND
);
expect(queryAllByA11yState({ disabled: true })).toEqual([]);

expect(getAllByA11yState({ expanded: false }).length).toEqual(2);
expect(queryAllByA11yState({ expanded: false }).length).toEqual(2);
});
85 changes: 57 additions & 28 deletions src/helpers/a11yAPI.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,48 @@
// @flow
import makeQuery from './makeQuery';
import type { A11yRole, A11yStates, A11yState } from '../types.flow';

type QueryFn = (string | RegExp) => ReactTestInstance | null;
type QueryAllFn = (string | RegExp) => Array<ReactTestInstance> | [];
type GetFn = (string | RegExp) => ReactTestInstance;
type GetAllFn = (string | RegExp) => Array<ReactTestInstance>;
type ArrayQueryFn = (string | Array<string>) => ReactTestInstance | null;
type ArrayQueryAllFn = (
string | Array<string>
) => Array<ReactTestInstance> | [];
type ArrayGetFn = (string | Array<string>) => ReactTestInstance;
type ArrayGetAllFn = (string | Array<string>) => Array<ReactTestInstance>;
type GetReturn = ReactTestInstance;
type GetAllReturn = Array<ReactTestInstance>;
type QueryReturn = ReactTestInstance | null;
type QueryAllReturn = Array<ReactTestInstance> | [];

type A11yAPI = {|
getByA11yLabel: GetFn,
getAllByA11yLabel: GetAllFn,
queryByA11yLabel: QueryFn,
queryAllByA11yLabel: QueryAllFn,
getByA11yHint: GetFn,
getAllByA11yHint: GetAllFn,
queryByA11yHint: QueryFn,
queryAllByA11yHint: QueryAllFn,
getByA11yRole: GetFn,
getAllByA11yRole: GetAllFn,
queryByA11yRole: QueryFn,
queryAllByA11yRole: QueryAllFn,
getByA11yStates: ArrayGetFn,
getAllByA11yStates: ArrayGetAllFn,
queryByA11yStates: ArrayQueryFn,
queryAllByA11yStates: ArrayQueryAllFn,
// Label
getByA11yLabel: (string | RegExp) => GetReturn,
getAllByA11yLabel: (string | RegExp) => GetAllReturn,
queryByA11yLabel: (string | RegExp) => QueryReturn,
queryAllByA11yLabel: (string | RegExp) => QueryAllReturn,

// Hint
getByA11yHint: (string | RegExp) => GetReturn,
getAllByA11yHint: (string | RegExp) => GetAllReturn,
queryByA11yHint: (string | RegExp) => QueryReturn,
queryAllByA11yHint: (string | RegExp) => QueryAllReturn,

// Role
getByA11yRole: (A11yRole | RegExp) => GetReturn,
getAllByA11yRole: (A11yRole | RegExp) => GetAllReturn,
queryByA11yRole: (A11yRole | RegExp) => QueryReturn,
queryAllByA11yRole: (A11yRole | RegExp) => QueryAllReturn,

// States
getByA11yStates: (A11yStates | Array<A11yStates>) => GetReturn,
getAllByA11yStates: (A11yStates | Array<A11yStates>) => GetAllReturn,
queryByA11yStates: (A11yStates | Array<A11yStates>) => QueryReturn,
queryAllByA11yStates: (A11yStates | Array<A11yStates>) => QueryAllReturn,

// State
getByA11yState: A11yState => GetReturn,
getAllByA11yState: A11yState => GetAllReturn,
queryByA11yState: A11yState => QueryReturn,
queryAllByA11yState: A11yState => QueryAllReturn,
|};

export function matchStringValue(prop?: string, matcher: string | RegExp) {
export function matchStringValue(
prop?: string,
matcher: string | RegExp
): boolean {
if (!prop) {
return false;
}
Expand All @@ -46,7 +57,7 @@ export function matchStringValue(prop?: string, matcher: string | RegExp) {
export function matchArrayValue(
prop?: Array<string>,
matcher: string | Array<string>
) {
): boolean {
if (!prop || matcher.length === 0) {
return false;
}
Expand All @@ -58,6 +69,14 @@ export function matchArrayValue(
return !matcher.some(e => !prop.includes(e));
}

export function matchObject<T: {}>(prop?: T, matcher: T): boolean {
return prop
? Object.keys(matcher).length !== 0 &&
Object.keys(prop).length !== 0 &&
!Object.keys(matcher).some(key => prop[key] !== matcher[key])
: false;
}

const a11yAPI = (instance: ReactTestInstance): A11yAPI =>
({
...makeQuery(
Expand Down Expand Up @@ -100,6 +119,16 @@ const a11yAPI = (instance: ReactTestInstance): A11yAPI =>
},
matchArrayValue
)(instance),
...makeQuery(
'accessibilityState',
{
getBy: ['getByA11yState', 'getByAccessibilityState'],
getAllBy: ['getAllByA11yState', 'getAllByAccessibilityState'],
queryBy: ['queryByA11yState', 'queryByAccessibilityState'],
queryAllBy: ['queryAllByA11yState', 'queryAllByAccessibilityState'],
},
matchObject
)(instance),
}: any);

export default a11yAPI;
48 changes: 48 additions & 0 deletions src/types.flow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @flow

export type A11yRole =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a reference link? https://reactnative.dev/docs/accessibility#accessibilityrole-ios-android
Ideally we could import directly from react-native, but I'm not sure if it's exported and if it would be backward compatible with older RN versions. This applies to TS as well

| 'none'
| 'button'
| 'link'
| 'search'
| 'image'
| 'keyboardkey'
| 'text'
| 'adjustable'
| 'imagebutton'
| 'header'
| 'summary'
| 'alert'
| 'checkbox'
| 'combobox'
| 'menu'
| 'menubar'
| 'menuitem'
| 'progressbar'
| 'radio'
| 'radiogroup'
| 'scrollbar'
| 'spinbutton'
| 'switch'
| 'tab'
| 'tablist'
| 'timer'
| 'toolbar';

export type A11yState = {|
disabled?: boolean,
selected?: boolean,
checked?: boolean | 'mixed',
busy?: boolean,
expanded?: boolean,
|};

export type A11yStates =
| 'disabled'
| 'selected'
| 'checked'
| 'unchecked'
| 'busy'
| 'expanded'
| 'collapsed'
| 'hasPopup';
29 changes: 17 additions & 12 deletions typings/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,32 +137,37 @@ const queryByA11yHint: ReactTestInstance = tree.queryByA11yHint('label');
const queryAllByA11yHint: Array<ReactTestInstance> = tree.queryAllByA11yHint(
'label'
);
const getByA11yRole: ReactTestInstance = tree.getByA11yRole('label');
const getByA11yRole: ReactTestInstance = tree.getByA11yRole('button');
const getAllByA11yRole: Array<ReactTestInstance> = tree.getAllByA11yRole(
'label'
'button'
);
const queryByA11yRole: ReactTestInstance = tree.queryByA11yRole('label');
const queryByA11yRole: ReactTestInstance = tree.queryByA11yRole('button');
const queryAllByA11yRole: Array<ReactTestInstance> = tree.queryAllByA11yRole(
'label'
'button'
);
const getByA11yStates: ReactTestInstance = tree.getByA11yStates('label');
const getByA11yStatesArray: ReactTestInstance = tree.getByA11yStates(['label']);
const getByA11yStates: ReactTestInstance = tree.getByA11yStates('selected');
const getByA11yStatesArray: ReactTestInstance = tree.getByA11yStates(['selected']);
const getAllByA11yStates: Array<ReactTestInstance> = tree.getAllByA11yStates(
'label'
'selected'
);
const getAllByA11yStatesArray: Array<
ReactTestInstance
> = tree.getAllByA11yStates(['label']);
const queryByA11yStates: ReactTestInstance = tree.queryByA11yStates('label');
> = tree.getAllByA11yStates(['selected']);
const queryByA11yStates: ReactTestInstance = tree.queryByA11yStates('selected');
const queryByA11yStatesArray: ReactTestInstance = tree.queryByA11yStates([
'label',
'selected',
]);
const queryAllByA11yStates: Array<
ReactTestInstance
> = tree.queryAllByA11yStates('label');
> = tree.queryAllByA11yStates('selected');
const queryAllByA11yStatesArray: Array<
ReactTestInstance
> = tree.queryAllByA11yStates(['label']);
> = tree.queryAllByA11yStates(['selected']);

const getByA11yState: ReactTestInstance = tree.getByA11yState({ busy: true });
const getAllByA11yState: Array<ReactTestInstance> = tree.getAllByA11yState({ busy: true });
const queryByA11yState: ReactTestInstance = tree.queryByA11yState({ busy: true });
const queryAllByA11yState: Array<ReactTestInstance> = tree.queryAllByA11yState({ busy: true });

const debugFn = tree.debug();
const debugFnWithMessage = tree.debug('my message');
Expand Down
66 changes: 37 additions & 29 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { AccessibilityState, AccessibilityStates, AccessibilityRole } from 'react-native';
import { ReactTestInstance, ReactTestRendererJSON } from 'react-test-renderer';

export interface GetByAPI {
Expand Down Expand Up @@ -49,35 +50,42 @@ export interface QueryByAPI {
) => Array<ReactTestInstance> | [];
}

type QueryFn = (text: string | RegExp) => ReactTestInstance | null;
type QueryAllFn = (text: string | RegExp) => Array<ReactTestInstance> | [];
type GetFn = (text: string | RegExp) => ReactTestInstance;
type GetAllFn = (text: string | RegExp) => Array<ReactTestInstance>;
type ArrayQueryFn = (text: string | Array<string>) => ReactTestInstance | null;
type ArrayQueryAllFn = (
text: string | Array<string>
) => Array<ReactTestInstance> | [];
type ArrayGetFn = (text: string | Array<string>) => ReactTestInstance;
type ArrayGetAllFn = (text: string | Array<string>) => Array<ReactTestInstance>;

export interface A11yAPI {
getByA11yLabel: GetFn;
getAllByA11yLabel: GetAllFn;
queryByA11yLabel: QueryFn;
queryAllByA11yLabel: QueryAllFn;
getByA11yHint: GetFn;
getAllByA11yHint: GetAllFn;
queryByA11yHint: QueryFn;
queryAllByA11yHint: QueryAllFn;
getByA11yRole: GetFn;
getAllByA11yRole: GetAllFn;
queryByA11yRole: QueryFn;
queryAllByA11yRole: QueryAllFn;
getByA11yStates: ArrayGetFn;
getAllByA11yStates: ArrayGetAllFn;
queryByA11yStates: ArrayQueryFn;
queryAllByA11yStates: ArrayQueryAllFn;
}
type GetReturn = ReactTestInstance;
type GetAllReturn = Array<ReactTestInstance>;
type QueryReturn = ReactTestInstance | null;
type QueryAllReturn = Array<ReactTestInstance> | [];

type A11yAPI = {
// Label
getByA11yLabel: (matcher: string | RegExp) => GetReturn,
getAllByA11yLabel: (matcher: string | RegExp) => GetAllReturn,
queryByA11yLabel: (matcher: string | RegExp) => QueryReturn,
queryAllByA11yLabel: (matcher: string | RegExp) => QueryAllReturn,

// Hint
getByA11yHint: (matcher: string | RegExp) => GetReturn,
getAllByA11yHint: (matcher: string | RegExp) => GetAllReturn,
queryByA11yHint: (matcher: string | RegExp) => QueryReturn,
queryAllByA11yHint: (matcher: string | RegExp) => QueryAllReturn,

// Role
getByA11yRole: (matcher: AccessibilityRole | RegExp) => GetReturn,
getAllByA11yRole: (matcher: AccessibilityRole | RegExp) => GetAllReturn,
queryByA11yRole: (matcher: AccessibilityRole | RegExp) => QueryReturn,
queryAllByA11yRole: (matcher: AccessibilityRole | RegExp) => QueryAllReturn,

// States
getByA11yStates: (matcher: AccessibilityStates | Array<AccessibilityStates>) => GetReturn,
getAllByA11yStates: (matcher: AccessibilityStates | Array<AccessibilityStates>) => GetAllReturn,
queryByA11yStates: (matcher: AccessibilityStates | Array<AccessibilityStates>) => QueryReturn,
queryAllByA11yStates: (matcher: AccessibilityStates | Array<AccessibilityStates>) => QueryAllReturn,

// State
getByA11yState: (matcher: AccessibilityState) => GetReturn,
getAllByA11yState: (matcher: AccessibilityState) => GetAllReturn,
queryByA11yState: (matcher: AccessibilityState) => QueryReturn,
queryAllByA11yState: (matcher: AccessibilityState) => QueryAllReturn,
};

export interface Thenable {
then: (resolve: () => any, reject?: () => any) => any;
Expand Down