@@ -404,6 +404,160 @@ describe('RNPickerSelect', () => {
404404 expect ( touchable . type ( ) . displayName ) . toEqual ( 'View' ) ;
405405 } ) ;
406406
407+ describe ( 'Android headless mode accessibility' , ( ) => {
408+ beforeEach ( ( ) => {
409+ Platform . OS = 'android' ;
410+ } ) ;
411+
412+ it ( 'should have accessibility props on the wrapper (Android headless)' , ( ) => {
413+ const wrapper = shallow (
414+ < RNPickerSelect
415+ items = { selectItems }
416+ onValueChange = { noop }
417+ useNativeAndroidPickerStyle = { false }
418+ pickerProps = { {
419+ accessibilityLabel : 'Select an item' ,
420+ } }
421+ />
422+ ) ;
423+
424+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
425+
426+ expect ( touchable . props ( ) . accessible ) . toEqual ( true ) ;
427+ expect ( touchable . props ( ) . accessibilityRole ) . toEqual ( 'combobox' ) ;
428+ expect ( touchable . props ( ) . accessibilityLabel ) . toEqual ( 'Select an item' ) ;
429+ expect ( touchable . props ( ) . accessibilityState ) . toEqual ( { disabled : false } ) ;
430+ expect ( touchable . props ( ) . accessibilityActions ) . toEqual ( [ { name : 'activate' } ] ) ;
431+ expect ( touchable . props ( ) . onAccessibilityAction ) . toBeDefined ( ) ;
432+ } ) ;
433+
434+ it ( 'should use accessibilityLabel from pickerProps (Android headless)' , ( ) => {
435+ const wrapper = shallow (
436+ < RNPickerSelect
437+ items = { selectItems }
438+ placeholder = { { } }
439+ onValueChange = { noop }
440+ useNativeAndroidPickerStyle = { false }
441+ value = "orange"
442+ pickerProps = { {
443+ accessibilityLabel : 'Choose a color' ,
444+ } }
445+ />
446+ ) ;
447+
448+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
449+
450+ expect ( touchable . props ( ) . accessibilityLabel ) . toEqual ( 'Choose a color' ) ;
451+ } ) ;
452+
453+ it ( 'should have undefined accessibilityLabel when not provided via pickerProps (Android headless)' , ( ) => {
454+ const wrapper = shallow (
455+ < RNPickerSelect
456+ items = { selectItems }
457+ placeholder = { { } }
458+ onValueChange = { noop }
459+ useNativeAndroidPickerStyle = { false }
460+ value = "orange"
461+ />
462+ ) ;
463+
464+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
465+
466+ expect ( touchable . props ( ) . accessibilityLabel ) . toBeUndefined ( ) ;
467+ } ) ;
468+
469+ it ( 'should have importantForAccessibility on inner container (Android headless)' , ( ) => {
470+ const wrapper = shallow (
471+ < RNPickerSelect
472+ items = { selectItems }
473+ onValueChange = { noop }
474+ useNativeAndroidPickerStyle = { false }
475+ />
476+ ) ;
477+
478+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
479+ const innerContainer = touchable . children ( ) . first ( ) ;
480+
481+ expect ( innerContainer . props ( ) . importantForAccessibility ) . toEqual ( 'no-hide-descendants' ) ;
482+ } ) ;
483+
484+ it ( 'should not trigger picker when disabled and accessibility action is called (Android headless)' , ( ) => {
485+ const wrapper = shallow (
486+ < RNPickerSelect
487+ items = { selectItems }
488+ onValueChange = { noop }
489+ useNativeAndroidPickerStyle = { false }
490+ disabled
491+ />
492+ ) ;
493+
494+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
495+ const onAccessibilityAction = touchable . props ( ) . onAccessibilityAction ;
496+
497+ // This should not throw and should be a no-op when disabled
498+ expect ( ( ) => {
499+ onAccessibilityAction ( { nativeEvent : { actionName : 'activate' } } ) ;
500+ } ) . not . toThrow ( ) ;
501+ } ) ;
502+
503+ it ( 'should set accessibilityState.disabled to true when disabled (Android headless)' , ( ) => {
504+ const wrapper = shallow (
505+ < RNPickerSelect
506+ items = { selectItems }
507+ onValueChange = { noop }
508+ useNativeAndroidPickerStyle = { false }
509+ disabled
510+ />
511+ ) ;
512+
513+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
514+
515+ expect ( touchable . props ( ) . accessibilityState ) . toEqual ( { disabled : true } ) ;
516+ } ) ;
517+
518+ it ( 'should call pickerRef.focus() when accessibility action "activate" is triggered (Android headless)' , ( ) => {
519+ const mockFocus = jest . fn ( ) ;
520+ const mockRef = { current : { focus : mockFocus } } ;
521+
522+ const wrapper = shallow (
523+ < RNPickerSelect
524+ items = { selectItems }
525+ onValueChange = { noop }
526+ useNativeAndroidPickerStyle = { false }
527+ pickerProps = { { ref : mockRef } }
528+ />
529+ ) ;
530+
531+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
532+ const onAccessibilityAction = touchable . props ( ) . onAccessibilityAction ;
533+
534+ onAccessibilityAction ( { nativeEvent : { actionName : 'activate' } } ) ;
535+
536+ expect ( mockFocus ) . toHaveBeenCalledTimes ( 1 ) ;
537+ } ) ;
538+
539+ it ( 'should not call pickerRef.focus() for non-activate actions (Android headless)' , ( ) => {
540+ const mockFocus = jest . fn ( ) ;
541+ const mockRef = { current : { focus : mockFocus } } ;
542+
543+ const wrapper = shallow (
544+ < RNPickerSelect
545+ items = { selectItems }
546+ onValueChange = { noop }
547+ useNativeAndroidPickerStyle = { false }
548+ pickerProps = { { ref : mockRef } }
549+ />
550+ ) ;
551+
552+ const touchable = wrapper . find ( '[testID="android_touchable_wrapper"]' ) ;
553+ const onAccessibilityAction = touchable . props ( ) . onAccessibilityAction ;
554+
555+ onAccessibilityAction ( { nativeEvent : { actionName : 'longpress' } } ) ;
556+
557+ expect ( mockFocus ) . not . toHaveBeenCalled ( ) ;
558+ } ) ;
559+ } ) ;
560+
407561 it ( 'should call the onClose callback when set' , ( ) => {
408562 Platform . OS = 'ios' ;
409563 const onCloseSpy = jest . fn ( ) ;
0 commit comments