mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-20 09:11:33 +01:00
* Isolate cancellation in root stores. * clean up * wip * fix cancellation id prefix. * wip
2678 lines
75 KiB
Swift
2678 lines
75 KiB
Swift
import ComposableArchitecture
|
|
import XCTest
|
|
|
|
@available(*, deprecated, message: "TODO: Update to use case pathable syntax with Swift 5.9")
|
|
final class PresentationReducerTests: BaseTCATestCase {
|
|
func testPresentationStateSubscriptCase() {
|
|
enum Child: Equatable {
|
|
case int(Int)
|
|
case text(String)
|
|
}
|
|
|
|
struct Parent: Equatable {
|
|
@PresentationState var child: Child?
|
|
}
|
|
|
|
var parent = Parent(child: .int(42))
|
|
|
|
parent.$child[case: /Child.int]? += 1
|
|
XCTAssertEqual(parent.child, .int(43))
|
|
|
|
parent.$child[case: /Child.int] = nil
|
|
XCTAssertNil(parent.child)
|
|
}
|
|
|
|
func testPresentationStateSubscriptCase_Unexpected() {
|
|
enum Child: Equatable {
|
|
case int(Int)
|
|
case text(String)
|
|
}
|
|
|
|
struct Parent: Equatable {
|
|
@PresentationState var child: Child?
|
|
}
|
|
|
|
var parent = Parent(child: .int(42))
|
|
|
|
XCTExpectFailure {
|
|
parent.$child[case: /Child.text]?.append("!")
|
|
} issueMatcher: {
|
|
$0.compactDescription == """
|
|
failed - Can't modify unrelated case "int"
|
|
"""
|
|
}
|
|
|
|
XCTExpectFailure {
|
|
parent.$child[case: /Child.text] = nil
|
|
} issueMatcher: {
|
|
$0.compactDescription == """
|
|
failed - Can't modify unrelated case "int"
|
|
"""
|
|
}
|
|
|
|
XCTAssertEqual(parent.child, .int(42))
|
|
}
|
|
|
|
func testPresentation_parentDismissal() 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 {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.incrementButtonTapped))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
func testPresentation_parentDismissal_NilOut() 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 {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case dismissChild
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .dismissChild:
|
|
state.child = nil
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.incrementButtonTapped))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.send(.dismissChild) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
func testPresentation_childDismissal() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case closeButtonTapped
|
|
case decrementButtonTapped
|
|
case incrementButtonTapped
|
|
}
|
|
@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 .decrementButtonTapped:
|
|
state.count -= 1
|
|
return .none
|
|
case .incrementButtonTapped:
|
|
state.count += 1
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
var lastCount: Int?
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child(.dismiss):
|
|
state.lastCount = state.child?.count
|
|
return .none
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.decrementButtonTapped))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = -1
|
|
}
|
|
}
|
|
await store.send(.child(.presented(.closeButtonTapped)))
|
|
await store.receive(.child(.dismiss)) {
|
|
$0.child = nil
|
|
$0.lastCount = -1
|
|
}
|
|
}
|
|
|
|
func testPresentation_parentDismissal_effects() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case startButtonTapped
|
|
case tick
|
|
}
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
for try await _ in clock.timer(interval: .seconds(1)) {
|
|
await send(.tick)
|
|
}
|
|
}
|
|
case .tick:
|
|
state.count += 1
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.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(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(2))
|
|
await store.receive(.child(.presented(.tick))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.receive(.child(.presented(.tick))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 2
|
|
}
|
|
}
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testPresentation_childDismissal_effects() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case closeButtonTapped
|
|
case startButtonTapped
|
|
case tick
|
|
}
|
|
@Dependency(\.continuousClock) var clock
|
|
@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 .startButtonTapped:
|
|
return .run { send in
|
|
for try await _ in clock.timer(interval: .seconds(1)) {
|
|
await send(.tick)
|
|
}
|
|
}
|
|
case .tick:
|
|
state.count += 1
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.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(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(2))
|
|
await store.receive(.child(.presented(.tick))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.receive(.child(.presented(.tick))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 2
|
|
}
|
|
}
|
|
await store.send(.child(.presented(.closeButtonTapped)))
|
|
await store.receive(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testPresentation_identifiableDismissal_effects() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable, Identifiable {
|
|
let id: UUID
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case startButtonTapped
|
|
case tick
|
|
}
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
for try await _ in clock.timer(interval: .seconds(1)) {
|
|
await send(.tick)
|
|
}
|
|
}
|
|
case .tick:
|
|
state.count += 1
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
@Dependency(\.uuid) var uuid
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State(id: self.uuid())
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let clock = TestClock()
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.continuousClock = clock
|
|
$0.uuid = .incrementing
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State(id: UUID(0))
|
|
}
|
|
await store.send(.child(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(2))
|
|
await store.receive(.child(.presented(.tick))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.receive(.child(.presented(.tick))) {
|
|
XCTModify(&$0.child) {
|
|
$0.count = 2
|
|
}
|
|
}
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State(id: UUID(1))
|
|
}
|
|
await clock.advance(by: .seconds(2))
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testPresentation_LeavePresented() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
}
|
|
|
|
func testPresentation_LeavePresented_FinishStore() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.finish()
|
|
}
|
|
|
|
func testInertPresentation() async {
|
|
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var alert: AlertState<Action.Alert>?
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(PresentationAction<Alert>)
|
|
case presentAlert
|
|
|
|
enum Alert: Equatable {}
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .alert:
|
|
return .none
|
|
case .presentAlert:
|
|
state.alert = AlertState {
|
|
TextState("Uh oh!")
|
|
}
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$alert, action: /Action.alert) {}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentAlert) {
|
|
$0.alert = AlertState {
|
|
TextState("Uh oh!")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testInertPresentation_dismissal() async {
|
|
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var alert: AlertState<Action.Alert>?
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(PresentationAction<Alert>)
|
|
case presentAlert
|
|
|
|
enum Alert: Equatable {}
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .alert:
|
|
return .none
|
|
case .presentAlert:
|
|
state.alert = AlertState {
|
|
TextState("Uh oh!")
|
|
}
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$alert, action: /Action.alert) {}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentAlert) {
|
|
$0.alert = AlertState {
|
|
TextState("Uh oh!")
|
|
}
|
|
}
|
|
await store.send(.alert(.dismiss)) {
|
|
$0.alert = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testInertPresentation_automaticDismissal() async {
|
|
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var alert: AlertState<Action.Alert>?
|
|
var isDeleted = false
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(PresentationAction<Alert>)
|
|
case presentAlert
|
|
|
|
enum Alert: Equatable {
|
|
case deleteButtonTapped
|
|
}
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .alert(.presented(.deleteButtonTapped)):
|
|
state.isDeleted = true
|
|
return .none
|
|
case .alert:
|
|
return .none
|
|
case .presentAlert:
|
|
state.alert = AlertState {
|
|
TextState("Uh oh!")
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteButtonTapped) {
|
|
TextState("Delete")
|
|
}
|
|
}
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$alert, action: /Action.alert) {}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentAlert) {
|
|
$0.alert = AlertState {
|
|
TextState("Uh oh!")
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteButtonTapped) {
|
|
TextState("Delete")
|
|
}
|
|
}
|
|
}
|
|
await store.send(.alert(.presented(.deleteButtonTapped))) {
|
|
$0.alert = nil
|
|
$0.isDeleted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func testPresentation_hydratedDestination_childDismissal() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case closeButtonTapped
|
|
case decrementButtonTapped
|
|
case incrementButtonTapped
|
|
}
|
|
@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 .decrementButtonTapped:
|
|
state.count -= 1
|
|
return .none
|
|
case .incrementButtonTapped:
|
|
state.count += 1
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State(child: Child.State())) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.child(.presented(.closeButtonTapped)))
|
|
await store.receive(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
func testPresentation_rehydratedDestination_childDismissal() async {
|
|
struct ChildFeature: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable { case cancel }
|
|
@Dependency(\.dismiss) var dismiss
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { _, action in
|
|
.run { _ in await dismiss() }
|
|
}
|
|
}
|
|
}
|
|
struct ChildContainerFeature: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: ChildFeature.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case openChild
|
|
case child(PresentationAction<ChildFeature.Action>)
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
ChildFeature()
|
|
}
|
|
}
|
|
}
|
|
struct ParentFeature: Reducer {
|
|
struct State: Equatable {
|
|
var childContainer = ChildContainerFeature.State()
|
|
}
|
|
enum Action: Equatable {
|
|
case childContainer(ChildContainerFeature.Action)
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Scope(state: \.childContainer, action: /Action.childContainer) {
|
|
ChildContainerFeature()
|
|
}
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .childContainer(.openChild):
|
|
state.childContainer.child = ChildFeature.State()
|
|
return .none
|
|
default:
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let store = await TestStore(initialState: ParentFeature.State()) { ParentFeature() }
|
|
|
|
await store.send(.childContainer(.openChild)) { state in
|
|
state.childContainer.child = ChildFeature.State()
|
|
}
|
|
await store.send(.childContainer(.child(.presented(.cancel))))
|
|
await store.receive(.childContainer(.child(.dismiss))) { state in
|
|
state.childContainer.child = nil
|
|
}
|
|
|
|
await store.send(.childContainer(.openChild)) { state in
|
|
state.childContainer.child = ChildFeature.State()
|
|
}
|
|
await store.send(.childContainer(.child(.presented(.cancel))))
|
|
await store.receive(.childContainer(.child(.dismiss))) { state in
|
|
state.childContainer.child = nil
|
|
}
|
|
}
|
|
|
|
func testEnumPresentation() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable, Identifiable {
|
|
let id: UUID
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case closeButtonTapped
|
|
case startButtonTapped
|
|
case tick
|
|
}
|
|
@Dependency(\.continuousClock) var clock
|
|
@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 .startButtonTapped:
|
|
return .run { send in
|
|
for try await _ in clock.timer(interval: .seconds(1)) {
|
|
await send(.tick)
|
|
}
|
|
}
|
|
case .tick:
|
|
state.count += 1
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var destination: Destination.State?
|
|
var isDeleted = false
|
|
}
|
|
enum Action: Equatable {
|
|
case destination(PresentationAction<Destination.Action>)
|
|
case presentAlert
|
|
case presentChild(id: UUID? = nil)
|
|
}
|
|
@Dependency(\.uuid) var uuid
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .destination(.presented(.alert(.deleteButtonTapped))):
|
|
state.isDeleted = true
|
|
return .none
|
|
case .destination:
|
|
return .none
|
|
case .presentAlert:
|
|
state.destination = .alert(
|
|
AlertState {
|
|
TextState("Uh oh!")
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteButtonTapped) {
|
|
TextState("Delete")
|
|
}
|
|
}
|
|
)
|
|
return .none
|
|
case let .presentChild(id):
|
|
state.destination = .child(Child.State(id: id ?? self.uuid()))
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$destination, action: /Action.destination) {
|
|
Destination()
|
|
}
|
|
}
|
|
struct Destination: Reducer {
|
|
enum State: Equatable {
|
|
case alert(AlertState<Action.Alert>)
|
|
case child(Child.State)
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(Alert)
|
|
case child(Child.Action)
|
|
|
|
enum Alert: Equatable {
|
|
case deleteButtonTapped
|
|
}
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Scope(state: /State.alert, action: /Action.alert) {}
|
|
Scope(state: /State.child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let clock = TestClock()
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.continuousClock = clock
|
|
$0.uuid = .incrementing
|
|
}
|
|
|
|
await store.send(.presentChild()) {
|
|
$0.destination = .child(
|
|
Child.State(id: UUID(0))
|
|
)
|
|
}
|
|
await store.send(.destination(.presented(.child(.startButtonTapped))))
|
|
await clock.advance(by: .seconds(2))
|
|
await store.receive(.destination(.presented(.child(.tick)))) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.receive(.destination(.presented(.child(.tick)))) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 2
|
|
}
|
|
}
|
|
await store.send(.destination(.presented(.child(.closeButtonTapped))))
|
|
await store.receive(.destination(.dismiss)) {
|
|
$0.destination = nil
|
|
}
|
|
await store.send(.presentChild()) {
|
|
$0.destination = .child(
|
|
Child.State(id: UUID(1))
|
|
)
|
|
}
|
|
await clock.advance(by: .seconds(2))
|
|
await store.send(.destination(.presented(.child(.startButtonTapped))))
|
|
await clock.advance(by: .seconds(2))
|
|
await store.receive(.destination(.presented(.child(.tick)))) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.receive(.destination(.presented(.child(.tick)))) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 2
|
|
}
|
|
}
|
|
await store.send(
|
|
.presentChild(id: UUID(1))
|
|
) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 0
|
|
}
|
|
}
|
|
await clock.advance(by: .seconds(2))
|
|
await store.receive(.destination(.presented(.child(.tick)))) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.receive(.destination(.presented(.child(.tick)))) {
|
|
try (/Parent.Destination.State.child).modify(&$0.destination) {
|
|
$0.count = 2
|
|
}
|
|
}
|
|
await store.send(.presentAlert) {
|
|
$0.destination = .alert(
|
|
AlertState {
|
|
TextState("Uh oh!")
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteButtonTapped) {
|
|
TextState("Delete")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
await store.send(.destination(.presented(.alert(.deleteButtonTapped)))) {
|
|
$0.destination = nil
|
|
$0.isDeleted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func testNavigation_cancelID_childCancellation() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {
|
|
case startButtonTapped
|
|
case stopButtonTapped
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .startButtonTapped:
|
|
return .run { _ in
|
|
try await Task.never()
|
|
}
|
|
.cancellable(id: 42)
|
|
|
|
case .stopButtonTapped:
|
|
return .cancel(id: 42)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
let presentationTask = await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.startButtonTapped)))
|
|
await store.send(.child(.presented(.stopButtonTapped)))
|
|
await presentationTask.cancel()
|
|
}
|
|
|
|
func testNavigation_cancelID_parentCancellation() async throws {
|
|
struct Grandchild: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {
|
|
case startButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .startButtonTapped:
|
|
return .run { _ in
|
|
try await Task.never()
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var grandchild: Grandchild.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case grandchild(PresentationAction<Grandchild.Action>)
|
|
case presentGrandchild
|
|
case startButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .grandchild:
|
|
return .none
|
|
case .presentGrandchild:
|
|
state.grandchild = Grandchild.State()
|
|
return .none
|
|
case .startButtonTapped:
|
|
return .run { _ in
|
|
try await Task.never()
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
}
|
|
}
|
|
.ifLet(\.$grandchild, action: /Action.grandchild) {
|
|
Grandchild()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case stopButtonTapped
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .stopButtonTapped:
|
|
return .merge(
|
|
.cancel(id: Child.CancelID.effect),
|
|
.cancel(id: Grandchild.CancelID.effect)
|
|
)
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.presentGrandchild))) {
|
|
$0.child?.grandchild = Grandchild.State()
|
|
}
|
|
await store.send(.child(.presented(.startButtonTapped)))
|
|
await store.send(.child(.presented(.grandchild(.presented(.startButtonTapped)))))
|
|
await store.send(.stopButtonTapped)
|
|
}
|
|
|
|
func testNavigation_cancelID_parentCancelTwoChildren() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case response(Int)
|
|
case startButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case let .response(value):
|
|
state.count = value
|
|
return .none
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
for await _ in self.clock.timer(interval: .seconds(1)) {
|
|
await send(.response(42))
|
|
}
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child1: Child.State?
|
|
@PresentationState var child2: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child1(PresentationAction<Child.Action>)
|
|
case child2(PresentationAction<Child.Action>)
|
|
case stopButtonTapped
|
|
case presentChildren
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child1, .child2:
|
|
return .none
|
|
case .stopButtonTapped:
|
|
return .cancel(id: Child.CancelID.effect)
|
|
case .presentChildren:
|
|
state.child1 = Child.State()
|
|
state.child2 = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child1, action: /Action.child1) {
|
|
Child()
|
|
}
|
|
.ifLet(\.$child2, action: /Action.child2) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let clock = TestClock()
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.continuousClock = clock
|
|
}
|
|
await store.send(.presentChildren) {
|
|
$0.child1 = Child.State()
|
|
$0.child2 = Child.State()
|
|
}
|
|
await store.send(.child1(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child1(.presented(.response(42)))) {
|
|
$0.child1?.count = 42
|
|
}
|
|
await store.send(.child2(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child1(.presented(.response(42))))
|
|
await store.receive(.child2(.presented(.response(42)))) {
|
|
$0.child2?.count = 42
|
|
}
|
|
await store.send(.stopButtonTapped)
|
|
await clock.run()
|
|
await store.send(.child1(.dismiss)) {
|
|
$0.child1 = nil
|
|
}
|
|
await store.send(.child2(.dismiss)) {
|
|
$0.child2 = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testNavigation_cancelID_childCannotCancelSibling() async throws {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case response(Int)
|
|
case startButtonTapped
|
|
case stopButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case let .response(value):
|
|
state.count = value
|
|
return .none
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
for await _ in self.clock.timer(interval: .seconds(1)) {
|
|
await send(.response(42))
|
|
}
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
case .stopButtonTapped:
|
|
return .cancel(id: CancelID.effect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child1: Child.State?
|
|
@PresentationState var child2: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child1(PresentationAction<Child.Action>)
|
|
case child2(PresentationAction<Child.Action>)
|
|
case presentChildren
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child1, .child2:
|
|
return .none
|
|
case .presentChildren:
|
|
state.child1 = Child.State()
|
|
state.child2 = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child1, action: /Action.child1) {
|
|
Child()
|
|
}
|
|
.ifLet(\.$child2, action: /Action.child2) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let clock = TestClock()
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.continuousClock = clock
|
|
}
|
|
await store.send(.presentChildren) {
|
|
$0.child1 = Child.State()
|
|
$0.child2 = Child.State()
|
|
}
|
|
await store.send(.child1(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child1(.presented(.response(42)))) {
|
|
$0.child1?.count = 42
|
|
}
|
|
await store.send(.child2(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child1(.presented(.response(42))))
|
|
await store.receive(.child2(.presented(.response(42)))) {
|
|
$0.child2?.count = 42
|
|
}
|
|
|
|
await store.send(.child1(.presented(.stopButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child2(.presented(.response(42))))
|
|
|
|
await store.send(.child2(.presented(.stopButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
|
|
await clock.run()
|
|
await store.send(.child1(.dismiss)) {
|
|
$0.child1 = nil
|
|
}
|
|
await store.send(.child2(.dismiss)) {
|
|
$0.child2 = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testNavigation_cancelID_childCannotCancelIdentifiableSibling() async throws {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable, Identifiable {
|
|
let id: UUID
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case response(Int)
|
|
case startButtonTapped
|
|
case stopButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case let .response(value):
|
|
state.count = value
|
|
return .none
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
for await _ in self.clock.timer(interval: .seconds(1)) {
|
|
await send(.response(42))
|
|
}
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
case .stopButtonTapped:
|
|
return .cancel(id: CancelID.effect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child1: Child.State?
|
|
@PresentationState var child2: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child1(PresentationAction<Child.Action>)
|
|
case child2(PresentationAction<Child.Action>)
|
|
case presentChildren
|
|
}
|
|
@Dependency(\.uuid) var uuid
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child1, .child2:
|
|
return .none
|
|
case .presentChildren:
|
|
state.child1 = Child.State(id: self.uuid())
|
|
state.child2 = Child.State(id: self.uuid())
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child1, action: /Action.child1) {
|
|
Child()
|
|
}
|
|
.ifLet(\.$child2, action: /Action.child2) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let clock = TestClock()
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.continuousClock = clock
|
|
$0.uuid = .incrementing
|
|
}
|
|
await store.send(.presentChildren) {
|
|
$0.child1 = Child.State(id: UUID(0))
|
|
$0.child2 = Child.State(id: UUID(1))
|
|
}
|
|
await store.send(.child1(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child1(.presented(.response(42)))) {
|
|
$0.child1?.count = 42
|
|
}
|
|
await store.send(.child2(.presented(.startButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child1(.presented(.response(42))))
|
|
await store.receive(.child2(.presented(.response(42)))) {
|
|
$0.child2?.count = 42
|
|
}
|
|
|
|
await store.send(.child1(.presented(.stopButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.child2(.presented(.response(42))))
|
|
|
|
await store.send(.child2(.presented(.stopButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
|
|
await clock.run()
|
|
await store.send(.child1(.dismiss)) {
|
|
$0.child1 = nil
|
|
}
|
|
await store.send(.child2(.dismiss)) {
|
|
$0.child2 = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testNavigation_cancelID_childCannotCancelParent() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {
|
|
case stopButtonTapped
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .stopButtonTapped:
|
|
return .cancel(id: Parent.CancelID.effect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
case response(Int)
|
|
case startButtonTapped
|
|
case stopButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
case let .response(value):
|
|
state.count = value
|
|
return .none
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
try await self.clock.sleep(for: .seconds(1))
|
|
await send(.response(42))
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
case .stopButtonTapped:
|
|
return .cancel(id: CancelID.effect)
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let clock = TestClock()
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.continuousClock = clock
|
|
}
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.startButtonTapped)
|
|
await store.send(.child(.presented(.stopButtonTapped)))
|
|
await clock.advance(by: .seconds(1))
|
|
await store.receive(.response(42)) {
|
|
$0.count = 42
|
|
}
|
|
await store.send(.stopButtonTapped)
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testNavigation_cancelID_parentDismissGrandchild() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
struct Grandchild: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {
|
|
case response(Int)
|
|
case startButtonTapped
|
|
}
|
|
enum CancelID { case effect }
|
|
@Dependency(\.continuousClock) var clock
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .response:
|
|
return .none
|
|
case .startButtonTapped:
|
|
return .run { send in
|
|
try await clock.sleep(for: .seconds(0))
|
|
await send(.response(42))
|
|
}
|
|
.cancellable(id: CancelID.effect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var grandchild: Grandchild.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case grandchild(PresentationAction<Grandchild.Action>)
|
|
case presentGrandchild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .grandchild:
|
|
return .none
|
|
case .presentGrandchild:
|
|
state.grandchild = Grandchild.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$grandchild, action: /Action.grandchild) {
|
|
Grandchild()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case dismissGrandchild
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .dismissGrandchild:
|
|
return .send(.child(.presented(.grandchild(.dismiss))))
|
|
case .presentChild:
|
|
state.child = Child.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(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.presentGrandchild))) {
|
|
$0.child?.grandchild = Grandchild.State()
|
|
}
|
|
|
|
await store.send(.child(.presented(.grandchild(.presented(.startButtonTapped)))))
|
|
await clock.advance()
|
|
await store.receive(.child(.presented(.grandchild(.presented(.response(42))))))
|
|
|
|
await store.send(.child(.presented(.grandchild(.presented(.startButtonTapped)))))
|
|
await store.send(.dismissGrandchild)
|
|
await store.receive(.child(.presented(.grandchild(.dismiss)))) {
|
|
$0.child?.grandchild = nil
|
|
}
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRuntimeWarn_NilChild_SendDismissAction() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
.none
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
XCTExpectFailure {
|
|
$0.compactDescription == """
|
|
failed - An "ifLet" at \
|
|
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 13)" received a \
|
|
presentation action when destination state was absent. …
|
|
|
|
Action:
|
|
PresentationReducerTests.Parent.Action.child(.dismiss)
|
|
|
|
This is generally considered an application logic error, and can happen for a few reasons:
|
|
|
|
• A parent reducer set destination state to "nil" before this reducer ran. This reducer \
|
|
must run before any other reducer sets destination state to "nil". This ensures that \
|
|
destination reducers can handle their actions while their state is still present.
|
|
|
|
• This action was sent to the store while destination state was "nil". Make sure that \
|
|
actions for this reducer can only be sent from a store when state is present, or \
|
|
from effects that start from this reducer.
|
|
"""
|
|
}
|
|
|
|
await store.send(.child(.dismiss))
|
|
}
|
|
|
|
func testRuntimeWarn_NilChild_SendChildAction() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {
|
|
case tap
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
.none
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
XCTExpectFailure {
|
|
$0.compactDescription == """
|
|
failed - An "ifLet" at \
|
|
"ComposableArchitectureTests/PresentationReducerTests.swift:\(#line - 13)" received a \
|
|
presentation action when destination state was absent. …
|
|
|
|
Action:
|
|
PresentationReducerTests.Parent.Action.child(.presented(.tap))
|
|
|
|
This is generally considered an application logic error, and can happen for a few reasons:
|
|
|
|
• A parent reducer set destination state to "nil" before this reducer ran. This reducer \
|
|
must run before any other reducer sets destination state to "nil". This ensures that \
|
|
destination reducers can handle their actions while their state is still present.
|
|
|
|
• This action was sent to the store while destination state was "nil". Make sure that \
|
|
actions for this reducer can only be sent from a store when state is present, or \
|
|
from effects that start from this reducer.
|
|
"""
|
|
}
|
|
|
|
await store.send(.child(.presented(.tap)))
|
|
}
|
|
|
|
func testRehydrateSameChild_SendDismissAction() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child(.dismiss):
|
|
state.child = Child.State()
|
|
return .none
|
|
default:
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State(child: Child.State())) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
func testRehydrateDifferentChild_SendDismissAction() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable, Identifiable {
|
|
let id: UUID
|
|
}
|
|
enum Action: Equatable {}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
}
|
|
@Dependency(\.uuid) var uuid
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child(.dismiss):
|
|
if state.child?.id == UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF") {
|
|
state.child = Child.State(id: self.uuid())
|
|
}
|
|
return .none
|
|
default:
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(
|
|
initialState: Parent.State(
|
|
child: Child.State(id: UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
|
|
)
|
|
) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.uuid = .incrementing
|
|
}
|
|
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = Child.State(id: UUID(0))
|
|
}
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
func testPresentation_parentNilsOutChildWithLongLivingEffect() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case dismiss
|
|
case dismissMe
|
|
case task
|
|
}
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .dismiss:
|
|
return .send(.dismissMe)
|
|
case .dismissMe:
|
|
return .none
|
|
case .task:
|
|
return .run { _ in
|
|
try await Task.never()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child(.presented(.dismissMe)):
|
|
state.child = nil
|
|
return .none
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.task)))
|
|
await store.send(.child(.presented(.dismiss)))
|
|
await store.receive(.child(.presented(.dismissMe))) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
func testPresentation_DestinationEnum_IdentityChange() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable, Identifiable {
|
|
var id = DependencyValues._current.uuid()
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case resetIdentity
|
|
case response
|
|
case tap
|
|
}
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
@Dependency(\.uuid) var uuid
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .resetIdentity:
|
|
state.count = 0
|
|
state.id = self.uuid()
|
|
return .none
|
|
case .response:
|
|
state.count = 999
|
|
return .none
|
|
case .tap:
|
|
state.count += 1
|
|
return .run { send in
|
|
try await self.mainQueue.sleep(for: .seconds(1))
|
|
await send(.response)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var destination: Destination.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case destination(PresentationAction<Destination.Action>)
|
|
case presentChild1
|
|
}
|
|
struct Destination: 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() }
|
|
}
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .destination:
|
|
return .none
|
|
case .presentChild1:
|
|
state.destination = .child1(Child.State())
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$destination, action: /Action.destination) {
|
|
Destination()
|
|
}
|
|
}
|
|
}
|
|
|
|
let mainQueue = DispatchQueue.test
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.uuid = .incrementing
|
|
$0.mainQueue = mainQueue.eraseToAnyScheduler()
|
|
}
|
|
|
|
await store.send(.presentChild1) {
|
|
$0.destination = .child1(
|
|
Child.State(id: UUID(0))
|
|
)
|
|
}
|
|
await store.send(.destination(.presented(.child1(.tap)))) {
|
|
try (/Parent.Destination.State.child1).modify(&$0.destination) {
|
|
$0.count = 1
|
|
}
|
|
}
|
|
await store.send(.destination(.presented(.child1(.resetIdentity)))) {
|
|
try (/Parent.Destination.State.child1).modify(&$0.destination) {
|
|
$0.id = UUID(1)
|
|
$0.count = 0
|
|
}
|
|
}
|
|
await mainQueue.run()
|
|
await store.send(.destination(.dismiss)) {
|
|
$0.destination = nil
|
|
}
|
|
}
|
|
|
|
func testAlertThenDialog() async {
|
|
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
|
|
struct Feature: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var destination: Destination.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case destination(PresentationAction<Destination.Action>)
|
|
case showAlert
|
|
case showDialog
|
|
}
|
|
|
|
struct Destination: Reducer {
|
|
enum State: Equatable {
|
|
case alert(AlertState<AlertDialogAction>)
|
|
case dialog(ConfirmationDialogState<AlertDialogAction>)
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(AlertDialogAction)
|
|
case dialog(AlertDialogAction)
|
|
}
|
|
enum AlertDialogAction {
|
|
case showAlert
|
|
case showDialog
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .destination(.presented(.alert(.showDialog))):
|
|
state.destination = .dialog(
|
|
ConfirmationDialogState {
|
|
TextState("Hello!")
|
|
} actions: {
|
|
})
|
|
return .none
|
|
case .destination(.presented(.dialog(.showAlert))):
|
|
state.destination = .alert(AlertState { TextState("Hello!") })
|
|
return .none
|
|
case .destination:
|
|
return .none
|
|
case .showAlert:
|
|
state.destination = .alert(Self.alert)
|
|
return .none
|
|
case .showDialog:
|
|
state.destination = .dialog(Self.dialog)
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$destination, action: /Action.destination) {
|
|
Destination()
|
|
}
|
|
}
|
|
|
|
static let alert = AlertState<Destination.AlertDialogAction> {
|
|
TextState("Choose")
|
|
} actions: {
|
|
ButtonState(action: .showAlert) { TextState("Show alert") }
|
|
ButtonState(action: .showDialog) { TextState("Show dialog") }
|
|
}
|
|
static let dialog = ConfirmationDialogState<Destination.AlertDialogAction> {
|
|
TextState("Choose")
|
|
} actions: {
|
|
ButtonState(action: .showAlert) { TextState("Show alert") }
|
|
ButtonState(action: .showDialog) { TextState("Show dialog") }
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Feature.State()) {
|
|
Feature()
|
|
}
|
|
|
|
await store.send(.showAlert) {
|
|
$0.destination = .alert(Feature.alert)
|
|
}
|
|
await store.send(.destination(.presented(.alert(.showDialog)))) {
|
|
$0.destination = .dialog(
|
|
ConfirmationDialogState {
|
|
TextState("Hello!")
|
|
} actions: {
|
|
})
|
|
}
|
|
await store.send(.destination(.dismiss)) {
|
|
$0.destination = nil
|
|
}
|
|
|
|
await store.send(.showDialog) {
|
|
$0.destination = .dialog(Feature.dialog)
|
|
}
|
|
await store.send(.destination(.presented(.dialog(.showAlert)))) {
|
|
$0.destination = .alert(AlertState { TextState("Hello!") })
|
|
}
|
|
await store.send(.destination(.dismiss)) {
|
|
$0.destination = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func testPresentation_leaveChildPresented() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {}
|
|
enum Action: Equatable {}
|
|
var body: some Reducer<State, Action> {
|
|
EmptyReducer()
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
}
|
|
|
|
func testPresentation_leaveChildPresented_WithLongLivingEffect() 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 {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
let line = #line
|
|
await store.send(.child(.presented(.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 testCancelInFlightEffects() async {
|
|
struct Child: Reducer {
|
|
struct State: Equatable {
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case response(Int)
|
|
case tap
|
|
}
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
struct CancelID: Hashable {}
|
|
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 mainQueue.sleep(for: .seconds(1))
|
|
await send(.response(42))
|
|
}
|
|
.cancellable(id: CancelID(), cancelInFlight: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Parent: Reducer {
|
|
struct State: Equatable {
|
|
@PresentationState var child: Child.State?
|
|
var count = 0
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case presentChild
|
|
case response(Int)
|
|
}
|
|
@Dependency(\.mainQueue) var mainQueue
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .presentChild:
|
|
state.child = Child.State()
|
|
return .run { send in
|
|
try await self.mainQueue.sleep(for: .seconds(2))
|
|
await send(.response(42))
|
|
}
|
|
.cancellable(id: Child.CancelID())
|
|
case let .response(value):
|
|
state.count = value
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
|
|
let mainQueue = DispatchQueue.test
|
|
let store = await TestStore(initialState: .init()) {
|
|
Parent()
|
|
} withDependencies: {
|
|
$0.mainQueue = mainQueue.eraseToAnyScheduler()
|
|
}
|
|
|
|
await store.send(.presentChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.tap)))
|
|
await mainQueue.advance(by: .milliseconds(500))
|
|
await store.send(.child(.presented(.tap)))
|
|
await mainQueue.advance(by: .milliseconds(1_000))
|
|
await store.receive(.child(.presented(.response(42)))) {
|
|
$0.child?.count = 42
|
|
}
|
|
await mainQueue.advance(by: .milliseconds(500))
|
|
await store.receive(.response(42)) {
|
|
$0.count = 42
|
|
}
|
|
await store.send(.child(.dismiss)) {
|
|
$0.child = nil
|
|
}
|
|
}
|
|
|
|
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 {
|
|
@PresentationState var child: Child.State?
|
|
}
|
|
enum Action: Equatable {
|
|
case child(PresentationAction<Child.Action>)
|
|
case tapAfter
|
|
case tapBefore
|
|
case tapChild
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .tapAfter:
|
|
return .none
|
|
case .tapBefore:
|
|
state.child = nil
|
|
return .none
|
|
case .tapChild:
|
|
return .none
|
|
}
|
|
}
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .tapAfter:
|
|
return .none
|
|
case .tapBefore:
|
|
return .none
|
|
case .tapChild:
|
|
state.child = Child.State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .tapAfter:
|
|
state.child = nil
|
|
return .none
|
|
case .tapBefore:
|
|
return .none
|
|
case .tapChild:
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.tapChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.onAppear)))
|
|
await store.send(.tapBefore) {
|
|
$0.child = nil
|
|
}
|
|
|
|
await store.send(.tapChild) {
|
|
$0.child = Child.State()
|
|
}
|
|
await store.send(.child(.presented(.onAppear)))
|
|
await store.send(.tapAfter) {
|
|
$0.child = nil
|
|
}
|
|
// NB: Another action needs to come into the `ifLet` to cancel the child action
|
|
await store.send(.tapAfter)
|
|
}
|
|
|
|
func testPresentation_leaveAlertPresentedForNonAlertActions() async {
|
|
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
|
|
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 {
|
|
@PresentationState var destination: Destination.State?
|
|
var isDeleted = false
|
|
}
|
|
enum Action: Equatable {
|
|
case destination(PresentationAction<Destination.Action>)
|
|
case presentAlert
|
|
case presentChild
|
|
}
|
|
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .destination(.presented(.alert(.deleteButtonTapped))):
|
|
state.isDeleted = true
|
|
return .none
|
|
case .destination:
|
|
return .none
|
|
case .presentAlert:
|
|
state.destination = .alert(
|
|
AlertState {
|
|
TextState("Uh oh!")
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteButtonTapped) {
|
|
TextState("Delete")
|
|
}
|
|
}
|
|
)
|
|
return .none
|
|
case .presentChild:
|
|
state.destination = .child(Child.State())
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$destination, action: /Action.destination) {
|
|
Destination()
|
|
}
|
|
}
|
|
struct Destination: Reducer {
|
|
enum State: Equatable {
|
|
case alert(AlertState<Action.Alert>)
|
|
case child(Child.State)
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(Alert)
|
|
case child(Child.Action)
|
|
|
|
enum Alert: Equatable {
|
|
case deleteButtonTapped
|
|
}
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Scope(state: /State.alert, action: /Action.alert) {}
|
|
Scope(state: /State.child, action: /Action.child) {
|
|
Child()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let line = #line - 6
|
|
|
|
let store = await TestStore(initialState: Parent.State()) {
|
|
Parent()
|
|
}
|
|
|
|
await store.send(.presentAlert) {
|
|
$0.destination = .alert(
|
|
AlertState {
|
|
TextState("Uh oh!")
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteButtonTapped) {
|
|
TextState("Delete")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
XCTExpectFailure {
|
|
$0.compactDescription.hasPrefix(
|
|
"""
|
|
failed - A "Scope" at "\(#fileID):\(line)" received a child action when child state was \
|
|
set to a different case. …
|
|
"""
|
|
)
|
|
}
|
|
await store.send(.destination(.presented(.child(.decrementButtonTapped))))
|
|
}
|
|
}
|
|
|
|
func testFastPathEquality() {
|
|
struct State: Equatable {
|
|
static func == (lhs: Self, rhs: Self) -> Bool {
|
|
Thread.sleep(forTimeInterval: 5)
|
|
return true
|
|
}
|
|
}
|
|
|
|
@PresentationState var state = State()
|
|
let start = Date()
|
|
XCTAssertEqual($state, $state)
|
|
XCTAssertLessThan(Date().timeIntervalSince(start), 0.1)
|
|
}
|
|
|
|
func testNestedDismiss() async {
|
|
let store = await TestStore(initialState: NestedDismissFeature.State()) {
|
|
NestedDismissFeature()
|
|
}
|
|
|
|
await store.send(\.presentButtonTapped) {
|
|
$0.child = NestedDismissFeature.State()
|
|
}
|
|
await store.send(\.child.presentButtonTapped) {
|
|
$0.child?.child = NestedDismissFeature.State()
|
|
}
|
|
await store.send(\.child.child.dismissButtonTapped)
|
|
await store.receive(\.child.child.dismiss) {
|
|
$0.child?.child = nil
|
|
}
|
|
}
|
|
|
|
#if !os(visionOS)
|
|
@Reducer
|
|
struct TestEphemeralBindingDismissalFeature {
|
|
@ObservableState
|
|
struct State: Equatable {
|
|
@Presents var alert: AlertState<Never>?
|
|
}
|
|
enum Action: Equatable {
|
|
case alert(PresentationAction<Never>)
|
|
}
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
return .none
|
|
}
|
|
.ifLet(\.$alert, action: /Action.alert)
|
|
}
|
|
}
|
|
// @MainActor
|
|
// func testEphemeralBindingDismissal() async {
|
|
// @Perception.Bindable var store = Store(
|
|
// initialState: TestEphemeralBindingDismissalFeature.State(
|
|
// alert: AlertState { TextState("Oops!") }
|
|
// )
|
|
// ) {
|
|
// TestEphemeralBindingDismissalFeature()
|
|
// }
|
|
//
|
|
// XCTAssertNotNil(store.alert)
|
|
// $store.scope(state: \.alert, action: \.alert).wrappedValue = nil
|
|
// XCTAssertNil(store.alert)
|
|
// }
|
|
#endif
|
|
}
|
|
|
|
@Reducer
|
|
private struct NestedDismissFeature {
|
|
struct State: Equatable {
|
|
@PresentationState var child: NestedDismissFeature.State?
|
|
}
|
|
enum Action {
|
|
case child(PresentationAction<NestedDismissFeature.Action>)
|
|
case dismissButtonTapped
|
|
case presentButtonTapped
|
|
}
|
|
@Dependency(\.dismiss) var dismiss
|
|
var body: some ReducerOf<Self> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .child:
|
|
return .none
|
|
case .dismissButtonTapped:
|
|
return .run { _ in await dismiss() }
|
|
case .presentButtonTapped:
|
|
state.child = State()
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$child, action: \.child) {
|
|
Self()
|
|
}
|
|
}
|
|
}
|