Files
swift-composable-architectu…/Tests/ComposableArchitectureTests/Reducers/IfLetReducerTests.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

352 lines
9.9 KiB
Swift

import ComposableArchitecture
import XCTest
@available(*, deprecated, message: "TODO: Update to use case pathable syntax with Swift 5.9")
final class IfLetReducerTests: BaseTCATestCase {
func testNilChild() async {
let store = await TestStore(initialState: Int?.none) {
EmptyReducer<Int?, Void>()
.ifLet(\.self, action: \.self) {}
}
XCTExpectFailure {
$0.compactDescription == """
failed - An "ifLet" at "\(#fileID):\(#line - 5)" received a child action when child state \
was "nil". …
Action:
()
This is generally considered an application logic error, and can happen for a few \
reasons:
• A parent reducer set child state to "nil" before this reducer ran. This reducer must \
run before any other reducer sets child state to "nil". This ensures that child \
reducers can handle their actions while their state is still available.
• An in-flight effect emitted this action when child state was "nil". While it may be \
perfectly reasonable to ignore this action, consider canceling the associated effect \
before child state becomes "nil", especially if it is a long-living effect.
• This action was sent to the store while state was "nil". Make sure that actions for \
this reducer can only be sent from a store when state is non-"nil". In SwiftUI \
applications, use "IfLetStore".
"""
}
await store.send(())
}
func testEffectCancellation() async {
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
struct Child: Reducer {
struct State: Equatable {
var count = 0
}
enum Action: Equatable {
case timerButtonTapped
case timerTick
}
@Dependency(\.continuousClock) var clock
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .timerButtonTapped:
return .run { send in
for await _ in self.clock.timer(interval: .seconds(1)) {
await send(.timerTick)
}
}
case .timerTick:
state.count += 1
return .none
}
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var child: Child.State?
}
enum Action: Equatable {
case child(Child.Action)
case childButtonTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .child:
return .none
case .childButtonTapped:
state.child = state.child == nil ? Child.State() : nil
return .none
}
}
.ifLet(\.child, action: /Action.child) {
Child()
}
}
}
let clock = TestClock()
let store = await TestStore(initialState: Parent.State()) {
Parent()
} withDependencies: {
$0.continuousClock = clock
}
await store.send(.childButtonTapped) {
$0.child = Child.State()
}
await store.send(.child(.timerButtonTapped))
await clock.advance(by: .seconds(2))
await store.receive(.child(.timerTick)) {
XCTModify(&$0.child) {
$0.count = 1
}
}
await store.receive(.child(.timerTick)) {
XCTModify(&$0.child) {
$0.count = 2
}
}
await store.send(.childButtonTapped) {
$0.child = nil
}
}
}
func testGrandchildEffectCancellation() async {
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
struct GrandChild: Reducer {
struct State: Equatable {
var count = 0
}
enum Action: Equatable {
case timerButtonTapped
case timerTick
}
@Dependency(\.continuousClock) var clock
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .timerButtonTapped:
return .run { send in
for await _ in self.clock.timer(interval: .seconds(1)) {
await send(.timerTick)
}
}
case .timerTick:
state.count += 1
return .none
}
}
}
}
struct Child: Reducer {
struct State: Equatable {
var grandChild: GrandChild.State?
}
enum Action: Equatable {
case grandChild(GrandChild.Action)
}
var body: some Reducer<State, Action> {
EmptyReducer()
.ifLet(\.grandChild, action: /Action.grandChild) {
GrandChild()
}
}
}
struct Parent: Reducer {
struct State: Equatable {
var child: Child.State?
}
enum Action: Equatable {
case child(Child.Action)
case exitButtonTapped
case startButtonTapped
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .child:
return .none
case .exitButtonTapped:
state.child = nil
return .none
case .startButtonTapped:
state.child = Child.State(grandChild: GrandChild.State())
return .none
}
}
.ifLet(\.child, action: /Action.child) {
Child()
}
}
}
let clock = TestClock()
let store = await TestStore(initialState: Parent.State()) {
Parent()
} withDependencies: {
$0.continuousClock = clock
}
await store.send(.startButtonTapped) {
$0.child = Child.State(grandChild: GrandChild.State())
}
await store.send(.child(.grandChild(.timerButtonTapped)))
await clock.advance(by: .seconds(1))
await store.receive(.child(.grandChild(.timerTick))) {
XCTModify(&$0.child) {
XCTModify(&$0.grandChild) {
$0.count = 1
}
}
}
await store.send(.exitButtonTapped) {
$0.child = nil
}
}
}
func testEphemeralState() async {
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
struct Parent: Reducer {
struct State: Equatable {
var alert: AlertState<AlertAction>?
}
enum Action: Equatable {
case alert(AlertAction)
case tap
}
enum AlertAction { case ok }
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .alert:
return .none
case .tap:
state.alert = AlertState { TextState("Hi!") }
return .none
}
}
.ifLet(\.alert, action: /Action.alert) {
}
}
}
let store = await TestStore(initialState: Parent.State()) {
Parent()
}
await store.send(.tap) {
$0.alert = AlertState { TextState("Hi!") }
}
await store.send(.alert(.ok)) {
$0.alert = nil
}
}
}
func testIdentifiableChild() async {
struct Feature: Reducer {
struct State: Equatable {
var child: Child.State?
}
enum Action: Equatable {
case child(Child.Action)
case newChild
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .child:
return .none
case .newChild:
guard let childState = state.child
else { return .none }
state.child = Child.State(id: childState.id + 1)
return .none
}
}
.ifLet(\.child, action: /Action.child) { Child() }
}
}
struct Child: Reducer {
struct State: Equatable, Identifiable {
let id: Int
var value = 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 { [id = state.id] send in
try await mainQueue.sleep(for: .seconds(0))
await send(.response(id))
}
case let .response(value):
state.value = value
return .none
}
}
}
}
let mainQueue = DispatchQueue.test
let store = await TestStore(initialState: Feature.State(child: Child.State(id: 1))) {
Feature()
} withDependencies: {
$0.mainQueue = mainQueue.eraseToAnyScheduler()
}
await store.send(.child(.tap))
await store.send(.newChild) {
$0.child = Child.State(id: 2)
}
await store.send(.child(.tap))
await mainQueue.advance()
await store.receive(.child(.response(2))) {
$0.child = Child.State(id: 2, value: 2)
}
}
func testEphemeralDismissal() async {
struct Feature: Reducer {
struct State: Equatable {
var alert: AlertState<AlertAction>?
}
enum Action: Equatable {
case alert(AlertAction)
case tap
}
enum AlertAction: Equatable {
case again
case ok
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .alert(.ok):
return .none
case .alert(.again), .tap:
state.alert = AlertState { TextState("Hello") }
return .none
}
}
.ifLet(\.alert, action: /Action.alert)
}
}
let store = await TestStore(initialState: Feature.State()) { Feature() }
await store.send(.tap) {
$0.alert = AlertState { TextState("Hello") }
}
await store.send(.alert(.again))
await store.send(.alert(.ok)) {
$0.alert = nil
}
}
}