diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessibility.test.tsx
similarity index 68%
rename from src/helpers/__tests__/accessiblity.test.tsx
rename to src/helpers/__tests__/accessibility.test.tsx
index b768c84b..39edcdd1 100644
--- a/src/helpers/__tests__/accessiblity.test.tsx
+++ b/src/helpers/__tests__/accessibility.test.tsx
@@ -4,9 +4,16 @@ import { Image, Pressable, Switch, Text, TextInput, TouchableOpacity, View } fro
import { isHiddenFromAccessibility, isInaccessible, render, screen } from '../..';
import {
computeAccessibleName,
+ computeAriaBusy,
+ computeAriaChecked,
computeAriaDisabled,
+ computeAriaExpanded,
computeAriaLabel,
+ computeAriaSelected,
+ computeAriaValue,
+ getRole,
isAccessibilityElement,
+ normalizeRole,
} from '../accessibility';
describe('isHiddenFromAccessibility', () => {
@@ -280,6 +287,21 @@ describe('isHiddenFromAccessibility', () => {
expect(isHiddenFromAccessibility(screen.getByTestId('subject'))).toBe(false);
});
+ test('uses cache when provided', async () => {
+ await render(
+
+
+ ,
+ );
+ const element = screen.getByTestId('subject', { includeHiddenElements: true });
+ const cache = new WeakMap();
+
+ // First call populates the cache
+ isHiddenFromAccessibility(element, { cache });
+ // Second call should use the cache
+ expect(isHiddenFromAccessibility(element, { cache })).toBe(false);
+ });
+
test('has isInaccessible alias', () => {
expect(isInaccessible).toBe(isHiddenFromAccessibility);
});
@@ -373,6 +395,17 @@ describe('isAccessibilityElement', () => {
expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy();
});
+ test('matches Image component with alt prop', async () => {
+ await render(
+
+
+
+ ,
+ );
+ expect(isAccessibilityElement(screen.getByTestId('with-alt'))).toBeTruthy();
+ expect(isAccessibilityElement(screen.getByTestId('without-alt'))).toBeFalsy();
+ });
+
test('returns false when given null', () => {
expect(isAccessibilityElement(null)).toEqual(false);
});
@@ -412,6 +445,33 @@ describe('computeAriaLabel', () => {
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External Label');
});
+
+ test('supports accessibilityLabel', async () => {
+ await render();
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('Legacy Label');
+ });
+
+ test('supports accessibilityLabelledBy', async () => {
+ await render(
+
+
+
+ External
+
+ ,
+ );
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External');
+ });
+
+ test('supports Image with alt prop', async () => {
+ await render();
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('Image Alt');
+ });
+
+ test('returns undefined when aria-labelledby references non-existent element', async () => {
+ await render();
+ expect(computeAriaLabel(screen.getByTestId('subject'))).toBeUndefined();
+ });
});
describe('computeAriaDisabled', () => {
@@ -482,6 +542,200 @@ describe('computeAriaDisabled', () => {
});
});
+describe('getRole', () => {
+ test('returns explicit role from "role" prop', async () => {
+ await render();
+ expect(getRole(screen.getByTestId('subject'))).toBe('button');
+ });
+
+ test('returns explicit role from "accessibilityRole" prop', async () => {
+ await render();
+ expect(getRole(screen.getByTestId('subject'))).toBe('link');
+ });
+
+ test('prefers "role" over "accessibilityRole"', async () => {
+ await render();
+ expect(getRole(screen.getByTestId('subject'))).toBe('button');
+ });
+
+ test('returns "text" for Text elements', async () => {
+ await render(Hello);
+ expect(getRole(screen.getByTestId('subject'))).toBe('text');
+ });
+
+ test('returns "none" for elements without explicit role', async () => {
+ await render();
+ expect(getRole(screen.getByTestId('subject'))).toBe('none');
+ });
+
+ test('normalizes "image" role to "img"', async () => {
+ await render();
+ expect(getRole(screen.getByTestId('subject'))).toBe('img');
+ });
+});
+
+describe('normalizeRole', () => {
+ test('converts "image" to "img"', () => {
+ expect(normalizeRole('image')).toBe('img');
+ });
+
+ test('passes through other roles unchanged', () => {
+ expect(normalizeRole('button')).toBe('button');
+ expect(normalizeRole('link')).toBe('link');
+ expect(normalizeRole('none')).toBe('none');
+ });
+});
+
+describe('computeAriaBusy', () => {
+ test('returns false by default', async () => {
+ await render();
+ expect(computeAriaBusy(screen.getByTestId('subject'))).toBe(false);
+ });
+
+ test('supports aria-busy prop', async () => {
+ await render();
+ expect(computeAriaBusy(screen.getByTestId('subject'))).toBe(true);
+ });
+
+ test('supports accessibilityState.busy', async () => {
+ await render();
+ expect(computeAriaBusy(screen.getByTestId('subject'))).toBe(true);
+ });
+});
+
+describe('computeAriaChecked', () => {
+ test('returns undefined for roles that do not support checked', async () => {
+ await render();
+ expect(computeAriaChecked(screen.getByTestId('subject'))).toBeUndefined();
+ });
+
+ test('supports aria-checked for checkbox role', async () => {
+ await render(
+
+
+
+
+ ,
+ );
+ expect(computeAriaChecked(screen.getByTestId('checked'))).toBe(true);
+ expect(computeAriaChecked(screen.getByTestId('unchecked'))).toBe(false);
+ expect(computeAriaChecked(screen.getByTestId('mixed'))).toBe('mixed');
+ });
+
+ test('supports accessibilityState.checked for radio role', async () => {
+ await render(
+ ,
+ );
+ expect(computeAriaChecked(screen.getByTestId('subject'))).toBe(true);
+ });
+
+ test('supports Switch component value', async () => {
+ await render(
+
+
+
+ ,
+ );
+ expect(computeAriaChecked(screen.getByTestId('on'))).toBe(true);
+ expect(computeAriaChecked(screen.getByTestId('off'))).toBe(false);
+ });
+});
+
+describe('computeAriaExpanded', () => {
+ test('returns undefined by default', async () => {
+ await render();
+ expect(computeAriaExpanded(screen.getByTestId('subject'))).toBeUndefined();
+ });
+
+ test('supports aria-expanded prop', async () => {
+ await render(
+
+
+
+ ,
+ );
+ expect(computeAriaExpanded(screen.getByTestId('expanded'))).toBe(true);
+ expect(computeAriaExpanded(screen.getByTestId('collapsed'))).toBe(false);
+ });
+
+ test('supports accessibilityState.expanded', async () => {
+ await render();
+ expect(computeAriaExpanded(screen.getByTestId('subject'))).toBe(true);
+ });
+});
+
+describe('computeAriaSelected', () => {
+ test('returns false by default', async () => {
+ await render();
+ expect(computeAriaSelected(screen.getByTestId('subject'))).toBe(false);
+ });
+
+ test('supports aria-selected prop', async () => {
+ await render();
+ expect(computeAriaSelected(screen.getByTestId('subject'))).toBe(true);
+ });
+
+ test('supports accessibilityState.selected', async () => {
+ await render();
+ expect(computeAriaSelected(screen.getByTestId('subject'))).toBe(true);
+ });
+});
+
+describe('computeAriaValue', () => {
+ test('returns empty values by default', async () => {
+ await render();
+ expect(computeAriaValue(screen.getByTestId('subject'))).toEqual({
+ min: undefined,
+ max: undefined,
+ now: undefined,
+ text: undefined,
+ });
+ });
+
+ test('supports aria-value* props', async () => {
+ await render(
+ ,
+ );
+ expect(computeAriaValue(screen.getByTestId('subject'))).toEqual({
+ min: 0,
+ max: 100,
+ now: 50,
+ text: '50%',
+ });
+ });
+
+ test('supports accessibilityValue prop', async () => {
+ await render(
+ ,
+ );
+ expect(computeAriaValue(screen.getByTestId('subject'))).toEqual({
+ min: 0,
+ max: 100,
+ now: 25,
+ text: '25%',
+ });
+ });
+
+ test('aria-value* props take precedence over accessibilityValue', async () => {
+ await render(
+ ,
+ );
+ const value = computeAriaValue(screen.getByTestId('subject'));
+ expect(value.now).toBe(75);
+ expect(value.min).toBe(0);
+ });
+});
+
describe('computeAccessibleName', () => {
test('basic cases', async () => {
await render(
diff --git a/src/helpers/accessibility.ts b/src/helpers/accessibility.ts
index 2187d0c5..640b7dbc 100644
--- a/src/helpers/accessibility.ts
+++ b/src/helpers/accessibility.ts
@@ -53,11 +53,6 @@ export function isHiddenFromAccessibility(
export const isInaccessible = isHiddenFromAccessibility;
function isSubtreeInaccessible(element: HostElement): boolean {
- // Null props can happen for React.Fragments
- if (element.props == null) {
- return false;
- }
-
// See: https://reactnative.dev/docs/accessibility#aria-hidden
if (element.props['aria-hidden']) {
return true;