diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/OffsetPager.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/OffsetPager.razor new file mode 100644 index 00000000..90b309e5 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/OffsetPager.razor @@ -0,0 +1,53 @@ +@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared + +@* T35a — reusable offset-pagination bar. Purely presentational: the host page + owns page-number state, query execution, and the HasNextPage decision. + Parameters: Page, PageChanged, HasNextPage, TotalCount, PageSize, Disabled. *@ + +
+ + + @SummaryText + + +
+ +@code { + [Parameter] public int Page { get; set; } = 1; + [Parameter] public EventCallback PageChanged { get; set; } + [Parameter] public bool HasNextPage { get; set; } + [Parameter] public int? TotalCount { get; set; } + [Parameter] public int PageSize { get; set; } + [Parameter] public bool Disabled { get; set; } + + private bool HasPrev => Page > 1; + + private int? PageCount => + TotalCount is { } t && PageSize > 0 + ? (int)Math.Ceiling(t / (double)PageSize) + : (int?)null; + + private string SummaryText + { + get + { + var text = $"Page {Page}"; + if (PageCount is { } pc) text += $" of {pc}"; + if (TotalCount is { } total) text += $" · {total} total"; + return text; + } + } + + private Task OnPrevAsync() => PageChanged.InvokeAsync(Page - 1); + private Task OnNextAsync() => PageChanged.InvokeAsync(Page + 1); +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/OffsetPagerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/OffsetPagerTests.cs new file mode 100644 index 00000000..4fe7d9cc --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/OffsetPagerTests.cs @@ -0,0 +1,109 @@ +using Bunit; +using Microsoft.AspNetCore.Components; +using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared; + +/// +/// bUnit tests for the reusable pagination bar (T35a). +/// The component is purely presentational — it emits PageChanged callbacks +/// and renders disabled states; the host page owns page-number and query state. +/// +public class OffsetPagerTests : BunitContext +{ + // ── PrevDisabled_OnFirstPage ────────────────────────────────────────────── + + [Fact] + public void PrevDisabled_OnFirstPage() + { + var cut = Render(ps => ps + .Add(p => p.Page, 1) + .Add(p => p.HasNextPage, true)); + + var prev = cut.Find("[data-test='pager-prev']"); + + Assert.NotNull(prev.GetAttribute("disabled")); + } + + // ── NextDisabled_WhenNoNextPage ─────────────────────────────────────────── + + [Fact] + public void NextDisabled_WhenNoNextPage() + { + var cut = Render(ps => ps + .Add(p => p.Page, 3) + .Add(p => p.HasNextPage, false)); + + var next = cut.Find("[data-test='pager-next']"); + + Assert.NotNull(next.GetAttribute("disabled")); + } + + // ── Next_Click_EmitsPagePlusOne ─────────────────────────────────────────── + + [Fact] + public void Next_Click_EmitsPagePlusOne() + { + int? emitted = null; + var cut = Render(ps => ps + .Add(p => p.Page, 2) + .Add(p => p.HasNextPage, true) + .Add(p => p.PageChanged, + EventCallback.Factory.Create(this, v => emitted = v))); + + cut.Find("[data-test='pager-next']").Click(); + + Assert.Equal(3, emitted); + } + + // ── Prev_Click_EmitsPageMinusOne ───────────────────────────────────────── + + [Fact] + public void Prev_Click_EmitsPageMinusOne() + { + int? emitted = null; + var cut = Render(ps => ps + .Add(p => p.Page, 2) + .Add(p => p.HasNextPage, false) + .Add(p => p.PageChanged, + EventCallback.Factory.Create(this, v => emitted = v))); + + cut.Find("[data-test='pager-prev']").Click(); + + Assert.Equal(1, emitted); + } + + // ── Summary_RendersPageOfTotal ──────────────────────────────────────────── + + [Fact] + public void Summary_RendersPageOfTotal() + { + var cut = Render(ps => ps + .Add(p => p.Page, 2) + .Add(p => p.PageSize, 50) + .Add(p => p.TotalCount, 230) + .Add(p => p.HasNextPage, true)); + + var summary = cut.Find("[data-test='pager-summary']"); + + Assert.Contains("Page 2 of 5", summary.TextContent); + Assert.Contains("230 total", summary.TextContent); + } + + // ── BothDisabled_WhenDisabled ───────────────────────────────────────────── + + [Fact] + public void BothDisabled_WhenDisabled() + { + var cut = Render(ps => ps + .Add(p => p.Page, 3) + .Add(p => p.HasNextPage, true) + .Add(p => p.Disabled, true)); + + var prev = cut.Find("[data-test='pager-prev']"); + var next = cut.Find("[data-test='pager-next']"); + + Assert.NotNull(prev.GetAttribute("disabled")); + Assert.NotNull(next.GetAttribute("disabled")); + } +}