#if DEBUG import Combine @_spi(Internals) import ComposableArchitecture import XCTest final class RuntimeWarningTests: BaseTCATestCase { @MainActor func testBindingUnhandledAction() async throws { let line = #line + 2 struct State: Equatable { @BindingState var value = 0 } enum Action: BindableAction, Equatable { case binding(BindingAction) } let store = Store(initialState: State()) {} XCTExpectFailure { $0.compactDescription == """ failed - A binding action sent from a store for binding state defined at \ "\(#fileID):\(line)" was not handled. … Action: RuntimeWarningTests.Action.binding(.set(_, 42)) To fix this, invoke "BindingReducer()" from your feature reducer's "body". """ } let viewStore = ViewStore(store, observe: { $0 }) viewStore.$value.wrappedValue = 42 try await Task.sleep(nanoseconds: 1_000_000) } @ObservableState struct TestObservableBindingUnhandledActionState: Equatable { var count = 0 } @MainActor func testObservableBindingUnhandledAction() async throws { typealias State = TestObservableBindingUnhandledActionState enum Action: BindableAction, Equatable { case binding(BindingAction) } let store = Store(initialState: State()) {} XCTExpectFailure { $0.compactDescription == """ failed - A binding action sent from a store was not handled. … Action: RuntimeWarningTests.Action.binding(.set(_, 42)) To fix this, invoke "BindingReducer()" from your feature reducer's "body". """ } store.count = 42 try await Task.sleep(nanoseconds: 1_000_000) } @MainActor func testBindingUnhandledAction_BindingState() async throws { struct State: Equatable { @BindingState var value = 0 } let line = #line - 2 enum Action: BindableAction, Equatable { case binding(BindingAction) } let store = Store(initialState: State()) {} XCTExpectFailure { $0.compactDescription == """ failed - A binding action sent from a store for binding state defined at \ "\(#fileID):\(line)" was not handled. … Action: RuntimeWarningTests.Action.binding(.set(_, 42)) To fix this, invoke "BindingReducer()" from your feature reducer's "body". """ } let viewStore = ViewStore(store, observe: { $0 }) viewStore.$value.wrappedValue = 42 try await Task.sleep(nanoseconds: 1_000_000) } @Reducer struct TestStorePath_NotIntegrated { @ObservableState struct State: Equatable { var path = StackState() } enum Action { case path(StackAction) } } @MainActor func testStorePath_NotIntegrated() { let store = Store(initialState: TestStorePath_NotIntegrated.State()) { TestStorePath_NotIntegrated() } XCTExpectFailure { store.scope(state: \.path, action: \.path)[ fileID: "file.swift", filePath: "/file.swift", line: 1, column: 1 ] = .init() } issueMatcher: { $0.compactDescription == """ failed - A navigation stack binding at "file.swift:1" was written to with a path that \ has the same number of elements that already exist in the store. A view should only \ write to this binding with a path that has pushed a new element onto the stack, or \ popped one or more elements from the stack. This usually means the "forEach" has not been integrated with the reducer powering the \ store, and this reducer is responsible for handling stack actions. To fix this, ensure that "forEach" is invoked from the reducer's "body": Reduce { state, action in // ... } .forEach(\\.path, action: \\.path) { Path() } And ensure that every parent reducer is integrated into the root reducer that powers \ the store. """ } } @Reducer struct TestStoreDestination_NotIntegrated { @Reducer struct Destination {} @ObservableState struct State: Equatable { @Presents var destination: Destination.State? } enum Action { case destination(PresentationAction) } } @MainActor func testStoreDestination_NotIntegrated() { let store = Store( initialState: TestStoreDestination_NotIntegrated.State(destination: .init()) ) { TestStoreDestination_NotIntegrated() } XCTExpectFailure { store[ id: nil, state: \.destination, action: \.destination, isInViewBody: false, fileID: "file.swift", filePath: "/file.swift", line: 1, column: 1 ] = nil } issueMatcher: { $0.compactDescription == """ failed - A binding at "file.swift:1" was set to "nil", but the store destination wasn't \ nil'd out. This usually means an "ifLet" has not been integrated with the reducer powering the \ store, and this reducer is responsible for handling presentation actions. To fix this, ensure that "ifLet" is invoked from the reducer's "body": Reduce { state, action in // ... } .ifLet(\\.destination, action: \\.destination) { Destination() } And ensure that every parent reducer is integrated into the root reducer that powers the \ store. """ } } } #endif