diff --git a/core/player/src/view/resolver/__tests__/index.test.ts b/core/player/src/view/resolver/__tests__/index.test.ts index 81565c4cf..831ad2a47 100644 --- a/core/player/src/view/resolver/__tests__/index.test.ts +++ b/core/player/src/view/resolver/__tests__/index.test.ts @@ -58,7 +58,7 @@ describe("Async Node Resolution", () => { }; }); - it("should", () => { + it("should clear the cache for the async node and its parent when it is updated", () => { const beforeResolveFunction = vi.fn((node: Node.Node | null) => node); const resolver = new Resolver(simpleViewWithAsync, resolverOptions); @@ -106,7 +106,7 @@ describe("Async Node Resolution", () => { ); }); - it("should also", () => { + it("should clear the cache for anything with a matching async node in its resolved list on update", () => { const beforeResolveFunction = vi.fn((node: Node.Node | null) => { // Add asyncNodesResolved to view to test tracking and invalidation of just the view. if (node?.type === NodeType.View) { diff --git a/plugins/async-node/core/src/__tests__/createAsyncTransform.test.ts b/plugins/async-node/core/src/__tests__/createAsyncTransform.test.ts index d4c0b2d0a..52f011acf 100644 --- a/plugins/async-node/core/src/__tests__/createAsyncTransform.test.ts +++ b/plugins/async-node/core/src/__tests__/createAsyncTransform.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { createAsyncTransform } from ".."; import { Builder, NodeType, Node } from "@player-ui/player"; @@ -128,6 +128,196 @@ describe("createAsyncTransform", () => { }); }); + describe("getNestedAsset - different node types", () => { + it("should add the async node to an existing multi node", () => { + const nodeIdFn = vi.fn(); + nodeIdFn.mockReturnValue("async-node"); + + const nestedAssetFn = vi.fn(); + const nestedAsset: Node.MultiNode = { + type: NodeType.MultiNode, + values: [ + { + type: NodeType.Value, + value: undefined, + children: [ + { + path: ["asset"], + value: { + type: NodeType.Asset, + value: { + type: "text", + id: "first-asset", + }, + }, + }, + ], + }, + { + type: NodeType.Value, + value: undefined, + children: [ + { + path: ["asset"], + value: { + type: NodeType.Asset, + value: { + type: "text", + id: "second-asset", + }, + }, + }, + ], + }, + ], + }; + nestedAssetFn.mockReturnValue(nestedAsset); + + const transform = createAsyncTransform({ + transformAssetType: "chat-message", + wrapperAssetType: "collection", + flatten: false, + path: ["array"], + getAsyncNodeId: nodeIdFn, + getNestedAsset: nestedAssetFn, + }); + + const result = transform(asset, {} as any, {} as any); + + expect(result).toStrictEqual({ + type: NodeType.Asset, + children: [ + { + path: ["array"], + value: { + type: NodeType.MultiNode, + override: true, + parent: expect.anything(), + values: [ + { + parent: expect.anything(), + type: NodeType.Value, + value: undefined, + children: [ + { + path: ["asset"], + value: { + type: NodeType.Asset, + value: { + type: "text", + id: "first-asset", + }, + }, + }, + ], + }, + { + parent: expect.anything(), + type: NodeType.Value, + value: undefined, + children: [ + { + path: ["asset"], + value: { + type: NodeType.Asset, + value: { + type: "text", + id: "second-asset", + }, + }, + }, + ], + }, + { + parent: expect.anything(), + type: NodeType.Async, + flatten: false, + onValueReceived: undefined, + id: "async-node", + value: { + type: NodeType.Value, + value: { + id: "async-node", + }, + }, + }, + ], + }, + }, + ], + value: { + id: "collection-async-node", + type: "collection", + }, + }); + }); + + it("should default to adding the node as-is", () => { + const nodeIdFn = vi.fn(); + nodeIdFn.mockReturnValue("async-node"); + + const nestedAssetFn = vi.fn(); + const nestedAsset: Node.Value = { + type: NodeType.Value, + value: { + prop: "value", + }, + }; + nestedAssetFn.mockReturnValue(nestedAsset); + + const transform = createAsyncTransform({ + transformAssetType: "chat-message", + wrapperAssetType: "collection", + flatten: false, + path: ["array"], + getAsyncNodeId: nodeIdFn, + getNestedAsset: nestedAssetFn, + }); + + const result = transform(asset, {} as any, {} as any); + + expect(result).toStrictEqual({ + type: NodeType.Asset, + children: [ + { + path: ["array"], + value: { + type: NodeType.MultiNode, + override: true, + parent: expect.anything(), + values: [ + { + parent: expect.anything(), + type: NodeType.Value, + value: { + prop: "value", + }, + }, + { + parent: expect.anything(), + type: NodeType.Async, + flatten: false, + onValueReceived: undefined, + id: "async-node", + value: { + type: NodeType.Value, + value: { + id: "async-node", + }, + }, + }, + ], + }, + }, + ], + value: { + id: "collection-async-node", + type: "collection", + }, + }); + }); + }); + describe("onValueReceived callback setup", () => { let transformedAsset: Node.Node; let onValueReceivedFuncion: ((node: Node.Node) => Node.Node) | undefined; @@ -188,7 +378,7 @@ describe("createAsyncTransform", () => { expect(onValueReceivedFuncion).toBeDefined(); }); - it("should add an onValueReceivedFunction that transforms the result into a chained asset on the same type into a multi-node", () => { + it("should use traverseAndReplace as the onValueReceived callback", () => { const result = onValueReceivedFuncion?.(asset); expect(result).toStrictEqual({ override: true, diff --git a/plugins/async-node/core/src/createAsyncTransform.ts b/plugins/async-node/core/src/createAsyncTransform.ts index 1fbd9d5fe..45bf49093 100644 --- a/plugins/async-node/core/src/createAsyncTransform.ts +++ b/plugins/async-node/core/src/createAsyncTransform.ts @@ -4,7 +4,12 @@ import { Node, NodeType, } from "@player-ui/player"; -import { extractNodeFromPath, traverseAndReplace, unwrapAsset } from "./utils"; +import { + extractNodeFromPath, + requiresAssetWrapper, + traverseAndReplace, + unwrapAsset, +} from "./utils"; export type AsyncTransformOptions = { /** Whether or not to flatten the results into its container. Defaults to true */ @@ -69,10 +74,15 @@ export const createAsyncTransform = ( const asyncNode = Builder.asyncNode(id, flatten, replaceFunction); let multiNode: Node.MultiNode | undefined; - if (asset) { - const assetNode = Builder.assetWrapper(asset); - multiNode = Builder.multiNode(assetNode, asyncNode); + if (requiresAssetWrapper(asset)) { + const assetWrappedNode = Builder.assetWrapper(asset); + multiNode = Builder.multiNode(assetWrappedNode, asyncNode); + } else if (asset.type === NodeType.MultiNode) { + multiNode = Builder.multiNode(...(asset.values as any[]), asyncNode); + } else { + multiNode = Builder.multiNode(asset as any, asyncNode); + } } else { multiNode = Builder.multiNode(asyncNode); } diff --git a/plugins/async-node/core/src/utils/__tests__/requiresAssetWrapper.test.ts b/plugins/async-node/core/src/utils/__tests__/requiresAssetWrapper.test.ts new file mode 100644 index 000000000..1600f5b84 --- /dev/null +++ b/plugins/async-node/core/src/utils/__tests__/requiresAssetWrapper.test.ts @@ -0,0 +1,63 @@ +import { NodeType, Node } from "@player-ui/player"; +import { describe, expect, it } from "vitest"; +import { requiresAssetWrapper } from "../requiresAssetWrapper"; + +describe("requiresAssetWrapper", () => { + it("should return true for asset nodes", () => { + const node: Node.Asset = { + type: NodeType.Asset, + value: { + type: "text", + id: "id", + }, + }; + + const result = requiresAssetWrapper(node); + + expect(result).toBe(true); + }); + + it("should return true for applicability nodes containing asset nodes", () => { + const node: Node.Applicability = { + type: NodeType.Applicability, + expression: "", + value: { + type: NodeType.Asset, + value: { + type: "text", + id: "id", + }, + }, + }; + + const result = requiresAssetWrapper(node); + + expect(result).toBe(true); + }); + + it("should return false for non-asset or non-applicability nodes", () => { + const node: Node.Value = { + type: NodeType.Value, + value: {}, + }; + + const result = requiresAssetWrapper(node); + + expect(result).toBe(false); + }); + + it("should return false for applicability nodes that do not contain an asset node", () => { + const node: Node.Applicability = { + type: NodeType.Applicability, + expression: "", + value: { + type: NodeType.Value, + value: {}, + }, + }; + + const result = requiresAssetWrapper(node); + + expect(result).toBe(false); + }); +}); diff --git a/plugins/async-node/core/src/utils/index.ts b/plugins/async-node/core/src/utils/index.ts index b97c76388..98139ad44 100644 --- a/plugins/async-node/core/src/utils/index.ts +++ b/plugins/async-node/core/src/utils/index.ts @@ -1,3 +1,4 @@ export * from "./extractNodeFromPath"; export * from "./traverseAndReplace"; export * from "./unwrapAsset"; +export * from "./requiresAssetWrapper"; diff --git a/plugins/async-node/core/src/utils/requiresAssetWrapper.ts b/plugins/async-node/core/src/utils/requiresAssetWrapper.ts new file mode 100644 index 000000000..f8907da67 --- /dev/null +++ b/plugins/async-node/core/src/utils/requiresAssetWrapper.ts @@ -0,0 +1,14 @@ +import { NodeType } from "@player-ui/player"; +import type { Node } from "@player-ui/player"; + +export const requiresAssetWrapper = (node: Node.Node): boolean => { + if (node.type === NodeType.Asset) { + return true; + } + + if (node.type !== NodeType.Applicability) { + return false; + } + + return node.value.type === NodeType.Asset; +};