test(playwright): Site Calls keyset pagination edge case (Wave 4)
This commit is contained in:
@@ -491,4 +491,89 @@ public class SiteCallsPageTests
|
||||
page.GetByText("No cached calls match the current filters."))
|
||||
.ToBeVisibleAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keyset pagination traverses forward (Next) and back (Previous) across a
|
||||
/// full page boundary. The grid pages at 50 rows ordered
|
||||
/// <c>CreatedAtUtc DESC, TrackedOperationId DESC</c>; the Next button is
|
||||
/// enabled only when the current page came back exactly full (50 rows), so a
|
||||
/// short page (1 row) is the last page.
|
||||
/// <para>
|
||||
/// <c>Target</c> is an exact match, so seeding 51 rows that all share ONE
|
||||
/// identical target string lets a single <c>#sc-search</c> keyword select all
|
||||
/// 51 → page 1 = 50 rows, page 2 = 1 row. Staggering <c>createdAtUtc</c> by
|
||||
/// the loop index makes the keyset order strict and deterministic. Every row
|
||||
/// uses the permitted source site <c>site-a</c> so
|
||||
/// <c>FilterPermittedAsync</c> keeps them.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Web-first only: each page-transition assertion checks the row COUNT first
|
||||
/// (which waits for the keyset fetch to render) BEFORE the pager button
|
||||
/// states, because the <c>_loading</c> flag also disables both buttons
|
||||
/// mid-fetch — reading button state before the fetch settles would race.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Pagination_KeysetNextAndPrev_TraversesPages()
|
||||
{
|
||||
Skip.IfNot(await SiteCallDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason);
|
||||
|
||||
var runId = Guid.NewGuid().ToString("N");
|
||||
var prefix = $"playwright-test/wave4-scpage/{runId}/";
|
||||
// All 51 rows share ONE identical exact target — a single keyword selects
|
||||
// the whole set, which then spans the 50-row page boundary.
|
||||
var sharedTarget = prefix + "row";
|
||||
|
||||
try
|
||||
{
|
||||
// 51 rows, identical target, distinct TrackedOperationId, all site-a /
|
||||
// Delivered, timestamps staggered by index so the keyset order
|
||||
// (CreatedAtUtc DESC, TrackedOperationId DESC) is strict.
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 51; i++)
|
||||
{
|
||||
var ts = now.AddSeconds(-i);
|
||||
await SiteCallDataSeeder.InsertSiteCallAsync(
|
||||
trackedOperationId: Guid.NewGuid(), channel: "ApiOutbound", target: sharedTarget,
|
||||
sourceSite: "site-a", status: "Delivered", retryCount: 0,
|
||||
createdAtUtc: ts, updatedAtUtc: ts);
|
||||
}
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{SiteCallsUrl}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await SetSearchKeywordAsync(page, sharedTarget);
|
||||
await page.ClickAsync("[data-test='site-calls-query']");
|
||||
|
||||
// The pager indicator span (`Page {N} · {rows} rows`). It is the only
|
||||
// text-muted small span in the table footer, so a scoped GetByText
|
||||
// regex is unambiguous.
|
||||
var pageIndicator = page.Locator("span.text-muted.small");
|
||||
|
||||
// ── Page 1: full page (50 rows). Assert COUNT first (waits for the
|
||||
// fetch), then the indicator and the button states. ──
|
||||
await Assertions.Expect(page.Locator("tbody tr")).ToHaveCountAsync(50);
|
||||
await Assertions.Expect(pageIndicator).ToContainTextAsync("Page 1");
|
||||
await Assertions.Expect(page.Locator("[data-test='site-calls-prev']")).ToBeDisabledAsync();
|
||||
await Assertions.Expect(page.Locator("[data-test='site-calls-next']")).ToBeEnabledAsync();
|
||||
|
||||
// ── Next → Page 2: short page (1 row). Last page, so Next disables. ──
|
||||
await page.ClickAsync("[data-test='site-calls-next']");
|
||||
await Assertions.Expect(page.Locator("tbody tr")).ToHaveCountAsync(1);
|
||||
await Assertions.Expect(pageIndicator).ToContainTextAsync("Page 2");
|
||||
await Assertions.Expect(page.Locator("[data-test='site-calls-prev']")).ToBeEnabledAsync();
|
||||
await Assertions.Expect(page.Locator("[data-test='site-calls-next']")).ToBeDisabledAsync();
|
||||
|
||||
// ── Previous → back on Page 1: full page again, Prev disables. ──
|
||||
await page.ClickAsync("[data-test='site-calls-prev']");
|
||||
await Assertions.Expect(page.Locator("tbody tr")).ToHaveCountAsync(50);
|
||||
await Assertions.Expect(pageIndicator).ToContainTextAsync("Page 1");
|
||||
await Assertions.Expect(page.Locator("[data-test='site-calls-prev']")).ToBeDisabledAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await SiteCallDataSeeder.DeleteByTargetPrefixAsync(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user