krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

remove-splash-highlighter: 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}