krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.4.0: HutchTests/TicketListViewModelTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5@MainActor
6struct TicketListViewModelTests {
7
8 @Test
9 func filteredTicketsReturnsStatusFilteredTicketsWhenSearchTextIsEmpty() {
10 let tickets = [
11 makeTicket(id: 1, title: "Crash on launch", status: .reported, submitter: "~owner", labels: []),
12 makeTicket(id: 2, title: "Already fixed", status: .resolved, submitter: "~owner", labels: [])
13 ]
14
15 let filtered = filterTickets(
16 tickets,
17 state: TicketListFilterState(status: .open),
18 query: ""
19 )
20
21 #expect(filtered.map(\.id) == [1])
22 }
23
24 @Test
25 func filteredTicketsMatchesTitleAndTicketId() {
26 let tickets = [
27 makeTicket(id: 42, title: "Crash on launch", status: .reported, submitter: "~owner", labels: []),
28 makeTicket(id: 99, title: "Settings polish", status: .reported, submitter: "~owner", labels: [])
29 ]
30
31 let titleMatches = filterTickets(
32 tickets,
33 state: TicketListFilterState(status: .all),
34 query: "settings"
35 )
36 let idMatches = filterTickets(
37 tickets,
38 state: TicketListFilterState(status: .all),
39 query: "42"
40 )
41
42 #expect(titleMatches.map(\.id) == [99])
43 #expect(idMatches.map(\.id) == [42])
44 }
45
46 @Test
47 func filteredTicketsMatchesSubmitterAndLabels() {
48 let tickets = [
49 makeTicket(id: 1, title: "Crash on launch", status: .reported, submitter: "~owner", labels: [makeLabel(id: 1, name: "bug")]),
50 makeTicket(id: 2, title: "Needs triage", status: .reported, submitter: "~triage", labels: [makeLabel(id: 2, name: "needs-info")])
51 ]
52
53 let submitterMatches = filterTickets(
54 tickets,
55 state: TicketListFilterState(status: .all),
56 query: "~triage"
57 )
58 let labelMatches = filterTickets(
59 tickets,
60 state: TicketListFilterState(status: .all),
61 query: "bug"
62 )
63
64 #expect(submitterMatches.map(\.id) == [2])
65 #expect(labelMatches.map(\.id) == [1])
66 }
67
68 @Test
69 func filteredTicketsMatchesAssigneeAndStatus() {
70 let tickets = [
71 makeTicket(
72 id: 1,
73 title: "Crash on launch",
74 status: .inProgress,
75 submitter: "~owner",
76 labels: [],
77 assignees: [Entity(canonicalName: "~alice")]
78 ),
79 makeTicket(
80 id: 2,
81 title: "Settings polish",
82 status: .resolved,
83 submitter: "~owner",
84 labels: [],
85 assignees: []
86 )
87 ]
88
89 let assigneeMatches = filterTickets(
90 tickets,
91 state: TicketListFilterState(status: .all),
92 query: "~alice"
93 )
94 let statusMatches = filterTickets(
95 tickets,
96 state: TicketListFilterState(status: .all),
97 query: "resolved"
98 )
99
100 #expect(assigneeMatches.map(\.id) == [1])
101 #expect(statusMatches.map(\.id) == [2])
102 }
103
104 @Test
105 func filteredTicketsMatchesAnySelectedLabel() {
106 let tickets = [
107 makeTicket(id: 1, title: "Crash on launch", status: .reported, submitter: "~owner", labels: [makeLabel(id: 1, name: "bug")]),
108 makeTicket(id: 2, title: "Needs triage", status: .reported, submitter: "~triage", labels: [makeLabel(id: 2, name: "needs-info")]),
109 makeTicket(id: 3, title: "Unlabeled", status: .reported, submitter: "~owner", labels: [])
110 ]
111
112 let filtered = filterTickets(
113 tickets,
114 state: TicketListFilterState(status: .all, labelIDs: [2, 3]),
115 query: ""
116 )
117
118 #expect(filtered.map(\.id) == [2])
119 }
120
121 @Test
122 func synchronizeTicketsReplacesEditedLabelsAndRemovesDeletedOnes() {
123 let existing = [
124 makeTicket(
125 id: 7,
126 title: "Triage me",
127 status: .reported,
128 submitter: "~owner",
129 labels: [
130 makeLabel(id: 1, name: "bug"),
131 makeLabel(id: 2, name: "stale")
132 ]
133 )
134 ]
135
136 let updated = TicketListViewModel.synchronizeTickets(
137 existing,
138 with: [makeLabel(id: 1, name: "bugfix")]
139 )
140
141 #expect(updated.first?.labels.map(\.id) == [1])
142 #expect(updated.first?.labels.first?.name == "bugfix")
143 }
144
145 @Test
146 func reconciledSelectedLabelIDsRemovesUnknownIDs() {
147 let reconciled = TicketListViewModel.reconciledSelectedLabelIDs(
148 [1, 2, 5],
149 availableLabels: [
150 makeLabel(id: 2, name: "triage"),
151 makeLabel(id: 3, name: "qa")
152 ]
153 )
154
155 #expect(reconciled == [2])
156 }
157
158 @Test
159 @MainActor
160 func resolveTicketInputHasCorrectStatusAndDefaultResolution() {
161 let input: [String: any Sendable] = [
162 "status": TicketStatus.resolved.rawValue,
163 "resolution": TicketResolution.fixed.rawValue
164 ]
165 #expect(input["status"] as? String == "resolved")
166 #expect(input["resolution"] as? String == "fixed")
167 }
168
169 @Test
170 @MainActor
171 func reopenTicketInputOmitsResolution() {
172 let input: [String: any Sendable] = [
173 "status": TicketStatus.reported.rawValue
174 ]
175 #expect(input["status"] as? String == "reported")
176 #expect(input["resolution"] == nil)
177 }
178
179 private func filterTickets(
180 _ tickets: [TicketSummary],
181 state: TicketListFilterState,
182 query: String
183 ) -> [TicketSummary] {
184 TicketListViewModel.filterTickets(tickets, state: state, query: query)
185 }
186
187 private func makeTicket(
188 id: Int,
189 title: String,
190 status: TicketStatus,
191 submitter: String,
192 labels: [TicketLabel],
193 assignees: [Entity] = []
194 ) -> TicketSummary {
195 TicketSummary(
196 id: id,
197 title: title,
198 status: status,
199 resolution: status == .resolved ? .fixed : nil,
200 created: Date(),
201 submitter: Entity(canonicalName: submitter),
202 labels: labels,
203 assignees: assignees
204 )
205 }
206
207 private func makeLabel(id: Int, name: String) -> TicketLabel {
208 TicketLabel(
209 id: id,
210 name: name,
211 backgroundColor: "#000000",
212 foregroundColor: "#ffffff"
213 )
214 }
215}