Skip to content

Commit 0a01a73

Browse files
chore: add accessibility test (#1882)
1 parent 42f4989 commit 0a01a73

File tree

2 files changed

+254
-5
lines changed

2 files changed

+254
-5
lines changed

src/helpers/__tests__/accessiblity.test.tsx renamed to src/helpers/__tests__/accessibility.test.tsx

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import { Image, Pressable, Switch, Text, TextInput, TouchableOpacity, View } fro
44
import { isHiddenFromAccessibility, isInaccessible, render, screen } from '../..';
55
import {
66
computeAccessibleName,
7+
computeAriaBusy,
8+
computeAriaChecked,
79
computeAriaDisabled,
10+
computeAriaExpanded,
811
computeAriaLabel,
12+
computeAriaSelected,
13+
computeAriaValue,
14+
getRole,
915
isAccessibilityElement,
16+
normalizeRole,
1017
} from '../accessibility';
1118

1219
describe('isHiddenFromAccessibility', () => {
@@ -280,6 +287,21 @@ describe('isHiddenFromAccessibility', () => {
280287
expect(isHiddenFromAccessibility(screen.getByTestId('subject'))).toBe(false);
281288
});
282289

290+
test('uses cache when provided', async () => {
291+
await render(
292+
<View>
293+
<View testID="subject" />
294+
</View>,
295+
);
296+
const element = screen.getByTestId('subject', { includeHiddenElements: true });
297+
const cache = new WeakMap();
298+
299+
// First call populates the cache
300+
isHiddenFromAccessibility(element, { cache });
301+
// Second call should use the cache
302+
expect(isHiddenFromAccessibility(element, { cache })).toBe(false);
303+
});
304+
283305
test('has isInaccessible alias', () => {
284306
expect(isInaccessible).toBe(isHiddenFromAccessibility);
285307
});
@@ -373,6 +395,17 @@ describe('isAccessibilityElement', () => {
373395
expect(isAccessibilityElement(screen.getByTestId('false'))).toBeFalsy();
374396
});
375397

398+
test('matches Image component with alt prop', async () => {
399+
await render(
400+
<View>
401+
<Image testID="with-alt" alt="Test image" />
402+
<Image testID="without-alt" />
403+
</View>,
404+
);
405+
expect(isAccessibilityElement(screen.getByTestId('with-alt'))).toBeTruthy();
406+
expect(isAccessibilityElement(screen.getByTestId('without-alt'))).toBeFalsy();
407+
});
408+
376409
test('returns false when given null', () => {
377410
expect(isAccessibilityElement(null)).toEqual(false);
378411
});
@@ -412,6 +445,33 @@ describe('computeAriaLabel', () => {
412445

413446
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External Label');
414447
});
448+
449+
test('supports accessibilityLabel', async () => {
450+
await render(<View testID="subject" accessibilityLabel="Legacy Label" />);
451+
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('Legacy Label');
452+
});
453+
454+
test('supports accessibilityLabelledBy', async () => {
455+
await render(
456+
<View>
457+
<View testID="subject" accessibilityLabelledBy="ext-label" />
458+
<View nativeID="ext-label">
459+
<Text>External</Text>
460+
</View>
461+
</View>,
462+
);
463+
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External');
464+
});
465+
466+
test('supports Image with alt prop', async () => {
467+
await render(<Image testID="subject" alt="Image Alt" />);
468+
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('Image Alt');
469+
});
470+
471+
test('returns undefined when aria-labelledby references non-existent element', async () => {
472+
await render(<View testID="subject" aria-labelledby="non-existent-id" />);
473+
expect(computeAriaLabel(screen.getByTestId('subject'))).toBeUndefined();
474+
});
415475
});
416476

417477
describe('computeAriaDisabled', () => {
@@ -482,6 +542,200 @@ describe('computeAriaDisabled', () => {
482542
});
483543
});
484544

545+
describe('getRole', () => {
546+
test('returns explicit role from "role" prop', async () => {
547+
await render(<View testID="subject" role="button" />);
548+
expect(getRole(screen.getByTestId('subject'))).toBe('button');
549+
});
550+
551+
test('returns explicit role from "accessibilityRole" prop', async () => {
552+
await render(<View testID="subject" accessibilityRole="link" />);
553+
expect(getRole(screen.getByTestId('subject'))).toBe('link');
554+
});
555+
556+
test('prefers "role" over "accessibilityRole"', async () => {
557+
await render(<View testID="subject" role="button" accessibilityRole="link" />);
558+
expect(getRole(screen.getByTestId('subject'))).toBe('button');
559+
});
560+
561+
test('returns "text" for Text elements', async () => {
562+
await render(<Text testID="subject">Hello</Text>);
563+
expect(getRole(screen.getByTestId('subject'))).toBe('text');
564+
});
565+
566+
test('returns "none" for elements without explicit role', async () => {
567+
await render(<View testID="subject" />);
568+
expect(getRole(screen.getByTestId('subject'))).toBe('none');
569+
});
570+
571+
test('normalizes "image" role to "img"', async () => {
572+
await render(<View testID="subject" accessibilityRole="image" />);
573+
expect(getRole(screen.getByTestId('subject'))).toBe('img');
574+
});
575+
});
576+
577+
describe('normalizeRole', () => {
578+
test('converts "image" to "img"', () => {
579+
expect(normalizeRole('image')).toBe('img');
580+
});
581+
582+
test('passes through other roles unchanged', () => {
583+
expect(normalizeRole('button')).toBe('button');
584+
expect(normalizeRole('link')).toBe('link');
585+
expect(normalizeRole('none')).toBe('none');
586+
});
587+
});
588+
589+
describe('computeAriaBusy', () => {
590+
test('returns false by default', async () => {
591+
await render(<View testID="subject" />);
592+
expect(computeAriaBusy(screen.getByTestId('subject'))).toBe(false);
593+
});
594+
595+
test('supports aria-busy prop', async () => {
596+
await render(<View testID="subject" aria-busy />);
597+
expect(computeAriaBusy(screen.getByTestId('subject'))).toBe(true);
598+
});
599+
600+
test('supports accessibilityState.busy', async () => {
601+
await render(<View testID="subject" accessibilityState={{ busy: true }} />);
602+
expect(computeAriaBusy(screen.getByTestId('subject'))).toBe(true);
603+
});
604+
});
605+
606+
describe('computeAriaChecked', () => {
607+
test('returns undefined for roles that do not support checked', async () => {
608+
await render(<View testID="subject" role="button" aria-checked />);
609+
expect(computeAriaChecked(screen.getByTestId('subject'))).toBeUndefined();
610+
});
611+
612+
test('supports aria-checked for checkbox role', async () => {
613+
await render(
614+
<View>
615+
<View testID="checked" role="checkbox" aria-checked />
616+
<View testID="unchecked" role="checkbox" aria-checked={false} />
617+
<View testID="mixed" role="checkbox" aria-checked="mixed" />
618+
</View>,
619+
);
620+
expect(computeAriaChecked(screen.getByTestId('checked'))).toBe(true);
621+
expect(computeAriaChecked(screen.getByTestId('unchecked'))).toBe(false);
622+
expect(computeAriaChecked(screen.getByTestId('mixed'))).toBe('mixed');
623+
});
624+
625+
test('supports accessibilityState.checked for radio role', async () => {
626+
await render(
627+
<View testID="subject" accessibilityRole="radio" accessibilityState={{ checked: true }} />,
628+
);
629+
expect(computeAriaChecked(screen.getByTestId('subject'))).toBe(true);
630+
});
631+
632+
test('supports Switch component value', async () => {
633+
await render(
634+
<View>
635+
<Switch testID="on" value={true} />
636+
<Switch testID="off" value={false} />
637+
</View>,
638+
);
639+
expect(computeAriaChecked(screen.getByTestId('on'))).toBe(true);
640+
expect(computeAriaChecked(screen.getByTestId('off'))).toBe(false);
641+
});
642+
});
643+
644+
describe('computeAriaExpanded', () => {
645+
test('returns undefined by default', async () => {
646+
await render(<View testID="subject" />);
647+
expect(computeAriaExpanded(screen.getByTestId('subject'))).toBeUndefined();
648+
});
649+
650+
test('supports aria-expanded prop', async () => {
651+
await render(
652+
<View>
653+
<View testID="expanded" aria-expanded />
654+
<View testID="collapsed" aria-expanded={false} />
655+
</View>,
656+
);
657+
expect(computeAriaExpanded(screen.getByTestId('expanded'))).toBe(true);
658+
expect(computeAriaExpanded(screen.getByTestId('collapsed'))).toBe(false);
659+
});
660+
661+
test('supports accessibilityState.expanded', async () => {
662+
await render(<View testID="subject" accessibilityState={{ expanded: true }} />);
663+
expect(computeAriaExpanded(screen.getByTestId('subject'))).toBe(true);
664+
});
665+
});
666+
667+
describe('computeAriaSelected', () => {
668+
test('returns false by default', async () => {
669+
await render(<View testID="subject" />);
670+
expect(computeAriaSelected(screen.getByTestId('subject'))).toBe(false);
671+
});
672+
673+
test('supports aria-selected prop', async () => {
674+
await render(<View testID="subject" aria-selected />);
675+
expect(computeAriaSelected(screen.getByTestId('subject'))).toBe(true);
676+
});
677+
678+
test('supports accessibilityState.selected', async () => {
679+
await render(<View testID="subject" accessibilityState={{ selected: true }} />);
680+
expect(computeAriaSelected(screen.getByTestId('subject'))).toBe(true);
681+
});
682+
});
683+
684+
describe('computeAriaValue', () => {
685+
test('returns empty values by default', async () => {
686+
await render(<View testID="subject" />);
687+
expect(computeAriaValue(screen.getByTestId('subject'))).toEqual({
688+
min: undefined,
689+
max: undefined,
690+
now: undefined,
691+
text: undefined,
692+
});
693+
});
694+
695+
test('supports aria-value* props', async () => {
696+
await render(
697+
<View
698+
testID="subject"
699+
aria-valuemin={0}
700+
aria-valuemax={100}
701+
aria-valuenow={50}
702+
aria-valuetext="50%"
703+
/>,
704+
);
705+
expect(computeAriaValue(screen.getByTestId('subject'))).toEqual({
706+
min: 0,
707+
max: 100,
708+
now: 50,
709+
text: '50%',
710+
});
711+
});
712+
713+
test('supports accessibilityValue prop', async () => {
714+
await render(
715+
<View testID="subject" accessibilityValue={{ min: 0, max: 100, now: 25, text: '25%' }} />,
716+
);
717+
expect(computeAriaValue(screen.getByTestId('subject'))).toEqual({
718+
min: 0,
719+
max: 100,
720+
now: 25,
721+
text: '25%',
722+
});
723+
});
724+
725+
test('aria-value* props take precedence over accessibilityValue', async () => {
726+
await render(
727+
<View
728+
testID="subject"
729+
aria-valuenow={75}
730+
accessibilityValue={{ min: 0, max: 100, now: 50, text: '50%' }}
731+
/>,
732+
);
733+
const value = computeAriaValue(screen.getByTestId('subject'));
734+
expect(value.now).toBe(75);
735+
expect(value.min).toBe(0);
736+
});
737+
});
738+
485739
describe('computeAccessibleName', () => {
486740
test('basic cases', async () => {
487741
await render(

src/helpers/accessibility.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@ export function isHiddenFromAccessibility(
5353
export const isInaccessible = isHiddenFromAccessibility;
5454

5555
function isSubtreeInaccessible(element: HostElement): boolean {
56-
// Null props can happen for React.Fragments
57-
if (element.props == null) {
58-
return false;
59-
}
60-
6156
// See: https://reactnative.dev/docs/accessibility#aria-hidden
6257
if (element.props['aria-hidden']) {
6358
return true;

0 commit comments

Comments
 (0)