test(playwright): make ApiKey name-commit deterministic under full-suite load (flake fix)
This commit is contained in:
@@ -61,9 +61,28 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
var nameInput = page.Locator("input[type='text'].form-control.form-control-sm").First;
|
||||
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" });
|
||||
|
||||
// Same under-load hydration race as Create_NoMethods (see that test): this page is
|
||||
// InteractiveServer with prerendering, so the form renders (and is "visible") before
|
||||
// blazor.web.js hydrates the component and attaches its @bind onchange / @onchange
|
||||
// handlers over the SignalR circuit. Under full-suite load that hydration lands late,
|
||||
// so a name fill / checkbox check dispatched against the not-yet-live circuit is
|
||||
// silently dropped — SaveKey then short-circuits on the empty _formName (or empty
|
||||
// method set) and no key is created, so the token panel never appears.
|
||||
//
|
||||
// PROVE the circuit is interactive with an observable server round-trip first: clicking
|
||||
// Save on the empty form forces SaveKey to run and render "Name is required." (harmless,
|
||||
// creates nothing). Once that message appears the component is hydrated and its handlers
|
||||
// are live, so the subsequent name fill + checkbox check are guaranteed to round-trip.
|
||||
await saveButton.ClickAsync();
|
||||
await Assertions.Expect(page.Locator("div.text-danger.small"))
|
||||
.ToContainTextAsync("Name is required.", new() { Timeout = 15_000 });
|
||||
|
||||
await nameInput.FillAsync(keyName);
|
||||
await nameInput.DispatchEventAsync("change");
|
||||
await page.Locator($"#method-access-{_api.MethodId}").CheckAsync();
|
||||
await page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
|
||||
await saveButton.ClickAsync();
|
||||
|
||||
await Assertions.Expect(page.Locator("[data-test='created-token']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
@@ -129,16 +148,35 @@ public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
var nameInput = page.Locator("input[type='text'].form-control.form-control-sm").First;
|
||||
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
// Fill the name but leave ALL method checkboxes unchecked so only the method rule fires.
|
||||
// The name input uses Blazor default @bind (onchange), which only commits to the server
|
||||
// (_formName) on change — dispatch the change event explicitly to flush the bound value
|
||||
// before Save rather than relying on blur + NetworkIdle timing (which flakes under load),
|
||||
// otherwise the name-required rule short-circuits first.
|
||||
var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" });
|
||||
var error = page.Locator("div.text-danger.small");
|
||||
|
||||
// ROOT CAUSE of the under-load flake: the page is InteractiveServer with prerendering,
|
||||
// so the name <input> renders (and is "visible") from the prerendered HTML BEFORE
|
||||
// blazor.web.js hydrates this component and attaches its @bind onchange handler over the
|
||||
// SignalR circuit. Under full-suite load (circuit pressure from the 4-context cap) that
|
||||
// hydration lands late, so a name FillAsync + change dispatched against the not-yet-live
|
||||
// circuit is silently dropped — _formName stays empty server-side and SaveKey short-
|
||||
// circuits to "Name is required." (the rule checked BEFORE the methods rule in
|
||||
// ApiKeyForm.SaveKey). In isolation the circuit hydrates fast enough that this never bites.
|
||||
//
|
||||
// Fix: PROVE the circuit is interactive with an observable server round-trip before we
|
||||
// rely on the name commit. Clicking Save with an empty name forces SaveKey to run on the
|
||||
// server and render the "Name is required." message — a circuit-driven re-render. Waiting
|
||||
// for that message guarantees the component is hydrated and its onchange handler is live.
|
||||
// Only THEN do we fill the name + dispatch change (now guaranteed to round-trip) and Save
|
||||
// again, so _formName is committed and the methods rule — not the name rule — fires.
|
||||
await saveButton.ClickAsync();
|
||||
await Assertions.Expect(error).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await Assertions.Expect(error).ToContainTextAsync("Name is required.");
|
||||
|
||||
// Circuit is now provably interactive. Commit the name as its own discrete change message
|
||||
// (ordered before the Save click over the in-order SignalR channel), leaving ALL method
|
||||
// checkboxes unchecked so only the method rule can fire.
|
||||
await nameInput.FillAsync(CliRunner.UniqueName("apikey"));
|
||||
await nameInput.DispatchEventAsync("change");
|
||||
await page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
|
||||
await saveButton.ClickAsync();
|
||||
|
||||
var error = page.Locator("div.text-danger.small");
|
||||
await Assertions.Expect(error).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await Assertions.Expect(error).ToContainTextAsync("Select at least one API method for this key.");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user