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

jvm and ios-async-node-ability-to-remove-resolved-async-node #488

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2f28724
Initial code commit
sakuntala-motukuri Aug 12, 2024
dcc3187
Merge branch 'main' into jvm-async-node-ability-to-remove-resolved-as…
sakuntala-motukuri Aug 12, 2024
477c0c6
jvm-async-node-ability-to-remove-resolved-async-node
sakuntala-motukuri Aug 13, 2024
8fd4dab
fixed linter
sakuntala-motukuri Aug 13, 2024
d865fb3
ios-async-node-ability-to-remove-resolved-async-node
sakuntala-motukuri Aug 13, 2024
f78df38
changed some logic and reverted NodeSerializableField
sakuntala-motukuri Aug 14, 2024
0340188
ios expose constantsController #446
cehan-Chloe Aug 9, 2024
2425424
add docstrings to public funcs
cehan-Chloe Aug 12, 2024
729929f
fix return type and add tests
cehan-Chloe Aug 12, 2024
da38906
add comments
cehan-Chloe Aug 14, 2024
bceb781
Android/JVM - expose constantController
cehan-Chloe Aug 14, 2024
850bf52
fix test build
cehan-Chloe Aug 14, 2024
f55e495
remove start player in tests
cehan-Chloe Aug 14, 2024
a2c46cc
fix test build
cehan-Chloe Aug 14, 2024
2a63f45
Changed logic and updated test cases for android
sakuntala-motukuri Aug 17, 2024
5bcdcb3
Merge branch 'main' into jvm-async-node-ability-to-remove-resolved-as…
sakuntala-motukuri Aug 17, 2024
6917491
Updated 'handle multiple updates through callback mechanism'
sakuntala-motukuri Aug 19, 2024
de6df57
updated async-node ios version and fixed review comments
sakuntala-motukuri Aug 21, 2024
d252411
Fixed ios tests
sakuntala-motukuri Aug 21, 2024
04f3573
Removed callback from testHandleMultipleUpdatesThroughCallback
sakuntala-motukuri Aug 22, 2024
663ce13
Merge branch 'main' into jvm-async-node-ability-to-remove-resolved-as…
sakuntala-motukuri Aug 22, 2024
91359bc
Fixed review comments and updated ios test cases
sakuntala-motukuri Aug 22, 2024
a9542b4
updated test to .emptyNode case
sakuntala-motukuri Aug 22, 2024
f864330
added assertions in ios
sakuntala-motukuri Aug 23, 2024
45fe9fc
added suspend in jvm
sakuntala-motukuri Aug 23, 2024
ad6d7b8
Fixed review comments for null node test and callback test
sakuntala-motukuri Aug 26, 2024
428d005
Linter fix
sakuntala-motukuri Aug 26, 2024
ace8b73
Updated review comments
sakuntala-motukuri Aug 26, 2024
e8dc375
Added doc comment
sakuntala-motukuri Aug 26, 2024
5b850e0
updated ios review comments and updated tests
sakuntala-motukuri Sep 3, 2024
df5c4d6
Merge branch 'main' into jvm-async-node-ability-to-remove-resolved-as…
sakuntala-motukuri Sep 3, 2024
32b9c57
removed white spaces
sakuntala-motukuri Sep 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.intuit.playerui.core.bridge.hooks

import com.intuit.hooks.AsyncParallelBailHook
import com.intuit.hooks.BailResult
import com.intuit.hooks.HookContext
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable

@OptIn(ExperimentalCoroutinesApi::class)
@Serializable(with = NodeAsyncParallelBailHook2.Serializer::class)
public class NodeAsyncParallelBailHook2<T1, T2, R : Any>(
override val node: Node,
serializer1: KSerializer<T1>,
serializer2: KSerializer<T2>
) : AsyncParallelBailHook<(HookContext, T1, T2) -> BailResult<R>, R>(), AsyncNodeHook<R> {

init {
init(serializer1, serializer2)
}

override suspend fun callAsync(context: HookContext, serializedArgs: Array<Any?>): R {
require(serializedArgs.size == 2) { "Expected exactly two arguments, but got ${serializedArgs.size}" }
val (p1, p2) = serializedArgs
val result = call(10) { f, _ ->
f(context, p1 as T1, p2 as T2)
} as R
return result
}

internal class Serializer<T1, T2, R : Any>(
private val serializer1: KSerializer<T1>,
private val serializer2: KSerializer<T2>,
`_`: KSerializer<R>
) : NodeWrapperSerializer<NodeAsyncParallelBailHook2<T1, T2, R>>({
NodeAsyncParallelBailHook2(it, serializer1, serializer2)
})
}
117 changes: 65 additions & 52 deletions plugins/async-node/ios/Sources/AsyncNodePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import JavaScriptCore
import PlayerUI
#endif

public typealias AsyncHookHandler = (JSValue) async throws -> AsyncNodeHandlerType
public typealias AsyncHookHandler = (JSValue?) async throws -> AsyncNodeHandlerType

public enum AsyncNodeHandlerType {
case multiNode([ReplacementNode])
case singleNode(ReplacementNode)
case nullOrUndefinedNode(ReplacementNode)
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -42,58 +43,70 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin {
self.plugins = plugins
}

override public func setup(context: JSContext) {
super.setup(context: context)
override public func setup(context: JSContext) {
super.setup(context: context)

if let pluginRef = pluginRef {
self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook(baseValue: pluginRef, name: "onAsyncNode"))
}
if let pluginRef = pluginRef {
self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook(baseValue: pluginRef, name: "onAsyncNode"))
}

hooks?.onAsyncNode.tap({ node in
// hook value is the original node
guard let asyncHookHandler = self.asyncHookHandler else {
return JSValue()
}

let replacementNode = try await (asyncHookHandler)(node)

switch replacementNode {
case .multiNode(let replacementNodes):
let jsValueArray = replacementNodes.compactMap({ node in
switch node {
case .concrete(let jsValue):
return jsValue
case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
} catch {
return nil
}
}
})

return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray])

case .singleNode(let replacementNode):
switch replacementNode {

case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
return context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
} catch {
break
}
case .concrete(let jsValue):
return jsValue
}
}

return nil
})
hooks?.onAsyncNode.tap({ node in
// hook value is the original node
guard let asyncHookHandler = self.asyncHookHandler else {
return JSValue()
}

let replacementNode = try await (asyncHookHandler)(node)
var result: JSValue?

switch replacementNode {
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved
case .multiNode(let replacementNodes):
let jsValueArray = replacementNodes.compactMap({ node in
switch node {
case .concrete(let jsValue):
return jsValue
case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
result = context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
return result
} catch {
return nil
}
}
})

result = context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray])

case .singleNode(let replacementNode):
switch replacementNode {
case .encodable(let encodable):
let encoder = JSONEncoder()
do {
let res = try encoder.encode(encodable)
result = context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
} catch {
result = nil
}
case .concrete(let jsValue):
return jsValue
}

case .nullOrUndefinedNode(let replacementNode):
switch replacementNode {
case .encodable(let encodable):
let encoder = JSONEncoder()
let res = try encoder.encode(encodable)
result = context.evaluateScript("(\(String(data: res, encoding: .utf8) ?? ""))") as JSValue
case .concrete(let jsValue):
return jsValue
}
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved

}

return result
})
}

/**
Expand Down Expand Up @@ -191,4 +204,4 @@ public class AsyncNodePluginPlugin: JSBasePlugin {
)
#endif
}
}
}
170 changes: 161 additions & 9 deletions plugins/async-node/ios/Tests/AsynNodePluginTests.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// AsyncNodePluginTests.swift
// PlayerUI
//
// Created by Zhao Xia Wu on 2024-02-05.
//

import Foundation
import XCTest
import SwiftUI
Expand Down Expand Up @@ -36,7 +29,6 @@ class AsyncNodePluginTests: XCTestCase {
XCTAssertNotNil(plugin.pluginRef)
}


func testAsyncNodeWithAnotherAsyncNodeDelay() {
let handlerExpectation = XCTestExpectation(description: "first data did not change")

Expand Down Expand Up @@ -292,6 +284,166 @@ class AsyncNodePluginTests: XCTestCase {
XCTAssert(count == 3)
XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2")
}

func testAsyncNodeWithNullNode() {
let handlerExpectation = XCTestExpectation(description: "null node handled")

let context = JSContext()

let resolveHandler: AsyncHookHandler = { node in
handlerExpectation.fulfill()
XCTAssertNil(node)
return .singleNode(.concrete(JSValue()))
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved
}

let asyncNodePluginPlugin = AsyncNodePluginPlugin()
let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolveHandler)

plugin.context = context

XCTAssertNotNil(asyncNodePluginPlugin.context)

let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext())

player.start(flow: .asyncNodeJson, completion: {_ in})

wait(for: [handlerExpectation], timeout: 5)
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved
}

func testAsyncNodeWithUndefinedNode() {
let handlerExpectation = XCTestExpectation(description: "undefined node handled")

let context = JSContext()

let resolveHandler: AsyncHookHandler = { node in
handlerExpectation.fulfill()
XCTAssertNil(node)
return .singleNode(.concrete(JSValue()))
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved
}

let asyncNodePluginPlugin = AsyncNodePluginPlugin()
let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolveHandler)

plugin.context = context

XCTAssertNotNil(asyncNodePluginPlugin.context)

let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext())

player.start(flow: .asyncNodeJson, completion: {_ in})

wait(for: [handlerExpectation], timeout: 5)
}

func testHandleMultipleUpdatesThroughCallbackMechanism() {
let handlerExpectation = XCTestExpectation(description: "first data did not change")

let context = JSContext()
var count = 0

let resolve: AsyncHookHandler = { _ in
handlerExpectation.fulfill()

if count == 1 {
return .multiNode([
ReplacementNode.concrete(context?.evaluateScript("""
(
{"asset": {"id": "text", "type": "text", "value":"1st value in the multinode"}}
)
""") ?? JSValue()),
ReplacementNode.encodable(AsyncNode(id: "id"))])
} else if count == 2 {
return .multiNode([
ReplacementNode.encodable(AssetPlaceholderNode(asset: PlaceholderNode(id: "text-2", type: "text", value: "2nd value in the multinode"))),
ReplacementNode.encodable(AsyncNode(id: "id-1"))])
} else if count == 3 {
return .singleNode(ReplacementNode.encodable(
AssetPlaceholderNode(asset: PlaceholderNode(id: "text", type: "text", value: "3rd value in the multinode"))
))
}

return .singleNode(ReplacementNode.concrete(context?.evaluateScript("") ?? JSValue()))
}
sakuntala-motukuri marked this conversation as resolved.
Show resolved Hide resolved

let asyncNodePluginPlugin = AsyncNodePluginPlugin()
let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve)

plugin.context = context

XCTAssertNotNil(asyncNodePluginPlugin.context)

let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext())

let textExpectation = XCTestExpectation(description: "newText found")
let textExpectation2 = XCTestExpectation(description: "newText found")
let textExpectation3 = XCTestExpectation(description: "newText found")

var expectedMultiNode1Text: String = ""
var expectedMultiNode2Text: String = ""
var expectedMultiNode3Text: String = ""

player.hooks?.viewController.tap({ (viewController) in
viewController.hooks.view.tap { (view) in
view.hooks.onUpdate.tap { val in
count += 1

if count == 2 {
let newText1 = val
.objectForKeyedSubscript("values")
.objectAtIndexedSubscript(1)
.objectForKeyedSubscript("asset")
.objectForKeyedSubscript("value")
guard let textString1 = newText1?.toString() else { return XCTFail("newText was not a string") }

expectedMultiNode1Text = textString1
textExpectation.fulfill()
}

if count == 3 {
let newText2 = val
.objectForKeyedSubscript("values")
.objectAtIndexedSubscript(2)
.objectForKeyedSubscript("asset")
.objectForKeyedSubscript("value")
guard let textString2 = newText2?.toString() else { return XCTFail("newText was not a string") }

expectedMultiNode2Text = textString2

textExpectation2.fulfill()
}

if count == 4 {
let newText3 = val
.objectForKeyedSubscript("values")
.objectAtIndexedSubscript(3)
.objectForKeyedSubscript("asset")
.objectForKeyedSubscript("value")
guard let textString3 = newText3?.toString() else { return XCTFail("newText was not a string") }

expectedMultiNode3Text = textString3
textExpectation3.fulfill()
}
}
}
})

player.start(flow: .asyncNodeJson, completion: { _ in})

wait(for: [handlerExpectation, textExpectation], timeout: 5)

XCTAssert(count == 2)
XCTAssertEqual(expectedMultiNode1Text, "1st value in the multinode")

wait(for: [textExpectation2], timeout: 6)

XCTAssert(count == 3)
XCTAssertEqual(expectedMultiNode2Text, "2nd value in the multinode")

wait(for: [textExpectation3], timeout: 7)

XCTAssert(count == 4)
XCTAssertEqual(expectedMultiNode3Text, "3rd value in the multinode")
}
}

extension String {
Expand Down Expand Up @@ -358,4 +510,4 @@ struct PlaceholderNode: Codable, Equatable, AssetData {
self.type = type
self.value = value
}
}
}
Loading