From b0194791ad4bfaa2da796f310443409092a93626 Mon Sep 17 00:00:00 2001 From: Jovan Goh <54385240+jovanimal@users.noreply.github.com> Date: Tue, 7 Nov 2023 05:18:14 +0800 Subject: [PATCH 1/8] fix: change broken hyperlink in README.md (#1520) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be93e3103..dd89dd711 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ test('form submits two answers', () => { }); ``` -You can find the source of `QuestionsBoard` component and this example [here](https://github.com/callstack/react-native-testing-library/blob/main/src/__tests__/questionsBoard.test.js). +You can find the source of `QuestionsBoard` component and this example [here](https://github.com/callstack/react-native-testing-library/blob/main/src/__tests__/questionsBoard.test.tsx). ## API / Usage From 2ada536829e2c7439274e49de797654d01f582e1 Mon Sep 17 00:00:00 2001 From: Anisha Malde <39306477+anishamalde@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:44:51 +0000 Subject: [PATCH 2/8] feat: toHaveAccessibleName matcher (#1509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: toHaveAccessibleName matcher * feat: toHaveAccessibleName matcher * feat: toHaveAccessibleName matcher * refactor: check logic * chore: fix lint * refactor: tests * chore: test tweaks --------- Co-authored-by: Maciej Jastrzębski --- src/helpers/accessiblity.ts | 23 ++- .../to-have-accessible-name.test.tsx | 134 ++++++++++++++++++ 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 | 53 +++++++ src/matchers/utils.tsx | 2 +- 7 files changed, 214 insertions(+), 2 deletions(-) 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..aed19ced6 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -4,7 +4,8 @@ import { StyleSheet, } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; -import { getHostSiblings } from './component-tree'; +import { getTextContent } from './text-content'; +import { getHostSiblings, getUnsafeRootElement } from './component-tree'; import { getHostComponentNames } from './host-component-names'; type IsInaccessibleOptions = { @@ -233,3 +234,23 @@ export function isElementSelected( const { accessibilityState, 'aria-selected': ariaSelected } = element.props; return ariaSelected ?? accessibilityState?.selected ?? false; } + +export function getAccessibleName( + element: ReactTestInstance +): string | undefined { + const label = getAccessibilityLabel(element); + if (label) { + return label; + } + + const labelElementId = getAccessibilityLabelledBy(element); + if (labelElementId) { + const rootElement = getUnsafeRootElement(element); + const labelElement = rootElement?.findByProps({ nativeID: labelElementId }); + if (labelElement) { + return getTextContent(labelElement); + } + } + + return getTextContent(element); +} 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..1a355efc7 --- /dev/null +++ b/src/matchers/__tests__/to-have-accessible-name.test.tsx @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { View, Text, TextInput } from 'react-native'; +import { render, screen } from '../..'; +import '../extend-expect'; + +test('toHaveAccessibleName() handles view with "accessibilityLabel" prop', () => { + render(); + const element = screen.getByTestId('view'); + expect(element).toHaveAccessibleName('Test label'); + expect(element).not.toHaveAccessibleName('Other label'); +}); + +test('toHaveAccessibleName() handles view with "aria-label" prop', () => { + render(); + const element = screen.getByTestId('view'); + expect(element).toHaveAccessibleName('Test label'); + expect(element).not.toHaveAccessibleName('Other label'); +}); + +test('toHaveAccessibleName() handles view with "accessibilityLabelledBy" prop', async () => { + render( + + External label + + + ); + + const element = screen.getByTestId('input'); + expect(element).toHaveAccessibleName('External label'); + expect(element).not.toHaveAccessibleName('Other label'); +}); + +test('toHaveAccessibleName() handles nested "accessibilityLabelledBy"', async () => { + render( + <> + + External label + + + + ); + + const element = screen.getByTestId('input'); + expect(element).toHaveAccessibleName('External label'); + expect(element).not.toHaveAccessibleName('Other label'); +}); + +test('toHaveAccessibleName() handles view with nested "accessibilityLabelledBy" with no text', async () => { + render( + <> + + + + + + ); + + const element = screen.getByTestId('text-input'); + expect(element).not.toHaveAccessibleName(); +}); + +test('toHaveAccessibleName() handles view with "aria-labelledby" prop', async () => { + render( + + External label + + + ); + + const element = screen.getByTestId('input'); + expect(element).toHaveAccessibleName('External label'); + expect(element).not.toHaveAccessibleName('Other label'); +}); + +test('toHaveAccessibleName() handles view with implicit accessible name', () => { + render(Text); + const element = screen.getByTestId('view'); + expect(element).toHaveAccessibleName('Text'); + expect(element).not.toHaveAccessibleName('Other text'); +}); + +test('toHaveAccessibleName() supports calling without expected name', () => { + render(); + const element = screen.getByTestId('view'); + + expect(element).toHaveAccessibleName(); + expect(() => expect(element).not.toHaveAccessibleName()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).not.toHaveAccessibleName() + + Expected element not to have accessible name: + undefined + Received: + Test label" + `); +}); + +test('toHaveAccessibleName() handles a view without name when called without expected name', () => { + render(); + const element = screen.getByTestId('view'); + + expect(element).not.toHaveAccessibleName(); + expect(() => expect(element).toHaveAccessibleName()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(element).toHaveAccessibleName() + + Expected element to have accessible name: + undefined + Received: + " + `); +}); + +it('toHaveAccessibleName() rejects 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"" + `); + + expect(() => expect(nonElement).not.toHaveAccessibleName()) + .toThrowErrorMatchingInlineSnapshot(` + "expect(received).not.toHaveAccessibleName() + + received value must be a host element. + Received has type: string + Received has value: "This is not a ReactTestInstance"" + `); +}); diff --git a/src/matchers/extend-expect.d.ts b/src/matchers/extend-expect.d.ts index 55cb2df53..7d76db0c1 100644 --- a/src/matchers/extend-expect.d.ts +++ b/src/matchers/extend-expect.d.ts @@ -18,6 +18,7 @@ export interface JestNativeMatchers { toBeVisible(): R; toContainElement(element: ReactTestInstance | null): R; toHaveAccessibilityValue(expectedValue: AccessibilityValueMatcher): R; + toHaveAccessibleName(expectedName?: TextMatch, options?: TextMatchOptions): R; toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R; toHaveProp(name: string, expectedValue?: unknown): R; toHaveStyle(style: StyleProp