Commit 3efd628ddd
Verified · cmc
Layout: unified · split
gitbay/Discovery/SearchViewModel.swift +6 −5
| @@ -81,20 +81,21 @@ final class SearchViewModel { | |||
| 81 | self.client = client | 81 | self.client = client |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | func clear() { | 84 | isolated deinit { |
| 85 | searchTask?.cancel() | 85 | searchTask?.cancel() |
| 86 | query = "" | ||
| 87 | } | 86 | } |
| 88 | 87 | ||
| 89 | private func scheduleSearch() { | 88 | private func scheduleSearch() { |
| 90 | searchTask?.cancel() | 89 | searchTask?.cancel() |
| 91 | guard isSearching else { return } | 90 | guard isSearching else { return } |
| 92 | state = .loading | 91 | // Keep prior results on screen while a refined search is in |
| 93 | searchTask = Task { | 92 | // flight, the way the repository list does. |
| 93 | if state.value == nil { state = .loading } | ||
| 94 | searchTask = Task { [weak self] in | ||
| 94 | // Debounce a person typing, the way the repository list does. | 95 | // Debounce a person typing, the way the repository list does. |
| 95 | guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return } | 96 | guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return } |
| 96 | guard !Task.isCancelled else { return } | 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 | #expect(argvFrom(try #require(stub.seen.last).url) == ["search", "c++"]) | 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 | let (client, stub) = try makeClient() | 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 | let model = SearchViewModel(client: client) | 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) | 181 | guard case .empty = model.state else { |
| 182 | #expect(model.query.isEmpty) | 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 | let (client, stub) = try makeClient() | 188 | let (client, stub) = try makeClient() |
| 187 | stub.enqueue(.init(status: 200, json: """ | 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 | let model = SearchViewModel(client: client) | 192 | let model = SearchViewModel(client: client) |
| 191 | 193 | ||
| 192 | model.query = "zzzznothing" | 194 | model.query = "parity" |
| 193 | try await Task.sleep(for: .milliseconds(600)) | 195 | try await Task.sleep(for: .milliseconds(600)) |
| 194 | 196 | ||
| 195 | guard case .empty = model.state else { | 197 | guard case .failed = model.state else { |
| 196 | Testing.Issue.record("expected empty, got \(model.state)") | 198 | Testing.Issue.record("expected failed, got \(model.state)") |
| 197 | return | 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 | } |