@player-ui/player 0.13.0 → 0.14.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "types"
7
7
  ],
8
8
  "name": "@player-ui/player",
9
- "version": "0.13.0",
9
+ "version": "0.14.0-next.0",
10
10
  "main": "dist/cjs/index.cjs",
11
11
  "dependencies": {
12
- "@player-ui/partial-match-registry": "0.13.0",
13
- "@player-ui/make-flow": "0.13.0",
14
- "@player-ui/types": "0.13.0",
12
+ "@player-ui/partial-match-registry": "0.14.0-next.0",
13
+ "@player-ui/make-flow": "0.14.0-next.0",
14
+ "@player-ui/types": "0.14.0-next.0",
15
15
  "@types/dlv": "^1.1.4",
16
16
  "dequal": "^2.0.2",
17
17
  "dlv": "^1.1.3",
@@ -11,14 +11,17 @@ export const testModel = {
11
11
  {
12
12
  name: "ginger",
13
13
  type: "dog",
14
+ isDog: true,
14
15
  },
15
16
  {
16
17
  name: "daisy",
17
18
  type: "dog",
19
+ isDog: true,
18
20
  },
19
21
  {
20
22
  name: "frodo",
21
23
  type: "cat",
24
+ isDog: false,
22
25
  },
23
26
  "other",
24
27
  ],
@@ -33,6 +36,8 @@ export const testCases: Array<[string, string]> = [
33
36
  ["foo.pets[01].name", "foo.pets.1.name"],
34
37
  ['foo.pets[name = "frodo"].type', "foo.pets.2.type"],
35
38
  ['foo.pets["name" = "sprinkles"].type', "foo.pets.4.type"],
39
+ ['foo.pets["isDog" = false].type', "foo.pets.2.type"],
40
+ ['foo.pets["isDog" = true].type', "foo.pets.0.type"],
36
41
  ];
37
42
 
38
43
  test.each(testCases)("Resolving binding: %s", (binding, expectedResolved) => {
@@ -107,7 +107,11 @@ export function resolveBindingAST(
107
107
  break;
108
108
 
109
109
  case "Value":
110
- appendPathSegments(resolvedNode.value);
110
+ appendPathSegments(
111
+ typeof resolvedNode.value === "boolean"
112
+ ? String(resolvedNode.value)
113
+ : resolvedNode.value,
114
+ );
111
115
  break;
112
116
 
113
117
  case "Query": {
@@ -6,40 +6,8 @@ import {
6
6
  VALID_AST_PARSER_CUSTOM_TESTS,
7
7
  } from "./test-utils/ast-cases";
8
8
  import type { ParserSuccessResult, ParserFailureResult } from "../ast";
9
- import { parse as parseParsimmon } from "./parsimmon";
10
- import { parse as parseEBNF } from "./ebnf";
11
9
  import { parse as parseCustom } from "../custom";
12
10
 
13
- describe("parsimmon", () => {
14
- test.each(VALID_AST_PARSER_TESTS)("Parsimmon Valid: %s", (binding, AST) => {
15
- const result = parseParsimmon(binding);
16
-
17
- expect(result.status).toBe(true);
18
- expect((result as ParserSuccessResult).path).toStrictEqual(AST);
19
- });
20
-
21
- test.each(INVALID_AST_PARSER_TESTS)("Parsimmon Invalid: %s", (binding) => {
22
- const result = parseParsimmon(binding);
23
- expect(result.status).toBe(false);
24
- expect((result as ParserFailureResult).error.length > 0).toBe(true);
25
- });
26
- });
27
-
28
- describe("ebnf", () => {
29
- test.each(VALID_AST_PARSER_TESTS)("EBNF Valid: %s", (binding, AST) => {
30
- const result = parseEBNF(binding);
31
-
32
- expect(result.status).toBe(true);
33
- expect((result as ParserSuccessResult).path).toStrictEqual(AST);
34
- });
35
-
36
- test.each(INVALID_AST_PARSER_TESTS)("EBNF Invalid: %s", (binding) => {
37
- const result = parseEBNF(binding);
38
- expect(result.status).toBe(false);
39
- expect((result as ParserFailureResult).error.length > 0).toBe(true);
40
- });
41
- });
42
-
43
11
  describe("custom", () => {
44
12
  test.each(VALID_AST_PARSER_TESTS)("Custom Valid: %s", (binding, AST) => {
45
13
  const result = parseCustom(binding);
@@ -54,6 +54,31 @@ export const VALID_AST_PARSER_TESTS: Array<[string, PathNode]> = [
54
54
  toQuery(toValue("Month"), toValue("New[.]]Mo,nth.")),
55
55
  ]),
56
56
  ],
57
+ // Boolean values only treated as bools for query compare values
58
+ [
59
+ `foo.true['true'=true]`,
60
+ toPath([
61
+ toValue("foo"),
62
+ toValue("true"),
63
+ toQuery(toValue("true"), toValue(true)),
64
+ ]),
65
+ ],
66
+ [
67
+ `foo.false['false'=false]`,
68
+ toPath([
69
+ toValue("foo"),
70
+ toValue("false"),
71
+ toQuery(toValue("false"), toValue(false)),
72
+ ]),
73
+ ],
74
+ [
75
+ `foo.bar[baz == true]`,
76
+ toPath([
77
+ toValue("foo"),
78
+ toValue("bar"),
79
+ toQuery(toValue("baz"), toValue(true)),
80
+ ]),
81
+ ],
57
82
 
58
83
  // Nested Paths
59
84
  ["{{foo}}", toPath([toPath([toValue("foo")])])],
@@ -176,6 +201,12 @@ export const VALID_AST_PARSER_TESTS: Array<[string, PathNode]> = [
176
201
  toValue("baz"),
177
202
  ]),
178
203
  ],
204
+
205
+ // With numbers
206
+ [
207
+ "foo.0[1=2]",
208
+ toPath([toValue("foo"), toValue(0), toQuery(toValue(1), toValue(2))]),
209
+ ],
179
210
  ];
180
211
 
181
212
  export const INVALID_AST_PARSER_TESTS: Array<string> = [
@@ -27,7 +27,7 @@ export interface QueryNode extends Node<"Query"> {
27
27
  /** A simple segment */
28
28
  export interface ValueNode extends Node<"Value"> {
29
29
  /** The segment value */
30
- value: string | number;
30
+ value: string | number | boolean;
31
31
  }
32
32
 
33
33
  /** A nested expression */
@@ -37,7 +37,7 @@ export interface ExpressionNode extends Node<"Expression"> {
37
37
  }
38
38
 
39
39
  /** Helper to create a value node */
40
- export const toValue = (value: string | number): ValueNode => ({
40
+ export const toValue = (value: string | number | boolean): ValueNode => ({
41
41
  name: "Value",
42
42
  value,
43
43
  });
@@ -76,7 +76,7 @@ export const parse: Parser = (path) => {
76
76
  };
77
77
 
78
78
  /** get an identifier if you can */
79
- const identifier = (): ValueNode | undefined => {
79
+ const identifier = (allowBoolValue = false): ValueNode | undefined => {
80
80
  if (!isIdentifierChar(ch)) {
81
81
  return;
82
82
  }
@@ -91,6 +91,15 @@ export const parse: Parser = (path) => {
91
91
  value += ch;
92
92
  }
93
93
 
94
+ if (allowBoolValue) {
95
+ if (value === "true") {
96
+ return toValue(true);
97
+ }
98
+ if (value === "false") {
99
+ return toValue(false);
100
+ }
101
+ }
102
+
94
103
  if (value) {
95
104
  const maybeNumber = Number(value);
96
105
  value = isNaN(maybeNumber) ? value : maybeNumber;
@@ -156,7 +165,8 @@ export const parse: Parser = (path) => {
156
165
  };
157
166
 
158
167
  /** get a simple segment node */
159
- const simpleSegment = () => nestedPath() ?? expression() ?? identifier();
168
+ const simpleSegment = (allowBoolValue = false) =>
169
+ nestedPath() ?? expression() ?? identifier(allowBoolValue);
160
170
 
161
171
  /** Parse a segment */
162
172
  const segment = ():
@@ -182,11 +192,9 @@ export const parse: Parser = (path) => {
182
192
  };
183
193
 
184
194
  /** get an optionally quoted block */
185
- const optionallyQuotedSegment = ():
186
- | ValueNode
187
- | PathNode
188
- | ExpressionNode
189
- | undefined => {
195
+ const optionallyQuotedSegment = (
196
+ allowBoolValue = false,
197
+ ): ValueNode | PathNode | ExpressionNode | undefined => {
190
198
  whitespace();
191
199
 
192
200
  // see if we have a quote
@@ -199,7 +207,7 @@ export const parse: Parser = (path) => {
199
207
  return id;
200
208
  }
201
209
 
202
- return simpleSegment();
210
+ return simpleSegment(allowBoolValue);
203
211
  };
204
212
 
205
213
  /** eat equals signs */
@@ -231,7 +239,7 @@ export const parse: Parser = (path) => {
231
239
  whitespace();
232
240
  if (equals()) {
233
241
  whitespace();
234
- const second = optionallyQuotedSegment();
242
+ const second = optionallyQuotedSegment(true);
235
243
  value = toQuery(value, second);
236
244
  whitespace();
237
245
  }
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, test } from "vitest";
1
+ import { describe, it, expect, test, vi } from "vitest";
2
2
  import { LocalModel, withParser } from "../../data";
3
3
  import { ExpressionEvaluator } from "../../expressions";
4
4
  import { BindingParser } from "../../binding";
@@ -1014,4 +1014,64 @@ describe("view", () => {
1014
1014
 
1015
1015
  expect(view).toBeDefined();
1016
1016
  });
1017
+
1018
+ test("should call onUpdate with undefined when triggering an async update with no resolver", () => {
1019
+ const model = withParser(new LocalModel({}), parseBinding);
1020
+ const evaluator = new ExpressionEvaluator({ model });
1021
+ const schema = new SchemaController();
1022
+
1023
+ const view = new ViewInstance(
1024
+ {
1025
+ id: "view",
1026
+ type: "asset",
1027
+ },
1028
+ {
1029
+ model,
1030
+ parseBinding,
1031
+ evaluator,
1032
+ schema,
1033
+ },
1034
+ );
1035
+ new StringResolverPlugin().apply(view);
1036
+
1037
+ const onUpdateTap = vi.fn();
1038
+ view.hooks.onUpdate.tap("test", onUpdateTap);
1039
+
1040
+ view.updateAsync("test");
1041
+
1042
+ expect(onUpdateTap).toHaveBeenCalledWith(undefined);
1043
+ });
1044
+
1045
+ test("should call onUpdate with the view when triggering an async update with a resolver", () => {
1046
+ const model = withParser(new LocalModel({}), parseBinding);
1047
+ const evaluator = new ExpressionEvaluator({ model });
1048
+ const schema = new SchemaController();
1049
+
1050
+ const view = new ViewInstance(
1051
+ {
1052
+ id: "view",
1053
+ type: "asset",
1054
+ },
1055
+ {
1056
+ model,
1057
+ parseBinding,
1058
+ evaluator,
1059
+ schema,
1060
+ },
1061
+ );
1062
+
1063
+ new StringResolverPlugin().apply(view);
1064
+
1065
+ const onUpdateTap = vi.fn();
1066
+
1067
+ // Trigger first update to get resolver ready.
1068
+ view.update();
1069
+
1070
+ view.hooks.onUpdate.tap("test", onUpdateTap);
1071
+ view.updateAsync("test");
1072
+ expect(onUpdateTap).toHaveBeenCalledWith({
1073
+ id: "view",
1074
+ type: "asset",
1075
+ });
1076
+ });
1017
1077
  });
@@ -62,11 +62,16 @@ export class Builder {
62
62
  *
63
63
  * @param id - the id of async node. It should be identical for each async node
64
64
  */
65
- static asyncNode(id: string, flatten = true): Node.Async {
65
+ static asyncNode(
66
+ id: string,
67
+ flatten = true,
68
+ onValueReceived?: (node: Node.Node) => Node.Node,
69
+ ): Node.Async {
66
70
  return {
67
71
  id,
68
72
  type: NodeType.Async,
69
73
  flatten: flatten,
74
+ onValueReceived,
70
75
  value: {
71
76
  type: NodeType.Value,
72
77
  value: {
@@ -21,6 +21,47 @@ export interface ParseObjectChildOptions {
21
21
  parentObj: object;
22
22
  }
23
23
 
24
+ export type ParserHooks = {
25
+ /**
26
+ * A hook to interact with an object _before_ parsing it into an AST
27
+ *
28
+ * @param value - The object we're are about to parse
29
+ * @returns - A new value to parse.
30
+ * If undefined, the original value is used.
31
+ * If null, we stop parsing this node.
32
+ */
33
+ onParseObject: SyncWaterfallHook<[object, NodeType]>;
34
+ /**
35
+ * A callback to interact with an AST _after_ we parse it into the AST
36
+ *
37
+ * @param value - The object we parsed
38
+ * @param node - The AST node we generated
39
+ * @returns - A new AST node to use
40
+ * If undefined, the original value is used.
41
+ * If null, we ignore this node all together
42
+ */
43
+ onCreateASTNode: SyncWaterfallHook<[Node.Node | undefined | null, object]>;
44
+ /** A hook to call when parsing an object into an AST node
45
+ *
46
+ * @param obj - The object we're are about to parse
47
+ * @param nodeType - The type of node we're parsing
48
+ * @param parseOptions - Additional options when parsing
49
+ * @param childOptions - Additional options that are populated when the node being parsed is a child of another node
50
+ * @returns - A new AST node to use
51
+ * If undefined, the original value is used.
52
+ * If null, we ignore this node all together
53
+ */
54
+ parseNode: SyncBailHook<
55
+ [
56
+ obj: object,
57
+ nodeType: Node.ChildrenTypes,
58
+ parseOptions: ParseObjectOptions,
59
+ childOptions?: ParseObjectChildOptions,
60
+ ],
61
+ Node.Node | Node.Child[]
62
+ >;
63
+ };
64
+
24
65
  interface NestedObj {
25
66
  /** The values of a nested local object */
26
67
  children: Node.Child[];
@@ -32,39 +73,10 @@ interface NestedObj {
32
73
  * It provides a few ways to interact with the parsing, including mutating an object before and after creation of an AST node
33
74
  */
34
75
  export class Parser {
35
- public readonly hooks = {
36
- /**
37
- * A hook to interact with an object _before_ parsing it into an AST
38
- *
39
- * @param value - The object we're are about to parse
40
- * @returns - A new value to parse.
41
- * If undefined, the original value is used.
42
- * If null, we stop parsing this node.
43
- */
44
- onParseObject: new SyncWaterfallHook<[object, NodeType]>(),
45
-
46
- /**
47
- * A callback to interact with an AST _after_ we parse it into the AST
48
- *
49
- * @param value - The object we parsed
50
- * @param node - The AST node we generated
51
- * @returns - A new AST node to use
52
- * If undefined, the original value is used.
53
- * If null, we ignore this node all together
54
- */
55
- onCreateASTNode: new SyncWaterfallHook<
56
- [Node.Node | undefined | null, object]
57
- >(),
58
-
59
- parseNode: new SyncBailHook<
60
- [
61
- obj: object,
62
- nodeType: Node.ChildrenTypes,
63
- parseOptions: ParseObjectOptions,
64
- childOptions?: ParseObjectChildOptions,
65
- ],
66
- Node.Node | Node.Child[]
67
- >(),
76
+ public readonly hooks: ParserHooks = {
77
+ onParseObject: new SyncWaterfallHook(),
78
+ onCreateASTNode: new SyncWaterfallHook(),
79
+ parseNode: new SyncBailHook(),
68
80
  };
69
81
 
70
82
  public parseView(value: AnyAssetType): Node.View {
@@ -22,6 +22,9 @@ export declare namespace Node {
22
22
 
23
23
  /** Every node (outside of the root) contains a reference to it's parent */
24
24
  parent?: Node;
25
+
26
+ /** The ids of async nodes resolved within this node */
27
+ asyncNodesResolved?: string[];
25
28
  }
26
29
 
27
30
  export type PathSegment = string | number;
@@ -123,6 +126,8 @@ export declare namespace Node {
123
126
  * Should the content streamed in be flattened during resolving
124
127
  */
125
128
  flatten?: boolean;
129
+ /** Function to run against parsed content from the node to manipulate the content before resolving it. */
130
+ onValueReceived?: (node: Node.Node) => Node.Node;
126
131
  }
127
132
 
128
133
  export interface PluginOptions {
@@ -35,4 +35,40 @@ describe("multi-node", () => {
35
35
  }),
36
36
  ).toMatchSnapshot();
37
37
  });
38
+
39
+ it("should parse an array into a multi node", () => {
40
+ expect(
41
+ parser.parseObject([
42
+ {
43
+ asset: {
44
+ type: "type",
45
+ id: "asset-1",
46
+ },
47
+ },
48
+ ]),
49
+ ).toStrictEqual({
50
+ override: false,
51
+ type: "multi-node",
52
+ values: [
53
+ {
54
+ type: "value",
55
+ value: undefined,
56
+ parent: expect.anything(),
57
+ children: [
58
+ {
59
+ path: ["asset"],
60
+ value: {
61
+ type: "asset",
62
+ parent: expect.anything(),
63
+ value: {
64
+ id: "asset-1",
65
+ type: "type",
66
+ },
67
+ },
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ });
73
+ });
38
74
  });
@@ -10,7 +10,7 @@ import { hasTemplateValues, hasTemplateKey } from "../parser/utils";
10
10
 
11
11
  /** A view plugin to resolve multi nodes */
12
12
  export default class MultiNodePlugin implements ViewPlugin {
13
- applyParser(parser: Parser) {
13
+ applyParser(parser: Parser): void {
14
14
  parser.hooks.parseNode.tap(
15
15
  "multi-node",
16
16
  (
@@ -20,8 +20,7 @@ export default class MultiNodePlugin implements ViewPlugin {
20
20
  childOptions?: ParseObjectChildOptions,
21
21
  ) => {
22
22
  if (
23
- childOptions &&
24
- !hasTemplateKey(childOptions.key) &&
23
+ (childOptions === undefined || !hasTemplateKey(childOptions.key)) &&
25
24
  Array.isArray(obj)
26
25
  ) {
27
26
  const values = obj
@@ -37,10 +36,9 @@ export default class MultiNodePlugin implements ViewPlugin {
37
36
  const multiNode = parser.createASTNode(
38
37
  {
39
38
  type: NodeType.MultiNode,
40
- override: !hasTemplateValues(
41
- childOptions.parentObj,
42
- childOptions.key,
43
- ),
39
+ override:
40
+ childOptions !== undefined &&
41
+ !hasTemplateValues(childOptions.parentObj, childOptions.key),
44
42
  values,
45
43
  },
46
44
  obj,
@@ -56,18 +54,20 @@ export default class MultiNodePlugin implements ViewPlugin {
56
54
  });
57
55
  }
58
56
 
59
- return [
60
- {
61
- path: [...childOptions.path, childOptions.key],
62
- value: multiNode,
63
- },
64
- ];
57
+ return childOptions === undefined
58
+ ? multiNode
59
+ : [
60
+ {
61
+ path: [...childOptions.path, childOptions.key],
62
+ value: multiNode,
63
+ },
64
+ ];
65
65
  }
66
66
  },
67
67
  );
68
68
  }
69
69
 
70
- apply(view: ViewInstance) {
70
+ apply(view: ViewInstance): void {
71
71
  view.hooks.parser.tap("multi-node", this.applyParser.bind(this));
72
72
  }
73
73
  }
@@ -0,0 +1,153 @@
1
+ import { describe, it, beforeEach, vi, expect } from "vitest";
2
+ import { BindingParser } from "../../../binding";
3
+ import { ExpressionEvaluator } from "../../../expressions";
4
+ import { LocalModel, withParser } from "../../../data";
5
+ import { SchemaController } from "../../../schema";
6
+ import { Resolve, Resolver } from "..";
7
+ import type { Node } from "../../parser";
8
+ import { NodeType, Parser } from "../../parser";
9
+
10
+ const simpleViewWithAsync: Node.View = {
11
+ type: NodeType.View,
12
+ children: [
13
+ {
14
+ path: ["value"],
15
+ value: {
16
+ type: NodeType.Async,
17
+ id: "async-node",
18
+ value: {
19
+ type: NodeType.Value,
20
+ value: {
21
+ id: "async-node",
22
+ },
23
+ },
24
+ },
25
+ },
26
+ {
27
+ path: ["value"],
28
+ value: {
29
+ type: NodeType.Value,
30
+ value: {
31
+ id: "value-node",
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ value: {
37
+ type: "view",
38
+ id: "view",
39
+ },
40
+ };
41
+
42
+ describe("Async Node Resolution", () => {
43
+ let resolverOptions: Resolve.ResolverOptions;
44
+
45
+ beforeEach(() => {
46
+ const model = new LocalModel({});
47
+ const parser = new Parser();
48
+ const bindingParser = new BindingParser();
49
+
50
+ resolverOptions = {
51
+ model,
52
+ parseBinding: bindingParser.parse.bind(bindingParser),
53
+ parseNode: parser.parseObject.bind(parser),
54
+ evaluator: new ExpressionEvaluator({
55
+ model: withParser(model, bindingParser.parse),
56
+ }),
57
+ schema: new SchemaController(),
58
+ };
59
+ });
60
+
61
+ it("should", () => {
62
+ const beforeResolveFunction = vi.fn((node: Node.Node | null) => node);
63
+
64
+ const resolver = new Resolver(simpleViewWithAsync, resolverOptions);
65
+ resolver.hooks.beforeResolve.tap("test", beforeResolveFunction);
66
+
67
+ // Update once to setup cache
68
+ resolver.update();
69
+ // Should call beforeResolve once for each node.
70
+ expect(beforeResolveFunction).toHaveBeenCalledTimes(3);
71
+
72
+ // Clear call information before next update.
73
+ beforeResolveFunction.mockClear();
74
+
75
+ // Confirm cache by running another update with no changes.
76
+ resolver.update(new Set());
77
+ // Should not need to call before resolve on cached nodes.
78
+ expect(beforeResolveFunction).toHaveBeenCalledTimes(0);
79
+
80
+ // Updating with changes marked on "async-node". Should invalidate cache for itself and its parent.
81
+ resolver.update(new Set(), new Set(["async-node"]));
82
+ // Should be called for the async node and the view parent.
83
+ expect(beforeResolveFunction).toHaveBeenCalledTimes(2);
84
+ expect(beforeResolveFunction).toHaveBeenCalledWith(
85
+ expect.objectContaining({
86
+ type: NodeType.View,
87
+ value: {
88
+ type: "view",
89
+ id: "view",
90
+ },
91
+ }),
92
+ expect.anything(),
93
+ );
94
+ expect(beforeResolveFunction).toHaveBeenCalledWith(
95
+ expect.objectContaining({
96
+ type: NodeType.Async,
97
+ id: "async-node",
98
+ value: {
99
+ type: NodeType.Value,
100
+ value: {
101
+ id: "async-node",
102
+ },
103
+ },
104
+ }),
105
+ expect.anything(),
106
+ );
107
+ });
108
+
109
+ it("should also", () => {
110
+ const beforeResolveFunction = vi.fn((node: Node.Node | null) => {
111
+ // Add asyncNodesResolved to view to test tracking and invalidation of just the view.
112
+ if (node?.type === NodeType.View) {
113
+ return {
114
+ ...node,
115
+ asyncNodesResolved: ["other-async-id"],
116
+ };
117
+ }
118
+
119
+ return node;
120
+ });
121
+
122
+ const resolver = new Resolver(simpleViewWithAsync, resolverOptions);
123
+ resolver.hooks.beforeResolve.tap("test", beforeResolveFunction);
124
+
125
+ // Update once to setup cache
126
+ resolver.update();
127
+ // Should call beforeResolve once for each node.
128
+ expect(beforeResolveFunction).toHaveBeenCalledTimes(3);
129
+
130
+ // Clear call information before next update.
131
+ beforeResolveFunction.mockClear();
132
+
133
+ // Confirm cache by running another update with no changes.
134
+ resolver.update(new Set());
135
+ // Should not need to call before resolve on cached nodes.
136
+ expect(beforeResolveFunction).toHaveBeenCalledTimes(0);
137
+
138
+ // Updating with changes marked on "async-node". Should invalidate cache for itself and its parent.
139
+ resolver.update(new Set(), new Set(["other-async-id"]));
140
+ // Should be called just for the view.
141
+ expect(beforeResolveFunction).toHaveBeenCalledOnce();
142
+ expect(beforeResolveFunction).toHaveBeenCalledWith(
143
+ expect.objectContaining({
144
+ type: NodeType.View,
145
+ value: {
146
+ type: "view",
147
+ id: "view",
148
+ },
149
+ }),
150
+ expect.anything(),
151
+ );
152
+ });
153
+ });