From 66e540dd0d7924c3d498a4203060d3c41b5062e3 Mon Sep 17 00:00:00 2001 From: anishapm Date: Thu, 28 Sep 2023 12:42:07 +0200 Subject: [PATCH 1/7] feat: toHaveAccessibleName matcher --- src/helpers/accessiblity.ts | 33 +++++- src/helpers/matchers/accessibilityName.ts | 18 +++ .../to-have-accessible-name.test.tsx | 111 ++++++++++++++++++ src/matchers/extend-expect.d.ts | 1 + src/matchers/extend-expect.ts | 2 + src/matchers/index.ts | 1 + src/matchers/to-have-accessible-name.tsx | 43 +++++++ 7 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/helpers/matchers/accessibilityName.ts create mode 100644 src/matchers/__tests__/to-have-accessible-name.test.tsx create mode 100644 src/matchers/to-have-accessible-name.tsx diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index 7419d3356..b64644a0c 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -4,7 +4,7 @@ import { StyleSheet, } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; -import { getHostSiblings } from './component-tree'; +import { getHostSiblings, getHostParent } from './component-tree'; import { getHostComponentNames } from './host-component-names'; type IsInaccessibleOptions = { @@ -233,3 +233,34 @@ export function isElementSelected( const { accessibilityState, 'aria-selected': ariaSelected } = element.props; return ariaSelected ?? accessibilityState?.selected ?? false; } + +export function getAccessibleName( + element: ReactTestInstance +): string | undefined { + const labelTextFromLabel = getAccessibilityLabel(element); + const labelTextFromLabelledBy = getAccessibilityLabelledBy(element); + + if (labelTextFromLabelledBy) { + const parentElement = getHostParent(element); + + if (parentElement) { + const labelElement = parentElement.findByProps({ + nativeID: labelTextFromLabelledBy, + }); + + if (labelElement) { + // Extract and return the text content of the label element + const label = labelElement.props.children; + return label; + } + } + } + + if (labelTextFromLabel) { + return labelTextFromLabel; + } + + const ownText = element?.children?.join(' '); + + return ownText || undefined; +} diff --git a/src/helpers/matchers/accessibilityName.ts b/src/helpers/matchers/accessibilityName.ts new file mode 100644 index 000000000..e90d2bcca --- /dev/null +++ b/src/helpers/matchers/accessibilityName.ts @@ -0,0 +1,18 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { getAccessibleName } from '../accessiblity'; +import { TextMatchOptions, matches, TextMatch } from '../../matches'; + +export function matchAccessibleName( + node: ReactTestInstance, + expectedName?: TextMatch, + normalizer?: TextMatchOptions['normalizer'], + exact?: TextMatchOptions['exact'] +): boolean { + const accessibleName = getAccessibleName(node); + + if (expectedName) { + return matches(expectedName, accessibleName, normalizer, exact); + } + + return !!accessibleName; +} diff --git a/src/matchers/__tests__/to-have-accessible-name.test.tsx b/src/matchers/__tests__/to-have-accessible-name.test.tsx new file mode 100644 index 000000000..482279604 --- /dev/null +++ b/src/matchers/__tests__/to-have-accessible-name.test.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import { View, Text, TextInput } from 'react-native'; +import { render, screen } from '../..'; +import '../extend-expect'; + +test('toHaveAccessibleName() on view with accessibilityLabel prop', () => { + render(); + const element = screen.getByTestId('accessibility-label'); + expect(element).toHaveAccessibleName('Test Label'); +}); + +test('toHaveAccessibleName() on view with aria-label prop', () => { + render(); + const element = screen.getByTestId('aria-label'); + expect(element).toHaveAccessibleName('Aria Test Label'); +}); + +test('toHaveAccessibleName() on view with accessibilityLabel prop with no expectedName', () => { + render(); + const element = screen.getByTestId('no-expectName-label'); + expect(element).toHaveAccessibleName(); +}); + +test('toHaveAccessibleName() on view with no accessibility props', () => { + render(Text); + const element = screen.getByTestId('accessibility-label'); + expect(element).toHaveAccessibleName('Text'); +}); + +test('toHaveAccessibleName() on view with that does not have the expected accessible name', () => { + render(); + const element = screen.getByTestId('wrong-label'); + expect(() => expect(element).toHaveAccessibleName('Not the label')) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toHaveAccessibleName() + + Expected element to have accessible name: + Not the label + Received: + The actual label" +`); +}); + +test('toHaveAccessibleName() on view that doesnt have accessible name defined', () => { + render(); + const element = screen.getByTestId('no-accessibile-name'); + + expect(() => expect(element).toHaveAccessibleName()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toHaveAccessibleName() + + Expected element to have accessible name: + + Received: + undefined" + `); +}); + +it('toHaveAccessibleName() on a non-host element', () => { + const nonElement = 'This is not a ReactTestInstance'; + expect(() => expect(nonElement).toHaveAccessibleName()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(received).toHaveAccessibleName() + + received value must be a host element. + Received has type: string + Received has value: "This is not a ReactTestInstance"" + `); +}); + +test('toHaveAccessibleName() on view with accessibilityLabelledBy prop', async () => { + render( + + Accessibility LabelledBy + + + ); + + const element = screen.getByTestId('accessibility-labelledby'); + expect(element).toHaveAccessibleName('Accessibility LabelledBy'); +}); + +test('toHaveAccessibleName() on view with ariaLabelledBy prop', async () => { + render( + + Aria LabelledBy + + + ); + + const element = screen.getByTestId('aria-labelledby'); + expect(element).toHaveAccessibleName('Aria LabelledBy'); +}); + +//TODO: This fails as expected as I am unsure on how to extract the Text from the nested Element +test('getByLabelText supports nested aria-labelledby', async () => { + const screen = render( + <> + + Nested Aria LabelledBy + + + + ); + + const element = screen.getByTestId('text-input'); + expect(element).toHaveAccessibleName('Nested Aria LabelledBy'); +}); diff --git a/src/matchers/extend-expect.d.ts b/src/matchers/extend-expect.d.ts index 55cb2df53..c7129df45 100644 --- a/src/matchers/extend-expect.d.ts +++ b/src/matchers/extend-expect.d.ts @@ -22,6 +22,7 @@ export interface JestNativeMatchers { toHaveProp(name: string, expectedValue?: unknown): R; toHaveStyle(style: StyleProp