@microsoft/fast-html 1.0.0-alpha.17 → 1.0.0-alpha.19

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.
@@ -1,3 +1,4 @@
1
+ import { Observable } from "@microsoft/fast-element/observable.js";
1
2
  const openClientSideBinding = "{";
2
3
  const closeClientSideBinding = "}";
3
4
  const openContentBinding = "{{";
@@ -251,8 +252,28 @@ export function getAllPartials(innerHTML, offset = 0, partials = {}) {
251
252
  * @returns A function to access the value from a given path.
252
253
  */
253
254
  export function pathResolver(path, self = false) {
254
- let splitPath = path.split(".");
255
- const usesContext = path.startsWith("../");
255
+ let splitPath = [];
256
+ path.split("../").forEach((pathItem, index) => {
257
+ if (pathItem === "") {
258
+ splitPath.unshift("../");
259
+ }
260
+ else {
261
+ splitPath.push(...pathItem.split("."));
262
+ }
263
+ });
264
+ splitPath = splitPath.map((pathItem, index) => {
265
+ if (pathItem === "../") {
266
+ if (splitPath[index + 1] === "../") {
267
+ return "parentContext";
268
+ }
269
+ return "parent";
270
+ }
271
+ return pathItem;
272
+ });
273
+ return pathWithContextResolver(splitPath, self);
274
+ }
275
+ function pathWithContextResolver(splitPath, self) {
276
+ const usesContext = splitPath[0] === "parent" || splitPath[0] === "parentContext";
256
277
  if (self && !usesContext) {
257
278
  if (splitPath.length > 1) {
258
279
  splitPath = splitPath.slice(1);
@@ -263,31 +284,78 @@ export function pathResolver(path, self = false) {
263
284
  };
264
285
  }
265
286
  }
266
- if (splitPath.length === 1 && !usesContext) {
267
- return (accessibleObject) => {
268
- return accessibleObject === null || accessibleObject === void 0 ? void 0 : accessibleObject[splitPath[0]];
269
- };
270
- }
271
- return (accessibleObject, context) => {
272
- if (usesContext) {
273
- splitPath = [];
274
- path.split("../").forEach(pathItem => {
275
- if (pathItem === "") {
276
- splitPath.unshift("parent");
277
- }
278
- else {
279
- splitPath.push(...pathItem.split("."));
280
- }
281
- });
287
+ if (usesContext) {
288
+ return (accessibleObject, context) => {
282
289
  return splitPath.reduce((previousAccessors, pathItem) => {
283
290
  return previousAccessors === null || previousAccessors === void 0 ? void 0 : previousAccessors[pathItem];
284
291
  }, context);
285
- }
292
+ };
293
+ }
294
+ return (accessibleObject) => {
286
295
  return splitPath.reduce((previousAccessors, pathItem) => {
287
296
  return previousAccessors === null || previousAccessors === void 0 ? void 0 : previousAccessors[pathItem];
288
297
  }, accessibleObject);
289
298
  };
290
299
  }
300
+ export function bindingResolver(path, self = false, parentContext, type, observerMap, contextPath, level) {
301
+ // Cache path during template processing when ObserverMap is provided
302
+ if (observerMap) {
303
+ observerMap.cachePathWithContext({
304
+ path,
305
+ self,
306
+ parentContext,
307
+ contextPath,
308
+ type,
309
+ level,
310
+ rootPath: null,
311
+ context: null,
312
+ });
313
+ }
314
+ return pathResolver(path, self);
315
+ }
316
+ export function expressionResolver(self, expression, parentContext, level, observerMap) {
317
+ // Cache paths from expression during template processing when ObserverMap is provided
318
+ if (observerMap) {
319
+ const paths = extractPathsFromChainedExpression(expression);
320
+ paths.forEach(path => {
321
+ observerMap.cachePathWithContext({
322
+ path,
323
+ self,
324
+ parentContext,
325
+ contextPath: null,
326
+ type: "access",
327
+ level,
328
+ rootPath: null,
329
+ context: null,
330
+ });
331
+ });
332
+ }
333
+ return (x, c) => resolveChainedExpression(x, c, self, expression);
334
+ }
335
+ /**
336
+ * Extracts all paths from a ChainedExpression, including nested expressions
337
+ * @param chainedExpression - The chained expression to extract paths from
338
+ * @returns A Set containing all unique paths found in the expression chain
339
+ */
340
+ export function extractPathsFromChainedExpression(chainedExpression) {
341
+ const paths = new Set();
342
+ function processExpression(expr) {
343
+ // Check left operand (only add if it's not a literal value)
344
+ if (typeof expr.left === "string" && !expr.leftIsValue) {
345
+ paths.add(expr.left);
346
+ }
347
+ // Check right operand (only add if it's not a literal value)
348
+ if (typeof expr.right === "string" && !expr.rightIsValue) {
349
+ paths.add(expr.right);
350
+ }
351
+ }
352
+ let current = chainedExpression;
353
+ while (current !== undefined) {
354
+ processExpression(current.expression);
355
+ current = current.next;
356
+ }
357
+ return paths;
358
+ }
291
359
  /**
292
360
  * Determine if the operand is a value (boolean, number, string) or an accessor.
293
361
  * @param operand
@@ -308,42 +376,79 @@ function isOperandValue(operand) {
308
376
  };
309
377
  }
310
378
  }
379
+ /**
380
+ * Evaluates parts of an expression chain
381
+ * @param parts Each part of an expression chain
382
+ * @param operator The operator between expressions
383
+ * @returns
384
+ */
385
+ function evaluatePartsInExpressionChain(parts, operator) {
386
+ // Process each part recursively and chain them with ||
387
+ const firstPart = getExpressionChain(parts[0]);
388
+ if (firstPart) {
389
+ let current = firstPart;
390
+ for (let i = 1; i < parts.length; i++) {
391
+ const nextPart = getExpressionChain(parts[i]);
392
+ if (nextPart) {
393
+ // Find the end of the current chain
394
+ while (current.next) {
395
+ current = current.next;
396
+ }
397
+ current.next = Object.assign({ operator }, nextPart);
398
+ }
399
+ }
400
+ return firstPart;
401
+ }
402
+ }
311
403
  /**
312
404
  * Gets the expression chain as a configuration object
313
405
  * @param value - The binding string value
314
406
  * @returns - A configuration object containing information about the expression
315
407
  */
316
408
  export function getExpressionChain(value) {
317
- const split = value.split(" ");
318
- let expressionString = "";
319
- let chainedExpression;
320
- // split expressions by chaining operators
321
- split.forEach((splitItem, index) => {
322
- if (splitItem === "&&" || splitItem === "||" || splitItem === "&amp;&amp;") {
323
- chainedExpression = {
324
- expression: getExpression(expressionString),
325
- next: Object.assign({ operator: splitItem }, getExpressionChain(split.slice(index + 1).join(" "))),
326
- };
409
+ // Handle operator precedence: || has lower precedence than &&
410
+ // First, split by || (lowest precedence)
411
+ const orParts = value.split(" || ");
412
+ if (orParts.length > 1) {
413
+ const firstPart = evaluatePartsInExpressionChain(orParts, "||");
414
+ if (firstPart) {
415
+ return firstPart;
327
416
  }
328
- else {
329
- expressionString = `${expressionString ? `${expressionString} ` : ""}${splitItem}`;
417
+ }
418
+ // If no ||, check for && (higher precedence)
419
+ const andParts = value.split(" && ");
420
+ if (andParts.length > 1) {
421
+ // Process each part recursively and chain them with &&
422
+ const firstPart = evaluatePartsInExpressionChain(andParts, "&&");
423
+ if (firstPart) {
424
+ return firstPart;
330
425
  }
331
- });
332
- if (chainedExpression) {
333
- return chainedExpression;
334
426
  }
335
- if (expressionString) {
427
+ // Handle HTML entity version of &&
428
+ const ampParts = value.split(" &amp;&amp; ");
429
+ if (ampParts.length > 1) {
430
+ // Process each part recursively and chain them with &amp;&amp;
431
+ const firstPart = evaluatePartsInExpressionChain(ampParts, "&amp;&amp;");
432
+ if (firstPart) {
433
+ return firstPart;
434
+ }
435
+ }
436
+ // No chaining operators found, create a single expression
437
+ if (value.trim()) {
336
438
  return {
337
- expression: getExpression(expressionString),
439
+ expression: getExpression(value.trim()),
338
440
  };
339
441
  }
340
442
  return void 0;
341
443
  }
342
444
  function getExpression(value) {
343
445
  if (value[0] === "!") {
446
+ const left = value.slice(1);
447
+ const operandValue = isOperandValue(left);
344
448
  return {
345
449
  operator: "!",
346
- left: value.slice(1),
450
+ left,
451
+ leftIsValue: operandValue.isValue,
347
452
  right: null,
348
453
  rightIsValue: null,
349
454
  };
@@ -351,17 +456,22 @@ function getExpression(value) {
351
456
  const split = value.split(" ");
352
457
  if (split.length === 3) {
353
458
  const operator = split[1];
354
- const { value, isValue } = isOperandValue(split[2]);
459
+ const right = split[2];
460
+ const rightOperandValue = isOperandValue(right);
461
+ const left = split[0];
462
+ const leftOperandValue = isOperandValue(left);
355
463
  return {
356
464
  operator,
357
465
  left: split[0],
358
- right: isValue ? value : split[2],
359
- rightIsValue: isValue,
466
+ leftIsValue: leftOperandValue.isValue,
467
+ right: rightOperandValue.isValue ? rightOperandValue.value : right,
468
+ rightIsValue: rightOperandValue.isValue,
360
469
  };
361
470
  }
362
471
  return {
363
472
  operator: "access",
364
473
  left: value,
474
+ leftIsValue: false,
365
475
  right: null,
366
476
  rightIsValue: null,
367
477
  };
@@ -410,19 +520,19 @@ function resolveExpression(x, c, self, expression) {
410
520
  * @param next - The next expression to be chained
411
521
  * @returns - A resolved return value for a binding
412
522
  */
413
- function resolveChainedExpression(x, c, self, expression, next) {
414
- if (next) {
415
- switch (next.operator) {
523
+ function resolveChainedExpression(x, c, self, expression) {
524
+ if (expression.next) {
525
+ switch (expression.next.operator) {
416
526
  case "&&":
417
527
  case "&amp;&amp;":
418
- return (resolveExpression(x, c, self, expression) &&
419
- resolveChainedExpression(x, c, self, next.expression, next.next));
528
+ return (resolveExpression(x, c, self, expression.expression) &&
529
+ resolveChainedExpression(x, c, self, expression.next));
420
530
  case "||":
421
- return (resolveExpression(x, c, self, expression) ||
422
- resolveChainedExpression(x, c, self, next.expression, next.next));
531
+ return (resolveExpression(x, c, self, expression.expression) ||
532
+ resolveChainedExpression(x, c, self, expression.next));
423
533
  }
424
534
  }
425
- return resolveExpression(x, c, self, expression);
535
+ return resolveExpression(x, c, self, expression.expression);
426
536
  }
427
537
  /**
428
538
  * This is the transform utility for rationalizing declarative HTML syntax
@@ -458,8 +568,149 @@ export function transformInnerHTML(innerHTML, index = 0) {
458
568
  * @param self - Where the first item in the path path refers to the item itself (used by repeat).
459
569
  * @param chainedExpression - The chained expression which includes the expression and the next expression
460
570
  * if there is another in the chain
571
+ * @param observerMap - Optional ObserverMap instance for caching paths during template processing
461
572
  * @returns - A binding that resolves the chained expression logic
462
573
  */
463
- export function resolveWhen(self, { expression, next }) {
464
- return (x, c) => resolveChainedExpression(x, c, self, expression, next);
574
+ export function resolveWhen(self, expression, parentContext, level, observerMap) {
575
+ const binding = expressionResolver(self, expression, parentContext, level, observerMap);
576
+ return (x, c) => binding(x, c);
577
+ }
578
+ /**
579
+ * Helper function to determine the data type of an object property
580
+ */
581
+ function getDataType(object) {
582
+ if (Array.isArray(object))
583
+ return "array";
584
+ if (typeof object === "object" && object !== null)
585
+ return "object";
586
+ return "primitive";
587
+ }
588
+ /**
589
+ * Get the next property
590
+ * @param path The dot syntax data path
591
+ * @returns The next property
592
+ */
593
+ export function getNextProperty(path) {
594
+ return path.split(".")[0];
595
+ }
596
+ function sortByDeepestNestingItem(first, second) {
597
+ const firstRelativePathLength = first.length;
598
+ const secondRelativePathLength = second.length;
599
+ return firstRelativePathLength > secondRelativePathLength
600
+ ? -1
601
+ : secondRelativePathLength > firstRelativePathLength
602
+ ? 1
603
+ : 0;
604
+ }
605
+ function assignObservablesToArray(proxiedData, cachePath) {
606
+ const data = proxiedData.map((item) => {
607
+ const originalItem = Object.assign({}, item);
608
+ assignProxyToItemsInArray(item, originalItem, cachePath);
609
+ return Object.assign(item, originalItem);
610
+ });
611
+ Observable.getNotifier(data).subscribe({
612
+ handleChange(subject, args) {
613
+ args.forEach((arg) => {
614
+ if (arg.addedCount > 0) {
615
+ for (let i = arg.addedCount - 1; i >= 0; i--) {
616
+ const item = subject[arg.index + i];
617
+ const originalItem = Object.assign({}, item);
618
+ assignProxyToItemsInArray(item, originalItem, cachePath);
619
+ return Object.assign(item, originalItem);
620
+ }
621
+ }
622
+ });
623
+ },
624
+ });
625
+ return data;
626
+ }
627
+ export function assignObservables(cachePath, data, target, rootProperty) {
628
+ const dataType = getDataType(data);
629
+ let proxiedData = data;
630
+ switch (dataType) {
631
+ case "array": {
632
+ if (cachePath.type === "repeat") {
633
+ proxiedData = assignObservablesToArray(proxiedData, cachePath);
634
+ }
635
+ break;
636
+ }
637
+ case "object": {
638
+ if (cachePath.type === "default") {
639
+ const relativePaths = Object.values(cachePath.paths)
640
+ .map((value) => {
641
+ return value.relativePath.split(".").slice(1); // the first item is the root path
642
+ })
643
+ .sort(sortByDeepestNestingItem);
644
+ for (const relativePath of relativePaths) {
645
+ proxiedData = assignProxyToItemsInObject(relativePath, target, rootProperty, proxiedData, cachePath);
646
+ }
647
+ }
648
+ break;
649
+ }
650
+ }
651
+ return proxiedData;
652
+ }
653
+ function assignProxyToItemsInArray(item, originalItem, cachePath) {
654
+ const itemProperties = Object.keys(item);
655
+ itemProperties.forEach(key => {
656
+ Observable.defineProperty(item, key);
657
+ const relativePaths = Object.values(cachePath.paths).filter(pathItem => {
658
+ if (pathItem.type === "access") {
659
+ return pathItem.relativePath.startsWith(`${cachePath.context}.${key}.`);
660
+ }
661
+ return false;
662
+ })
663
+ .map(value => {
664
+ return value.relativePath.split(".").slice(2); // the first item is the context, the next is the property
665
+ })
666
+ .sort(sortByDeepestNestingItem);
667
+ for (const relativePath of relativePaths) {
668
+ originalItem[key] = assignProxyToItemsInObject(relativePath, item, key, originalItem[key], cachePath);
669
+ }
670
+ });
671
+ }
672
+ function assignProxyToItemsInObject(paths, target, rootProperty, data, cachePath) {
673
+ const type = getDataType(data);
674
+ let proxiedData = data;
675
+ if (type === "object") {
676
+ // navigate through all items in the object
677
+ proxiedData[paths[0]] = assignProxyToItemsInObject(paths.slice(1), target, rootProperty, proxiedData[paths[0]], cachePath);
678
+ // assign a Proxy to the object
679
+ proxiedData = assignProxy(cachePath, target, rootProperty, data);
680
+ }
681
+ else if (type === "array") {
682
+ if (cachePath.type === "repeat") {
683
+ proxiedData = assignObservablesToArray(proxiedData, cachePath.paths[rootProperty]);
684
+ }
685
+ }
686
+ return proxiedData;
687
+ }
688
+ export function assignProxy(cachePath, target, rootProperty, object) {
689
+ if (object.$isProxy === undefined) {
690
+ // Create a proxy for the object that triggers Observable.notify on mutations
691
+ return new Proxy(object, {
692
+ set: (obj, prop, value) => {
693
+ obj[prop] = assignObservables(cachePath, value, target, rootProperty);
694
+ // Trigger notification for property changes
695
+ Observable.notify(target, rootProperty);
696
+ return true;
697
+ },
698
+ get: (target, key) => {
699
+ if (key !== "$isProxy") {
700
+ return target[key];
701
+ }
702
+ return true;
703
+ },
704
+ deleteProperty: (obj, prop) => {
705
+ if (prop in obj) {
706
+ delete obj[prop];
707
+ // Trigger notification for property deletion
708
+ Observable.notify(target, rootProperty);
709
+ return true;
710
+ }
711
+ return false;
712
+ },
713
+ });
714
+ }
715
+ return object;
465
716
  }
@@ -1,6 +1,6 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { expect, test } from "@playwright/test";
3
- import { getNextBehavior, getAllPartials, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML, getExpressionChain } from "./utilities.js";
3
+ import { getNextBehavior, getAllPartials, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML, getExpressionChain, extractPathsFromChainedExpression, } from "./utilities.js";
4
4
  test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* () {
5
5
  test.describe("content", () => __awaiter(void 0, void 0, void 0, function* () {
6
6
  test("get the next content binding", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -192,6 +192,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
192
192
  expression: {
193
193
  operator: "access",
194
194
  left: "foo",
195
+ leftIsValue: false,
195
196
  right: null,
196
197
  rightIsValue: null,
197
198
  }
@@ -202,6 +203,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
202
203
  expression: {
203
204
  operator: "!",
204
205
  left: "foo",
206
+ leftIsValue: false,
205
207
  right: null,
206
208
  rightIsValue: null,
207
209
  }
@@ -212,6 +214,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
212
214
  expression: {
213
215
  operator: "!=",
214
216
  left: "foo",
217
+ leftIsValue: false,
215
218
  right: "test",
216
219
  rightIsValue: true,
217
220
  }
@@ -222,6 +225,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
222
225
  expression: {
223
226
  operator: "!=",
224
227
  left: "foo",
228
+ leftIsValue: false,
225
229
  right: false,
226
230
  rightIsValue: true,
227
231
  }
@@ -232,6 +236,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
232
236
  expression: {
233
237
  operator: "!=",
234
238
  left: "foo",
239
+ leftIsValue: false,
235
240
  right: 5,
236
241
  rightIsValue: true,
237
242
  }
@@ -242,6 +247,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
242
247
  expression: {
243
248
  operator: "!=",
244
249
  left: "foo",
250
+ leftIsValue: false,
245
251
  right: "bat",
246
252
  rightIsValue: true,
247
253
  },
@@ -250,6 +256,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
250
256
  expression: {
251
257
  operator: "==",
252
258
  left: "bar",
259
+ leftIsValue: false,
253
260
  right: "baz",
254
261
  rightIsValue: true,
255
262
  }
@@ -259,6 +266,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
259
266
  expression: {
260
267
  operator: "access",
261
268
  left: "foo",
269
+ leftIsValue: false,
262
270
  right: null,
263
271
  rightIsValue: null,
264
272
  },
@@ -267,6 +275,7 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
267
275
  expression: {
268
276
  operator: "access",
269
277
  left: "bar",
278
+ leftIsValue: false,
270
279
  right: null,
271
280
  rightIsValue: null,
272
281
  }
@@ -274,4 +283,103 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
274
283
  });
275
284
  }));
276
285
  }));
286
+ test.describe("extractPathsFromChainedExpression", () => __awaiter(void 0, void 0, void 0, function* () {
287
+ test("should extract paths from simple access expression", () => __awaiter(void 0, void 0, void 0, function* () {
288
+ const expressionChain = getExpressionChain("user");
289
+ expect(expressionChain).toBeDefined();
290
+ const paths = extractPathsFromChainedExpression(expressionChain);
291
+ expect(paths.size).toEqual(1);
292
+ expect(paths.has("user")).toBe(true);
293
+ }));
294
+ test("should extract paths from dot notation expression", () => __awaiter(void 0, void 0, void 0, function* () {
295
+ const expressionChain = getExpressionChain("user.name");
296
+ expect(expressionChain).toBeDefined();
297
+ const paths = extractPathsFromChainedExpression(expressionChain);
298
+ expect(paths.size).toEqual(1);
299
+ expect(paths.has("user.name")).toBe(true);
300
+ }));
301
+ test("should extract paths from comparison with literal values", () => __awaiter(void 0, void 0, void 0, function* () {
302
+ const expressionChain = getExpressionChain("user.age > 18");
303
+ expect(expressionChain).toBeDefined();
304
+ const paths = extractPathsFromChainedExpression(expressionChain);
305
+ expect(paths.size).toEqual(1);
306
+ expect(paths.has("user.age")).toBe(true);
307
+ }));
308
+ test("should extract paths from comparison between two properties", () => __awaiter(void 0, void 0, void 0, function* () {
309
+ const expressionChain = getExpressionChain("user.age >= admin.minAge");
310
+ expect(expressionChain).toBeDefined();
311
+ const paths = extractPathsFromChainedExpression(expressionChain);
312
+ expect(paths.size).toEqual(2);
313
+ expect(paths.has("user.age")).toBe(true);
314
+ expect(paths.has("admin.minAge")).toBe(true);
315
+ }));
316
+ test("should extract paths from chained AND expressions", () => __awaiter(void 0, void 0, void 0, function* () {
317
+ const expressionChain = getExpressionChain("isActive && user.name");
318
+ expect(expressionChain).toBeDefined();
319
+ const paths = extractPathsFromChainedExpression(expressionChain);
320
+ expect(paths.size).toEqual(2);
321
+ expect(paths.has("isActive")).toBe(true);
322
+ expect(paths.has("user.name")).toBe(true);
323
+ }));
324
+ test("should extract paths from chained OR expressions", () => __awaiter(void 0, void 0, void 0, function* () {
325
+ const expressionChain = getExpressionChain("user.isAdmin || permissions.canEdit");
326
+ expect(expressionChain).toBeDefined();
327
+ const paths = extractPathsFromChainedExpression(expressionChain);
328
+ expect(paths.size).toEqual(2);
329
+ expect(paths.has("user.isAdmin")).toBe(true);
330
+ expect(paths.has("permissions.canEdit")).toBe(true);
331
+ }));
332
+ test("should extract paths from complex chained expressions", () => __awaiter(void 0, void 0, void 0, function* () {
333
+ const expressionChain = getExpressionChain("user.age > 18 && user.status == 'active' || admin.override");
334
+ expect(expressionChain).toBeDefined();
335
+ const paths = extractPathsFromChainedExpression(expressionChain);
336
+ expect(paths.size).toEqual(3);
337
+ expect(paths.has("user.age")).toBe(true);
338
+ expect(paths.has("user.status")).toBe(true);
339
+ expect(paths.has("admin.override")).toBe(true);
340
+ }));
341
+ test("should extract paths from NOT expressions", () => __awaiter(void 0, void 0, void 0, function* () {
342
+ const expressionChain = getExpressionChain("!user.isDisabled");
343
+ expect(expressionChain).toBeDefined();
344
+ const paths = extractPathsFromChainedExpression(expressionChain);
345
+ expect(paths.size).toEqual(1);
346
+ expect(paths.has("user.isDisabled")).toBe(true);
347
+ }));
348
+ test("should handle expressions with only literal values", () => __awaiter(void 0, void 0, void 0, function* () {
349
+ const expressionChain = getExpressionChain("5 > 3");
350
+ expect(expressionChain).toBeDefined();
351
+ const paths = extractPathsFromChainedExpression(expressionChain);
352
+ expect(paths.size).toEqual(0);
353
+ }));
354
+ test("should handle mixed literal and property expressions", () => __awaiter(void 0, void 0, void 0, function* () {
355
+ const expressionChain = getExpressionChain("count > 0 && status == 'ready'");
356
+ expect(expressionChain).toBeDefined();
357
+ const paths = extractPathsFromChainedExpression(expressionChain);
358
+ expect(paths.size).toEqual(2);
359
+ expect(paths.has("count")).toBe(true);
360
+ expect(paths.has("status")).toBe(true);
361
+ }));
362
+ test("should deduplicate identical paths", () => __awaiter(void 0, void 0, void 0, function* () {
363
+ const expressionChain = getExpressionChain("user.name && user.name != 'anonymous'");
364
+ expect(expressionChain).toBeDefined();
365
+ const paths = extractPathsFromChainedExpression(expressionChain);
366
+ expect(paths.size).toEqual(1);
367
+ expect(paths.has("user.name")).toBe(true);
368
+ }));
369
+ test("should handle HTML entity operators", () => __awaiter(void 0, void 0, void 0, function* () {
370
+ const expressionChain = getExpressionChain("isValid &amp;&amp; data.ready");
371
+ expect(expressionChain).toBeDefined();
372
+ const paths = extractPathsFromChainedExpression(expressionChain);
373
+ expect(paths.size).toEqual(2);
374
+ expect(paths.has("isValid")).toBe(true);
375
+ expect(paths.has("data.ready")).toBe(true);
376
+ }));
377
+ test("should handle deeply nested property paths", () => __awaiter(void 0, void 0, void 0, function* () {
378
+ const expressionChain = getExpressionChain("app.user.profile.settings.theme");
379
+ expect(expressionChain).toBeDefined();
380
+ const paths = extractPathsFromChainedExpression(expressionChain);
381
+ expect(paths.size).toEqual(1);
382
+ expect(paths.has("app.user.profile.settings.theme")).toBe(true);
383
+ }));
384
+ }));
277
385
  }));