Skip to content

Commit fe039b5

Browse files
committed
refactor(test): replace Task.Delay with explicit polling and fix XAML HR tests
- Replace all Task.Delay waits with polling helpers per PR review - Add WaitForPageReplacementAsync for XAML HR page instance changes - Add WaitForPageMatchingAsync for content-aware XAML HR polling - Add WaitForTabAbsentAsync for tab region removal verification - Fix tests 8/9 race condition with prior test file reverts - Fix Test 10 to re-resolve page/navigator after XAML HR revert
1 parent d7eb683 commit fe039b5

2 files changed

Lines changed: 125 additions & 30 deletions

File tree

src/Uno.Extensions.Navigation.UI.Tests/Given_TabBarHotReload.cs

Lines changed: 124 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

src/Uno.Extensions.Navigation.UI.Tests/HotReloadRouteGate.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ internal static class HotReloadRouteGate
99
{
1010
internal static bool IsAvailable()
1111
{
12-
return false;
12+
return true;
1313
}
1414
}

0 commit comments

Comments
 (0)