diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22d0c9bc4..cee442654 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: - name: Setup Node.js and deps uses: ./.github/actions/setup-deps - - name: Test in concurrent mode + - name: Test in legacy mode run: CONCURRENT_MODE=0 yarn test:ci test-website: diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index cbfd021c4..b206ccdc7 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { View, Text, TextInput, Pressable, Switch, TouchableOpacity } from 'react-native'; import { render, isHiddenFromAccessibility, isInaccessible, screen } from '../..'; -import { isAccessibilityElement } from '../accessibility'; +import { computeAriaLabel, isAccessibilityElement } from '../accessibility'; describe('isHiddenFromAccessibility', () => { test('returns false for accessible elements', () => { @@ -371,3 +371,39 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(null)).toEqual(false); }); }); + +describe('computeAriaLabel', () => { + test('supports basic usage', () => { + render( + + + + + External Text + + + + Text Content + + , + ); + + expect(computeAriaLabel(screen.getByTestId('label'))).toEqual('Internal Label'); + expect(computeAriaLabel(screen.getByTestId('label-by-id'))).toEqual('External Text'); + expect(computeAriaLabel(screen.getByTestId('no-label'))).toBeUndefined(); + expect(computeAriaLabel(screen.getByTestId('text-content'))).toBeUndefined(); + }); + + test('label priority', () => { + render( + + + + External Label + + , + ); + + expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External Label'); + }); +}); diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index 0746d58ff..fe7e3838f 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -228,8 +228,4 @@ describe('getUnsafeRootElement()', () => { const view = screen.getByTestId('view'); expect(getUnsafeRootElement(view)).toEqual(screen.UNSAFE_root); }); - - it('returns null for null', () => { - expect(getUnsafeRootElement(null)).toEqual(null); - }); }); diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 062ea8fb2..40f7008ce 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -5,8 +5,9 @@ import { Role, StyleSheet, } from 'react-native'; -import { ReactTestInstance } from 'react-test-renderer'; -import { getHostSiblings, getUnsafeRootElement } from './component-tree'; +import type { ReactTestInstance } from 'react-test-renderer'; +import { getHostSiblings, getUnsafeRootElement, isHostElement } from './component-tree'; +import { findAll } from './find-all'; import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names'; import { getTextContent } from './text-content'; import { isTextInputEditable } from './text-input'; @@ -158,6 +159,19 @@ export function computeAriaModal(element: ReactTestInstance): boolean | undefine } export function computeAriaLabel(element: ReactTestInstance): string | undefined { + const labelElementId = element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy; + if (labelElementId) { + const rootElement = getUnsafeRootElement(element); + const labelElement = findAll( + rootElement, + (node) => isHostElement(node) && node.props.nativeID === labelElementId, + { includeHiddenElements: true }, + ); + if (labelElement.length > 0) { + return getTextContent(labelElement[0]); + } + } + const explicitLabel = element.props['aria-label'] ?? element.props.accessibilityLabel; if (explicitLabel) { return explicitLabel; @@ -171,10 +185,6 @@ export function computeAriaLabel(element: ReactTestInstance): string | undefined return undefined; } -export function computeAriaLabelledBy(element: ReactTestInstance): string | undefined { - return element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy; -} - // See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#busy-state export function computeAriaBusy({ props }: ReactTestInstance): boolean { return props['aria-busy'] ?? props.accessibilityState?.busy ?? false; @@ -234,21 +244,7 @@ export function computeAriaValue(element: ReactTestInstance): AccessibilityValue } export function computeAccessibleName(element: ReactTestInstance): string | undefined { - const label = computeAriaLabel(element); - if (label) { - return label; - } - - const labelElementId = computeAriaLabelledBy(element); - if (labelElementId) { - const rootElement = getUnsafeRootElement(element); - const labelElement = rootElement?.findByProps({ nativeID: labelElementId }); - if (labelElement) { - return getTextContent(labelElement); - } - } - - return getTextContent(element); + return computeAriaLabel(element) ?? getTextContent(element); } type RoleSupportMap = Partial>; diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 4a4a00897..bcb2a9f08 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -13,7 +13,7 @@ export function isHostElement(element?: ReactTestInstance | null): element is Ho return typeof element?.type === 'string'; } -export function isElementMounted(element: ReactTestInstance | null) { +export function isElementMounted(element: ReactTestInstance) { return getUnsafeRootElement(element) === screen.UNSAFE_root; } @@ -91,11 +91,7 @@ export function getHostSiblings(element: ReactTestInstance | null): HostTestInst * @param element The element start traversing from. * @returns The root element of the tree (host or composite). */ -export function getUnsafeRootElement(element: ReactTestInstance | null) { - if (element == null) { - return null; - } - +export function getUnsafeRootElement(element: ReactTestInstance) { let current = element; while (current.parent) { current = current.parent; diff --git a/src/helpers/matchers/match-label-text.ts b/src/helpers/matchers/match-label-text.ts index 1da29d867..f1ceaaac9 100644 --- a/src/helpers/matchers/match-label-text.ts +++ b/src/helpers/matchers/match-label-text.ts @@ -1,43 +1,11 @@ import { ReactTestInstance } from 'react-test-renderer'; import { matches, TextMatch, TextMatchOptions } from '../../matches'; -import { computeAriaLabel, computeAriaLabelledBy } from '../accessibility'; -import { findAll } from '../find-all'; -import { matchTextContent } from './match-text-content'; +import { computeAriaLabel } from '../accessibility'; -export function matchLabelText( - root: ReactTestInstance, - element: ReactTestInstance, - expectedText: TextMatch, - options: TextMatchOptions = {}, -) { - return ( - matchAccessibilityLabel(element, expectedText, options) || - matchAccessibilityLabelledBy(root, computeAriaLabelledBy(element), expectedText, options) - ); -} - -function matchAccessibilityLabel( +export function matchAccessibilityLabel( element: ReactTestInstance, expectedLabel: TextMatch, - options: TextMatchOptions, + options?: TextMatchOptions, ) { - return matches(expectedLabel, computeAriaLabel(element), options.normalizer, options.exact); -} - -function matchAccessibilityLabelledBy( - root: ReactTestInstance, - nativeId: string | undefined, - text: TextMatch, - options: TextMatchOptions, -) { - if (!nativeId) { - return false; - } - - return ( - findAll( - root, - (node) => node.props.nativeID === nativeId && matchTextContent(node, text, options), - ).length > 0 - ); + return matches(expectedLabel, computeAriaLabel(element), options?.normalizer, options?.exact); } diff --git a/src/matchers/to-be-busy.tsx b/src/matchers/to-be-busy.ts similarity index 100% rename from src/matchers/to-be-busy.tsx rename to src/matchers/to-be-busy.ts diff --git a/src/matchers/to-be-checked.tsx b/src/matchers/to-be-checked.ts similarity index 100% rename from src/matchers/to-be-checked.tsx rename to src/matchers/to-be-checked.ts diff --git a/src/matchers/to-be-disabled.tsx b/src/matchers/to-be-disabled.ts similarity index 100% rename from src/matchers/to-be-disabled.tsx rename to src/matchers/to-be-disabled.ts diff --git a/src/matchers/to-be-empty-element.tsx b/src/matchers/to-be-empty-element.ts similarity index 100% rename from src/matchers/to-be-empty-element.tsx rename to src/matchers/to-be-empty-element.ts diff --git a/src/matchers/to-be-expanded.tsx b/src/matchers/to-be-expanded.ts similarity index 100% rename from src/matchers/to-be-expanded.tsx rename to src/matchers/to-be-expanded.ts diff --git a/src/matchers/to-be-on-the-screen.tsx b/src/matchers/to-be-on-the-screen.ts similarity index 100% rename from src/matchers/to-be-on-the-screen.tsx rename to src/matchers/to-be-on-the-screen.ts diff --git a/src/matchers/to-be-partially-checked.tsx b/src/matchers/to-be-partially-checked.ts similarity index 100% rename from src/matchers/to-be-partially-checked.tsx rename to src/matchers/to-be-partially-checked.ts diff --git a/src/matchers/to-be-visible.tsx b/src/matchers/to-be-visible.ts similarity index 100% rename from src/matchers/to-be-visible.tsx rename to src/matchers/to-be-visible.ts diff --git a/src/matchers/to-contain-element.tsx b/src/matchers/to-contain-element.ts similarity index 100% rename from src/matchers/to-contain-element.tsx rename to src/matchers/to-contain-element.ts diff --git a/src/matchers/to-have-accessibility-value.tsx b/src/matchers/to-have-accessibility-value.ts similarity index 100% rename from src/matchers/to-have-accessibility-value.tsx rename to src/matchers/to-have-accessibility-value.ts diff --git a/src/matchers/to-have-accessible-name.tsx b/src/matchers/to-have-accessible-name.ts similarity index 100% rename from src/matchers/to-have-accessible-name.tsx rename to src/matchers/to-have-accessible-name.ts diff --git a/src/matchers/to-have-display-value.tsx b/src/matchers/to-have-display-value.ts similarity index 100% rename from src/matchers/to-have-display-value.tsx rename to src/matchers/to-have-display-value.ts diff --git a/src/matchers/to-have-style.tsx b/src/matchers/to-have-style.ts similarity index 100% rename from src/matchers/to-have-style.tsx rename to src/matchers/to-have-style.ts diff --git a/src/matchers/to-have-text-content.tsx b/src/matchers/to-have-text-content.ts similarity index 100% rename from src/matchers/to-have-text-content.tsx rename to src/matchers/to-have-text-content.ts diff --git a/src/matchers/utils.tsx b/src/matchers/utils.ts similarity index 100% rename from src/matchers/utils.tsx rename to src/matchers/utils.ts diff --git a/src/queries/label-text.ts b/src/queries/label-text.ts index c9dd6dc06..2e018a6a0 100644 --- a/src/queries/label-text.ts +++ b/src/queries/label-text.ts @@ -1,7 +1,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { findAll } from '../helpers/find-all'; import { TextMatch, TextMatchOptions } from '../matches'; -import { matchLabelText } from '../helpers/matchers/match-label-text'; +import { matchAccessibilityLabel } from '../helpers/matchers/match-label-text'; import { makeQueries } from './make-queries'; import type { FindAllByQuery, @@ -19,7 +19,7 @@ function queryAllByLabelText(instance: ReactTestInstance) { return (text: TextMatch, queryOptions?: ByLabelTextOptions) => { return findAll( instance, - (node) => matchLabelText(instance, node, text, queryOptions), + (node) => matchAccessibilityLabel(node, text, queryOptions), queryOptions, ); };