Files
swift-composable-architectu…/Tests/ComposableArchitectureTests/EffectDebounceTests.swift
Stephen Celis ee3e96ce10 Bring back debounce? (#2372)
Having brought back an explicit interface for `throttle`
[here](https://github.com/pointfreeco/swift-composable-architecture/pull/2368)
we should also consider providing an explicit interface for `debounce`.

Though a clock-based debounce is a matter of:

```swift
return .run { send in
  try await withTaskCancellation(id: CancelID.debounce) {
    try await clock.sleep(for: .seconds(1))
    await send(.action)
  }
}
```

A dedicated operator simplifies and flattens:

```swift
return .send(.action)
  .debounce(id: CancelID.debounce, for: .seconds(1), clock: clock)
```

This PR only brings back the scheduler-based API, so alongside #2368 we
are reintroducing two Combine-forward APIs for this work. Before
we release a 1.1 that fully commits these APIs, we should consider if we
can make clock-based APIs work, instead (or in addition). If in
addition, we should also consider introducing the Combine-based APIs as
soft-deprecated to begin with.
2023-08-14 09:38:37 -07:00

99 lines
2.4 KiB
Swift

import Combine
@_spi(Internals) import ComposableArchitecture
import XCTest
@MainActor
final class EffectDebounceTests: BaseTCATestCase {
var cancellables: Set<AnyCancellable> = []
func testDebounce() async {
let mainQueue = DispatchQueue.test
var values: [Int] = []
@discardableResult
func runDebouncedEffect(value: Int) -> Task<Void, Never> {
Task {
struct CancelToken: Hashable {}
let effect = Effect.send(value)
.debounce(id: CancelToken(), for: 1, scheduler: mainQueue)
for await action in effect.actions {
values.append(action)
}
}
}
runDebouncedEffect(value: 1)
// Nothing emits right away.
XCTAssertEqual(values, [])
// Waiting half the time also emits nothing
await mainQueue.advance(by: 0.5)
XCTAssertEqual(values, [])
// Run another debounced effect.
runDebouncedEffect(value: 2)
// Waiting half the time emits nothing because the first debounced effect has been canceled.
await mainQueue.advance(by: 0.5)
XCTAssertEqual(values, [])
// Run another debounced effect.
runDebouncedEffect(value: 3)
// Waiting half the time emits nothing because the second debounced effect has been canceled.
await mainQueue.advance(by: 0.5)
XCTAssertEqual(values, [])
// Waiting the rest of the time emits the final effect value.
await mainQueue.advance(by: 0.5)
XCTAssertEqual(values, [3])
// Running out the scheduler
await mainQueue.run()
XCTAssertEqual(values, [3])
}
func testDebounceIsLazy() async {
let mainQueue = DispatchQueue.test
var values: [Int] = []
var effectRuns = 0
@discardableResult
func runDebouncedEffect(value: Int) -> Task<Void, Never> {
Task {
struct CancelToken: Hashable {}
let effect = Effect.publisher {
Deferred { () -> Just<Int> in
effectRuns += 1
return Just(1)
}
}
.debounce(id: CancelToken(), for: 1, scheduler: mainQueue)
for await action in effect.actions {
values.append(action)
}
}
}
runDebouncedEffect(value: 1)
XCTAssertEqual(values, [])
XCTAssertEqual(effectRuns, 0)
await mainQueue.advance(by: 0.5)
XCTAssertEqual(values, [])
XCTAssertEqual(effectRuns, 0)
await mainQueue.advance(by: 0.5)
XCTAssertEqual(values, [1])
XCTAssertEqual(effectRuns, 1)
}
}