@usertour/helpers 0.0.17 → 0.0.20

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.
@@ -0,0 +1,928 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/__tests__/condition.test.ts
26
+ var import_types3 = require("@usertour/types");
27
+
28
+ // src/conditions/condition.ts
29
+ var import_types2 = require("@usertour/types");
30
+ var import_fast_deep_equal = __toESM(require("fast-deep-equal"), 1);
31
+
32
+ // src/conditions/url-v2.ts
33
+ function parseUrl(url) {
34
+ const match = url.match(/^(([a-z\d]+):\/\/)?([^/?#]+)?(\/[^?#]*)?(\?([^#]*))?(#.*)?$/i);
35
+ if (!match)
36
+ return null;
37
+ const [, , scheme, domain, path, , query, fragment] = match;
38
+ return {
39
+ scheme: scheme || "",
40
+ domain: domain || "",
41
+ path: path || "",
42
+ query: query || "",
43
+ fragment: fragment || ""
44
+ };
45
+ }
46
+ function escapeRegex(str) {
47
+ return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
48
+ }
49
+ function processUrlPattern(pattern, wildcardReplacement, paramReplacement) {
50
+ let processed = escapeRegex(pattern);
51
+ processed = processed.replace(/\\\*/g, `${wildcardReplacement}*`);
52
+ if (paramReplacement) {
53
+ processed = processed.replace(/:[a-zA-Z0-9_]+/g, `[^${paramReplacement}]+`);
54
+ }
55
+ return processed;
56
+ }
57
+ function urlPatternToRegex(pattern) {
58
+ const parsed = parseUrl(pattern);
59
+ if (!parsed) {
60
+ return null;
61
+ }
62
+ const { scheme, domain, path, query, fragment } = parsed;
63
+ const schemePattern = scheme ? escapeRegex(scheme) : "[a-z\\d]+";
64
+ const domainPattern = domain ? processUrlPattern(domain, "[^/]", ".") : "[^/]*";
65
+ const portPattern = "(:\\d+)?";
66
+ const pathPattern = path ? processUrlPattern(path, "[^?#]", "/") : "/[^?#]*";
67
+ let queryPattern;
68
+ if (query) {
69
+ queryPattern = "";
70
+ new URLSearchParams(query).forEach((value, key) => {
71
+ let valuePattern;
72
+ if (value === "") {
73
+ valuePattern = "=?";
74
+ } else if (value === "*") {
75
+ valuePattern = "(=[^&#]*)?";
76
+ } else {
77
+ const encodedValue = value.split(/\*/g).map((part) => encodeURI(part)).join("*");
78
+ valuePattern = `=${processUrlPattern(encodedValue, "[^#]")}`;
79
+ }
80
+ queryPattern += `(?=.*[?&]${escapeRegex(key)}${valuePattern}([&#]|$))`;
81
+ });
82
+ queryPattern += "\\?[^#]*";
83
+ } else {
84
+ queryPattern = "(\\?[^#]*)?";
85
+ }
86
+ const fragmentPattern = fragment ? processUrlPattern(fragment, ".", "/") : "(#.*)?";
87
+ return new RegExp(
88
+ `^${schemePattern}://${domainPattern}${portPattern}${pathPattern}${queryPattern}${fragmentPattern}$`
89
+ );
90
+ }
91
+ function matchUrlPattern(urlPattern, url) {
92
+ if (urlPattern.includes.length === 0 && urlPattern.excludes.length === 0) {
93
+ return false;
94
+ }
95
+ const matchesInclude = urlPattern.includes.length === 0 || urlPattern.includes.some((includePattern) => {
96
+ const regex = urlPatternToRegex(includePattern);
97
+ return regex && url.match(regex);
98
+ });
99
+ const matchesExclude = urlPattern.excludes.some((excludePattern) => {
100
+ const regex = urlPatternToRegex(excludePattern);
101
+ return regex && url.match(regex);
102
+ });
103
+ return matchesInclude && !matchesExclude;
104
+ }
105
+
106
+ // src/conditions/url.ts
107
+ var isMatchUrlPattern = (_url, includes, excludes) => {
108
+ return matchUrlPattern({ includes, excludes }, _url);
109
+ };
110
+ var evaluateUrlCondition = (rules, url) => {
111
+ const { excludes = [], includes = [] } = rules.data || {};
112
+ return isMatchUrlPattern(url, includes, excludes);
113
+ };
114
+
115
+ // src/conditions/time.ts
116
+ var import_date_fns = require("date-fns");
117
+ var evaluateTimeCondition = (rules) => {
118
+ try {
119
+ const { endDate, endDateHour, endDateMinute, startDate, startDateHour, startDateMinute } = rules.data || {};
120
+ if (!startDate || !startDateHour || !startDateMinute) {
121
+ return false;
122
+ }
123
+ const startTimeString = `${startDate}T${startDateHour.padStart(2, "0")}:${startDateMinute.padStart(2, "0")}:00`;
124
+ const startTime = (0, import_date_fns.parseISO)(startTimeString);
125
+ if (!(0, import_date_fns.isValid)(startTime)) {
126
+ return false;
127
+ }
128
+ const now = /* @__PURE__ */ new Date();
129
+ if (!endDate || !endDateHour || !endDateMinute) {
130
+ return (0, import_date_fns.isAfter)(now, startTime);
131
+ }
132
+ const endTimeString = `${endDate}T${endDateHour.padStart(2, "0")}:${endDateMinute.padStart(2, "0")}:00`;
133
+ const endTime = (0, import_date_fns.parseISO)(endTimeString);
134
+ if (!(0, import_date_fns.isValid)(endTime)) {
135
+ return false;
136
+ }
137
+ return (0, import_date_fns.isAfter)(now, startTime) && (0, import_date_fns.isBefore)(now, endTime);
138
+ } catch {
139
+ return false;
140
+ }
141
+ };
142
+
143
+ // src/conditions/attribute.ts
144
+ var import_types = require("@usertour/types");
145
+ var import_date_fns2 = require("date-fns");
146
+ function evaluateAttributeCondition(condition, attributes, userAttributes) {
147
+ const { data } = condition;
148
+ if (!data) {
149
+ return false;
150
+ }
151
+ const { logic, value, attrId, value2, listValues = [] } = data;
152
+ if (!attrId) {
153
+ return false;
154
+ }
155
+ const attr = attributes.find((attr2) => attr2.id === attrId);
156
+ if (!attr) {
157
+ return false;
158
+ }
159
+ const actualValue = getAttributeValue(attr.codeName, userAttributes);
160
+ if (attr.dataType === import_types.BizAttributeTypes.String) {
161
+ return evaluateStringCondition(logic, actualValue, value);
162
+ }
163
+ if (attr.dataType === import_types.BizAttributeTypes.Number) {
164
+ return evaluateNumberCondition(logic, actualValue, value, value2);
165
+ }
166
+ if (attr.dataType === import_types.BizAttributeTypes.Boolean) {
167
+ return evaluateBooleanCondition(logic, actualValue);
168
+ }
169
+ if (attr.dataType === import_types.BizAttributeTypes.List) {
170
+ return evaluateListCondition(logic, actualValue, listValues);
171
+ }
172
+ if (attr.dataType === import_types.BizAttributeTypes.DateTime) {
173
+ return evaluateDateTimeCondition(logic, actualValue, value);
174
+ }
175
+ return false;
176
+ }
177
+ function getAttributeValue(codeName, userAttributes) {
178
+ return userAttributes == null ? void 0 : userAttributes[codeName];
179
+ }
180
+ function evaluateStringCondition(logic, actualValue, expectedValue) {
181
+ const stringValue = actualValue === null || actualValue === void 0 ? "" : String(actualValue);
182
+ switch (logic) {
183
+ case "is":
184
+ return stringValue === expectedValue;
185
+ case "not":
186
+ return stringValue !== expectedValue;
187
+ case "contains":
188
+ return stringValue.includes(expectedValue);
189
+ case "notContain":
190
+ return !stringValue.includes(expectedValue);
191
+ case "startsWith":
192
+ return stringValue.startsWith(expectedValue);
193
+ case "endsWith":
194
+ return stringValue.endsWith(expectedValue);
195
+ case "empty": {
196
+ const isEmpty = !stringValue || stringValue === "";
197
+ return isEmpty;
198
+ }
199
+ case "any":
200
+ return Boolean(stringValue && stringValue !== "");
201
+ default:
202
+ return false;
203
+ }
204
+ }
205
+ function evaluateNumberCondition(logic, actualValue, expectedValue, expectedValue2) {
206
+ const numValue = Number(actualValue);
207
+ const numValue2 = Number(expectedValue2);
208
+ if (Number.isNaN(numValue)) {
209
+ return false;
210
+ }
211
+ switch (logic) {
212
+ case "is":
213
+ return numValue === expectedValue;
214
+ case "not":
215
+ return numValue !== expectedValue;
216
+ case "isLessThan":
217
+ return numValue < expectedValue;
218
+ case "isLessThanOrEqualTo":
219
+ return numValue <= expectedValue;
220
+ case "isGreaterThan":
221
+ return numValue > expectedValue;
222
+ case "isGreaterThanOrEqualTo":
223
+ return numValue >= expectedValue;
224
+ case "between":
225
+ return numValue >= expectedValue && numValue <= numValue2;
226
+ case "empty":
227
+ return actualValue === null || actualValue === void 0 || actualValue === "";
228
+ case "any":
229
+ return actualValue !== null && actualValue !== void 0 && actualValue !== "";
230
+ default:
231
+ return false;
232
+ }
233
+ }
234
+ function evaluateBooleanCondition(logic, actualValue) {
235
+ switch (logic) {
236
+ case "true":
237
+ return actualValue === true;
238
+ case "false":
239
+ return actualValue === false;
240
+ case "empty":
241
+ return actualValue === null || actualValue === void 0 || actualValue === "";
242
+ case "any":
243
+ return actualValue !== null && actualValue !== void 0 && actualValue !== "";
244
+ default:
245
+ return false;
246
+ }
247
+ }
248
+ function evaluateListCondition(logic, actualValue, expectedValues) {
249
+ const arrayValue = Array.isArray(actualValue) ? actualValue : [];
250
+ if (logic === "empty" || logic === "any") {
251
+ switch (logic) {
252
+ case "empty":
253
+ return !arrayValue || arrayValue.length === 0;
254
+ case "any":
255
+ return arrayValue && arrayValue.length > 0;
256
+ default:
257
+ return false;
258
+ }
259
+ }
260
+ const filteredValues = expectedValues.filter(
261
+ (value) => value !== null && value !== void 0 && value !== ""
262
+ );
263
+ if (!filteredValues.length) {
264
+ return false;
265
+ }
266
+ switch (logic) {
267
+ case "includesAtLeastOne":
268
+ return filteredValues.some((value) => arrayValue.includes(value));
269
+ case "includesAll":
270
+ return filteredValues.every((value) => arrayValue.includes(value));
271
+ case "notIncludesAtLeastOne":
272
+ return !filteredValues.some((value) => arrayValue.includes(value));
273
+ case "notIncludesAll":
274
+ return !filteredValues.every((value) => arrayValue.includes(value));
275
+ default:
276
+ return false;
277
+ }
278
+ }
279
+ function evaluateDateTimeCondition(logic, actualValue, expectedValue) {
280
+ const actualDate = actualValue ? new Date(actualValue) : null;
281
+ const now = /* @__PURE__ */ new Date();
282
+ if (!actualDate || Number.isNaN(actualDate.getTime())) {
283
+ return false;
284
+ }
285
+ switch (logic) {
286
+ case "lessThan": {
287
+ const targetDate = (0, import_date_fns2.subDays)(now, Number(expectedValue));
288
+ return actualDate >= targetDate;
289
+ }
290
+ case "exactly": {
291
+ const targetDate = (0, import_date_fns2.subDays)(now, Number(expectedValue));
292
+ const start = (0, import_date_fns2.startOfDay)(targetDate);
293
+ const end = (0, import_date_fns2.endOfDay)(targetDate);
294
+ return actualDate >= start && actualDate <= end;
295
+ }
296
+ case "moreThan": {
297
+ const targetDate = (0, import_date_fns2.subDays)(now, Number(expectedValue));
298
+ return actualDate <= targetDate;
299
+ }
300
+ case "before": {
301
+ const expectedDate = new Date(expectedValue);
302
+ return actualDate <= expectedDate;
303
+ }
304
+ case "on": {
305
+ const expectedDateOn = new Date(expectedValue);
306
+ const start = (0, import_date_fns2.startOfDay)(expectedDateOn);
307
+ const end = (0, import_date_fns2.endOfDay)(expectedDateOn);
308
+ return actualDate >= start && actualDate <= end;
309
+ }
310
+ case "after": {
311
+ const expectedDateAfter = new Date(expectedValue);
312
+ return actualDate >= expectedDateAfter;
313
+ }
314
+ case "empty":
315
+ return !actualValue || actualValue === "";
316
+ case "any":
317
+ return actualValue && actualValue !== "";
318
+ default:
319
+ return false;
320
+ }
321
+ }
322
+
323
+ // src/conditions/condition.ts
324
+ var isConditionsActived = (conditions) => {
325
+ if (!conditions || conditions.length === 0) {
326
+ return false;
327
+ }
328
+ const operator = conditions[0].operators;
329
+ const actives = conditions.filter((rule) => {
330
+ if (!rule.conditions) {
331
+ return rule.actived;
332
+ }
333
+ return isConditionsActived(rule.conditions);
334
+ });
335
+ return operator === "and" ? actives.length === conditions.length : actives.length > 0;
336
+ };
337
+ var filterConditionsByType = (conditions, allowedTypes) => {
338
+ return conditions.filter((condition) => {
339
+ if (condition.type === "group" && condition.conditions) {
340
+ const filteredGroupConditions = filterConditionsByType(condition.conditions, allowedTypes);
341
+ return filteredGroupConditions.length > 0;
342
+ }
343
+ return allowedTypes.includes(condition.type);
344
+ }).map((condition) => {
345
+ if (condition.type === "group" && condition.conditions) {
346
+ return {
347
+ ...condition,
348
+ conditions: filterConditionsByType(condition.conditions, allowedTypes)
349
+ };
350
+ }
351
+ return condition;
352
+ });
353
+ };
354
+ var evaluateRule = (rule, options) => {
355
+ var _a;
356
+ const { typeControl = {}, activatedIds, deactivatedIds } = options;
357
+ const ruleId = rule.id;
358
+ if (activatedIds == null ? void 0 : activatedIds.includes(ruleId))
359
+ return true;
360
+ if (deactivatedIds == null ? void 0 : deactivatedIds.includes(ruleId))
361
+ return false;
362
+ if (typeControl[rule.type] === false) {
363
+ return rule.actived || false;
364
+ }
365
+ switch (rule.type) {
366
+ case import_types2.RulesType.CURRENT_PAGE:
367
+ return evaluateUrlCondition(rule, ((_a = options.clientContext) == null ? void 0 : _a.page_url) || "");
368
+ case import_types2.RulesType.TIME:
369
+ return evaluateTimeCondition(rule);
370
+ case import_types2.RulesType.USER_ATTR:
371
+ case import_types2.RulesType.COMPANY_ATTR:
372
+ return evaluateAttributeCondition(
373
+ rule,
374
+ options.attributes || [],
375
+ options.userAttributes || {}
376
+ );
377
+ default:
378
+ return rule.actived || false;
379
+ }
380
+ };
381
+ var activedRulesConditions = (conditions, options = {}) => {
382
+ return conditions.map((rule) => {
383
+ if (rule.type === "group" && rule.conditions) {
384
+ return {
385
+ ...rule,
386
+ conditions: activedRulesConditions(rule.conditions, options)
387
+ };
388
+ }
389
+ return {
390
+ ...rule,
391
+ actived: evaluateRule(rule, options)
392
+ };
393
+ });
394
+ };
395
+
396
+ // src/__tests__/condition.test.ts
397
+ describe("filterConditionsByType", () => {
398
+ test("should return empty array when no conditions provided", () => {
399
+ const result = filterConditionsByType([], [import_types3.RulesType.CURRENT_PAGE, import_types3.RulesType.TIME]);
400
+ expect(result).toEqual([]);
401
+ });
402
+ test("should filter out non-matching types", () => {
403
+ const conditions = [
404
+ {
405
+ id: "1",
406
+ type: "element",
407
+ operators: "and",
408
+ actived: true,
409
+ data: {}
410
+ },
411
+ {
412
+ id: "2",
413
+ type: "current-page",
414
+ operators: "and",
415
+ actived: true,
416
+ data: {}
417
+ }
418
+ ];
419
+ const result = filterConditionsByType(conditions, [import_types3.RulesType.CURRENT_PAGE, import_types3.RulesType.TIME]);
420
+ expect(result).toHaveLength(1);
421
+ expect(result[0].type).toBe("current-page");
422
+ });
423
+ test("should keep matching types", () => {
424
+ const conditions = [
425
+ {
426
+ id: "1",
427
+ type: "current-page",
428
+ operators: "and",
429
+ actived: true,
430
+ data: {}
431
+ },
432
+ {
433
+ id: "2",
434
+ type: "time",
435
+ operators: "and",
436
+ actived: true,
437
+ data: {}
438
+ }
439
+ ];
440
+ const result = filterConditionsByType(conditions, [import_types3.RulesType.CURRENT_PAGE, import_types3.RulesType.TIME]);
441
+ expect(result).toHaveLength(2);
442
+ expect(result.map((c) => c.type)).toEqual(["current-page", "time"]);
443
+ });
444
+ test("should handle nested group conditions", () => {
445
+ const conditions = [
446
+ {
447
+ id: "1",
448
+ type: "group",
449
+ operators: "and",
450
+ data: {},
451
+ conditions: [
452
+ {
453
+ id: "2",
454
+ type: "current-page",
455
+ operators: "and",
456
+ actived: true,
457
+ data: {}
458
+ },
459
+ {
460
+ id: "3",
461
+ type: "element",
462
+ // This should be filtered out
463
+ operators: "and",
464
+ actived: true,
465
+ data: {}
466
+ }
467
+ ]
468
+ }
469
+ ];
470
+ const result = filterConditionsByType(conditions, [import_types3.RulesType.CURRENT_PAGE, import_types3.RulesType.TIME]);
471
+ expect(result).toHaveLength(1);
472
+ expect(result[0].type).toBe("group");
473
+ expect(result[0].conditions).toHaveLength(1);
474
+ expect(result[0].conditions[0].type).toBe("current-page");
475
+ });
476
+ test("should remove groups with no matching conditions", () => {
477
+ const conditions = [
478
+ {
479
+ id: "1",
480
+ type: "group",
481
+ operators: "and",
482
+ data: {},
483
+ conditions: [
484
+ {
485
+ id: "2",
486
+ type: "element",
487
+ // This should be filtered out
488
+ operators: "and",
489
+ actived: true,
490
+ data: {}
491
+ }
492
+ ]
493
+ }
494
+ ];
495
+ const result = filterConditionsByType(conditions, [import_types3.RulesType.CURRENT_PAGE, import_types3.RulesType.TIME]);
496
+ expect(result).toHaveLength(0);
497
+ });
498
+ });
499
+ describe("isConditionsActived", () => {
500
+ test("should return false when no conditions provided", () => {
501
+ const result = isConditionsActived([]);
502
+ expect(result).toBe(false);
503
+ });
504
+ test("should return true when all conditions are actived with AND logic", () => {
505
+ const conditions = [
506
+ {
507
+ id: "1",
508
+ type: "current-page",
509
+ operators: "and",
510
+ actived: true,
511
+ data: {}
512
+ },
513
+ {
514
+ id: "2",
515
+ type: "time",
516
+ operators: "and",
517
+ actived: true,
518
+ data: {}
519
+ }
520
+ ];
521
+ const result = isConditionsActived(conditions);
522
+ expect(result).toBe(true);
523
+ });
524
+ test("should return false when one condition is not actived with AND logic", () => {
525
+ const conditions = [
526
+ {
527
+ id: "1",
528
+ type: "current-page",
529
+ operators: "and",
530
+ actived: true,
531
+ data: {}
532
+ },
533
+ {
534
+ id: "2",
535
+ type: "time",
536
+ operators: "and",
537
+ actived: false,
538
+ data: {}
539
+ }
540
+ ];
541
+ const result = isConditionsActived(conditions);
542
+ expect(result).toBe(false);
543
+ });
544
+ test("should return true when at least one condition is actived with OR logic", () => {
545
+ const conditions = [
546
+ {
547
+ id: "1",
548
+ type: "current-page",
549
+ operators: "or",
550
+ actived: false,
551
+ data: {}
552
+ },
553
+ {
554
+ id: "2",
555
+ type: "time",
556
+ operators: "or",
557
+ actived: true,
558
+ data: {}
559
+ }
560
+ ];
561
+ const result = isConditionsActived(conditions);
562
+ expect(result).toBe(true);
563
+ });
564
+ test("should handle nested group conditions", () => {
565
+ const conditions = [
566
+ {
567
+ id: "1",
568
+ type: "group",
569
+ operators: "and",
570
+ data: {},
571
+ conditions: [
572
+ {
573
+ id: "2",
574
+ type: "current-page",
575
+ operators: "and",
576
+ actived: true,
577
+ data: {}
578
+ },
579
+ {
580
+ id: "3",
581
+ type: "time",
582
+ operators: "and",
583
+ actived: true,
584
+ data: {}
585
+ }
586
+ ]
587
+ }
588
+ ];
589
+ const result = isConditionsActived(conditions);
590
+ expect(result).toBe(true);
591
+ });
592
+ });
593
+ describe("activedRulesConditions", () => {
594
+ const mockConditions = [
595
+ {
596
+ id: "rule-1",
597
+ type: "current-page",
598
+ operators: "and",
599
+ actived: false,
600
+ data: {
601
+ includes: ["https://example.com"],
602
+ excludes: []
603
+ }
604
+ },
605
+ {
606
+ id: "rule-2",
607
+ type: "time",
608
+ operators: "and",
609
+ actived: false,
610
+ data: {
611
+ startDate: "2024-01-01",
612
+ startDateHour: "00",
613
+ startDateMinute: "00"
614
+ }
615
+ },
616
+ {
617
+ id: "rule-3",
618
+ type: "user-attr",
619
+ operators: "and",
620
+ actived: false,
621
+ data: {
622
+ attrId: "email",
623
+ logic: "is",
624
+ value: "test@example.com"
625
+ }
626
+ },
627
+ {
628
+ id: "rule-4",
629
+ type: "element",
630
+ operators: "and",
631
+ actived: true,
632
+ data: {}
633
+ }
634
+ ];
635
+ const mockOptions = {
636
+ clientContext: {
637
+ page_url: "https://example.com",
638
+ viewport_width: 1920,
639
+ viewport_height: 1080
640
+ },
641
+ attributes: [
642
+ {
643
+ id: "email",
644
+ codeName: "email",
645
+ dataType: 2
646
+ // String
647
+ }
648
+ ],
649
+ userAttributes: {
650
+ email: "test@example.com"
651
+ }
652
+ };
653
+ test("should return conditions with default actived state when no options provided", () => {
654
+ const conditions = [
655
+ {
656
+ id: "rule-1",
657
+ type: "element",
658
+ operators: "and",
659
+ actived: true,
660
+ data: {}
661
+ },
662
+ {
663
+ id: "rule-2",
664
+ type: "element",
665
+ operators: "and",
666
+ actived: false,
667
+ data: {}
668
+ }
669
+ ];
670
+ const result = activedRulesConditions(conditions);
671
+ expect(result).toHaveLength(2);
672
+ expect(result[0].actived).toBe(true);
673
+ expect(result[1].actived).toBe(false);
674
+ });
675
+ test("should force activate rules by ID", () => {
676
+ const result = activedRulesConditions(mockConditions, {
677
+ ...mockOptions,
678
+ activatedIds: ["rule-1", "rule-2"]
679
+ });
680
+ expect(result[0].actived).toBe(true);
681
+ expect(result[1].actived).toBe(true);
682
+ expect(result[2].actived).toBe(true);
683
+ expect(result[3].actived).toBe(true);
684
+ });
685
+ test("should force deactivate rules by ID", () => {
686
+ const result = activedRulesConditions(mockConditions, {
687
+ ...mockOptions,
688
+ deactivatedIds: ["rule-1", "rule-4"]
689
+ });
690
+ expect(result[0].actived).toBe(false);
691
+ expect(result[1].actived).toBe(true);
692
+ expect(result[2].actived).toBe(true);
693
+ expect(result[3].actived).toBe(false);
694
+ });
695
+ test("should prioritize activatedIds over deactivatedIds", () => {
696
+ const result = activedRulesConditions(mockConditions, {
697
+ ...mockOptions,
698
+ activatedIds: ["rule-1"],
699
+ deactivatedIds: ["rule-1"]
700
+ });
701
+ expect(result[0].actived).toBe(true);
702
+ });
703
+ test("should disable evaluation for specific rule types", () => {
704
+ const result = activedRulesConditions(mockConditions, {
705
+ ...mockOptions,
706
+ typeControl: {
707
+ [import_types3.RulesType.CURRENT_PAGE]: false,
708
+ [import_types3.RulesType.TIME]: false
709
+ }
710
+ });
711
+ expect(result[0].actived).toBe(false);
712
+ expect(result[1].actived).toBe(false);
713
+ expect(result[2].actived).toBe(true);
714
+ expect(result[3].actived).toBe(true);
715
+ });
716
+ test("should evaluate URL conditions correctly", () => {
717
+ const result = activedRulesConditions(mockConditions, mockOptions);
718
+ expect(result[0].actived).toBe(false);
719
+ });
720
+ test("should evaluate time conditions correctly", () => {
721
+ const result = activedRulesConditions(mockConditions, mockOptions);
722
+ expect(typeof result[1].actived).toBe("boolean");
723
+ });
724
+ test("should evaluate attribute conditions correctly", () => {
725
+ const result = activedRulesConditions(mockConditions, mockOptions);
726
+ expect(result[2].actived).toBe(true);
727
+ });
728
+ test("should handle rules without ID", () => {
729
+ const conditions = [
730
+ {
731
+ id: "rule-without-id",
732
+ type: "element",
733
+ operators: "and",
734
+ actived: true,
735
+ data: {}
736
+ }
737
+ ];
738
+ const result = activedRulesConditions(conditions, {
739
+ activatedIds: ["non-existent"],
740
+ deactivatedIds: ["non-existent"]
741
+ });
742
+ expect(result[0].actived).toBe(true);
743
+ });
744
+ test("should handle nested group conditions", () => {
745
+ const nestedConditions = [
746
+ {
747
+ id: "group-1",
748
+ type: "group",
749
+ operators: "and",
750
+ actived: false,
751
+ data: {},
752
+ conditions: [
753
+ {
754
+ id: "rule-1",
755
+ type: "current-page",
756
+ operators: "and",
757
+ actived: false,
758
+ data: {
759
+ includes: ["https://example.com"],
760
+ excludes: []
761
+ }
762
+ },
763
+ {
764
+ id: "rule-2",
765
+ type: "element",
766
+ operators: "and",
767
+ actived: true,
768
+ data: {}
769
+ }
770
+ ]
771
+ }
772
+ ];
773
+ const result = activedRulesConditions(nestedConditions, {
774
+ ...mockOptions,
775
+ activatedIds: ["rule-1"]
776
+ });
777
+ expect(result[0].type).toBe("group");
778
+ expect(result[0].conditions).toBeDefined();
779
+ expect(result[0].conditions[0].actived).toBe(true);
780
+ expect(result[0].conditions[1].actived).toBe(true);
781
+ });
782
+ test("should handle deep nested group conditions", () => {
783
+ const deepNestedConditions = [
784
+ {
785
+ id: "group-1",
786
+ type: "group",
787
+ operators: "and",
788
+ actived: false,
789
+ data: {},
790
+ conditions: [
791
+ {
792
+ id: "group-2",
793
+ type: "group",
794
+ operators: "and",
795
+ actived: false,
796
+ data: {},
797
+ conditions: [
798
+ {
799
+ id: "rule-1",
800
+ type: "current-page",
801
+ operators: "and",
802
+ actived: false,
803
+ data: {
804
+ includes: ["https://example.com"],
805
+ excludes: []
806
+ }
807
+ }
808
+ ]
809
+ }
810
+ ]
811
+ }
812
+ ];
813
+ const result = activedRulesConditions(deepNestedConditions, {
814
+ ...mockOptions,
815
+ activatedIds: ["rule-1"]
816
+ });
817
+ expect(result[0].conditions[0].conditions[0].actived).toBe(true);
818
+ });
819
+ test("should handle empty conditions array", () => {
820
+ const result = activedRulesConditions([]);
821
+ expect(result).toEqual([]);
822
+ });
823
+ test("should handle conditions with missing data", () => {
824
+ const conditions = [
825
+ {
826
+ id: "rule-1",
827
+ type: "current-page",
828
+ operators: "and",
829
+ actived: false,
830
+ data: void 0
831
+ }
832
+ ];
833
+ const result = activedRulesConditions(conditions, mockOptions);
834
+ expect(result[0].actived).toBe(false);
835
+ });
836
+ test("should handle mixed rule types with different evaluation results", () => {
837
+ const mixedConditions = [
838
+ {
839
+ id: "url-rule",
840
+ type: "current-page",
841
+ operators: "and",
842
+ actived: false,
843
+ data: {
844
+ includes: ["https://wrong-url.com"],
845
+ excludes: []
846
+ }
847
+ },
848
+ {
849
+ id: "attr-rule",
850
+ type: "user-attr",
851
+ operators: "and",
852
+ actived: false,
853
+ data: {
854
+ attrId: "email",
855
+ logic: "is",
856
+ value: "test@example.com"
857
+ }
858
+ },
859
+ {
860
+ id: "element-rule",
861
+ type: "element",
862
+ operators: "and",
863
+ actived: false,
864
+ data: {}
865
+ }
866
+ ];
867
+ const result = activedRulesConditions(mixedConditions, mockOptions);
868
+ expect(result[0].actived).toBe(false);
869
+ expect(result[1].actived).toBe(true);
870
+ expect(result[2].actived).toBe(false);
871
+ });
872
+ test("should handle type control with partial configuration", () => {
873
+ const result = activedRulesConditions(mockConditions, {
874
+ ...mockOptions,
875
+ typeControl: {
876
+ [import_types3.RulesType.CURRENT_PAGE]: false
877
+ // Other types not specified, should evaluate normally
878
+ }
879
+ });
880
+ expect(result[0].actived).toBe(false);
881
+ expect(result[1].actived).toBe(true);
882
+ expect(result[2].actived).toBe(true);
883
+ expect(result[3].actived).toBe(true);
884
+ });
885
+ test("should handle missing clientContext gracefully", () => {
886
+ const result = activedRulesConditions(mockConditions, {
887
+ attributes: mockOptions.attributes,
888
+ userAttributes: mockOptions.userAttributes
889
+ });
890
+ expect(result).toHaveLength(4);
891
+ expect(typeof result[0].actived).toBe("boolean");
892
+ });
893
+ test("should handle missing attributes gracefully", () => {
894
+ const result = activedRulesConditions(mockConditions, {
895
+ clientContext: mockOptions.clientContext,
896
+ userAttributes: mockOptions.userAttributes
897
+ });
898
+ expect(result[2].actived).toBe(false);
899
+ });
900
+ test("should handle missing userAttributes gracefully", () => {
901
+ const result = activedRulesConditions(mockConditions, {
902
+ clientContext: mockOptions.clientContext,
903
+ attributes: mockOptions.attributes
904
+ });
905
+ expect(result[2].actived).toBe(false);
906
+ });
907
+ test("debug: should test URL evaluation directly", () => {
908
+ const urlCondition = {
909
+ id: "debug-rule",
910
+ type: "current-page",
911
+ operators: "and",
912
+ actived: false,
913
+ data: {
914
+ includes: ["https://example.com"],
915
+ excludes: []
916
+ }
917
+ };
918
+ const options = {
919
+ clientContext: {
920
+ page_url: "https://example.com",
921
+ viewport_width: 1920,
922
+ viewport_height: 1080
923
+ }
924
+ };
925
+ const result = activedRulesConditions([urlCondition], options);
926
+ expect(result[0].actived).toBe(false);
927
+ });
928
+ });