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
2 changes: 1 addition & 1 deletion 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
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
73 changes: 73 additions & 0 deletions ios/packages/core/Tests/utilities/JSValueExtensionsTests.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 @@ -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,20 +122,15 @@ 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
guard let state = player.state as? InProgressState else { return }
state.controllers?.flow.transition(with: "Next")
}
}
}

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 }
flowController.transition(with: "NEXT")
do {
try flowController.transition(with: "NEXT")
} catch {
XCTFail("Transition with 'NEXT' failed")
}
}
}
})
Expand Down
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 @@ -86,7 +86,12 @@ class StageRevertDataPluginTests: XCTestCase {
flow.hooks.afterTransition.tap { flowInstance in
guard flowInstance.currentState?.name == "VIEW_3" else {
(player.state as? InProgressState)?.controllers?.data.set(transaction: ["name": "Test"])
flowController.transition(with: "clear")
do {
try flowController.transition(with: "clear")
} catch {
XCTFail("Transition with 'clear' failed")
}

return
}
}
Expand Down Expand Up @@ -121,7 +126,12 @@ class StageRevertDataPluginTests: XCTestCase {
flow.hooks.afterTransition.tap { flowInstance in
guard flowInstance.currentState?.name == "VIEW_2" else {
(player.state as? InProgressState)?.controllers?.data.set(transaction: ["name": "Test"])
flowController.transition(with: "commit")
do {
try flowController.transition(with: "commit")
} catch {
XCTFail("Transition with 'commit' failed")
}

return
}
}
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