@nextsparkjs/plugin-walkme 0.1.0-beta.104

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 (43) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +625 -0
  4. package/components/WalkmeBeacon.tsx +64 -0
  5. package/components/WalkmeControls.tsx +111 -0
  6. package/components/WalkmeModal.tsx +144 -0
  7. package/components/WalkmeOverlay.tsx +107 -0
  8. package/components/WalkmeProgress.tsx +53 -0
  9. package/components/WalkmeProvider.tsx +674 -0
  10. package/components/WalkmeSpotlight.tsx +188 -0
  11. package/components/WalkmeTooltip.tsx +152 -0
  12. package/examples/basic-tour.ts +38 -0
  13. package/examples/conditional-tour.ts +56 -0
  14. package/examples/cross-window-tour.ts +54 -0
  15. package/hooks/useTour.ts +52 -0
  16. package/hooks/useTourProgress.ts +38 -0
  17. package/hooks/useTourState.ts +146 -0
  18. package/hooks/useWalkme.ts +52 -0
  19. package/jest.config.cjs +27 -0
  20. package/lib/conditions.ts +113 -0
  21. package/lib/core.ts +323 -0
  22. package/lib/plugin-env.ts +87 -0
  23. package/lib/positioning.ts +172 -0
  24. package/lib/storage.ts +203 -0
  25. package/lib/targeting.ts +186 -0
  26. package/lib/triggers.ts +127 -0
  27. package/lib/validation.ts +122 -0
  28. package/messages/en.json +21 -0
  29. package/messages/es.json +21 -0
  30. package/package.json +18 -0
  31. package/plugin.config.ts +26 -0
  32. package/providers/walkme-context.ts +17 -0
  33. package/tests/lib/conditions.test.ts +172 -0
  34. package/tests/lib/core.test.ts +514 -0
  35. package/tests/lib/positioning.test.ts +43 -0
  36. package/tests/lib/storage.test.ts +232 -0
  37. package/tests/lib/targeting.test.ts +191 -0
  38. package/tests/lib/triggers.test.ts +198 -0
  39. package/tests/lib/validation.test.ts +249 -0
  40. package/tests/setup.ts +52 -0
  41. package/tests/tsconfig.json +32 -0
  42. package/tsconfig.json +47 -0
  43. package/types/walkme.types.ts +316 -0
@@ -0,0 +1,514 @@
1
+ import type { Tour, WalkmeState } from '../../types/walkme.types'
2
+ import {
3
+ createInitialState,
4
+ walkmeReducer,
5
+ canStartTour,
6
+ getActiveTour,
7
+ getActiveStep,
8
+ isFirstStep,
9
+ isLastStep,
10
+ getTourProgress,
11
+ getGlobalProgress,
12
+ } from '../../lib/core'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Test Fixtures
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const mockTour: Tour = {
19
+ id: 'test-tour',
20
+ name: 'Test Tour',
21
+ trigger: { type: 'manual' },
22
+ steps: [
23
+ { id: 'step-1', type: 'modal', title: 'Step 1', content: 'First', actions: ['next'] },
24
+ { id: 'step-2', type: 'tooltip', target: '#el', title: 'Step 2', content: 'Second', actions: ['next', 'prev'] },
25
+ { id: 'step-3', type: 'spotlight', target: '#el2', title: 'Step 3', content: 'Third', actions: ['complete'] },
26
+ ],
27
+ }
28
+
29
+ const emptyTour: Tour = {
30
+ id: 'empty-tour',
31
+ name: 'Empty Tour',
32
+ trigger: { type: 'manual' },
33
+ steps: [],
34
+ }
35
+
36
+ function initState(tours: Tour[] = [mockTour]): WalkmeState {
37
+ return walkmeReducer(createInitialState(), { type: 'INITIALIZE', tours })
38
+ }
39
+
40
+ function startedState(): WalkmeState {
41
+ const s = initState()
42
+ return walkmeReducer(s, { type: 'START_TOUR', tourId: 'test-tour' })
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // createInitialState
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe('createInitialState', () => {
50
+ it('returns a clean initial state', () => {
51
+ const state = createInitialState()
52
+ expect(state.tours).toEqual({})
53
+ expect(state.activeTour).toBeNull()
54
+ expect(state.completedTours).toEqual([])
55
+ expect(state.skippedTours).toEqual([])
56
+ expect(state.tourHistory).toEqual({})
57
+ expect(state.initialized).toBe(false)
58
+ expect(state.debug).toBe(false)
59
+ })
60
+ })
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // walkmeReducer - INITIALIZE
64
+ // ---------------------------------------------------------------------------
65
+
66
+ describe('walkmeReducer INITIALIZE', () => {
67
+ it('registers tours and sets initialized to true', () => {
68
+ const state = initState()
69
+ expect(state.initialized).toBe(true)
70
+ expect(state.tours['test-tour']).toBeDefined()
71
+ expect(state.tours['test-tour'].name).toBe('Test Tour')
72
+ })
73
+
74
+ it('handles multiple tours', () => {
75
+ const state = initState([mockTour, { ...mockTour, id: 'tour-2', name: 'Tour 2' }])
76
+ expect(Object.keys(state.tours)).toHaveLength(2)
77
+ })
78
+
79
+ it('handles empty tour array', () => {
80
+ const state = initState([])
81
+ expect(state.initialized).toBe(true)
82
+ expect(Object.keys(state.tours)).toHaveLength(0)
83
+ })
84
+ })
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // walkmeReducer - START_TOUR
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe('walkmeReducer START_TOUR', () => {
91
+ it('starts a tour and sets activeTour', () => {
92
+ const state = startedState()
93
+ expect(state.activeTour).not.toBeNull()
94
+ expect(state.activeTour!.tourId).toBe('test-tour')
95
+ expect(state.activeTour!.status).toBe('active')
96
+ expect(state.activeTour!.currentStepIndex).toBe(0)
97
+ expect(state.activeTour!.startedAt).toBeTruthy()
98
+ })
99
+
100
+ it('does nothing if a tour is already active', () => {
101
+ const state = startedState()
102
+ const state2 = walkmeReducer(state, { type: 'START_TOUR', tourId: 'test-tour' })
103
+ expect(state2).toBe(state)
104
+ })
105
+
106
+ it('does nothing for unknown tour id', () => {
107
+ const state = initState()
108
+ const state2 = walkmeReducer(state, { type: 'START_TOUR', tourId: 'nonexistent' })
109
+ expect(state2).toBe(state)
110
+ })
111
+
112
+ it('does nothing for empty tours (no steps)', () => {
113
+ const state = initState([emptyTour])
114
+ const state2 = walkmeReducer(state, { type: 'START_TOUR', tourId: 'empty-tour' })
115
+ expect(state2).toBe(state)
116
+ })
117
+
118
+ it('records tour history on start', () => {
119
+ const state = startedState()
120
+ expect(state.tourHistory['test-tour']).toBeDefined()
121
+ expect(state.tourHistory['test-tour'].status).toBe('active')
122
+ })
123
+ })
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // walkmeReducer - NEXT_STEP
127
+ // ---------------------------------------------------------------------------
128
+
129
+ describe('walkmeReducer NEXT_STEP', () => {
130
+ it('advances the step index', () => {
131
+ const state = walkmeReducer(startedState(), { type: 'NEXT_STEP' })
132
+ expect(state.activeTour!.currentStepIndex).toBe(1)
133
+ })
134
+
135
+ it('completes the tour when advancing past last step', () => {
136
+ let state = startedState()
137
+ state = walkmeReducer(state, { type: 'NEXT_STEP' }) // → step 1
138
+ state = walkmeReducer(state, { type: 'NEXT_STEP' }) // → step 2
139
+ state = walkmeReducer(state, { type: 'NEXT_STEP' }) // → completes
140
+ expect(state.activeTour).toBeNull()
141
+ expect(state.completedTours).toContain('test-tour')
142
+ })
143
+
144
+ it('does nothing when no active tour', () => {
145
+ const state = initState()
146
+ const state2 = walkmeReducer(state, { type: 'NEXT_STEP' })
147
+ expect(state2).toBe(state)
148
+ })
149
+
150
+ it('does nothing when tour is paused', () => {
151
+ let state = startedState()
152
+ state = walkmeReducer(state, { type: 'PAUSE_TOUR' })
153
+ const state2 = walkmeReducer(state, { type: 'NEXT_STEP' })
154
+ expect(state2).toBe(state)
155
+ })
156
+ })
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // walkmeReducer - PREV_STEP
160
+ // ---------------------------------------------------------------------------
161
+
162
+ describe('walkmeReducer PREV_STEP', () => {
163
+ it('decrements the step index', () => {
164
+ let state = startedState()
165
+ state = walkmeReducer(state, { type: 'NEXT_STEP' }) // → step 1
166
+ state = walkmeReducer(state, { type: 'PREV_STEP' }) // → step 0
167
+ expect(state.activeTour!.currentStepIndex).toBe(0)
168
+ })
169
+
170
+ it('does nothing at step 0', () => {
171
+ const state = startedState()
172
+ const state2 = walkmeReducer(state, { type: 'PREV_STEP' })
173
+ expect(state2).toBe(state)
174
+ })
175
+
176
+ it('does nothing when no active tour', () => {
177
+ const state = initState()
178
+ const state2 = walkmeReducer(state, { type: 'PREV_STEP' })
179
+ expect(state2).toBe(state)
180
+ })
181
+ })
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // walkmeReducer - NAVIGATE_TO_STEP
185
+ // ---------------------------------------------------------------------------
186
+
187
+ describe('walkmeReducer NAVIGATE_TO_STEP', () => {
188
+ it('jumps to a specific step', () => {
189
+ const state = walkmeReducer(startedState(), { type: 'NAVIGATE_TO_STEP', stepIndex: 2 })
190
+ expect(state.activeTour!.currentStepIndex).toBe(2)
191
+ })
192
+
193
+ it('rejects negative index', () => {
194
+ const state = startedState()
195
+ const state2 = walkmeReducer(state, { type: 'NAVIGATE_TO_STEP', stepIndex: -1 })
196
+ expect(state2).toBe(state)
197
+ })
198
+
199
+ it('rejects out of bounds index', () => {
200
+ const state = startedState()
201
+ const state2 = walkmeReducer(state, { type: 'NAVIGATE_TO_STEP', stepIndex: 99 })
202
+ expect(state2).toBe(state)
203
+ })
204
+
205
+ it('does nothing when no active tour', () => {
206
+ const state = initState()
207
+ const state2 = walkmeReducer(state, { type: 'NAVIGATE_TO_STEP', stepIndex: 0 })
208
+ expect(state2).toBe(state)
209
+ })
210
+ })
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // walkmeReducer - SKIP_TOUR
214
+ // ---------------------------------------------------------------------------
215
+
216
+ describe('walkmeReducer SKIP_TOUR', () => {
217
+ it('skips the active tour', () => {
218
+ const state = walkmeReducer(startedState(), { type: 'SKIP_TOUR' })
219
+ expect(state.activeTour).toBeNull()
220
+ expect(state.skippedTours).toContain('test-tour')
221
+ expect(state.tourHistory['test-tour'].status).toBe('skipped')
222
+ })
223
+
224
+ it('does not duplicate in skippedTours', () => {
225
+ let state = startedState()
226
+ state = walkmeReducer(state, { type: 'SKIP_TOUR' })
227
+ // Start and skip again
228
+ state = walkmeReducer(state, { type: 'START_TOUR', tourId: 'test-tour' })
229
+ state = walkmeReducer(state, { type: 'SKIP_TOUR' })
230
+ expect(state.skippedTours.filter((id) => id === 'test-tour')).toHaveLength(1)
231
+ })
232
+
233
+ it('does nothing when no active tour', () => {
234
+ const state = initState()
235
+ const state2 = walkmeReducer(state, { type: 'SKIP_TOUR' })
236
+ expect(state2).toBe(state)
237
+ })
238
+ })
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // walkmeReducer - COMPLETE_TOUR
242
+ // ---------------------------------------------------------------------------
243
+
244
+ describe('walkmeReducer COMPLETE_TOUR', () => {
245
+ it('completes the active tour', () => {
246
+ const state = walkmeReducer(startedState(), { type: 'COMPLETE_TOUR' })
247
+ expect(state.activeTour).toBeNull()
248
+ expect(state.completedTours).toContain('test-tour')
249
+ expect(state.tourHistory['test-tour'].status).toBe('completed')
250
+ })
251
+
252
+ it('does not duplicate in completedTours', () => {
253
+ let state = startedState()
254
+ state = walkmeReducer(state, { type: 'COMPLETE_TOUR' })
255
+ state = walkmeReducer(state, { type: 'START_TOUR', tourId: 'test-tour' })
256
+ state = walkmeReducer(state, { type: 'COMPLETE_TOUR' })
257
+ expect(state.completedTours.filter((id) => id === 'test-tour')).toHaveLength(1)
258
+ })
259
+
260
+ it('does nothing when no active tour', () => {
261
+ const state = initState()
262
+ const state2 = walkmeReducer(state, { type: 'COMPLETE_TOUR' })
263
+ expect(state2).toBe(state)
264
+ })
265
+ })
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // walkmeReducer - PAUSE/RESUME
269
+ // ---------------------------------------------------------------------------
270
+
271
+ describe('walkmeReducer PAUSE_TOUR / RESUME_TOUR', () => {
272
+ it('pauses an active tour', () => {
273
+ const state = walkmeReducer(startedState(), { type: 'PAUSE_TOUR' })
274
+ expect(state.activeTour!.status).toBe('paused')
275
+ })
276
+
277
+ it('resumes a paused tour', () => {
278
+ let state = walkmeReducer(startedState(), { type: 'PAUSE_TOUR' })
279
+ state = walkmeReducer(state, { type: 'RESUME_TOUR' })
280
+ expect(state.activeTour!.status).toBe('active')
281
+ })
282
+
283
+ it('pause does nothing when already paused', () => {
284
+ let state = walkmeReducer(startedState(), { type: 'PAUSE_TOUR' })
285
+ const state2 = walkmeReducer(state, { type: 'PAUSE_TOUR' })
286
+ expect(state2).toBe(state)
287
+ })
288
+
289
+ it('resume does nothing when not paused', () => {
290
+ const state = startedState()
291
+ const state2 = walkmeReducer(state, { type: 'RESUME_TOUR' })
292
+ expect(state2).toBe(state)
293
+ })
294
+
295
+ it('pause does nothing when no active tour', () => {
296
+ const state = initState()
297
+ const state2 = walkmeReducer(state, { type: 'PAUSE_TOUR' })
298
+ expect(state2).toBe(state)
299
+ })
300
+ })
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // walkmeReducer - RESET_TOUR
304
+ // ---------------------------------------------------------------------------
305
+
306
+ describe('walkmeReducer RESET_TOUR', () => {
307
+ it('resets a specific tour from completed', () => {
308
+ let state = startedState()
309
+ state = walkmeReducer(state, { type: 'COMPLETE_TOUR' })
310
+ state = walkmeReducer(state, { type: 'RESET_TOUR', tourId: 'test-tour' })
311
+ expect(state.completedTours).not.toContain('test-tour')
312
+ expect(state.tourHistory['test-tour']).toBeUndefined()
313
+ })
314
+
315
+ it('resets a specific tour from skipped', () => {
316
+ let state = startedState()
317
+ state = walkmeReducer(state, { type: 'SKIP_TOUR' })
318
+ state = walkmeReducer(state, { type: 'RESET_TOUR', tourId: 'test-tour' })
319
+ expect(state.skippedTours).not.toContain('test-tour')
320
+ })
321
+
322
+ it('clears activeTour if it matches the tour being reset', () => {
323
+ const state = walkmeReducer(startedState(), { type: 'RESET_TOUR', tourId: 'test-tour' })
324
+ expect(state.activeTour).toBeNull()
325
+ })
326
+
327
+ it('does not clear activeTour if it does not match', () => {
328
+ const state = walkmeReducer(startedState(), { type: 'RESET_TOUR', tourId: 'some-other-tour' })
329
+ expect(state.activeTour).not.toBeNull()
330
+ })
331
+ })
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // walkmeReducer - RESET_ALL
335
+ // ---------------------------------------------------------------------------
336
+
337
+ describe('walkmeReducer RESET_ALL', () => {
338
+ it('resets all state', () => {
339
+ let state = startedState()
340
+ state = walkmeReducer(state, { type: 'COMPLETE_TOUR' })
341
+ state = walkmeReducer(state, { type: 'RESET_ALL' })
342
+ expect(state.activeTour).toBeNull()
343
+ expect(state.completedTours).toEqual([])
344
+ expect(state.skippedTours).toEqual([])
345
+ expect(state.tourHistory).toEqual({})
346
+ // tours and initialized should still be set
347
+ expect(state.tours['test-tour']).toBeDefined()
348
+ expect(state.initialized).toBe(true)
349
+ })
350
+ })
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // walkmeReducer - SET_DEBUG
354
+ // ---------------------------------------------------------------------------
355
+
356
+ describe('walkmeReducer SET_DEBUG', () => {
357
+ it('enables debug mode', () => {
358
+ const state = walkmeReducer(createInitialState(), { type: 'SET_DEBUG', enabled: true })
359
+ expect(state.debug).toBe(true)
360
+ })
361
+
362
+ it('disables debug mode', () => {
363
+ let state = walkmeReducer(createInitialState(), { type: 'SET_DEBUG', enabled: true })
364
+ state = walkmeReducer(state, { type: 'SET_DEBUG', enabled: false })
365
+ expect(state.debug).toBe(false)
366
+ })
367
+ })
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // walkmeReducer - RESTORE_STATE
371
+ // ---------------------------------------------------------------------------
372
+
373
+ describe('walkmeReducer RESTORE_STATE', () => {
374
+ it('restores persisted state', () => {
375
+ const state = walkmeReducer(initState(), {
376
+ type: 'RESTORE_STATE',
377
+ completedTours: ['tour-a'],
378
+ skippedTours: ['tour-b'],
379
+ tourHistory: {},
380
+ activeTour: null,
381
+ })
382
+ expect(state.completedTours).toEqual(['tour-a'])
383
+ expect(state.skippedTours).toEqual(['tour-b'])
384
+ })
385
+ })
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // walkmeReducer - unknown action
389
+ // ---------------------------------------------------------------------------
390
+
391
+ describe('walkmeReducer unknown action', () => {
392
+ it('returns state unchanged for unknown action type', () => {
393
+ const state = initState()
394
+ const state2 = walkmeReducer(state, { type: 'UNKNOWN' } as any)
395
+ expect(state2).toBe(state)
396
+ })
397
+ })
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Helper functions
401
+ // ---------------------------------------------------------------------------
402
+
403
+ describe('canStartTour', () => {
404
+ it('returns true when tour exists and no active tour', () => {
405
+ expect(canStartTour(initState(), 'test-tour')).toBe(true)
406
+ })
407
+
408
+ it('returns false when a tour is already active', () => {
409
+ expect(canStartTour(startedState(), 'test-tour')).toBe(false)
410
+ })
411
+
412
+ it('returns false for unknown tour id', () => {
413
+ expect(canStartTour(initState(), 'nonexistent')).toBe(false)
414
+ })
415
+
416
+ it('returns false for empty tour (no steps)', () => {
417
+ expect(canStartTour(initState([emptyTour]), 'empty-tour')).toBe(false)
418
+ })
419
+ })
420
+
421
+ describe('getActiveTour', () => {
422
+ it('returns the active Tour object', () => {
423
+ const tour = getActiveTour(startedState())
424
+ expect(tour).not.toBeNull()
425
+ expect(tour!.id).toBe('test-tour')
426
+ })
427
+
428
+ it('returns null when no active tour', () => {
429
+ expect(getActiveTour(initState())).toBeNull()
430
+ })
431
+ })
432
+
433
+ describe('getActiveStep', () => {
434
+ it('returns the current step', () => {
435
+ const step = getActiveStep(startedState())
436
+ expect(step).not.toBeNull()
437
+ expect(step!.id).toBe('step-1')
438
+ })
439
+
440
+ it('returns null when no active tour', () => {
441
+ expect(getActiveStep(initState())).toBeNull()
442
+ })
443
+ })
444
+
445
+ describe('isFirstStep', () => {
446
+ it('returns true at step 0', () => {
447
+ expect(isFirstStep(startedState())).toBe(true)
448
+ })
449
+
450
+ it('returns false at step > 0', () => {
451
+ const state = walkmeReducer(startedState(), { type: 'NEXT_STEP' })
452
+ expect(isFirstStep(state)).toBe(false)
453
+ })
454
+
455
+ it('returns false when no active tour', () => {
456
+ expect(isFirstStep(initState())).toBe(false)
457
+ })
458
+ })
459
+
460
+ describe('isLastStep', () => {
461
+ it('returns false at step 0 with 3 steps', () => {
462
+ expect(isLastStep(startedState())).toBe(false)
463
+ })
464
+
465
+ it('returns true at last step', () => {
466
+ let state = startedState()
467
+ state = walkmeReducer(state, { type: 'NEXT_STEP' })
468
+ state = walkmeReducer(state, { type: 'NEXT_STEP' })
469
+ expect(isLastStep(state)).toBe(true)
470
+ })
471
+
472
+ it('returns false when no active tour', () => {
473
+ expect(isLastStep(initState())).toBe(false)
474
+ })
475
+ })
476
+
477
+ describe('getTourProgress', () => {
478
+ it('returns progress for active tour', () => {
479
+ const progress = getTourProgress(startedState(), 'test-tour')
480
+ expect(progress.current).toBe(1)
481
+ expect(progress.total).toBe(3)
482
+ expect(progress.percentage).toBe(33)
483
+ })
484
+
485
+ it('returns 0 progress for non-active tour', () => {
486
+ const progress = getTourProgress(initState(), 'test-tour')
487
+ expect(progress.current).toBe(0)
488
+ expect(progress.total).toBe(3)
489
+ expect(progress.percentage).toBe(0)
490
+ })
491
+
492
+ it('returns zero for unknown tour', () => {
493
+ const progress = getTourProgress(initState(), 'nonexistent')
494
+ expect(progress).toEqual({ current: 0, total: 0, percentage: 0 })
495
+ })
496
+ })
497
+
498
+ describe('getGlobalProgress', () => {
499
+ it('returns 0% when no tours completed', () => {
500
+ const progress = getGlobalProgress(initState())
501
+ expect(progress.completed).toBe(0)
502
+ expect(progress.total).toBe(1)
503
+ expect(progress.percentage).toBe(0)
504
+ })
505
+
506
+ it('returns 100% when all tours completed', () => {
507
+ let state = startedState()
508
+ state = walkmeReducer(state, { type: 'COMPLETE_TOUR' })
509
+ const progress = getGlobalProgress(state)
510
+ expect(progress.completed).toBe(1)
511
+ expect(progress.total).toBe(1)
512
+ expect(progress.percentage).toBe(100)
513
+ })
514
+ })
@@ -0,0 +1,43 @@
1
+ import { getPlacementFromPosition, getViewportInfo } from '../../lib/positioning'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // getPlacementFromPosition
5
+ // ---------------------------------------------------------------------------
6
+
7
+ describe('getPlacementFromPosition', () => {
8
+ it('maps "top" to "top"', () => {
9
+ expect(getPlacementFromPosition('top')).toBe('top')
10
+ })
11
+
12
+ it('maps "bottom" to "bottom"', () => {
13
+ expect(getPlacementFromPosition('bottom')).toBe('bottom')
14
+ })
15
+
16
+ it('maps "left" to "left"', () => {
17
+ expect(getPlacementFromPosition('left')).toBe('left')
18
+ })
19
+
20
+ it('maps "right" to "right"', () => {
21
+ expect(getPlacementFromPosition('right')).toBe('right')
22
+ })
23
+
24
+ it('maps "auto" to "bottom" (default)', () => {
25
+ expect(getPlacementFromPosition('auto')).toBe('bottom')
26
+ })
27
+ })
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // getViewportInfo
31
+ // ---------------------------------------------------------------------------
32
+
33
+ describe('getViewportInfo', () => {
34
+ it('returns viewport dimensions from window', () => {
35
+ const info = getViewportInfo()
36
+ expect(info).toHaveProperty('width')
37
+ expect(info).toHaveProperty('height')
38
+ expect(info).toHaveProperty('scrollX')
39
+ expect(info).toHaveProperty('scrollY')
40
+ expect(typeof info.width).toBe('number')
41
+ expect(typeof info.height).toBe('number')
42
+ })
43
+ })