Skip to content

Commit

Permalink
sync ios code for blocked transitions
Browse files Browse the repository at this point in the history
  • Loading branch information
hborawski committed Apr 12, 2024
1 parent a5fb4eb commit 3dac3fc
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 17 deletions.
5 changes: 5 additions & 0 deletions ios/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ xcodeproj(
"//ios/logger:PlayerUILoggerTests",
"//ios/swiftui:PlayerUISwiftUITests",
"//ios/swiftui:PlayerUISwiftUIViewInspectorTests",
"//ios/test-utils-core:PlayerUITestUtilitiesCoreTests",
"//ios/test-utils:PlayerUITestUtilitiesTests",
"//ios/test-utils:PlayerUITestUtilitiesViewInspectorTests",

# plugins
"//plugins/beacon/ios:PlayerUIBaseBeaconPluginTests",
Expand Down Expand Up @@ -44,6 +47,8 @@ xcodeproj(
"//plugins/reference-assets/swiftui:PlayerUIReferenceAssetsViewInspectorTests",
"//plugins/reference-assets/swiftui:PlayerUIReferenceAssetsUITests",

"//plugins/stage-revert-data/ios:PlayerUIStageRevertDataPluginTests",

"//plugins/transition/swiftui:PlayerUITransitionPluginViewInspectorTests",
"//plugins/types-provider/ios:PlayerUITypesProviderPluginTests"
],
Expand Down
5 changes: 4 additions & 1 deletion ios/core/Sources/Types/Core/Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ public class Flow: CreatedFromJSValue {
*/
public init(_ value: JSValue) {
self.value = value
hooks = FlowHooks(transition: Hook2<NamedState?, NamedState>(baseValue: value, name: "transition"))
hooks = FlowHooks(transition: Hook2(baseValue: value, name: "transition"), afterTransition: Hook(baseValue: value, name: "afterTransition"))
}
}

public struct FlowHooks {
/// A hook that fires when transitioning states and giving the old and new states as parameters
public var transition: Hook2<NamedState?, NamedState>

/// A hook that fires after a transition occurs giving the FlowInstance as parameter
public var afterTransition: Hook<Flow>
}

public struct NamedState: CreatedFromJSValue {
Expand Down
4 changes: 2 additions & 2 deletions ios/core/Sources/Types/Core/FlowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public class FlowController: CreatedFromJSValue {
- parameters:
- action: The action to use for transitioning
*/
public func transition(with action: String) {
value.invokeMethod("transition", withArguments: [action])
public func transition(with action: String) throws {
try self.value.objectForKeyedSubscript("transition").tryCatch(args: [action])
}
}

59 changes: 59 additions & 0 deletions ios/core/Sources/utilities/JSValue+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// JSValue+Extensions.swift
// PlayerUI
//
// Created by Zhao Xia Wu on 2024-01-18.
//

import Foundation
import JavaScriptCore

extension JSValue {


/**
A way to catch errors for functions not called inside a player process. Can be called on functions with a return value and void with discardableResult.
- parameters:
- args: List of arguments taken by the function
*/
@discardableResult
public func tryCatch(args: Any...) throws -> JSValue? {
var tryCatchWrapper: JSValue? {
self.context.evaluateScript(
"""
(fn, args) => {
try {
return fn(...args)
} catch(e) {
return e
}
}
""")
}

var errorCheckWrapper: JSValue? {
self.context.evaluateScript(
"""
(obj) => (obj instanceof Error)
""")
}
let result = tryCatchWrapper?.call(withArguments: [self, args])

let isError = errorCheckWrapper?.call(withArguments: [result as Any])

let errorMessage = result?.toString() ?? ""

if isError?.toBool() == true {
throw JSValueError.thrownFromJS(message: errorMessage)
} else {
return result
}
}
}

/**
Represents the different errors that occur when evaluating JSValue
*/
public enum JSValueError: Error, Equatable {
case thrownFromJS(message: String)
}
12 changes: 10 additions & 2 deletions ios/core/Tests/HeadlessPlayerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ class HeadlessPlayerTests: XCTestCase {
})

player.start(flow: FlowData.COUNTER, completion: {_ in})
(player.state as? InProgressState)?.controllers?.flow.transition(with: "NEXT")
do {
try (player.state as? InProgressState)?.controllers?.flow.transition(with: "NEXT")
} catch {
XCTFail("Transition with 'NEXT' failed")
}

wait(for: [inProgress, completed], timeout: 5)
}
Expand Down Expand Up @@ -144,7 +148,11 @@ class HeadlessPlayerTests: XCTestCase {
}
XCTAssertNotNil(player.state as? InProgressState)
XCTAssertEqual(player.state?.status, .inProgress)
(player.state as? InProgressState)?.controllers?.flow.transition(with: "NEXT")
do {
try (player.state as? InProgressState)?.controllers?.flow.transition(with: "NEXT")
} catch {
XCTFail("Transition with 'NEXT' failed")
}
}

func testPlayerControllers() {
Expand Down
73 changes: 73 additions & 0 deletions ios/core/Tests/utilities/JSValueExtensionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// JSValueExtensionsTests.swift
// PlayerUI-Unit-Unit
//
// Created by Zhao Xia Wu on 2024-01-22.
//


import Foundation
import XCTest
import JavaScriptCore
@testable import PlayerUI

class JSValueExtensionsTests: XCTestCase {
let context: JSContext = JSContext()
func testTryCatchWrapperReturningError() {

let functionReturningError = self.context
.evaluateScript("""
(() => {
throw new Error("Fail")
})
""")

do {
let _ = try functionReturningError?.tryCatch(args: [] as [String])
} catch let error {
XCTAssertEqual(error as? JSValueError, JSValueError.thrownFromJS(message: "Error: Fail"))
}
}

func testTryCatchWrapperReturningNumber() {
let functionReturningInt = self.context
.evaluateScript("""
(() => {
return 1
})
""")

do {
let result = try functionReturningInt?.tryCatch(args: [] as [String])
XCTAssertEqual(result?.toInt32(), 1)
} catch let error {
XCTFail("Should have returned Int but failed with \(error)")
}
}

func testTransitionDuringAnActiveTransitionShouldCatchErrorUsingTryCatchWrapper() {
let player = HeadlessPlayerImpl(plugins: [])

let expectation = expectation(description: "Wait for on update")

player.hooks?.viewController.tap { viewController in
viewController.hooks.view.tap { view in
view.hooks.onUpdate.tap { value in
guard view.id == "view-2" else {
do {
try (player.state as? InProgressState)?.controllers?.flow.transition(with: "NEXT")
} catch let error {
XCTAssertEqual(error as? JSValueError, JSValueError.thrownFromJS(message: "Error: Transitioning while ongoing transition from VIEW_1 is in progress is not supported"))
expectation.fulfill()
}

return
}
}
}
}

player.start(flow: FlowData.MULTIPAGE, completion: {_ in})
wait(for: [expectation], timeout: 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,11 @@ class ManagedPlayerViewModelTests: XCTestCase {

XCTAssertNotNil(model.currentState)

(player.state as? InProgressState)?.controllers?.flow.transition(with: "next")
do {
try (player.state as? InProgressState)?.controllers?.flow.transition(with: "next")
} catch {
XCTFail("Transition with 'next' Failed")
}

XCTAssertNil(model.currentState)
}
Expand Down
14 changes: 9 additions & 5 deletions ios/test-utils/ViewInspector/ui-test/AssetFlowViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,17 @@ class ForceTransitionPlugin: NativePlugin {

func apply<P>(player: P) where P: HeadlessPlayer {
guard let player = player as? SwiftUIPlayer else { return }
player.hooks?.viewController.tap { viewController in
viewController.hooks.view.tap { view in
view.hooks.onUpdate.tap { _ in
player.hooks?.flowController.tap({ flowController in
flowController.hooks.flow.tap { flow in
flow.hooks.afterTransition.tap { _ in
guard let state = player.state as? InProgressState else { return }
state.controllers?.flow.transition(with: "Next")
do {
try flowController.transition(with: "NEXT")
} catch {
XCTFail("Transition with 'NEXT' failed")
}
}
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ class ExternalActionViewModifierPluginTests: XCTestCase {
let content = try view.vStack().first?.anyView().anyView().modifier(ExternalStateSheetModifier.self).viewModifierContent()
let value = try content?.sheet().anyView().text().string()
XCTAssertEqual(value, "External State")
(try view.actualView().state as? InProgressState)?.controllers?.flow.transition(with: "Next")
do {
try (view.actualView().state as? InProgressState)?.controllers?.flow.transition(with: "Next")
} catch {
XCTFail("Transition with 'Next' failed")
}
}

wait(for: [exp, handlerExpectation], timeout: 10)
Expand All @@ -181,7 +185,11 @@ class ExternalActionViewModifierPluginTests: XCTestCase {
XCTAssertEqual(state?.controllers?.flow.current?.currentState?.value?.stateType, "VIEW")
XCTAssertNil(plugin.state)
XCTAssertFalse(plugin.isExternalState)
state?.controllers?.flow.transition(with: "Next")
do {
try state?.controllers?.flow.transition(with: "Next")
} catch {
XCTFail("Transition with 'Next' failed")
}
wait(for: [completionExpectation], timeout: 10)

ViewHosting.expel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ class StageRevertDataPluginTests: XCTestCase {
player.hooks?.viewController.tap { viewController in
viewController.hooks.view.tap { view in
guard view.id == "view-3" else {
(player.state as? InProgressState)?.controllers?.data.set(transaction: ["name": "Test"])
(player.state as? InProgressState)?.controllers?.flow.transition(with: "clear")
return
}
view.hooks.onUpdate.tap { value in
Expand All @@ -83,6 +81,22 @@ class StageRevertDataPluginTests: XCTestCase {
}
}

player.hooks?.flowController.tap({ flowController in
flowController.hooks.flow.tap { flow in
flow.hooks.afterTransition.tap { flowInstance in
guard flowInstance.currentState?.name == "VIEW_3" else {
(player.state as? InProgressState)?.controllers?.data.set(transaction: ["name": "Test"])
do {
try flowController.transition(with: "clear")
} catch {
XCTFail("Transition with 'clear' failed")
}
return
}
}
}
})

player.start(flow: json, completion: {_ in})
wait(for: [expected], timeout: 1)
}
Expand All @@ -94,8 +108,6 @@ class StageRevertDataPluginTests: XCTestCase {
player.hooks?.viewController.tap { viewController in
viewController.hooks.view.tap { view in
guard view.id == "view-2" else {
(player.state as? InProgressState)?.controllers?.data.set(transaction: ["name": "Test"])
(player.state as? InProgressState)?.controllers?.flow.transition(with: "commit")
return
}
view.hooks.onUpdate.tap { value in
Expand All @@ -107,6 +119,22 @@ class StageRevertDataPluginTests: XCTestCase {
}
}

player.hooks?.flowController.tap({ flowController in
flowController.hooks.flow.tap { flow in
flow.hooks.afterTransition.tap { flowInstance in
guard flowInstance.currentState?.name == "VIEW_2" else {
(player.state as? InProgressState)?.controllers?.data.set(transaction: ["name": "Test"])
do {
try flowController.transition(with: "commit")
} catch {
XCTFail("Transition with 'commit' failed")
}
return
}
}
}
})

player.start(flow: json, completion: {_ in})
wait(for: [expected], timeout: 1)
}
Expand Down

0 comments on commit 3dac3fc

Please sign in to comment.