diff options
| author | Christian Cleberg <[email protected]> | 2026-04-12 21:33:14 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-12 21:33:14 -0500 |
| commit | 31cbca674957eea7ae2529012e20a9d95158e561 (patch) | |
| tree | ea57e5b7faf17b3a3a0967e074960f41d3556393 /HutchTests | |
| parent | 7548bfb595d853c682653c0dc21bb3bf17af80d5 (diff) | |
| download | hutch-31cbca674957eea7ae2529012e20a9d95158e561.tar.gz hutch-31cbca674957eea7ae2529012e20a9d95158e561.tar.bz2 hutch-31cbca674957eea7ae2529012e20a9d95158e561.zip | |
feat: add ticket saved filters and label filtering
Implements: https://todo.sr.ht/~ccleberg/hutch/32
Implements: https://todo.sr.ht/~ccleberg/hutch/33
Diffstat (limited to 'HutchTests')
| -rw-r--r-- | HutchTests/TicketListViewModelTests.swift | 72 | ||||
| -rw-r--r-- | HutchTests/TicketSavedFilterStoreTests.swift | 88 |
2 files changed, 136 insertions, 24 deletions
diff --git a/HutchTests/TicketListViewModelTests.swift b/HutchTests/TicketListViewModelTests.swift index 23be24c..0f42bf8 100644 --- a/HutchTests/TicketListViewModelTests.swift +++ b/HutchTests/TicketListViewModelTests.swift @@ -11,7 +11,11 @@ struct TicketListViewModelTests { makeTicket(id: 2, title: "Already fixed", status: .resolved, submitter: "~owner", labels: []) ] - let filtered = filterTickets(tickets, filter: .open, query: "") + let filtered = filterTickets( + tickets, + state: TicketListFilterState(status: .open), + query: "" + ) #expect(filtered.map(\.id) == [1]) } @@ -23,8 +27,16 @@ struct TicketListViewModelTests { makeTicket(id: 99, title: "Settings polish", status: .reported, submitter: "~owner", labels: []) ] - let titleMatches = filterTickets(tickets, filter: .all, query: "settings") - let idMatches = filterTickets(tickets, filter: .all, query: "42") + let titleMatches = filterTickets( + tickets, + state: TicketListFilterState(status: .all), + query: "settings" + ) + let idMatches = filterTickets( + tickets, + state: TicketListFilterState(status: .all), + query: "42" + ) #expect(titleMatches.map(\.id) == [99]) #expect(idMatches.map(\.id) == [42]) @@ -37,14 +49,39 @@ struct TicketListViewModelTests { makeTicket(id: 2, title: "Needs triage", status: .reported, submitter: "~triage", labels: [makeLabel(id: 2, name: "needs-info")]) ] - let submitterMatches = filterTickets(tickets, filter: .all, query: "~triage") - let labelMatches = filterTickets(tickets, filter: .all, query: "bug") + let submitterMatches = filterTickets( + tickets, + state: TicketListFilterState(status: .all), + query: "~triage" + ) + let labelMatches = filterTickets( + tickets, + state: TicketListFilterState(status: .all), + query: "bug" + ) #expect(submitterMatches.map(\.id) == [2]) #expect(labelMatches.map(\.id) == [1]) } @Test + func filteredTicketsMatchesAnySelectedLabel() { + let tickets = [ + makeTicket(id: 1, title: "Crash on launch", status: .reported, submitter: "~owner", labels: [makeLabel(id: 1, name: "bug")]), + makeTicket(id: 2, title: "Needs triage", status: .reported, submitter: "~triage", labels: [makeLabel(id: 2, name: "needs-info")]), + makeTicket(id: 3, title: "Unlabeled", status: .reported, submitter: "~owner", labels: []) + ] + + let filtered = filterTickets( + tickets, + state: TicketListFilterState(status: .all, labelIDs: [2, 3]), + query: "" + ) + + #expect(filtered.map(\.id) == [2]) + } + + @Test @MainActor func resolveTicketInputHasCorrectStatusAndDefaultResolution() { let input: [String: any Sendable] = [ @@ -65,25 +102,12 @@ struct TicketListViewModelTests { #expect(input["resolution"] == nil) } - private func filterTickets(_ tickets: [TicketSummary], filter: TicketFilter, query: String) -> [TicketSummary] { - let statusFiltered: [TicketSummary] - switch filter { - case .open: - statusFiltered = tickets.filter { $0.status.isOpen } - case .resolved: - statusFiltered = tickets.filter { !$0.status.isOpen } - case .all: - statusFiltered = tickets - } - - let q = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard !q.isEmpty else { return statusFiltered } - return statusFiltered.filter { - String($0.id).contains(q) || - $0.title.lowercased().contains(q) || - $0.submitter.canonicalName.lowercased().contains(q) || - $0.labels.contains { $0.name.lowercased().contains(q) } - } + private func filterTickets( + _ tickets: [TicketSummary], + state: TicketListFilterState, + query: String + ) -> [TicketSummary] { + TicketListViewModel.filterTickets(tickets, state: state, query: query) } private func makeTicket( diff --git a/HutchTests/TicketSavedFilterStoreTests.swift b/HutchTests/TicketSavedFilterStoreTests.swift new file mode 100644 index 0000000..2ff64e6 --- /dev/null +++ b/HutchTests/TicketSavedFilterStoreTests.swift @@ -0,0 +1,88 @@ +import Foundation +import Testing +@testable import Hutch + +struct TicketSavedFilterStoreTests { + + @Test + func storesCurrentFilterStatePerTracker() { + let suiteName = "TicketSavedFilterStoreTests-\(#function)-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + TicketSavedFilterStore.saveCurrentState( + TicketListFilterState(status: .all, labelIDs: [4, 1]), + for: "tracker-a", + defaults: defaults + ) + TicketSavedFilterStore.saveCurrentState( + TicketListFilterState(status: .resolved, labelIDs: [9]), + for: "tracker-b", + defaults: defaults + ) + + #expect( + TicketSavedFilterStore.loadCurrentState(for: "tracker-a", defaults: defaults) == + TicketListFilterState(status: .all, labelIDs: [1, 4]) + ) + #expect( + TicketSavedFilterStore.loadCurrentState(for: "tracker-b", defaults: defaults) == + TicketListFilterState(status: .resolved, labelIDs: [9]) + ) + } + + @Test + func savesNamedFiltersPerTrackerAndReplacesDuplicateNames() { + let suiteName = "TicketSavedFilterStoreTests-\(#function)-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + _ = TicketSavedFilterStore.saveFilter( + named: "Bugs", + state: TicketListFilterState(status: .open, labelIDs: [1]), + for: "tracker-a", + defaults: defaults, + now: Date(timeIntervalSince1970: 100) + ) + _ = TicketSavedFilterStore.saveFilter( + named: "bugs", + state: TicketListFilterState(status: .resolved, labelIDs: [2]), + for: "tracker-a", + defaults: defaults, + now: Date(timeIntervalSince1970: 200) + ) + _ = TicketSavedFilterStore.saveFilter( + named: "Needs Info", + state: TicketListFilterState(status: .all, labelIDs: [3]), + for: "tracker-b", + defaults: defaults, + now: Date(timeIntervalSince1970: 300) + ) + + let trackerAFilters = TicketSavedFilterStore.loadSavedFilters(for: "tracker-a", defaults: defaults) + let trackerBFilters = TicketSavedFilterStore.loadSavedFilters(for: "tracker-b", defaults: defaults) + + #expect(trackerAFilters.count == 1) + #expect(trackerAFilters.first?.name == "bugs") + #expect(trackerAFilters.first?.state == TicketListFilterState(status: .resolved, labelIDs: [2])) + #expect(trackerBFilters.map(\.name) == ["Needs Info"]) + } + + @Test + func deletesSavedFilter() { + let suiteName = "TicketSavedFilterStoreTests-\(#function)-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + let savedFilter = TicketSavedFilterStore.saveFilter( + named: "Bugs", + state: TicketListFilterState(status: .open, labelIDs: [1]), + for: "tracker-a", + defaults: defaults + ) + + TicketSavedFilterStore.deleteFilter(id: savedFilter!.id, for: "tracker-a", defaults: defaults) + + #expect(TicketSavedFilterStore.loadSavedFilters(for: "tracker-a", defaults: defaults).isEmpty) + } +} |
