Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playa 8756 - iOS add callTryCatchWrapper function on JSValue #270

Merged
merged 13 commits into from
Jan 25, 2024
75 changes: 75 additions & 0 deletions ios/packages/JSValueExtensionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// JSValueExtensionsTests.swift
// PlayerUI
//
// Created by Zhao Xia Wu on 2024-01-17.
//

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: [])

var caughtError = false

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 {
caughtError = true
XCTAssertEqual(error as? JSValueError, JSValueError.thrownFromJS(message: "Error: Transitioning while ongoing transition from VIEW_1 is in progress is not supported"))
}

if !caughtError {
XCTFail("Expected error, but no error was caught")
}

return
}
}
}
}

player.start(flow: FlowData.MULTIPAGE, completion: {_ in})
}
}
7 changes: 5 additions & 2 deletions ios/packages/core/Sources/Types/Core/Flow.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Flow.swift
//
//
//
// Created by Borawski, Harris on 2/13/20.
//
Expand Down 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/packages/core/Sources/Types/Core/FlowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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/packages/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/packages/core/Tests/HeadlessPlayerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,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 @@ -143,7 +147,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("Error while transitioning")
}
}

func testPlayerControllers() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,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 Expand Up @@ -313,4 +317,4 @@ internal extension XCTestCase {
await fulfillment(of: [expectation], timeout: timeout)
return cancel
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,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 @@ -165,7 +165,11 @@ class ExternalActionViewModifierPluginTests: ViewInspectorTestCase {
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 @@ -174,7 +178,11 @@ class ExternalActionViewModifierPluginTests: ViewInspectorTestCase {
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 @@ -69,8 +69,6 @@ class StageRevertDataPluginTests: XCTestCase {
viewController.hooks.view.tap { view in
view.hooks.onUpdate.tap { value 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
}

Expand All @@ -82,6 +80,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 {
viewController.hooks.view.tap { view in
view.hooks.onUpdate.tap { value 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
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@ class TransitionPluginTests: ViewInspectorTestCase {

let playerTransition1 = player.hooks?.transition.call()
XCTAssertEqual(playerTransition1, .identity)
(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")
}

let playerTransitions3 = player.hooks?.transition.call()
XCTAssertEqual(playerTransitions3, .test1)
(player.state as? InProgressState)?.controllers?.flow.transition(with: "prev")
do {
try (player.state as? InProgressState)?.controllers?.flow.transition(with: "prev")
} catch {
"Transition with 'next' failed"
}

let playerTransitions4 = player.hooks?.transition.call()
XCTAssertEqual(playerTransitions4, .test2)
Expand Down
3 changes: 2 additions & 1 deletion xcode/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ PODS:
- PlayerUI/BeaconPlugin
- PlayerUI/Core
- PlayerUI/SwiftUI
- PlayerUI/SwiftUIPendingTransactionPlugin
- PlayerUI/StageRevertDataPlugin (0.0.1-placeholder):
- PlayerUI/Core
- PlayerUI/SwiftUI (0.0.1-placeholder):
Expand Down Expand Up @@ -133,7 +134,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
EyesXCUI: bbb10a48b8bd1a15d541f2bc1f4d18f4db654ef1
PlayerUI: 7b9bdbf01b4da672a0b6be1281f7b70e134d4e3c
PlayerUI: da5780c2da508c19a029536a070e73efe14b1bd4
SwiftHooks: 3ecc67c23da335d44914a8a74bd1dd23c7c149e6
SwiftLint: 4fa9579c63416865179bc416f0a92d55f009600d
ViewInspector: 53313c757eddc5c4842bc7943a66821a68d02d3e
Expand Down