@_spi(Internals) import ComposableArchitecture import XCTest @available(*, deprecated, message: "TODO: Update to use case pathable syntax with Swift 5.9") final class StackReducerTests: BaseTCATestCase { func testStackStateSubscriptCase() { enum Element: Equatable { case int(Int) case text(String) } var stack = StackState([.int(42)]) stack[id: 0, case: /Element.int]? += 1 XCTAssertEqual(stack[id: 0], .int(43)) stack[id: 0, case: /Element.int] = nil XCTAssertTrue(stack.isEmpty) } func testStackStateSubscriptCase_Unexpected() { enum Element: Equatable { case int(Int) case text(String) } var stack = StackState([.int(42)]) XCTExpectFailure { stack[id: 0, case: /Element.text]?.append("!") } issueMatcher: { $0.compactDescription == """ failed - Can't modify unrelated case "int" """ } XCTExpectFailure { stack[id: 0, case: /Element.text] = nil } issueMatcher: { $0.compactDescription == """ failed - Can't modify unrelated case "int" """ } XCTAssertEqual(Array(stack), [.int(42)]) } func testCustomDebugStringConvertible() { @Dependency(\.stackElementID) var stackElementID XCTAssertEqual(stackElementID.peek().generation, 0) XCTAssertEqual(stackElementID.next().customDumpDescription, "#0") XCTAssertEqual(stackElementID.peek().generation, 1) XCTAssertEqual(stackElementID.next().customDumpDescription, "#1") withDependencies { $0.context = .live } operation: { XCTAssertEqual(stackElementID.next().customDumpDescription, "#0") XCTAssertEqual(stackElementID.next().customDumpDescription, "#1") } } func testPresent() async { struct Child: Reducer { struct State: Equatable { var count = 0 } enum Action: Equatable { case decrementButtonTapped case incrementButtonTapped } var body: some Reducer { Reduce { state, action in switch action { case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case children(StackActionOf) case pushChild } var body: some ReducerOf { Reduce { state, action in switch action { case .children: return .none case .pushChild: state.children.append(Child.State()) return .none } } .forEach(\.children, action: /Action.children) { Child() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.pushChild) { $0.children.append(Child.State()) } } func testDismissFromParent() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case onAppear } var body: some Reducer { Reduce { state, action in switch action { case .onAppear: return .run { _ in try await Task.never() } } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case children(StackActionOf) case popChild case pushChild } var body: some ReducerOf { Reduce { state, action in switch action { case .children: return .none case .popChild: state.children.removeLast() return .none case .pushChild: state.children.append(Child.State()) return .none } } .forEach(\.children, action: /Action.children) { Child() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.pushChild) { $0.children.append(Child.State()) } await store.send(.children(.element(id: 0, action: .onAppear))) await store.send(.popChild) { $0.children.removeLast() } } func testDismissFromChild() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case closeButtonTapped case onAppear } @Dependency(\.dismiss) var dismiss var body: some Reducer { Reduce { state, action in switch action { case .closeButtonTapped: return .run { _ in await self.dismiss() } case .onAppear: return .run { _ in try await Task.never() } } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case children(StackActionOf) case pushChild } var body: some ReducerOf { Reduce { state, action in switch action { case .children: return .none case .pushChild: state.children.append(Child.State()) return .none } } .forEach(\.children, action: /Action.children) { Child() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.pushChild) { $0.children.append(Child.State()) } await store.send(.children(.element(id: 0, action: .onAppear))) await store.send(.children(.element(id: 0, action: .closeButtonTapped))) await store.receive(.children(.popFrom(id: 0))) { $0.children.removeLast() } } func testDismissReceiveWrongAction() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case tap } @Dependency(\.dismiss) var dismiss var body: some Reducer { Reduce { state, action in .run { _ in await self.dismiss() } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case children(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none }.forEach(\.children, action: /Action.children) { Child() } } } let store = await TestStore(initialState: Parent.State(children: StackState([Child.State()]))) { Parent() } XCTExpectFailure { $0.compactDescription == """ failed - Received unexpected action: …   StackReducerTests.Parent.Action.children( − .popFrom(id: #1) + .popFrom(id: #0)   ) (Expected: −, Received: +) """ } await store.send(.children(.element(id: 0, action: .tap))) await store.receive(.children(.popFrom(id: 1))) { $0.children = StackState() } } func testDismissFromIntermediateChild() async { struct Child: Reducer { struct State: Equatable { var count = 0 } enum Action: Equatable { case onAppear } @Dependency(\.dismiss) var dismiss @Dependency(\.mainQueue) var mainQueue var body: some Reducer { Reduce { state, action in switch action { case .onAppear: return .run { [count = state.count] _ in try await self.mainQueue.sleep(for: .seconds(count)) await self.dismiss() } } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case child(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none } .forEach(\.children, action: /Action.child) { Child() } } } let mainQueue = DispatchQueue.test let store = await TestStore(initialState: Parent.State()) { Parent() } withDependencies: { $0.mainQueue = mainQueue.eraseToAnyScheduler() } await store.send(.child(.push(id: 0, state: Child.State(count: 2)))) { $0.children[id: 0] = Child.State(count: 2) } await store.send(.child(.element(id: 0, action: .onAppear))) await store.send(.child(.push(id: 1, state: Child.State(count: 1)))) { $0.children[id: 1] = Child.State(count: 1) } await store.send(.child(.element(id: 1, action: .onAppear))) await store.send(.child(.push(id: 2, state: Child.State(count: 2)))) { $0.children[id: 2] = Child.State(count: 2) } await store.send(.child(.element(id: 2, action: .onAppear))) await mainQueue.advance(by: .seconds(1)) await store.receive(.child(.popFrom(id: 1))) { $0.children.removeLast(2) } await store.send(.child(.popFrom(id: 0))) { $0.children = StackState() } } func testDismissFromDeepLinkedChild() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case closeButtonTapped } @Dependency(\.dismiss) var dismiss var body: some Reducer { Reduce { state, action in switch action { case .closeButtonTapped: return .run { _ in await self.dismiss() } } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case children(StackActionOf) case pushChild } var body: some ReducerOf { Reduce { state, action in switch action { case .children: return .none case .pushChild: state.children.append(Child.State()) return .none } } .forEach(\.children, action: /Action.children) { Child() } } } var children = StackState() children.append(Child.State()) let store = await TestStore(initialState: Parent.State(children: children)) { Parent() } await store.send(.children(.element(id: 0, action: .closeButtonTapped))) await store.receive(.children(.popFrom(id: 0))) { $0.children.removeAll() } } func testEnumChild() async { struct Child: Reducer { struct State: Equatable { var count = 0 } enum Action: Equatable { case closeButtonTapped case incrementButtonTapped case onAppear } @Dependency(\.dismiss) var dismiss var body: some Reducer { Reduce { state, action in switch action { case .closeButtonTapped: return .run { _ in await self.dismiss() } case .incrementButtonTapped: state.count += 1 return .none case .onAppear: return .run { _ in try await Task.never() } } } } } struct Path: Reducer { enum State: Equatable { case child1(Child.State) case child2(Child.State) } enum Action: Equatable { case child1(Child.Action) case child2(Child.Action) } var body: some ReducerOf { Scope(state: /State.child1, action: /Action.child1) { Child() } Scope(state: /State.child2, action: /Action.child2) { Child() } } } struct Parent: Reducer { struct State: Equatable { var path = StackState() } enum Action: Equatable { case path(StackActionOf) case pushChild1 case pushChild2 } var body: some ReducerOf { Reduce { state, action in switch action { case .path: return .none case .pushChild1: state.path.append(.child1(Child.State())) return .none case .pushChild2: state.path.append(.child2(Child.State())) return .none } } .forEach(\.path, action: /Action.path) { Path() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.pushChild1) { $0.path.append(.child1(Child.State())) } await store.send(.path(.element(id: 0, action: .child1(.onAppear)))) await store.send(.pushChild2) { $0.path.append(.child2(Child.State())) } await store.send(.path(.element(id: 1, action: .child2(.onAppear)))) await store.send(.path(.popFrom(id: 0))) { $0.path.removeAll() } } func testParentDismiss() async { struct Child: Reducer { struct State: Equatable {} enum Action { case tap } @Dependency(\.dismiss) var dismiss var body: some Reducer { Reduce { state, action in .run { _ in try await Task.never() } } } } struct Parent: Reducer { struct State: Equatable { var path = StackState() } enum Action { case path(StackActionOf) case popToRoot case pushChild } var body: some ReducerOf { Reduce { state, action in switch action { case .path: return .none case .popToRoot: state.path.removeAll() return .none case .pushChild: state.path.append(Child.State()) return .none } } .forEach(\.path, action: /Action.path) { Child() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.pushChild) { $0.path.append(Child.State()) } await store.send(.path(.element(id: 0, action: .tap))) await store.send(.pushChild) { $0.path.append(Child.State()) } await store.send(.path(.element(id: 1, action: .tap))) await store.send(.popToRoot) { $0.path.removeAll() } } enum TestSiblingCannotCancel { @Reducer struct Child { struct State: Equatable { var count = 0 } enum Action: Equatable { case cancel case response(Int) case tap } @Dependency(\.mainQueue) var mainQueue enum CancelID: Hashable { case cancel } var body: some Reducer { Reduce { state, action in switch action { case .cancel: return .cancel(id: CancelID.cancel) case let .response(value): state.count = value return .none case .tap: return .run { send in try await self.mainQueue.sleep(for: .seconds(1)) await send(.response(42)) } .cancellable(id: CancelID.cancel) } } } } @Reducer struct Path { enum State: Equatable { case child1(Child.State) case child2(Child.State) } enum Action: Equatable { case child1(Child.Action) case child2(Child.Action) } var body: some ReducerOf { Scope(state: \.child1, action: \.child1) { Child() } Scope(state: \.child2, action: \.child2) { Child() } } } @Reducer struct Parent { struct State: Equatable { var path = StackState() } enum Action: Equatable { case path(StackActionOf) case pushChild1 case pushChild2 } var body: some ReducerOf { Reduce { state, action in switch action { case .path: return .none case .pushChild1: state.path.append(.child1(Child.State())) return .none case .pushChild2: state.path.append(.child2(Child.State())) return .none } } .forEach(\.path, action: \.path) { Path() } } } } func testSiblingCannotCancel() async { var path = StackState() path.append(.child1(TestSiblingCannotCancel.Child.State())) path.append(.child2(TestSiblingCannotCancel.Child.State())) let mainQueue = DispatchQueue.test let store = await TestStore(initialState: TestSiblingCannotCancel.Parent.State(path: path)) { TestSiblingCannotCancel.Parent() } withDependencies: { $0.mainQueue = mainQueue.eraseToAnyScheduler() } await store.send(.path(.element(id: 0, action: .child1(.tap)))) await store.send(.path(.element(id: 1, action: .child2(.tap)))) await store.send(.path(.element(id: 0, action: .child1(.cancel)))) await mainQueue.advance(by: .seconds(1)) await store.receive(.path(.element(id: 1, action: .child2(.response(42))))) { $0.path[id: 1, case: \.child2]?.count = 42 } await store.send(.path(.element(id: 0, action: .child1(.tap)))) await store.send(.path(.element(id: 1, action: .child2(.tap)))) await store.send(.path(.element(id: 1, action: .child2(.cancel)))) await mainQueue.advance(by: .seconds(1)) await store.receive(.path(.element(id: 0, action: .child1(.response(42))))) { $0.path[id: 0, case: \.child1]?.count = 42 } } enum TestFirstChildWhileEffectInFlight_DeliversToCorrectID { @Reducer struct Child { let id: Int struct State: Equatable { var count = 0 } enum Action: Equatable { case response(Int) case tap } @Dependency(\.mainQueue) var mainQueue var body: some Reducer { Reduce { state, action in switch action { case let .response(value): state.count += value return .none case .tap: return .run { send in try await self.mainQueue.sleep(for: .seconds(self.id)) await send(.response(self.id)) } } } } } @Reducer struct Path { enum State: Equatable { case child1(Child.State) case child2(Child.State) } enum Action: Equatable { case child1(Child.Action) case child2(Child.Action) } var body: some ReducerOf { Scope(state: \.child1, action: \.child1) { Child(id: 1) } Scope(state: \.child2, action: \.child2) { Child(id: 2) } } } @Reducer struct Parent { struct State: Equatable { var path = StackState() } enum Action: Equatable { case path(StackActionOf) case popAll case popFirst } var body: some ReducerOf { Reduce { state, action in switch action { case .path: return .none case .popAll: state.path = StackState() return .none case .popFirst: state.path[id: state.path.ids[0]] = nil return .none } } .forEach(\.path, action: \.path) { Path() } } } } func testFirstChildWhileEffectInFlight_DeliversToCorrectID() async { let mainQueue = DispatchQueue.test let store = await TestStore( initialState: TestFirstChildWhileEffectInFlight_DeliversToCorrectID.Parent.State( path: StackState([ .child1(TestFirstChildWhileEffectInFlight_DeliversToCorrectID.Child.State()), .child2(TestFirstChildWhileEffectInFlight_DeliversToCorrectID.Child.State()), ]) ) ) { TestFirstChildWhileEffectInFlight_DeliversToCorrectID.Parent() } withDependencies: { $0.mainQueue = mainQueue.eraseToAnyScheduler() } await store.send(.path(.element(id: 0, action: .child1(.tap)))) await store.send(.path(.element(id: 1, action: .child2(.tap)))) await mainQueue.advance(by: .seconds(1)) await store.receive(.path(.element(id: 0, action: .child1(.response(1))))) { $0.path[id: 0, case: \.child1]?.count = 1 } await mainQueue.advance(by: .seconds(1)) await store.receive(.path(.element(id: 1, action: .child2(.response(2))))) { $0.path[id: 1, case: \.child2]?.count = 2 } await store.send(.path(.element(id: 0, action: .child1(.tap)))) await store.send(.path(.element(id: 1, action: .child2(.tap)))) await store.send(.popFirst) { $0.path[id: 0] = nil } await mainQueue.advance(by: .seconds(2)) await store.receive(.path(.element(id: 1, action: .child2(.response(2))))) { $0.path[id: 1, case: \.child2]?.count = 4 } await store.send(.popFirst) { $0.path[id: 1] = nil } } @MainActor func testSendActionWithIDThatDoesNotExist() async { struct Parent: Reducer { struct State: Equatable { var path = StackState() } enum Action { case path(StackAction) } var body: some ReducerOf { EmptyReducer() .forEach(\.path, action: /Action.path) {} } } let line = #line - 3 XCTExpectFailure { $0.compactDescription == """ failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ received an action for a missing element. … Action: () This is generally considered an application logic error, and can happen for a few reasons: • A parent reducer removed an element with this ID before this reducer ran. This reducer \ must run before any other reducer removes an element, which ensures that element \ reducers can handle their actions while their state is still available. • An in-flight effect emitted this action when state contained no element at this ID. \ While it may be perfectly reasonable to ignore this action, consider canceling the \ associated effect before an element is removed, especially if it is a long-living effect. • This action was sent to the store while its state contained no element at this ID. To \ fix this make sure that actions for this reducer can only be sent from a store when \ its state contains an element at this id. In SwiftUI applications, use \ "NavigationStack.init(path:)" with a binding to a store. """ } var path = StackState() path.append(1) let store = TestStore(initialState: Parent.State(path: path)) { Parent() } await store.send(.path(.element(id: 999, action: ()))) } @MainActor func testPopIDThatDoesNotExist() async { struct Parent: Reducer { struct State: Equatable { var path = StackState() } enum Action { case path(StackAction) } var body: some ReducerOf { EmptyReducer() .forEach(\.path, action: /Action.path) {} } } let line = #line - 3 XCTExpectFailure { $0.compactDescription == """ failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ received a "popFrom" action for a missing element. … ID: #999 Path IDs: [#0] """ } let store = TestStore(initialState: Parent.State(path: StackState([1]))) { Parent() } await store.send(.path(.popFrom(id: 999))) } func testChildWithInFlightEffect() async { struct Child: Reducer { struct State: Equatable {} enum Action { case tap } var body: some Reducer { Reduce { state, action in .run { _ in try await Task.never() } } } } struct Parent: Reducer { struct State: Equatable { var path = StackState() } enum Action { case path(StackActionOf) } var body: some ReducerOf { EmptyReducer() .forEach(\.path, action: /Action.path) { Child() } } } var path = StackState() path.append(Child.State()) let store = await TestStore(initialState: Parent.State(path: path)) { Parent() } let line = #line await store.send(.path(.element(id: 0, action: .tap))) XCTExpectFailure { $0.sourceCodeContext.location?.fileURL.absoluteString.contains("BaseTCATestCase") == true || $0.sourceCodeContext.location?.lineNumber == line + 1 && $0.compactDescription == """ failed - An effect returned for this action is still running. It must complete before \ the end of the test. … To fix, inspect any effects the reducer returns for this action and ensure that all \ of them complete by the end of the test. There are a few reasons why an effect may \ not have completed: • If using async/await in your effect, it may need a little bit of time to properly \ finish. To fix you can simply perform "await store.finish()" at the end of your test. • If an effect uses a clock (or scheduler, via "receive(on:)", "delay", "debounce", \ etc.), make sure that you wait enough time for it to perform the effect. If you are \ using a test clock/scheduler, advance it so that the effects may complete, or \ consider using an immediate clock/scheduler to immediately perform the effect instead. • If you are returning a long-living effect (timers, notifications, subjects, etc.), \ then make sure those effects are torn down by marking the effect ".cancellable" and \ returning a corresponding cancellation effect ("Effect.cancel") from another action, \ or, if your effect is driven by a Combine subject, send it a completion. • If you do not wish to assert on these effects, perform "await \ store.skipInFlightEffects()", or consider using a non-exhaustive test store: \ "store.exhaustivity = .off". """ } } func testMultipleChildEffects() async { struct Child: Reducer { struct State: Equatable { var count = 0 } enum Action: Equatable { case tap case response(Int) } @Dependency(\.mainQueue) var mainQueue var body: some Reducer { Reduce { state, action in switch action { case .tap: return .run { [count = state.count] send in try await self.mainQueue.sleep(for: .seconds(count)) await send(.response(42)) } case let .response(value): state.count = value return .none } } } } struct Parent: Reducer { struct State: Equatable { var children: StackState } enum Action: Equatable { case child(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none } .forEach(\.children, action: /Action.child) { Child() } } } let mainQueue = DispatchQueue.test let store = await TestStore( initialState: Parent.State( children: StackState([ Child.State(count: 1), Child.State(count: 2), ]) ) ) { Parent() } withDependencies: { $0.mainQueue = mainQueue.eraseToAnyScheduler() } await store.send(.child(.element(id: 0, action: .tap))) await store.send(.child(.element(id: 1, action: .tap))) await mainQueue.advance(by: .seconds(1)) await store.receive(.child(.element(id: 0, action: .response(42)))) { $0.children[id: 0]?.count = 42 } await mainQueue.advance(by: .seconds(1)) await store.receive(.child(.element(id: 1, action: .response(42)))) { $0.children[id: 1]?.count = 42 } } func testChildEffectCancellation() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case tap } var body: some Reducer { Reduce { state, action in .run { _ in try await Task.never() } } } } struct Parent: Reducer { struct State: Equatable { var children: StackState } enum Action: Equatable { case child(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none } .forEach(\.children, action: /Action.child) { Child() } } } let store = await TestStore( initialState: Parent.State( children: StackState([ Child.State() ]) ) ) { Parent() } await store.send(.child(.element(id: 0, action: .tap))) await store.send(.child(.popFrom(id: 0))) { $0.children[id: 0] = nil } } func testPush() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} var body: some Reducer { EmptyReducer() } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case child(StackActionOf) case push } var body: some ReducerOf { Reduce { state, action in switch action { case .child: return .none case .push: state.children.append(Child.State()) return .none } } .forEach(\.children, action: /Action.child) { Child() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.child(.push(id: 0, state: Child.State()))) { $0.children[id: 0] = Child.State() } await store.send(.push) { $0.children[id: 1] = Child.State() } await store.send(.child(.push(id: 2, state: Child.State()))) { $0.children[id: 2] = Child.State() } await store.send(.push) { $0.children[id: 3] = Child.State() } await store.send(.child(.popFrom(id: 0))) { $0.children = StackState() } await store.send(.child(.push(id: 0, state: Child.State()))) { $0.children[id: 0] = Child.State() } } func testPushReusedID() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} var body: some Reducer { EmptyReducer() } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case child(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none } .forEach(\.children, action: /Action.child) { Child() } } } let line = #line - 3 let store = await TestStore(initialState: Parent.State()) { Parent() } XCTExpectFailure { $0.compactDescription == """ failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ received a "push" action for an element it already contains. … ID: #0 Path IDs: [#0] """ } await store.send(.child(.push(id: 0, state: Child.State()))) { $0.children[id: 0] = Child.State() } await store.send(.child(.push(id: 0, state: Child.State()))) } func testPushIDGreaterThanNextGeneration() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} var body: some Reducer { EmptyReducer() } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case child(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none } .forEach(\.children, action: /Action.child) { Child() } } } let line = #line - 3 let store = await TestStore(initialState: Parent.State()) { Parent() } XCTExpectFailure { $0.compactDescription == """ failed - A "forEach" at "ComposableArchitectureTests/StackReducerTests.swift:\(line)" \ received a "push" action with an unexpected generational ID. … Received ID: #1 Expected ID: #0 """ } await store.send(.child(.push(id: 1, state: Child.State()))) { $0.children[id: 1] = Child.State() } } func testMismatchedIDFailure() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable {} var body: some Reducer { EmptyReducer() } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case child(StackActionOf) } var body: some ReducerOf { Reduce { _, _ in .none }.forEach(\.children, action: /Action.child) { Child() } } } let store = await TestStore(initialState: Parent.State()) { Parent() } XCTExpectFailure { $0.compactDescription == """ failed - A state change does not match expectation: …   StackReducerTests.Parent.State(   children: [ − #1: StackReducerTests.Child.State() + #0: StackReducerTests.Child.State()   ]   ) (Expected: −, Actual: +) """ } await store.send(.child(.push(id: 0, state: Child.State()))) { $0.children[id: 1] = Child.State() } } func testSendCopiesStackElementIDGenerator() async { struct Feature: Reducer { struct State: Equatable { var path = StackState() } enum Action: Equatable { case buttonTapped case path(StackAction) case response } var body: some ReducerOf { Reduce { state, action in switch action { case .buttonTapped: state.path.append(1) return .send(.response) case .path: return .none case .response: state.path.append(2) return .none } } .forEach(\.path, action: /Action.path) {} } } let store = await TestStore(initialState: Feature.State()) { Feature() } await store.send(.buttonTapped) { $0.path[id: 0] = 1 @Dependency(\.stackElementID) var stackElementID _ = stackElementID.next() _ = stackElementID.next() _ = stackElementID.next() } await store.receive(.response) { $0.path[id: 1] = 2 @Dependency(\.stackElementID) var stackElementID _ = stackElementID.next() _ = stackElementID.next() _ = stackElementID.next() } await store.send(.buttonTapped) { $0.path[id: 2] = 1 } await store.receive(.response) { $0.path[id: 3] = 2 } } func testOuterCancellation() async { struct Child: Reducer { struct State: Equatable {} enum Action: Equatable { case onAppear } var body: some ReducerOf { Reduce { state, action in .run { _ in try await Task.never() } } } } struct Parent: Reducer { struct State: Equatable { var children = StackState() } enum Action: Equatable { case children(StackActionOf) case tapAfter case tapBefore } var body: some ReducerOf { Reduce { state, action in switch action { case .children: return .none case .tapAfter: return .none case .tapBefore: state.children.removeAll() return .none } } Reduce { state, action in switch action { case .children: return .none case .tapAfter: return .none case .tapBefore: return .none } } .forEach(\.children, action: /Action.children) { Child() } Reduce { state, action in switch action { case .children: return .none case .tapAfter: state.children.removeAll() return .none case .tapBefore: return .none } } } } let store = await TestStore(initialState: Parent.State()) { Parent() } await store.send(.children(.push(id: 0, state: Child.State()))) { $0.children[id: 0] = Child.State() } await store.send(.children(.element(id: 0, action: .onAppear))) await store.send(.tapBefore) { $0.children.removeAll() } await store.send(.children(.push(id: 1, state: Child.State()))) { $0.children[id: 1] = Child.State() } await store.send(.children(.element(id: 1, action: .onAppear))) await store.send(.tapAfter) { $0.children.removeAll() } // NB: Another action needs to come into the `ifLet` to cancel the child action await store.send(.tapAfter) } }