From 5a10b312e252427b6573d54270a8944be8025396 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 5 Nov 2024 09:03:57 +0100 Subject: [PATCH 1/6] chore: change tsx to ts --- src/matchers/{to-be-busy.tsx => to-be-busy.ts} | 0 src/matchers/{to-be-checked.tsx => to-be-checked.ts} | 0 src/matchers/{to-be-disabled.tsx => to-be-disabled.ts} | 0 src/matchers/{to-be-empty-element.tsx => to-be-empty-element.ts} | 0 src/matchers/{to-be-expanded.tsx => to-be-expanded.ts} | 0 src/matchers/{to-be-on-the-screen.tsx => to-be-on-the-screen.ts} | 0 .../{to-be-partially-checked.tsx => to-be-partially-checked.ts} | 0 src/matchers/{to-be-visible.tsx => to-be-visible.ts} | 0 src/matchers/{to-contain-element.tsx => to-contain-element.ts} | 0 ...ave-accessibility-value.tsx => to-have-accessibility-value.ts} | 0 .../{to-have-accessible-name.tsx => to-have-accessible-name.ts} | 0 .../{to-have-display-value.tsx => to-have-display-value.ts} | 0 src/matchers/{to-have-style.tsx => to-have-style.ts} | 0 .../{to-have-text-content.tsx => to-have-text-content.ts} | 0 src/matchers/{utils.tsx => utils.ts} | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename src/matchers/{to-be-busy.tsx => to-be-busy.ts} (100%) rename src/matchers/{to-be-checked.tsx => to-be-checked.ts} (100%) rename src/matchers/{to-be-disabled.tsx => to-be-disabled.ts} (100%) rename src/matchers/{to-be-empty-element.tsx => to-be-empty-element.ts} (100%) rename src/matchers/{to-be-expanded.tsx => to-be-expanded.ts} (100%) rename src/matchers/{to-be-on-the-screen.tsx => to-be-on-the-screen.ts} (100%) rename src/matchers/{to-be-partially-checked.tsx => to-be-partially-checked.ts} (100%) rename src/matchers/{to-be-visible.tsx => to-be-visible.ts} (100%) rename src/matchers/{to-contain-element.tsx => to-contain-element.ts} (100%) rename src/matchers/{to-have-accessibility-value.tsx => to-have-accessibility-value.ts} (100%) rename src/matchers/{to-have-accessible-name.tsx => to-have-accessible-name.ts} (100%) rename src/matchers/{to-have-display-value.tsx => to-have-display-value.ts} (100%) rename src/matchers/{to-have-style.tsx => to-have-style.ts} (100%) rename src/matchers/{to-have-text-content.tsx => to-have-text-content.ts} (100%) rename src/matchers/{utils.tsx => utils.ts} (100%) 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 From 23e9b56f6bd05e962806177efc521c000cf508c4 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 12 Sep 2024 09:40:04 +0200 Subject: [PATCH 2/6] refactor: a11y label helpers --- src/helpers/accessibility.ts | 29 ++++++----------- src/helpers/matchers/match-label-text.ts | 40 +++--------------------- src/queries/label-text.ts | 4 +-- 3 files changed, 16 insertions(+), 57 deletions(-) diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts index 062ea8fb2..9792b28d6 100644 --- a/src/helpers/accessibility.ts +++ b/src/helpers/accessibility.ts @@ -158,6 +158,15 @@ 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 = rootElement?.findByProps({ nativeID: labelElementId }); + if (labelElement) { + return getTextContent(labelElement); + } + } + const explicitLabel = element.props['aria-label'] ?? element.props.accessibilityLabel; if (explicitLabel) { return explicitLabel; @@ -171,10 +180,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 +239,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/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/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, ); }; From cafb7d37543c721385513521aabfe257a2c1b000 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Fri, 18 Oct 2024 11:22:09 +0200 Subject: [PATCH 3/6] refactor: self code review --- src/helpers/__tests__/component-tree.test.tsx | 4 ---- src/helpers/accessibility.ts | 15 ++++++++++----- src/helpers/component-tree.ts | 6 +----- 3 files changed, 11 insertions(+), 14 deletions(-) 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 9792b28d6..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'; @@ -161,9 +162,13 @@ export function computeAriaLabel(element: ReactTestInstance): string | undefined const labelElementId = element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy; if (labelElementId) { const rootElement = getUnsafeRootElement(element); - const labelElement = rootElement?.findByProps({ nativeID: labelElementId }); - if (labelElement) { - return getTextContent(labelElement); + const labelElement = findAll( + rootElement, + (node) => isHostElement(node) && node.props.nativeID === labelElementId, + { includeHiddenElements: true }, + ); + if (labelElement.length > 0) { + return getTextContent(labelElement[0]); } } diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 4a4a00897..656a0ba0f 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -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; From 8429b7b402cf5415d191c8cb9e373716359ba052 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Sat, 19 Oct 2024 14:26:21 +0200 Subject: [PATCH 4/6] chore: add tests --- src/helpers/__tests__/accessiblity.test.tsx | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) 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'); + }); +}); From b5816c43271a689e1dc79d8aa1a283f83a1d873a Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 31 Oct 2024 12:23:09 +0100 Subject: [PATCH 5/6] chore: fix --- src/helpers/component-tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 656a0ba0f..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; } From 05c4284d2ff5f361526f53bc6ed3ba7bc5a47c04 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 31 Oct 2024 12:28:33 +0100 Subject: [PATCH 6/6] chore: tweaks --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: