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;
|
var nameInput = page.Locator("input[type='text'].form-control.form-control-sm").First;
|
||||||
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
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.FillAsync(keyName);
|
||||||
|
await nameInput.DispatchEventAsync("change");
|
||||||
await page.Locator($"#method-access-{_api.MethodId}").CheckAsync();
|
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']"))
|
await Assertions.Expect(page.Locator("[data-test='created-token']"))
|
||||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
.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;
|
var nameInput = page.Locator("input[type='text'].form-control.form-control-sm").First;
|
||||||
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||||
|
|
||||||
// Fill the name but leave ALL method checkboxes unchecked so only the method rule fires.
|
var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" });
|
||||||
// The name input uses Blazor default @bind (onchange), which only commits to the server
|
var error = page.Locator("div.text-danger.small");
|
||||||
// (_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),
|
// ROOT CAUSE of the under-load flake: the page is InteractiveServer with prerendering,
|
||||||
// otherwise the name-required rule short-circuits first.
|
// 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.FillAsync(CliRunner.UniqueName("apikey"));
|
||||||
await nameInput.DispatchEventAsync("change");
|
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).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||||
await Assertions.Expect(error).ToContainTextAsync("Select at least one API method for this key.");
|
await Assertions.Expect(error).ToContainTextAsync("Select at least one API method for this key.");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user