Files
swift-composable-architectu…/Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift
Stephen Celis 57e804f1cc Macro bonanza (#2553)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Silence test warnings

* wip

* wip

* wip

* update a bunch of docs

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Kill integration tests for now

* wip

* wip

* wip

* wip

* updating docs for @Reducer macro

* replaced more Reducer protocols with @Reducer

* Fixed some broken docc references

* wip

* Some @Reducer docs

* more docs

* convert some old styles to new style

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* bump

* update tutorials to use body

* update tutorials to use DML on destination state enum

* Add diagnostic

* wip

* updated a few more tests

* wip

* wip

* Add another gotcha

* wip

* wip

* wip

* fixes

* wip

* wip

* wip

* wip

* wip

* fix

* wip

* remove for now

* wip

* wip

* updated some docs

* migration guides

* more migration guide

* fix ci

* fix

* soft deprecate all apis using AnyCasePath

* wip

* Fix

* fix tests

* swift-format 509 compatibility

* wip

* wip

* Update Sources/ComposableArchitecture/Macros.swift

Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com>

* wip

* wip

* update optional state case study

* remove initializer

* Don't use @State for BasicsView integration demo

* fix tests

* remove reduce diagnostics for now

* diagnose error not warning

* Update Sources/ComposableArchitecture/Macros.swift

Co-authored-by: Jesse Tipton <jesse@jessetipton.com>

* wip

* move integration tests to cron

* Revert "move integration tests to cron"

This reverts commit f9bdf2f04b.

* disable flakey tests on CI

* wip

* wip

* Revert "Revert "move integration tests to cron""

This reverts commit 66aafa7327.

* fix

* wip

* fix

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>
Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com>
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
Co-authored-by: Jesse Tipton <jesse@jessetipton.com>
2023-11-13 12:57:35 -08:00

997 lines
29 KiB
Swift
Raw 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.
#if DEBUG
import ComposableArchitecture
import XCTest
@MainActor
final class TestStoreNonExhaustiveTests: BaseTCATestCase {
func testSkipReceivedActions_NonStrict() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { state, action in
if action {
state += 1
return .send(false)
} else {
state += 1
return .none
}
}
}
await store.send(true) { $0 = 1 }
XCTAssertEqual(store.state, 1)
await store.skipReceivedActions(strict: false)
XCTAssertEqual(store.state, 2)
}
func testSkipReceivedActions_Strict() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { state, action in
if action {
state += 1
return .send(false)
} else {
state += 1
return .none
}
}
}
await store.send(true) { $0 = 1 }
XCTAssertEqual(store.state, 1)
await store.receive(false) { $0 = 2 }
XCTAssertEqual(store.state, 2)
XCTExpectFailure {
$0.compactDescription == "There were no received actions to skip."
}
await store.skipReceivedActions(strict: true)
}
func testSkipReceivedActions_NonExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { state, action in
if action {
state += 1
return .send(false)
} else {
state += 1
return .none
}
}
}
store.exhaustivity = .off
await store.send(true) { $0 = 1 }
XCTAssertEqual(store.state, 1)
await store.skipReceivedActions(strict: false)
XCTAssertEqual(store.state, 2)
}
func testSkipReceivedActions_PartialExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { state, action in
if action {
state += 1
return .send(false)
} else {
state += 1
return .none
}
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(true) { $0 = 1 }
XCTAssertEqual(store.state, 1)
await store.skipReceivedActions(strict: false)
XCTAssertEqual(store.state, 2)
}
func testCancelInFlightEffects_NonStrict() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
.run { _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) }
}
}
await store.send(true)
await store.skipInFlightEffects(strict: false)
}
func testCancelInFlightEffects_Strict() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
.run { _ in }
}
}
let task = await store.send(true)
await task.finish(timeout: NSEC_PER_SEC / 2)
XCTExpectFailure {
$0.compactDescription == "There were no in-flight effects to skip."
}
await store.skipInFlightEffects(strict: true)
}
func testCancelInFlightEffects_NonExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
.run { _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) }
}
}
store.exhaustivity = .off
await store.send(true)
await store.skipInFlightEffects(strict: false)
}
func testCancelInFlightEffects_PartialExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
.run { _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) }
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(true)
await store.skipInFlightEffects(strict: false)
}
// Confirms that you don't have to receive all actions before the test completes.
func testIgnoreReceiveActions_PartialExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
action ? .send(false) : .none
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(true)
}
// Confirms that you don't have to receive all actions before the test completes.
func testIgnoreReceiveActions_NonExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
action ? .send(false) : .none
}
}
store.exhaustivity = .off
await store.send(true)
}
// Confirms that all effects do not need to complete before the test completes.
func testIgnoreInFlightEffects_PartialExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
.run { _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) }
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(true)
}
// Confirms that all effects do not need to complete before the test completes.
func testIgnoreInFlightEffects_NonExhaustive() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Bool> { _, action in
.run { _ in try await Task.sleep(nanoseconds: NSEC_PER_SEC) }
}
}
store.exhaustivity = .off
await store.send(true)
}
// Confirms that you don't have to assert on all state changes in a non-exhaustive test store.
func testNonExhaustiveSend_PartialExhaustive() async {
let store = TestStore(initialState: Counter.State()) {
Counter()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.increment) {
$0.count = 1
// Ignoring state change: isEven = false
}
await store.send(.increment) {
$0.isEven = true
// Ignoring state change: count = 2
}
await store.send(.increment) {
$0.count = 3
$0.isEven = false
}
}
func testNonExhaustiveSend_PartialExhaustive_Prefix() async {
let store = TestStore(initialState: Counter.State()) {
Counter()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.increment) {
$0.count = 1
// Ignoring state change: isEven = false
}
}
// Confirms that you don't have to assert on all state changes in a non-exhaustive test store,
// *but* if you make an incorrect mutation you will still get a failure.
func testNonExhaustiveSend_PartialExhaustive_BadAssertion() async {
let store = TestStore(initialState: Counter.State()) {
Counter()
}
store.exhaustivity = .off(showSkippedAssertions: true)
#if DEBUG
XCTExpectFailure {
$0.compactDescription == """
A state change does not match expectation: …
Counter.State(
count: 0,
+ count: 1,
isEven: false
)
(Expected: , Actual: +)
"""
}
await store.send(.increment) {
$0.count = 0
}
#endif
}
// Confirms that you don't have to assert on all state changes in a non-exhaustive test store,
// *and* that informational boxes of what was not asserted on is not shown.
func testNonExhaustiveSend_NonExhaustive() async {
let store = TestStore(initialState: Counter.State()) {
Counter()
}
store.exhaustivity = .off
await store.send(.increment) {
$0.count = 1
// Ignoring state change: isEven = false
}
}
// Confirms that you can send actions without having received all effect actions in
// non-exhaustive test stores.
func testSend_SkipReceivedActions() async {
struct State: Equatable {
var count = 0
var isLoggedIn = false
}
enum Action {
case decrement
case increment
case loggedInResponse(Bool)
}
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .decrement:
state.count -= 1
return .none
case .increment:
state.count += 1
return .send(.loggedInResponse(true))
case let .loggedInResponse(response):
state.isLoggedIn = response
return .none
}
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.increment) {
$0.count = 1
}
// Ignored received action: .loggedInResponse(true)
await store.send(.decrement) {
$0.count = 0
// Ignored state change: isLoggedIn = true
}
}
// Confirms that if you receive an action in a non-exhaustive test store with a bad assertion
// you will still get a failure.
func testSend_SkipReceivedActions_BadAssertion() async {
struct State: Equatable {
var count = 0
var isLoggedIn = false
}
enum Action: Equatable {
case increment
case loggedInResponse(Bool)
}
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .increment:
state.count += 1
return .send(.loggedInResponse(true))
case let .loggedInResponse(response):
state.isLoggedIn = response
return .none
}
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.increment) {
$0.count = 1
}
XCTExpectFailure {
$0.compactDescription == """
A state change does not match expectation: …
TestStoreNonExhaustiveTests.State(
count: 2,
+ count: 1,
isLoggedIn: true
)
(Expected: , Actual: +)
"""
}
await store.receive(.loggedInResponse(true)) {
$0.count = 2
$0.isLoggedIn = true
}
}
// Confirms that with non-exhaustive test stores you can send multiple actions without asserting
// on any state changes until the very last action.
func testMultipleSendsWithAssertionOnLast() async {
let store = TestStore(initialState: Counter.State()) {
Counter()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.increment)
XCTAssertEqual(store.state, Counter.State(count: 1, isEven: false))
await store.send(.increment)
XCTAssertEqual(store.state, Counter.State(count: 2, isEven: true))
await store.send(.increment) {
$0.count = 3
}
XCTAssertEqual(store.state, Counter.State(count: 3, isEven: false))
}
// Confirms that you don't have to assert on all state changes when receiving an action from an
// effect in a non-exhaustive test store.
func testReceive_StateChange() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
XCTAssertEqual(store.state, NonExhaustiveReceive.State(count: 0, int: 0, string: ""))
await store.receive(.response1(42)) {
// Ignored state change: count = 1
$0.int = 42
}
XCTAssertEqual(store.state, NonExhaustiveReceive.State(count: 1, int: 42, string: ""))
await store.receive(.response2("Hello")) {
// Ignored state change: count = 2
$0.string = "Hello"
}
XCTAssertEqual(store.state, NonExhaustiveReceive.State(count: 2, int: 42, string: "Hello"))
}
// Confirms that you can skip receiving certain effect actions in a non-exhaustive test store.
func testReceive_SkipAction() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
XCTAssertEqual(store.state, NonExhaustiveReceive.State(count: 0, int: 0, string: ""))
// Ignored received action: .response1(42)
await store.receive(.response2("Hello")) {
$0.count = 2
$0.string = "Hello"
}
XCTAssertEqual(store.state, NonExhaustiveReceive.State(count: 2, int: 42, string: "Hello"))
}
// Confirms that you are allowed to send actions without having received all actions queued
// from effects.
func testSendWithUnreceivedAction() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
// Ignored received action: .response1(42)
// Ignored received action: .response2("Hello")
await store.send(.onAppear)
// Ignored received action: .response1(42)
// Ignored received action: .response2("Hello")
}
// Confirms that when you send an action the test store skips any unreceived actions
// automatically.
func testSendWithUnreceivedActions_SkipsActions() async {
enum Action: Equatable {
case tap
case response(Int)
}
let store = TestStore(initialState: 0) {
Reduce<Int, Action> { state, action in
switch action {
case .tap:
state += 1
return .run { [state] send in await send(.response(state + 42)) }
case let .response(number):
state = number
return .none
}
}
}
store.exhaustivity = .off
await store.send(.tap)
XCTAssertEqual(store.state, 1)
// Ignored received action: .response(43)
await store.send(.tap)
XCTAssertEqual(store.state, 44)
await store.skipReceivedActions()
XCTAssertEqual(store.state, 86)
}
func testPartialExhaustivityPrefix() async {
let testScheduler = DispatchQueue.test
enum Action {
case buttonTapped
case response(Int)
}
let store = TestStore(initialState: 0) {
Reduce<Int, Action> { state, action in
switch action {
case .buttonTapped:
state += 1
return .run { send in
await send(.response(42))
try await testScheduler.sleep(for: .seconds(1))
await send(.response(1729))
}
case let .response(number):
state = number
return .none
}
}
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.buttonTapped)
// Ignored state mutation: state = 1
// Ignored received action: .response(42)
await testScheduler.advance(by: .milliseconds(500))
await store.send(.buttonTapped) {
$0 = 43
}
await testScheduler.advance(by: .milliseconds(500))
await store.skipInFlightEffects()
await store.skipReceivedActions()
// Ignored received action: .response(42)
// Ignored received action: .response(1729)
// Ignore in-flight effect
}
func testCasePathReceive_PartialExhaustive() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
await store.receive(\.response1) {
$0.int = 42
}
await store.receive(\.response2) {
$0.string = "Hello"
}
}
func testCasePathReceive_NonExhaustive() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off
await store.send(.onAppear)
await store.receive(\.response1) {
$0.int = 42
}
await store.receive(\.response2) {
$0.string = "Hello"
}
}
func testCasePathReceive_Exhaustive() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
await store.send(.onAppear)
await store.receive(\.response1) {
$0.count = 1
$0.int = 42
}
await store.receive(\.response2) {
$0.count = 2
$0.string = "Hello"
}
}
func testCasePathReceive_Exhaustive_NonEquatable() async {
let store = TestStore(initialState: 0) {
Reduce<Int, NonEquatableAction> { state, action in
switch action {
case .tap:
return .send(.response(NonEquatable()))
case .response:
return .none
}
}
}
await store.send(.tap)
await store.receive(\.response)
}
func testPredicateReceive_Exhaustive_NonEquatable() async {
struct NonEquatable {}
enum Action {
case tap
case response(NonEquatable)
}
let store = TestStore(initialState: 0) {
Reduce<Int, Action> { state, action in
switch action {
case .tap:
return .send(.response(NonEquatable()))
case .response:
return .none
}
}
}
await store.send(.tap)
await store.receive { if case .response = $0 { return true } else { return false } }
}
func testCasePathReceive_SkipReceivedAction() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
await store.receive(\.response2) {
$0.string = "Hello"
}
}
func testCasePathReceive_WrongAction() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
XCTExpectFailure {
$0.compactDescription == """
Expected to receive an action matching case path, but didn't get one.
"""
}
await store.receive(\.onAppear)
await store.receive(\.response1)
await store.receive(\.response2)
}
func testCasePathReceive_ReceivedExtraAction() async {
let store = TestStore(initialState: NonExhaustiveReceive.State()) {
NonExhaustiveReceive()
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.onAppear)
await store.receive(\.response2)
XCTExpectFailure {
$0.compactDescription == """
Expected to receive an action matching case path, but didn't get one.
"""
}
await store.receive(\.response2)
}
func testXCTModifyExhaustive() async {
struct State: Equatable {
var child: Int? = 0
var count = 0
}
enum Action: Equatable { case tap, response }
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .tap:
state.count += 1
return .send(.response)
case .response:
state.count += 1
return .none
}
}
}
await store.send(.tap) { state in
state.count = 1
XCTExpectFailure {
XCTModify(&state.child) { _ in }
} issueMatcher: {
$0.compactDescription == """
XCTModify failed: expected "Int" value to be modified but it was unchanged.
"""
}
}
await store.receive(.response) { state in
state.count = 2
XCTExpectFailure {
XCTModify(&state.child) { _ in }
} issueMatcher: {
$0.compactDescription == """
XCTModify failed: expected "Int" value to be modified but it was unchanged.
"""
}
}
}
func testXCTModifyNonExhaustive() async {
enum Action { case tap, response }
let store = TestStore(initialState: Optional(1)) {
Reduce<Int?, Action> { state, action in
switch action {
case .tap:
return .send(.response)
case .response:
return .none
}
}
}
store.exhaustivity = .off
await store.send(.tap) {
XCTModify(&$0) { _ in }
}
await store.receive(.response) {
XCTModify(&$0) { _ in }
}
}
func testReceiveNonExhaustiveWithTimeout() async {
struct State: Equatable {}
enum Action: Equatable { case tap, response1, response2 }
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .tap:
return .run { send in
try await Task.sleep(nanoseconds: 10_000_000)
await send(.response1)
try await Task.sleep(nanoseconds: 10_000_000)
await send(.response2)
}
case .response1, .response2:
return .none
}
}
}
store.exhaustivity = .off
await store.send(.tap)
await store.receive(.response2, timeout: 1_000_000_000)
}
func testReceiveNonExhaustiveWithTimeoutMultipleNonMatching() async {
struct State: Equatable {}
enum Action: Equatable { case tap, response1, response2 }
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .tap:
return .run { send in
try await Task.sleep(nanoseconds: 10_000_000)
await send(.response1)
try await Task.sleep(nanoseconds: 10_000_000)
await send(.response1)
}
case .response1:
return .none
case .response2:
return .none
}
}
}
store.exhaustivity = .off
await store.send(.tap)
XCTExpectFailure { issue in
issue.compactDescription.contains(
"""
Expected to receive a matching action, but received none after 1.0 seconds.
""")
|| (issue.compactDescription.contains(
"Expected to receive the following action, but didn't")
&& issue.compactDescription.contains("Action.response2"))
}
await store.receive(.response2, timeout: 1_000_000_000)
}
func testReceiveNonExhaustiveWithTimeoutMultipleMatching() async {
struct State: Equatable {}
enum Action: Equatable { case tap, response1, response2 }
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .tap:
return .run { send in
try await Task.sleep(nanoseconds: 10_000_000)
await send(.response2)
try await Task.sleep(nanoseconds: 10_000_000)
await send(.response2)
}
case .response1, .response2:
return .none
}
}
}
store.exhaustivity = .off
await store.send(.tap)
await store.receive(.response2, timeout: 1_000_000_000)
await store.receive(.response2, timeout: 1_000_000_000)
}
func testNonExhaustive_ShowSkippedAssertions_ExpectedStateToChange() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Void> { _, _ in .none }
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(()) { $0 = 0 }
store.assert { $0 = 0 }
}
// This example comes from Krzysztof Zabłocki's blog post:
// https://www.merowing.info/exhaustive-testing-in-tca/
func testKrzysztofExample1() async {
let store = TestStore(initialState: KrzysztofExample.State()) {
KrzysztofExample()
}
store.exhaustivity = .off
await store.send(.changeIdentity(name: "Marek", surname: "Ignored")) {
$0.name = "Marek"
}
}
// This example comes from Krzysztof Zabłocki's blog post:
// https://www.merowing.info/exhaustive-testing-in-tca/
func testKrzysztofExample2() async {
let store = TestStore(initialState: KrzysztofExample.State()) {
KrzysztofExample()
}
store.exhaustivity = .off
await store.send(.changeIdentity(name: "Adam", surname: "Stern"))
await store.send(.changeIdentity(name: "Piotr", surname: "Galiszewski"))
await store.send(.changeIdentity(name: "Merowing", surname: "Info")) {
$0.name = "Merowing"
$0.surname = "Info"
}
}
// This example comes from Krzysztof Zabłocki's blog post:
// https://www.merowing.info/exhaustive-testing-in-tca/
func testKrzysztofExample3() async {
let mainQueue = DispatchQueue.test
let store = TestStore(initialState: KrzysztofExample.State()) {
KrzysztofExample()
} withDependencies: {
$0.mainQueue = mainQueue.eraseToAnyScheduler()
}
store.exhaustivity = .off
await store.send(.advanceAgeAndMoodAfterDelay)
await mainQueue.advance(by: 1)
await store.receive(.changeAge(34)) {
$0.age = 34
}
XCTAssertEqual(store.state.age, 34)
}
func testEffectfulAssertion_NonExhaustiveTestStore_ShowSkippedAssertions() async {
struct Model: Equatable {
let id: UUID
init() {
@Dependency(\.uuid) var uuid
self.id = uuid()
}
}
struct State: Equatable {
var values: [Model] = []
}
enum Action {
case addButtonTapped
}
XCTTODO(
"""
This test should pass once we have the concept of "copyable" dependencies.
"""
)
let store = TestStore(initialState: State()) {
Reduce<State, Action> { state, action in
switch action {
case .addButtonTapped:
state.values.append(Model())
return .none
}
}
} withDependencies: {
$0.uuid = .incrementing
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.addButtonTapped) {
$0.values.insert(Model(), at: 0)
}
await store.send(.addButtonTapped) {
$0.values.insert(Model(), at: 1)
}
}
}
@Reducer
struct Counter {
struct State: Equatable {
var count = 0
var isEven = true
}
enum Action {
case increment
case decrement
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1
state.isEven.toggle()
return .none
case .decrement:
state.count -= 1
state.isEven.toggle()
return .none
}
}
}
}
@Reducer
struct NonExhaustiveReceive {
struct State: Equatable {
var count = 0
var int = 0
var string = ""
}
@dynamicMemberLookup
enum Action: Equatable {
case onAppear
case response1(Int)
case response2(String)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
state = State()
return .merge(
.send(.response1(42)),
.send(.response2("Hello"))
)
case let .response1(int):
state.count += 1
state.int = int
return .none
case let .response2(string):
state.count += 1
state.string = string
return .none
}
}
}
}
// This example comes from Krzysztof Zabłocki's blog post:
// https://www.merowing.info/exhaustive-testing-in-tca/
@Reducer
struct KrzysztofExample {
struct State: Equatable {
var name: String = "Krzysztof"
var surname: String = "Zabłocki"
var age: Int = 33
var mood: Int = 0
}
enum Action: Equatable {
case changeIdentity(name: String, surname: String)
case changeAge(Int)
case changeMood(Int)
case advanceAgeAndMoodAfterDelay
}
@Dependency(\.mainQueue) var mainQueue
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .changeIdentity(name, surname):
state.name = name
state.surname = surname
return .none
case .advanceAgeAndMoodAfterDelay:
return .run { [state] send in
try await self.mainQueue.sleep(for: .seconds(1))
async let changeAge: () = send(.changeAge(state.age + 1))
async let changeMood: () = send(.changeMood(state.mood + 1))
_ = await (changeAge, changeMood)
}
case let .changeAge(age):
state.age = age
return .none
case let .changeMood(mood):
state.mood = mood
return .none
}
}
}
}
private struct NonEquatable {}
@CasePathable
@dynamicMemberLookup
private enum NonEquatableAction {
case tap
case response(NonEquatable)
}
#endif