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