|
221 | 221 | line: UInt = #line
|
222 | 222 | ) {
|
223 | 223 | var receivedActions: [(action: Action, state: State)] = []
|
224 |
| - var longLivingEffects: [String: Set<UUID>] = [:] |
| 224 | + var longLivingEffects: Set<LongLivingEffect> = [] |
225 | 225 | var snapshotState = self.state
|
226 | 226 |
|
227 | 227 | let store = Store(
|
228 | 228 | initialState: self.state,
|
229 | 229 | reducer: Reducer<State, TestAction, Void> { state, action, _ in
|
230 | 230 | let effects: Effect<Action, Never>
|
231 |
| - switch action { |
| 231 | + switch action.origin { |
232 | 232 | case let .send(localAction):
|
233 | 233 | effects = self.reducer.run(&state, self.fromLocalAction(localAction), self.environment)
|
234 | 234 | snapshotState = state
|
|
238 | 238 | receivedActions.append((action, state))
|
239 | 239 | }
|
240 | 240 |
|
241 |
| - let key = debugCaseOutput(action) |
242 |
| - let id = UUID() |
| 241 | + let effect = LongLivingEffect(file: action.file, line: action.line) |
243 | 242 | return
|
244 | 243 | effects.on(
|
245 |
| - starting: { longLivingEffects[key, default: []].insert(id) }, |
246 |
| - completed: { longLivingEffects[key]?.remove(id) }, |
247 |
| - // terminated: { longLivingEffects[key]?.remove(id) }, |
248 |
| - disposed: { longLivingEffects[key]?.remove(id) } |
| 244 | + starting: { longLivingEffects.insert(effect) }, |
| 245 | + completed: { longLivingEffects.remove(effect) }, |
| 246 | + disposed: { longLivingEffects.remove(effect) } |
249 | 247 | )
|
250 |
| - .map(TestAction.receive) |
251 |
| - |
| 248 | + .map { .init(origin: .receive($0), file: action.file, line: action.line) } |
252 | 249 | },
|
253 | 250 | environment: ()
|
254 | 251 | )
|
255 |
| - |
256 | 252 | defer { self.state = store.$state.value }
|
257 |
| - let viewStore = ViewStore( |
258 |
| - store.scope(state: self.toLocalState, action: TestAction.send) |
259 |
| - ) |
260 | 253 |
|
261 | 254 | func assert(step: Step) {
|
262 | 255 | var expectedState = toLocalState(snapshotState)
|
|
299 | 292 | file: step.file, line: step.line
|
300 | 293 | )
|
301 | 294 | }
|
302 |
| - viewStore.send(action) |
| 295 | + ViewStore( |
| 296 | + store.scope( |
| 297 | + state: self.toLocalState, |
| 298 | + action: { .init(origin: .send($0), file: step.file, line: step.line) } |
| 299 | + ) |
| 300 | + ) |
| 301 | + .send(action) |
303 | 302 | do {
|
304 | 303 | try update(&expectedState)
|
305 | 304 | } catch {
|
|
402 | 401 | )
|
403 | 402 | }
|
404 | 403 |
|
405 |
| - let unfinishedActions = longLivingEffects.filter { !$0.value.isEmpty }.map { $0.key } |
406 |
| - if unfinishedActions.count > 0 { |
407 |
| - let initiatingActions = unfinishedActions.map { "• \($0)" }.joined(separator: "\n") |
408 |
| - let pluralSuffix = unfinishedActions.count == 1 ? "" : "s" |
409 |
| - |
| 404 | + for effect in longLivingEffects { |
410 | 405 | _XCTFail(
|
411 | 406 | """
|
412 |
| - Some effects are still running. All effects must complete by the end of the assertion. |
413 |
| -
|
414 |
| - The effects that are still running were started by the following action\(pluralSuffix): |
415 |
| -
|
416 |
| - \(initiatingActions) |
| 407 | + An effect returned for this action is still running. It must complete before the end of \ |
| 408 | + the assertion. … |
| 409 | +
|
| 410 | + To fix, inspect any effects the reducer returns for this action and ensure that all of \ |
| 411 | + them complete by the end of the test. There are a few reasons why an effect may not have \ |
| 412 | + completed: |
| 413 | +
|
| 414 | + • If an effect uses a scheduler (via "receive(on:)", "delay", "debounce", etc.), make \ |
| 415 | + sure that you wait enough time for the scheduler to perform the effect. If you are using \ |
| 416 | + a test scheduler, advance the scheduler so that the effects may complete, or consider \ |
| 417 | + using an immediate scheduler to immediately perform the effect instead. |
| 418 | +
|
| 419 | + • If you are returning a long-living effect (timers, notifications, subjects, etc.), \ |
| 420 | + then make sure those effects are torn down by marking the effect ".cancellable" and \ |
| 421 | + returning a corresponding cancellation effect ("Effect.cancel") from another action, or, \ |
| 422 | + if your effect is driven by a Combine subject, send it a completion. |
| 423 | + """, |
| 424 | + file: effect.file, |
| 425 | + line: effect.line |
| 426 | + ) |
| 427 | + } |
| 428 | + } |
417 | 429 |
|
418 |
| - To fix you need to inspect the effects returned from the above action\(pluralSuffix) and \ |
419 |
| - make sure that all of them are completed by the end of your assertion. There are a few \ |
420 |
| - reasons why your effects may not have completed: |
| 430 | + private struct LongLivingEffect: Hashable { |
| 431 | + let id = UUID() |
| 432 | + let file: StaticString |
| 433 | + let line: UInt |
421 | 434 |
|
422 |
| - • If you are using a scheduler in your effect, then make sure that you wait enough time \ |
423 |
| - for the effect to finish. If you are using a test scheduler, then make sure you advance \ |
424 |
| - the scheduler so that the effects complete. |
| 435 | + static func == (lhs: Self, rhs: Self) -> Bool { |
| 436 | + lhs.id == rhs.id |
| 437 | + } |
425 | 438 |
|
426 |
| - • If you are using long-living effects (for example timers, notifications, etc.), then \ |
427 |
| - ensure those effects are completed by returning an `Effect.cancel` effect from a \ |
428 |
| - particular action in your reducer, and sending that action in the test. |
429 |
| - """, |
430 |
| - file: file, line: line |
431 |
| - ) |
| 439 | + func hash(into hasher: inout Hasher) { |
| 440 | + self.id.hash(into: &hasher) |
432 | 441 | }
|
433 | 442 | }
|
434 | 443 | }
|
|
579 | 588 | }
|
580 | 589 | }
|
581 | 590 |
|
582 |
| - private enum TestAction { |
583 |
| - case send(LocalAction) |
584 |
| - case receive(Action) |
| 591 | + private struct TestAction { |
| 592 | + let origin: Origin |
| 593 | + let file: StaticString |
| 594 | + let line: UInt |
| 595 | + |
| 596 | + enum Origin { |
| 597 | + case send(LocalAction) |
| 598 | + case receive(Action) |
| 599 | + } |
585 | 600 | }
|
586 | 601 | }
|
587 | 602 |
|
|
0 commit comments