@@ -617,15 +617,217 @@ test('a toolbar host enclosing the bars of a presentation stacked over it keeps
617617 for ( const label of COVER_CONTROLS ) assert . ok ( ! covered . includes ( label ) ) ;
618618} ) ;
619619
620- test ( 'a toolbar host enclosing the bars of a presentation beneath it keeps covering ' , ( ) => {
620+ test ( 'a toolbar host enclosing the bars of a presentation beneath it covers that presentation, not its own ' , ( ) => {
621621 // The capture without the cover: a sheet host enclosing Home's bar and list, which sit before Home's
622- // own host, covers Home. It covers the sheet too, failing closed; iOS publishes no presentation
623- // beneath a sheet, so this shape is not captured live.
622+ // own host, covers Home, including the tab bar listed right after Home's host, but not the sheet.
624623 const rows = COVER_OVER_SHEET_ROWS . filter ( ( [ index ] ) => ! COVER_ROW_INDEXES . has ( index ) ) ;
625624 const covered = coveredLabels ( capturedTree ( rows ) ) ;
626625
627- assert . ok ( covered . includes ( 'Edit' ) ) ;
628- assert . ok ( covered . includes ( 'Open medium sheet' ) ) ;
626+ for ( const label of [ 'Edit' , 'Open medium sheet' , 'Tab Bar' , 'Home' ] ) {
627+ assert . ok ( covered . includes ( label ) , `${ label } sits beneath the sheet` ) ;
628+ }
629+ for ( const label of [ 'Close' , 'Primary action' , 'Row 1' , 'Bottom' ] ) {
630+ assert . ok ( ! covered . includes ( label ) , `${ label } is on the sheet` ) ;
631+ }
632+ } ) ;
633+
634+ // `snapshot --json` of an undimmed `formSheet` over a tab screen on an iOS 27 simulator, trimmed. It
635+ // keeps the screen and tab bar beneath it in the capture, and no sibling matches the host's frame.
636+ const UNDIMMED_SHEET_ROWS : CapturedNode [ ] = [
637+ [ 0 , undefined , 'Application' , 'UIApplication' , 'Agent Device Tester' , [ 0 , 0 , 402 , 874 ] ] ,
638+ [ 1 , 0 , 'ScrollView' , 'RCTEnhancedScrollView' , undefined , [ 18 , 62 , 366 , 729 ] ] ,
639+ [ 18 , 1 , 'Button' , 'RCTViewComponentView' , 'Open undimmed sheet' , [ 34.33 , 515 , 333.33 , 44.67 ] ] ,
640+ [ 26 , 0 , 'TabBar' , 'UITabBar' , 'Tab Bar' , [ 0 , 791 , 402 , 83 ] ] ,
641+ [ 35 , 26 , 'Button' , '_UITabButton' , 'Settings' , [ 286.81 , 795 , 90.19 , 54 ] ] ,
642+ [ 37 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 0 , 402 , 874 ] ] ,
643+ [ 38 , 0 , 'NavigationBar' , 'UINavigationBar' , 'Undimmed sheet' , [ 0 , 78 , 402 , 54 ] ] ,
644+ [ 40 , 0 , 'Other' , 'RCTScrollViewComponentView' , undefined , [ 0 , 62 , 402 , 723.33 ] ] ,
645+ [ 41 , 40 , 'ScrollView' , 'RCTEnhancedScrollView' , undefined , [ 0 , 62 , 402 , 723.33 ] ] ,
646+ [ 44 , 41 , 'TextField' , 'RCTUITextField' , 'Sheet name' , [ 20.33 , 107.33 , 361.33 , 51.33 ] ] ,
647+ [ 47 , 41 , 'Button' , 'RCTViewComponentView' , 'Close undimmed sheet' , [ 20 , 209 , 362 , 44.67 ] ] ,
648+ [ 52 , 0 , 'Button' , 'RCTViewComponentView' , 'Continue' , [ 20 , 797.33 , 362 , 44.67 ] ] ,
649+ [ 53 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 62 , 402 , 812 ] ] ,
650+ ] ;
651+
652+ const UNDIMMED_SHEET_CONTROLS = [
653+ 'Undimmed sheet' ,
654+ 'Sheet name' ,
655+ 'Close undimmed sheet' ,
656+ 'Continue' ,
657+ ] ;
658+
659+ test ( 'the captured iOS 27 undimmed sheet keeps its controls pressable and covers the screen beneath' , ( ) => {
660+ const covered = coveredLabels ( capturedTree ( UNDIMMED_SHEET_ROWS ) ) ;
661+
662+ for ( const label of UNDIMMED_SHEET_CONTROLS ) {
663+ assert . ok ( ! covered . includes ( label ) , `${ label } is on the sheet` ) ;
664+ }
665+ for ( const label of [ 'Open undimmed sheet' , 'Tab Bar' , 'Settings' ] ) {
666+ assert . ok ( covered . includes ( label ) , `${ label } sits beneath the sheet` ) ;
667+ }
668+ } ) ;
669+
670+ test ( 'a tab bar listed right after the presenting host stays beneath the undimmed sheet' , ( ) => {
671+ // The SwiftUI capture order: the presenting screen's tab bar follows that screen's host instead of
672+ // preceding it. It still belongs to the screen beneath, so the sheet's host keeps covering it.
673+ const tabBarRows = UNDIMMED_SHEET_ROWS . filter ( ( [ index ] ) => index === 26 || index === 35 ) ;
674+ const rows = UNDIMMED_SHEET_ROWS . filter ( ( [ index ] ) => index !== 26 && index !== 35 ) ;
675+ rows . splice ( rows . findIndex ( ( [ index ] ) => index === 37 ) + 1 , 0 , ...tabBarRows ) ;
676+ const covered = coveredLabels ( capturedTree ( rows ) ) ;
677+
678+ for ( const label of [ 'Tab Bar' , 'Settings' ] ) {
679+ assert . ok ( covered . includes ( label ) , `${ label } sits beneath the sheet` ) ;
680+ }
681+ for ( const label of UNDIMMED_SHEET_CONTROLS ) {
682+ assert . ok ( ! covered . includes ( label ) , `${ label } is on the sheet` ) ;
683+ }
684+ } ) ;
685+
686+ test ( "a sheet control listed before the sheet's first bar stays pressable" , ( ) => {
687+ // A grabber and a leading button sit between the presenting host and the sheet's navigation bar.
688+ // They belong to the sheet, so only a tab bar there is read as the screen beneath.
689+ const rows = [ ...UNDIMMED_SHEET_ROWS ] ;
690+ rows . splice (
691+ rows . findIndex ( ( [ index ] ) => index === 37 ) + 1 ,
692+ 0 ,
693+ [ 60 , 0 , 'Button' , '_UIGrabber' , 'Sheet Grabber' , [ 153 , 64 , 96 , 10 ] ] ,
694+ [ 61 , 0 , 'Button' , 'RCTViewComponentView' , 'Leading action' , [ 300 , 140 , 80 , 36 ] ] ,
695+ ) ;
696+ const covered = coveredLabels ( capturedTree ( rows ) ) ;
697+
698+ for ( const label of [ 'Sheet Grabber' , 'Leading action' , ...UNDIMMED_SHEET_CONTROLS ] ) {
699+ assert . ok ( ! covered . includes ( label ) , `${ label } is on the sheet` ) ;
700+ }
701+ } ) ;
702+
703+ test ( 'a lone screen keeps its tab bar uncovered by a host with no presentation before it' , ( ) => {
704+ // With no previous toolbar host there is no screen beneath, so the tab bar is the screen's own.
705+ const nodes = capturedTree ( [
706+ [ 0 , undefined , 'Application' , 'UIApplication' , 'App' , [ 0 , 0 , 402 , 874 ] ] ,
707+ [ 1 , 0 , 'TabBar' , 'UITabBar' , 'Tab Bar' , [ 0 , 791 , 402 , 83 ] ] ,
708+ [ 2 , 1 , 'Button' , '_UITabButton' , 'Home' , [ 25 , 795 , 98 , 54 ] ] ,
709+ [ 3 , 0 , 'NavigationBar' , 'UINavigationBar' , 'Title' , [ 0 , 62 , 402 , 54 ] ] ,
710+ [ 4 , 0 , 'CollectionView' , 'UICollectionView' , undefined , [ 0 , 116 , 402 , 675 ] ] ,
711+ [ 5 , 4 , 'Button' , 'UIButton' , 'Row' , [ 16 , 132 , 370 , 52 ] ] ,
712+ [ 6 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 62 , 402 , 812 ] ] ,
713+ ] ) ;
714+
715+ assert . deepEqual ( coveredLabels ( nodes ) , [ ] ) ;
716+ } ) ;
717+
718+ test ( 'a host listing its own tab bar first keeps it pressable when it encloses nothing beneath' , ( ) => {
719+ // After a previous host, a tab bar listed first counts as the screen beneath only when the host also
720+ // encloses a region before that previous host.
721+ const nodes = capturedTree ( [
722+ [ 0 , undefined , 'Application' , 'UIApplication' , 'App' , [ 0 , 0 , 402 , 874 ] ] ,
723+ [ 1 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 0 , 402 , 874 ] ] ,
724+ [ 2 , 0 , 'TabBar' , 'UITabBar' , 'Tab Bar' , [ 0 , 791 , 402 , 83 ] ] ,
725+ [ 3 , 2 , 'Button' , '_UITabButton' , 'Home' , [ 25 , 795 , 98 , 54 ] ] ,
726+ [ 4 , 0 , 'NavigationBar' , 'UINavigationBar' , 'Title' , [ 0 , 62 , 402 , 54 ] ] ,
727+ [ 5 , 0 , 'CollectionView' , 'UICollectionView' , undefined , [ 0 , 116 , 402 , 675 ] ] ,
728+ [ 6 , 5 , 'Button' , 'UIButton' , 'Row' , [ 16 , 132 , 370 , 52 ] ] ,
729+ [ 7 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 62 , 402 , 812 ] ] ,
730+ ] ) ;
731+
732+ assert . deepEqual ( coveredLabels ( nodes ) , [ ] ) ;
733+ } ) ;
734+
735+ test ( 'a bottom host enclosing nothing but a tab bar keeps that tab bar pressable' , ( ) => {
736+ // A host after a viewport-sized one that hosts only the tab bar: with no other region of its own,
737+ // the tab bar is its own presentation's, as before.
738+ const nodes = capturedTree ( [
739+ [ 0 , undefined , 'Application' , 'UIApplication' , 'App' , [ 0 , 0 , 402 , 874 ] ] ,
740+ [ 1 , 0 , 'NavigationBar' , 'UINavigationBar' , 'Title' , [ 0 , 62 , 402 , 54 ] ] ,
741+ [ 2 , 0 , 'CollectionView' , 'UICollectionView' , undefined , [ 0 , 116 , 402 , 675 ] ] ,
742+ [ 3 , 2 , 'Button' , 'UIButton' , 'Row' , [ 16 , 132 , 370 , 52 ] ] ,
743+ [ 4 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 0 , 402 , 874 ] ] ,
744+ [ 5 , 0 , 'TabBar' , 'UITabBar' , 'Tab Bar' , [ 0 , 791 , 402 , 83 ] ] ,
745+ [ 6 , 5 , 'Button' , '_UITabButton' , 'Home' , [ 25 , 795 , 98 , 54 ] ] ,
746+ [ 7 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 700 , 402 , 174 ] ] ,
747+ ] ) ;
748+
749+ assert . deepEqual ( coveredLabels ( nodes ) , [ ] ) ;
750+ } ) ;
751+
752+ test ( 'a barless undimmed sheet still covers the tab bar listed right after the presenting host' , ( ) => {
753+ // The sheet keeps only its footer button: no bar or scroll view of its own. The presenting screen's
754+ // scroll view is still enclosed, so the tab bar after the presenting host stays beneath the sheet.
755+ const tabBarRows = UNDIMMED_SHEET_ROWS . filter ( ( [ index ] ) => index === 26 || index === 35 ) ;
756+ const rows = UNDIMMED_SHEET_ROWS . filter (
757+ ( [ index ] ) => ! [ 26 , 35 , 38 , 40 , 41 , 44 , 47 ] . includes ( index ) ,
758+ ) ;
759+ rows . splice ( rows . findIndex ( ( [ index ] ) => index === 37 ) + 1 , 0 , ...tabBarRows ) ;
760+ const covered = coveredLabels ( capturedTree ( rows ) ) ;
761+
762+ for ( const label of [ 'Tab Bar' , 'Settings' , 'Open undimmed sheet' ] ) {
763+ assert . ok ( covered . includes ( label ) , `${ label } sits beneath the sheet` ) ;
764+ }
765+ assert . ok ( ! covered . includes ( 'Continue' ) , 'Continue is on the sheet' ) ;
766+ } ) ;
767+
768+ test ( 'the captured iOS 27 undimmed sheet with no bar or scroll view keeps its controls pressable' , ( ) => {
769+ // `snapshot --json` of the `UNDIMMED_SHEET_ROWS` sheet with its header hidden and a plain view for
770+ // content. Its host encloses only the screen beneath, so nothing of the sheet's own marks it.
771+ const covered = coveredLabels (
772+ capturedTree ( [
773+ [ 0 , undefined , 'Application' , 'UIApplication' , 'Agent Device Tester' , [ 0 , 0 , 402 , 874 ] ] ,
774+ [ 1 , 0 , 'ScrollView' , 'RCTEnhancedScrollView' , undefined , [ 18 , 62 , 366 , 729 ] ] ,
775+ [
776+ 27 ,
777+ 1 ,
778+ 'Button' ,
779+ 'RCTViewComponentView' ,
780+ 'Open undimmed sheet' ,
781+ [ 34.33 , 655.33 , 333.33 , 44.67 ] ,
782+ ] ,
783+ [ 34 , 0 , 'TabBar' , 'UITabBar' , 'Tab Bar' , [ 0 , 791 , 402 , 83 ] ] ,
784+ [ 43 , 34 , 'Button' , '_UITabButton' , 'Settings' , [ 286.81 , 795 , 90.19 , 54 ] ] ,
785+ [ 45 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 0 , 402 , 874 ] ] ,
786+ [ 46 , 0 , 'Other' , 'RCTViewComponentView' , undefined , [ 0 , 62 , 402 , 211.67 ] ] ,
787+ [ 47 , 0 , 'StaticText' , 'RCTParagraphComponentView' , 'Sheet name' , [ 20 , 82 , 362 , 17 ] ] ,
788+ [ 49 , 0 , 'TextField' , 'RCTUITextField' , 'Sheet name' , [ 20.33 , 107.33 , 361.33 , 51.33 ] ] ,
789+ [ 50 , 0 , 'StaticText' , 'RCTParagraphComponentView' , 'Not continued' , [ 20 , 175 , 362 , 18 ] ] ,
790+ [ 52 , 0 , 'Button' , 'RCTViewComponentView' , 'Close undimmed sheet' , [ 20 , 209 , 362 , 44.67 ] ] ,
791+ [ 53 , 0 , 'Button' , 'RCTViewComponentView' , 'Continue' , [ 20 , 285.67 , 362 , 44.67 ] ] ,
792+ [ 54 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 62 , 402 , 812 ] ] ,
793+ ] ) ,
794+ ) ;
795+
796+ for ( const label of [ 'Sheet name' , 'Not continued' , 'Close undimmed sheet' , 'Continue' ] ) {
797+ assert . ok ( ! covered . includes ( label ) , `${ label } is on the sheet` ) ;
798+ }
799+ for ( const label of [ 'Open undimmed sheet' , 'Tab Bar' , 'Settings' ] ) {
800+ assert . ok ( covered . includes ( label ) , `${ label } sits beneath the sheet` ) ;
801+ }
802+ } ) ;
803+
804+ test ( 'a bar-height toolbar after a host that encloses nothing keeps covering the field under it' , ( ) => {
805+ // A toolbar enclosing no bar or scroll view gives no evidence that it is a host, so it is read as an
806+ // opaque bar even with a previous host before it.
807+ const nodes = capturedTree ( [
808+ [ 0 , undefined , 'Application' , 'UIApplication' , 'App' , [ 0 , 0 , 402 , 874 ] ] ,
809+ [ 1 , 0 , 'ScrollView' , 'UIScrollView' , undefined , [ 0 , 62 , 402 , 812 ] ] ,
810+ [ 2 , 1 , 'Button' , 'UIButton' , 'Row' , [ 16 , 132 , 370 , 52 ] ] ,
811+ [ 3 , 0 , 'Toolbar' , '_UIFloatingBarContainerView' , 'Toolbar' , [ 0 , 0 , 402 , 874 ] ] ,
812+ [ 4 , 0 , 'TextField' , 'UITextField' , 'Field under accessory' , [ 16 , 505 , 370 , 34 ] ] ,
813+ [ 5 , 0 , 'Toolbar' , 'UIToolbar' , 'Accessory' , [ 0 , 500 , 402 , 44 ] ] ,
814+ ] ) ;
815+
816+ assert . deepEqual ( coveredLabels ( nodes ) , [ 'Field under accessory' ] ) ;
817+ } ) ;
818+
819+ test ( 'an undimmed sheet host stops exempting its own controls once a presentation is stacked over it' , ( ) => {
820+ // Non-vacuity for the stacked direction: a bar listed after the host, inside its frame, belongs to a
821+ // presentation drawn over the sheet, so the host keeps covering the sheet as before.
822+ const rows : CapturedNode [ ] = [
823+ ...UNDIMMED_SHEET_ROWS ,
824+ [ 54 , 0 , 'NavigationBar' , 'UINavigationBar' , 'Upper sheet' , [ 0 , 88 , 402 , 54 ] ] ,
825+ ] ;
826+ const covered = coveredLabels ( capturedTree ( rows ) ) ;
827+
828+ for ( const label of [ 'Sheet name' , 'Close undimmed sheet' , 'Continue' ] ) {
829+ assert . ok ( covered . includes ( label ) , `${ label } sits beneath the upper presentation` ) ;
830+ }
629831} ) ;
630832
631833test . for ( [
@@ -674,7 +876,7 @@ test.for([
674876] satisfies { name : string ; rows : CapturedNode [ ] } [ ] ) (
675877 'the captured iOS 27 $name publishes only the top sheet, and none of it is covered' ,
676878 ( { rows } ) => {
677- // iOS drops the presentation beneath a sheet from the capture, so no lower control gets a ref.
879+ // iOS drops the presentation beneath a SwiftUI sheet from the capture, so no lower control gets a ref.
678880 assert . deepEqual ( coveredLabels ( capturedTree ( rows ) ) , [ ] ) ;
679881 } ,
680882) ;
0 commit comments