import Combine @_spi(Logging) import ComposableArchitecture import XCTest @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) final class StoreLifetimeTests: BaseTCATestCase { @available(*, deprecated) @MainActor func testStoreCaching() { let grandparentStore = Store(initialState: Grandparent.State()) { Grandparent() } let parentStore = grandparentStore.scope(state: \.child, action: \.child) XCTAssertTrue(parentStore === grandparentStore.scope(state: \.child, action: \.child)) XCTAssertFalse( parentStore === grandparentStore.scope(state: { $0.child }, action: { .child($0) }) ) let childStore = parentStore.scope(state: \.child, action: \.child) XCTAssertTrue(childStore === parentStore.scope(state: \.child, action: \.child)) XCTAssertFalse( childStore === parentStore.scope(state: { $0.child }, action: { .child($0) }) ) } @available(*, deprecated) @MainActor func testStoreInvalidation() { let grandparentStore = Store(initialState: Grandparent.State()) { Grandparent() } var parentStore: Store! = grandparentStore.scope(state: { $0.child }, action: { .child($0) }) let childStore = parentStore.scope(state: \.child, action: \.child) childStore.send(.tap) XCTAssertEqual(1, grandparentStore.withState(\.child.child.count)) XCTAssertEqual(1, parentStore.withState(\.child.count)) XCTAssertEqual(1, childStore.withState(\.count)) grandparentStore.send(.incrementGrandchild) XCTAssertEqual(2, grandparentStore.withState(\.child.child.count)) XCTAssertEqual(2, parentStore.withState(\.child.count)) XCTAssertEqual(2, childStore.withState(\.count)) parentStore = nil childStore.send(.tap) XCTAssertEqual(3, grandparentStore.withState(\.child.child.count)) XCTAssertEqual(3, childStore.withState(\.count)) grandparentStore.send(.incrementGrandchild) XCTAssertEqual(4, grandparentStore.withState(\.child.child.count)) XCTAssertEqual(4, childStore.withState(\.count)) } #if DEBUG @MainActor func testStoreDeinit() { Logger.shared.isEnabled = true do { let store = Store(initialState: ()) {} _ = store } XCTAssertEqual( Logger.shared.logs, [ "Store<(), ()>.init", "Store<(), ()>.deinit", ] ) } @MainActor func testStoreDeinit_RunningEffect_MainActor() async { Logger.shared.isEnabled = true let effectFinished = self.expectation(description: "Effect finished") do { let store = Store(initialState: ()) { Reduce { state, _ in .run { _ in try? await Task.never() effectFinished.fulfill() } } } store.send(()) _ = store } XCTAssertEqual( Logger.shared.logs, [ "Store<(), ()>.init", "Store<(), ()>.deinit", ] ) await self.fulfillment(of: [effectFinished], timeout: 0.5) } func testStoreDeinit_RunningEffect() async { let effectFinished = self.expectation(description: "Effect finished") do { let store = await Store(initialState: ()) { Reduce { state, _ in .run { _ in try? await Task.never() effectFinished.fulfill() } } } await store.send(()) _ = store } await self.fulfillment(of: [effectFinished], timeout: 0.5) } @MainActor func testStoreDeinit_RunningCombineEffect() async { Logger.shared.isEnabled = true let effectFinished = self.expectation(description: "Effect finished") do { let store = Store(initialState: ()) { Reduce { state, _ in .publisher { Empty(completeImmediately: false) .handleEvents(receiveCancel: { effectFinished.fulfill() }) } } } store.send(()) _ = store } XCTAssertEqual( Logger.shared.logs, [ "Store<(), ()>.init", "Store<(), ()>.deinit", ] ) await self.fulfillment(of: [effectFinished], timeout: 0.5) } #endif @MainActor @available(*, deprecated) func testUnCachedStores() async throws { Logger.shared.isEnabled = true let store = Store(initialState: Parent.State()) { Parent() } withDependencies: { $0.continuousClock = ContinuousClock() } do { let child = store.scope(state: { $0.child }, action: { .child($0) }) child.send(.start) XCTAssertEqual(store.withState(\.child.count), 1) } try await Task.sleep(nanoseconds: 1_000_000_000) XCTAssertEqual(store.withState(\.child.count), 2) } } @Reducer @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Child { struct State: Equatable { var count = 0 } enum Action { case tap case start case response } @Dependency(\.continuousClock) var clock var body: some ReducerOf { Reduce { state, action in switch action { case .tap: state.count += 1 return .none case .start: state.count += 1 return .run { send in try await clock.sleep(for: .seconds(0)) await send(.response) } case .response: state.count += 1 return .none } } } } @Reducer @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Parent { struct State: Equatable { var child = Child.State() } enum Action { case child(Child.Action) } var body: some ReducerOf { Scope(state: \.child, action: \.child) { Child() } } } @Reducer @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Grandparent { struct State: Equatable { var child = Parent.State() } enum Action { case child(Parent.Action) case incrementGrandchild } var body: some ReducerOf { Scope(state: \.child, action: \.child) { Parent() } Reduce { state, action in switch action { case .child: return .none case .incrementGrandchild: state.child.child.count += 1 return .none } } } }