From e61a42a5b78c925f56caf8a5804023e762fed187 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 14:57:33 +0200 Subject: [PATCH 01/11] refactor: detect Image host component name --- src/__tests__/host-component-names.test.tsx | 8 +++++++- src/config.ts | 1 + src/helpers/host-component-names.tsx | 12 +++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index ca526742d..0e55f1a82 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -14,6 +14,7 @@ describe('getHostComponentNames', () => { hostComponentNames: { text: 'banana', textInput: 'banana', + image: 'banana', switch: 'banana', scrollView: 'banana', modal: 'banana', @@ -23,6 +24,7 @@ describe('getHostComponentNames', () => { expect(getHostComponentNames()).toEqual({ text: 'banana', textInput: 'banana', + image: 'banana', switch: 'banana', scrollView: 'banana', modal: 'banana', @@ -37,6 +39,7 @@ describe('getHostComponentNames', () => { expect(hostComponentNames).toEqual({ text: 'Text', textInput: 'TextInput', + image: 'Image', switch: 'RCTSwitch', scrollView: 'RCTScrollView', modal: 'Modal', @@ -67,6 +70,7 @@ describe('configureHostComponentNamesIfNeeded', () => { expect(getConfig().hostComponentNames).toEqual({ text: 'Text', textInput: 'TextInput', + image: 'Image', switch: 'RCTSwitch', scrollView: 'RCTScrollView', modal: 'Modal', @@ -78,6 +82,7 @@ describe('configureHostComponentNamesIfNeeded', () => { hostComponentNames: { text: 'banana', textInput: 'banana', + image: 'banana', switch: 'banana', scrollView: 'banana', modal: 'banana', @@ -89,13 +94,14 @@ describe('configureHostComponentNamesIfNeeded', () => { expect(getConfig().hostComponentNames).toEqual({ text: 'banana', textInput: 'banana', + image: 'banana', switch: 'banana', scrollView: 'banana', modal: 'banana', }); }); - test('throw an error when autodetection fails', () => { + test('throw an error when auto-detection fails', () => { const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock; const renderer = TestRenderer.create(); diff --git a/src/config.ts b/src/config.ts index fd867a895..c343a3e15 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,7 @@ export type ConfigAliasOptions = { export type HostComponentNames = { text: string; textInput: string; + image: string; switch: string; scrollView: string; modal: string; diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index cd4b52239..b450c930b 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ReactTestInstance } from 'react-test-renderer'; -import { Modal, ScrollView, Switch, Text, TextInput, View } from 'react-native'; +import { Image, Modal, ScrollView, Switch, Text, TextInput, View } from 'react-native'; import { configureInternal, getConfig, HostComponentNames } from '../config'; import { renderWithAct } from '../render-act'; import { HostTestInstance } from './component-tree'; @@ -34,6 +34,7 @@ function detectHostComponentNames(): HostComponentNames { Hello + @@ -43,6 +44,7 @@ function detectHostComponentNames(): HostComponentNames { return { text: getByTestId(renderer.root, 'text').type as string, textInput: getByTestId(renderer.root, 'textInput').type as string, + image: getByTestId(renderer.root, 'image').type as string, switch: getByTestId(renderer.root, 'switch').type as string, scrollView: getByTestId(renderer.root, 'scrollView').type as string, modal: getByTestId(renderer.root, 'modal').type as string, @@ -85,6 +87,14 @@ export function isHostTextInput(element?: ReactTestInstance | null): element is return element?.type === getHostComponentNames().textInput; } +/** + * Checks if the given element is a host Image element. + * @param element The element to check. + */ +export function isHostImage(element?: ReactTestInstance | null): element is HostTestInstance { + return element?.type === getHostComponentNames().image; +} + /** * Checks if the given element is a host Switch element. * @param element The element to check. From 70dd329823dde8ed08d2d3eda7aa55d0e144ba49 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 15:00:31 +0200 Subject: [PATCH 02/11] feat: support Image alt prop in *ByLabelText --- src/helpers/accessibility.ts | 5 +++++ src/queries/__tests__/label-text.test.tsx | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 227a47a65..cf3c57ccc 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -9,6 +9,7 @@ import { ReactTestInstance } from 'react-test-renderer'; import { getHostSiblings, getUnsafeRootElement } from './component-tree'; import { getHostComponentNames, + isHostImage, isHostSwitch, isHostText, isHostTextInput, @@ -145,6 +146,10 @@ export function computeAriaModal(element: ReactTestInstance): boolean | undefine } export function computeAriaLabel(element: ReactTestInstance): string | undefined { + if (isHostImage(element) && element.props.alt) { + return element.props.alt; + } + return element.props['aria-label'] ?? element.props.accessibilityLabel; } diff --git a/src/queries/__tests__/label-text.test.tsx b/src/queries/__tests__/label-text.test.tsx index db965b4ac..bd554f285 100644 --- a/src/queries/__tests__/label-text.test.tsx +++ b/src/queries/__tests__/label-text.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { View, Text, TextInput, Pressable } from 'react-native'; +import { View, Text, TextInput, Image, Pressable } from 'react-native'; import { render, screen } from '../..'; const BUTTON_LABEL = 'cool button'; @@ -220,6 +220,18 @@ test('getByLabelText supports nested aria-labelledby', () => { expect(screen.getByLabelText(/nested text label/i)).toBe(screen.getByTestId('text-input')); }); +test('getByLabelText supports "Image"" with "alt" prop', () => { + render( + <> + Image Label + , + ); + + const expectedElement = screen.getByTestId('image'); + expect(screen.getByLabelText('Image Label')).toBe(expectedElement); + expect(screen.getByLabelText(/image label/i)).toBe(expectedElement); +}); + test('error message renders the element tree, preserving only helpful props', async () => { render(); From 125c23326548720db00a671aee16b81ac65347a4 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 15:25:42 +0200 Subject: [PATCH 03/11] feat: support *ByRole queries --- src/helpers/accessibility.ts | 18 +++++++++- src/helpers/format-default.ts | 1 + src/queries/__tests__/role-value.test.tsx | 8 ++--- src/queries/__tests__/role.test.tsx | 40 ++++++++++++++--------- src/queries/role.ts | 12 +++++-- 5 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index cf3c57ccc..edf5cfb68 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -103,6 +103,10 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole return false; } + if (isHostImage(element) && element.props.alt) { + return true; + } + if (element.props.accessible !== undefined) { return element.props.accessible; } @@ -131,16 +135,28 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole export function getRole(element: ReactTestInstance): Role | AccessibilityRole { const explicitRole = element.props.role ?? element.props.accessibilityRole; if (explicitRole) { - return explicitRole; + return normalizeRole(explicitRole); } if (isHostText(element)) { return 'text'; } + if (isHostImage(element) && element.props.alt) { + return 'img'; + } + return 'none'; } +export function normalizeRole(role: string): Role | AccessibilityRole { + if (role === 'image') { + return 'img'; + } + + return role as Role | AccessibilityRole; +} + export function computeAriaModal(element: ReactTestInstance): boolean | undefined { return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal; } diff --git a/src/helpers/format-default.ts b/src/helpers/format-default.ts index 223140753..7ee42b5b1 100644 --- a/src/helpers/format-default.ts +++ b/src/helpers/format-default.ts @@ -9,6 +9,7 @@ const propsToDisplay = [ 'accessibilityLabelledBy', 'accessibilityRole', 'accessibilityViewIsModal', + 'alt', 'aria-busy', 'aria-checked', 'aria-disabled', diff --git a/src/queries/__tests__/role-value.test.tsx b/src/queries/__tests__/role-value.test.tsx index 56a6e38cd..4b56a8dee 100644 --- a/src/queries/__tests__/role-value.test.tsx +++ b/src/queries/__tests__/role-value.test.tsx @@ -77,7 +77,7 @@ describe('accessibility value', () => { expect(() => screen.getByRole('adjustable', { name: 'Hello', value: { min: 5 } })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "adjustable", name: "Hello", min value: 5 + "Unable to find an element with role: adjustable, name: Hello, min value: 5 { `); expect(() => screen.getByRole('adjustable', { name: 'World', value: { min: 10 } })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "adjustable", name: "World", min value: 10 + "Unable to find an element with role: adjustable, name: World, min value: 10 { `); expect(() => screen.getByRole('adjustable', { name: 'Hello', value: { min: 5 } })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "adjustable", name: "Hello", min value: 5 + "Unable to find an element with role: adjustable, name: Hello, min value: 5 { `); expect(() => screen.getByRole('adjustable', { selected: true, value: { min: 10 } })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "adjustable", selected state: true, min value: 10 + "Unable to find an element with role: adjustable, selected state: true, min value: 10 { - return `Found multiple elements with role: "${value}"`; + return `Found multiple elements with role: ${value}`; }; const getNoInstancesFoundMessage = (value: string) => { - return `Unable to find an element with role: "${value}"`; + return `Unable to find an element with role: ${value}`; }; const Typography = ({ children, ...rest }: any) => { @@ -224,6 +225,15 @@ describe('supports name option', () => { expect(screen.getByRole('header', { name: 'About' })).toBe(screen.getByTestId('target-header')); expect(screen.getByRole('header', { name: 'About' }).props.testID).toBe('target-header'); }); + + test('supports host Image element with "alt" prop', () => { + render(An elephant); + + const expectedElement = screen.getByTestId('image'); + expect(screen.getByRole('img', { name: 'An elephant' })).toBe(expectedElement); + expect(screen.getByRole('image', { name: 'An elephant' })).toBe(expectedElement); + expect(screen.getByRole(/img/, { name: 'An elephant' })).toBe(expectedElement); + }); }); describe('supports accessibility states', () => { @@ -768,7 +778,7 @@ describe('error messages', () => { render(); expect(() => screen.getByRole('button')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "button" + "Unable to find an element with role: button " `); @@ -778,7 +788,7 @@ describe('error messages', () => { render(); expect(() => screen.getByRole('button', { name: 'Save' })).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "button", name: "Save" + "Unable to find an element with role: button, name: Save " `); @@ -789,7 +799,7 @@ describe('error messages', () => { expect(() => screen.getByRole('button', { name: 'Save', disabled: true })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "button", name: "Save", disabled state: true + "Unable to find an element with role: button, name: Save, disabled state: true " `); @@ -800,7 +810,7 @@ describe('error messages', () => { expect(() => screen.getByRole('button', { name: 'Save', disabled: true, selected: true })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true + "Unable to find an element with role: button, name: Save, disabled state: true, selected state: true " `); @@ -811,7 +821,7 @@ describe('error messages', () => { expect(() => screen.getByRole('button', { disabled: true })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "button", disabled state: true + "Unable to find an element with role: button, disabled state: true " `); @@ -822,7 +832,7 @@ describe('error messages', () => { expect(() => screen.getByRole('adjustable', { value: { min: 1 } })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "adjustable", min value: 1 + "Unable to find an element with role: adjustable, min value: 1 " `); @@ -832,7 +842,7 @@ describe('error messages', () => { 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/ + "Unable to find an element with role: adjustable, min value: 1, max value: 2, now value: 1, text value: /hello/ " `); @@ -852,7 +862,7 @@ test('byRole queries support hidden option', () => { expect(screen.queryByRole('button', { includeHiddenElements: false })).toBeFalsy(); expect(() => screen.getByRole('button', { includeHiddenElements: false })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "button" + "Unable to find an element with role: button { expect(screen.queryByRole('button', { name: 'Action' })).toBeFalsy(); }); - test('ignores elements with accessible={undefined} and that are implicitely not accessible', () => { + test('ignores elements with accessible={undefined} and that are implicitly not accessible', () => { render( Action @@ -903,7 +913,7 @@ test('error message renders the element tree, preserving only helpful props', as render(); expect(() => screen.getByRole('link')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "link" + "Unable to find an element with role: link screen.getAllByRole('link')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with role: "link" + "Unable to find an element with role: link { + return matchStringProp(getRole(node), role); +}; + const matchAccessibleNameIfNeeded = (node: ReactTestInstance, name?: TextMatch) => { if (name == null) return true; @@ -60,12 +65,13 @@ const queryAllByRole = ( instance: ReactTestInstance, ): QueryAllByQuery => function queryAllByRoleFn(role, options) { + const normalizedRole = typeof role === 'string' ? normalizeRole(role) : role; return findAll( instance, (node) => // run the cheapest checks first, and early exit to avoid unneeded computations isAccessibilityElement(node) && - matchStringProp(getRole(node), role) && + matchRole(node, normalizedRole) && matchAccessibleStateIfNeeded(node, options) && matchAccessibilityValueIfNeeded(node, options?.value) && matchAccessibleNameIfNeeded(node, options?.name), @@ -74,10 +80,10 @@ const queryAllByRole = ( }; const formatQueryParams = (role: TextMatch, options: ByRoleOptions = {}) => { - const params = [`role: "${String(role)}"`]; + const params = [`role: ${String(role)}`]; if (options.name) { - params.push(`name: "${String(options.name)}"`); + params.push(`name: ${String(options.name)}`); } accessibilityStateKeys.forEach((stateKey) => { From 235709f3a03cf78b39090d35950fcb3a2f19ce02 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 15:27:57 +0200 Subject: [PATCH 04/11] feat: support toHaveAccessibleName matcher --- .../__tests__/to-have-accessible-name.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/matchers/__tests__/to-have-accessible-name.test.tsx b/src/matchers/__tests__/to-have-accessible-name.test.tsx index f4ef96128..4bb8f92cc 100644 --- a/src/matchers/__tests__/to-have-accessible-name.test.tsx +++ b/src/matchers/__tests__/to-have-accessible-name.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { View, Text, TextInput } from 'react-native'; +import { View, Text, TextInput, Image } from 'react-native'; import { render, screen } from '../..'; import '../extend-expect'; @@ -72,17 +72,26 @@ test('toHaveAccessibleName() handles view with "aria-labelledby" prop', () => { expect(element).not.toHaveAccessibleName('Other label'); }); -test('toHaveAccessibleName() handles view with implicit accessible name', () => { +test('toHaveAccessibleName() handles Text with text content', () => { render(Text); + const element = screen.getByTestId('view'); expect(element).toHaveAccessibleName('Text'); expect(element).not.toHaveAccessibleName('Other text'); }); +test('toHaveAccessibleName() handles Image with "alt" prop', () => { + render(Test image); + + const element = screen.getByTestId('image'); + expect(element).toHaveAccessibleName('Test image'); + expect(element).not.toHaveAccessibleName('Other text'); +}); + test('toHaveAccessibleName() supports calling without expected name', () => { render(); - const element = screen.getByTestId('view'); + const element = screen.getByTestId('view'); expect(element).toHaveAccessibleName(); expect(() => expect(element).not.toHaveAccessibleName()).toThrowErrorMatchingInlineSnapshot(` "expect(element).not.toHaveAccessibleName() From b6f0e671bf8dbc47fe87a6bbe539ea02dcf7756e Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 15:33:47 +0200 Subject: [PATCH 05/11] chore: fix typecheck --- src/__tests__/config.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 47cdc6627..aca62f304 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -33,6 +33,7 @@ test('resetToDefaults() resets internal config to defaults', () => { hostComponentNames: { text: 'A', textInput: 'A', + image: 'A', switch: 'A', scrollView: 'A', modal: 'A', @@ -41,6 +42,7 @@ test('resetToDefaults() resets internal config to defaults', () => { expect(getConfig().hostComponentNames).toEqual({ text: 'A', textInput: 'A', + image: 'A', switch: 'A', scrollView: 'A', modal: 'A', From 5323d816c6d1ee340611a4d388f51eb12f32f86b Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 15:55:52 +0200 Subject: [PATCH 06/11] refactor: self code review --- src/helpers/accessibility.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index edf5cfb68..9243b41ae 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -103,7 +103,8 @@ export function isAccessibilityElement(element: ReactTestInstance | null): boole return false; } - if (isHostImage(element) && element.props.alt) { + // https://github.com/facebook/react-native/blob/8dabed60f456e76a9e53273b601446f34de41fb5/packages/react-native/Libraries/Image/Image.ios.js#L172 + if (isHostImage(element) && element.props.alt !== undefined) { return true; } @@ -162,11 +163,17 @@ export function computeAriaModal(element: ReactTestInstance): boolean | undefine } export function computeAriaLabel(element: ReactTestInstance): string | undefined { + const explicitLabel = element.props['aria-label'] ?? element.props.accessibilityLabel; + if (explicitLabel) { + return explicitLabel; + } + + //https://github.com/facebook/react-native/blob/8dabed60f456e76a9e53273b601446f34de41fb5/packages/react-native/Libraries/Image/Image.ios.js#L173 if (isHostImage(element) && element.props.alt) { return element.props.alt; } - return element.props['aria-label'] ?? element.props.accessibilityLabel; + return undefined; } export function computeAriaLabelledBy(element: ReactTestInstance): string | undefined { From 0caf27f5831e6169fea7bf12ed929da1211c461a Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 11 Sep 2024 16:38:17 +0200 Subject: [PATCH 07/11] refactor: adjust finding to experiment results --- src/helpers/accessibility.ts | 5 ++--- src/queries/__tests__/role.test.tsx | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 9243b41ae..2049303d5 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -143,9 +143,8 @@ export function getRole(element: ReactTestInstance): Role | AccessibilityRole { return 'text'; } - if (isHostImage(element) && element.props.alt) { - return 'img'; - } + // Note: host Image elements report "image" role in screen reader only on Android, but not on iOS. + // It's better to require explicit role for Image elements. return 'none'; } diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 7d805f991..9feb21baf 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -227,12 +227,22 @@ describe('supports name option', () => { }); test('supports host Image element with "alt" prop', () => { - render(An elephant); + render( + <> + an elephant + a tiger + , + ); + + const expectedElement1 = screen.getByTestId('image1'); + expect(screen.getByRole('img', { name: 'an elephant' })).toBe(expectedElement1); + expect(screen.getByRole('image', { name: 'an elephant' })).toBe(expectedElement1); + expect(screen.getByRole(/img/, { name: /elephant/ })).toBe(expectedElement1); - const expectedElement = screen.getByTestId('image'); - expect(screen.getByRole('img', { name: 'An elephant' })).toBe(expectedElement); - expect(screen.getByRole('image', { name: 'An elephant' })).toBe(expectedElement); - expect(screen.getByRole(/img/, { name: 'An elephant' })).toBe(expectedElement); + const expectedElement2 = screen.getByTestId('image2'); + expect(screen.getByRole('img', { name: 'a tiger' })).toBe(expectedElement2); + expect(screen.getByRole('image', { name: 'a tiger' })).toBe(expectedElement2); + expect(screen.getByRole(/img/, { name: /tiger/ })).toBe(expectedElement2); }); }); From 59206046a88009d51b9f4aeab3bf068ce8a76320 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 12 Sep 2024 08:41:46 +0200 Subject: [PATCH 08/11] chore: add missing tests --- src/__tests__/react-native-api.test.tsx | 202 ++++++++++++++---------- 1 file changed, 119 insertions(+), 83 deletions(-) diff --git a/src/__tests__/react-native-api.test.tsx b/src/__tests__/react-native-api.test.tsx index 7393fd4b1..3ce5fc5f2 100644 --- a/src/__tests__/react-native-api.test.tsx +++ b/src/__tests__/react-native-api.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FlatList, ScrollView, Switch, Text, TextInput, View } from 'react-native'; +import { FlatList, Image, Modal, ScrollView, Switch, Text, TextInput, View } from 'react-native'; import { render, screen } from '..'; /** @@ -7,7 +7,7 @@ import { render, screen } from '..'; * changed in a way that may impact our code like queries or event handling. */ -test('React Native API assumption: renders single host element', () => { +test('React Native API assumption: renders a single host element', () => { render(); expect(screen.toJSON()).toMatchInlineSnapshot(` @@ -17,7 +17,7 @@ test('React Native API assumption: renders single host element', () => { `); }); -test('React Native API assumption: renders single host element', () => { +test('React Native API assumption: renders a single host element', () => { render(Hello); expect(screen.toJSON()).toMatchInlineSnapshot(` @@ -29,7 +29,7 @@ test('React Native API assumption: renders single host element', () => { `); }); -test('React Native API assumption: nested renders single host element', () => { +test('React Native API assumption: nested renders a single host element', () => { render( Before @@ -63,7 +63,7 @@ test('React Native API assumption: nested renders single host element', ( `); }); -test('React Native API assumption: renders single host element', () => { +test('React Native API assumption: renders a single host element', () => { render( with nested Text renders single h `); }); -test('React Native API assumption: renders single host element', () => { +test('React Native API assumption: renders a single host element', () => { render(); expect(screen.toJSON()).toMatchInlineSnapshot(` @@ -123,7 +123,117 @@ test('React Native API assumption: renders single host element', () => `); }); -test('React Native API assumption: aria-* props render on host View', () => { +test('React Native API assumption: renders a single host element', () => { + render(Alt text); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + Alt text + `); +}); + +test('React Native API assumption: renders a single host element', () => { + render( + + + , + ); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + + + + + + `); +}); + +test('React Native API assumption: renders a single host element', () => { + render( + {item}} />, + ); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + + + + + 1 + + + + + 2 + + + + + `); +}); + +test('React Native API assumption: renders a single host element', () => { + render( + + Modal Content + , + ); + + expect(screen.toJSON()).toMatchInlineSnapshot(` + + + Modal Content + + + `); +}); + +test('React Native API assumption: aria-* props render directly on host View', () => { render( { `); }); -test('React Native API assumption: aria-* props render on host Text', () => { +test('React Native API assumption: aria-* props render directly on host Text', () => { render( { `); }); -test('React Native API assumption: aria-* props render on host TextInput', () => { +test('React Native API assumption: aria-* props render directly on host TextInput', () => { render( /> `); }); - -test('ScrollView renders correctly', () => { - render( - - - , - ); - - expect(screen.toJSON()).toMatchInlineSnapshot(` - - - - - - `); -}); - -test('FlatList renders correctly', () => { - render( - {item}} />, - ); - - expect(screen.toJSON()).toMatchInlineSnapshot(` - - - - - 1 - - - - - 2 - - - - - `); -}); From 829043cd9c6b0eee81c1b9fddf7405bb73738109 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 12 Sep 2024 08:51:34 +0200 Subject: [PATCH 09/11] refactor: self code review --- src/queries/role.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/queries/role.ts b/src/queries/role.ts index cdee4b7b4..9b30a4af9 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -39,10 +39,6 @@ export type ByRoleOptions = CommonQueryOptions & value?: AccessibilityValueMatcher; }; -const matchRole = (node: ReactTestInstance, role: ByRoleMatcher) => { - return matchStringProp(getRole(node), role); -}; - const matchAccessibleNameIfNeeded = (node: ReactTestInstance, name?: TextMatch) => { if (name == null) return true; @@ -71,7 +67,7 @@ const queryAllByRole = ( (node) => // run the cheapest checks first, and early exit to avoid unneeded computations isAccessibilityElement(node) && - matchRole(node, normalizedRole) && + matchStringProp(getRole(node), normalizedRole) && matchAccessibleStateIfNeeded(node, options) && matchAccessibilityValueIfNeeded(node, options?.value) && matchAccessibleNameIfNeeded(node, options?.name), From 28edb67bbedd4cd548d42f0d9226fdf54c4f7e42 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 12 Sep 2024 08:55:10 +0200 Subject: [PATCH 10/11] refactor: self code review --- src/helpers/accessibility.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 2049303d5..5eee9401a 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -149,6 +149,13 @@ export function getRole(element: ReactTestInstance): Role | AccessibilityRole { return 'none'; } +/** + * There are some duplications between (ARIA) `Role` and `AccessibilityRole` types. + * Resolve them by using ARIA `Role` type where possible. + * + * @param role Role to normalize + * @returns Normalized role + */ export function normalizeRole(role: string): Role | AccessibilityRole { if (role === 'image') { return 'img'; From 0a9623de458fbd6847a3b3f5c1277e1ed6b796bc Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 12 Sep 2024 09:04:47 +0200 Subject: [PATCH 11/11] refactor: self-code review --- src/queries/__tests__/hint-text.test.tsx | 14 +++++++------- src/queries/hint-text.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/queries/__tests__/hint-text.test.tsx b/src/queries/__tests__/hint-text.test.tsx index 1292bb7b3..2ff9f8419 100644 --- a/src/queries/__tests__/hint-text.test.tsx +++ b/src/queries/__tests__/hint-text.test.tsx @@ -8,11 +8,11 @@ const TEXT_HINT = 'static text'; const NO_MATCHES_TEXT: any = 'not-existent-element'; const getMultipleInstancesFoundMessage = (value: string) => { - return `Found multiple elements with accessibilityHint: ${value}`; + return `Found multiple elements with accessibility hint: ${value}`; }; const getNoInstancesFoundMessage = (value: string) => { - return `Unable to find an element with accessibilityHint: ${value}`; + return `Unable to find an element with accessibility hint: ${value}`; }; const Typography = ({ children, ...rest }: any) => { @@ -114,7 +114,7 @@ test('byHintText queries support hidden option', () => { expect(screen.queryByHintText('hidden', { includeHiddenElements: false })).toBeFalsy(); expect(() => screen.getByHintText('hidden', { includeHiddenElements: false })) .toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: hidden + "Unable to find an element with accessibility hint: hidden ); expect(() => screen.getByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: FOO + "Unable to find an element with accessibility hint: FOO screen.getAllByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(` - "Unable to find an element with accessibilityHint: FOO + "Unable to find an element with accessibility hint: FOO - `Found multiple elements with accessibilityHint: ${String(hint)} `; + `Found multiple elements with accessibility hint: ${String(hint)} `; const getMissingError = (hint: TextMatch) => - `Unable to find an element with accessibilityHint: ${String(hint)}`; + `Unable to find an element with accessibility hint: ${String(hint)}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( queryAllByHintText,