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());
}
}