From 2f287241a6e337c631c4e3327ac5e4e1ba13a1b0 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Mon, 12 Aug 2024 14:43:03 -0400 Subject: [PATCH 01/28] Initial code commit --- .../hooks/NodeAsyncParallelBailHook2.kt | 40 +++++++++++++++++++ .../serializers/NodeSerializableField.kt | 11 +++++ .../plugins/asyncnode/AsyncNodePlugin.kt | 8 ++-- .../plugins/asyncnode/AsyncNodePluginTest.kt | 4 +- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt new file mode 100644 index 000000000..20fbf5113 --- /dev/null +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt @@ -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( + override val node: Node, + serializer1: KSerializer, + serializer2: KSerializer +) : AsyncParallelBailHook<(HookContext, T1, T2) -> BailResult, R>(), AsyncNodeHook { + + init { + init(serializer1, serializer2) + } + + override suspend fun callAsync(context: HookContext, serializedArgs: Array): 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( + private val serializer1: KSerializer, + private val serializer2: KSerializer, + `_`: KSerializer + ) : NodeWrapperSerializer>({ + NodeAsyncParallelBailHook2(it, serializer1, serializer2) + }) +} diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt index f71ffe5a1..84122e624 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt @@ -115,6 +115,17 @@ public fun NodeWrapper.NodeSerializableField( defaultValue: (Node.(String) -> T)? = null, ): NodeSerializableField = NodeSerializableField(::node, serializer, strategy, name, defaultValue) +//@ExperimentalPlayerApi +//public inline fun NodeWrapper.NodeSerializableField( +// serializer: KSerializer? = null, +// strategy: NodeSerializableField.CacheStrategy? = null, +// name: String? = null, +// noinline defaultValue: (Node.(String) -> T)? = null, +//): NodeSerializableField { +// val effectiveSerializer = serializer ?: node.format.serializer() +// return NodeSerializableField(::node, effectiveSerializer, strategy, name, defaultValue) +//} + @ExperimentalPlayerApi public inline fun NodeWrapper.NodeSerializableField( strategy: NodeSerializableField.CacheStrategy? = null, diff --git a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt index 37a3154d5..2ab65d44d 100644 --- a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt +++ b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt @@ -3,12 +3,10 @@ package com.intuit.playerui.plugins.asyncnode import com.intuit.playerui.core.bridge.Node import com.intuit.playerui.core.bridge.NodeWrapper import com.intuit.playerui.core.bridge.hooks.NodeAsyncParallelBailHook1 +import com.intuit.playerui.core.bridge.hooks.NodeAsyncParallelBailHook2 import com.intuit.playerui.core.bridge.runtime.Runtime import com.intuit.playerui.core.bridge.runtime.ScriptContext -import com.intuit.playerui.core.bridge.serialization.serializers.GenericSerializer -import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField -import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializer -import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer +import com.intuit.playerui.core.bridge.serialization.serializers.* import com.intuit.playerui.core.player.Player import com.intuit.playerui.core.player.PlayerException import com.intuit.playerui.core.plugins.JSScriptPluginWrapper @@ -31,7 +29,7 @@ public class AsyncNodePlugin : JSScriptPluginWrapper(pluginName, sourcePath = bu @Serializable(with = Hooks.Serializer::class) public class Hooks internal constructor(override val node: Node) : NodeWrapper { /** The hook right before the View starts resolving. Attach anything custom here */ - public val onAsyncNode: NodeAsyncParallelBailHook1>> by NodeSerializableField(NodeAsyncParallelBailHook1.serializer(NodeSerializer(), ListSerializer(MapSerializer(String.serializer(), GenericSerializer())))) + public val onAsyncNode: NodeAsyncParallelBailHook2) -> Unit, List>> by NodeSerializableField(NodeAsyncParallelBailHook2.serializer(NodeSerializer(), Function1Serializer(MapSerializer(String.serializer(), GenericSerializer()), GenericSerializer()), ListSerializer(MapSerializer(String.serializer(), GenericSerializer())))) internal object Serializer : NodeWrapperSerializer(AsyncNodePlugin::Hooks) } diff --git a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt index fb30efa76..a5d12f6fc 100644 --- a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt +++ b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt @@ -73,7 +73,7 @@ internal class AsyncNodePluginTest : PlayerTest() { } @TestTemplate - fun `async node hook is tappable`() = runBlockingTest() { + fun `async node hook is tappable`() = runBlockingTest { var update: Asset? = null plugin?.hooks?.onAsyncNode?.tap("") { _, node -> BailResult.Bail( @@ -228,4 +228,4 @@ internal class AsyncNodePluginTest : PlayerTest() { } Assertions.assertTrue(true) } -} +} \ No newline at end of file From 477c0c65e0314a40cafc9ee4649ffaf6abcf31c1 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Tue, 13 Aug 2024 17:27:54 -0400 Subject: [PATCH 02/28] jvm-async-node-ability-to-remove-resolved-async-node --- .../plugins/asyncnode/AsyncNodePlugin.kt | 24 +++++-- .../plugins/asyncnode/AsyncNodePluginTest.kt | 68 +++++++++++++++++-- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt index 2ab65d44d..e04256dd5 100644 --- a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt +++ b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt @@ -2,15 +2,19 @@ package com.intuit.playerui.plugins.asyncnode import com.intuit.playerui.core.bridge.Node import com.intuit.playerui.core.bridge.NodeWrapper -import com.intuit.playerui.core.bridge.hooks.NodeAsyncParallelBailHook1 import com.intuit.playerui.core.bridge.hooks.NodeAsyncParallelBailHook2 import com.intuit.playerui.core.bridge.runtime.Runtime import com.intuit.playerui.core.bridge.runtime.ScriptContext -import com.intuit.playerui.core.bridge.serialization.serializers.* +import com.intuit.playerui.core.bridge.serialization.serializers.Function1Serializer +import com.intuit.playerui.core.bridge.serialization.serializers.GenericSerializer +import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField +import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializer +import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer import com.intuit.playerui.core.player.Player import com.intuit.playerui.core.player.PlayerException import com.intuit.playerui.core.plugins.JSScriptPluginWrapper import com.intuit.playerui.core.plugins.findPlugin +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer @@ -23,13 +27,25 @@ public class AsyncNodePlugin : JSScriptPluginWrapper(pluginName, sourcePath = bu override fun apply(runtime: Runtime<*>) { runtime.load(ScriptContext(script, bundledSourcePath)) instance = runtime.buildInstance("(new $pluginName({plugins: [new AsyncNodePlugin.AsyncNodePluginPlugin()]}))") - hooks = instance.getSerializable("hooks", Hooks.serializer()) ?: throw PlayerException("AsyncNodePlugin is not loaded correctly") + hooks = instance.getSerializable("hooks", Hooks.serializer()) + ?: throw PlayerException("AsyncNodePlugin is not loaded correctly") } @Serializable(with = Hooks.Serializer::class) public class Hooks internal constructor(override val node: Node) : NodeWrapper { /** The hook right before the View starts resolving. Attach anything custom here */ - public val onAsyncNode: NodeAsyncParallelBailHook2) -> Unit, List>> by NodeSerializableField(NodeAsyncParallelBailHook2.serializer(NodeSerializer(), Function1Serializer(MapSerializer(String.serializer(), GenericSerializer()), GenericSerializer()), ListSerializer(MapSerializer(String.serializer(), GenericSerializer())))) + public val onAsyncNode: NodeAsyncParallelBailHook2) -> Unit, List>> by + NodeSerializableField( + NodeAsyncParallelBailHook2.serializer( + NodeSerializer(), + Function1Serializer( + MapSerializer(String.serializer(), GenericSerializer()), + GenericSerializer() + ) as KSerializer<(Map) -> Unit>, + ListSerializer(MapSerializer(String.serializer(), GenericSerializer())), + ) + ) + internal object Serializer : NodeWrapperSerializer(AsyncNodePlugin::Hooks) } diff --git a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt index a5d12f6fc..a63d498bf 100644 --- a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt +++ b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt @@ -75,7 +75,7 @@ internal class AsyncNodePluginTest : PlayerTest() { @TestTemplate fun `async node hook is tappable`() = runBlockingTest { var update: Asset? = null - plugin?.hooks?.onAsyncNode?.tap("") { _, node -> + plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> BailResult.Bail( listOf( mapOf( @@ -106,7 +106,7 @@ internal class AsyncNodePluginTest : PlayerTest() { @TestTemplate fun `replace async node with multiNode`() = runBlockingTest { var update: Asset? = null - plugin?.hooks?.onAsyncNode?.tap("") { _, node -> + plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> BailResult.Bail( listOf( mapOf( @@ -164,7 +164,7 @@ internal class AsyncNodePluginTest : PlayerTest() { } } } - plugin?.hooks?.onAsyncNode?.tap("") { _, node -> + plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> asyncTaps++ when (asyncTaps) { 1 -> BailResult.Bail( @@ -228,4 +228,64 @@ internal class AsyncNodePluginTest : PlayerTest() { } Assertions.assertTrue(true) } -} \ No newline at end of file + + @TestTemplate + fun `handle multiple updates through callback mechanism`() = runBlockingTest { + var update: Asset? = null + var updateCount = 0 + + plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> + when (updateCount) { + 0 -> { + callback(mapOf("asset" to mapOf("id" to "asset-1", "type" to "text", "value" to "First update"))) + } + + 1 -> { + callback(mapOf("asset" to mapOf("id" to "asset-2", "type" to "text", "value" to "Second update"))) + } + + 2 -> { + callback(mapOf("asset" to mapOf("id" to "asset-3", "type" to "text", "value" to "Third update"))) + } + } + updateCount++ + BailResult.Bail(emptyList()) + } + + var count = 0 + suspendCancellableCoroutine { cont -> + player.hooks.view.tap { v -> + v?.hooks?.onUpdate?.tap { asset -> + count++ + update = asset + if (count == 3) cont.resume(true) {} + } + } + player.start(asyncNodeFlowSimple) + } + + Assertions.assertTrue(count == 3) + Assertions.assertTrue((update?.get("actions") as List<*>).isNotEmpty()) + } + + @TestTemplate + fun `handle undefined node`() = runBlockingTest { + var update: Asset? = null + plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> + BailResult.Bail(emptyList()) + } + var count = 0 + suspendCancellableCoroutine { cont -> + player.hooks.view.tap { v -> + v?.hooks?.onUpdate?.tap { asset -> + count++ + update = asset + if (count == 2) cont.resume(true) {} + } + } + player.start(asyncNodeFlowSimple) + } + Assertions.assertTrue(count == 2) + Assertions.assertTrue((update?.get("actions") as List<*>).isNotEmpty()) + } +} From 8fd4dab3ea80601b8fb7b7f19dc16e5c6f7a7fe8 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Tue, 13 Aug 2024 17:44:29 -0400 Subject: [PATCH 03/28] fixed linter --- .../plugins/asyncnode/AsyncNodePlugin.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt index e04256dd5..9a51d0e5c 100644 --- a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt +++ b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt @@ -35,16 +35,16 @@ public class AsyncNodePlugin : JSScriptPluginWrapper(pluginName, sourcePath = bu public class Hooks internal constructor(override val node: Node) : NodeWrapper { /** The hook right before the View starts resolving. Attach anything custom here */ public val onAsyncNode: NodeAsyncParallelBailHook2) -> Unit, List>> by - NodeSerializableField( - NodeAsyncParallelBailHook2.serializer( - NodeSerializer(), - Function1Serializer( - MapSerializer(String.serializer(), GenericSerializer()), - GenericSerializer() - ) as KSerializer<(Map) -> Unit>, - ListSerializer(MapSerializer(String.serializer(), GenericSerializer())), + NodeSerializableField( + NodeAsyncParallelBailHook2.serializer( + NodeSerializer(), + Function1Serializer( + MapSerializer(String.serializer(), GenericSerializer()), + GenericSerializer(), + ) as KSerializer<(Map) -> Unit>, + ListSerializer(MapSerializer(String.serializer(), GenericSerializer())), + ), ) - ) internal object Serializer : NodeWrapperSerializer(AsyncNodePlugin::Hooks) } From d865fb3f186b880d9e4fd14257bde610c70098e4 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Tue, 13 Aug 2024 19:36:21 -0400 Subject: [PATCH 04/28] ios-async-node-ability-to-remove-resolved-async-node --- .../ios/Sources/AsyncNodePlugin.swift | 121 +++++++------ .../ios/Tests/AsynNodePluginTests.swift | 170 +++++++++++++++++- 2 files changed, 230 insertions(+), 61 deletions(-) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index a77a12458..a11dec805 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -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) } /** @@ -42,58 +43,74 @@ 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 { + 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() + 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 + } + + } + + return result + }) } /** @@ -191,4 +208,4 @@ public class AsyncNodePluginPlugin: JSBasePlugin { ) #endif } -} +} \ No newline at end of file diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index ef7da334d..521c72119 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -1,10 +1,3 @@ -// -// AsyncNodePluginTests.swift -// PlayerUI -// -// Created by Zhao Xia Wu on 2024-02-05. -// - import Foundation import XCTest import SwiftUI @@ -36,7 +29,6 @@ class AsyncNodePluginTests: XCTestCase { XCTAssertNotNil(plugin.pluginRef) } - func testAsyncNodeWithAnotherAsyncNodeDelay() { let handlerExpectation = XCTestExpectation(description: "first data did not change") @@ -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())) + } + + 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 testAsyncNodeWithUndefinedNode() { + let handlerExpectation = XCTestExpectation(description: "undefined node handled") + + let context = JSContext() + + let resolveHandler: AsyncHookHandler = { node in + handlerExpectation.fulfill() + XCTAssertNil(node) + return .singleNode(.concrete(JSValue())) + } + + 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())) + } + + 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 { @@ -358,4 +510,4 @@ struct PlaceholderNode: Codable, Equatable, AssetData { self.type = type self.value = value } -} +} \ No newline at end of file From f78df383be14eb101e4897c17c4b44d9fffee04e Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Tue, 13 Aug 2024 20:19:58 -0400 Subject: [PATCH 05/28] changed some logic and reverted NodeSerializableField --- .../serializers/NodeSerializableField.kt | 11 ----------- plugins/async-node/ios/Sources/AsyncNodePlugin.swift | 4 ---- 2 files changed, 15 deletions(-) diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt index 84122e624..f71ffe5a1 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/NodeSerializableField.kt @@ -115,17 +115,6 @@ public fun NodeWrapper.NodeSerializableField( defaultValue: (Node.(String) -> T)? = null, ): NodeSerializableField = NodeSerializableField(::node, serializer, strategy, name, defaultValue) -//@ExperimentalPlayerApi -//public inline fun NodeWrapper.NodeSerializableField( -// serializer: KSerializer? = null, -// strategy: NodeSerializableField.CacheStrategy? = null, -// name: String? = null, -// noinline defaultValue: (Node.(String) -> T)? = null, -//): NodeSerializableField { -// val effectiveSerializer = serializer ?: node.format.serializer() -// return NodeSerializableField(::node, effectiveSerializer, strategy, name, defaultValue) -//} - @ExperimentalPlayerApi public inline fun NodeWrapper.NodeSerializableField( strategy: NodeSerializableField.CacheStrategy? = null, diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index a11dec805..5ab9b05a1 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -97,12 +97,8 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { 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 } From 0340188c1faaf9b02e13b53a0d71f4ccdac9a1f4 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Fri, 9 Aug 2024 10:06:20 -0400 Subject: [PATCH 06/28] ios expose constantsController #446 --- ios/core/Sources/Player/HeadlessPlayer.swift | 11 +++ .../Types/Core/ConstantsController.swift | 31 ++++++ ios/core/Tests/HeadlessPlayerTests.swift | 95 +++++++++++++++++++ ios/swiftui/Sources/SwiftUIPlayer.swift | 12 +++ 4 files changed, 149 insertions(+) create mode 100644 ios/core/Sources/Types/Core/ConstantsController.swift diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 59a337860..92f3ec8f0 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -137,6 +137,8 @@ public protocol HeadlessPlayer { var hooks: HooksType? { get } /// A logger reference for use in plugins to log through the shared player logger var logger: TapableLogger { get } + /// A reference to the Key/Value store for constants and context for player + var constantsController: ConstantsController? { get } /** Sets up the core javascript player in the given context @@ -166,6 +168,15 @@ public extension HeadlessPlayer { var state: BaseFlowState? { return jsPlayerReference?.getState() } + + var constantsController: ConstantsController? { + guard + let constantControllerJSValue = jsPlayerReference?.objectForKeyedSubscript("constantsController") + else { + return nil + } + return ConstantsController(constantsController: constantControllerJSValue) + } /** Sets up the core javascript player in the given context diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift new file mode 100644 index 000000000..ab45fd08e --- /dev/null +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -0,0 +1,31 @@ +import JavaScriptCore + +public class ConstantsController { + var constantsController: JSValue? + + public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { + if let fallbackValue = fallback { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) + return value?.toString() as? T + } else { + let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace]) + return value?.toString() as? T + } + } + + public func addConstants(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("addConstants", withArguments: [data, namespace]) + } + + public func setTemporaryValues(data: Any, namespace: String) -> Void { + self.constantsController?.invokeMethod("setTemporaryValues", withArguments: [data, namespace]) + } + + public func clearTemporaryValues() -> Void { + self.constantsController?.invokeMethod("clearTemporaryValues", withArguments: []) + } + + public init(constantsController: JSValue) { + self.constantsController = constantsController + } +} diff --git a/ios/core/Tests/HeadlessPlayerTests.swift b/ios/core/Tests/HeadlessPlayerTests.swift index 9299520e6..39e935f39 100644 --- a/ios/core/Tests/HeadlessPlayerTests.swift +++ b/ios/core/Tests/HeadlessPlayerTests.swift @@ -272,6 +272,101 @@ class HeadlessPlayerTests: XCTestCase { player.start(flow: FlowData.COUNTER) { _ in} wait(for: [updateExp], timeout: 1) } + + func testConstantsController() { + let player = HeadlessPlayerImpl(plugins: []) + + guard let constantsController = player.constantsController else { return } + + // Basic get/set tests + let data: Any = [ + "firstname": "john", + "lastname": "doe", + "favorite": [ + "color": "red" + ] + ] + + constantsController.addConstants(data: data, namespace: "constants") + + let firstname: String? = constantsController.getConstants(key: "firstname", namespace: "constants") + XCTAssertEqual(firstname, "john") + + let middleName: String? = constantsController.getConstants(key:"middlename", namespace: "constants") + XCTAssertEqual(middleName, "undefined") + + let middleNameSafe: String? = constantsController.getConstants(key:"middlename", namespace: "constants", fallback: "A") + XCTAssertEqual(middleNameSafe, "A") + + let favoriteColor: String? = constantsController.getConstants(key:"favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColor, "red") + + let nonExistantNamespace: String? = constantsController.getConstants(key:"test", namespace: "foo") + XCTAssertEqual(nonExistantNamespace, "undefined") + + let nonExistantNamespaceWithFallback: String? = constantsController.getConstants(key:"test", namespace: "foo", fallback: "B") + XCTAssertEqual(nonExistantNamespaceWithFallback, "B") + + // Test and make sure keys override properly + let newData: Any = [ + "favorite": [ + "color": "blue", + ], + ]; + + constantsController.addConstants(data: newData, namespace: "constants"); + + let newFavoriteColor: String? = constantsController.getConstants(key: "favorite.color", namespace:"constants") + XCTAssertEqual(newFavoriteColor, "blue") + } + + func testConstantsControllerTempValues() { + let player = HeadlessPlayerImpl(plugins: []) + + guard let constantsController = player.constantsController else { return } + + // Add initial constants + let data: Any = [ + "firstname": "john", + "lastname": "doe", + "favorite": [ + "color": "red" + ] + ] + constantsController.addConstants(data: data, namespace: "constants") + + // Override with temporary values + let tempData: Any = [ + "firstname": "jane", + "favorite": [ + "color": "blue" + ] + ] + constantsController.setTemporaryValues(data:tempData, namespace: "constants") + + // Test temporary override + let firstnameTemp: String? = constantsController.getConstants(key:"firstname", namespace: "constants") + XCTAssertEqual(firstnameTemp, "jane") + + let favoriteColorTemp: String? = constantsController.getConstants(key: "favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColorTemp, "blue") + + // Test fallback to original values when temporary values are not present + let lastnameTemp: String? = constantsController.getConstants(key: "lastname", namespace: "constants") + XCTAssertEqual(lastnameTemp, "doe") + + // Reset temp and values should be the same as the original data + constantsController.clearTemporaryValues(); + + let firstname: String? = constantsController.getConstants(key:"firstname", namespace: "constants") + XCTAssertEqual(firstname, "john") + + let favoriteColor: String? = constantsController.getConstants(key: "favorite.color", namespace: "constants") + XCTAssertEqual(favoriteColor, "red") + + let lastname: String? = constantsController.getConstants(key: "lastname", namespace: "constants") + XCTAssertEqual(lastname, "doe") + } } class FakePlugin: JSBasePlugin, NativePlugin { diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index d80c288ec..415c03792 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -205,6 +205,7 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { public var body: some View { bodyContent .environment(\.inProgressState, (state as? InProgressState)) + .environment(\.constantsController, constantsController) // forward results from our Context along to our result binding .onReceive(context.$result.debounce(for: 0.1, scheduler: RunLoop.main)) { self.result = $0 @@ -238,12 +239,23 @@ struct InProgressStateKey: EnvironmentKey { static var defaultValue: InProgressState? } +/// EnvironmentKey for storing `constantsController` +struct ConstantsControllerStateKey: EnvironmentKey { + /// The default value for `@Environment(\.constantsController)` + static var defaultValue: ConstantsController? = nil +} + public extension EnvironmentValues { /// The `InProgressState` of Player if it is in progress, and in scope var inProgressState: InProgressState? { get { self[InProgressStateKey.self] } set { self[InProgressStateKey.self] = newValue } } + + var constantsController: ConstantsController? { + get { self[ConstantsControllerStateKey.self] } + set { self[ConstantsControllerStateKey.self] = newValue } + } } internal extension SwiftUIPlayer { From 242542412d89579127a5c8cf734c0649876ec44f Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Mon, 12 Aug 2024 10:14:16 -0400 Subject: [PATCH 07/28] add docstrings to public funcs --- .../Types/Core/ConstantsController.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift index ab45fd08e..681d42205 100644 --- a/ios/core/Sources/Types/Core/ConstantsController.swift +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -2,7 +2,13 @@ import JavaScriptCore public class ConstantsController { var constantsController: JSValue? - + + /// Function to retrieve constants from the providers store + /// - Parameters: + /// - key: Key used for the store access + /// - namespace: Namespace values were loaded under + /// - fallback:Optional - if key doesn't exist in namespace what to return (will return unknown if not provided) + /// - Returns: Constant values from store public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { if let fallbackValue = fallback { let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) @@ -12,15 +18,24 @@ public class ConstantsController { return value?.toString() as? T } } - + + /// Function to add constants to the providers store + /// - Parameters: + /// - data: Values to add to the constants store + /// - namespace: Namespace values to be added under public func addConstants(data: Any, namespace: String) -> Void { self.constantsController?.invokeMethod("addConstants", withArguments: [data, namespace]) } - + + /// Function to set values to temporarily override certain keys in the perminant store + /// - Parameters: + /// - data: Values to override store with + /// - namespace: Namespace to override public func setTemporaryValues(data: Any, namespace: String) -> Void { self.constantsController?.invokeMethod("setTemporaryValues", withArguments: [data, namespace]) } + /// Clears any temporary values that were previously set public func clearTemporaryValues() -> Void { self.constantsController?.invokeMethod("clearTemporaryValues", withArguments: []) } From 729929ff084dd983074c4e35c62a9d956180a943 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Mon, 12 Aug 2024 10:21:51 -0400 Subject: [PATCH 08/28] fix return type and add tests --- ios/core/Sources/Types/Core/ConstantsController.swift | 4 ++-- ios/core/Tests/HeadlessPlayerTests.swift | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ios/core/Sources/Types/Core/ConstantsController.swift b/ios/core/Sources/Types/Core/ConstantsController.swift index 681d42205..cbd74c854 100644 --- a/ios/core/Sources/Types/Core/ConstantsController.swift +++ b/ios/core/Sources/Types/Core/ConstantsController.swift @@ -12,10 +12,10 @@ public class ConstantsController { public func getConstants(key: Any, namespace: String, fallback: Any? = nil) -> T? { if let fallbackValue = fallback { let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace, fallbackValue]) - return value?.toString() as? T + return value?.toObject() as? T } else { let value = self.constantsController?.invokeMethod("getConstants", withArguments: [key, namespace]) - return value?.toString() as? T + return value?.toObject() as? T } } diff --git a/ios/core/Tests/HeadlessPlayerTests.swift b/ios/core/Tests/HeadlessPlayerTests.swift index 39e935f39..26b4062e9 100644 --- a/ios/core/Tests/HeadlessPlayerTests.swift +++ b/ios/core/Tests/HeadlessPlayerTests.swift @@ -284,7 +284,8 @@ class HeadlessPlayerTests: XCTestCase { "lastname": "doe", "favorite": [ "color": "red" - ] + ], + "age": 1 ] constantsController.addConstants(data: data, namespace: "constants") @@ -293,16 +294,19 @@ class HeadlessPlayerTests: XCTestCase { XCTAssertEqual(firstname, "john") let middleName: String? = constantsController.getConstants(key:"middlename", namespace: "constants") - XCTAssertEqual(middleName, "undefined") + XCTAssertNil(middleName) let middleNameSafe: String? = constantsController.getConstants(key:"middlename", namespace: "constants", fallback: "A") XCTAssertEqual(middleNameSafe, "A") let favoriteColor: String? = constantsController.getConstants(key:"favorite.color", namespace: "constants") XCTAssertEqual(favoriteColor, "red") + + let age: Int? = constantsController.getConstants(key:"age", namespace: "constants") + XCTAssertEqual(age, 1) let nonExistantNamespace: String? = constantsController.getConstants(key:"test", namespace: "foo") - XCTAssertEqual(nonExistantNamespace, "undefined") + XCTAssertNil(nonExistantNamespace) let nonExistantNamespaceWithFallback: String? = constantsController.getConstants(key:"test", namespace: "foo", fallback: "B") XCTAssertEqual(nonExistantNamespaceWithFallback, "B") From da38906b9c6026f7e63f111a953df64b5d038a9f Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Wed, 14 Aug 2024 10:08:37 -0400 Subject: [PATCH 09/28] add comments --- ios/core/Sources/Player/HeadlessPlayer.swift | 1 + ios/swiftui/Sources/SwiftUIPlayer.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index 92f3ec8f0..4df8ef273 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -169,6 +169,7 @@ public extension HeadlessPlayer { return jsPlayerReference?.getState() } + /// The constants and context for player var constantsController: ConstantsController? { guard let constantControllerJSValue = jsPlayerReference?.objectForKeyedSubscript("constantsController") diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index 415c03792..13bb62ee0 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -251,7 +251,8 @@ public extension EnvironmentValues { get { self[InProgressStateKey.self] } set { self[InProgressStateKey.self] = newValue } } - + + /// The ConstantsController reference of Player var constantsController: ConstantsController? { get { self[ConstantsControllerStateKey.self] } set { self[ConstantsControllerStateKey.self] = newValue } From bceb781a12c60f3afcd8a8d43ec4419d0fd29ed3 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Wed, 14 Aug 2024 11:17:13 -0400 Subject: [PATCH 10/28] Android/JVM - expose constantController --- .../intuit/playerui/android/AndroidPlayer.kt | 11 +++ .../core/constants/ConstantsController.kt | 46 ++++++++++ .../playerui/core/player/HeadlessPlayer.kt | 3 + .../core/player/HeadlessPlayerTest.kt | 90 +++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt diff --git a/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt b/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt index 76ab00b2c..c2de7bb8d 100644 --- a/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt +++ b/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt @@ -18,6 +18,7 @@ import com.intuit.playerui.core.bridge.Completable import com.intuit.playerui.core.bridge.format import com.intuit.playerui.core.bridge.runtime.PlayerRuntimeConfig import com.intuit.playerui.core.bridge.serialization.format.registerContextualSerializer +import com.intuit.playerui.core.constants.ConstantsController import com.intuit.playerui.core.logger.TapableLogger import com.intuit.playerui.core.player.HeadlessPlayer import com.intuit.playerui.core.player.Player @@ -89,6 +90,16 @@ public class AndroidPlayer private constructor( override val logger: TapableLogger by player::logger + public fun AndroidPlayer.addConstants(data: Map, namespace: String) = player.constantsController.addConstants(data, namespace) + + public fun AndroidPlayer.getConstants(key: Any, namespace: String, fallback: Any? = null) = player.constantsController.getConstants(key, namespace) + + public fun AndroidPlayer.setTemporaryValues(data: Any, namespace: String) = player.constantsController.setTemporaryValues(data, namespace) + + public fun AndroidPlayer.clearTemporaryValues() = player.constantsController.clearTemporaryValues() + + public val constantsController: ConstantsController by player::constantsController + public class Hooks internal constructor(hooks: Player.Hooks) : Player.Hooks by hooks { public class ContextHook : SyncWaterfallHook<(HookContext, Context) -> Context, Context>() { public fun call(context: Context): Context = super.call( diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt new file mode 100644 index 000000000..8479e2e35 --- /dev/null +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt @@ -0,0 +1,46 @@ +package com.intuit.playerui.core.constants +import com.intuit.playerui.core.bridge.Node +import com.intuit.playerui.core.bridge.NodeWrapper +import com.intuit.playerui.core.bridge.getInvokable +import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer +import kotlinx.serialization.Serializable + +@Serializable(with = ConstantsController.Serializer::class) +public class ConstantsController(override val node: Node) : NodeWrapper { + /** + * Function to add constants to the providers store + * @param data values to add to the constants store + * @param namespace namespace to add the constants under + */ + public fun addConstants(data: Map, namespace: String) { + node.getInvokable("addConstants")?.invoke(data, namespace) + } + + /** + * Function to retrieve constants from the providers store + * @param key Key used for the store access + * @param namespace namespace values were loaded under (defined in the plugin) + * @param fallback Optional - if key doesn't exist in namespace what to return (will return unknown if not provided) + */ + public fun getConstants(key: Any, namespace: String, fallback: Any? = null): Any? { + return node.getInvokable("getConstants")?.invoke(key, namespace, fallback) + } + + /** + * Function to set values to temporarily override certain keys in the permanent store + * @param data values to override store with + * @param namespace namespace to override + */ + public fun setTemporaryValues(data: Any, namespace: String) { + node.getInvokable("setTemporaryValues")?.invoke(data, namespace) + } + + /** + * Clears any temporary values that were previously set + */ + public fun clearTemporaryValues() { + node.getInvokable("clearTemporaryValues")?.invoke() + } + + internal object Serializer : NodeWrapperSerializer(::ConstantsController) +} diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt index 04d239891..d7a7a4aa7 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt @@ -11,6 +11,7 @@ import com.intuit.playerui.core.bridge.runtime.ScriptContext import com.intuit.playerui.core.bridge.runtime.add import com.intuit.playerui.core.bridge.runtime.runtimeFactory import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField +import com.intuit.playerui.core.constants.ConstantsController import com.intuit.playerui.core.experimental.ExperimentalPlayerApi import com.intuit.playerui.core.logger.TapableLogger import com.intuit.playerui.core.player.HeadlessPlayer.Companion.bundledSource @@ -77,6 +78,8 @@ public constructor( override val hooks: Hooks by NodeSerializableField(Hooks.serializer(), NodeSerializableField.CacheStrategy.Full) + public val constantsController: ConstantsController by NodeSerializableField(ConstantsController.serializer(), NodeSerializableField.CacheStrategy.Full) + override val state: PlayerFlowState get() = if (player.isReleased()) { ReleasedState } else { diff --git a/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt b/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt index 74d043093..5391228ab 100644 --- a/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt +++ b/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt @@ -455,4 +455,94 @@ internal class HeadlessPlayerTest : PlayerTest(), ThreadUtils { assertTrue(player.state is ErrorState) } + + @TestTemplate + fun `test constantsController get and set`() = runBlockingTest { + player.start(simpleFlowString) + val constantsController = player.constantsController + + val data = mapOf( + "firstname" to "john", + "lastname" to "doe", + "favorite" to mapOf("color" to "red"), + "age" to 1, + ) + + constantsController.addConstants(data = data, namespace = "constants") + + val firstname = constantsController.getConstants(key = "firstname", namespace = "constants") + assertEquals("john", firstname) + + val middleName = constantsController.getConstants(key = "middlename", namespace = "constants") + assertNull(middleName) + + val middleNameSafe = constantsController.getConstants(key = "middlename", namespace = "constants", fallback = "A") + assertEquals("A", middleNameSafe) + + val favoriteColor = constantsController.getConstants(key = "favorite.color", namespace = "constants") + assertEquals("red", favoriteColor) + + val age = constantsController.getConstants(key = "age", namespace = "constants") + assertEquals(1, age) + + val nonExistentNamespace = constantsController.getConstants(key = "test", namespace = "foo") + assertNull(nonExistentNamespace) + + val nonExistentNamespaceWithFallback = constantsController.getConstants(key = "test", namespace = "foo", fallback = "B") + assertEquals("B", nonExistentNamespaceWithFallback) + + // Test and make sure keys override properly + val newData = mapOf( + "favorite" to mapOf("color" to "blue"), + ) + + constantsController.addConstants(data = newData, namespace = "constants") + + val newFavoriteColor = constantsController.getConstants(key = "favorite.color", namespace = "constants") + assertEquals("blue", newFavoriteColor) + } + + @TestTemplate + fun `test constantsController temp override functionality`() = runBlockingTest { + player.start(simpleFlowString) + val constantsController = player.constantsController + + // Add initial constants + val data = mapOf( + "firstname" to "john", + "lastname" to "doe", + "favorite" to mapOf("color" to "red"), + ) + constantsController.addConstants(data = data, namespace = "constants") + + // Override with temporary values + val tempData = mapOf( + "firstname" to "jane", + "favorite" to mapOf("color" to "blue"), + ) + constantsController.setTemporaryValues(data = tempData, namespace = "constants") + + // Test temporary override + val firstnameTemp = constantsController.getConstants(key = "firstname", namespace = "constants") + assertEquals("jane", firstnameTemp) + + val favoriteColorTemp = constantsController.getConstants(key = "favorite.color", namespace = "constants") + assertEquals("blue", favoriteColorTemp) + + // Test fallback to original values when temporary values are not present + val lastnameTemp = constantsController.getConstants(key = "lastname", namespace = "constants") + assertEquals("doe", lastnameTemp) + + // Reset temp and values should be the same as the original data + constantsController.clearTemporaryValues() + + val firstname = constantsController.getConstants(key = "firstname", namespace = "constants") + assertEquals("john", firstname) + + val favoriteColor = constantsController.getConstants(key = "favorite.color", namespace = "constants") + assertEquals("red", favoriteColor) + + val lastname = constantsController.getConstants(key = "lastname", namespace = "constants") + assertEquals("doe", lastname) + } } From 850bf529ee015b4d9f243197f81afdc17efe42f6 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Wed, 14 Aug 2024 11:43:36 -0400 Subject: [PATCH 11/28] fix test build --- .../com/intuit/playerui/core/constants/ConstantsController.kt | 2 +- .../kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt | 2 +- .../src/main/kotlin/com/intuit/playerui/core/player/Player.kt | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt index 8479e2e35..9a22a4417 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/constants/ConstantsController.kt @@ -22,7 +22,7 @@ public class ConstantsController(override val node: Node) : NodeWrapper { * @param namespace namespace values were loaded under (defined in the plugin) * @param fallback Optional - if key doesn't exist in namespace what to return (will return unknown if not provided) */ - public fun getConstants(key: Any, namespace: String, fallback: Any? = null): Any? { + public fun getConstants(key: String, namespace: String, fallback: Any? = null): Any? { return node.getInvokable("getConstants")?.invoke(key, namespace, fallback) } diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt index d7a7a4aa7..e49ceaed2 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt @@ -78,7 +78,7 @@ public constructor( override val hooks: Hooks by NodeSerializableField(Hooks.serializer(), NodeSerializableField.CacheStrategy.Full) - public val constantsController: ConstantsController by NodeSerializableField(ConstantsController.serializer(), NodeSerializableField.CacheStrategy.Full) + override val constantsController: ConstantsController by NodeSerializableField(ConstantsController.serializer(), NodeSerializableField.CacheStrategy.Full) override val state: PlayerFlowState get() = if (player.isReleased()) { ReleasedState diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/Player.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/Player.kt index 3cb518777..e13bfb750 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/Player.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/Player.kt @@ -6,6 +6,7 @@ import com.intuit.playerui.core.bridge.NodeWrapper import com.intuit.playerui.core.bridge.hooks.NodeSyncHook1 import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer +import com.intuit.playerui.core.constants.ConstantsController import com.intuit.playerui.core.data.DataController import com.intuit.playerui.core.experimental.ExperimentalPlayerApi import com.intuit.playerui.core.expressions.ExpressionController @@ -33,6 +34,8 @@ public abstract class Player : Pluggable { public abstract val logger: TapableLogger + public abstract val constantsController: ConstantsController + /** * Expose [PlayerHooks] which allow consumers to plug * into the flow and subscribe to different events. From f55e495ee9f2dad78be94616235033b6e4b8e824 Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Wed, 14 Aug 2024 12:28:39 -0400 Subject: [PATCH 12/28] remove start player in tests --- .../java/com/intuit/playerui/android/AndroidPlayer.kt | 10 ---------- .../intuit/playerui/core/player/HeadlessPlayerTest.kt | 2 -- 2 files changed, 12 deletions(-) diff --git a/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt b/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt index c2de7bb8d..cad5c5948 100644 --- a/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt +++ b/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt @@ -90,16 +90,6 @@ public class AndroidPlayer private constructor( override val logger: TapableLogger by player::logger - public fun AndroidPlayer.addConstants(data: Map, namespace: String) = player.constantsController.addConstants(data, namespace) - - public fun AndroidPlayer.getConstants(key: Any, namespace: String, fallback: Any? = null) = player.constantsController.getConstants(key, namespace) - - public fun AndroidPlayer.setTemporaryValues(data: Any, namespace: String) = player.constantsController.setTemporaryValues(data, namespace) - - public fun AndroidPlayer.clearTemporaryValues() = player.constantsController.clearTemporaryValues() - - public val constantsController: ConstantsController by player::constantsController - public class Hooks internal constructor(hooks: Player.Hooks) : Player.Hooks by hooks { public class ContextHook : SyncWaterfallHook<(HookContext, Context) -> Context, Context>() { public fun call(context: Context): Context = super.call( diff --git a/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt b/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt index 5391228ab..7bf1f925f 100644 --- a/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt +++ b/jvm/core/src/test/kotlin/com/intuit/playerui/core/player/HeadlessPlayerTest.kt @@ -458,7 +458,6 @@ internal class HeadlessPlayerTest : PlayerTest(), ThreadUtils { @TestTemplate fun `test constantsController get and set`() = runBlockingTest { - player.start(simpleFlowString) val constantsController = player.constantsController val data = mapOf( @@ -504,7 +503,6 @@ internal class HeadlessPlayerTest : PlayerTest(), ThreadUtils { @TestTemplate fun `test constantsController temp override functionality`() = runBlockingTest { - player.start(simpleFlowString) val constantsController = player.constantsController // Add initial constants From a2c46cc8fa920fa60ca420dad6062d6cfdbd218b Mon Sep 17 00:00:00 2001 From: Chloe Han Date: Wed, 14 Aug 2024 12:59:46 -0400 Subject: [PATCH 13/28] fix test build --- .../src/main/java/com/intuit/playerui/android/AndroidPlayer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt b/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt index cad5c5948..dfae5a8b0 100644 --- a/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt +++ b/android/player/src/main/java/com/intuit/playerui/android/AndroidPlayer.kt @@ -90,6 +90,8 @@ public class AndroidPlayer private constructor( override val logger: TapableLogger by player::logger + override val constantsController: ConstantsController by player::constantsController + public class Hooks internal constructor(hooks: Player.Hooks) : Player.Hooks by hooks { public class ContextHook : SyncWaterfallHook<(HookContext, Context) -> Context, Context>() { public fun call(context: Context): Context = super.call( From 2a63f454df60029bcb4f5147b03b56a9098ca65c Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Fri, 16 Aug 2024 21:20:08 -0400 Subject: [PATCH 14/28] Changed logic and updated test cases for android --- MODULE.bazel | 2 +- ios/core/Sources/Types/Hooks/Hook.swift | 44 ++++ .../intuit/playerui/core/bridge/Promise.kt | 2 +- .../hooks/NodeAsyncParallelBailHook2.kt | 2 +- .../playerui/core/bridge/hooks/NodeHook.kt | 2 +- .../ios/Sources/AsyncNodePlugin.swift | 22 +- .../plugins/asyncnode/AsyncNodePlugin.kt | 10 +- .../plugins/asyncnode/AsyncNodePluginTest.kt | 196 ++++++++++++++---- 8 files changed, 217 insertions(+), 63 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 33863e917..e751b1897 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,7 +21,7 @@ bazel_dep(name = "rules_pkg", version = "1.0.1") bazel_dep(name = "aspect_rules_ts", version = "2.4.2") # C++ -bazel_dep(name = "rules_foreign_cc", version = "0.10.1") +bazel_dep(name = "rules_foreign_cc", version = "0.11.1") bazel_dep(name = "googletest", version = "1.14.0") ####### Node.js version ######### diff --git a/ios/core/Sources/Types/Hooks/Hook.swift b/ios/core/Sources/Types/Hooks/Hook.swift index 618117620..967ccf59a 100644 --- a/ios/core/Sources/Types/Hooks/Hook.swift +++ b/ios/core/Sources/Types/Hooks/Hook.swift @@ -176,3 +176,47 @@ public class AsyncHook: BaseJSHook where T: CreatedFromJSValue { self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } + +/** + This class represents an object in the JS runtime that can be tapped into + and returns a promise that resolves when the asynchronous task is completed + */ +public class AsyncHook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFromJSValue { + private var handler: AsyncHookHandler? + + public typealias AsyncHookHandler = (T, U) async throws -> JSValue? + + /** + Attach a closure to the hook, so when the hook is fired in the JS runtime + we receive the event in the native runtime + + - parameters: + - hook: A function to run when the JS hook is fired + */ + public func tap(_ hook: @escaping AsyncHookHandler) { + let tapMethod: @convention(block) (JSValue?,JSValue?) -> JSValue = { value, value2 in + guard + let val = value, + let val2 = value2, + let hookValue = T.createInstance(value: val) as? T, + let hookValue2 = U.createInstance(value: val2) as? U + else { return JSValue() } + + + let promise = + JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in + Task { + let result = try await hook(hookValue, hookValue2) + DispatchQueue.main.async { + resolve(result as Any) + } + } + }) + + return promise ?? JSValue() + } + + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) + } +} + diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/Promise.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/Promise.kt index b23e2441c..3ad7f659c 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/Promise.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/Promise.kt @@ -178,7 +178,7 @@ public val Runtime<*>.Promise: Promise.Api get() = getObject("Promise")?.let { p } ?: throw PlayerRuntimeException("'Promise' not defined in runtime") /** Helper to bridge complex [Promise] logic with the JS promise constructor */ -public fun Runtime<*>.Promise(block: suspend ((T) -> Unit, (Throwable) -> Unit) -> Unit): Promise { +public fun Runtime<*>.Promise(block: suspend ((T) -> Unit, (Throwable) -> Unit) -> Unit): Promise { val key = "promiseHandler_${UUID.randomUUID().toString().replace("-", "")}" add(key) { resolve: Invokable, reject: Invokable -> runtime.scope.launch { diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt index 20fbf5113..720dbe1b7 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.Serializable @OptIn(ExperimentalCoroutinesApi::class) @Serializable(with = NodeAsyncParallelBailHook2.Serializer::class) -public class NodeAsyncParallelBailHook2( +public class NodeAsyncParallelBailHook2( override val node: Node, serializer1: KSerializer, serializer2: KSerializer diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeHook.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeHook.kt index 0d344e04c..26d940af4 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeHook.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeHook.kt @@ -39,7 +39,7 @@ internal interface NodeHook : NodeWrapper { fun call(context: HookContext, serializedArgs: Array): R } -internal interface AsyncNodeHook : NodeHook { +internal interface AsyncNodeHook : NodeHook { override fun call(context: HookContext, serializedArgs: Array): Promise = node.runtime.Promise { resolve, reject -> val result = callAsync(context, serializedArgs) resolve(result) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index 5ab9b05a1..16c5cedec 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -47,10 +47,11 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { super.setup(context: context) if let pluginRef = pluginRef { - self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook(baseValue: pluginRef, name: "onAsyncNode")) + self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode")) } - hooks?.onAsyncNode.tap({ node in + hooks?.onAsyncNode.tap({ node, callback in + print("Value of callback \(callback)") // hook value is the original node guard let asyncHookHandler = self.asyncHookHandler else { return JSValue() @@ -93,19 +94,6 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { 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 - } - - } - - return result }) } @@ -137,7 +125,7 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { } public struct AsyncNodeHook { - public let onAsyncNode: AsyncHook + public let onAsyncNode: AsyncHook2 } /** @@ -204,4 +192,4 @@ public class AsyncNodePluginPlugin: JSBasePlugin { ) #endif } -} \ No newline at end of file +} diff --git a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt index 9a51d0e5c..c183c5088 100644 --- a/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt +++ b/plugins/async-node/jvm/src/main/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePlugin.kt @@ -18,8 +18,10 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.serializer +public typealias asyncNodeUpdate = List>? public class AsyncNodePlugin : JSScriptPluginWrapper(pluginName, sourcePath = bundledSourcePath) { public lateinit var hooks: Hooks @@ -34,15 +36,15 @@ public class AsyncNodePlugin : JSScriptPluginWrapper(pluginName, sourcePath = bu @Serializable(with = Hooks.Serializer::class) public class Hooks internal constructor(override val node: Node) : NodeWrapper { /** The hook right before the View starts resolving. Attach anything custom here */ - public val onAsyncNode: NodeAsyncParallelBailHook2) -> Unit, List>> by + public val onAsyncNode: NodeAsyncParallelBailHook2 Unit, asyncNodeUpdate> by NodeSerializableField( NodeAsyncParallelBailHook2.serializer( NodeSerializer(), Function1Serializer( - MapSerializer(String.serializer(), GenericSerializer()), + ListSerializer(MapSerializer(String.serializer(), GenericSerializer())).nullable, GenericSerializer(), - ) as KSerializer<(Map) -> Unit>, - ListSerializer(MapSerializer(String.serializer(), GenericSerializer())), + ) as KSerializer<(asyncNodeUpdate) -> Unit>, + ListSerializer(MapSerializer(String.serializer(), GenericSerializer())).nullable, ), ) diff --git a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt index a63d498bf..3748ea785 100644 --- a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt +++ b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt @@ -15,7 +15,11 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) internal class AsyncNodePluginTest : PlayerTest() { - val asyncNodeFlowSimple = + // TODO: This typing is not great - need to go fix hook types + private var deferredResolve: ((asyncNodeUpdate) -> Unit)? = null; + private var updateContent: ((asyncNodeUpdate) -> Unit)? = null; + + private val asyncNodeFlowSimple = """{ "id": "counter-flow", "views": [ @@ -231,61 +235,177 @@ internal class AsyncNodePluginTest : PlayerTest() { @TestTemplate fun `handle multiple updates through callback mechanism`() = runBlockingTest { - var update: Asset? = null - var updateCount = 0 - plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> - when (updateCount) { - 0 -> { - callback(mapOf("asset" to mapOf("id" to "asset-1", "type" to "text", "value" to "First update"))) + plugin.hooks.onAsyncNode.tap("") { _, node, callback -> + updateContent = callback + val result = suspendCoroutine { cont -> + deferredResolve = { value -> + cont.resume(value) } + } - 1 -> { - callback(mapOf("asset" to mapOf("id" to "asset-2", "type" to "text", "value" to "Second update"))) - } + BailResult.Bail(result) + } - 2 -> { - callback(mapOf("asset" to mapOf("id" to "asset-3", "type" to "text", "value" to "Third update"))) - } + var viewUpdateContinuation: Continuation? = null + var count = 0 + + player.hooks.view.tap { v -> + v?.hooks?.onUpdate?.tap { asset -> + count++ + println("Update after callback undefined node $count: $asset") // Debug statement + viewUpdateContinuation?.resume(asset) + viewUpdateContinuation = null } - updateCount++ - BailResult.Bail(emptyList()) } - var count = 0 + player.start(asyncNodeFlowSimple) + + var view = player.inProgressState?.lastViewUpdate + Assertions.assertNotNull(view); + Assertions.assertEquals( + "action", + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals(1, view.getList("actions")?.size) + + while (true) { + if (deferredResolve != null) + break + + yield() + } + suspendCancellableCoroutine { cont -> - player.hooks.view.tap { v -> - v?.hooks?.onUpdate?.tap { asset -> - count++ - update = asset - if (count == 3) cont.resume(true) {} + viewUpdateContinuation = cont + + val updates = listOf( + mapOf( + "asset" to mapOf( + "id" to "next-label-action-1", + "type" to "action", + "value" to "dummy value 1" + ) + ), + mapOf( + "asset" to mapOf( + "id" to "next-label-action-2", + "type" to "action", + "value" to "dummy value 2" + ) + ), + mapOf( + "asset" to mapOf( + "id" to "next-label-action-3", + "type" to "action", + "value" to "dummy value 3" + ) + ) + ) + + runBlocking { + for (update in updates) { + deferredResolve?.invoke(listOf(update)) + yield() // Allow coroutine to be suspended and resumed } } - player.start(asyncNodeFlowSimple) } - Assertions.assertTrue(count == 3) - Assertions.assertTrue((update?.get("actions") as List<*>).isNotEmpty()) + Assertions.assertEquals(1, count) + + view = player.inProgressState?.lastViewUpdate + + Assertions.assertNotNull(view); + Assertions.assertEquals( + "action", + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals( + "action", + view.getList("actions")?.filterIsInstance()?.get(1)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals(2, view.getList("actions")?.size) + + updateContent!!.invoke(null) + + Assertions.assertEquals(3, count) + view = player.inProgressState?.lastViewUpdate + + Assertions.assertNotNull(view); + Assertions.assertEquals( + "action", + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals(1, view.getList("actions")?.size) } @TestTemplate - fun `handle undefined node`() = runBlockingTest { - var update: Asset? = null - plugin?.hooks?.onAsyncNode?.tap("") { _, node, callback -> - BailResult.Bail(emptyList()) + fun `handle null node`() = runBlockingTest { + + plugin.hooks.onAsyncNode.tap("") { _, node, callback -> + + updateContent = callback + val result = suspendCoroutine { cont -> + deferredResolve = { value -> + cont.resume(value) + } + } + + BailResult.Bail(result) } var count = 0 - suspendCancellableCoroutine { cont -> - player.hooks.view.tap { v -> - v?.hooks?.onUpdate?.tap { asset -> - count++ - update = asset - if (count == 2) cont.resume(true) {} - } + player.hooks.view.tap { v -> + v?.hooks?.onUpdate?.tap { asset -> + count++ + println("Update after callback undefined node $count: $asset") // Debug statement + viewUpdateContinuation?.resume(asset) + viewUpdateContinuation = null } - player.start(asyncNodeFlowSimple) } - Assertions.assertTrue(count == 2) - Assertions.assertTrue((update?.get("actions") as List<*>).isNotEmpty()) + + player.start(asyncNodeFlowSimple) + + var view = player.inProgressState?.lastViewUpdate + + Assertions.assertNotNull(view); + Assertions.assertEquals( + "action", + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals(1, view.getList("actions")?.size) + + while (true) { + + if (deferredResolve != null) + break + + yield() + } + + deferredResolve!!.invoke(null) + + Assertions.assertEquals(1, count) + + view = player.inProgressState?.lastViewUpdate + + Assertions.assertNotNull(view); + Assertions.assertEquals( + "action", + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals(1, view.getList("actions")?.size) + + updateContent!!.invoke(null) + + Assertions.assertEquals(1, count) + + view = player.inProgressState?.lastViewUpdate + + Assertions.assertNotNull(view); + Assertions.assertEquals( + "action", + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + ) + Assertions.assertEquals(1, view.getList("actions")?.size) } } From 691749196a68e7fedfade8be8ac931f085c95698 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Mon, 19 Aug 2024 16:46:05 -0400 Subject: [PATCH 15/28] Updated 'handle multiple updates through callback mechanism' --- .../plugins/asyncnode/AsyncNodePluginTest.kt | 100 +++++++----------- 1 file changed, 41 insertions(+), 59 deletions(-) diff --git a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt index 3748ea785..b0455d133 100644 --- a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt +++ b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt @@ -8,17 +8,17 @@ import io.mockk.junit5.MockKExtension import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.yield import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.TestTemplate import org.junit.jupiter.api.extension.ExtendWith +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine @ExtendWith(MockKExtension::class) internal class AsyncNodePluginTest : PlayerTest() { - // TODO: This typing is not great - need to go fix hook types - private var deferredResolve: ((asyncNodeUpdate) -> Unit)? = null; - private var updateContent: ((asyncNodeUpdate) -> Unit)? = null; - private val asyncNodeFlowSimple = """{ "id": "counter-flow", @@ -235,6 +235,11 @@ internal class AsyncNodePluginTest : PlayerTest() { @TestTemplate fun `handle multiple updates through callback mechanism`() = runBlockingTest { + // TODO: This typing is not great - need to go fix hook types + var deferredResolve: ((asyncNodeUpdate) -> Unit)? = null + var updateContent: ((asyncNodeUpdate) -> Unit)? = null + + var count = 0 plugin.hooks.onAsyncNode.tap("") { _, node, callback -> updateContent = callback @@ -243,107 +248,82 @@ internal class AsyncNodePluginTest : PlayerTest() { cont.resume(value) } } - BailResult.Bail(result) } var viewUpdateContinuation: Continuation? = null - var count = 0 - player.hooks.view.tap { v -> v?.hooks?.onUpdate?.tap { asset -> count++ - println("Update after callback undefined node $count: $asset") // Debug statement viewUpdateContinuation?.resume(asset) viewUpdateContinuation = null } } - player.start(asyncNodeFlowSimple) var view = player.inProgressState?.lastViewUpdate - Assertions.assertNotNull(view); + Assertions.assertNotNull(view) Assertions.assertEquals( "action", - view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type"), ) Assertions.assertEquals(1, view.getList("actions")?.size) while (true) { - if (deferredResolve != null) + if (deferredResolve != null) { break + } yield() } - suspendCancellableCoroutine { cont -> - viewUpdateContinuation = cont - - val updates = listOf( - mapOf( - "asset" to mapOf( - "id" to "next-label-action-1", - "type" to "action", - "value" to "dummy value 1" - ) - ), - mapOf( - "asset" to mapOf( - "id" to "next-label-action-2", - "type" to "action", - "value" to "dummy value 2" - ) - ), - mapOf( - "asset" to mapOf( - "id" to "next-label-action-3", - "type" to "action", - "value" to "dummy value 3" - ) - ) - ) + // create a view object to pass it to the deferred resolve + val viewObject = mapOf( + "asset" to mapOf( + "id" to "asset-1", + "type" to "action", + "value" to "New asset!", + ), + ) - runBlocking { - for (update in updates) { - deferredResolve?.invoke(listOf(update)) - yield() // Allow coroutine to be suspended and resumed - } - } - } + deferredResolve!!.invoke(listOf(viewObject)) Assertions.assertEquals(1, count) view = player.inProgressState?.lastViewUpdate - Assertions.assertNotNull(view); + Assertions.assertNotNull(view) Assertions.assertEquals( "action", - view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type"), ) Assertions.assertEquals( "action", - view.getList("actions")?.filterIsInstance()?.get(1)?.getObject("asset")?.get("type") + view.getList("actions")?.filterIsInstance()?.get(1)?.getObject("asset")?.get("type"), ) Assertions.assertEquals(2, view.getList("actions")?.size) + Assertions.assertEquals(2, count) updateContent!!.invoke(null) Assertions.assertEquals(3, count) view = player.inProgressState?.lastViewUpdate - Assertions.assertNotNull(view); + Assertions.assertNotNull(view) Assertions.assertEquals( "action", - view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type"), ) Assertions.assertEquals(1, view.getList("actions")?.size) } @TestTemplate fun `handle null node`() = runBlockingTest { + // TODO: This typing is not great - need to go fix hook types + var deferredResolve: ((asyncNodeUpdate) -> Unit)? = null + var updateContent: ((asyncNodeUpdate) -> Unit)? = null plugin.hooks.onAsyncNode.tap("") { _, node, callback -> - updateContent = callback val result = suspendCoroutine { cont -> deferredResolve = { value -> @@ -353,6 +333,8 @@ internal class AsyncNodePluginTest : PlayerTest() { BailResult.Bail(result) } + + var viewUpdateContinuation: Continuation? = null var count = 0 player.hooks.view.tap { v -> v?.hooks?.onUpdate?.tap { asset -> @@ -367,17 +349,17 @@ internal class AsyncNodePluginTest : PlayerTest() { var view = player.inProgressState?.lastViewUpdate - Assertions.assertNotNull(view); + Assertions.assertNotNull(view) Assertions.assertEquals( "action", - view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type"), ) Assertions.assertEquals(1, view.getList("actions")?.size) while (true) { - - if (deferredResolve != null) + if (deferredResolve != null) { break + } yield() } @@ -388,10 +370,10 @@ internal class AsyncNodePluginTest : PlayerTest() { view = player.inProgressState?.lastViewUpdate - Assertions.assertNotNull(view); + Assertions.assertNotNull(view) Assertions.assertEquals( "action", - view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type"), ) Assertions.assertEquals(1, view.getList("actions")?.size) @@ -401,10 +383,10 @@ internal class AsyncNodePluginTest : PlayerTest() { view = player.inProgressState?.lastViewUpdate - Assertions.assertNotNull(view); + Assertions.assertNotNull(view) Assertions.assertEquals( "action", - view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type") + view!!.getList("actions")?.filterIsInstance()?.get(0)?.getObject("asset")?.get("type"), ) Assertions.assertEquals(1, view.getList("actions")?.size) } From de6df571fb96c7cba9ba392be1e3839a0e378b5e Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Wed, 21 Aug 2024 01:26:24 -0400 Subject: [PATCH 16/28] updated async-node ios version and fixed review comments --- .../ios/Sources/AsyncNodePlugin.swift | 109 ++++----- .../ios/Tests/AsynNodePluginTests.swift | 217 ++++++++---------- .../plugins/asyncnode/AsyncNodePluginTest.kt | 11 +- 3 files changed, 151 insertions(+), 186 deletions(-) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index 16c5cedec..997d19740 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -12,12 +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) + case emptyNode } /** @@ -43,58 +43,61 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { self.plugins = plugins } - override public func setup(context: JSContext) { - super.setup(context: context) +func handleAsyncNodeReplacement(_ replacementNode: AsyncNodeHandlerType, context: JSContext) -> JSValue? { + 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 { + return nil + } + case .concrete(let jsValue): + return jsValue + } + + case .emptyNode: + return nil + } + } - if let pluginRef = pluginRef { - self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode")) - } + override public func setup(context: JSContext) { + super.setup(context: context) + + if let pluginRef = pluginRef { + self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode")) + } + + hooks?.onAsyncNode.tap({ node, callback in + print("Value of callback \(callback)") + // hook value is the original node + guard let asyncHookHandler = self.asyncHookHandler else { + return JSValue() + } - hooks?.onAsyncNode.tap({ node, callback in - print("Value of callback \(callback)") - // 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 { - 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 - } - - }) + let replacementNode = try await (asyncHookHandler)(node) + return self.handleAsyncNodeReplacement(replacementNode, context: context) ?? JSValue() + }) } /** @@ -125,7 +128,7 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { } public struct AsyncNodeHook { - public let onAsyncNode: AsyncHook2 + public let onAsyncNode: AsyncHook2 } /** diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index 521c72119..965c66db9 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -1,3 +1,10 @@ +// +// AsyncNodePluginTests.swift +// PlayerUI +// +// Created by Zhao Xia Wu on 2024-02-05. +// + import Foundation import XCTest import SwiftUI @@ -29,6 +36,7 @@ class AsyncNodePluginTests: XCTestCase { XCTAssertNotNil(plugin.pluginRef) } + func testAsyncNodeWithAnotherAsyncNodeDelay() { let handlerExpectation = XCTestExpectation(description: "first data did not change") @@ -285,165 +293,124 @@ class AsyncNodePluginTests: XCTestCase { XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") } - func testAsyncNodeWithNullNode() { - let handlerExpectation = XCTestExpectation(description: "null node handled") - + func handleEmptyNode() { let context = JSContext() - - let resolveHandler: AsyncHookHandler = { node in - handlerExpectation.fulfill() - XCTAssertNil(node) - return .singleNode(.concrete(JSValue())) + let plugin = AsyncNodePlugin { _ in + return .emptyNode } - - let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolveHandler) - plugin.context = context - XCTAssertNotNil(asyncNodePluginPlugin.context) + let result = plugin.handleAsyncNodeReplacement(.emptyNode, context: context!) + XCTAssertNil(result) + } - let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext()) +func handleReplacement(plugin: AsyncNodePlugin, context: JSContext, count: Int) -> AsyncNodeHandlerType { + if count == 1 { + return .multiNode([ + ReplacementNode.encodable(AssetPlaceholderNode(asset: PlaceholderNode(id: "text", type: "text", value: "new node from the hook 1"))), + ReplacementNode.encodable(AsyncNode(id: "id")) + ]) + } else if count == 2 { + return .singleNode(.concrete(context.evaluateScript(""" + ( + {"asset": {"id": "text", "type": "text", "value":"new node from the hook 2"}} + ) + """) ?? JSValue())) + } else { + return .singleNode(.concrete(context.evaluateScript("") ?? JSValue())) + } +} - player.start(flow: .asyncNodeJson, completion: {_ in}) +func testHandleMultipleUpdatesThroughCallback() { + let handlerExpectation = XCTestExpectation(description: "first data did not change") - wait(for: [handlerExpectation], timeout: 5) + guard let context = JSContext() else { + XCTFail("JSContext initialization failed") + return } - func testAsyncNodeWithUndefinedNode() { - let handlerExpectation = XCTestExpectation(description: "undefined node handled") + var count = 0 + var args: [JSValue] = [] + var callbackFunction: JSValue? - let context = JSContext() + let resolve: AsyncHookHandler = { callback in + handlerExpectation.fulfill() + callbackFunction = callback - let resolveHandler: AsyncHookHandler = { node in - handlerExpectation.fulfill() - XCTAssertNil(node) + let plugin = AsyncNodePlugin { _ in return .singleNode(.concrete(JSValue())) } - - let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolveHandler) - plugin.context = context - XCTAssertNotNil(asyncNodePluginPlugin.context) - - let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext()) + let replacementResult = handleReplacement(plugin: plugin, context: context, count: count) + args = [plugin.handleAsyncNodeReplacement(replacementResult, context: context) ?? JSValue()] - player.start(flow: .asyncNodeJson, completion: {_ in}) - - wait(for: [handlerExpectation], timeout: 5) + return callback.call(withArguments: args) } - 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())) - } - - let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) + let asyncNodePluginPlugin = AsyncNodePluginPlugin() + let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) - plugin.context = context + plugin.context = context - XCTAssertNotNil(asyncNodePluginPlugin.context) + var handleFunc = plugin.handleAsyncNodeReplacement(.emptyNode, context: context) - let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext()) + XCTAssertNotNil(asyncNodePluginPlugin.context) - let textExpectation = XCTestExpectation(description: "newText found") - let textExpectation2 = XCTestExpectation(description: "newText found") - let textExpectation3 = XCTestExpectation(description: "newText found") + let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) - var expectedMultiNode1Text: String = "" - var expectedMultiNode2Text: String = "" - var expectedMultiNode3Text: String = "" + let textExpectation = XCTestExpectation(description: "newText found") + let textExpectation2 = XCTestExpectation(description: "newText found") - player.hooks?.viewController.tap({ (viewController) in - viewController.hooks.view.tap { (view) in - view.hooks.onUpdate.tap { val in - count += 1 + var expectedMultiNode1Text: String = "" + var expectedMultiNode2Text: String = "" - 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") } + player.hooks?.viewController.tap({ (viewController) in + viewController.hooks.view.tap { (view) in + view.hooks.onUpdate.tap { val in + count += 1 - expectedMultiNode1Text = textString1 - textExpectation.fulfill() - } + 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") } - 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() - } - } + expectedMultiNode1Text = textString1 + textExpectation.fulfill() } - }) - player.start(flow: .asyncNodeJson, completion: { _ in}) + 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") } - wait(for: [handlerExpectation, textExpectation], timeout: 5) + expectedMultiNode2Text = textString2 + textExpectation2.fulfill() + } + } + } + }) - XCTAssert(count == 2) - XCTAssertEqual(expectedMultiNode1Text, "1st value in the multinode") + player.start(flow: .asyncNodeJson, completion: { _ in}) - wait(for: [textExpectation2], timeout: 6) + wait(for: [handlerExpectation, textExpectation], timeout: 5) - XCTAssert(count == 3) - XCTAssertEqual(expectedMultiNode2Text, "2nd value in the multinode") + callbackFunction(handleFunc) + + XCTAssert(count == 2) + XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") - wait(for: [textExpectation3], timeout: 7) + wait(for: [textExpectation2], timeout: 5) - XCTAssert(count == 4) - XCTAssertEqual(expectedMultiNode3Text, "3rd value in the multinode") - } + XCTAssert(count == 3) + XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") +} } extension String { @@ -510,4 +477,4 @@ struct PlaceholderNode: Codable, Equatable, AssetData { self.type = type self.value = value } -} \ No newline at end of file +} diff --git a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt index b0455d133..8b15416b3 100644 --- a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt +++ b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt @@ -2,6 +2,9 @@ package com.intuit.playerui.plugins.asyncnode import com.intuit.hooks.BailResult import com.intuit.playerui.core.asset.Asset +import com.intuit.playerui.core.bridge.Node +import com.intuit.playerui.core.player.state.inProgressState +import com.intuit.playerui.core.player.state.lastViewUpdate import com.intuit.playerui.utils.test.PlayerTest import com.intuit.playerui.utils.test.runBlockingTest import io.mockk.junit5.MockKExtension @@ -12,7 +15,6 @@ import kotlinx.coroutines.yield import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.TestTemplate import org.junit.jupiter.api.extension.ExtendWith -import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -251,12 +253,9 @@ internal class AsyncNodePluginTest : PlayerTest() { BailResult.Bail(result) } - var viewUpdateContinuation: Continuation? = null player.hooks.view.tap { v -> v?.hooks?.onUpdate?.tap { asset -> count++ - viewUpdateContinuation?.resume(asset) - viewUpdateContinuation = null } } player.start(asyncNodeFlowSimple) @@ -334,14 +333,10 @@ internal class AsyncNodePluginTest : PlayerTest() { BailResult.Bail(result) } - var viewUpdateContinuation: Continuation? = null var count = 0 player.hooks.view.tap { v -> v?.hooks?.onUpdate?.tap { asset -> count++ - println("Update after callback undefined node $count: $asset") // Debug statement - viewUpdateContinuation?.resume(asset) - viewUpdateContinuation = null } } From d252411db3ae2e9e297a5b08a15340cffaa84edc Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Wed, 21 Aug 2024 14:36:59 -0400 Subject: [PATCH 17/28] Fixed ios tests --- .../async-node/ios/Sources/AsyncNodePlugin.swift | 4 ---- .../async-node/ios/Tests/AsynNodePluginTests.swift | 13 +++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index 997d19740..a0e1b98ff 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -7,10 +7,7 @@ import Foundation import JavaScriptCore - -#if SWIFT_PACKAGE import PlayerUI -#endif public typealias AsyncHookHandler = (JSValue) async throws -> AsyncNodeHandlerType @@ -89,7 +86,6 @@ func handleAsyncNodeReplacement(_ replacementNode: AsyncNodeHandlerType, context } hooks?.onAsyncNode.tap({ node, callback in - print("Value of callback \(callback)") // hook value is the original node guard let asyncHookHandler = self.asyncHookHandler else { return JSValue() diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index 965c66db9..d72c458ff 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -342,10 +342,11 @@ func testHandleMultipleUpdatesThroughCallback() { } plugin.context = context - let replacementResult = handleReplacement(plugin: plugin, context: context, count: count) + let replacementResult = self.handleReplacement(plugin: plugin, context: context, count: count) args = [plugin.handleAsyncNodeReplacement(replacementResult, context: context) ?? JSValue()] - return callback.call(withArguments: args) + callback.call(withArguments: args) + return replacementResult } let asyncNodePluginPlugin = AsyncNodePluginPlugin() @@ -353,8 +354,6 @@ func testHandleMultipleUpdatesThroughCallback() { plugin.context = context - var handleFunc = plugin.handleAsyncNodeReplacement(.emptyNode, context: context) - XCTAssertNotNil(asyncNodePluginPlugin.context) let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) @@ -401,10 +400,12 @@ func testHandleMultipleUpdatesThroughCallback() { wait(for: [handlerExpectation, textExpectation], timeout: 5) - callbackFunction(handleFunc) - XCTAssert(count == 2) XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") + + if let callbackFunction = callbackFunction { + callbackFunction.call(withArguments: args) + } wait(for: [textExpectation2], timeout: 5) From 04f3573f74a1b1aa5cb2cee51846b1b6dbf4c645 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Thu, 22 Aug 2024 14:21:57 -0400 Subject: [PATCH 18/28] Removed callback from testHandleMultipleUpdatesThroughCallback --- plugins/async-node/ios/Tests/AsynNodePluginTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index d72c458ff..82974aca5 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -345,7 +345,6 @@ func testHandleMultipleUpdatesThroughCallback() { let replacementResult = self.handleReplacement(plugin: plugin, context: context, count: count) args = [plugin.handleAsyncNodeReplacement(replacementResult, context: context) ?? JSValue()] - callback.call(withArguments: args) return replacementResult } @@ -402,7 +401,7 @@ func testHandleMultipleUpdatesThroughCallback() { XCTAssert(count == 2) XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") - + if let callbackFunction = callbackFunction { callbackFunction.call(withArguments: args) } From 91359bc663521ee4b735438b42e55b5429d751c7 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Thu, 22 Aug 2024 19:17:28 -0400 Subject: [PATCH 19/28] Fixed review comments and updated ios test cases --- .../ios/Sources/AsyncNodePlugin.swift | 11 ++- .../ios/Tests/AsynNodePluginTests.swift | 92 ++++++++++--------- .../AsynNodePluginViewInspectorTests.swift | 2 +- 3 files changed, 59 insertions(+), 46 deletions(-) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index a0e1b98ff..b75b50de8 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -9,7 +9,7 @@ import Foundation import JavaScriptCore import PlayerUI -public typealias AsyncHookHandler = (JSValue) async throws -> AsyncNodeHandlerType +public typealias AsyncHookHandler = (JSValue, JSValue) async throws -> AsyncNodeHandlerType public enum AsyncNodeHandlerType { case multiNode([ReplacementNode]) @@ -40,7 +40,10 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { self.plugins = plugins } -func handleAsyncNodeReplacement(_ replacementNode: AsyncNodeHandlerType, context: JSContext) -> JSValue? { +func handleAsyncNodeReplacement(_ replacementNode: AsyncNodeHandlerType) -> JSValue? { + guard let context = context else { + return JSValue() + } switch replacementNode { case .multiNode(let replacementNodes): let jsValueArray = replacementNodes.compactMap { node in @@ -91,8 +94,8 @@ func handleAsyncNodeReplacement(_ replacementNode: AsyncNodeHandlerType, context return JSValue() } - let replacementNode = try await (asyncHookHandler)(node) - return self.handleAsyncNodeReplacement(replacementNode, context: context) ?? JSValue() + let replacementNode = try await (asyncHookHandler)(node, callback) + return self.handleAsyncNodeReplacement(replacementNode) ?? JSValue() }) } diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index 82974aca5..cd5e310a6 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -19,7 +19,7 @@ class AsyncNodePluginTests: XCTestCase { func testConstructionAsyncPlugin() { let context = JSContext() - let plugin = AsyncNodePlugin { _ in + let plugin = AsyncNodePlugin { _,_ in return .singleNode(.concrete(JSValue())) } plugin.context = context @@ -44,7 +44,7 @@ class AsyncNodePluginTests: XCTestCase { var count = 0 - let resolveHandler: AsyncHookHandler = { _ in + let resolveHandler: AsyncHookHandler = { _,_ in handlerExpectation.fulfill() sleep(3) @@ -102,7 +102,7 @@ class AsyncNodePluginTests: XCTestCase { let context = JSContext() var count = 0 - let resolve: AsyncHookHandler = { _ in + let resolve: AsyncHookHandler = { _,_ in handlerExpectation.fulfill() if count == 1 { @@ -213,7 +213,7 @@ class AsyncNodePluginTests: XCTestCase { var count = 0 - let resolve: AsyncHookHandler = { _ in + let resolve: AsyncHookHandler = { _,_ in handlerExpectation.fulfill() if count == 1 { @@ -295,33 +295,18 @@ class AsyncNodePluginTests: XCTestCase { func handleEmptyNode() { let context = JSContext() - let plugin = AsyncNodePlugin { _ in + let plugin = AsyncNodePlugin { _,_ in return .emptyNode } plugin.context = context - let result = plugin.handleAsyncNodeReplacement(.emptyNode, context: context!) + let result = plugin.handleAsyncNodeReplacement(.emptyNode) XCTAssertNil(result) } -func handleReplacement(plugin: AsyncNodePlugin, context: JSContext, count: Int) -> AsyncNodeHandlerType { - if count == 1 { - return .multiNode([ - ReplacementNode.encodable(AssetPlaceholderNode(asset: PlaceholderNode(id: "text", type: "text", value: "new node from the hook 1"))), - ReplacementNode.encodable(AsyncNode(id: "id")) - ]) - } else if count == 2 { - return .singleNode(.concrete(context.evaluateScript(""" - ( - {"asset": {"id": "text", "type": "text", "value":"new node from the hook 2"}} - ) - """) ?? JSValue())) - } else { - return .singleNode(.concrete(context.evaluateScript("") ?? JSValue())) - } -} func testHandleMultipleUpdatesThroughCallback() { + let handlerExpectation = XCTestExpectation(description: "first data did not change") guard let context = JSContext() else { @@ -333,23 +318,19 @@ func testHandleMultipleUpdatesThroughCallback() { var args: [JSValue] = [] var callbackFunction: JSValue? - let resolve: AsyncHookHandler = { callback in + let resolve: AsyncHookHandler = { node, callback in handlerExpectation.fulfill() callbackFunction = callback - let plugin = AsyncNodePlugin { _ in - return .singleNode(.concrete(JSValue())) - } - plugin.context = context - - let replacementResult = self.handleReplacement(plugin: plugin, context: context, count: count) - args = [plugin.handleAsyncNodeReplacement(replacementResult, context: context) ?? JSValue()] - - return replacementResult + return .singleNode(.concrete(context.evaluateScript(""" + ( + {"asset": {"id": "text", "type": "text", "value":"new node from the hook 1"}} + ) + """) ?? JSValue())) } let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) + var plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) plugin.context = context @@ -362,7 +343,7 @@ func testHandleMultipleUpdatesThroughCallback() { var expectedMultiNode1Text: String = "" var expectedMultiNode2Text: String = "" - + player.hooks?.viewController.tap({ (viewController) in viewController.hooks.view.tap { (view) in view.hooks.onUpdate.tap { val in @@ -383,7 +364,7 @@ func testHandleMultipleUpdatesThroughCallback() { if count == 3 { let newText2 = val .objectForKeyedSubscript("values") - .objectAtIndexedSubscript(2) + .objectAtIndexedSubscript(1) .objectForKeyedSubscript("asset") .objectForKeyedSubscript("value") guard let textString2 = newText2?.toString() else { return XCTFail("newText was not a string") } @@ -391,6 +372,18 @@ func testHandleMultipleUpdatesThroughCallback() { expectedMultiNode2Text = textString2 textExpectation2.fulfill() } + + if count == 4 { + let newText3 = val + .objectForKeyedSubscript("values") + .objectAtIndexedSubscript(1) + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("value") + guard let textString3 = newText3?.toString() else { return XCTFail("newText was not a string") } + + expectedMultiNode2Text = textString3 + textExpectation2.fulfill() + } } } }) @@ -402,14 +395,30 @@ func testHandleMultipleUpdatesThroughCallback() { XCTAssert(count == 2) XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") - if let callbackFunction = callbackFunction { - callbackFunction.call(withArguments: args) - } - - wait(for: [textExpectation2], timeout: 5) - + var replacementResult = AsyncNodeHandlerType.singleNode(.concrete(context.evaluateScript(""" + ( + {"asset": {"id": "text", "type": "text", "value":"new node from the hook 2"}} + ) + """) ?? JSValue())) + + args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] + + let _ = callbackFunction?.call(withArguments: args) + XCTAssert(count == 3) XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") + + + wait(for: [textExpectation2], timeout: 5) + + replacementResult = AsyncNodeHandlerType.singleNode(.concrete( context.evaluateScript("null"))) + + args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] + + _ = callbackFunction?.call(withArguments: args) + + XCTAssert(count == 4) + XCTAssertEqual(expectedMultiNode2Text, "undefined") } } @@ -438,6 +447,7 @@ extension String { }, { "id": "async", + "async": true } ] diff --git a/plugins/async-node/ios/ViewInspector/AsynNodePluginViewInspectorTests.swift b/plugins/async-node/ios/ViewInspector/AsynNodePluginViewInspectorTests.swift index 2c30fc58d..34c7682d9 100644 --- a/plugins/async-node/ios/ViewInspector/AsynNodePluginViewInspectorTests.swift +++ b/plugins/async-node/ios/ViewInspector/AsynNodePluginViewInspectorTests.swift @@ -26,7 +26,7 @@ class AsyncNodePluginViewInspectorTests: XCTestCase { let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin]) { _ in + let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin]) { _,_ in handlerExpectation.fulfill() return .singleNode(.concrete(jsContext?.evaluateScript(""" From a9542b4a11d05d5ced13fc6cd69fc92170ea4930 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Thu, 22 Aug 2024 19:25:06 -0400 Subject: [PATCH 20/28] updated test to .emptyNode case --- plugins/async-node/ios/Tests/AsynNodePluginTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index cd5e310a6..b4ab6ed46 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -330,7 +330,7 @@ func testHandleMultipleUpdatesThroughCallback() { } let asyncNodePluginPlugin = AsyncNodePluginPlugin() - var plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) + let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) plugin.context = context @@ -411,7 +411,7 @@ func testHandleMultipleUpdatesThroughCallback() { wait(for: [textExpectation2], timeout: 5) - replacementResult = AsyncNodeHandlerType.singleNode(.concrete( context.evaluateScript("null"))) + replacementResult = AsyncNodeHandlerType.emptyNode args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] From f86433074749b994ac947e8d747c5afb9d5464de Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Fri, 23 Aug 2024 11:18:32 -0400 Subject: [PATCH 21/28] added assertions in ios --- .../ios/Tests/AsynNodePluginTests.swift | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index b4ab6ed46..93a6852e6 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -343,6 +343,8 @@ func testHandleMultipleUpdatesThroughCallback() { var expectedMultiNode1Text: String = "" var expectedMultiNode2Text: String = "" + var expectedMultiNode3Text: String = "" + var expectedMultiNode4Text: String = "" player.hooks?.viewController.tap({ (viewController) in viewController.hooks.view.tap { (view) in @@ -374,14 +376,23 @@ func testHandleMultipleUpdatesThroughCallback() { } if count == 4 { - let newText3 = val + + let newText3 = val .objectForKeyedSubscript("values") - .objectAtIndexedSubscript(1) + .objectAtIndexedSubscript(0) .objectForKeyedSubscript("asset") .objectForKeyedSubscript("value") guard let textString3 = newText3?.toString() else { return XCTFail("newText was not a string") } - expectedMultiNode2Text = textString3 + let newText4 = val + .objectForKeyedSubscript("values") + .objectAtIndexedSubscript(1) + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("value") + guard let textString4 = newText4?.toString() else { return XCTFail("newText was not a string") } + + expectedMultiNode3Text = textString3 + expectedMultiNode4Text = textString4 textExpectation2.fulfill() } } @@ -418,7 +429,9 @@ func testHandleMultipleUpdatesThroughCallback() { _ = callbackFunction?.call(withArguments: args) XCTAssert(count == 4) - XCTAssertEqual(expectedMultiNode2Text, "undefined") + // asset that the value at index 0 for the object + XCTAssertEqual(expectedMultiNode3Text, "undefined") + XCTAssertEqual(expectedMultiNode4Text, "undefined") } } From 45fe9fc80928084f1e752fbbdf2372c5278464fa Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Fri, 23 Aug 2024 11:30:32 -0400 Subject: [PATCH 22/28] added suspend in jvm --- .../playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt index 720dbe1b7..caf225d63 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt @@ -15,7 +15,7 @@ public class NodeAsyncParallelBailHook2( override val node: Node, serializer1: KSerializer, serializer2: KSerializer -) : AsyncParallelBailHook<(HookContext, T1, T2) -> BailResult, R>(), AsyncNodeHook { +) : AsyncParallelBailHook BailResult, R>(), AsyncNodeHook { init { init(serializer1, serializer2) From ad6d7b8939f957be3b48acb252ffd929bbf15a99 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Mon, 26 Aug 2024 15:03:53 -0400 Subject: [PATCH 23/28] Fixed review comments for null node test and callback test --- .../ios/Tests/AsynNodePluginTests.swift | 101 ++++++++++++++++-- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index 93a6852e6..90eb25207 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -293,17 +293,93 @@ class AsyncNodePluginTests: XCTestCase { XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") } - func handleEmptyNode() { - let context = JSContext() - let plugin = AsyncNodePlugin { _,_ in - return .emptyNode - } - plugin.context = context +func testHandleEmptyNode() { + let handlerExpectation = XCTestExpectation(description: "first data did not change") + + guard let context = JSContext() else { + XCTFail("JSContext initialization failed") + return + } + + var count = 0 + var args: [JSValue] = [] + var callbackFunction: JSValue? - let result = plugin.handleAsyncNodeReplacement(.emptyNode) - XCTAssertNil(result) + let resolve: AsyncHookHandler = { node, callback in + handlerExpectation.fulfill() + callbackFunction = callback + + return .singleNode(.concrete(context.evaluateScript(""" + ( + {"asset": {"id": "text", "type": "text", "value":"new node from the hook 1"}} + ) + """) ?? JSValue())) } + let asyncNodePluginPlugin = AsyncNodePluginPlugin() + let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) + + plugin.context = context + + XCTAssertNotNil(asyncNodePluginPlugin.context) + + let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) + + let textExpectation = XCTestExpectation(description: "newText found") + let textExpectation2 = XCTestExpectation(description: "newText found") + + var expectedMultiNode1Text: String = "" + var expectedMultiNode2Text: 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(1) + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("value") + guard let textString2 = newText2?.toString() else { return XCTFail("newText was not a string") } + + expectedMultiNode2Text = textString2 + textExpectation2.fulfill() + } + } + } + }) + + player.start(flow: .asyncNodeJson, completion: { _ in}) + + wait(for: [handlerExpectation, textExpectation], timeout: 5) + + XCTAssert(count == 2) + XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") + + let replacementResult = AsyncNodeHandlerType.emptyNode + + args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] + + let _ = callbackFunction?.call(withArguments: args) + + XCTAssert(count == 3) + XCTAssertEqual(expectedMultiNode2Text, "undefined") +} + func testHandleMultipleUpdatesThroughCallback() { @@ -340,6 +416,7 @@ func testHandleMultipleUpdatesThroughCallback() { let textExpectation = XCTestExpectation(description: "newText found") let textExpectation2 = XCTestExpectation(description: "newText found") + let textExpectation3 = XCTestExpectation(description: "newText found") var expectedMultiNode1Text: String = "" var expectedMultiNode2Text: String = "" @@ -377,10 +454,12 @@ func testHandleMultipleUpdatesThroughCallback() { if count == 4 { - let newText3 = val + let newText3 = val .objectForKeyedSubscript("values") .objectAtIndexedSubscript(0) .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("label") + .objectForKeyedSubscript("asset") .objectForKeyedSubscript("value") guard let textString3 = newText3?.toString() else { return XCTFail("newText was not a string") } @@ -393,7 +472,7 @@ func testHandleMultipleUpdatesThroughCallback() { expectedMultiNode3Text = textString3 expectedMultiNode4Text = textString4 - textExpectation2.fulfill() + textExpectation3.fulfill() } } } @@ -430,7 +509,7 @@ func testHandleMultipleUpdatesThroughCallback() { XCTAssert(count == 4) // asset that the value at index 0 for the object - XCTAssertEqual(expectedMultiNode3Text, "undefined") + XCTAssertEqual(expectedMultiNode3Text, "test") XCTAssertEqual(expectedMultiNode4Text, "undefined") } } From 428d00595c6f06e6a36a4cdf2643365ee88b3805 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Mon, 26 Aug 2024 16:07:50 -0400 Subject: [PATCH 24/28] Linter fix --- .../playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt index caf225d63..59636c555 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/hooks/NodeAsyncParallelBailHook2.kt @@ -14,7 +14,7 @@ import kotlinx.serialization.Serializable public class NodeAsyncParallelBailHook2( override val node: Node, serializer1: KSerializer, - serializer2: KSerializer + serializer2: KSerializer, ) : AsyncParallelBailHook BailResult, R>(), AsyncNodeHook { init { @@ -33,7 +33,7 @@ public class NodeAsyncParallelBailHook2( internal class Serializer( private val serializer1: KSerializer, private val serializer2: KSerializer, - `_`: KSerializer + `_`: KSerializer, ) : NodeWrapperSerializer>({ NodeAsyncParallelBailHook2(it, serializer1, serializer2) }) From ace8b734b943bdd15a1fbeeddf32eedf54859cf9 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Mon, 26 Aug 2024 18:17:04 -0400 Subject: [PATCH 25/28] Updated review comments --- ios/core/Sources/Types/Hooks/Hook.swift | 139 ++--- .../ios/Sources/AsyncNodePlugin.swift | 124 ++-- .../ios/Tests/AsynNodePluginTests.swift | 552 +++++++++--------- 3 files changed, 415 insertions(+), 400 deletions(-) diff --git a/ios/core/Sources/Types/Hooks/Hook.swift b/ios/core/Sources/Types/Hooks/Hook.swift index 967ccf59a..be4655110 100644 --- a/ios/core/Sources/Types/Hooks/Hook.swift +++ b/ios/core/Sources/Types/Hooks/Hook.swift @@ -1,6 +1,6 @@ // // Hook.swift -// +// // // Created by Borawski, Harris on 2/12/20. // @@ -11,16 +11,16 @@ import JavaScriptCore /// A base for implementing JS backed hooks open class BaseJSHook { private let baseValue: JSValue - + /// The JS reference to the hook public var hook: JSValue { baseValue.objectForKeyedSubscript("hooks").objectForKeyedSubscript(name) } - + /// The JSContext for the hook public var context: JSContext { hook.context } - + /// The name of the hook public let name: String - + /// Retrieves a hook by name from an object in JS /// - Parameters: /// - baseValue: The object that has `hooks` @@ -39,9 +39,9 @@ public class Hook: BaseJSHook where T: CreatedFromJSValue { /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - - hook: A function to run when the JS hook is fired + - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping (T) -> Void) { let tapMethod: @convention(block) (JSValue?) -> Void = { value in @@ -51,7 +51,7 @@ public class Hook: BaseJSHook where T: CreatedFromJSValue { else { return } hook(hookValue) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -64,9 +64,9 @@ public class Hook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - - hook: A function to run when the JS hook is fired + - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping (T, U) -> Void) { let tapMethod: @convention(block) (JSValue?, JSValue?) -> Void = { value, value2 in @@ -78,7 +78,7 @@ public class Hook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom else { return } hook(hookValue, hookValue2) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -91,9 +91,9 @@ public class HookDecode: BaseJSHook where T: Decodable { /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - - hook: A function to run when the JS hook is fired + - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping (T) -> Void) { let tapMethod: @convention(block) (JSValue?) -> Void = { value in @@ -103,7 +103,7 @@ public class HookDecode: BaseJSHook where T: Decodable { else { return } hook(hookValue) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -116,13 +116,13 @@ public class Hook2Decode: BaseJSHook where T: Decodable, U: Decodable { /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - - hook: A function to run when the JS hook is fired + - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping (T, U) -> Void) { let tapMethod: @convention(block) (JSValue?, JSValue?) -> Void = { value, value2 in - + let decoder = JSONDecoder() guard let val = value, @@ -132,7 +132,7 @@ public class Hook2Decode: BaseJSHook where T: Decodable, U: Decodable { else { return } hook(hookValue, hookValue2) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -143,80 +143,81 @@ public class Hook2Decode: BaseJSHook where T: Decodable, U: Decodable { */ public class AsyncHook: BaseJSHook where T: CreatedFromJSValue { private var handler: AsyncHookHandler? - + public typealias AsyncHookHandler = (T) async throws -> JSValue? - + /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - - hook: A function to run when the JS hook is fired + - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping AsyncHookHandler) { let tapMethod: @convention(block) (JSValue?) -> JSValue = { value in - guard - let val = value, - let hookValue = T.createInstance(value: val) as? T - else { return JSValue() } - - let promise = - JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in - Task { - let result = try await hook(hookValue) - DispatchQueue.main.async { - resolve(result as Any) - } - } - }) - - return promise ?? JSValue() - } - - self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) - } + guard + let val = value, + let hookValue = T.createInstance(value: val) as? T + else { return JSValue() } + + let promise = + JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in + Task { + let result = try await hook(hookValue) + DispatchQueue.main.async { + resolve(result as Any) + } + } + }) + + return promise ?? JSValue() + } + + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) + } } /** This class represents an object in the JS runtime that can be tapped into - and returns a promise that resolves when the asynchronous task is completed + to receive JS events that has 2 parameters and + returns a promise that resolves when the asynchronous task is completed */ public class AsyncHook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFromJSValue { private var handler: AsyncHookHandler? - + public typealias AsyncHookHandler = (T, U) async throws -> JSValue? - + /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - - hook: A function to run when the JS hook is fired + - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping AsyncHookHandler) { let tapMethod: @convention(block) (JSValue?,JSValue?) -> JSValue = { value, value2 in - guard - let val = value, - let val2 = value2, - let hookValue = T.createInstance(value: val) as? T, - let hookValue2 = U.createInstance(value: val2) as? U - else { return JSValue() } + guard + let val = value, + let val2 = value2, + let hookValue = T.createInstance(value: val) as? T, + let hookValue2 = U.createInstance(value: val2) as? U + else { return JSValue() } + + + let promise = + JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in + Task { + let result = try await hook(hookValue, hookValue2) + DispatchQueue.main.async { + resolve(result as Any) + } + } + }) + + return promise ?? JSValue() + } - - let promise = - JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in - Task { - let result = try await hook(hookValue, hookValue2) - DispatchQueue.main.async { - resolve(result as Any) - } - } - }) - - return promise ?? JSValue() - } - - self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) - } + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) + } } diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index b75b50de8..f3a07d234 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -22,107 +22,121 @@ public enum AsyncNodeHandlerType { */ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { public var hooks: AsyncNodeHook? - + private var asyncHookHandler: AsyncHookHandler? - + public var plugins: [JSBasePlugin] = [] - + /** Constructs the AsyncNodePlugin - Parameters: - - handler: The callback that is used to tap into the core `onAsyncNode` hook - exposed to users of the plugin allowing them to supply the replacement node used in the tap callback + - handler: The callback that is used to tap into the core `onAsyncNode` hook + exposed to users of the plugin allowing them to supply the replacement node used in the tap callback */ public convenience init(plugins: [JSBasePlugin] = [AsyncNodePluginPlugin()], _ handler: @escaping AsyncHookHandler) { - + self.init(fileName: "AsyncNodePlugin.native", pluginName: "AsyncNodePlugin.AsyncNodePlugin") self.asyncHookHandler = handler self.plugins = plugins } - -func handleAsyncNodeReplacement(_ replacementNode: AsyncNodeHandlerType) -> JSValue? { - guard let context = context else { - return JSValue() + + func convertReplacementNodeToJSValue(_ replacementNode: ReplacementNode, context: JSContext) -> JSValue? { + 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 { + return nil + } + case .concrete(let jsValue): + return jsValue + } } + + /** + Converts a given `AsyncNodeHandlerType` to a `JSValue` that can be used in the JavaScript context. + + - Parameter replacementNode: The `AsyncNodeHandlerType` to be converted. + - Returns: A `JSValue` representing the given `AsyncNodeHandlerType`, or `nil` if the conversion fails. + */ + + func replacementNodeToJSValue(_ replacementNode: AsyncNodeHandlerType) -> JSValue? { + guard let context = context else { + return JSValue() + } 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 convertReplacementNodeToJSValue(.encodable(encodable), context: context) + case .concrete(let jsValue): + return convertReplacementNodeToJSValue(.concrete(jsValue), context: context) + } } 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 { - return nil - } + return convertReplacementNodeToJSValue(.encodable(encodable), context: context) case .concrete(let jsValue): - return jsValue + return convertReplacementNodeToJSValue(.concrete(jsValue), context: context) + } - + case .emptyNode: return nil } - } - + } + override public func setup(context: JSContext) { super.setup(context: context) - + if let pluginRef = pluginRef { self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode")) } - + hooks?.onAsyncNode.tap({ node, callback in // hook value is the original node guard let asyncHookHandler = self.asyncHookHandler else { return JSValue() } - + let replacementNode = try await (asyncHookHandler)(node, callback) - return self.handleAsyncNodeReplacement(replacementNode) ?? JSValue() + return self.replacementNodeToJSValue(replacementNode) ?? JSValue() }) } - + /** Retrieves the arguments for constructing this plugin, this is necessary because the arguments need to be supplied after construction of the swift object, once the context has been provided - returns: An array of arguments to construct the plugin */ override public func getArguments() -> [Any] { - for plugin in plugins { - plugin.context = self.context - } - - return [["plugins": plugins.map { $0.pluginRef }]] - } - + for plugin in plugins { + plugin.context = self.context + } + + return [["plugins": plugins.map { $0.pluginRef }]] + } + override open func getUrlForFile(fileName: String) -> URL? { - #if SWIFT_PACKAGE +#if SWIFT_PACKAGE ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module) - #else +#else ResourceUtilities.urlForFile( name: fileName, ext: "js", bundle: Bundle(for: AsyncNodePlugin.self), pathComponent: "PlayerUIAsyncNodePlugin.bundle" ) - #endif +#endif } } @@ -136,10 +150,10 @@ public struct AsyncNodeHook { public enum ReplacementNode: Encodable { case concrete(JSValue) case encodable(Encodable) - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .encodable(let value): try container.encode(value) @@ -153,12 +167,12 @@ public struct AssetPlaceholderNode: Encodable { public enum CodingKeys: String, CodingKey { case asset } - + var asset: Encodable public init(asset: Encodable) { self.asset = asset } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try? container.encode(asset, forKey: .asset) @@ -168,7 +182,7 @@ public struct AssetPlaceholderNode: Encodable { public struct AsyncNode: Codable, Equatable { var id: String var async: Bool = true - + public init(id: String) { self.id = id } @@ -181,17 +195,17 @@ public class AsyncNodePluginPlugin: JSBasePlugin { public convenience init() { self.init(fileName: "AsyncNodePlugin.native", pluginName: "AsyncNodePlugin.AsyncNodePluginPlugin") } - + override open func getUrlForFile(fileName: String) -> URL? { - #if SWIFT_PACKAGE +#if SWIFT_PACKAGE ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module) - #else +#else ResourceUtilities.urlForFile( name: fileName, ext: "js", bundle: Bundle(for: AsyncNodePluginPlugin.self), pathComponent: "PlayerUIAsyncNodePlugin.bundle" ) - #endif +#endif } } diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index 90eb25207..e7081cf39 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -16,37 +16,37 @@ import JavaScriptCore @testable import PlayerUIAsyncNodePlugin class AsyncNodePluginTests: XCTestCase { - + func testConstructionAsyncPlugin() { let context = JSContext() let plugin = AsyncNodePlugin { _,_ in return .singleNode(.concrete(JSValue())) } plugin.context = context - + XCTAssertNotNil(plugin.pluginRef) } - + func testConstructionAsyncPluginPlugin() { let context = JSContext() - + let plugin = AsyncNodePluginPlugin() plugin.context = context - + XCTAssertNotNil(plugin.pluginRef) } - - + + func testAsyncNodeWithAnotherAsyncNodeDelay() { let handlerExpectation = XCTestExpectation(description: "first data did not change") - + let context = JSContext() - + var count = 0 - + let resolveHandler: AsyncHookHandler = { _,_ in handlerExpectation.fulfill() - + sleep(3) return .singleNode(.concrete(context?.evaluateScript(""" ([ @@ -54,25 +54,25 @@ class AsyncNodePluginTests: XCTestCase { ]) """) ?? JSValue())) } - + let asyncNodePluginPlugin = AsyncNodePluginPlugin() let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolveHandler) - + plugin.context = context - + XCTAssertNotNil(asyncNodePluginPlugin.context) - + let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context ?? JSContext()) - + let textExpectation = XCTestExpectation(description: "newText1 found") - + var expectedMultiNode1Text: 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") @@ -80,31 +80,31 @@ class AsyncNodePluginTests: XCTestCase { .objectForKeyedSubscript("asset") .objectForKeyedSubscript("value") guard let textString1 = newText1?.toString() else { return XCTFail("newText1 was not a string") } - + expectedMultiNode1Text = textString1 textExpectation.fulfill() } } } }) - + player.start(flow: .asyncNodeJson, completion: {_ in}) - + wait(for: [handlerExpectation, textExpectation], timeout: 5) - + XCTAssert(count == 2) XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") } - + func testReplaceAsyncNodeWithChainedMultiNodes() { 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(""" @@ -122,32 +122,32 @@ class AsyncNodePluginTests: XCTestCase { AssetPlaceholderNode(asset: PlaceholderNode(id: "text", type: "text", value: "3rd value in the multinode")) )) } - + return .singleNode(ReplacementNode.concrete(context?.evaluateScript("") ?? JSValue())) } - + 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") @@ -155,11 +155,11 @@ class AsyncNodePluginTests: XCTestCase { .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") @@ -167,12 +167,12 @@ class AsyncNodePluginTests: XCTestCase { .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") @@ -180,42 +180,42 @@ class AsyncNodePluginTests: XCTestCase { .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") } - + func testAsyncNodeReplacementWithChainedMultiNodesSinglular() { 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.encodable(AssetPlaceholderNode(asset: PlaceholderNode(id: "text", type: "text", value: "new node from the hook 1"))), @@ -228,30 +228,30 @@ class AsyncNodePluginTests: XCTestCase { ) """) ?? JSValue())) } - + return .singleNode(ReplacementNode.concrete(context?.evaluateScript("") ?? JSValue())) } - + 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") - + var expectedMultiNode1Text: String = "" var expectedMultiNode2Text: 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") @@ -259,11 +259,11 @@ class AsyncNodePluginTests: XCTestCase { .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") @@ -271,247 +271,247 @@ class AsyncNodePluginTests: XCTestCase { .objectForKeyedSubscript("asset") .objectForKeyedSubscript("value") guard let textString2 = newText2?.toString() else { return XCTFail("newText was not a string") } - + expectedMultiNode2Text = textString2 textExpectation2.fulfill() - + } } } }) - + player.start(flow: .asyncNodeJson, completion: { _ in}) - + wait(for: [handlerExpectation, textExpectation], timeout: 5) - + XCTAssert(count == 2) XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") - + wait(for: [textExpectation2], timeout: 5) - + XCTAssert(count == 3) XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") } - -func testHandleEmptyNode() { - let handlerExpectation = XCTestExpectation(description: "first data did not change") - - guard let context = JSContext() else { - XCTFail("JSContext initialization failed") - return - } - - var count = 0 - var args: [JSValue] = [] - var callbackFunction: JSValue? - - let resolve: AsyncHookHandler = { node, callback in - handlerExpectation.fulfill() - callbackFunction = callback - - return .singleNode(.concrete(context.evaluateScript(""" + + func testHandleEmptyNode() { + let handlerExpectation = XCTestExpectation(description: "first data did not change") + + guard let context = JSContext() else { + XCTFail("JSContext initialization failed") + return + } + + var count = 0 + var args: [JSValue] = [] + var callbackFunction: JSValue? + + let resolve: AsyncHookHandler = { node, callback in + handlerExpectation.fulfill() + callbackFunction = callback + + return .singleNode(.concrete(context.evaluateScript(""" ( {"asset": {"id": "text", "type": "text", "value":"new node from the hook 1"}} ) """) ?? JSValue())) - } - - let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) - - plugin.context = context - - XCTAssertNotNil(asyncNodePluginPlugin.context) - - let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) - - let textExpectation = XCTestExpectation(description: "newText found") - let textExpectation2 = XCTestExpectation(description: "newText found") - - var expectedMultiNode1Text: String = "" - var expectedMultiNode2Text: 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(1) - .objectForKeyedSubscript("asset") - .objectForKeyedSubscript("value") - guard let textString2 = newText2?.toString() else { return XCTFail("newText was not a string") } - - expectedMultiNode2Text = textString2 - textExpectation2.fulfill() + } + + let asyncNodePluginPlugin = AsyncNodePluginPlugin() + let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) + + plugin.context = context + + XCTAssertNotNil(asyncNodePluginPlugin.context) + + let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) + + let textExpectation = XCTestExpectation(description: "newText found") + let textExpectation2 = XCTestExpectation(description: "newText found") + + var expectedMultiNode1Text: String = "" + var expectedMultiNode2Text: 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(1) + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("value") + guard let textString2 = newText2?.toString() else { return XCTFail("newText was not a string") } + + expectedMultiNode2Text = textString2 + textExpectation2.fulfill() + } } } - } - }) - - player.start(flow: .asyncNodeJson, completion: { _ in}) - - wait(for: [handlerExpectation, textExpectation], timeout: 5) - - XCTAssert(count == 2) - XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") - - let replacementResult = AsyncNodeHandlerType.emptyNode - - args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] - - let _ = callbackFunction?.call(withArguments: args) + }) + + player.start(flow: .asyncNodeJson, completion: { _ in}) + + wait(for: [handlerExpectation, textExpectation], timeout: 5) + + XCTAssert(count == 2) + XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") + + let replacementResult = AsyncNodeHandlerType.emptyNode + + args = [plugin.replacementNodeToJSValue(replacementResult) ?? JSValue()] + + let _ = callbackFunction?.call(withArguments: args) + + XCTAssert(count == 3) + XCTAssertEqual(expectedMultiNode2Text, "undefined") + } - XCTAssert(count == 3) - XCTAssertEqual(expectedMultiNode2Text, "undefined") -} - - -func testHandleMultipleUpdatesThroughCallback() { - let handlerExpectation = XCTestExpectation(description: "first data did not change") - - guard let context = JSContext() else { - XCTFail("JSContext initialization failed") - return - } - - var count = 0 - var args: [JSValue] = [] - var callbackFunction: JSValue? - - let resolve: AsyncHookHandler = { node, callback in - handlerExpectation.fulfill() - callbackFunction = callback - - return .singleNode(.concrete(context.evaluateScript(""" + func testHandleMultipleUpdatesThroughCallback() { + + let handlerExpectation = XCTestExpectation(description: "first data did not change") + + guard let context = JSContext() else { + XCTFail("JSContext initialization failed") + return + } + + var count = 0 + var args: [JSValue] = [] + var callbackFunction: JSValue? + + let resolve: AsyncHookHandler = { node, callback in + handlerExpectation.fulfill() + callbackFunction = callback + + return .singleNode(.concrete(context.evaluateScript(""" ( {"asset": {"id": "text", "type": "text", "value":"new node from the hook 1"}} ) """) ?? JSValue())) - } - - let asyncNodePluginPlugin = AsyncNodePluginPlugin() - let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) - - plugin.context = context - - XCTAssertNotNil(asyncNodePluginPlugin.context) - - let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) - - 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 = "" - var expectedMultiNode4Text: 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(1) - .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(0) - .objectForKeyedSubscript("asset") - .objectForKeyedSubscript("label") - .objectForKeyedSubscript("asset") - .objectForKeyedSubscript("value") - guard let textString3 = newText3?.toString() else { return XCTFail("newText was not a string") } - - let newText4 = val - .objectForKeyedSubscript("values") - .objectAtIndexedSubscript(1) - .objectForKeyedSubscript("asset") - .objectForKeyedSubscript("value") - guard let textString4 = newText4?.toString() else { return XCTFail("newText was not a string") } - - expectedMultiNode3Text = textString3 - expectedMultiNode4Text = textString4 - textExpectation3.fulfill() + } + + let asyncNodePluginPlugin = AsyncNodePluginPlugin() + let plugin = AsyncNodePlugin(plugins: [asyncNodePluginPlugin], resolve) + + plugin.context = context + + XCTAssertNotNil(asyncNodePluginPlugin.context) + + let player = HeadlessPlayerImpl(plugins: [ReferenceAssetsPlugin(), plugin], context: context) + + 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 = "" + var expectedMultiNode4Text: 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(1) + .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(0) + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("label") + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("value") + guard let textString3 = newText3?.toString() else { return XCTFail("newText was not a string") } + + let newText4 = val + .objectForKeyedSubscript("values") + .objectAtIndexedSubscript(1) + .objectForKeyedSubscript("asset") + .objectForKeyedSubscript("value") + guard let textString4 = newText4?.toString() else { return XCTFail("newText was not a string") } + + expectedMultiNode3Text = textString3 + expectedMultiNode4Text = textString4 + textExpectation3.fulfill() + } } } - } - }) - - player.start(flow: .asyncNodeJson, completion: { _ in}) - - wait(for: [handlerExpectation, textExpectation], timeout: 5) - - XCTAssert(count == 2) - XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") - - var replacementResult = AsyncNodeHandlerType.singleNode(.concrete(context.evaluateScript(""" + }) + + player.start(flow: .asyncNodeJson, completion: { _ in}) + + wait(for: [handlerExpectation, textExpectation], timeout: 5) + + XCTAssert(count == 2) + XCTAssertEqual(expectedMultiNode1Text, "new node from the hook 1") + + var replacementResult = AsyncNodeHandlerType.singleNode(.concrete(context.evaluateScript(""" ( {"asset": {"id": "text", "type": "text", "value":"new node from the hook 2"}} ) """) ?? JSValue())) - - args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] - - let _ = callbackFunction?.call(withArguments: args) - - XCTAssert(count == 3) - XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") - - - wait(for: [textExpectation2], timeout: 5) - - replacementResult = AsyncNodeHandlerType.emptyNode - - args = [plugin.handleAsyncNodeReplacement(replacementResult) ?? JSValue()] - - _ = callbackFunction?.call(withArguments: args) - - XCTAssert(count == 4) - // asset that the value at index 0 for the object - XCTAssertEqual(expectedMultiNode3Text, "test") - XCTAssertEqual(expectedMultiNode4Text, "undefined") -} + + args = [plugin.replacementNodeToJSValue(replacementResult) ?? JSValue()] + + let _ = callbackFunction?.call(withArguments: args) + + XCTAssert(count == 3) + XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") + + + wait(for: [textExpectation2], timeout: 5) + + replacementResult = AsyncNodeHandlerType.emptyNode + + args = [plugin.replacementNodeToJSValue(replacementResult) ?? JSValue()] + + _ = callbackFunction?.call(withArguments: args) + + XCTAssert(count == 4) + // asset that the value at index 0 for the object + XCTAssertEqual(expectedMultiNode3Text, "test") + XCTAssertEqual(expectedMultiNode4Text, "undefined") + } } extension String { @@ -539,7 +539,7 @@ extension String { }, { "id": "async", - + "async": true } ] @@ -573,7 +573,7 @@ struct PlaceholderNode: Codable, Equatable, AssetData { public var id: String public var type: String var value: String? - + public init(id: String, type: String, value: String? = nil) { self.id = id self.type = type From e8dc3750d1dd2157bd2f9fe702490fbdb9194b96 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Mon, 26 Aug 2024 18:25:41 -0400 Subject: [PATCH 26/28] Added doc comment --- plugins/async-node/ios/Sources/AsyncNodePlugin.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index f3a07d234..87bb6b30d 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -40,6 +40,14 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { self.plugins = plugins } + /** + Converts a given `ReplacementNode` to a `JSValue` that can be used in the JavaScript context. + + - Parameters: + - replacementNode: The `ReplacementNode` to be converted. + - context: The `JSContext` in which the `JSValue` will be used. + - Returns: A `JSValue` representing the given `ReplacementNode`, or `nil` if the conversion fails. + */ func convertReplacementNodeToJSValue(_ replacementNode: ReplacementNode, context: JSContext) -> JSValue? { switch replacementNode { case .encodable(let encodable): From 5b850e03e1f0d996afd045ae85d590ca2898c896 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Tue, 3 Sep 2024 13:57:36 -0400 Subject: [PATCH 27/28] updated ios review comments and updated tests --- .../ios/Sources/AsyncNodePlugin.swift | 110 ++++++++---------- .../ios/Tests/AsynNodePluginTests.swift | 16 +-- 2 files changed, 54 insertions(+), 72 deletions(-) diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index 87bb6b30d..4e6fad717 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -17,39 +17,14 @@ public enum AsyncNodeHandlerType { case emptyNode } -/** - Wraps the core AsyncNodePlugin and taps into the `onAsyncNode` hook to allow asynchronous replacement of the node object that contains `async` - */ -public class AsyncNodePlugin: JSBasePlugin, NativePlugin { - public var hooks: AsyncNodeHook? - - private var asyncHookHandler: AsyncHookHandler? - - public var plugins: [JSBasePlugin] = [] - - /** - Constructs the AsyncNodePlugin - - Parameters: - - handler: The callback that is used to tap into the core `onAsyncNode` hook - exposed to users of the plugin allowing them to supply the replacement node used in the tap callback - */ - public convenience init(plugins: [JSBasePlugin] = [AsyncNodePluginPlugin()], _ handler: @escaping AsyncHookHandler) { - - self.init(fileName: "AsyncNodePlugin.native", pluginName: "AsyncNodePlugin.AsyncNodePlugin") - self.asyncHookHandler = handler - self.plugins = plugins - } - - /** - Converts a given `ReplacementNode` to a `JSValue` that can be used in the JavaScript context. - - - Parameters: - - replacementNode: The `ReplacementNode` to be converted. - - context: The `JSContext` in which the `JSValue` will be used. - - Returns: A `JSValue` representing the given `ReplacementNode`, or `nil` if the conversion fails. - */ - func convertReplacementNodeToJSValue(_ replacementNode: ReplacementNode, context: JSContext) -> JSValue? { - switch replacementNode { +/// Extension for `ReplacementNode` to convert it to a `JSValue` in a given `JSContext`. +public extension ReplacementNode { + /// Converts the `ReplacementNode` to a `JSValue` in the provided `JSContext`. + /// + /// - Parameter context: The `JSContext` in which the `JSValue` will be created. + /// - Returns: A `JSValue` representing the `ReplacementNode`, or `nil` if the conversion fails. + func toJSValue(context: JSContext) -> JSValue? { + switch self { case .encodable(let encodable): let encoder = JSONEncoder() do { @@ -62,46 +37,53 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { return jsValue } } - - /** - Converts a given `AsyncNodeHandlerType` to a `JSValue` that can be used in the JavaScript context. - - - Parameter replacementNode: The `AsyncNodeHandlerType` to be converted. - - Returns: A `JSValue` representing the given `AsyncNodeHandlerType`, or `nil` if the conversion fails. - */ - - func replacementNodeToJSValue(_ replacementNode: AsyncNodeHandlerType) -> JSValue? { - guard let context = context else { - return JSValue() - } - switch replacementNode { +} + +/// Extension for `AsyncNodeHandlerType` to convert it to a `JSValue` in a given `JSContext`. +public extension AsyncNodeHandlerType { + /// Converts the `AsyncNodeHandlerType` to a `JSValue` in the provided `JSContext`. + /// + /// - Parameter context: The `JSContext` in which the `JSValue` will be created. + /// - Returns: A `JSValue` representing the `AsyncNodeHandlerType`, or `nil` if the conversion fails. + func handlerTypeToJSValue(context: JSContext) -> JSValue? { + switch self { case .multiNode(let replacementNodes): - let jsValueArray = replacementNodes.compactMap { node in - switch node { - - case .encodable(let encodable): - return convertReplacementNodeToJSValue(.encodable(encodable), context: context) - case .concrete(let jsValue): - return convertReplacementNodeToJSValue(.concrete(jsValue), context: context) - - } + let jsValueArray = replacementNodes.compactMap { + $0.toJSValue(context: context) } return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray]) case .singleNode(let replacementNode): - switch replacementNode { - - case .encodable(let encodable): - return convertReplacementNodeToJSValue(.encodable(encodable), context: context) - case .concrete(let jsValue): - return convertReplacementNodeToJSValue(.concrete(jsValue), context: context) - - } + return replacementNode.toJSValue(context: context) case .emptyNode: return nil } } +} + +/** + Wraps the core AsyncNodePlugin and taps into the `onAsyncNode` hook to allow asynchronous replacement of the node object that contains `async` + */ +public class AsyncNodePlugin: JSBasePlugin, NativePlugin { + public var hooks: AsyncNodeHook? + + private var asyncHookHandler: AsyncHookHandler? + + public var plugins: [JSBasePlugin] = [] + + /** + Constructs the AsyncNodePlugin + - Parameters: + - handler: The callback that is used to tap into the core `onAsyncNode` hook + exposed to users of the plugin allowing them to supply the replacement node used in the tap callback + */ + public convenience init(plugins: [JSBasePlugin] = [AsyncNodePluginPlugin()], _ handler: @escaping AsyncHookHandler) { + + self.init(fileName: "AsyncNodePlugin.native", pluginName: "AsyncNodePlugin.AsyncNodePlugin") + self.asyncHookHandler = handler + self.plugins = plugins + } override public func setup(context: JSContext) { super.setup(context: context) @@ -117,7 +99,7 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { } let replacementNode = try await (asyncHookHandler)(node, callback) - return self.replacementNodeToJSValue(replacementNode) ?? JSValue() + return replacementNode.handlerTypeToJSValue(context:context) ?? JSValue() }) } diff --git a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift index e7081cf39..b6fddb8b7 100644 --- a/plugins/async-node/ios/Tests/AsynNodePluginTests.swift +++ b/plugins/async-node/ios/Tests/AsynNodePluginTests.swift @@ -302,7 +302,7 @@ class AsyncNodePluginTests: XCTestCase { } var count = 0 - var args: [JSValue] = [] + var args: JSValue? var callbackFunction: JSValue? let resolve: AsyncHookHandler = { node, callback in @@ -372,9 +372,9 @@ class AsyncNodePluginTests: XCTestCase { let replacementResult = AsyncNodeHandlerType.emptyNode - args = [plugin.replacementNodeToJSValue(replacementResult) ?? JSValue()] + args = replacementResult.handlerTypeToJSValue(context: context ?? JSContext()) - let _ = callbackFunction?.call(withArguments: args) + let _ = callbackFunction?.call(withArguments: [args]) XCTAssert(count == 3) XCTAssertEqual(expectedMultiNode2Text, "undefined") @@ -391,7 +391,7 @@ class AsyncNodePluginTests: XCTestCase { } var count = 0 - var args: [JSValue] = [] + var args: JSValue? var callbackFunction: JSValue? let resolve: AsyncHookHandler = { node, callback in @@ -491,9 +491,9 @@ class AsyncNodePluginTests: XCTestCase { ) """) ?? JSValue())) - args = [plugin.replacementNodeToJSValue(replacementResult) ?? JSValue()] + args = replacementResult.handlerTypeToJSValue(context: context ?? JSContext()) - let _ = callbackFunction?.call(withArguments: args) + let _ = callbackFunction?.call(withArguments: [args]) XCTAssert(count == 3) XCTAssertEqual(expectedMultiNode2Text, "new node from the hook 2") @@ -503,9 +503,9 @@ class AsyncNodePluginTests: XCTestCase { replacementResult = AsyncNodeHandlerType.emptyNode - args = [plugin.replacementNodeToJSValue(replacementResult) ?? JSValue()] + args = replacementResult.handlerTypeToJSValue(context: context ?? JSContext()) - _ = callbackFunction?.call(withArguments: args) + _ = callbackFunction?.call(withArguments: [args]) XCTAssert(count == 4) // asset that the value at index 0 for the object From 32b9c57bdab22be954774d8c401b445b4c8aa225 Mon Sep 17 00:00:00 2001 From: sakuntala_motukuri Date: Tue, 3 Sep 2024 14:24:43 -0400 Subject: [PATCH 28/28] removed white spaces --- ios/core/Sources/Types/Hooks/Hook.swift | 52 +++++++++---------- .../ios/Sources/AsyncNodePlugin.swift | 36 ++++++------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/ios/core/Sources/Types/Hooks/Hook.swift b/ios/core/Sources/Types/Hooks/Hook.swift index be4655110..6510ae7a1 100644 --- a/ios/core/Sources/Types/Hooks/Hook.swift +++ b/ios/core/Sources/Types/Hooks/Hook.swift @@ -11,16 +11,16 @@ import JavaScriptCore /// A base for implementing JS backed hooks open class BaseJSHook { private let baseValue: JSValue - + /// The JS reference to the hook public var hook: JSValue { baseValue.objectForKeyedSubscript("hooks").objectForKeyedSubscript(name) } - + /// The JSContext for the hook public var context: JSContext { hook.context } - + /// The name of the hook public let name: String - + /// Retrieves a hook by name from an object in JS /// - Parameters: /// - baseValue: The object that has `hooks` @@ -39,7 +39,7 @@ public class Hook: BaseJSHook where T: CreatedFromJSValue { /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - hook: A function to run when the JS hook is fired */ @@ -51,7 +51,7 @@ public class Hook: BaseJSHook where T: CreatedFromJSValue { else { return } hook(hookValue) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -64,7 +64,7 @@ public class Hook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - hook: A function to run when the JS hook is fired */ @@ -78,7 +78,7 @@ public class Hook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFrom else { return } hook(hookValue, hookValue2) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -91,7 +91,7 @@ public class HookDecode: BaseJSHook where T: Decodable { /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - hook: A function to run when the JS hook is fired */ @@ -103,7 +103,7 @@ public class HookDecode: BaseJSHook where T: Decodable { else { return } hook(hookValue) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -116,13 +116,13 @@ public class Hook2Decode: BaseJSHook where T: Decodable, U: Decodable { /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - hook: A function to run when the JS hook is fired */ public func tap(_ hook: @escaping (T, U) -> Void) { let tapMethod: @convention(block) (JSValue?, JSValue?) -> Void = { value, value2 in - + let decoder = JSONDecoder() guard let val = value, @@ -132,7 +132,7 @@ public class Hook2Decode: BaseJSHook where T: Decodable, U: Decodable { else { return } hook(hookValue, hookValue2) } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -143,13 +143,13 @@ public class Hook2Decode: BaseJSHook where T: Decodable, U: Decodable { */ public class AsyncHook: BaseJSHook where T: CreatedFromJSValue { private var handler: AsyncHookHandler? - + public typealias AsyncHookHandler = (T) async throws -> JSValue? - + /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - hook: A function to run when the JS hook is fired */ @@ -159,7 +159,7 @@ public class AsyncHook: BaseJSHook where T: CreatedFromJSValue { let val = value, let hookValue = T.createInstance(value: val) as? T else { return JSValue() } - + let promise = JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in Task { @@ -169,10 +169,10 @@ public class AsyncHook: BaseJSHook where T: CreatedFromJSValue { } } }) - + return promise ?? JSValue() } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } @@ -184,13 +184,13 @@ public class AsyncHook: BaseJSHook where T: CreatedFromJSValue { */ public class AsyncHook2: BaseJSHook where T: CreatedFromJSValue, U: CreatedFromJSValue { private var handler: AsyncHookHandler? - + public typealias AsyncHookHandler = (T, U) async throws -> JSValue? - + /** Attach a closure to the hook, so when the hook is fired in the JS runtime we receive the event in the native runtime - + - parameters: - hook: A function to run when the JS hook is fired */ @@ -202,8 +202,8 @@ public class AsyncHook2: BaseJSHook where T: CreatedFromJSValue, U: Create let hookValue = T.createInstance(value: val) as? T, let hookValue2 = U.createInstance(value: val2) as? U else { return JSValue() } - - + + let promise = JSUtilities.createPromise(context: self.context, handler: { (resolve, _) in Task { @@ -213,10 +213,10 @@ public class AsyncHook2: BaseJSHook where T: CreatedFromJSValue, U: Create } } }) - + return promise ?? JSValue() } - + self.hook.invokeMethod("tap", withArguments: [name, JSValue(object: tapMethod, in: context) as Any]) } } diff --git a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift index 4e6fad717..539f3dc22 100644 --- a/plugins/async-node/ios/Sources/AsyncNodePlugin.swift +++ b/plugins/async-node/ios/Sources/AsyncNodePlugin.swift @@ -52,10 +52,10 @@ public extension AsyncNodeHandlerType { $0.toJSValue(context: context) } return context.objectForKeyedSubscript("Array").objectForKeyedSubscript("from").call(withArguments: [jsValueArray]) - + case .singleNode(let replacementNode): return replacementNode.toJSValue(context: context) - + case .emptyNode: return nil } @@ -67,11 +67,11 @@ public extension AsyncNodeHandlerType { */ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { public var hooks: AsyncNodeHook? - + private var asyncHookHandler: AsyncHookHandler? - + public var plugins: [JSBasePlugin] = [] - + /** Constructs the AsyncNodePlugin - Parameters: @@ -79,30 +79,30 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { exposed to users of the plugin allowing them to supply the replacement node used in the tap callback */ public convenience init(plugins: [JSBasePlugin] = [AsyncNodePluginPlugin()], _ handler: @escaping AsyncHookHandler) { - + self.init(fileName: "AsyncNodePlugin.native", pluginName: "AsyncNodePlugin.AsyncNodePlugin") self.asyncHookHandler = handler self.plugins = plugins } - + override public func setup(context: JSContext) { super.setup(context: context) - + if let pluginRef = pluginRef { self.hooks = AsyncNodeHook(onAsyncNode: AsyncHook2(baseValue: pluginRef, name: "onAsyncNode")) } - + hooks?.onAsyncNode.tap({ node, callback in // hook value is the original node guard let asyncHookHandler = self.asyncHookHandler else { return JSValue() } - + let replacementNode = try await (asyncHookHandler)(node, callback) return replacementNode.handlerTypeToJSValue(context:context) ?? JSValue() }) } - + /** Retrieves the arguments for constructing this plugin, this is necessary because the arguments need to be supplied after construction of the swift object, once the context has been provided @@ -112,10 +112,10 @@ public class AsyncNodePlugin: JSBasePlugin, NativePlugin { for plugin in plugins { plugin.context = self.context } - + return [["plugins": plugins.map { $0.pluginRef }]] } - + override open func getUrlForFile(fileName: String) -> URL? { #if SWIFT_PACKAGE ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module) @@ -140,10 +140,10 @@ public struct AsyncNodeHook { public enum ReplacementNode: Encodable { case concrete(JSValue) case encodable(Encodable) - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .encodable(let value): try container.encode(value) @@ -157,12 +157,12 @@ public struct AssetPlaceholderNode: Encodable { public enum CodingKeys: String, CodingKey { case asset } - + var asset: Encodable public init(asset: Encodable) { self.asset = asset } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try? container.encode(asset, forKey: .asset) @@ -185,7 +185,7 @@ public class AsyncNodePluginPlugin: JSBasePlugin { public convenience init() { self.init(fileName: "AsyncNodePlugin.native", pluginName: "AsyncNodePlugin.AsyncNodePluginPlugin") } - + override open func getUrlForFile(fileName: String) -> URL? { #if SWIFT_PACKAGE ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)