fix(centralui): stabilise Site Calls + Audit grid Playwright E2E
Three Playwright E2E failures, all test-side timing/data bugs (no
feature defects found):
- AuditGridColumnTests.ColumnOrderAndWidths_PersistAcrossReload: read
sessionStorage synchronously right after Mouse.UpAsync, racing the
async OnColumnResized/OnColumnReordered JS->.NET->JS save round-trip.
Now polls (WaitForFunctionAsync) for the storage keys and for the
reorder re-render to settle; also hardens the flaky ReorderDrag test.
- SiteCallsPageTests.FilterNarrowing_ChannelFilterShrinksGrid: the
Target-keyword #sc-search @bind committed via the Query click's own
blur, racing change vs click on the circuit so Search() sometimes
ran with a stale empty filter. Commit the value with an explicit,
fully-awaited DispatchEventAsync('change') and use the retrying
ToHaveCount assertion for the negative row checks.
- SiteCallsPageTests.RetryClickThrough_OnParkedRow: seeded SourceSite
'plant-a' is not a real cluster site (site-a/b/c), so the relay had
no ClusterClient route and only resolved on the 10s inner Ask
timeout - past the 5s toast wait. Seed a live site (site-a) for a
fast NotParked round-trip and give the toast a 15s wait.
Playwright E2E suite: 60 passed, 0 failed, 0 skipped.
This commit is contained in:
@@ -51,6 +51,40 @@ public class SiteCallsPageTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Target-keyword search box and commits the value to the server
|
||||
/// as its own discrete circuit message before the caller clicks Query.
|
||||
/// <para>
|
||||
/// The <c>#sc-search</c> input is a Blazor <c>@bind</c>
|
||||
/// (commit-on-<c>change</c>): <see cref="ILocator.FillAsync"/> only fires
|
||||
/// <c>input</c> events, and the <c>change</c> that actually updates
|
||||
/// <c>_targetFilter</c> on the server fires on blur. The original test
|
||||
/// relied on the Query <c>ClickAsync</c> itself to blur the field — that
|
||||
/// makes the <c>change</c> (blur) and the <c>click</c> a single, near-
|
||||
/// simultaneous gesture and races them over the SignalR circuit: when the
|
||||
/// <c>click</c> is processed before the <c>change</c> has updated
|
||||
/// <c>_targetFilter</c>, <c>Search()</c> runs with a stale (empty) keyword
|
||||
/// and the grid returns unfiltered rows.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="ILocator.DispatchEventAsync"/> raises the <c>change</c> as a
|
||||
/// fully-awaited action of its own, so its circuit message is enqueued and
|
||||
/// sent before the later Query <c>ClickAsync</c>'s message. The SignalR
|
||||
/// connection delivers messages in send order and the Blazor circuit
|
||||
/// processes them sequentially, so <c>_targetFilter</c> is guaranteed
|
||||
/// committed before <c>Search()</c> runs — the two are no longer one
|
||||
/// racing gesture.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static async Task SetSearchKeywordAsync(IPage page, string keyword)
|
||||
{
|
||||
var search = page.Locator("#sc-search");
|
||||
await search.FillAsync(keyword);
|
||||
// Commit the @bind as a discrete change event — not a blur side effect
|
||||
// of the subsequent Query click.
|
||||
await search.DispatchEventAsync("change");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PageLoads_ForDeploymentUser()
|
||||
{
|
||||
@@ -98,22 +132,26 @@ public class SiteCallsPageTests
|
||||
|
||||
// Unfiltered query: both seeded rows appear (the Target keyword scopes
|
||||
// to this run so unrelated cluster rows do not interfere).
|
||||
await page.Locator("#sc-search").FillAsync(targetPrefix + "api");
|
||||
await SetSearchKeywordAsync(page, targetPrefix + "api");
|
||||
await page.Locator("[data-test='site-calls-query']").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Only the ApiOutbound row matches the exact target keyword.
|
||||
// Only the ApiOutbound row matches the exact target keyword. The
|
||||
// grid filters with an exact Target match, so the db row must be
|
||||
// absent — use the retrying ToHaveCount assertion so the negative
|
||||
// check waits out the post-query re-render rather than reading a
|
||||
// point-in-time count.
|
||||
await Assertions.Expect(page.Locator($"text={targetPrefix}api")).ToBeVisibleAsync();
|
||||
Assert.Equal(0, await page.Locator($"text={targetPrefix}db").CountAsync());
|
||||
await Assertions.Expect(page.Locator($"text={targetPrefix}db")).ToHaveCountAsync(0);
|
||||
|
||||
// Now filter by Channel = DbOutbound with the db target — the row flips.
|
||||
await page.Locator("#sc-search").FillAsync(targetPrefix + "db");
|
||||
await SetSearchKeywordAsync(page, targetPrefix + "db");
|
||||
await page.Locator("#sc-channel").SelectOptionAsync("DbOutbound");
|
||||
await page.Locator("[data-test='site-calls-query']").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await Assertions.Expect(page.Locator($"text={targetPrefix}db")).ToBeVisibleAsync();
|
||||
Assert.Equal(0, await page.Locator($"text={targetPrefix}api").CountAsync());
|
||||
await Assertions.Expect(page.Locator($"text={targetPrefix}api")).ToHaveCountAsync(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -142,7 +180,7 @@ public class SiteCallsPageTests
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SiteCallsUrl}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await page.Locator("#sc-search").FillAsync(targetPrefix + "endpoint");
|
||||
await SetSearchKeywordAsync(page, targetPrefix + "endpoint");
|
||||
await page.Locator("[data-test='site-calls-query']").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
@@ -199,7 +237,7 @@ public class SiteCallsPageTests
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Query the parked row first.
|
||||
await page.Locator("#sc-search").FillAsync(targetPrefix + "parked");
|
||||
await SetSearchKeywordAsync(page, targetPrefix + "parked");
|
||||
await page.Locator("[data-test='site-calls-query']").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
@@ -210,7 +248,7 @@ public class SiteCallsPageTests
|
||||
await Assertions.Expect(parkedRow.Locator("button:has-text('Discard')")).ToBeVisibleAsync();
|
||||
|
||||
// Now the Failed row — Retry/Discard are absent.
|
||||
await page.Locator("#sc-search").FillAsync(targetPrefix + "failed");
|
||||
await SetSearchKeywordAsync(page, targetPrefix + "failed");
|
||||
await page.Locator("[data-test='site-calls-query']").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
@@ -238,10 +276,18 @@ public class SiteCallsPageTests
|
||||
try
|
||||
{
|
||||
// A single Parked row — the only status from which Retry/Discard can
|
||||
// be relayed to the owning site.
|
||||
// be relayed to the owning site. Unlike the display-only tests above,
|
||||
// this one actually relays to the owning site, so the SourceSite must
|
||||
// be a *real* site identifier from the running cluster (site-a) and
|
||||
// not the cosmetic "plant-a" label: an unknown site has no registered
|
||||
// ClusterClient, so CentralCommunicationActor drops the envelope
|
||||
// without replying and the relay only resolves on the 10s inner Ask
|
||||
// timeout — too slow for the toast assertion below. Relayed to a live
|
||||
// site, the site finds no parked S&F message for this freshly-seeded
|
||||
// GUID and replies a fast NotParked ack, which still surfaces a toast.
|
||||
await SiteCallDataSeeder.InsertSiteCallAsync(
|
||||
trackedOperationId: parkedId, channel: "ApiOutbound", target: targetPrefix + "parked",
|
||||
sourceSite: "plant-a", status: "Parked", retryCount: 3,
|
||||
sourceSite: "site-a", status: "Parked", retryCount: 3,
|
||||
lastError: "HTTP 503 from ERP", httpStatus: 503,
|
||||
createdAtUtc: now, updatedAtUtc: now);
|
||||
|
||||
@@ -249,7 +295,7 @@ public class SiteCallsPageTests
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SiteCallsUrl}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await page.Locator("#sc-search").FillAsync(targetPrefix + "parked");
|
||||
await SetSearchKeywordAsync(page, targetPrefix + "parked");
|
||||
await page.Locator("[data-test='site-calls-query']").ClickAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
@@ -269,9 +315,14 @@ public class SiteCallsPageTests
|
||||
// the owning site is offline in this environment, SiteUnreachable.
|
||||
// We only assert that an outcome toast appears (exactly one — the
|
||||
// single-toast contract), not which one, since the live cluster
|
||||
// state determines the outcome.
|
||||
// state determines the outcome. The wait is generous (15s): the
|
||||
// relay round-trips to the site over ClusterClient, and a worst-case
|
||||
// path can sit on the 10s inner relay timeout before the response —
|
||||
// and the toast itself auto-dismisses 5s after it appears, so the
|
||||
// assertion must catch it inside that window.
|
||||
var toast = page.Locator(".toast");
|
||||
await Assertions.Expect(toast).ToBeVisibleAsync();
|
||||
await Assertions.Expect(toast).ToBeVisibleAsync(
|
||||
new() { Timeout = 15_000 });
|
||||
Assert.Equal(1, await toast.CountAsync());
|
||||
}
|
||||
finally
|
||||
|
||||
Reference in New Issue
Block a user