Commit 3efd628ddd
Verified · cmc
Layout: unified · split
gitbay/Discovery/SearchViewModel.swift +6 −5
| @@ -81,20 +81,21 @@ final class SearchViewModel { | ||
| 81 | 81 | self.client = client |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | func clear() { | |
| 84 | isolated deinit { | |
| 85 | 85 | searchTask?.cancel() |
| 86 | query = "" | |
| 87 | 86 | } |
| 88 | 87 | |
| 89 | 88 | private func scheduleSearch() { |
| 90 | 89 | searchTask?.cancel() |
| 91 | 90 | guard isSearching else { return } |
| 92 | state = .loading | |
| 93 | searchTask = Task { | |
| 91 | // Keep prior results on screen while a refined search is in | |
| 92 | // flight, the way the repository list does. | |
| 93 | if state.value == nil { state = .loading } | |
| 94 | searchTask = Task { [weak self] in | |
| 94 | 95 | // Debounce a person typing, the way the repository list does. |
| 95 | 96 | guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return } |
| 96 | 97 | guard !Task.isCancelled else { return } |
| 97 | await run() | |
| 98 | await self?.run() | |
| 98 | 99 | } |
| 99 | 100 | } |
| 100 | 101 | |
gitbayTests/SearchTests.swift +26 −13
| @@ -168,33 +168,46 @@ struct SearchViewModelTests { | ||
| 168 | 168 | #expect(argvFrom(try #require(stub.seen.last).url) == ["search", "c++"]) |
| 169 | 169 | } |
| 170 | 170 | |
| 171 | @Test func clearingStopsSearchingAndDropsResults() async throws { | |
| 171 | @Test func nothingFoundIsAnEmptyStateNotAFailure() async throws { | |
| 172 | 172 | let (client, stub) = try makeClient() |
| 173 | stub.enqueue(.init(status: 200, json: mixedResults)) | |
| 173 | stub.enqueue(.init(status: 200, json: """ | |
| 174 | {"protocol_version":1,"data":[],"exit_code":0} | |
| 175 | """)) | |
| 174 | 176 | let model = SearchViewModel(client: client) |
| 175 | model.query = "parity" | |
| 176 | try await Task.sleep(for: .milliseconds(600)) | |
| 177 | #expect(model.isSearching) | |
| 178 | 177 | |
| 179 | model.clear() | |
| 178 | model.query = "zzzznothing" | |
| 179 | try await Task.sleep(for: .milliseconds(600)) | |
| 180 | 180 | |
| 181 | #expect(model.isSearching == false) | |
| 182 | #expect(model.query.isEmpty) | |
| 181 | guard case .empty = model.state else { | |
| 182 | Testing.Issue.record("expected empty, got \(model.state)") | |
| 183 | return | |
| 184 | } | |
| 183 | 185 | } |
| 184 | 186 | |
| 185 | @Test func nothingFoundIsAnEmptyStateNotAFailure() async throws { | |
| 187 | @Test func aServerFailureLandsInFailedNotEmpty() async throws { | |
| 186 | 188 | let (client, stub) = try makeClient() |
| 187 | 189 | stub.enqueue(.init(status: 200, json: """ |
| 188 | {"protocol_version":1,"data":[],"exit_code":0} | |
| 190 | {"protocol_version":1,"error":"boom","exit_code":1} | |
| 189 | 191 | """)) |
| 190 | 192 | let model = SearchViewModel(client: client) |
| 191 | 193 | |
| 192 | model.query = "zzzznothing" | |
| 194 | model.query = "parity" | |
| 193 | 195 | try await Task.sleep(for: .milliseconds(600)) |
| 194 | 196 | |
| 195 | guard case .empty = model.state else { | |
| 196 | Testing.Issue.record("expected empty, got \(model.state)") | |
| 197 | guard case .failed = model.state else { | |
| 198 | Testing.Issue.record("expected failed, got \(model.state)") | |
| 197 | 199 | return |
| 198 | 200 | } |
| 199 | 201 | } |
| 202 | ||
| 203 | @Test func theQueryIsTrimmedBeforeBeingSent() async throws { | |
| 204 | let (client, stub) = try makeClient() | |
| 205 | stub.enqueue(.init(status: 200, json: mixedResults)) | |
| 206 | let model = SearchViewModel(client: client) | |
| 207 | ||
| 208 | model.query = " parity " | |
| 209 | try await Task.sleep(for: .milliseconds(600)) | |
| 210 | ||
| 211 | #expect(argvFrom(try #require(stub.seen.last).url) == ["search", "parity"]) | |
| 212 | } | |
| 200 | 213 | } |