Files
swift-composable-architectu…/Tests/ComposableArchitectureTests/Reducers/StackReducerTests.swift
Mason Kim 71f8291ee7 Replace deprecated viewStore with store (#3341)
* [FIX] Replace deprecated viewStore with store

* [FIX] Replace deprecated viewStore with store in Article

* [TEST] Match the changed view store message with the test message

* [TEST] Match the changed view store message with the test message
2024-09-05 14:41:59 -07:00

1329 lines
37 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@_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<Element>([.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<Element>([.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<State, Action> {
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<Child.State>()
}
enum Action: Equatable {
case children(StackActionOf<Child>)
case pushChild
}
var body: some ReducerOf<Self> {
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<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
return .run { _ in
try await Task.never()
}
}
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case children(StackActionOf<Child>)
case popChild
case pushChild
}
var body: some ReducerOf<Self> {
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<State, Action> {
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<Child.State>()
}
enum Action: Equatable {
case children(StackActionOf<Child>)
case pushChild
}
var body: some ReducerOf<Self> {
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<State, Action> {
Reduce { state, action in
.run { _ in await self.dismiss() }
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case children(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<State, Action> {
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<Child.State>()
}
enum Action: Equatable {
case child(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<State, Action> {
Reduce { state, action in
switch action {
case .closeButtonTapped:
return .run { _ in
await self.dismiss()
}
}
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case children(StackActionOf<Child>)
case pushChild
}
var body: some ReducerOf<Self> {
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<Child.State>()
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<State, Action> {
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<Self> {
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<Path.State>()
}
enum Action: Equatable {
case path(StackActionOf<Path>)
case pushChild1
case pushChild2
}
var body: some ReducerOf<Self> {
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<State, Action> {
Reduce { state, action in
.run { _ in try await Task.never() }
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var path = StackState<Child.State>()
}
enum Action {
case path(StackActionOf<Child>)
case popToRoot
case pushChild
}
var body: some ReducerOf<Self> {
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<State, Action> {
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<Self> {
Scope(state: \.child1, action: \.child1) { Child() }
Scope(state: \.child2, action: \.child2) { Child() }
}
}
@Reducer
struct Parent {
struct State: Equatable {
var path = StackState<Path.State>()
}
enum Action: Equatable {
case path(StackActionOf<Path>)
case pushChild1
case pushChild2
}
var body: some ReducerOf<Self> {
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<TestSiblingCannotCancel.Path.State>()
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<State, Action> {
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<Self> {
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<Path.State>()
}
enum Action: Equatable {
case path(StackActionOf<Path>)
case popAll
case popFirst
}
var body: some ReducerOf<Self> {
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<Int>()
}
enum Action {
case path(StackAction<Int, Void>)
}
var body: some ReducerOf<Self> {
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<Int>()
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<Int>()
}
enum Action {
case path(StackAction<Int, Void>)
}
var body: some ReducerOf<Self> {
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<Int>([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<State, Action> {
Reduce { state, action in
.run { _ in try await Task.never() }
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var path = StackState<Child.State>()
}
enum Action {
case path(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
EmptyReducer()
.forEach(\.path, action: /Action.path) { Child() }
}
}
var path = StackState<Child.State>()
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<State, Action> {
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<Child.State>
}
enum Action: Equatable {
case child(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<State, Action> {
Reduce { state, action in
.run { _ in try await Task.never() }
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var children: StackState<Child.State>
}
enum Action: Equatable {
case child(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<State, Action> {
EmptyReducer()
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case child(StackActionOf<Child>)
case push
}
var body: some ReducerOf<Self> {
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<State, Action> {
EmptyReducer()
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case child(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<State, Action> {
EmptyReducer()
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case child(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<State, Action> {
EmptyReducer()
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case child(StackActionOf<Child>)
}
var body: some ReducerOf<Self> {
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<Int>()
}
enum Action: Equatable {
case buttonTapped
case path(StackAction<Int, Never>)
case response
}
var body: some ReducerOf<Self> {
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<Self> {
Reduce { state, action in
.run { _ in
try await Task.never()
}
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var children = StackState<Child.State>()
}
enum Action: Equatable {
case children(StackActionOf<Child>)
case tapAfter
case tapBefore
}
var body: some ReducerOf<Self> {
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)
}
}