Skip to content

Commit

Permalink
Merge pull request #270 from player-ui/PLAYA-8756
Browse files Browse the repository at this point in the history
Playa 8756 - iOS add callTryCatchWrapper function on JSValue
  • Loading branch information
nancywu1 authored Jan 25, 2024
2 parents fa3480b + d9470de commit 58a47fd
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 23 deletions.
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

0 comments on commit 58a47fd

Please sign in to comment.