@preconcurrency import Combine import ComposableArchitecture import XCTest final class TestStoreTests: BaseTCATestCase { func testEffectConcatenation() async { struct State: Equatable {} enum Action: Equatable { case a, b1, b2, b3, c1, c2, c3, d } let mainQueue = DispatchQueue.test let store = await TestStore(initialState: State()) { Reduce { _, action in switch action { case .a: return .merge( .run { send in try await mainQueue.sleep(for: .seconds(1)) await send(.b1) await send(.c1) }, .run { _ in try await Task.never() } .cancellable(id: 1) ) case .b1: return .concatenate(.send(.b2), .send(.b3)) case .c1: return .concatenate(.send(.c2), .send(.c3)) case .b2, .b3, .c2, .c3: return .none case .d: return .cancel(id: 1) } } } await store.send(.a) await mainQueue.advance(by: 1) await store.receive(.b1) await store.receive(.b2) await store.receive(.b3) await store.receive(.c1) await store.receive(.c2) await store.receive(.c3) await store.send(.d) } func testAsync() async { enum Action: Equatable { case tap case response(Int) } let store = await TestStore(initialState: 0) { Reduce { state, action in switch action { case .tap: return .run { send in await send(.response(42)) } case let .response(number): state = number return .none } } } await store.send(.tap) await store.receive(.response(42)) { $0 = 42 } } func testExpectedStateEquality() async { struct State: Equatable { var count: Int = 0 var isChanging: Bool = false } enum Action: Equatable { case increment case changed(from: Int, to: Int) } let store = await TestStore(initialState: State()) { Reduce { state, action in switch action { case .increment: state.isChanging = true return .send(.changed(from: state.count, to: state.count + 1)) case let .changed(from, to): state.isChanging = false if state.count == from { state.count = to } return .none } } } await store.send(.increment) { $0.isChanging = true } await store.receive(.changed(from: 0, to: 1)) { $0.isChanging = false $0.count = 1 } XCTExpectFailure() await store.send(.increment) { $0.isChanging = false } XCTExpectFailure() await store.receive(.changed(from: 1, to: 2)) { $0.isChanging = true $0.count = 1100 } } func testExpectedStateEqualityMustModify() async { struct State: Equatable { var count: Int = 0 } enum Action: Equatable { case noop, finished } let store = await TestStore(initialState: State()) { Reduce { state, action in switch action { case .noop: return .send(.finished) case .finished: return .none } } } await store.send(.noop) await store.receive(.finished) XCTExpectFailure() await store.send(.noop) { $0.count = 0 } XCTExpectFailure() await store.receive(.finished) { $0.count = 0 } } func testReceiveActionMatchingPredicate() async { enum Action: Equatable { case noop, finished } let store = await TestStore(initialState: 0) { Reduce { state, action in switch action { case .noop: return .send(.finished) case .finished: return .none } } } let predicateShouldBeCalledExpectation = expectation( description: "predicate should be called") await store.send(.noop) await store.receive { action in predicateShouldBeCalledExpectation.fulfill() return action == .finished } _ = { wait(for: [predicateShouldBeCalledExpectation], timeout: 0) }() await store.send(.noop) XCTExpectFailure() await store.receive(.noop) await store.send(.noop) XCTExpectFailure() await store.receive { $0 == .noop } } @MainActor func testStateAccess() async { enum Action { case a, b, c, d } let store = TestStore(initialState: 0) { Reduce { count, action in switch action { case .a: count += 1 return .merge(.send(.b), .send(.c), .send(.d)) case .b, .c, .d: count += 1 return .none } } } await store.send(.a) { $0 = 1 XCTAssertEqual(store.state, 0) } XCTAssertEqual(store.state, 1) await store.receive(.b) { $0 = 2 XCTAssertEqual(store.state, 1) } XCTAssertEqual(store.state, 2) await store.receive(.c) { $0 = 3 XCTAssertEqual(store.state, 2) } XCTAssertEqual(store.state, 3) await store.receive(.d) { $0 = 4 XCTAssertEqual(store.state, 3) } XCTAssertEqual(store.state, 4) } @Reducer struct Feature_testOverrideDependenciesDirectlyOnReducer { @Dependency(\.calendar) var calendar @Dependency(\.locale) var locale @Dependency(\.timeZone) var timeZone @Dependency(\.urlSession) var urlSession var body: some Reducer { Reduce { state, action in _ = self.calendar _ = self.locale _ = self.timeZone _ = self.urlSession state += action ? 1 : -1 return .none } } } func testOverrideDependenciesDirectlyOnReducer() async { let store = await TestStore(initialState: 0) { Feature_testOverrideDependenciesDirectlyOnReducer() .dependency(\.calendar, Calendar(identifier: .gregorian)) .dependency(\.locale, Locale(identifier: "en_US")) .dependency(\.timeZone, TimeZone(secondsFromGMT: 0)!) .dependency(\.urlSession, URLSession(configuration: .ephemeral)) } await store.send(true) { $0 = 1 } } @Reducer struct Feature_testOverrideDependenciesOnTestStore { @Dependency(\.calendar) var calendar @Dependency(\.locale) var locale @Dependency(\.timeZone) var timeZone @Dependency(\.urlSession) var urlSession var body: some Reducer { Reduce { state, action in _ = self.calendar _ = self.locale _ = self.timeZone _ = self.urlSession state += action ? 1 : -1 return .none } } } @MainActor func testOverrideDependenciesOnTestStore() async { let store = TestStore(initialState: 0) { Feature_testOverrideDependenciesOnTestStore() } store.dependencies.calendar = Calendar(identifier: .gregorian) store.dependencies.locale = Locale(identifier: "en_US") store.dependencies.timeZone = TimeZone(secondsFromGMT: 0)! store.dependencies.urlSession = URLSession(configuration: .ephemeral) await store.send(true) { $0 = 1 } } @Reducer struct Feature_testOverrideDependenciesOnTestStore_MidwayChange { @Dependency(\.date.now) var now var body: some Reducer { Reduce { state, _ in state = Int(self.now.timeIntervalSince1970) return .none } } } @MainActor func testOverrideDependenciesOnTestStore_MidwayChange() async { let store = TestStore(initialState: 0) { Feature_testOverrideDependenciesOnTestStore_MidwayChange() } withDependencies: { $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) } await store.send(()) { $0 = 1_234_567_890 } store.dependencies.date.now = Date(timeIntervalSince1970: 987_654_321) await store.send(()) { $0 = 987_654_321 } } @Reducer struct Feature_testOverrideDependenciesOnTestStore_Init { @Dependency(\.calendar) var calendar @Dependency(\.client.fetch) var fetch @Dependency(\.locale) var locale @Dependency(\.timeZone) var timeZone @Dependency(\.urlSession) var urlSession var body: some Reducer { Reduce { state, action in _ = self.calendar _ = self.fetch() _ = self.locale _ = self.timeZone _ = self.urlSession state += action ? 1 : -1 return .none } } } func testOverrideDependenciesOnTestStore_Init() async { let store = await TestStore(initialState: 0) { Feature_testOverrideDependenciesOnTestStore_Init() } withDependencies: { $0.calendar = Calendar(identifier: .gregorian) $0.client.fetch = { 1 } $0.locale = Locale(identifier: "en_US") $0.timeZone = TimeZone(secondsFromGMT: 0)! $0.urlSession = URLSession(configuration: .ephemeral) } await store.send(true) { $0 = 1 } } @Reducer struct Feature_testDependenciesEarlyBinding { struct State: Equatable { var count = 0 var date: Date init() { @Dependency(\.date.now) var now: Date self.date = now } } enum Action: Equatable { case tap case response(Int) } @Dependency(\.date.now) var now: Date var body: some Reducer { Reduce { state, action in switch action { case .tap: state.count += 1 return .run { send in await send(.response(42)) } case let .response(number): state.count = number state.date = now return .none } } } } func testDependenciesEarlyBinding() async { let store = await TestStore(initialState: Feature_testDependenciesEarlyBinding.State()) { Feature_testDependenciesEarlyBinding() } withDependencies: { $0.date = .constant(Date(timeIntervalSince1970: 1_234_567_890)) } await store.send(.tap) { @Dependency(\.date.now) var now: Date $0.count = 1 $0.date = now } await store.receive(.response(42)) { @Dependency(\.date.now) var now: Date $0.count = 42 $0.date = now } } @MainActor func testPrepareDependenciesCalledOnce() { var count = 0 let store = TestStore(initialState: 0) { EmptyReducer() } withDependencies: { _ in count += 1 } XCTAssertEqual(count, 1) _ = store } func testEffectEmitAfterSkipInFlightEffects() async { let mainQueue = DispatchQueue.test enum Action: Equatable { case tap, response } let store = await TestStore(initialState: 0) { Reduce { state, action in switch action { case .tap: return .run { send in try await mainQueue.sleep(for: .seconds(1)) await send(.response) } case .response: state = 42 return .none } } } await store.send(.tap) await store.skipInFlightEffects() await mainQueue.advance(by: .seconds(1)) await store.receive(.response) { $0 = 42 } } @MainActor func testAssert_NonExhaustiveTestStore() async { let store = TestStore(initialState: 0) { EmptyReducer() } store.exhaustivity = .off store.assert { $0 = 0 } } @MainActor func testAssert_NonExhaustiveTestStore_Failure() async { let store = TestStore(initialState: 0) { EmptyReducer() } store.exhaustivity = .off XCTExpectFailure { store.assert { $0 = 1 } } issueMatcher: { $0.compactDescription == """ failed - A state change does not match expectation: … − 1 + 0 (Expected: −, Actual: +) """ } } @MainActor func testSubscribeReceiveCombineScheduler() async { let subject = PassthroughSubject() let scheduler = DispatchQueue.test struct State: Equatable { var count: Int = 0 } enum Action: Equatable { case increment case start } let store = TestStore(initialState: State()) { Reduce { state, action in switch action { case .start: return .publisher { subject .subscribe(on: scheduler) .receive(on: scheduler) .map { .increment } } case .increment: state.count += 1 return .none } } } let task = await store.send(.start) await scheduler.advance() subject.send() await scheduler.advance() await store.receive(.increment) { $0.count = 1 } await task.cancel() } func testMainSerialExecutor_AutoAssignsAndResets_False() async { uncheckedUseMainSerialExecutor = false XCTAssertFalse(uncheckedUseMainSerialExecutor) var store: TestStore? = await TestStore(initialState: 0) { EmptyReducer() } XCTAssertTrue(uncheckedUseMainSerialExecutor) store = nil XCTAssertFalse(uncheckedUseMainSerialExecutor) _ = store } func testMainSerialExecutor_AutoAssignsAndResets_True() async { uncheckedUseMainSerialExecutor = true XCTAssertTrue(uncheckedUseMainSerialExecutor) var store: TestStore? = await TestStore(initialState: 0) { EmptyReducer() } XCTAssertTrue(uncheckedUseMainSerialExecutor) store = nil XCTAssertTrue(uncheckedUseMainSerialExecutor) _ = store } func testReceiveCaseKeyPathWithValue() async { let store = await TestStore(initialState: 0) { Reduce { state, action in switch action { case .tap: return .send(.delegate(.success(42))) case .delegate: return .none case .view: return .none } } } await store.send(.tap) await store.receive(\.delegate.success, 42) XCTExpectFailure { $0.compactDescription == """ failed - Received unexpected action: …   Action.delegate( − .success(43) + .success(42)   ) (Expected: −, Actual: +) """ } await store.send(.tap) await store.receive(\.delegate.success, 43) } func testSendCaseKeyPath() async { let store = await TestStore(initialState: 0) { Reduce { state, action in switch action { case .tap: return .send(.delegate(.success(42))) case .delegate: return .none case .view(.tap): state = state + 1 return .send(.delegate(.success(42 * 42))) case let .view(.delete(indexSet)): let sum = indexSet.reduce(0, +) if sum == 42 { state = state + 1 } return .send(.delegate(.success(sum))) } } } await store.send(\.tap) await store.receive(\.delegate.success, 42) await store.send(\.view.tap) { $0 = 1 } await store.receive(\.delegate.success, 42 * 42) await store.send(\.view.delete, [0]) await store.receive(\.delegate.success, 0) await store.send(\.view.delete, [19, 23]) { $0 = 2 } await store.receive(\.delegate.success, 42) } func testBindingTestStore_WhenStateAndActionHaveSameName() async { let store = await TestStore(initialState: .init()) { SameNameForStateAndAction() } await store.send(.onAppear) await store.receive(\.isOn) await store.receive(\.binding.isOn) { $0.isOn = true } } @Reducer struct TestDismissCancelsEffects { struct State: Equatable {} enum Action { case dismiss case onTask } @Dependency(\.dismiss) var dismiss var body: some ReducerOf { Reduce { state, action in switch action { case .dismiss: return .run { _ in await dismiss() } case .onTask: return .run { _ in try await Task.never() } } } } } func testDismissCancelsEffects() async { let store = await TestStore(initialState: TestDismissCancelsEffects.State()) { TestDismissCancelsEffects() } await store.send(.onTask) await store.send(.dismiss) } func testDismissedStoreSend() async { let store = await TestStore(initialState: TestDismissCancelsEffects.State()) { TestDismissCancelsEffects() } await store.send(.dismiss) XCTExpectFailure { $0.compactDescription == """ failed - Can't send action to dismissed test store. """ } await store.send(.onTask) } } private struct Client: DependencyKey { var fetch: @Sendable () -> Int static let liveValue = Client(fetch: { 42 }) } extension DependencyValues { fileprivate var client: Client { get { self[Client.self] } set { self[Client.self] = newValue } } } @CasePathable private enum Action { case tap case delegate(Delegate) case view(View) @CasePathable enum Delegate { case success(Int) } @CasePathable enum View { case tap case delete(IndexSet) } } @Reducer struct SameNameForStateAndAction { @ObservableState struct State: Equatable { var isOn = false } enum Action: BindableAction { case binding(BindingAction) case onAppear case isOn(Bool) } var body: some ReducerOf { BindingReducer() Reduce { state, action in switch action { case .binding: return .none case .onAppear: return .send(.isOn(true)) case .isOn: return .send(.set(\.isOn, true)) } } } }