@syntrologie/adapt-nav 2.5.1 → 2.6.0-canary.2

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.
Files changed (76) hide show
  1. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -1
  2. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.js +22 -153
  3. package/node_modules/@syntrologie/shared-editor-ui/package.json +2 -2
  4. package/package.json +1 -1
  5. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +0 -2
  6. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +0 -1
  7. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +0 -224
  8. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/BeforeAfterToggle.test.d.ts +0 -2
  9. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/BeforeAfterToggle.test.d.ts.map +0 -1
  10. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/BeforeAfterToggle.test.js +0 -29
  11. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts +0 -2
  12. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts.map +0 -1
  13. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +0 -260
  14. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.d.ts +0 -2
  15. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.d.ts.map +0 -1
  16. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +0 -70
  17. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.d.ts +0 -2
  18. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.d.ts.map +0 -1
  19. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +0 -46
  20. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditBackButton.test.d.ts +0 -2
  21. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditBackButton.test.d.ts.map +0 -1
  22. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditBackButton.test.js +0 -20
  23. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorBody.test.d.ts +0 -2
  24. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorBody.test.d.ts.map +0 -1
  25. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorBody.test.js +0 -12
  26. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.d.ts +0 -2
  27. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.d.ts.map +0 -1
  28. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +0 -84
  29. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorFooter.test.d.ts +0 -2
  30. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorFooter.test.d.ts.map +0 -1
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorFooter.test.js +0 -23
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorHeader.test.d.ts +0 -2
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorHeader.test.d.ts.map +0 -1
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorHeader.test.js +0 -23
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorInput.test.d.ts +0 -2
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorInput.test.d.ts.map +0 -1
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorInput.test.js +0 -26
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorLayout.test.d.ts +0 -2
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorLayout.test.d.ts.map +0 -1
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorLayout.test.js +0 -13
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts +0 -2
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts.map +0 -1
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +0 -496
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorSelect.test.d.ts +0 -2
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorSelect.test.d.ts.map +0 -1
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorSelect.test.js +0 -22
  47. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorTextarea.test.d.ts +0 -2
  48. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorTextarea.test.d.ts.map +0 -1
  49. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorTextarea.test.js +0 -20
  50. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.d.ts +0 -2
  51. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.d.ts.map +0 -1
  52. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +0 -176
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EmptyState.test.d.ts +0 -2
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EmptyState.test.d.ts.map +0 -1
  55. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EmptyState.test.js +0 -10
  56. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/GroupHeader.test.d.ts +0 -2
  57. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/GroupHeader.test.d.ts.map +0 -1
  58. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/GroupHeader.test.js +0 -14
  59. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/TriggerJourney.test.d.ts +0 -2
  60. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/TriggerJourney.test.d.ts.map +0 -1
  61. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/TriggerJourney.test.js +0 -189
  62. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/cn.test.d.ts +0 -2
  63. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/cn.test.d.ts.map +0 -1
  64. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/cn.test.js +0 -16
  65. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/formatConditionLabel.test.d.ts +0 -2
  66. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/formatConditionLabel.test.d.ts.map +0 -1
  67. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/formatConditionLabel.test.js +0 -329
  68. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +0 -2
  69. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +0 -1
  70. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +0 -257
  71. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useElementRect.test.d.ts +0 -2
  72. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useElementRect.test.d.ts.map +0 -1
  73. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useElementRect.test.js +0 -112
  74. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +0 -2
  75. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +0 -1
  76. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +0 -1015
@@ -1,1015 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { useTriggerWhenStatus } from '../hooks/useTriggerWhenStatus';
4
- function createMockRuntime(overrides = {}) {
5
- return {
6
- context: {
7
- get: () => ({
8
- page: { url: '/products', routeId: 'products' },
9
- viewport: { width: 1024, height: 768 },
10
- anchors: [],
11
- }),
12
- },
13
- accumulator: {
14
- getCount: () => 0,
15
- subscribe: () => () => { },
16
- },
17
- events: {
18
- hasRecentEvent: () => false,
19
- },
20
- state: {
21
- isDismissed: () => false,
22
- isCooldownActive: () => false,
23
- getFrequencyCount: () => 0,
24
- getSessionMetric: () => 0,
25
- },
26
- ...overrides,
27
- };
28
- }
29
- function setRuntime(runtime) {
30
- if (runtime) {
31
- window.SynOS = { handle: { runtime } };
32
- }
33
- else {
34
- delete window.SynOS;
35
- }
36
- }
37
- // =============================================================================
38
- // TESTS
39
- // =============================================================================
40
- describe('useTriggerWhenStatus', () => {
41
- beforeEach(() => {
42
- vi.useFakeTimers();
43
- setRuntime(null);
44
- });
45
- afterEach(() => {
46
- vi.useRealTimers();
47
- setRuntime(null);
48
- });
49
- // ===========================================================================
50
- // Basic behavior
51
- // ===========================================================================
52
- it('returns empty map when no runtime is available', () => {
53
- setRuntime(null);
54
- const { result } = renderHook(() => useTriggerWhenStatus([]));
55
- expect(result.current.size).toBe(0);
56
- });
57
- it('returns null status for items without triggerWhen', () => {
58
- setRuntime(createMockRuntime());
59
- const items = [{ id: 'item-1' }];
60
- const { result } = renderHook(() => useTriggerWhenStatus(items));
61
- expect(result.current.get('item-1')).toBeNull();
62
- });
63
- it('returns fallback status for non-rules type triggerWhen', () => {
64
- setRuntime(createMockRuntime());
65
- const items = [
66
- {
67
- id: 'item-1',
68
- triggerWhen: { type: 'always', default: true },
69
- },
70
- ];
71
- const { result } = renderHook(() => useTriggerWhenStatus(items));
72
- const status = result.current.get('item-1');
73
- expect(status?.visible).toBe(true);
74
- expect(status?.isFallback).toBe(true);
75
- expect(status?.conditions).toEqual([]);
76
- });
77
- it('returns fallback status for rules type with no rules', () => {
78
- setRuntime(createMockRuntime());
79
- const items = [
80
- {
81
- id: 'item-1',
82
- triggerWhen: { type: 'rules', rules: [], default: false },
83
- },
84
- ];
85
- const { result } = renderHook(() => useTriggerWhenStatus(items));
86
- const status = result.current.get('item-1');
87
- expect(status?.visible).toBe(false);
88
- expect(status?.isFallback).toBe(true);
89
- });
90
- // ===========================================================================
91
- // event_count condition
92
- // ===========================================================================
93
- describe('event_count condition', () => {
94
- it('passes when count >= target (gte)', () => {
95
- setRuntime(createMockRuntime({
96
- accumulator: {
97
- getCount: (key) => (key === 'page-views' ? 5 : 0),
98
- subscribe: () => () => { },
99
- },
100
- }));
101
- const items = [
102
- {
103
- id: 'item-1',
104
- triggerWhen: {
105
- type: 'rules',
106
- rules: [
107
- {
108
- conditions: [{ type: 'event_count', key: 'page-views', operator: 'gte', count: 3 }],
109
- value: true,
110
- },
111
- ],
112
- default: false,
113
- },
114
- },
115
- ];
116
- const { result } = renderHook(() => useTriggerWhenStatus(items));
117
- const status = result.current.get('item-1');
118
- expect(status?.visible).toBe(true);
119
- expect(status?.isFallback).toBe(false);
120
- expect(status?.conditions[0].passed).toBe(true);
121
- });
122
- it('fails when count < target (gte)', () => {
123
- setRuntime(createMockRuntime({
124
- accumulator: {
125
- getCount: () => 2,
126
- subscribe: () => () => { },
127
- },
128
- }));
129
- const items = [
130
- {
131
- id: 'item-1',
132
- triggerWhen: {
133
- type: 'rules',
134
- rules: [
135
- {
136
- conditions: [{ type: 'event_count', key: 'clicks', operator: 'gte', count: 5 }],
137
- value: true,
138
- },
139
- ],
140
- default: false,
141
- },
142
- },
143
- ];
144
- const { result } = renderHook(() => useTriggerWhenStatus(items));
145
- const status = result.current.get('item-1');
146
- expect(status?.visible).toBe(false);
147
- expect(status?.isFallback).toBe(true);
148
- });
149
- it.each([
150
- { operator: 'lte', count: 5, actual: 3, expected: true },
151
- { operator: 'lte', count: 3, actual: 5, expected: false },
152
- { operator: 'eq', count: 3, actual: 3, expected: true },
153
- { operator: 'eq', count: 3, actual: 4, expected: false },
154
- { operator: 'gt', count: 3, actual: 4, expected: true },
155
- { operator: 'gt', count: 3, actual: 3, expected: false },
156
- { operator: 'lt', count: 3, actual: 2, expected: true },
157
- { operator: 'lt', count: 3, actual: 3, expected: false },
158
- ])('event_count $operator: actual=$actual vs target=$count => $expected', ({ operator, count, actual, expected, }) => {
159
- setRuntime(createMockRuntime({
160
- accumulator: {
161
- getCount: () => actual,
162
- subscribe: () => () => { },
163
- },
164
- }));
165
- const items = [
166
- {
167
- id: 'item-1',
168
- triggerWhen: {
169
- type: 'rules',
170
- rules: [
171
- {
172
- conditions: [{ type: 'event_count', key: 'events', operator, count }],
173
- value: true,
174
- },
175
- ],
176
- default: false,
177
- },
178
- },
179
- ];
180
- const { result } = renderHook(() => useTriggerWhenStatus(items));
181
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(expected);
182
- });
183
- it('skips event_count when accumulator is missing', () => {
184
- setRuntime(createMockRuntime({
185
- accumulator: undefined,
186
- }));
187
- const items = [
188
- {
189
- id: 'item-1',
190
- triggerWhen: {
191
- type: 'rules',
192
- rules: [
193
- {
194
- conditions: [{ type: 'event_count', key: 'clicks', operator: 'gte', count: 1 }],
195
- value: true,
196
- },
197
- ],
198
- default: false,
199
- },
200
- },
201
- ];
202
- const { result } = renderHook(() => useTriggerWhenStatus(items));
203
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
204
- });
205
- });
206
- // ===========================================================================
207
- // page_url condition
208
- // ===========================================================================
209
- describe('page_url condition', () => {
210
- it('passes for exact URL match', () => {
211
- setRuntime(createMockRuntime({
212
- context: {
213
- get: () => ({
214
- page: { url: '/products' },
215
- viewport: { width: 1024, height: 768 },
216
- }),
217
- },
218
- }));
219
- const items = [
220
- {
221
- id: 'item-1',
222
- triggerWhen: {
223
- type: 'rules',
224
- rules: [{ conditions: [{ type: 'page_url', url: '/products' }], value: true }],
225
- default: false,
226
- },
227
- },
228
- ];
229
- const { result } = renderHook(() => useTriggerWhenStatus(items));
230
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
231
- });
232
- it('passes for single-star wildcard URL match (single segment)', () => {
233
- setRuntime(createMockRuntime({
234
- context: {
235
- get: () => ({
236
- page: { url: '/products/shoes' },
237
- viewport: { width: 1024, height: 768 },
238
- }),
239
- },
240
- }));
241
- const items = [
242
- {
243
- id: 'item-1',
244
- triggerWhen: {
245
- type: 'rules',
246
- rules: [{ conditions: [{ type: 'page_url', url: '/products/*' }], value: true }],
247
- default: false,
248
- },
249
- },
250
- ];
251
- const { result } = renderHook(() => useTriggerWhenStatus(items));
252
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
253
- });
254
- it('single-star wildcard does not match multi-segment paths', () => {
255
- setRuntime(createMockRuntime({
256
- context: {
257
- get: () => ({
258
- page: { url: '/products/shoes/running' },
259
- viewport: { width: 1024, height: 768 },
260
- }),
261
- },
262
- }));
263
- const items = [
264
- {
265
- id: 'item-1',
266
- triggerWhen: {
267
- type: 'rules',
268
- rules: [{ conditions: [{ type: 'page_url', url: '/products/*' }], value: true }],
269
- default: false,
270
- },
271
- },
272
- ];
273
- const { result } = renderHook(() => useTriggerWhenStatus(items));
274
- // Single * only matches one path segment
275
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
276
- });
277
- it('fails for non-matching URL', () => {
278
- setRuntime(createMockRuntime({
279
- context: {
280
- get: () => ({
281
- page: { url: '/about' },
282
- viewport: { width: 1024, height: 768 },
283
- }),
284
- },
285
- }));
286
- const items = [
287
- {
288
- id: 'item-1',
289
- triggerWhen: {
290
- type: 'rules',
291
- rules: [{ conditions: [{ type: 'page_url', url: '/products' }], value: true }],
292
- default: false,
293
- },
294
- },
295
- ];
296
- const { result } = renderHook(() => useTriggerWhenStatus(items));
297
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
298
- });
299
- });
300
- // ===========================================================================
301
- // route condition
302
- // ===========================================================================
303
- describe('route condition', () => {
304
- it('passes when routeId matches', () => {
305
- setRuntime(createMockRuntime({
306
- context: {
307
- get: () => ({
308
- page: { url: '/products', routeId: 'products' },
309
- viewport: { width: 1024, height: 768 },
310
- }),
311
- },
312
- }));
313
- const items = [
314
- {
315
- id: 'item-1',
316
- triggerWhen: {
317
- type: 'rules',
318
- rules: [{ conditions: [{ type: 'route', routeId: 'products' }], value: true }],
319
- default: false,
320
- },
321
- },
322
- ];
323
- const { result } = renderHook(() => useTriggerWhenStatus(items));
324
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
325
- });
326
- it('fails when routeId does not match', () => {
327
- setRuntime(createMockRuntime({
328
- context: {
329
- get: () => ({
330
- page: { url: '/about', routeId: 'about' },
331
- viewport: { width: 1024, height: 768 },
332
- }),
333
- },
334
- }));
335
- const items = [
336
- {
337
- id: 'item-1',
338
- triggerWhen: {
339
- type: 'rules',
340
- rules: [{ conditions: [{ type: 'route', routeId: 'home' }], value: true }],
341
- default: false,
342
- },
343
- },
344
- ];
345
- const { result } = renderHook(() => useTriggerWhenStatus(items));
346
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
347
- });
348
- });
349
- // ===========================================================================
350
- // anchor_visible condition
351
- // ===========================================================================
352
- describe('anchor_visible condition', () => {
353
- it('passes for visible anchor', () => {
354
- setRuntime(createMockRuntime({
355
- context: {
356
- get: () => ({
357
- page: { url: '/' },
358
- viewport: { width: 1024, height: 768 },
359
- anchors: [{ anchorId: 'hero', visible: true, present: true }],
360
- }),
361
- },
362
- }));
363
- const items = [
364
- {
365
- id: 'item-1',
366
- triggerWhen: {
367
- type: 'rules',
368
- rules: [
369
- {
370
- conditions: [{ type: 'anchor_visible', anchorId: 'hero', state: 'visible' }],
371
- value: true,
372
- },
373
- ],
374
- default: false,
375
- },
376
- },
377
- ];
378
- const { result } = renderHook(() => useTriggerWhenStatus(items));
379
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
380
- });
381
- it('passes for present anchor', () => {
382
- setRuntime(createMockRuntime({
383
- context: {
384
- get: () => ({
385
- page: { url: '/' },
386
- viewport: { width: 1024, height: 768 },
387
- anchors: [{ anchorId: 'sidebar', visible: false, present: true }],
388
- }),
389
- },
390
- }));
391
- const items = [
392
- {
393
- id: 'item-1',
394
- triggerWhen: {
395
- type: 'rules',
396
- rules: [
397
- {
398
- conditions: [{ type: 'anchor_visible', anchorId: 'sidebar', state: 'present' }],
399
- value: true,
400
- },
401
- ],
402
- default: false,
403
- },
404
- },
405
- ];
406
- const { result } = renderHook(() => useTriggerWhenStatus(items));
407
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
408
- });
409
- it('passes for absent anchor', () => {
410
- setRuntime(createMockRuntime({
411
- context: {
412
- get: () => ({
413
- page: { url: '/' },
414
- viewport: { width: 1024, height: 768 },
415
- anchors: [],
416
- }),
417
- },
418
- }));
419
- const items = [
420
- {
421
- id: 'item-1',
422
- triggerWhen: {
423
- type: 'rules',
424
- rules: [
425
- {
426
- conditions: [
427
- { type: 'anchor_visible', anchorId: 'deleted-section', state: 'absent' },
428
- ],
429
- value: true,
430
- },
431
- ],
432
- default: false,
433
- },
434
- },
435
- ];
436
- const { result } = renderHook(() => useTriggerWhenStatus(items));
437
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
438
- });
439
- });
440
- // ===========================================================================
441
- // event_occurred condition
442
- // ===========================================================================
443
- describe('event_occurred condition', () => {
444
- it('passes when recent event exists', () => {
445
- setRuntime(createMockRuntime({
446
- events: { hasRecentEvent: (name) => name === 'click_cta' },
447
- }));
448
- const items = [
449
- {
450
- id: 'item-1',
451
- triggerWhen: {
452
- type: 'rules',
453
- rules: [
454
- {
455
- conditions: [{ type: 'event_occurred', eventName: 'click_cta', withinMs: 5000 }],
456
- value: true,
457
- },
458
- ],
459
- default: false,
460
- },
461
- },
462
- ];
463
- const { result } = renderHook(() => useTriggerWhenStatus(items));
464
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
465
- });
466
- it('falls back to false when events service is missing', () => {
467
- setRuntime(createMockRuntime({
468
- events: undefined,
469
- }));
470
- const items = [
471
- {
472
- id: 'item-1',
473
- triggerWhen: {
474
- type: 'rules',
475
- rules: [
476
- {
477
- conditions: [{ type: 'event_occurred', eventName: 'click_cta' }],
478
- value: true,
479
- },
480
- ],
481
- default: false,
482
- },
483
- },
484
- ];
485
- const { result } = renderHook(() => useTriggerWhenStatus(items));
486
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
487
- });
488
- });
489
- // ===========================================================================
490
- // viewport condition
491
- // ===========================================================================
492
- describe('viewport condition', () => {
493
- it('passes when viewport matches all constraints', () => {
494
- setRuntime(createMockRuntime({
495
- context: {
496
- get: () => ({
497
- page: { url: '/' },
498
- viewport: { width: 1024, height: 768 },
499
- }),
500
- },
501
- }));
502
- const items = [
503
- {
504
- id: 'item-1',
505
- triggerWhen: {
506
- type: 'rules',
507
- rules: [
508
- {
509
- conditions: [
510
- {
511
- type: 'viewport',
512
- minWidth: 800,
513
- maxWidth: 1200,
514
- minHeight: 600,
515
- maxHeight: 900,
516
- },
517
- ],
518
- value: true,
519
- },
520
- ],
521
- default: false,
522
- },
523
- },
524
- ];
525
- const { result } = renderHook(() => useTriggerWhenStatus(items));
526
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
527
- });
528
- it('fails when width is below minWidth', () => {
529
- setRuntime(createMockRuntime({
530
- context: {
531
- get: () => ({
532
- page: { url: '/' },
533
- viewport: { width: 500, height: 768 },
534
- }),
535
- },
536
- }));
537
- const items = [
538
- {
539
- id: 'item-1',
540
- triggerWhen: {
541
- type: 'rules',
542
- rules: [{ conditions: [{ type: 'viewport', minWidth: 800 }], value: true }],
543
- default: false,
544
- },
545
- },
546
- ];
547
- const { result } = renderHook(() => useTriggerWhenStatus(items));
548
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
549
- });
550
- it('fails when width exceeds maxWidth', () => {
551
- setRuntime(createMockRuntime({
552
- context: {
553
- get: () => ({
554
- page: { url: '/' },
555
- viewport: { width: 2000, height: 768 },
556
- }),
557
- },
558
- }));
559
- const items = [
560
- {
561
- id: 'item-1',
562
- triggerWhen: {
563
- type: 'rules',
564
- rules: [{ conditions: [{ type: 'viewport', maxWidth: 1200 }], value: true }],
565
- default: false,
566
- },
567
- },
568
- ];
569
- const { result } = renderHook(() => useTriggerWhenStatus(items));
570
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
571
- });
572
- it('fails when height is below minHeight', () => {
573
- setRuntime(createMockRuntime({
574
- context: {
575
- get: () => ({
576
- page: { url: '/' },
577
- viewport: { width: 1024, height: 300 },
578
- }),
579
- },
580
- }));
581
- const items = [
582
- {
583
- id: 'item-1',
584
- triggerWhen: {
585
- type: 'rules',
586
- rules: [{ conditions: [{ type: 'viewport', minHeight: 600 }], value: true }],
587
- default: false,
588
- },
589
- },
590
- ];
591
- const { result } = renderHook(() => useTriggerWhenStatus(items));
592
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
593
- });
594
- it('fails when height exceeds maxHeight', () => {
595
- setRuntime(createMockRuntime({
596
- context: {
597
- get: () => ({
598
- page: { url: '/' },
599
- viewport: { width: 1024, height: 1500 },
600
- }),
601
- },
602
- }));
603
- const items = [
604
- {
605
- id: 'item-1',
606
- triggerWhen: {
607
- type: 'rules',
608
- rules: [{ conditions: [{ type: 'viewport', maxHeight: 900 }], value: true }],
609
- default: false,
610
- },
611
- },
612
- ];
613
- const { result } = renderHook(() => useTriggerWhenStatus(items));
614
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
615
- });
616
- });
617
- // ===========================================================================
618
- // session_metric condition
619
- // ===========================================================================
620
- describe('session_metric condition', () => {
621
- it.each([
622
- { operator: 'gte', threshold: 3, actual: 5, expected: true },
623
- { operator: 'gte', threshold: 5, actual: 3, expected: false },
624
- { operator: 'lte', threshold: 5, actual: 3, expected: true },
625
- { operator: 'eq', threshold: 3, actual: 3, expected: true },
626
- { operator: 'gt', threshold: 3, actual: 4, expected: true },
627
- { operator: 'lt', threshold: 3, actual: 2, expected: true },
628
- ])('session_metric $operator: $actual vs $threshold => $expected', ({ operator, threshold, actual, expected, }) => {
629
- setRuntime(createMockRuntime({
630
- state: {
631
- isDismissed: () => false,
632
- isCooldownActive: () => false,
633
- getFrequencyCount: () => 0,
634
- getSessionMetric: (key) => (key === 'engagement' ? actual : 0),
635
- },
636
- }));
637
- const items = [
638
- {
639
- id: 'item-1',
640
- triggerWhen: {
641
- type: 'rules',
642
- rules: [
643
- {
644
- conditions: [{ type: 'session_metric', key: 'engagement', operator, threshold }],
645
- value: true,
646
- },
647
- ],
648
- default: false,
649
- },
650
- },
651
- ];
652
- const { result } = renderHook(() => useTriggerWhenStatus(items));
653
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(expected);
654
- });
655
- });
656
- // ===========================================================================
657
- // dismissed condition
658
- // ===========================================================================
659
- describe('dismissed condition', () => {
660
- it('passes when item is dismissed', () => {
661
- setRuntime(createMockRuntime({
662
- state: {
663
- isDismissed: (key) => key === 'tooltip-1',
664
- isCooldownActive: () => false,
665
- getFrequencyCount: () => 0,
666
- getSessionMetric: () => 0,
667
- },
668
- }));
669
- const items = [
670
- {
671
- id: 'item-1',
672
- triggerWhen: {
673
- type: 'rules',
674
- rules: [
675
- {
676
- conditions: [{ type: 'dismissed', key: 'tooltip-1' }],
677
- value: true,
678
- },
679
- ],
680
- default: false,
681
- },
682
- },
683
- ];
684
- const { result } = renderHook(() => useTriggerWhenStatus(items));
685
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
686
- });
687
- it('passes inverted dismissed (not dismissed)', () => {
688
- setRuntime(createMockRuntime({
689
- state: {
690
- isDismissed: () => false,
691
- isCooldownActive: () => false,
692
- getFrequencyCount: () => 0,
693
- getSessionMetric: () => 0,
694
- },
695
- }));
696
- const items = [
697
- {
698
- id: 'item-1',
699
- triggerWhen: {
700
- type: 'rules',
701
- rules: [
702
- {
703
- conditions: [{ type: 'dismissed', key: 'tooltip-1', inverted: true }],
704
- value: true,
705
- },
706
- ],
707
- default: false,
708
- },
709
- },
710
- ];
711
- const { result } = renderHook(() => useTriggerWhenStatus(items));
712
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
713
- });
714
- });
715
- // ===========================================================================
716
- // cooldown_active condition
717
- // ===========================================================================
718
- describe('cooldown_active condition', () => {
719
- it('passes when cooldown is active', () => {
720
- setRuntime(createMockRuntime({
721
- state: {
722
- isDismissed: () => false,
723
- isCooldownActive: () => true,
724
- getFrequencyCount: () => 0,
725
- getSessionMetric: () => 0,
726
- },
727
- }));
728
- const items = [
729
- {
730
- id: 'item-1',
731
- triggerWhen: {
732
- type: 'rules',
733
- rules: [
734
- {
735
- conditions: [{ type: 'cooldown_active', key: 'tip-1' }],
736
- value: true,
737
- },
738
- ],
739
- default: false,
740
- },
741
- },
742
- ];
743
- const { result } = renderHook(() => useTriggerWhenStatus(items));
744
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
745
- });
746
- it('passes inverted cooldown (cooldown not active)', () => {
747
- setRuntime(createMockRuntime({
748
- state: {
749
- isDismissed: () => false,
750
- isCooldownActive: () => false,
751
- getFrequencyCount: () => 0,
752
- getSessionMetric: () => 0,
753
- },
754
- }));
755
- const items = [
756
- {
757
- id: 'item-1',
758
- triggerWhen: {
759
- type: 'rules',
760
- rules: [
761
- {
762
- conditions: [{ type: 'cooldown_active', key: 'tip-1', inverted: true }],
763
- value: true,
764
- },
765
- ],
766
- default: false,
767
- },
768
- },
769
- ];
770
- const { result } = renderHook(() => useTriggerWhenStatus(items));
771
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
772
- });
773
- });
774
- // ===========================================================================
775
- // frequency_limit condition
776
- // ===========================================================================
777
- describe('frequency_limit condition', () => {
778
- it('passes when frequency count >= limit', () => {
779
- setRuntime(createMockRuntime({
780
- state: {
781
- isDismissed: () => false,
782
- isCooldownActive: () => false,
783
- getFrequencyCount: () => 5,
784
- getSessionMetric: () => 0,
785
- },
786
- }));
787
- const items = [
788
- {
789
- id: 'item-1',
790
- triggerWhen: {
791
- type: 'rules',
792
- rules: [
793
- {
794
- conditions: [{ type: 'frequency_limit', key: 'tip-1', limit: 3 }],
795
- value: true,
796
- },
797
- ],
798
- default: false,
799
- },
800
- },
801
- ];
802
- const { result } = renderHook(() => useTriggerWhenStatus(items));
803
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
804
- });
805
- it('passes inverted frequency limit (below limit)', () => {
806
- setRuntime(createMockRuntime({
807
- state: {
808
- isDismissed: () => false,
809
- isCooldownActive: () => false,
810
- getFrequencyCount: () => 1,
811
- getSessionMetric: () => 0,
812
- },
813
- }));
814
- const items = [
815
- {
816
- id: 'item-1',
817
- triggerWhen: {
818
- type: 'rules',
819
- rules: [
820
- {
821
- conditions: [{ type: 'frequency_limit', key: 'tip-1', limit: 3, inverted: true }],
822
- value: true,
823
- },
824
- ],
825
- default: false,
826
- },
827
- },
828
- ];
829
- const { result } = renderHook(() => useTriggerWhenStatus(items));
830
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
831
- });
832
- });
833
- // ===========================================================================
834
- // Multi-condition rules
835
- // ===========================================================================
836
- describe('multi-condition rules', () => {
837
- it('requires ALL conditions to pass for a rule to match', () => {
838
- setRuntime(createMockRuntime({
839
- context: {
840
- get: () => ({
841
- page: { url: '/products', routeId: 'products' },
842
- viewport: { width: 1024, height: 768 },
843
- }),
844
- },
845
- accumulator: {
846
- getCount: () => 5,
847
- subscribe: () => () => { },
848
- },
849
- }));
850
- const items = [
851
- {
852
- id: 'item-1',
853
- triggerWhen: {
854
- type: 'rules',
855
- rules: [
856
- {
857
- conditions: [
858
- { type: 'page_url', url: '/products' },
859
- { type: 'event_count', key: 'views', operator: 'gte', count: 3 },
860
- ],
861
- value: true,
862
- },
863
- ],
864
- default: false,
865
- },
866
- },
867
- ];
868
- const { result } = renderHook(() => useTriggerWhenStatus(items));
869
- expect(result.current.get('item-1')?.visible).toBe(true);
870
- });
871
- it('falls through to next rule when first rule fails', () => {
872
- setRuntime(createMockRuntime({
873
- context: {
874
- get: () => ({
875
- page: { url: '/about', routeId: 'about' },
876
- viewport: { width: 1024, height: 768 },
877
- }),
878
- },
879
- }));
880
- const items = [
881
- {
882
- id: 'item-1',
883
- triggerWhen: {
884
- type: 'rules',
885
- rules: [
886
- {
887
- conditions: [{ type: 'page_url', url: '/products' }],
888
- value: true,
889
- },
890
- {
891
- conditions: [{ type: 'page_url', url: '/about' }],
892
- value: true,
893
- },
894
- ],
895
- default: false,
896
- },
897
- },
898
- ];
899
- const { result } = renderHook(() => useTriggerWhenStatus(items));
900
- expect(result.current.get('item-1')?.visible).toBe(true);
901
- });
902
- it('falls to default when no rules match', () => {
903
- setRuntime(createMockRuntime({
904
- context: {
905
- get: () => ({
906
- page: { url: '/contact' },
907
- viewport: { width: 1024, height: 768 },
908
- }),
909
- },
910
- }));
911
- const items = [
912
- {
913
- id: 'item-1',
914
- triggerWhen: {
915
- type: 'rules',
916
- rules: [{ conditions: [{ type: 'page_url', url: '/products' }], value: true }],
917
- default: true,
918
- },
919
- },
920
- ];
921
- const { result } = renderHook(() => useTriggerWhenStatus(items));
922
- const status = result.current.get('item-1');
923
- expect(status?.visible).toBe(true);
924
- expect(status?.isFallback).toBe(true);
925
- });
926
- });
927
- // ===========================================================================
928
- // Reactive updates
929
- // ===========================================================================
930
- describe('reactive updates', () => {
931
- it('subscribes to accumulator and re-evaluates on changes', () => {
932
- let subscribeCb = null;
933
- let currentCount = 0;
934
- setRuntime(createMockRuntime({
935
- accumulator: {
936
- getCount: () => currentCount,
937
- subscribe: (cb) => {
938
- subscribeCb = cb;
939
- return () => {
940
- subscribeCb = null;
941
- };
942
- },
943
- },
944
- }));
945
- const items = [
946
- {
947
- id: 'item-1',
948
- triggerWhen: {
949
- type: 'rules',
950
- rules: [
951
- {
952
- conditions: [{ type: 'event_count', key: 'clicks', operator: 'gte', count: 3 }],
953
- value: true,
954
- },
955
- ],
956
- default: false,
957
- },
958
- },
959
- ];
960
- const { result } = renderHook(() => useTriggerWhenStatus(items));
961
- // Initially: count=0, should fail
962
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(false);
963
- // Update count and trigger subscription
964
- currentCount = 5;
965
- act(() => {
966
- subscribeCb?.();
967
- });
968
- expect(result.current.get('item-1')?.conditions[0].passed).toBe(true);
969
- });
970
- it('re-evaluates on polling interval', () => {
971
- let currentUrl = '/home';
972
- setRuntime(createMockRuntime({
973
- context: {
974
- get: () => ({
975
- page: { url: currentUrl },
976
- viewport: { width: 1024, height: 768 },
977
- }),
978
- },
979
- }));
980
- const items = [
981
- {
982
- id: 'item-1',
983
- triggerWhen: {
984
- type: 'rules',
985
- rules: [{ conditions: [{ type: 'page_url', url: '/products' }], value: true }],
986
- default: false,
987
- },
988
- },
989
- ];
990
- const { result } = renderHook(() => useTriggerWhenStatus(items));
991
- // Initially: URL is /home, should fail
992
- expect(result.current.get('item-1')?.visible).toBe(false);
993
- // Change URL and advance timer
994
- currentUrl = '/products';
995
- act(() => {
996
- vi.advanceTimersByTime(2000);
997
- });
998
- expect(result.current.get('item-1')?.visible).toBe(true);
999
- });
1000
- it('cleans up subscription and interval on unmount', () => {
1001
- let unsubCalled = false;
1002
- setRuntime(createMockRuntime({
1003
- accumulator: {
1004
- getCount: () => 0,
1005
- subscribe: () => () => {
1006
- unsubCalled = true;
1007
- },
1008
- },
1009
- }));
1010
- const { unmount } = renderHook(() => useTriggerWhenStatus([]));
1011
- unmount();
1012
- expect(unsubCalled).toBe(true);
1013
- });
1014
- });
1015
- });