@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.
- package/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +625 -0
- package/components/WalkmeBeacon.tsx +64 -0
- package/components/WalkmeControls.tsx +111 -0
- package/components/WalkmeModal.tsx +144 -0
- package/components/WalkmeOverlay.tsx +107 -0
- package/components/WalkmeProgress.tsx +53 -0
- package/components/WalkmeProvider.tsx +674 -0
- package/components/WalkmeSpotlight.tsx +188 -0
- package/components/WalkmeTooltip.tsx +152 -0
- package/examples/basic-tour.ts +38 -0
- package/examples/conditional-tour.ts +56 -0
- package/examples/cross-window-tour.ts +54 -0
- package/hooks/useTour.ts +52 -0
- package/hooks/useTourProgress.ts +38 -0
- package/hooks/useTourState.ts +146 -0
- package/hooks/useWalkme.ts +52 -0
- package/jest.config.cjs +27 -0
- package/lib/conditions.ts +113 -0
- package/lib/core.ts +323 -0
- package/lib/plugin-env.ts +87 -0
- package/lib/positioning.ts +172 -0
- package/lib/storage.ts +203 -0
- package/lib/targeting.ts +186 -0
- package/lib/triggers.ts +127 -0
- package/lib/validation.ts +122 -0
- package/messages/en.json +21 -0
- package/messages/es.json +21 -0
- package/package.json +18 -0
- package/plugin.config.ts +26 -0
- package/providers/walkme-context.ts +17 -0
- package/tests/lib/conditions.test.ts +172 -0
- package/tests/lib/core.test.ts +514 -0
- package/tests/lib/positioning.test.ts +43 -0
- package/tests/lib/storage.test.ts +232 -0
- package/tests/lib/targeting.test.ts +191 -0
- package/tests/lib/triggers.test.ts +198 -0
- package/tests/lib/validation.test.ts +249 -0
- package/tests/setup.ts +52 -0
- package/tests/tsconfig.json +32 -0
- package/tsconfig.json +47 -0
- package/types/walkme.types.ts +316 -0
package/lib/triggers.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WalkMe Triggers Module
|
|
3
|
+
*
|
|
4
|
+
* Tour trigger evaluation system.
|
|
5
|
+
* Determines when a tour should be automatically activated.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Tour, TourTrigger } from '../types/walkme.types'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface TriggerEvaluationContext {
|
|
15
|
+
/** Current page route/pathname */
|
|
16
|
+
currentRoute: string
|
|
17
|
+
/** Total number of page visits */
|
|
18
|
+
visitCount: number
|
|
19
|
+
/** ISO date string of first visit */
|
|
20
|
+
firstVisitDate: string | null
|
|
21
|
+
/** IDs of completed tours */
|
|
22
|
+
completedTourIds: string[]
|
|
23
|
+
/** Set of custom events that have been emitted */
|
|
24
|
+
customEvents: Set<string>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Main Evaluation
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Evaluate whether a tour should be triggered based on its trigger config
|
|
33
|
+
* and the current context. Does NOT check conditions (that's a separate step).
|
|
34
|
+
*/
|
|
35
|
+
export function shouldTriggerTour(
|
|
36
|
+
tour: Tour,
|
|
37
|
+
context: TriggerEvaluationContext,
|
|
38
|
+
): boolean {
|
|
39
|
+
const { trigger } = tour
|
|
40
|
+
|
|
41
|
+
switch (trigger.type) {
|
|
42
|
+
case 'onFirstVisit':
|
|
43
|
+
return evaluateOnFirstVisit(trigger, context)
|
|
44
|
+
case 'onRouteEnter':
|
|
45
|
+
return evaluateOnRouteEnter(trigger, context)
|
|
46
|
+
case 'onEvent':
|
|
47
|
+
return evaluateOnEvent(trigger, context)
|
|
48
|
+
case 'manual':
|
|
49
|
+
return false // Manual tours are started programmatically only
|
|
50
|
+
case 'scheduled':
|
|
51
|
+
return evaluateScheduled(trigger, context)
|
|
52
|
+
default:
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Individual Trigger Evaluators
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** First visit: triggers only when visitCount === 1 */
|
|
62
|
+
export function evaluateOnFirstVisit(
|
|
63
|
+
_trigger: TourTrigger,
|
|
64
|
+
context: TriggerEvaluationContext,
|
|
65
|
+
): boolean {
|
|
66
|
+
return context.visitCount === 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Route enter: triggers when current route matches the trigger's route pattern */
|
|
70
|
+
export function evaluateOnRouteEnter(
|
|
71
|
+
trigger: TourTrigger,
|
|
72
|
+
context: TriggerEvaluationContext,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (!trigger.route) return false
|
|
75
|
+
|
|
76
|
+
const pattern = trigger.route
|
|
77
|
+
const route = context.currentRoute
|
|
78
|
+
|
|
79
|
+
// Exact match
|
|
80
|
+
if (pattern === route) return true
|
|
81
|
+
|
|
82
|
+
// Wildcard match: /dashboard/* matches /dashboard/anything
|
|
83
|
+
if (pattern.endsWith('/*')) {
|
|
84
|
+
const prefix = pattern.slice(0, -2)
|
|
85
|
+
return route.startsWith(prefix)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Glob match: /dashboard/** matches /dashboard/a/b/c
|
|
89
|
+
if (pattern.endsWith('/**')) {
|
|
90
|
+
const prefix = pattern.slice(0, -3)
|
|
91
|
+
return route.startsWith(prefix)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Event trigger: activates when a specific custom event has been emitted */
|
|
98
|
+
export function evaluateOnEvent(
|
|
99
|
+
trigger: TourTrigger,
|
|
100
|
+
context: TriggerEvaluationContext,
|
|
101
|
+
): boolean {
|
|
102
|
+
if (!trigger.event) return false
|
|
103
|
+
return context.customEvents.has(trigger.event)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Scheduled trigger: activates after N visits or N days since first visit */
|
|
107
|
+
export function evaluateScheduled(
|
|
108
|
+
trigger: TourTrigger,
|
|
109
|
+
context: TriggerEvaluationContext,
|
|
110
|
+
): boolean {
|
|
111
|
+
// Check visit-based threshold
|
|
112
|
+
if (trigger.afterVisits !== undefined) {
|
|
113
|
+
if (context.visitCount >= trigger.afterVisits) return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check day-based threshold
|
|
117
|
+
if (trigger.afterDays !== undefined && context.firstVisitDate) {
|
|
118
|
+
const firstVisit = new Date(context.firstVisitDate)
|
|
119
|
+
const now = new Date()
|
|
120
|
+
const daysSinceFirstVisit = Math.floor(
|
|
121
|
+
(now.getTime() - firstVisit.getTime()) / (1000 * 60 * 60 * 24),
|
|
122
|
+
)
|
|
123
|
+
if (daysSinceFirstVisit >= trigger.afterDays) return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WalkMe Validation Module
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for runtime validation of tour configurations.
|
|
5
|
+
* Ensures tour definitions are well-formed before they're used.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import type { Tour, TourStep as TourStepType } from '../types/walkme.types'
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Schemas
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const TourTriggerSchema = z.object({
|
|
16
|
+
type: z.enum(['onFirstVisit', 'onRouteEnter', 'onEvent', 'manual', 'scheduled']),
|
|
17
|
+
delay: z.number().min(0).optional(),
|
|
18
|
+
route: z.string().optional(),
|
|
19
|
+
event: z.string().optional(),
|
|
20
|
+
afterVisits: z.number().min(1).optional(),
|
|
21
|
+
afterDays: z.number().min(0).optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const TourConditionsSchema = z.object({
|
|
25
|
+
userRole: z.array(z.string()).optional(),
|
|
26
|
+
featureFlags: z.array(z.string()).optional(),
|
|
27
|
+
completedTours: z.array(z.string()).optional(),
|
|
28
|
+
notCompletedTours: z.array(z.string()).optional(),
|
|
29
|
+
custom: z.function().optional(),
|
|
30
|
+
}).optional()
|
|
31
|
+
|
|
32
|
+
export const TourStepSchema = z.object({
|
|
33
|
+
id: z.string().min(1),
|
|
34
|
+
type: z.enum(['tooltip', 'modal', 'spotlight', 'beacon', 'floating']),
|
|
35
|
+
title: z.string().min(1),
|
|
36
|
+
content: z.string(),
|
|
37
|
+
target: z.string().optional(),
|
|
38
|
+
route: z.string().optional(),
|
|
39
|
+
position: z.enum(['top', 'bottom', 'left', 'right', 'auto']).optional(),
|
|
40
|
+
actions: z.array(z.enum(['next', 'prev', 'skip', 'complete', 'close'])).min(1),
|
|
41
|
+
delay: z.number().min(0).optional(),
|
|
42
|
+
autoAdvance: z.number().min(0).optional(),
|
|
43
|
+
beforeShow: z.function().optional(),
|
|
44
|
+
afterShow: z.function().optional(),
|
|
45
|
+
}).refine(
|
|
46
|
+
(step) => {
|
|
47
|
+
// tooltip, spotlight, and beacon require a target
|
|
48
|
+
if (['tooltip', 'spotlight', 'beacon'].includes(step.type)) {
|
|
49
|
+
return !!step.target
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
message: 'Steps of type tooltip, spotlight, and beacon require a target selector',
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export const TourSchema = z.object({
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
name: z.string().min(1),
|
|
61
|
+
description: z.string().optional(),
|
|
62
|
+
trigger: TourTriggerSchema,
|
|
63
|
+
conditions: TourConditionsSchema,
|
|
64
|
+
steps: z.array(TourStepSchema).min(1),
|
|
65
|
+
onComplete: z.function().optional(),
|
|
66
|
+
onSkip: z.function().optional(),
|
|
67
|
+
priority: z.number().optional(),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const TourArraySchema = z.array(TourSchema)
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Validation Functions
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/** Validate a single tour configuration */
|
|
77
|
+
export function validateTour(
|
|
78
|
+
tour: unknown,
|
|
79
|
+
): { valid: boolean; errors?: z.ZodError; tour?: Tour } {
|
|
80
|
+
const result = TourSchema.safeParse(tour)
|
|
81
|
+
if (result.success) {
|
|
82
|
+
// Zod's output matches our Tour type structurally; functions (onComplete,
|
|
83
|
+
// onSkip, beforeShow, afterShow, custom) are validated as z.function() but
|
|
84
|
+
// their signatures aren't preserved in the inferred type, so we cast once here.
|
|
85
|
+
return { valid: true, tour: result.data as Tour }
|
|
86
|
+
}
|
|
87
|
+
return { valid: false, errors: result.error }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Validate an array of tours, returning only the valid ones */
|
|
91
|
+
export function validateTours(
|
|
92
|
+
tours: unknown[],
|
|
93
|
+
): { valid: boolean; errors?: z.ZodError[]; validTours: Tour[] } {
|
|
94
|
+
const validTours: Tour[] = []
|
|
95
|
+
const errors: z.ZodError[] = []
|
|
96
|
+
|
|
97
|
+
for (const tour of tours) {
|
|
98
|
+
const result = TourSchema.safeParse(tour)
|
|
99
|
+
if (result.success) {
|
|
100
|
+
validTours.push(result.data as Tour)
|
|
101
|
+
} else {
|
|
102
|
+
errors.push(result.error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
valid: errors.length === 0,
|
|
108
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
109
|
+
validTours,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Validate a single step configuration */
|
|
114
|
+
export function validateStep(
|
|
115
|
+
step: unknown,
|
|
116
|
+
): { valid: boolean; errors?: z.ZodError; step?: TourStepType } {
|
|
117
|
+
const result = TourStepSchema.safeParse(step)
|
|
118
|
+
if (result.success) {
|
|
119
|
+
return { valid: true, step: result.data as TourStepType }
|
|
120
|
+
}
|
|
121
|
+
return { valid: false, errors: result.error }
|
|
122
|
+
}
|
package/messages/en.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"walkme": {
|
|
3
|
+
"next": "Next",
|
|
4
|
+
"prev": "Previous",
|
|
5
|
+
"skip": "Skip tour",
|
|
6
|
+
"complete": "Complete",
|
|
7
|
+
"close": "Close",
|
|
8
|
+
"progress": "Step {current} of {total}",
|
|
9
|
+
"tourAvailable": "Tour available",
|
|
10
|
+
"beaconLabel": "Click to start guided tour",
|
|
11
|
+
"modalTitle": "Guided Tour",
|
|
12
|
+
"tooltipLabel": "Tour step",
|
|
13
|
+
"spotlightLabel": "Highlighted element",
|
|
14
|
+
"keyboardHint": "Press Arrow Right for next step, Escape to skip",
|
|
15
|
+
"tourCompleted": "Tour completed!",
|
|
16
|
+
"tourSkipped": "Tour skipped",
|
|
17
|
+
"errorTargetNotFound": "Element not found, skipping step",
|
|
18
|
+
"resumeTour": "Resume tour",
|
|
19
|
+
"restartTour": "Restart tour"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/messages/es.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"walkme": {
|
|
3
|
+
"next": "Siguiente",
|
|
4
|
+
"prev": "Anterior",
|
|
5
|
+
"skip": "Saltar tour",
|
|
6
|
+
"complete": "Completar",
|
|
7
|
+
"close": "Cerrar",
|
|
8
|
+
"progress": "Paso {current} de {total}",
|
|
9
|
+
"tourAvailable": "Tour disponible",
|
|
10
|
+
"beaconLabel": "Haz clic para iniciar el tour guiado",
|
|
11
|
+
"modalTitle": "Tour Guiado",
|
|
12
|
+
"tooltipLabel": "Paso del tour",
|
|
13
|
+
"spotlightLabel": "Elemento destacado",
|
|
14
|
+
"keyboardHint": "Presiona flecha derecha para siguiente paso, Escape para saltar",
|
|
15
|
+
"tourCompleted": "Tour completado!",
|
|
16
|
+
"tourSkipped": "Tour saltado",
|
|
17
|
+
"errorTargetNotFound": "Elemento no encontrado, saltando paso",
|
|
18
|
+
"resumeTour": "Reanudar tour",
|
|
19
|
+
"restartTour": "Reiniciar tour"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nextsparkjs/plugin-walkme",
|
|
3
|
+
"version": "0.1.0-beta.104",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "./plugin.config.ts",
|
|
6
|
+
"requiredPlugins": [],
|
|
7
|
+
"dependencies": {},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@phosphor-icons/react": "^2.0.0",
|
|
10
|
+
"next": "^15.0.0",
|
|
11
|
+
"react": "^19.0.0",
|
|
12
|
+
"react-dom": "^19.0.0"
|
|
13
|
+
},
|
|
14
|
+
"nextspark": {
|
|
15
|
+
"type": "plugin",
|
|
16
|
+
"name": "walkme"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/plugin.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WalkMe Plugin Configuration
|
|
3
|
+
*
|
|
4
|
+
* Guided tours and onboarding system for NextSpark applications.
|
|
5
|
+
* Provides declarative tour definitions, multi-step tooltips, modals,
|
|
6
|
+
* spotlights, beacons, cross-page navigation, and full persistence.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PluginConfig } from '@nextsparkjs/core/types/plugin'
|
|
10
|
+
|
|
11
|
+
export const walkmePluginConfig: PluginConfig = {
|
|
12
|
+
name: 'walkme',
|
|
13
|
+
displayName: 'WalkMe',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
description: 'Guided tours and onboarding system for NextSpark applications',
|
|
16
|
+
enabled: true,
|
|
17
|
+
dependencies: [],
|
|
18
|
+
api: {},
|
|
19
|
+
hooks: {
|
|
20
|
+
onLoad: async () => {
|
|
21
|
+
console.log('[WalkMe Plugin] Loaded - guided tours system ready')
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default walkmePluginConfig
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react'
|
|
4
|
+
import type { WalkmeContextValue } from '../types/walkme.types'
|
|
5
|
+
|
|
6
|
+
export const WalkmeContext = createContext<WalkmeContextValue | null>(null)
|
|
7
|
+
|
|
8
|
+
export function useWalkmeContext(): WalkmeContextValue {
|
|
9
|
+
const context = useContext(WalkmeContext)
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'useWalkmeContext must be used within a <WalkmeProvider>. ' +
|
|
13
|
+
'Wrap your app or page with <WalkmeProvider tours={[...]}> to use WalkMe hooks.',
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
return context
|
|
17
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ConditionContext, TourConditions } from '../../types/walkme.types'
|
|
2
|
+
import {
|
|
3
|
+
evaluateConditions,
|
|
4
|
+
evaluateRoleCondition,
|
|
5
|
+
evaluateFeatureFlagCondition,
|
|
6
|
+
evaluateCompletedToursCondition,
|
|
7
|
+
evaluateNotCompletedCondition,
|
|
8
|
+
} from '../../lib/conditions'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Fixtures
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function createContext(overrides: Partial<ConditionContext> = {}): ConditionContext {
|
|
15
|
+
return {
|
|
16
|
+
userRole: 'user',
|
|
17
|
+
featureFlags: [],
|
|
18
|
+
completedTourIds: [],
|
|
19
|
+
visitCount: 1,
|
|
20
|
+
...overrides,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// evaluateConditions (integration)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
describe('evaluateConditions', () => {
|
|
29
|
+
it('returns true when conditions is undefined', () => {
|
|
30
|
+
expect(evaluateConditions(undefined, createContext())).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns true when conditions is empty object', () => {
|
|
34
|
+
expect(evaluateConditions({}, createContext())).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns true when all conditions pass', () => {
|
|
38
|
+
const conditions: TourConditions = {
|
|
39
|
+
userRole: ['admin', 'superadmin'],
|
|
40
|
+
featureFlags: ['feature-a'],
|
|
41
|
+
completedTours: ['onboarding'],
|
|
42
|
+
}
|
|
43
|
+
const ctx = createContext({
|
|
44
|
+
userRole: 'admin',
|
|
45
|
+
featureFlags: ['feature-a', 'feature-b'],
|
|
46
|
+
completedTourIds: ['onboarding'],
|
|
47
|
+
})
|
|
48
|
+
expect(evaluateConditions(conditions, ctx)).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns false when role check fails (AND logic)', () => {
|
|
52
|
+
const conditions: TourConditions = {
|
|
53
|
+
userRole: ['admin'],
|
|
54
|
+
featureFlags: ['feature-a'],
|
|
55
|
+
}
|
|
56
|
+
const ctx = createContext({
|
|
57
|
+
userRole: 'user',
|
|
58
|
+
featureFlags: ['feature-a'],
|
|
59
|
+
})
|
|
60
|
+
expect(evaluateConditions(conditions, ctx)).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns false when feature flag check fails', () => {
|
|
64
|
+
const conditions: TourConditions = {
|
|
65
|
+
featureFlags: ['feature-x'],
|
|
66
|
+
}
|
|
67
|
+
const ctx = createContext({ featureFlags: ['feature-y'] })
|
|
68
|
+
expect(evaluateConditions(conditions, ctx)).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns false when completedTours check fails', () => {
|
|
72
|
+
const conditions: TourConditions = {
|
|
73
|
+
completedTours: ['required-tour'],
|
|
74
|
+
}
|
|
75
|
+
const ctx = createContext({ completedTourIds: [] })
|
|
76
|
+
expect(evaluateConditions(conditions, ctx)).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns false when notCompletedTours check fails', () => {
|
|
80
|
+
const conditions: TourConditions = {
|
|
81
|
+
notCompletedTours: ['excluded-tour'],
|
|
82
|
+
}
|
|
83
|
+
const ctx = createContext({ completedTourIds: ['excluded-tour'] })
|
|
84
|
+
expect(evaluateConditions(conditions, ctx)).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('evaluates custom condition function', () => {
|
|
88
|
+
const conditions: TourConditions = {
|
|
89
|
+
custom: (ctx) => ctx.visitCount > 3,
|
|
90
|
+
}
|
|
91
|
+
expect(evaluateConditions(conditions, createContext({ visitCount: 5 }))).toBe(true)
|
|
92
|
+
expect(evaluateConditions(conditions, createContext({ visitCount: 1 }))).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('skips empty arrays in conditions', () => {
|
|
96
|
+
const conditions: TourConditions = {
|
|
97
|
+
userRole: [],
|
|
98
|
+
featureFlags: [],
|
|
99
|
+
completedTours: [],
|
|
100
|
+
notCompletedTours: [],
|
|
101
|
+
}
|
|
102
|
+
expect(evaluateConditions(conditions, createContext())).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// evaluateRoleCondition
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('evaluateRoleCondition', () => {
|
|
111
|
+
it('returns true when user role is in allowed list', () => {
|
|
112
|
+
expect(evaluateRoleCondition(['admin', 'editor'], 'admin')).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns false when user role is not in allowed list', () => {
|
|
116
|
+
expect(evaluateRoleCondition(['admin', 'editor'], 'user')).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns false when user role is undefined', () => {
|
|
120
|
+
expect(evaluateRoleCondition(['admin'], undefined)).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// evaluateFeatureFlagCondition
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
describe('evaluateFeatureFlagCondition', () => {
|
|
129
|
+
it('returns true when all required flags are active', () => {
|
|
130
|
+
expect(evaluateFeatureFlagCondition(['a', 'b'], ['a', 'b', 'c'])).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('returns false when a required flag is missing', () => {
|
|
134
|
+
expect(evaluateFeatureFlagCondition(['a', 'b'], ['a'])).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('returns true when required flags is empty', () => {
|
|
138
|
+
expect(evaluateFeatureFlagCondition([], ['a'])).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// evaluateCompletedToursCondition
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
describe('evaluateCompletedToursCondition', () => {
|
|
147
|
+
it('returns true when all required tours are completed', () => {
|
|
148
|
+
expect(evaluateCompletedToursCondition(['t1', 't2'], ['t1', 't2', 't3'])).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('returns false when a required tour is not completed', () => {
|
|
152
|
+
expect(evaluateCompletedToursCondition(['t1', 't2'], ['t1'])).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// evaluateNotCompletedCondition
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
describe('evaluateNotCompletedCondition', () => {
|
|
161
|
+
it('returns true when none of the excluded tours are completed', () => {
|
|
162
|
+
expect(evaluateNotCompletedCondition(['t1', 't2'], ['t3'])).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('returns false when any excluded tour is completed', () => {
|
|
166
|
+
expect(evaluateNotCompletedCondition(['t1', 't2'], ['t2'])).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('returns true when excluded list is empty', () => {
|
|
170
|
+
expect(evaluateNotCompletedCondition([], ['t1'])).toBe(true)
|
|
171
|
+
})
|
|
172
|
+
})
|