Просмотр исходного кода

Fix flaky E2E add: wait for the Blazor circuit before typing

Blazor Server prerenders every page before the circuit attaches, and input
events sent to that static HTML are silently lost — so a fast test run could
fill the add form, click add, and be told "name is required" because the
server-side model never saw the keystrokes. CI runners lose that race
intermittently (seen on OtherCardTests); a fast machine wins it, which is why
it never reproduced locally.

MainLayout now renders a hidden probe that flips to data-circuit-ready="true"
on the first OnAfterRender — which never runs during prerendering, so the flip
proves a live circuit. The add page object waits on the probe before typing.

Reproduced deterministically first with the existing BlazorLatency helper
(300ms per SignalR frame widens the attach window from milliseconds to
seconds): fails on the old page object, passes with the wait. That repro
stays as a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tim Jones 10 часов назад
Родитель
Сommit
b8f6d23442

+ 14 - 0
Shared.Rcl/Layout/MainLayout.razor

@@ -6,6 +6,11 @@
 @inject NavigationManager Nav
 @inject IJSRuntime JS
 
+<!-- Flips to true only after the first interactive render, which cannot happen on
+     the prerendered HTML. Tests wait on it before typing: input events sent before
+     the circuit attaches are silently lost and never reach the server-side model. -->
+<span hidden data-testid="circuit-probe" data-circuit-ready="@(_circuitReady ? "true" : "false")"></span>
+
 <div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono"
      data-testid="app-root">
 
@@ -142,6 +147,7 @@
     private const string _outsideDismissId = "rpk-mobile-nav";
     private const string _containerSelector = "#rpk-mobile-nav";
 
+    private bool _circuitReady;
     private bool _dropdownOpen;
     private bool _listenerRegistered;
     private DotNetObjectReference<MainLayout>? _selfRef;
@@ -169,6 +175,14 @@
 
     protected override async Task OnAfterRenderAsync(bool firstRender)
     {
+        // OnAfterRender never runs during prerendering, so this re-render proves a
+        // live circuit to anything watching the probe above.
+        if (firstRender && !_circuitReady)
+        {
+            _circuitReady = true;
+            StateHasChanged();
+        }
+
         // Mirror the dropdown state into the JS dismiss-listener registration
         // so the listener only exists while it's needed.
         if (_dropdownOpen && !_listenerRegistered)

+ 38 - 0
Tests.E2e/AddResourceRaceTests.cs

@@ -0,0 +1,38 @@
+using Microsoft.Playwright;
+using Tests.E2e.Infra;
+using Tests.E2e.PageObjectModels;
+using Xunit.Abstractions;
+
+namespace Tests.E2e;
+
+/// <summary>
+///     Blazor Server prerenders every page before the circuit attaches, and anything
+///     typed into that static HTML never reaches the server-side model. A fast test —
+///     or a fast typist on a slow link — can therefore submit the add form and be told
+///     "name is required" despite having filled it in. The injected latency makes the
+///     attach window, which CI runners only sometimes lose, wide enough to lose every
+///     time; the page object must wait for the circuit before it types.
+/// </summary>
+public class AddResourceRaceTests(
+    PlaywrightFixture fixture,
+    ITestOutputHelper output) : E2ETestBase(fixture, output) {
+    private readonly PlaywrightFixture _fixture = fixture;
+
+    [Fact]
+    public async Task Adding_A_Resource_Works_Before_The_Circuit_Has_Warmed_Up() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        await BlazorLatency.AddAsync(page, TimeSpan.FromMilliseconds(300));
+
+        var name = $"e2e-oth-{Guid.NewGuid():N}"[..16];
+
+        try {
+            await page.GotoAsync($"{_fixture.BaseUrl}/other/list");
+
+            var list = new OtherListPom(page);
+            await list.AddOtherAsync(name);
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+}

+ 5 - 0
Tests.E2e/PageObjectModels/AddResourceComponent.cs

@@ -34,6 +34,11 @@ public class AddResourceComponent(IPage page, string resourceType) {
     public async Task AddAsync(string name) {
         await Assertions.Expect(Root).ToBeVisibleAsync();
 
+        // Never type into the prerendered page: those input events are lost before
+        // the circuit attaches, and the submit then fails with an empty name.
+        await Assertions.Expect(page.GetByTestId("circuit-probe"))
+            .ToHaveAttributeAsync("data-circuit-ready", "true");
+
         await Input.FillAsync(name);
         await Button.ClickAsync();
     }