Skip to content

feat: *ByRole a11y value option #1210

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
Nov 15, 2022
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
2 changes: 1 addition & 1 deletion src/helpers/__tests__/accessiblity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test('returns false for accessible elements', () => {
).toBe(false);
});

test('returns true for hidden elements', () => {
test('returns true for null elements', () => {
expect(isHiddenFromAccessibility(null)).toBe(true);
});

Expand Down
17 changes: 13 additions & 4 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { AccessibilityState, StyleSheet } from 'react-native';
import {
AccessibilityState,
AccessibilityValue,
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[] = [
export const accessibilityStateKeys: (keyof AccessibilityState)[] = [
'disabled',
'selected',
'checked',
'busy',
'expanded',
];

export const accessiblityValueKeys: (keyof AccessibilityValue)[] = [
'min',
'max',
'now',
'text',
];

export function isHiddenFromAccessibility(
element: ReactTestInstance | null,
{ cache }: IsInaccessibleOptions = {}
Expand Down
24 changes: 24 additions & 0 deletions src/helpers/matchers/accessibilityValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AccessibilityValue } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
import { TextMatch } from '../../matches';
import { matchStringProp } from './matchStringProp';

export interface AccessibilityValueMatcher {
min?: number;
max?: number;
now?: number;
text?: TextMatch;
}

export function matchAccessibilityValue(
node: ReactTestInstance,
matcher: AccessibilityValueMatcher
): boolean {
const value: AccessibilityValue = node.props.accessibilityValue ?? {};
return (
(matcher.min === undefined || matcher.min === value.min) &&
(matcher.max === undefined || matcher.max === value.max) &&
(matcher.now === undefined || matcher.now === value.now) &&
(matcher.text === undefined || matchStringProp(value.text, matcher.text))
);
}
50 changes: 33 additions & 17 deletions src/queries/__tests__/a11yValue.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, TouchableOpacity } from 'react-native';
import { render } from '../..';

const TEXT_LABEL = 'cool text';

const getMultipleInstancesFoundMessage = (value: string) => {
return `Found multiple elements with accessibilityValue: ${value}`;
};

const getNoInstancesFoundMessage = (value: string) => {
return `Unable to find an element with accessibilityValue: ${value}`;
};

const Typography = ({ children, ...rest }: any) => {
return <Text {...rest}>{children}</Text>;
};
Expand Down Expand Up @@ -46,15 +38,15 @@ test('getByA11yValue, queryByA11yValue, findByA11yValue', async () => {
});

expect(() => getByA11yValue({ min: 50 })).toThrow(
getNoInstancesFoundMessage('{"min":50}')
'Unable to find an element with min value: 50'
);
expect(queryByA11yValue({ min: 50 })).toEqual(null);

expect(() => getByA11yValue({ max: 60 })).toThrow(
getMultipleInstancesFoundMessage('{"max":60}')
'Found multiple elements with max value: 60'
);
expect(() => queryByA11yValue({ max: 60 })).toThrow(
getMultipleInstancesFoundMessage('{"max":60}')
'Found multiple elements with max value: 60'
);

const asyncElement = await findByA11yValue({ min: 40 });
Expand All @@ -63,10 +55,10 @@ test('getByA11yValue, queryByA11yValue, findByA11yValue', async () => {
max: 60,
});
await expect(findByA11yValue({ min: 50 })).rejects.toThrow(
getNoInstancesFoundMessage('{"min":50}')
'Unable to find an element with min value: 50'
);
await expect(findByA11yValue({ max: 60 })).rejects.toThrow(
getMultipleInstancesFoundMessage('{"max":60}')
'Found multiple elements with max value: 60'
);
});

Expand All @@ -79,7 +71,7 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => {
expect(queryAllByA11yValue({ min: 40 })).toHaveLength(1);

expect(() => getAllByA11yValue({ min: 50 })).toThrow(
getNoInstancesFoundMessage('{"min":50}')
'Unable to find an element with min value: 50'
);
expect(queryAllByA11yValue({ min: 50 })).toEqual([]);

Expand All @@ -88,7 +80,7 @@ test('getAllByA11yValue, queryAllByA11yValue, findAllByA11yValue', async () => {

await expect(findAllByA11yValue({ min: 40 })).resolves.toHaveLength(1);
await expect(findAllByA11yValue({ min: 50 })).rejects.toThrow(
getNoInstancesFoundMessage('{"min":50}')
'Unable to find an element with min value: 50'
);
await expect(findAllByA11yValue({ max: 60 })).resolves.toHaveLength(2);
});
Expand All @@ -111,6 +103,30 @@ test('byA11yValue queries support hidden option', () => {
expect(() =>
getByA11yValue({ max: 10 }, { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with accessibilityValue: {"max":10}"`
`"Unable to find an element with max value: 10"`
);
});

test('byA11yValue error messages', () => {
const { getByA11yValue } = render(<View />);
expect(() =>
getByA11yValue({ min: 10, max: 10 })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with min value: 10, max value: 10"`
);
expect(() =>
getByA11yValue({ max: 20, now: 5 })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with max value: 20, now value: 5"`
);
expect(() =>
getByA11yValue({ min: 1, max: 2, now: 3 })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with min value: 1, max value: 2, now value: 3"`
);
expect(() =>
getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i"`
);
});
101 changes: 101 additions & 0 deletions src/queries/__tests__/role-value.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from 'react';
import { View, Text } from 'react-native';
import { render } from '../..';

describe('accessibility value', () => {
test('matches using all value props', () => {
const { getByRole, queryByRole } = render(
<View
accessibilityRole="adjustable"
accessibilityValue={{ min: 0, max: 100, now: 50, text: '50%' }}
/>
);

expect(
getByRole('adjustable', {
value: { min: 0, max: 100, now: 50, text: '50%' },
})
).toBeTruthy();
expect(
queryByRole('adjustable', {
value: { min: 1, max: 100, now: 50, text: '50%' },
})
).toBeFalsy();
expect(
queryByRole('adjustable', {
value: { min: 0, max: 99, now: 50, text: '50%' },
})
).toBeFalsy();
expect(
queryByRole('adjustable', {
value: { min: 0, max: 100, now: 45, text: '50%' },
})
).toBeFalsy();
expect(
queryByRole('adjustable', {
value: { min: 0, max: 100, now: 50, text: '55%' },
})
).toBeFalsy();
});

test('matches using single value', () => {
const { getByRole, queryByRole } = render(
<View
accessibilityRole="adjustable"
accessibilityValue={{ min: 10, max: 20, now: 12, text: 'Hello' }}
/>
);

expect(getByRole('adjustable', { value: { min: 10 } })).toBeTruthy();
expect(getByRole('adjustable', { value: { max: 20 } })).toBeTruthy();
expect(getByRole('adjustable', { value: { now: 12 } })).toBeTruthy();
expect(getByRole('adjustable', { value: { text: 'Hello' } })).toBeTruthy();
expect(getByRole('adjustable', { value: { text: /hello/i } })).toBeTruthy();

expect(queryByRole('adjustable', { value: { min: 11 } })).toBeFalsy();
expect(queryByRole('adjustable', { value: { max: 19 } })).toBeFalsy();
expect(queryByRole('adjustable', { value: { now: 15 } })).toBeFalsy();
expect(queryByRole('adjustable', { value: { text: 'No' } })).toBeFalsy();
expect(queryByRole('adjustable', { value: { text: /no/ } })).toBeFalsy();
});

test('matches using single value and other options', () => {
const { getByRole } = render(
<Text
accessibilityRole="adjustable"
accessibilityState={{ disabled: true }}
accessibilityValue={{ min: 10, max: 20, now: 12, text: 'Hello' }}
>
Hello
</Text>
);

expect(
getByRole('adjustable', { name: 'Hello', value: { min: 10 } })
).toBeTruthy();
expect(
getByRole('adjustable', { disabled: true, value: { min: 10 } })
).toBeTruthy();

expect(() =>
getByRole('adjustable', { name: 'Hello', value: { min: 5 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"`
);
expect(() =>
getByRole('adjustable', { name: 'World', value: { min: 10 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", name: "World", min value: 10"`
);
expect(() =>
getByRole('adjustable', { name: 'Hello', value: { min: 5 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"`
);
expect(() =>
getByRole('adjustable', { selected: true, value: { min: 10 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", selected state: true, min value: 10"`
);
});
});
18 changes: 18 additions & 0 deletions src/queries/__tests__/role.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,24 @@ describe('error messages', () => {
`"Unable to find an element with role: "button", disabled state: true"`
);
});

test('gives a descriptive error message when querying with a role and an accessibility value', () => {
const { getByRole } = render(<View />);

expect(() =>
getByRole('adjustable', { value: { min: 1 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", min value: 1"`
);

expect(() =>
getByRole('adjustable', {
value: { min: 1, max: 2, now: 1, text: /hello/ },
})
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/"`
);
});
});

test('byRole queries support hidden option', () => {
Expand Down
Loading