From 4031f3dc062d11b26d4145a2efedfd40fee73386 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:56:48 +0000 Subject: [PATCH 1/3] Initial plan From a9bc612c387a00af6ce33381f8336f9f5e4604e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:07:57 +0000 Subject: [PATCH 2/3] feat(tests): add IssuesPageFilterTests bUnit filter/search wiring tests (#125) Agent-Logs-Url: https://github.com/mpaulosky/IssueManager/sessions/7faad0b6-6a82-44e9-9ad2-b09508c6e5a3 Co-authored-by: mpaulosky <60372079+mpaulosky@users.noreply.github.com> --- .../Features/Issues/IssuesPageFilterTests.cs | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs diff --git a/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs b/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs new file mode 100644 index 0000000..6ee3047 --- /dev/null +++ b/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs @@ -0,0 +1,260 @@ +// ============================================ +// Copyright (c) 2026. All rights reserved. +// File Name : IssuesPageFilterTests.cs +// Company : mpaulosky +// Author : Matthew Paulosky +// Solution Name : IssueManager +// Project Name : Web.Tests.Bunit +// ============================================= + +namespace Web.Components.Features.Issues; + +/// +/// bUnit tests for IssuesPage filter and search wiring. +/// Verifies that filter values are correctly forwarded to . +/// +/// Tests marked [Fact(Skip = "Pending #116")] depend on the filter-bug fix and/or +/// the updated signature from issue #116: +/// GetAllAsync(page, pageSize, searchTerm, authorName, statusName, categoryName, cancellationToken). +/// +/// Closes #125. Depends on #116. +/// +[ExcludeFromCodeCoverage] +public class IssuesPageFilterTests : ComponentTestBase +{ + private readonly IIssueApiClient _mockIssueClient; + + /// + /// Pre-seeded category for dropdown tests. + /// + private static readonly CategoryDto BugCategory = new( + ObjectId.GenerateNewId(), + "Bug", + "Bug category", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + /// + /// Pre-seeded status for dropdown tests. + /// + private static readonly StatusDto OpenStatus = new( + ObjectId.GenerateNewId(), + "Open", + "Open status", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + /// + /// Initializes a new instance of the class, + /// registering a mock and pre-seeding dropdown data. + /// + public IssuesPageFilterTests() + { + _mockIssueClient = Substitute.For(); + _mockIssueClient + .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(PaginatedResponse.Empty)); + TestContext.Services.AddSingleton(_mockIssueClient); + + // Seed category and status dropdown data so filters have selectable options. + // (ComponentTestBase registers the mocks; we reconfigure them here with real data.) + var categoryClient = TestContext.Services.GetRequiredService(); + categoryClient.GetAllAsync(Arg.Any()) + .Returns(Task.FromResult>([BugCategory])); + + var statusClient = TestContext.Services.GetRequiredService(); + statusClient.GetAllAsync(Arg.Any()) + .Returns(Task.FromResult>([OpenStatus])); + } + + /// + /// Verifies that on component mount GetAllAsync is called once with the default + /// parameters: page=1, pageSize=20, searchTerm=null, authorName=null. + /// + [Fact] + public void LoadIssues_OnInit_CallsApiWithDefaultParams() + { + // Act + TestContext.Render(); + + // Assert — exactly one call with the default/null filter values + _ = _mockIssueClient.Received(1) + .GetAllAsync(1, 20, null, null, Arg.Any()); + } + + /// + /// Verifies that typing a search term and clicking Search passes searchTerm + /// to . + /// + /// + /// Skipped pending issue #116: LoadIssues currently calls + /// GetAllAsync(page, 20) without forwarding _searchTerm. + /// Remove the Skip attribute once the component bug is fixed. + /// + [Fact(Skip = "Pending #116: LoadIssues does not yet forward _searchTerm to GetAllAsync")] + public async Task SearchBox_WhenFilled_PassesSearchTermToApi() + { + // Arrange + var cut = TestContext.Render(); + + // Act — type a search term then click Search + await cut.Find("#search").InputAsync(new ChangeEventArgs { Value = "my-bug" }); + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") + .ClickAsync(new MouseEventArgs()); + + // Assert — the second call (after init) should carry the search term + _ = _mockIssueClient.Received() + .GetAllAsync(1, 20, "my-bug", null, Arg.Any()); + } + + /// + /// Verifies that selecting a status from the dropdown and clicking Search + /// passes statusName to . + /// + /// + /// Skipped pending issue #116: IIssueApiClient.GetAllAsync does not yet + /// include a statusName parameter. + /// When #116 adds the parameter, update the assertion to: + /// GetAllAsync(1, 20, null, null, "Open", null, Arg.Any<CancellationToken>()) + /// and remove the Skip attribute. + /// + [Fact(Skip = "Pending #116: IIssueApiClient.GetAllAsync does not yet include statusName parameter")] + public async Task StatusFilter_WhenSelected_PassesStatusNameToApi() + { + // Arrange + var cut = TestContext.Render(); + + // Act — select "Open" from the status filter, then click Search + await cut.Find("#status-filter").ChangeAsync(new ChangeEventArgs { Value = "Open" }); + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") + .ClickAsync(new MouseEventArgs()); + + // Assert + // TODO (#116): Replace with the specific statusName assertion once the parameter is added: + // _mockIssueClient.Received().GetAllAsync(1, 20, null, null, "Open", null, Arg.Any()); + _ = _mockIssueClient.Received() + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + } + + /// + /// Verifies that selecting a category from the dropdown and clicking Search + /// passes categoryName to . + /// + /// + /// Skipped pending issue #116: IIssueApiClient.GetAllAsync does not yet + /// include a categoryName parameter. + /// When #116 adds the parameter, update the assertion to: + /// GetAllAsync(1, 20, null, null, null, "Bug", Arg.Any<CancellationToken>()) + /// and remove the Skip attribute. + /// + [Fact(Skip = "Pending #116: IIssueApiClient.GetAllAsync does not yet include categoryName parameter")] + public async Task CategoryFilter_WhenSelected_PassesCategoryNameToApi() + { + // Arrange + var cut = TestContext.Render(); + + // Act — select "Bug" from the category filter, then click Search + await cut.Find("#category-filter").ChangeAsync(new ChangeEventArgs { Value = "Bug" }); + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") + .ClickAsync(new MouseEventArgs()); + + // Assert + // TODO (#116): Replace with the specific categoryName assertion once the parameter is added: + // _mockIssueClient.Received().GetAllAsync(1, 20, null, null, null, "Bug", Arg.Any()); + _ = _mockIssueClient.Received() + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + } + + /// + /// Verifies that combining search term, status, and category all flow through + /// together to a single call. + /// + /// + /// Skipped pending issue #116: requires both the interface update (statusName/categoryName) + /// and the component bug fix (forwarding all filters in LoadIssues). + /// When #116 lands, update the assertion to: + /// GetAllAsync(1, 20, "crash", null, "Open", "Bug", Arg.Any<CancellationToken>()) + /// and remove the Skip attribute. + /// + [Fact(Skip = "Pending #116: GetAllAsync missing statusName/categoryName params; LoadIssues does not forward filters")] + public async Task MultipleFilters_AllPassedToApiCombined() + { + // Arrange + var cut = TestContext.Render(); + + // Act — set all three filters then click Search + await cut.Find("#search").InputAsync(new ChangeEventArgs { Value = "crash" }); + await cut.Find("#status-filter").ChangeAsync(new ChangeEventArgs { Value = "Open" }); + await cut.Find("#category-filter").ChangeAsync(new ChangeEventArgs { Value = "Bug" }); + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") + .ClickAsync(new MouseEventArgs()); + + // Assert + // TODO (#116): Replace with the full assertion once the interface and component are fixed: + // _mockIssueClient.Received().GetAllAsync(1, 20, "crash", null, "Open", "Bug", Arg.Any()); + _ = _mockIssueClient.Received() + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + } + + /// + /// Verifies that clicking Clear resets all filter fields to null and + /// calls with the default parameters. + /// + /// + /// Skipped pending issue #116: once the component forwards filters to LoadIssues, + /// we can assert that the post-clear call uses all-null filter args. + /// When #116 lands, update the assertion to verify a third call with all nulls: + /// GetAllAsync(1, 20, null, null, null, null, Arg.Any<CancellationToken>()) + /// and remove the Skip attribute. + /// + [Fact(Skip = "Pending #116: LoadIssues does not yet forward filter values; full clear verification needs the fix")] + public async Task ClearButton_ResetsAllFiltersAndCallsApiWithDefaults() + { + // Arrange — render and set a search term + var cut = TestContext.Render(); + await cut.Find("#search").InputAsync(new ChangeEventArgs { Value = "something" }); + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") + .ClickAsync(new MouseEventArgs()); + + // Act — click Clear + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Clear") + .ClickAsync(new MouseEventArgs()); + + // Assert — three total calls (init, search, clear); the clear call must use page 1 and null filters + // TODO (#116): Once fixed, assert the third call uses all null filter params: + // _mockIssueClient.Received(3).GetAllAsync(Arg.Any(), ...); + // And use ReceivedCalls() to inspect the last call specifically. + _ = _mockIssueClient.Received(3) + .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + /// + /// Verifies that changing a filter resets the current page back to 1 before + /// calling . + /// + /// + /// Skipped pending issue #116: the Search button triggers LoadIssues(1) + /// directly, so pagination reset already happens structurally — but it cannot + /// be meaningfully verified until filter params are wired through LoadIssues. + /// Remove the Skip attribute and verify the page argument is 1 once #116 lands. + /// + [Fact(Skip = "Pending #116: pagination reset verification requires filter-wiring fix from #116")] + public async Task FilterChange_ResetsPageToOne() + { + // Arrange — render (triggers init call at page 1) + var cut = TestContext.Render(); + + // Act — change the search term and click Search (should always use page 1) + await cut.Find("#search").InputAsync(new ChangeEventArgs { Value = "reset-test" }); + await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") + .ClickAsync(new MouseEventArgs()); + + // Assert — the Search call must target page 1 regardless of prior navigation + _ = _mockIssueClient.Received() + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + } +} From 32bd4cbc8d8c11a2b637a2a3cb4d45900be5b7f7 Mon Sep 17 00:00:00 2001 From: Scribe Date: Wed, 15 Apr 2026 10:36:11 -0700 Subject: [PATCH 3/3] fix(tests): update GetAllAsync calls to 7-param signature (add statusName, categoryName) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Features/Issues/IssuesPageFilterTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs b/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs index 6ee3047..e6e6c84 100644 --- a/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs +++ b/tests/Web.Tests.Bunit/Components/Features/Issues/IssuesPageFilterTests.cs @@ -56,7 +56,7 @@ public IssuesPageFilterTests() { _mockIssueClient = Substitute.For(); _mockIssueClient - .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(PaginatedResponse.Empty)); TestContext.Services.AddSingleton(_mockIssueClient); @@ -83,7 +83,7 @@ public void LoadIssues_OnInit_CallsApiWithDefaultParams() // Assert — exactly one call with the default/null filter values _ = _mockIssueClient.Received(1) - .GetAllAsync(1, 20, null, null, Arg.Any()); + .GetAllAsync(1, 20, null, null, null, null, Arg.Any()); } /// @@ -108,7 +108,7 @@ await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") // Assert — the second call (after init) should carry the search term _ = _mockIssueClient.Received() - .GetAllAsync(1, 20, "my-bug", null, Arg.Any()); + .GetAllAsync(1, 20, "my-bug", null, null, null, Arg.Any()); } /// @@ -137,7 +137,7 @@ await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") // TODO (#116): Replace with the specific statusName assertion once the parameter is added: // _mockIssueClient.Received().GetAllAsync(1, 20, null, null, "Open", null, Arg.Any()); _ = _mockIssueClient.Received() - .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } /// @@ -166,7 +166,7 @@ await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") // TODO (#116): Replace with the specific categoryName assertion once the parameter is added: // _mockIssueClient.Received().GetAllAsync(1, 20, null, null, null, "Bug", Arg.Any()); _ = _mockIssueClient.Received() - .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } /// @@ -197,7 +197,7 @@ await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") // TODO (#116): Replace with the full assertion once the interface and component are fixed: // _mockIssueClient.Received().GetAllAsync(1, 20, "crash", null, "Open", "Bug", Arg.Any()); _ = _mockIssueClient.Received() - .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } /// @@ -229,7 +229,7 @@ await cut.FindAll("button").First(b => b.TextContent.Trim() == "Clear") // _mockIssueClient.Received(3).GetAllAsync(Arg.Any(), ...); // And use ReceivedCalls() to inspect the last call specifically. _ = _mockIssueClient.Received(3) - .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + .GetAllAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } /// @@ -255,6 +255,6 @@ await cut.FindAll("button").First(b => b.TextContent.Trim() == "Search") // Assert — the Search call must target page 1 regardless of prior navigation _ = _mockIssueClient.Received() - .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any()); + .GetAllAsync(1, 20, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } }