Skip to content

feat: option for queries to respect accessibility #1064

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 32 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fc1f724
chore: fix typo
Oct 8, 2022
de92da5
feat: add options to isInaccessible
Oct 8, 2022
13b0d73
feat: implement new findAll helper including hidden option
Oct 8, 2022
be19318
feat: use new findAll with hiden option in all queries
Aug 25, 2022
f0c9e56
test: add tests for the queries to check hidden option support
Oct 14, 2022
42f04e6
chore: fix typo
Oct 28, 2022
b188843
feat: use global config for default hidden
Oct 28, 2022
56e3b20
refactor: simplify cache handling for accessibiilty
Oct 29, 2022
102664a
refactor: correct cache default
Nov 3, 2022
726dd72
refactor: rename options file
Nov 3, 2022
a88ac5d
refactor: rename AccessibiilityOption to BaseOptions
Nov 3, 2022
8d71039
refactor: move TextMatchOptiond'
Nov 3, 2022
8abf657
fixup! feat: implement new findAll helper including hidden option
Nov 3, 2022
fb830bd
refactor: simplify naming in findAll
Nov 3, 2022
97225d0
refactor: simplify options syntax for findAll'
Nov 3, 2022
0971813
refactor: renaming in findAll args
Nov 3, 2022
e9bebb6
refactor: clean type ByRoleOptions
Nov 3, 2022
f0d6244
refactor: rename type BaseOptions to CommonQueryoptions
Nov 3, 2022
18bca34
chore: remove deprecated common
Nov 3, 2022
58816b0
refactor: separate query and findAll types
Nov 3, 2022
32651fa
fix: add options to flow types for queryByTestId
Nov 3, 2022
61a11f8
feat: update flow types
Nov 3, 2022
b7824d9
test: fix config test
Nov 3, 2022
c97446e
doc: add doc on hidden option
Nov 4, 2022
63f3e86
doc: correct docs for hidden option
Nov 4, 2022
ee4addd
doc: add doc for configure api
Nov 4, 2022
eec0c1b
doc: few tweaks to hidden option
Nov 4, 2022
8557eaa
refactor: rename hidden global option to defaultHidden
Nov 4, 2022
7e869de
refactor: formatting
mdjastrzebski Nov 4, 2022
b9d6404
refactor: final tweaks
mdjastrzebski Nov 4, 2022
66b4fcb
chore: run prettier
mdjastrzebski Nov 4, 2022
dbccaf5
docs: add hidden option to queries doc
mdjastrzebski Nov 4, 2022
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
1 change: 1 addition & 0 deletions src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ test('configure() overrides existing config values', () => {
expect(getConfig()).toEqual({
asyncUtilTimeout: 5000,
defaultDebugOptions: { message: 'debug message' },
hidden: true,
});
});

Expand Down
4 changes: 3 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { DebugOptions } from './helpers/debugDeep';
export type Config = {
/** Default timeout, in ms, for `waitFor` and `findBy*` queries. */
asyncUtilTimeout: number;

/** Default options for `debug` helper. */
defaultDebugOptions?: Partial<DebugOptions>;
/** Default hidden value for all queries */
hidden: boolean;
};

const defaultConfig: Config = {
asyncUtilTimeout: 1000,
hidden: true,
};

let config = {
Expand Down
24 changes: 20 additions & 4 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { AccessibilityState, StyleSheet } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
import { getHostSiblings } from './component-tree';

type IsInaccessibleOptions = {
cache?: WeakMap<ReactTestInstance, boolean>;
};

export type AccessibilityStateKey = keyof AccessibilityState;

export const accessibilityStateKeys: AccessibilityStateKey[] = [
Expand All @@ -12,14 +16,24 @@ export const accessibilityStateKeys: AccessibilityStateKey[] = [
'expanded',
];

export function isInaccessible(element: ReactTestInstance | null): boolean {
export function isInaccessible(
element: ReactTestInstance | null,
{ cache }: IsInaccessibleOptions = {}
): boolean {
if (element == null) {
return true;
}

let current: ReactTestInstance | null = element;
while (current) {
if (isSubtreeInaccessible(current)) {
let isCurrentSubtreeInaccessible = cache?.get(current);

if (isCurrentSubtreeInaccessible === undefined) {
isCurrentSubtreeInaccessible = isSubtreeInaccessible(current);
cache?.set(current, isCurrentSubtreeInaccessible);
}

if (isCurrentSubtreeInaccessible) {
return true;
}

Expand All @@ -29,7 +43,9 @@ export function isInaccessible(element: ReactTestInstance | null): boolean {
return false;
}

function isSubtreeInaccessible(element: ReactTestInstance | null): boolean {
export function isSubtreeInaccessible(
element: ReactTestInstance | null
): boolean {
if (element == null) {
return true;
}
Expand All @@ -46,7 +62,7 @@ function isSubtreeInaccessible(element: ReactTestInstance | null): boolean {
return true;
}

// Note that `opacity: 0` is not threated as inassessible on iOS
// Note that `opacity: 0` is not treated as inaccessible on iOS
const flatStyle = StyleSheet.flatten(element.props.style) ?? {};
if (flatStyle.display === 'none') return true;

Expand Down
24 changes: 24 additions & 0 deletions src/helpers/findAll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ReactTestInstance } from 'react-test-renderer';
import { getConfig } from '../config';
import { isInaccessible } from './accessiblity';

interface FindAllOptions {
hidden?: boolean;
}

export function findAll(
root: ReactTestInstance,
predicate: (node: ReactTestInstance) => boolean,
options?: FindAllOptions
) {
const results = root.findAll(predicate);

const hidden = options?.hidden ?? getConfig().hidden;

if (hidden) {
return results;
}

const cache = new WeakMap<ReactTestInstance>();
return results.filter((element) => !isInaccessible(element, { cache }));
}
17 changes: 17 additions & 0 deletions src/queries/__tests__/a11yState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,20 @@ test('*ByA11yState on TouchableOpacity with "disabled" prop', () => {
expect(view.getByA11yState({ disabled: true })).toBeTruthy();
expect(view.queryByA11yState({ disabled: false })).toBeFalsy();
});

test('byA11yState queries support hidden option', () => {
const { queryByA11yState, getByA11yState } = render(
<TouchableOpacity
accessibilityState={{ expanded: false }}
style={{ display: 'none' }}
>
<Text>I am inaccessible</Text>
</TouchableOpacity>
);
expect(queryByA11yState({ expanded: false }, { hidden: false })).toBeFalsy();
expect(() =>
getByA11yState({ expanded: false }, { hidden: false })
).toThrow();
expect(queryByA11yState({ expanded: false }, { hidden: true })).toBeTruthy();
expect(getByA11yState({ expanded: false }, { hidden: true })).toBeTruthy();
});
12 changes: 12 additions & 0 deletions src/queries/__tests__/a11yValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,15 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => {
);
await expect(findAllByA11yValue({ max: 60 })).resolves.toHaveLength(2);
});

test('byA11yValue queries support hidden option', () => {
const { queryByA11yValue, getByA11yValue } = render(
<Text accessibilityValue={{ max: 10 }} style={{ display: 'none' }}>
I am inaccessible
</Text>
);
expect(queryByA11yValue({ max: 10 }, { hidden: false })).toBeFalsy();
expect(() => getByA11yValue({ max: 10 }, { hidden: false })).toThrow();
expect(queryByA11yValue({ max: 10 }, { hidden: true })).toBeTruthy();
expect(getByA11yValue({ max: 10 }, { hidden: true })).toBeTruthy();
});
14 changes: 14 additions & 0 deletions src/queries/__tests__/displayValue.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,17 @@ test('findBy queries work asynchronously', async () => {
await expect(findByDisplayValue('Display Value')).resolves.toBeTruthy();
await expect(findAllByDisplayValue('Display Value')).resolves.toHaveLength(1);
}, 20000);

test('byDisplayValue queries support hidden option', () => {
const { queryByDisplayValue, getByDisplayValue } = render(
<TextInput value="im-inaccessible" style={{ display: 'none' }}>
I am inaccessible
</TextInput>
);
expect(queryByDisplayValue('im-inaccessible', { hidden: false })).toBeFalsy();
expect(() =>
getByDisplayValue('im-inaccessible', { hidden: false })
).toThrow();
expect(queryByDisplayValue('im-inaccessible', { hidden: true })).toBeTruthy();
expect(getByDisplayValue('im-inaccessible', { hidden: true })).toBeTruthy();
});
12 changes: 12 additions & 0 deletions src/queries/__tests__/hintText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,15 @@ test('getByHintText, getByHintText and exact = true', () => {
expect(queryByHintText('id', { exact: true })).toBeNull();
expect(getAllByHintText('test', { exact: true })).toHaveLength(1);
});

test('byHintText queries support hidden option', () => {
const { queryByHintText, getByHintText } = render(
<Text accessibilityHint="im-inaccessible" style={{ display: 'none' }}>
I am inaccessible
</Text>
);
expect(queryByHintText('im-inaccessible', { hidden: false })).toBeFalsy();
expect(() => getByHintText('im-inaccessible', { hidden: false })).toThrow();
expect(queryByHintText('im-inaccessible', { hidden: true })).toBeTruthy();
expect(getByHintText('im-inaccessible', { hidden: true })).toBeTruthy();
});
12 changes: 12 additions & 0 deletions src/queries/__tests__/labelText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,15 @@ describe('findBy options deprecations', () => {
);
}, 20000);
});

test('byLabelText queries support hidden option', () => {
const { queryByLabelText, getByLabelText } = render(
<Text accessibilityLabel="im-inaccessible" style={{ display: 'none' }}>
I am inaccessible
</Text>
);
expect(queryByLabelText('im-inaccessible', { hidden: false })).toBeFalsy();
expect(() => getByLabelText('im-inaccessible', { hidden: false })).toThrow();
expect(queryByLabelText('im-inaccessible', { hidden: true })).toBeTruthy();
expect(getByLabelText('im-inaccessible', { hidden: true })).toBeTruthy();
});
20 changes: 20 additions & 0 deletions src/queries/__tests__/placeholderText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,23 @@ test('getAllByPlaceholderText, queryAllByPlaceholderText', () => {
expect(queryAllByPlaceholderText(/fresh/i)).toEqual(inputs);
expect(queryAllByPlaceholderText('no placeholder')).toHaveLength(0);
});

test('byPlaceholderText queries support hidden option', () => {
const { queryByPlaceholderText, getByPlaceholderText } = render(
<TextInput
placeholder={PLACEHOLDER_CHEF}
value={INPUT_CHEF}
style={{ display: 'none' }}
/>
);
expect(
queryByPlaceholderText(PLACEHOLDER_CHEF, { hidden: false })
).toBeFalsy();
expect(() =>
getByPlaceholderText(PLACEHOLDER_CHEF, { hidden: false })
).toThrow();
expect(
queryByPlaceholderText(PLACEHOLDER_CHEF, { hidden: true })
).toBeTruthy();
expect(getByPlaceholderText(PLACEHOLDER_CHEF, { hidden: true })).toBeTruthy();
});
12 changes: 12 additions & 0 deletions src/queries/__tests__/role.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -697,3 +697,15 @@ describe('error messages', () => {
);
});
});

test('byRole queries support hidden option', () => {
const { queryByRole, getByRole } = render(
<TouchableOpacity accessibilityRole="button" style={{ display: 'none' }}>
<Text>I am inaccessible</Text>
</TouchableOpacity>
);
expect(queryByRole('button', { hidden: false })).toBeFalsy();
expect(() => getByRole('button', { hidden: false })).toThrow();
expect(queryByRole('button', { hidden: true })).toBeTruthy();
expect(getByRole('button', { hidden: true })).toBeTruthy();
});
13 changes: 13 additions & 0 deletions src/queries/__tests__/testId.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,16 @@ test('findByTestId and findAllByTestId work asynchronously', async () => {
await expect(findByTestId('aTestId')).resolves.toBeTruthy();
await expect(findAllByTestId('aTestId')).resolves.toHaveLength(1);
}, 20000);

test('byTestId queries support hidden option', () => {
const { queryByTestId, getByTestId } = render(
<Text style={{ display: 'none' }} testID="im-inaccessible">
I am inaccessible
</Text>
);

expect(queryByTestId('im-inaccessible', { hidden: false })).toBeFalsy();
expect(() => getByTestId('im-inaccessible', { hidden: false })).toThrow();
expect(queryByTestId('im-inaccessible', { hidden: true })).toBeTruthy();
expect(getByTestId('im-inaccessible', { hidden: true })).toBeTruthy();
});
10 changes: 10 additions & 0 deletions src/queries/__tests__/text.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,13 @@ test('getByText searches for text within self host element', () => {
const textNode = within(getByTestId('subject'));
expect(textNode.getByText('Hello')).toBeTruthy();
});

test('byText support hidden option', () => {
const { queryByText, getByText } = render(
<Text style={{ display: 'none' }}>I am inaccessible</Text>
);
expect(queryByText('I am inaccessible', { hidden: false })).toBeFalsy();
expect(() => getByText('I am inaccessible', { hidden: false })).toThrow();
expect(queryByText('I am inaccessible', { hidden: true })).toBeTruthy();
expect(getByText('I am inaccessible', { hidden: true })).toBeTruthy();
});
53 changes: 36 additions & 17 deletions src/queries/a11yState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ReactTestInstance } from 'react-test-renderer';
import type { AccessibilityState } from 'react-native';
import { AccessibilityState } from 'react-native';
import { accessibilityStateKeys } from '../helpers/accessiblity';
import { matchAccessibilityState } from '../helpers/matchers/accessibilityState';
import { findAll } from '../helpers/findAll';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
Expand All @@ -11,14 +12,20 @@ import type {
QueryAllByQuery,
QueryByQuery,
} from './makeQueries';
import { CommonQueryOptions } from './options';

const queryAllByA11yState = (
instance: ReactTestInstance
): ((matcher: AccessibilityState) => Array<ReactTestInstance>) =>
function queryAllByA11yStateFn(matcher) {
return instance.findAll(
): ((
matcher: AccessibilityState,
queryOptions?: CommonQueryOptions
) => Array<ReactTestInstance>) =>
function queryAllByA11yStateFn(matcher, queryOptions) {
return findAll(
instance,
(node) =>
typeof node.type === 'string' && matchAccessibilityState(node, matcher)
typeof node.type === 'string' && matchAccessibilityState(node, matcher),
queryOptions
);
};

Expand Down Expand Up @@ -47,19 +54,31 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
);

export type ByA11yStateQueries = {
getByA11yState: GetByQuery<AccessibilityState>;
getAllByA11yState: GetAllByQuery<AccessibilityState>;
queryByA11yState: QueryByQuery<AccessibilityState>;
queryAllByA11yState: QueryAllByQuery<AccessibilityState>;
findByA11yState: FindByQuery<AccessibilityState>;
findAllByA11yState: FindAllByQuery<AccessibilityState>;
getByA11yState: GetByQuery<AccessibilityState, CommonQueryOptions>;
getAllByA11yState: GetAllByQuery<AccessibilityState, CommonQueryOptions>;
queryByA11yState: QueryByQuery<AccessibilityState, CommonQueryOptions>;
queryAllByA11yState: QueryAllByQuery<AccessibilityState, CommonQueryOptions>;
findByA11yState: FindByQuery<AccessibilityState, CommonQueryOptions>;
findAllByA11yState: FindAllByQuery<AccessibilityState, CommonQueryOptions>;

getByAccessibilityState: GetByQuery<AccessibilityState>;
getAllByAccessibilityState: GetAllByQuery<AccessibilityState>;
queryByAccessibilityState: QueryByQuery<AccessibilityState>;
queryAllByAccessibilityState: QueryAllByQuery<AccessibilityState>;
findByAccessibilityState: FindByQuery<AccessibilityState>;
findAllByAccessibilityState: FindAllByQuery<AccessibilityState>;
getByAccessibilityState: GetByQuery<AccessibilityState, CommonQueryOptions>;
getAllByAccessibilityState: GetAllByQuery<
AccessibilityState,
CommonQueryOptions
>;
queryByAccessibilityState: QueryByQuery<
AccessibilityState,
CommonQueryOptions
>;
queryAllByAccessibilityState: QueryAllByQuery<
AccessibilityState,
CommonQueryOptions
>;
findByAccessibilityState: FindByQuery<AccessibilityState, CommonQueryOptions>;
findAllByAccessibilityState: FindAllByQuery<
AccessibilityState,
CommonQueryOptions
>;
};

export const bindByA11yStateQueries = (
Expand Down
Loading