test(playwright): Site Calls status-filter + empty-state edge cases (Wave 4)

This commit is contained in:
Joseph Doherty
2026-06-07 03:58:52 -04:00
parent 79778e12b7
commit eea68b97f6
@@ -387,4 +387,108 @@ public class SiteCallsPageTests
await SiteCallDataSeeder.DeleteByTargetPrefixAsync(targetPrefix);
}
}
/// <summary>
/// The Status dropdown filters the grid to the selected lifecycle status.
/// Seeds two rows sharing a per-run Target prefix — one <c>Parked</c>, one
/// <c>Delivered</c> — then filters status=Parked: the Parked marker row
/// surfaces (with a <c>Parked</c> status badge) while the Delivered marker
/// row is excluded even though both share the prefix. This mirrors
/// <see cref="FilterNarrowing_ChannelFilterShrinksGrid"/> but exercises the
/// STATUS axis rather than the channel axis.
/// <para>
/// <c>SourceSite</c> is <c>site-a</c> (a permitted site) on both rows —
/// <c>FilterPermittedAsync</c> drops rows whose source site is not permitted.
/// </para>
/// </summary>
[SkippableFact]
public async Task StatusFilter_NarrowsToSelectedStatus()
{
Skip.IfNot(await SiteCallDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var runId = Guid.NewGuid().ToString("N");
var targetPrefix = $"playwright-test/wave4-sc/{runId}/";
var parkedId = Guid.NewGuid();
var deliveredId = Guid.NewGuid();
var now = DateTime.UtcNow;
try
{
// Two rows sharing the prefix: one Parked, one Delivered. site-a is a
// permitted source site (FilterPermittedAsync keeps it).
await SiteCallDataSeeder.InsertSiteCallAsync(
trackedOperationId: parkedId, channel: "ApiOutbound", target: targetPrefix + "parked",
sourceSite: "site-a", status: "Parked", retryCount: 3,
lastError: "HTTP 503 from ERP", httpStatus: 503,
createdAtUtc: now, updatedAtUtc: now);
await SiteCallDataSeeder.InsertSiteCallAsync(
trackedOperationId: deliveredId, channel: "ApiOutbound", target: targetPrefix + "delivered",
sourceSite: "site-a", status: "Delivered", retryCount: 0,
createdAtUtc: now, updatedAtUtc: now, httpStatus: 200, terminalAtUtc: now);
var page = await _fixture.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SiteCallsUrl}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Filter status=Parked and scope to the exact Parked marker — the row
// surfaces and its status badge reads Parked.
await page.Locator("#sc-status").SelectOptionAsync("Parked");
await SetSearchKeywordAsync(page, targetPrefix + "parked");
await page.ClickAsync("[data-test='site-calls-query']");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
var parkedRow = page.Locator("tbody tr", new() { HasText = targetPrefix + "parked" });
await Assertions.Expect(parkedRow).ToBeVisibleAsync();
await Assertions.Expect(parkedRow.Locator("span.badge:has-text('Parked')")).ToBeVisibleAsync();
// Same status=Parked filter, now searching the Delivered marker: the
// Delivered row is excluded by the status filter, so no row carries
// its marker. The retrying ToHaveCount waits out the re-render.
await SetSearchKeywordAsync(page, targetPrefix + "delivered");
await page.ClickAsync("[data-test='site-calls-query']");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Assertions.Expect(
page.Locator("tbody tr").Filter(new() { HasText = targetPrefix + "delivered" }))
.ToHaveCountAsync(0);
}
finally
{
await SiteCallDataSeeder.DeleteByTargetPrefixAsync(targetPrefix);
}
}
/// <summary>
/// When the filters match no rows the grid renders the empty-state card
/// rather than a table. A per-run GUID Target is searched (exact match), so
/// nothing can match — guaranteed empty without seeding. Asserts both the
/// absence of data rows and the empty-state literal.
/// <para>
/// The empty-state literal lives in <c>div.card &gt; div.card-body…</c>, but
/// the filter card also uses <c>.card-body</c>, so a bare <c>.card-body</c>
/// locator is ambiguous under strict mode. We assert the literal via
/// <see cref="IPage.GetByText(string,PageGetByTextOptions)"/> instead.
/// </para>
/// </summary>
[SkippableFact]
public async Task EmptyState_NoMatch_ShowsEmptyCard()
{
Skip.IfNot(await SiteCallDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
var page = await _fixture.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SiteCallsUrl}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// A per-run GUID target matches nothing (exact match → guaranteed empty).
await SetSearchKeywordAsync(page, $"playwright-test/wave4-sc-empty/{Guid.NewGuid():N}/none");
await page.ClickAsync("[data-test='site-calls-query']");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// No data rows, and the empty-state literal renders. GetByText avoids the
// strict-mode ambiguity of the shared .card-body class.
await Assertions.Expect(page.Locator("tbody tr")).ToHaveCountAsync(0);
await Assertions.Expect(
page.GetByText("No cached calls match the current filters."))
.ToBeVisibleAsync();
}
}