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."))
|
page.GetByText("No cached calls match the current filters."))
|
||||||
.ToBeVisibleAsync();
|
.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