@@ -267,8 +267,8 @@ public async Task When_GatedTabUnlockedByHR_Then_TabContentLoads(CancellationTok
267267 hostPage . TabBar , TimeSpan . FromSeconds ( 30 ) , ct ) ;
268268 await tabBarNavigator . NavigateRouteAsync ( hostPage , "TabThree" ) ;
269269
270- // Give the redirect a moment to settle, then verify TabThree content is absent .
271- await Task . Delay ( 500 , ct ) ;
270+ // Poll until TabThree is confirmed absent (redirect to TabOne should happen quickly) .
271+ await WaitForTabAbsentAsync ( hostPage . ContentGrid , "TabThree" , TimeSpan . FromSeconds ( 5 ) , ct ) ;
272272 var tabThreeBeforeHR = FindTabContentVm ( hostPage . ContentGrid , "TabThree" ) ;
273273 tabThreeBeforeHR . Should ( ) . BeNull (
274274 "while the Init gate is closed, TabThree should not populate content" ) ;
@@ -319,11 +319,9 @@ public async Task When_RegionAttachedRemovedFromTabBarViaXamlHR_Then_ContentNotB
319319 """<utu:TabBar x:Name="TB" Grid.Row="1">""" ,
320320 ct ) ;
321321
322- // Give the visual tree time to settle after XAML HR.
323- await Task . Delay ( 1000 , ct ) ;
324-
325- // XAML HR replaces the page — re-resolve to check the NEW page's state.
326- var activePage = ResolveCurrentPage < HotReloadTabBarXamlPage > ( app . NavigationRoot ) ! ;
322+ // Wait for XAML HR to replace the page instance.
323+ var activePage = await WaitForPageReplacementAsync < HotReloadTabBarXamlPage > (
324+ app . NavigationRoot , hostPage , TimeSpan . FromSeconds ( 10 ) , ct ) ;
327325
328326 // The NEW page's content area should NOT be blank (#2971 causes this).
329327 activePage . ContentGrid . Children . Count . Should ( ) . BeGreaterThan ( 0 ,
@@ -373,12 +371,16 @@ public async Task When_RegionNameChangedOnTabBarItemViaXamlHR_Then_NavigationRes
373371 """<utu:TabBarItem Content="Tab Two" uen:Region.Name="TabTwoRenamed" IsSelectable="True" />""" ,
374372 ct ) ;
375373
376- await Task . Delay ( 1000 , ct ) ;
377-
378- // XAML HR replaces the page instance — re-resolve to get the new one.
379- var activePage = ResolveCurrentPage < HotReloadTabBarXamlPage > ( app . NavigationRoot ) ! ;
374+ // Wait for XAML HR to produce a page with the renamed region.
375+ // Uses content-aware polling because async XAML HR from a prior test's
376+ // file revert (same .xaml file) could race with this test's modification.
377+ var activePage = await WaitForPageMatchingAsync < HotReloadTabBarXamlPage > (
378+ app . NavigationRoot ,
379+ page => page . TabBar . Items . OfType < FrameworkElement > ( )
380+ . Any ( i => Uno . Extensions . Navigation . UI . Region . GetName ( i ) == "TabTwoRenamed" ) ,
381+ TimeSpan . FromSeconds ( 30 ) , ct ) ;
380382
381- // Verify the Region.Name DP was updated on the new page's TabBarItems .
383+ // Sanity-check: full region name list on the replaced page.
382384 var regionNames = activePage . TabBar . Items . OfType < FrameworkElement > ( )
383385 . Select ( i => Uno . Extensions . Navigation . UI . Region . GetName ( i ) )
384386 . ToList ( ) ;
@@ -389,9 +391,9 @@ public async Task When_RegionNameChangedOnTabBarItemViaXamlHR_Then_NavigationRes
389391 var activeNavigator = await WaitForTabBarNavigatorAsync (
390392 activePage . TabBar , TimeSpan . FromSeconds ( 30 ) , ct ) ;
391393 await activeNavigator . NavigateRouteAsync ( activePage , "TabTwoRenamed" ) ;
392- await Task . Delay ( 500 , ct ) ;
393394
394- var renamedVm = FindTabContentVm ( activePage . ContentGrid , "TabTwoRenamed" ) ;
395+ var renamedVm = await WaitForTabContentVmAsync (
396+ activePage . ContentGrid , "TabTwoRenamed" , TimeSpan . FromSeconds ( 30 ) , ct ) ;
395397 renamedVm . Should ( ) . NotBeNull (
396398 "Navigation should resolve the renamed Region.Name after XAML HR" ) ;
397399 }
@@ -402,7 +404,11 @@ public async Task When_RegionNameChangedOnTabBarItemViaXamlHR_Then_NavigationRes
402404
403405 /// <summary>
404406 /// XAML HR adds a third <c>TabBarItem</c> with Region.Name="TabThree".
405- /// The route is pre-registered so the SelectorNavigator can navigate to it.
407+ /// The route is pre-registered in <see cref="SetupXamlThreeRouteAppAsync"/> so
408+ /// the SelectorNavigator can resolve it. In a real application, adding a new
409+ /// TabBarItem via XAML HR would also require updating the route registration
410+ /// in C# (which would itself be a separate HR delta). This test isolates the
411+ /// XAML-side behavior by pre-registering the route ahead of time.
406412 /// XAML HR replaces the page instance — references must be re-resolved after HR.
407413 /// </summary>
408414 [ TestMethod ]
@@ -435,12 +441,15 @@ public async Task When_TabBarItemAddedViaXamlHR_Then_NewTabNavigable(Cancellatio
435441 replacementLines ,
436442 ct ) ;
437443
438- await Task . Delay ( 1000 , ct ) ;
444+ // Wait for XAML HR to produce a page with 3 tabs.
445+ // Uses content-aware polling because async XAML HR from a prior test's
446+ // file revert (same .xaml file) could race with this test's modification.
447+ var activePage = await WaitForPageMatchingAsync < HotReloadTabBarXamlPage > (
448+ app . NavigationRoot ,
449+ page => page . TabBar . Items . Count == 3 ,
450+ TimeSpan . FromSeconds ( 30 ) , ct ) ;
439451
440- // XAML HR replaces the page instance — re-resolve to get the new one.
441- var activePage = ResolveCurrentPage < HotReloadTabBarXamlPage > ( app . NavigationRoot ) ! ;
442-
443- // TabBar should now have 3 items on the replaced page.
452+ // Sanity-check: the replaced page has 3 TabBarItems.
444453 activePage . TabBar . Items . Count . Should ( ) . Be ( 3 ,
445454 "XAML HR should have added a third TabBarItem on the replaced page" ) ;
446455
@@ -488,7 +497,8 @@ public async Task When_CommandBindingRemovedAndRestoredViaXamlHR_Then_TabSwitchi
488497
489498 // Return to TabOne for the HR test.
490499 await tabBarNavigator . NavigateRouteAsync ( hostPage , "TabOne" ) ;
491- await Task . Delay ( 200 , ct ) ;
500+ await WaitForTabContentVmAsync (
501+ hostPage . ContentGrid , "TabOne" , TimeSpan . FromSeconds ( 30 ) , ct ) ;
492502
493503 // Phase 1: remove the Command binding.
494504 var revert = await HotReloadHelper . UpdateSourceFile (
@@ -499,20 +509,27 @@ public async Task When_CommandBindingRemovedAndRestoredViaXamlHR_Then_TabSwitchi
499509
500510 // Tab switching should still work without the Command.
501511 await tabBarNavigator . NavigateRouteAsync ( hostPage , "TabTwo" ) ;
502- await Task . Delay ( 200 , ct ) ;
503-
504- FindTabContentVm ( hostPage . ContentGrid , "TabTwo" ) . Should ( ) . NotBeNull (
512+ var tabTwoAfterRemoval = await WaitForTabContentVmAsync (
513+ hostPage . ContentGrid , "TabTwo" , TimeSpan . FromSeconds ( 30 ) , ct ) ;
514+ tabTwoAfterRemoval . Should ( ) . NotBeNull (
505515 "Tab switching should work after Command binding removal" ) ;
506516
507517 // Phase 2: file revert re-adds the Command binding via XAML HR.
508518 await revert . DisposeAsync ( ) ;
509- await Task . Delay ( 1000 , ct ) ;
510519
511- // #2912: tab switching should still work after Command is restored .
512- await tabBarNavigator . NavigateRouteAsync ( hostPage , "TabOne" ) ;
513- await Task . Delay ( 500 , ct ) ;
520+ // Wait for the reverted page to load (XAML HR replaces the page) .
521+ await WaitForPageReplacementAsync < HotReloadTabBarCommandPage > (
522+ app . NavigationRoot , hostPage , TimeSpan . FromSeconds ( 10 ) , ct ) ;
514523
515- FindTabContentVm ( hostPage . ContentGrid , "TabOne" ) . Should ( ) . NotBeNull (
524+ // #2912: tab switching should still work after Command is restored.
525+ // Re-resolve page and navigator since XAML HR replaced the instance.
526+ var revertedPage = ResolveCurrentPage < HotReloadTabBarCommandPage > ( app . NavigationRoot ) ! ;
527+ var revertedNavigator = await WaitForTabBarNavigatorAsync (
528+ revertedPage . TabBar , TimeSpan . FromSeconds ( 30 ) , ct ) ;
529+ await revertedNavigator . NavigateRouteAsync ( revertedPage , "TabOne" ) ;
530+ var tabOneAfterRestore = await WaitForTabContentVmAsync (
531+ revertedPage . ContentGrid , "TabOne" , TimeSpan . FromSeconds ( 30 ) , ct ) ;
532+ tabOneAfterRestore . Should ( ) . NotBeNull (
516533 "Tab switching should work after Command binding is restored (#2912)" ) ;
517534 }
518535
@@ -888,6 +905,84 @@ frame.Content is HotReloadTabContentPage page &&
888905 return root . Content as TPage ;
889906 }
890907
908+ /// <summary>
909+ /// Polls until XAML HR replaces the page instance (new object reference != old).
910+ /// </summary>
911+ private static async Task < TPage > WaitForPageReplacementAsync < TPage > (
912+ ContentControl root ,
913+ TPage oldPage ,
914+ TimeSpan timeout ,
915+ CancellationToken ct ) where TPage : class
916+ {
917+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
918+ while ( sw . Elapsed < timeout )
919+ {
920+ ct . ThrowIfCancellationRequested ( ) ;
921+ var current = ResolveCurrentPage < TPage > ( root ) ;
922+ if ( current is not null && ! ReferenceEquals ( current , oldPage ) )
923+ {
924+ return current ;
925+ }
926+ await Task . Delay ( 50 , ct ) ;
927+ }
928+
929+ throw new TimeoutException (
930+ $ "XAML HR did not replace the { typeof ( TPage ) . Name } instance within { timeout . TotalSeconds : F0} s.") ;
931+ }
932+
933+ /// <summary>
934+ /// Polls until the current page matches a content condition.
935+ /// Unlike <see cref="WaitForPageReplacementAsync{TPage}"/> which checks only
936+ /// for a different object reference, this waits for the page to have specific
937+ /// content — avoiding false positives from stale replacements caused by a
938+ /// prior test's async file revert on the same .xaml.
939+ /// </summary>
940+ private static async Task < TPage > WaitForPageMatchingAsync < TPage > (
941+ ContentControl root ,
942+ Func < TPage , bool > condition ,
943+ TimeSpan timeout ,
944+ CancellationToken ct ) where TPage : class
945+ {
946+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
947+ while ( sw . Elapsed < timeout )
948+ {
949+ ct . ThrowIfCancellationRequested ( ) ;
950+ var current = ResolveCurrentPage < TPage > ( root ) ;
951+ if ( current is not null && condition ( current ) )
952+ {
953+ return current ;
954+ }
955+ await Task . Delay ( 50 , ct ) ;
956+ }
957+
958+ throw new TimeoutException (
959+ $ "No { typeof ( TPage ) . Name } matching the expected content appeared within { timeout . TotalSeconds : F0} s.") ;
960+ }
961+
962+ /// <summary>
963+ /// Polls until a specific tab region is confirmed absent from the content grid.
964+ /// </summary>
965+ private static async Task WaitForTabAbsentAsync (
966+ Grid contentGrid ,
967+ string regionName ,
968+ TimeSpan timeout ,
969+ CancellationToken ct )
970+ {
971+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
972+ while ( sw . Elapsed < timeout )
973+ {
974+ ct . ThrowIfCancellationRequested ( ) ;
975+ if ( FindTabContentVm ( contentGrid , regionName ) is null )
976+ {
977+ return ;
978+ }
979+ await Task . Delay ( 50 , ct ) ;
980+ }
981+
982+ throw new TimeoutException (
983+ $ "Tab '{ regionName } ' was still present after { timeout . TotalSeconds : F0} s.") ;
984+ }
985+
891986 private static async Task WaitForRouteAsync (
892987 ContentControl root ,
893988 global ::Uno . Extensions . Navigation . INavigator nav ,
0 commit comments