diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift index 656ee79..7c12962 100644 --- a/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift +++ b/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift @@ -9,27 +9,24 @@ import XCTest final class AppCoreTests: XCTestCase { @MainActor func testIntegration() async { - let store = TestStore(TicTacToe()) { - TicTacToe.body - } withDependencies: { + let middleware = TestMiddleware() + let store = Store(TicTacToe.login()).transformDI { $0.authenticationClient.login = { @Sendable _, _ in AuthenticationResponse(token: "deadbeef", twoFactorRequired: false) } } + .middleware(middleware) - await store.send(\.login.view.binding.email, "blob@pointfree.co") { - $0.login?.email = "blob@pointfree.co" - } - await store.send(\.login.view.binding.password, "bl0bbl0b") { - $0.login?.password = "bl0bbl0b" - $0.login?.isFormValid = true - } - await store.send(\.login.view.loginButtonTapped) { - $0.login?.isLoginRequestInFlight = true - } - await store.receive(\.login.loginResponse.success) { - $0 = .newGame(NewGame.State()) - } + store.state.login.email = "daniil@voidilov.com" + XCTAssertEqual(store.state.login.email, "daniil@voidilov.com") + store.state.login.password = "bl0bbl0b" + XCTAssertEqual(store.state.login.password, "bl0bbl0b") + XCTAssertTrue(store.state.login.isFormValid) + + await store.login.loginButtonTapped() +// XCTAssertTrue(store.login.isLoginRequestInFlight) + XCTAssertEqual(store.state.selected, .newGame) + store.state.newGame.oPlayerName = "Blob Sr." await store.send(\.newGame.binding.oPlayerName, "Blob Sr.") { $0.newGame?.oPlayerName = "Blob Sr." } diff --git a/Sources/VDStore/TestMiddlware.swift b/Sources/VDStore/TestMiddlware.swift index e68c304..1e78a08 100644 --- a/Sources/VDStore/TestMiddlware.swift +++ b/Sources/VDStore/TestMiddlware.swift @@ -1,192 +1,192 @@ -//#if canImport(XCTest) -//import Foundation -//import XCTest -// -//public final class TestMiddleware: StoreMiddleware { -// -// private var calledActions: [StoreActionID] = [] -// private var calledActionsContinuations: [StoreActionID: [UUID: CheckedContinuation]] = [:] -// private var executedActions: [(StoreActionID, Error?)] = [] -// private var executedActionsContinuations: [StoreActionID: [UUID: CheckedContinuation]] = [:] -// -// public init() {} -// -// public func execute( -// _ args: Args, -// context: Store.Action.Context, -// dependencies: StoreDIValues, -// next: (Args) -> Res -// ) -> Res { -// didCallAction(context.actionID) -// let result = next(args) -// executedActions.append((context.actionID, nil)) -// return result -// } -// -// public func executeThrows( -// _ args: Args, -// context: Store.Action.Throws.Context, -// dependencies: StoreDIValues, -// next: (Args) -> Result -// ) -> Result { -// didCallAction(context.actionID) -// let result = next(args) -// switch result { -// case .success: -// didExecuteAction(context.actionID, error: nil) -// case let .failure(failure): -// didExecuteAction(context.actionID, error: failure) -// } -// return result -// } -// -// public func executeAsync( -// _ args: Args, -// context: Store.Action.Async.Context, -// dependencies: StoreDIValues, -// next: (Args) -> Task -// ) -> Task where Res: Sendable { -// didCallAction(context.actionID) -// let nextTask = next(args) -// Task { -// _ = await nextTask.value -// self.didExecuteAction(context.actionID, error: nil) -// } -// return nextTask -// } -// -// public func executeAsyncThrows( -// _ args: Args, -// context: Store.Action.AsyncThrows.Context, -// dependencies: StoreDIValues, -// next: (Args) -> Task -// ) -> Task where Res: Sendable { -// didCallAction(context.actionID) -// let nextTask = next(args) -// Task { -// do { -// _ = try await nextTask.value -// self.didExecuteAction(context.actionID, error: nil) -// } catch { -// self.didExecuteAction(context.actionID, error: error) -// } -// } -// return nextTask -// } -// -// public func didExecute( -// _ action: Store.Action, -// file: StaticString = #file, -// line: UInt = #line -// ) throws { -// if let i = executedActions.firstIndex(where: { $0.0 == action.id }) { -// defer { executedActions.remove(at: i) } -// if let error = executedActions[i].1 { -// throw error -// } -// return -// } -// XCTFail("Action \(action.id) was not executed", file: file, line: line) -// } -// -// public func waitExecution( -// of action: Store.Action, -// timeout: TimeInterval = 1, -// file: StaticString = #file, -// line: UInt = #line -// ) async throws { -// if let i = executedActions.firstIndex(where: { $0.0 == action.id }) { -// defer { executedActions.remove(at: i) } -// if let error = executedActions[i].1 { -// throw error -// } -// return -// } -// guard timeout > 0 else { -// XCTFail("Timeout waiting for action \(action.id) to be executed", file: file, line: line) -// return -// } -// try await withThrowingTaskGroup(of: Void.self) { group in -// let uuid = UUID() -// group.addTask { -// try await withCheckedThrowingContinuation { continuation in -// self.executedActionsContinuations[action.id, default: [:]][uuid] = continuation -// } -// } -// group.addTask { -// try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) -// if let continuation = self.executedActionsContinuations[action.id]?[uuid] { -// self.executedActionsContinuations[action.id]?[uuid] = nil -// XCTFail("Timeout waiting for action \(action.id) to be executed", file: file, line: line) -// continuation.resume() -// } -// } -// try await group.waitForAll() -// } -// } -// -// public func didCall( -// _ action: Store.Action, -// file: StaticString = #file, -// line: UInt = #line -// ) { -// if let i = calledActions.firstIndex(of: action.id) { -// calledActions.remove(at: i) -// return -// } -// XCTFail("Action \(action.id) was not called", file: file, line: line) -// } -// -// public func waitCall( -// of action: Store.Action, -// timeout: TimeInterval = 0.1, -// file: StaticString = #file, -// line: UInt = #line -// ) async { -// if let i = calledActions.firstIndex(of: action.id) { -// calledActions.remove(at: i) -// return -// } -// guard timeout > 0 else { -// XCTFail("Timeout waiting for action \(action.id) to be called", file: file, line: line) -// return -// } -// await withTaskGroup(of: Void.self) { group in -// let uuid = UUID() -// group.addTask { -// await withCheckedContinuation { continuation in -// self.calledActionsContinuations[action.id, default: [:]][uuid] = continuation -// } -// } -// group.addTask { -// try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) -// if let continuation = self.calledActionsContinuations[action.id]?[uuid] { -// self.calledActionsContinuations[action.id]?[uuid] = nil -// XCTFail("Timeout waiting for action \(action.id) to be called", file: file, line: line) -// continuation.resume() -// } -// } -// await group.waitForAll() -// } -// } -// -// private func didCallAction(_ actionID: StoreActionID) { -// calledActions.append(actionID) -// calledActionsContinuations[actionID]?.values.forEach { $0.resume() } -// calledActionsContinuations[actionID] = nil -// } -// -// private func didExecuteAction(_ actionID: StoreActionID, error: Error?) { -// executedActions.append((actionID, error)) -// executedActionsContinuations[actionID]?.values.forEach { -// if let error { -// $0.resume(throwing: error) -// } else { -// $0.resume() -// } -// } -// executedActionsContinuations[actionID] = nil -// } -//} -// -//#endif +#if canImport(XCTest) +import Foundation +import XCTest + +public final class TestMiddleware: StoreMiddleware { + + private var calledActions: [StoreActionID] = [] + private var calledActionsContinuations: [StoreActionID: [UUID: CheckedContinuation]] = [:] + private var executedActions: [(StoreActionID, Error?)] = [] + private var executedActionsContinuations: [StoreActionID: [UUID: CheckedContinuation]] = [:] + + public init() {} + + public func execute( + _ args: Args, + context: Store.Action.Context, + dependencies: StoreDIValues, + next: (Args) -> Res + ) -> Res { + didCallAction(context.actionID) + let result = next(args) + executedActions.append((context.actionID, nil)) + return result + } + + public func executeThrows( + _ args: Args, + context: Store.Action.Throws.Context, + dependencies: StoreDIValues, + next: (Args) -> Result + ) -> Result { + didCallAction(context.actionID) + let result = next(args) + switch result { + case .success: + didExecuteAction(context.actionID, error: nil) + case let .failure(failure): + didExecuteAction(context.actionID, error: failure) + } + return result + } + + public func executeAsync( + _ args: Args, + context: Store.Action.Async.Context, + dependencies: StoreDIValues, + next: (Args) -> Task + ) -> Task where Res: Sendable { + didCallAction(context.actionID) + let nextTask = next(args) + Task { + _ = await nextTask.value + self.didExecuteAction(context.actionID, error: nil) + } + return nextTask + } + + public func executeAsyncThrows( + _ args: Args, + context: Store.Action.AsyncThrows.Context, + dependencies: StoreDIValues, + next: (Args) -> Task + ) -> Task where Res: Sendable { + didCallAction(context.actionID) + let nextTask = next(args) + Task { + do { + _ = try await nextTask.value + self.didExecuteAction(context.actionID, error: nil) + } catch { + self.didExecuteAction(context.actionID, error: error) + } + } + return nextTask + } + + public func didExecute( + _ action: Store.Action, + file: StaticString = #file, + line: UInt = #line + ) throws { + if let i = executedActions.firstIndex(where: { $0.0 == action.id }) { + defer { executedActions.remove(at: i) } + if let error = executedActions[i].1 { + throw error + } + return + } + XCTFail("Action \(action.id) was not executed", file: file, line: line) + } + + public func waitExecution( + of action: Store.Action, + timeout: TimeInterval = 1, + file: StaticString = #file, + line: UInt = #line + ) async throws { + if let i = executedActions.firstIndex(where: { $0.0 == action.id }) { + defer { executedActions.remove(at: i) } + if let error = executedActions[i].1 { + throw error + } + return + } + guard timeout > 0 else { + XCTFail("Timeout waiting for action \(action.id) to be executed", file: file, line: line) + return + } + try await withThrowingTaskGroup(of: Void.self) { group in + let uuid = UUID() + group.addTask { + try await withCheckedThrowingContinuation { continuation in + self.executedActionsContinuations[action.id, default: [:]][uuid] = continuation + } + } + group.addTask { + try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + if let continuation = self.executedActionsContinuations[action.id]?[uuid] { + self.executedActionsContinuations[action.id]?[uuid] = nil + XCTFail("Timeout waiting for action \(action.id) to be executed", file: file, line: line) + continuation.resume() + } + } + try await group.waitForAll() + } + } + + public func didCall( + _ action: Store.Action, + file: StaticString = #file, + line: UInt = #line + ) { + if let i = calledActions.firstIndex(of: action.id) { + calledActions.remove(at: i) + return + } + XCTFail("Action \(action.id) was not called", file: file, line: line) + } + + public func waitCall( + of action: Store.Action, + timeout: TimeInterval = 0.1, + file: StaticString = #file, + line: UInt = #line + ) async { + if let i = calledActions.firstIndex(of: action.id) { + calledActions.remove(at: i) + return + } + guard timeout > 0 else { + XCTFail("Timeout waiting for action \(action.id) to be called", file: file, line: line) + return + } + await withTaskGroup(of: Void.self) { group in + let uuid = UUID() + group.addTask { + await withCheckedContinuation { continuation in + self.calledActionsContinuations[action.id, default: [:]][uuid] = continuation + } + } + group.addTask { + try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + if let continuation = self.calledActionsContinuations[action.id]?[uuid] { + self.calledActionsContinuations[action.id]?[uuid] = nil + XCTFail("Timeout waiting for action \(action.id) to be called", file: file, line: line) + continuation.resume() + } + } + await group.waitForAll() + } + } + + private func didCallAction(_ actionID: StoreActionID) { + calledActions.append(actionID) + calledActionsContinuations[actionID]?.values.forEach { $0.resume() } + calledActionsContinuations[actionID] = nil + } + + private func didExecuteAction(_ actionID: StoreActionID, error: Error?) { + executedActions.append((actionID, error)) + executedActionsContinuations[actionID]?.values.forEach { + if let error { + $0.resume(throwing: error) + } else { + $0.resume() + } + } + executedActionsContinuations[actionID] = nil + } +} + +#endif