@@ -4,9 +4,16 @@ import { Image, Pressable, Switch, Text, TextInput, TouchableOpacity, View } fro
44import { isHiddenFromAccessibility , isInaccessible , render , screen } from '../..' ;
55import {
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
1219describe ( '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
417477describe ( '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+
485739describe ( 'computeAccessibleName' , ( ) => {
486740 test ( 'basic cases' , async ( ) => {
487741 await render (
0 commit comments