@cognior/iap-sdk 0.1.0
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.
Potentially problematic release.
This version of @cognior/iap-sdk might be problematic. Click here for more details.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
// src/core/triggerManager.ts
|
|
2
|
+
// Enhanced trigger system for step-level triggers with fallback support
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
TriggerDefinition,
|
|
6
|
+
TriggerCondition,
|
|
7
|
+
TriggerEvaluationContext,
|
|
8
|
+
TriggerEvaluationResult,
|
|
9
|
+
TriggerConditionKind,
|
|
10
|
+
TriggerType,
|
|
11
|
+
LogicalOperator,
|
|
12
|
+
RuleOperator
|
|
13
|
+
} from '../experiences/types';
|
|
14
|
+
import { resolveSelector, waitForElement } from '../utils/selectors';
|
|
15
|
+
import { pageContextService, type PageChangeEvent } from '../services/pageContextService';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve selector to multiple elements (handles both CSS and XPath)
|
|
19
|
+
*/
|
|
20
|
+
function resolveSelectorAll(selector: string, root: Document | Element = document): Element[] {
|
|
21
|
+
if (!selector || typeof selector !== 'string') return [];
|
|
22
|
+
|
|
23
|
+
// Try CSS
|
|
24
|
+
try {
|
|
25
|
+
const cssElements = (root as Document | Element).querySelectorAll(selector);
|
|
26
|
+
if (cssElements.length > 0) return Array.from(cssElements);
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore invalid CSS: we'll try XPath next
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Try XPath
|
|
32
|
+
try {
|
|
33
|
+
const doc = root instanceof Document ? root : root.ownerDocument ?? document;
|
|
34
|
+
const result = doc.evaluate(
|
|
35
|
+
selector,
|
|
36
|
+
root as Node,
|
|
37
|
+
null,
|
|
38
|
+
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
39
|
+
null
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const elements: Element[] = [];
|
|
43
|
+
for (let i = 0; i < result.snapshotLength; i++) {
|
|
44
|
+
const element = result.snapshotItem(i) as Element;
|
|
45
|
+
if (element) elements.push(element);
|
|
46
|
+
}
|
|
47
|
+
return elements;
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enhanced step interface with trigger support
|
|
55
|
+
*/
|
|
56
|
+
export interface EnhancedStepData {
|
|
57
|
+
stepId: string;
|
|
58
|
+
stepOrder: number;
|
|
59
|
+
stepType: string; // "Mandatory" | "Optional"
|
|
60
|
+
trigger?: TriggerDefinition;
|
|
61
|
+
uxExperience?: {
|
|
62
|
+
uxExperienceType: string;
|
|
63
|
+
elementSelector?: string;
|
|
64
|
+
elementTrigger?: string;
|
|
65
|
+
elementLocation?: string;
|
|
66
|
+
position?: { x: string; y: string };
|
|
67
|
+
content: any;
|
|
68
|
+
modalContent?: any;
|
|
69
|
+
};
|
|
70
|
+
conditionRuleBlocks?: any[];
|
|
71
|
+
userInputSelector?: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Trigger Manager handles step-level trigger resolution and evaluation
|
|
76
|
+
* with page-aware context management for SPA and multi-page applications
|
|
77
|
+
*/
|
|
78
|
+
export class TriggerManager {
|
|
79
|
+
private static _instance: TriggerManager;
|
|
80
|
+
private _activeListeners: Map<string, (() => void)[]> = new Map();
|
|
81
|
+
private _triggeredOnceSet: Set<string> = new Set();
|
|
82
|
+
private _pageContextUnsubscribe: (() => void) | null = null;
|
|
83
|
+
private _initialized = false;
|
|
84
|
+
private _registeredTriggers: Record<string, {
|
|
85
|
+
trigger: TriggerDefinition;
|
|
86
|
+
onTrigger: (context: TriggerEvaluationContext) => void;
|
|
87
|
+
flowContext?: { mode: 'Linear' | 'AnyOrder', currentStepActive: boolean };
|
|
88
|
+
}> = {};
|
|
89
|
+
private _waitTimeouts: Map<string, number> = new Map();
|
|
90
|
+
private _selectorWaitTimeout: number = 30000; // 30 seconds default
|
|
91
|
+
|
|
92
|
+
private constructor() {}
|
|
93
|
+
|
|
94
|
+
public static getInstance(): TriggerManager {
|
|
95
|
+
if (!this._instance) {
|
|
96
|
+
this._instance = new TriggerManager();
|
|
97
|
+
}
|
|
98
|
+
return this._instance;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Initialize trigger manager with page context tracking
|
|
103
|
+
*/
|
|
104
|
+
public initialize(): void {
|
|
105
|
+
if (this._initialized) return;
|
|
106
|
+
|
|
107
|
+
// Initialize page context service
|
|
108
|
+
pageContextService.initialize();
|
|
109
|
+
|
|
110
|
+
// Subscribe to page changes
|
|
111
|
+
this._pageContextUnsubscribe = pageContextService.subscribe(this.handlePageChange.bind(this));
|
|
112
|
+
|
|
113
|
+
this._initialized = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handle page change events - re-evaluate all registered triggers
|
|
118
|
+
*/
|
|
119
|
+
private handlePageChange(event: PageChangeEvent): void {
|
|
120
|
+
console.debug(`[DAP] 📄 Page changed from ${event.previous?.pathname} to ${event.current.pathname}`);
|
|
121
|
+
console.debug('[DAP] Re-evaluating all registered triggers for new page context');
|
|
122
|
+
|
|
123
|
+
// Clear existing timeouts as page context changed
|
|
124
|
+
this.clearAllTimeouts();
|
|
125
|
+
|
|
126
|
+
// Clear triggered-once set on page changes for lifecycle triggers
|
|
127
|
+
if (event.type !== 'initial') {
|
|
128
|
+
this.clearLifecycleTriggers();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Re-evaluate all registered triggers for new page
|
|
132
|
+
for (const [stepId, registration] of Object.entries(this._registeredTriggers)) {
|
|
133
|
+
console.debug(`[DAP] Re-registering triggers for step ${stepId} on new page`);
|
|
134
|
+
|
|
135
|
+
// Remove old listeners first
|
|
136
|
+
this.removeTriggerListeners(stepId);
|
|
137
|
+
|
|
138
|
+
// Re-register with new page context
|
|
139
|
+
this.registerTriggerListeners(
|
|
140
|
+
stepId,
|
|
141
|
+
registration.trigger,
|
|
142
|
+
registration.onTrigger,
|
|
143
|
+
registration.flowContext
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Clear triggered-once set for lifecycle triggers on page change
|
|
150
|
+
*/
|
|
151
|
+
private clearLifecycleTriggers(): void {
|
|
152
|
+
const lifecycleKeys = Array.from(this._triggeredOnceSet).filter(key =>
|
|
153
|
+
key.includes(':Lifecycle:')
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
lifecycleKeys.forEach(key => {
|
|
157
|
+
this._triggeredOnceSet.delete(key);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Re-evaluate active triggers on page change
|
|
163
|
+
*/
|
|
164
|
+
private reEvaluateActiveTriggers(event: PageChangeEvent): void {
|
|
165
|
+
// Re-check all registered steps after page change
|
|
166
|
+
Object.entries(this._registeredTriggers).forEach(([stepId, { trigger, onTrigger, flowContext }]) => {
|
|
167
|
+
this.registerTriggerListeners(stepId, trigger, onTrigger, flowContext);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Re-register all triggers for page context changes
|
|
173
|
+
*/
|
|
174
|
+
public reRegisterAllTriggers(): void {
|
|
175
|
+
Object.entries(this._registeredTriggers).forEach(([stepId, { trigger, onTrigger, flowContext }]) => {
|
|
176
|
+
this.registerTriggerListeners(stepId, trigger, onTrigger, flowContext);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Clean up resources
|
|
182
|
+
*/
|
|
183
|
+
public destroy(): void {
|
|
184
|
+
if (!this._initialized) return;
|
|
185
|
+
|
|
186
|
+
// Clean up all active listeners
|
|
187
|
+
for (const stepId of this._activeListeners.keys()) {
|
|
188
|
+
this.removeTriggerListeners(stepId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Unsubscribe from page changes
|
|
192
|
+
if (this._pageContextUnsubscribe) {
|
|
193
|
+
this._pageContextUnsubscribe();
|
|
194
|
+
this._pageContextUnsubscribe = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this._activeListeners.clear();
|
|
198
|
+
this._triggeredOnceSet.clear();
|
|
199
|
+
this._registeredTriggers = {};
|
|
200
|
+
this._initialized = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve trigger for a step following the priority rules:
|
|
205
|
+
* 1. Use step.trigger if it exists and has valid conditions
|
|
206
|
+
* 2. Fallback to uxExperience.elementTrigger if available
|
|
207
|
+
* 3. Return null if no trigger is resolvable
|
|
208
|
+
*/
|
|
209
|
+
public resolveTrigger(step: EnhancedStepData): TriggerDefinition | null {
|
|
210
|
+
console.debug(`[DAP] Resolving trigger for step: ${step.stepId}`);
|
|
211
|
+
|
|
212
|
+
// Rule 1: Check if step has trigger with valid conditions
|
|
213
|
+
if (step.trigger && step.trigger.conditions && step.trigger.conditions.length > 0) {
|
|
214
|
+
console.log(`✅ [DAP] Step ${step.stepId}: Using STEP-LEVEL trigger with ${step.trigger.conditions.length} conditions`);
|
|
215
|
+
console.log(` └── Trigger type: ${step.trigger.type}, Event: ${step.trigger.conditions[0]?.event}, Kind: ${step.trigger.conditions[0]?.kind}`);
|
|
216
|
+
|
|
217
|
+
// Also log if elementTrigger exists (it will be ignored)
|
|
218
|
+
if (step.uxExperience?.elementTrigger) {
|
|
219
|
+
console.log(` ⚠️ Note: elementTrigger "${step.uxExperience.elementTrigger}" is present but IGNORED (step-level takes priority)`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return step.trigger;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Rule 2: Fallback to elementTrigger if available
|
|
226
|
+
if (step.uxExperience?.elementTrigger) {
|
|
227
|
+
console.warn(`⚠️ [DAP] Step ${step.stepId}: Falling back to ELEMENT-LEVEL trigger: "${step.uxExperience.elementTrigger}"`);
|
|
228
|
+
console.warn(` └── This fallback will be removed in the future! Please add step-level trigger.`);
|
|
229
|
+
console.warn(` └── Element selector: ${step.uxExperience.elementSelector}`);
|
|
230
|
+
|
|
231
|
+
const fallbackTrigger = this.convertElementTriggerToTriggerDefinition(
|
|
232
|
+
step.uxExperience.elementTrigger,
|
|
233
|
+
step.uxExperience.elementSelector
|
|
234
|
+
);
|
|
235
|
+
return fallbackTrigger;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Rule 3: No trigger available
|
|
239
|
+
console.error(`❌ [DAP] Step ${step.stepId}: NO TRIGGER FOUND! Step will execute immediately.`);
|
|
240
|
+
console.error(` └── Consider adding either step-level trigger or elementTrigger`);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Convert legacy elementTrigger to TriggerDefinition
|
|
246
|
+
*/
|
|
247
|
+
private convertElementTriggerToTriggerDefinition(
|
|
248
|
+
elementTrigger: string,
|
|
249
|
+
elementSelector?: string
|
|
250
|
+
): TriggerDefinition {
|
|
251
|
+
let condition: TriggerCondition;
|
|
252
|
+
|
|
253
|
+
// Map legacy elementTrigger values
|
|
254
|
+
switch (elementTrigger.toLowerCase().trim()) {
|
|
255
|
+
case "on click":
|
|
256
|
+
case "click":
|
|
257
|
+
condition = {
|
|
258
|
+
kind: "Dom",
|
|
259
|
+
event: "click",
|
|
260
|
+
selector: elementSelector
|
|
261
|
+
};
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case "on hover":
|
|
265
|
+
case "hover":
|
|
266
|
+
condition = {
|
|
267
|
+
kind: "Dom",
|
|
268
|
+
event: "hover",
|
|
269
|
+
selector: elementSelector
|
|
270
|
+
};
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case "on page load":
|
|
274
|
+
case "page load":
|
|
275
|
+
condition = {
|
|
276
|
+
kind: "Lifecycle",
|
|
277
|
+
event: "page-load"
|
|
278
|
+
};
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case "on focus":
|
|
282
|
+
case "focus":
|
|
283
|
+
condition = {
|
|
284
|
+
kind: "Dom",
|
|
285
|
+
event: "focus",
|
|
286
|
+
selector: elementSelector
|
|
287
|
+
};
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
default:
|
|
291
|
+
// Default to click if unrecognized
|
|
292
|
+
console.warn(`[DAP] Unknown elementTrigger: ${elementTrigger}, defaulting to click`);
|
|
293
|
+
condition = {
|
|
294
|
+
kind: "Dom",
|
|
295
|
+
event: "click",
|
|
296
|
+
selector: elementSelector
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
type: "Single",
|
|
302
|
+
operator: "And",
|
|
303
|
+
once: true,
|
|
304
|
+
conditions: [condition]
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Register trigger listeners for a step (page-aware)
|
|
310
|
+
*/
|
|
311
|
+
public registerTriggerListeners(
|
|
312
|
+
stepId: string,
|
|
313
|
+
trigger: TriggerDefinition,
|
|
314
|
+
onTrigger: (context: TriggerEvaluationContext) => void,
|
|
315
|
+
flowContext?: { mode: 'Linear' | 'AnyOrder', currentStepActive: boolean }
|
|
316
|
+
): void {
|
|
317
|
+
const currentPage = pageContextService.getPageId();
|
|
318
|
+
|
|
319
|
+
// Store trigger registration for re-evaluation on page changes
|
|
320
|
+
this._registeredTriggers[stepId] = { trigger, onTrigger, flowContext };
|
|
321
|
+
|
|
322
|
+
// Remove any existing listeners for this step
|
|
323
|
+
this.removeTriggerListeners(stepId);
|
|
324
|
+
|
|
325
|
+
const listeners: (() => void)[] = [];
|
|
326
|
+
|
|
327
|
+
for (const condition of trigger.conditions) {
|
|
328
|
+
const listener = this.createConditionListener(stepId, condition, trigger, onTrigger, flowContext);
|
|
329
|
+
if (listener) {
|
|
330
|
+
listeners.push(listener);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Store listeners for cleanup
|
|
335
|
+
if (listeners.length > 0) {
|
|
336
|
+
this._activeListeners.set(stepId, listeners);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create listener for individual trigger condition
|
|
342
|
+
*/
|
|
343
|
+
private createConditionListener(
|
|
344
|
+
stepId: string,
|
|
345
|
+
condition: TriggerCondition,
|
|
346
|
+
trigger: TriggerDefinition,
|
|
347
|
+
onTrigger: (context: TriggerEvaluationContext) => void,
|
|
348
|
+
flowContext?: { mode: 'Linear' | 'AnyOrder', currentStepActive: boolean }
|
|
349
|
+
): (() => void) | null {
|
|
350
|
+
|
|
351
|
+
switch (condition.kind) {
|
|
352
|
+
case "Dom":
|
|
353
|
+
return this.createDomListener(stepId, condition, trigger, onTrigger);
|
|
354
|
+
|
|
355
|
+
case "Lifecycle":
|
|
356
|
+
return this.createLifecycleListener(stepId, condition, trigger, onTrigger, flowContext);
|
|
357
|
+
|
|
358
|
+
case "Input":
|
|
359
|
+
return this.createInputListener(stepId, condition, trigger, onTrigger);
|
|
360
|
+
|
|
361
|
+
case "Time":
|
|
362
|
+
return this.createTimeListener(stepId, condition, trigger, onTrigger);
|
|
363
|
+
|
|
364
|
+
default:
|
|
365
|
+
console.warn(`[DAP] Unsupported condition kind: ${condition.kind}`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Map trigger events to actual DOM events
|
|
372
|
+
*/
|
|
373
|
+
private mapTriggerEventToDOMEvents(triggerEvent: string): string[] {
|
|
374
|
+
switch (triggerEvent) {
|
|
375
|
+
case 'hover':
|
|
376
|
+
return ['mouseenter'];
|
|
377
|
+
case 'click':
|
|
378
|
+
return ['click'];
|
|
379
|
+
case 'focus':
|
|
380
|
+
return ['focus'];
|
|
381
|
+
case 'blur':
|
|
382
|
+
return ['blur'];
|
|
383
|
+
case 'change':
|
|
384
|
+
return ['change'];
|
|
385
|
+
case 'input':
|
|
386
|
+
return ['input'];
|
|
387
|
+
case 'submit':
|
|
388
|
+
return ['submit'];
|
|
389
|
+
default:
|
|
390
|
+
// Return the event as-is for custom events
|
|
391
|
+
return [triggerEvent];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Create DOM event listener
|
|
397
|
+
*/
|
|
398
|
+
private createDomListener(
|
|
399
|
+
stepId: string,
|
|
400
|
+
condition: TriggerCondition,
|
|
401
|
+
trigger: TriggerDefinition,
|
|
402
|
+
onTrigger: (context: TriggerEvaluationContext) => void
|
|
403
|
+
): (() => void) | null {
|
|
404
|
+
if (!condition.selector) {
|
|
405
|
+
console.warn(`[DAP] DOM condition missing selector for step: ${stepId}`);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Page context validation
|
|
410
|
+
const validation = this.validateSelectorOnCurrentPage(condition.selector);
|
|
411
|
+
console.debug(`[DAP] 📄 Page context validation for ${stepId}: selector exists=${validation.exists}, count=${validation.elementCount}`);
|
|
412
|
+
|
|
413
|
+
let targetElement: HTMLElement | null = null;
|
|
414
|
+
let observer: MutationObserver | null = null;
|
|
415
|
+
let timeoutCleanup: (() => void) | null = null;
|
|
416
|
+
|
|
417
|
+
// Function to attach listener to element
|
|
418
|
+
const attachListener = (element: HTMLElement) => {
|
|
419
|
+
// Clear timeout since we found the element
|
|
420
|
+
if (timeoutCleanup) {
|
|
421
|
+
timeoutCleanup();
|
|
422
|
+
timeoutCleanup = null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Map trigger event to appropriate DOM events
|
|
426
|
+
const eventNames = this.mapTriggerEventToDOMEvents(condition.event);
|
|
427
|
+
const cleanupFunctions: (() => void)[] = [];
|
|
428
|
+
|
|
429
|
+
console.debug(`[DAP] Mapping trigger event "${condition.event}" to DOM events:`, eventNames);
|
|
430
|
+
|
|
431
|
+
for (const eventName of eventNames) {
|
|
432
|
+
const eventHandler = (event: Event) => {
|
|
433
|
+
console.debug(`[DAP] DOM event triggered:`, event.type, condition.selector);
|
|
434
|
+
|
|
435
|
+
// Check if this is a "once" trigger and already fired
|
|
436
|
+
const onceKey = `${stepId}:${condition.kind}:${condition.event}`;
|
|
437
|
+
if (trigger.once && this._triggeredOnceSet.has(onceKey)) {
|
|
438
|
+
console.debug(`[DAP] Trigger already fired once for: ${onceKey}`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Debouncing
|
|
443
|
+
if (condition.debounceMs) {
|
|
444
|
+
const debounceKey = `debounce:${stepId}:${condition.event}`;
|
|
445
|
+
const lastFired = this.getLastFiredTime(debounceKey);
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
if (lastFired && (now - lastFired) < condition.debounceMs) {
|
|
448
|
+
console.debug(`[DAP] Event debounced for step: ${stepId}`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.setLastFiredTime(debounceKey, now);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Evaluate trigger
|
|
455
|
+
const context: TriggerEvaluationContext = {
|
|
456
|
+
stepId,
|
|
457
|
+
flowId: '', // Will be set by caller
|
|
458
|
+
element,
|
|
459
|
+
event
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// For input events, capture the current input value
|
|
463
|
+
if (element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) {
|
|
464
|
+
if (condition.event === 'input' || condition.event === 'change' || condition.event === 'keyup') {
|
|
465
|
+
context.userInput = element.value;
|
|
466
|
+
console.debug(`[DAP] Captured input value: "${context.userInput}" for step: ${stepId}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const result = this.evaluateTrigger(trigger, context);
|
|
471
|
+
if (result.triggered) {
|
|
472
|
+
if (trigger.once) {
|
|
473
|
+
this._triggeredOnceSet.add(onceKey);
|
|
474
|
+
}
|
|
475
|
+
onTrigger(context);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
element.addEventListener(eventName, eventHandler);
|
|
480
|
+
|
|
481
|
+
cleanupFunctions.push(() => {
|
|
482
|
+
element.removeEventListener(eventName, eventHandler);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return () => {
|
|
487
|
+
cleanupFunctions.forEach(cleanup => cleanup());
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// Try to find element immediately if validation passed
|
|
492
|
+
if (validation.exists) {
|
|
493
|
+
try {
|
|
494
|
+
targetElement = resolveSelector<HTMLElement>(condition.selector);
|
|
495
|
+
if (targetElement) {
|
|
496
|
+
console.debug(`[DAP] Element found immediately for selector: ${condition.selector}`);
|
|
497
|
+
return attachListener(targetElement);
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.warn(`[DAP] Invalid selector: ${condition.selector}`, error);
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Element not found, set up observer with timeout
|
|
506
|
+
console.debug(`[DAP] Element not found, waiting for: ${condition.selector}`);
|
|
507
|
+
|
|
508
|
+
let listenerCleanup: (() => void) | null = null;
|
|
509
|
+
|
|
510
|
+
// Set up timeout for element waiting
|
|
511
|
+
this.setupSelectorTimeout(stepId, condition.selector, () => {
|
|
512
|
+
console.warn(`[DAP] ⚠️ Selector timeout for step ${stepId}: ${condition.selector}`);
|
|
513
|
+
|
|
514
|
+
// Disconnect observer
|
|
515
|
+
if (observer) {
|
|
516
|
+
observer.disconnect();
|
|
517
|
+
observer = null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Emit telemetry event
|
|
521
|
+
console.warn(`[DAP] 📊 Telemetry: selector-not-found - Step: ${stepId}, Selector: ${condition.selector}`);
|
|
522
|
+
|
|
523
|
+
// TODO: Based on step type, either skip (Optional) or fail safely (Mandatory)
|
|
524
|
+
// For now, we just clean up and let the flow continue
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Keep reference to timeout cleanup
|
|
528
|
+
timeoutCleanup = () => this.clearTimeoutForStep(stepId);
|
|
529
|
+
|
|
530
|
+
observer = new MutationObserver(() => {
|
|
531
|
+
try {
|
|
532
|
+
const element = resolveSelector<HTMLElement>(condition.selector!);
|
|
533
|
+
if (element && element !== targetElement) {
|
|
534
|
+
targetElement = element;
|
|
535
|
+
console.debug(`[DAP] Element appeared: ${condition.selector}`);
|
|
536
|
+
|
|
537
|
+
// Cleanup observer and attach listener
|
|
538
|
+
if (observer) {
|
|
539
|
+
observer.disconnect();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
listenerCleanup = attachListener(element);
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
// Ignore selector errors during observation
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
observer.observe(document.body, {
|
|
550
|
+
childList: true,
|
|
551
|
+
subtree: true
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Return cleanup function
|
|
555
|
+
return () => {
|
|
556
|
+
if (observer) {
|
|
557
|
+
observer.disconnect();
|
|
558
|
+
}
|
|
559
|
+
if (listenerCleanup) {
|
|
560
|
+
listenerCleanup();
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create lifecycle event listener (page-aware)
|
|
567
|
+
*/
|
|
568
|
+
private createLifecycleListener(
|
|
569
|
+
stepId: string,
|
|
570
|
+
condition: TriggerCondition,
|
|
571
|
+
trigger: TriggerDefinition,
|
|
572
|
+
onTrigger: (context: TriggerEvaluationContext) => void,
|
|
573
|
+
flowContext?: { mode: 'Linear' | 'AnyOrder', currentStepActive: boolean }
|
|
574
|
+
): (() => void) | null {
|
|
575
|
+
switch (condition.event) {
|
|
576
|
+
case "page-load":
|
|
577
|
+
// Check if this trigger should fire immediately
|
|
578
|
+
const onceKey = `${stepId}:${condition.kind}:${condition.event}`;
|
|
579
|
+
const shouldFireImmediately = !trigger.once || !this._triggeredOnceSet.has(onceKey);
|
|
580
|
+
|
|
581
|
+
if (shouldFireImmediately) {
|
|
582
|
+
// Fire immediately for current page
|
|
583
|
+
setTimeout(() => {
|
|
584
|
+
const context: TriggerEvaluationContext = {
|
|
585
|
+
stepId,
|
|
586
|
+
flowId: '', // Will be set by caller
|
|
587
|
+
pageState: {
|
|
588
|
+
loaded: true,
|
|
589
|
+
pageId: pageContextService.getPageId()
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const result = this.evaluateTrigger(trigger, context);
|
|
594
|
+
if (result.triggered) {
|
|
595
|
+
if (trigger.once) {
|
|
596
|
+
this._triggeredOnceSet.add(onceKey);
|
|
597
|
+
}
|
|
598
|
+
onTrigger(context);
|
|
599
|
+
}
|
|
600
|
+
}, 100); // Small delay to ensure page is ready
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Also listen for future page changes
|
|
604
|
+
const pageChangeUnsubscribe = pageContextService.subscribe((event) => {
|
|
605
|
+
// Only fire on navigation events, not initial load
|
|
606
|
+
if (event.type === 'navigation' || event.type === 'reload') {
|
|
607
|
+
console.debug(`[DAP] Page change detected for page-load trigger, step: ${stepId}`);
|
|
608
|
+
|
|
609
|
+
const pageLoadOnceKey = `${stepId}:${condition.kind}:${condition.event}`;
|
|
610
|
+
|
|
611
|
+
// For page-load triggers, reset the "once" flag on page changes
|
|
612
|
+
if (trigger.once && event.type === 'navigation') {
|
|
613
|
+
this._triggeredOnceSet.delete(pageLoadOnceKey);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Check if we should fire again
|
|
617
|
+
const shouldFire = !trigger.once || !this._triggeredOnceSet.has(pageLoadOnceKey);
|
|
618
|
+
|
|
619
|
+
if (shouldFire) {
|
|
620
|
+
const context: TriggerEvaluationContext = {
|
|
621
|
+
stepId,
|
|
622
|
+
flowId: '',
|
|
623
|
+
pageState: {
|
|
624
|
+
loaded: true,
|
|
625
|
+
pageId: pageContextService.getPageId(),
|
|
626
|
+
navigationEvent: event
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const result = this.evaluateTrigger(trigger, context);
|
|
631
|
+
if (result.triggered) {
|
|
632
|
+
if (trigger.once) {
|
|
633
|
+
this._triggeredOnceSet.add(pageLoadOnceKey);
|
|
634
|
+
}
|
|
635
|
+
onTrigger(context);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return () => {
|
|
642
|
+
pageChangeUnsubscribe();
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
case "page-unload":
|
|
646
|
+
const unloadHandler = () => {
|
|
647
|
+
const context: TriggerEvaluationContext = {
|
|
648
|
+
stepId,
|
|
649
|
+
flowId: '',
|
|
650
|
+
pageState: {
|
|
651
|
+
unloading: true,
|
|
652
|
+
pageId: pageContextService.getPageId()
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const result = this.evaluateTrigger(trigger, context);
|
|
657
|
+
if (result.triggered) {
|
|
658
|
+
onTrigger(context);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
window.addEventListener('beforeunload', unloadHandler);
|
|
663
|
+
return () => {
|
|
664
|
+
window.removeEventListener('beforeunload', unloadHandler);
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
default:
|
|
668
|
+
console.warn(`[DAP] Unsupported lifecycle event: ${condition.event}`);
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Create input event listener
|
|
675
|
+
*/
|
|
676
|
+
private createInputListener(
|
|
677
|
+
stepId: string,
|
|
678
|
+
condition: TriggerCondition,
|
|
679
|
+
trigger: TriggerDefinition,
|
|
680
|
+
onTrigger: (context: TriggerEvaluationContext) => void
|
|
681
|
+
): (() => void) | null {
|
|
682
|
+
if (!condition.selector) {
|
|
683
|
+
console.warn(`[DAP] Input condition missing selector for step: ${stepId}`);
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Page context validation for input selectors
|
|
688
|
+
const validation = this.validateSelectorOnCurrentPage(condition.selector);
|
|
689
|
+
console.debug(`[DAP] 📄 Input page context validation for ${stepId}: selector exists=${validation.exists}, count=${validation.elementCount}`);
|
|
690
|
+
|
|
691
|
+
const inputHandler = (event: Event) => {
|
|
692
|
+
const target = event.target as HTMLInputElement;
|
|
693
|
+
const value = target.value;
|
|
694
|
+
|
|
695
|
+
// Evaluate condition based on operator
|
|
696
|
+
let conditionMet = false;
|
|
697
|
+
if (condition.operator && condition.value !== undefined) {
|
|
698
|
+
conditionMet = this.evaluateCondition(value, condition.operator, condition.value);
|
|
699
|
+
} else {
|
|
700
|
+
conditionMet = true; // No specific condition, just trigger on input
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (conditionMet) {
|
|
704
|
+
const context: TriggerEvaluationContext = {
|
|
705
|
+
stepId,
|
|
706
|
+
flowId: '',
|
|
707
|
+
element: target,
|
|
708
|
+
event,
|
|
709
|
+
userInput: value
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const result = this.evaluateTrigger(trigger, context);
|
|
713
|
+
if (result.triggered) {
|
|
714
|
+
onTrigger(context);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const elements = resolveSelectorAll(condition.selector!);
|
|
720
|
+
|
|
721
|
+
if (elements.length === 0) {
|
|
722
|
+
console.log(`[DAP] Input elements not found, waiting: ${condition.selector}`);
|
|
723
|
+
|
|
724
|
+
// Set up timeout for input element waiting
|
|
725
|
+
this.setupSelectorTimeout(stepId, condition.selector, () => {
|
|
726
|
+
console.warn(`[DAP] ⚠️ Input selector timeout for step ${stepId}: ${condition.selector}`);
|
|
727
|
+
console.warn(`[DAP] 📊 Telemetry: input-selector-not-found - Step: ${stepId}, Selector: ${condition.selector}`);
|
|
728
|
+
|
|
729
|
+
// Input elements critical for rule-based steps - this might indicate cross-page issue
|
|
730
|
+
if (stepId.includes('rule') || stepId.includes('condition')) {
|
|
731
|
+
console.error(`[DAP] 🚨 Rule-based step ${stepId} cannot find input selector - possible cross-page navigation issue`);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Set up a MutationObserver to wait for the elements to appear
|
|
736
|
+
const observer = new MutationObserver(() => {
|
|
737
|
+
const foundElements = resolveSelectorAll(condition.selector!);
|
|
738
|
+
if (foundElements.length > 0) {
|
|
739
|
+
console.log(`[DAP] Input elements appeared: ${condition.selector}`);
|
|
740
|
+
|
|
741
|
+
// Clear timeout since elements found
|
|
742
|
+
this.clearTimeoutForStep(stepId);
|
|
743
|
+
|
|
744
|
+
foundElements.forEach(element => {
|
|
745
|
+
element.addEventListener('input', inputHandler);
|
|
746
|
+
element.addEventListener('change', inputHandler);
|
|
747
|
+
});
|
|
748
|
+
observer.disconnect();
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
observer.observe(document.body, {
|
|
753
|
+
childList: true,
|
|
754
|
+
subtree: true
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Return cleanup function that disconnects the observer and clears timeout
|
|
758
|
+
return () => {
|
|
759
|
+
this.clearTimeoutForStep(stepId);
|
|
760
|
+
observer.disconnect();
|
|
761
|
+
const foundElements = resolveSelectorAll(condition.selector!);
|
|
762
|
+
foundElements.forEach(element => {
|
|
763
|
+
element.removeEventListener('input', inputHandler);
|
|
764
|
+
element.removeEventListener('change', inputHandler);
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Elements found immediately
|
|
770
|
+
console.debug(`[DAP] ✅ Input elements found immediately: ${elements.length} element(s)`);
|
|
771
|
+
elements.forEach(element => {
|
|
772
|
+
element.addEventListener('input', inputHandler);
|
|
773
|
+
element.addEventListener('change', inputHandler);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
return () => {
|
|
777
|
+
elements.forEach(element => {
|
|
778
|
+
element.removeEventListener('input', inputHandler);
|
|
779
|
+
element.removeEventListener('change', inputHandler);
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Create time-based listener
|
|
786
|
+
*/
|
|
787
|
+
private createTimeListener(
|
|
788
|
+
stepId: string,
|
|
789
|
+
condition: TriggerCondition,
|
|
790
|
+
trigger: TriggerDefinition,
|
|
791
|
+
onTrigger: (context: TriggerEvaluationContext) => void
|
|
792
|
+
): (() => void) | null {
|
|
793
|
+
const delay = typeof condition.value === 'number' ? condition.value : 1000;
|
|
794
|
+
|
|
795
|
+
const timeoutId = setTimeout(() => {
|
|
796
|
+
const context: TriggerEvaluationContext = {
|
|
797
|
+
stepId,
|
|
798
|
+
flowId: '',
|
|
799
|
+
pageState: { timeElapsed: delay }
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
const result = this.evaluateTrigger(trigger, context);
|
|
803
|
+
if (result.triggered) {
|
|
804
|
+
onTrigger(context);
|
|
805
|
+
}
|
|
806
|
+
}, delay);
|
|
807
|
+
|
|
808
|
+
return () => {
|
|
809
|
+
clearTimeout(timeoutId);
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Evaluate complete trigger (all conditions with logical operator)
|
|
815
|
+
*/
|
|
816
|
+
public evaluateTrigger(
|
|
817
|
+
trigger: TriggerDefinition,
|
|
818
|
+
context: TriggerEvaluationContext
|
|
819
|
+
): TriggerEvaluationResult {
|
|
820
|
+
const startTime = Date.now();
|
|
821
|
+
let matchedConditions = 0;
|
|
822
|
+
const totalConditions = trigger.conditions.length;
|
|
823
|
+
|
|
824
|
+
// For single condition triggers, just check if we have one condition
|
|
825
|
+
if (trigger.type === "Single" && totalConditions === 1) {
|
|
826
|
+
matchedConditions = 1; // Assume the condition was met if we're evaluating
|
|
827
|
+
} else if (trigger.type === "Composite") {
|
|
828
|
+
// For composite triggers, we need more sophisticated evaluation
|
|
829
|
+
// For now, we'll assume if we got here, at least one condition was met
|
|
830
|
+
matchedConditions = 1;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
let triggered = false;
|
|
834
|
+
if (trigger.operator === "And") {
|
|
835
|
+
triggered = matchedConditions === totalConditions;
|
|
836
|
+
} else if (trigger.operator === "Or") {
|
|
837
|
+
triggered = matchedConditions > 0;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const result: TriggerEvaluationResult = {
|
|
841
|
+
triggered,
|
|
842
|
+
matchedConditions,
|
|
843
|
+
totalConditions,
|
|
844
|
+
evaluationTime: Date.now() - startTime,
|
|
845
|
+
debugInfo: {
|
|
846
|
+
triggerType: trigger.type,
|
|
847
|
+
operator: trigger.operator,
|
|
848
|
+
once: trigger.once
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
console.debug(`[DAP] Trigger evaluation result:`, result);
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Evaluate individual condition value
|
|
858
|
+
*/
|
|
859
|
+
private evaluateCondition(actualValue: any, operator: RuleOperator, expectedValue: any): boolean {
|
|
860
|
+
switch (operator) {
|
|
861
|
+
case "Equals":
|
|
862
|
+
return actualValue === expectedValue;
|
|
863
|
+
case "NotEquals":
|
|
864
|
+
return actualValue !== expectedValue;
|
|
865
|
+
case "Contains":
|
|
866
|
+
return String(actualValue).includes(String(expectedValue));
|
|
867
|
+
case "NotContains":
|
|
868
|
+
return !String(actualValue).includes(String(expectedValue));
|
|
869
|
+
case "StartsWith":
|
|
870
|
+
return String(actualValue).startsWith(String(expectedValue));
|
|
871
|
+
case "EndsWith":
|
|
872
|
+
return String(actualValue).endsWith(String(expectedValue));
|
|
873
|
+
case "GreaterThan":
|
|
874
|
+
return Number(actualValue) > Number(expectedValue);
|
|
875
|
+
case "LessThan":
|
|
876
|
+
return Number(actualValue) < Number(expectedValue);
|
|
877
|
+
case "GreaterThanOrEqual":
|
|
878
|
+
return Number(actualValue) >= Number(expectedValue);
|
|
879
|
+
case "LessThanOrEqual":
|
|
880
|
+
return Number(actualValue) <= Number(expectedValue);
|
|
881
|
+
case "Empty":
|
|
882
|
+
return !actualValue || String(actualValue).trim() === '';
|
|
883
|
+
case "In":
|
|
884
|
+
if (Array.isArray(expectedValue)) {
|
|
885
|
+
return expectedValue.includes(actualValue);
|
|
886
|
+
}
|
|
887
|
+
return false;
|
|
888
|
+
case "NotIn":
|
|
889
|
+
if (Array.isArray(expectedValue)) {
|
|
890
|
+
return !expectedValue.includes(actualValue);
|
|
891
|
+
}
|
|
892
|
+
return true;
|
|
893
|
+
case "Regex":
|
|
894
|
+
try {
|
|
895
|
+
const regex = new RegExp(String(expectedValue));
|
|
896
|
+
return regex.test(String(actualValue));
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
default:
|
|
901
|
+
console.warn(`[DAP] Unsupported operator: ${operator}`);
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Remove all listeners for a step
|
|
908
|
+
*/
|
|
909
|
+
public removeTriggerListeners(stepId: string): void {
|
|
910
|
+
const listeners = this._activeListeners.get(stepId);
|
|
911
|
+
if (listeners) {
|
|
912
|
+
listeners.forEach(cleanup => cleanup());
|
|
913
|
+
this._activeListeners.delete(stepId);
|
|
914
|
+
}
|
|
915
|
+
// Note: Don't remove from _registeredTriggers as we need it for re-registration
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Clear all listeners
|
|
920
|
+
*/
|
|
921
|
+
public clearAllListeners(): void {
|
|
922
|
+
for (const [stepId, listeners] of this._activeListeners) {
|
|
923
|
+
listeners.forEach(cleanup => cleanup());
|
|
924
|
+
}
|
|
925
|
+
this._activeListeners.clear();
|
|
926
|
+
this._triggeredOnceSet.clear();
|
|
927
|
+
this._registeredTriggers = {};
|
|
928
|
+
this.clearAllTimeouts();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Clear all active timeouts
|
|
933
|
+
*/
|
|
934
|
+
private clearAllTimeouts(): void {
|
|
935
|
+
for (const timeoutId of this._waitTimeouts.values()) {
|
|
936
|
+
clearTimeout(timeoutId);
|
|
937
|
+
}
|
|
938
|
+
this._waitTimeouts.clear();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Clear specific timeout for a step
|
|
943
|
+
*/
|
|
944
|
+
private clearTimeoutForStep(stepId: string): void {
|
|
945
|
+
const timeoutId = this._waitTimeouts.get(stepId);
|
|
946
|
+
if (timeoutId) {
|
|
947
|
+
clearTimeout(timeoutId);
|
|
948
|
+
this._waitTimeouts.delete(stepId);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Validate if selector exists on current page
|
|
954
|
+
*/
|
|
955
|
+
private validateSelectorOnCurrentPage(selector: string): {
|
|
956
|
+
exists: boolean;
|
|
957
|
+
elementCount: number;
|
|
958
|
+
validationTimestamp: number;
|
|
959
|
+
} {
|
|
960
|
+
const elements = resolveSelectorAll(selector);
|
|
961
|
+
return {
|
|
962
|
+
exists: elements.length > 0,
|
|
963
|
+
elementCount: elements.length,
|
|
964
|
+
validationTimestamp: Date.now()
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Set up timeout for selector waiting with proper cleanup
|
|
970
|
+
*/
|
|
971
|
+
private setupSelectorTimeout(
|
|
972
|
+
stepId: string,
|
|
973
|
+
selector: string,
|
|
974
|
+
onTimeout: () => void,
|
|
975
|
+
timeoutMs: number = this._selectorWaitTimeout
|
|
976
|
+
): void {
|
|
977
|
+
// Clear any existing timeout
|
|
978
|
+
this.clearTimeoutForStep(stepId);
|
|
979
|
+
|
|
980
|
+
const timeoutId = setTimeout(() => {
|
|
981
|
+
console.warn(`[DAP] ⏰ Timeout: Selector not found within ${timeoutMs}ms for step ${stepId}: ${selector}`);
|
|
982
|
+
console.warn(`[DAP] Triggering timeout handler and cleaning up for step: ${stepId}`);
|
|
983
|
+
this._waitTimeouts.delete(stepId);
|
|
984
|
+
onTimeout();
|
|
985
|
+
}, timeoutMs);
|
|
986
|
+
|
|
987
|
+
this._waitTimeouts.set(stepId, timeoutId);
|
|
988
|
+
console.debug(`[DAP] ⏱️ Set ${timeoutMs}ms timeout for selector waiting: ${stepId}`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Reset once-fired triggers for a new flow
|
|
993
|
+
*/
|
|
994
|
+
public resetOnceTriggersForFlow(flowId: string): void {
|
|
995
|
+
// Clear all once-fired triggers (we could be more selective in the future)
|
|
996
|
+
this._triggeredOnceSet.clear();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Helper methods for debouncing
|
|
1000
|
+
private _debounceTimestamps: Map<string, number> = new Map();
|
|
1001
|
+
|
|
1002
|
+
private getLastFiredTime(key: string): number | undefined {
|
|
1003
|
+
return this._debounceTimestamps.get(key);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private setLastFiredTime(key: string, timestamp: number): void {
|
|
1007
|
+
this._debounceTimestamps.set(key, timestamp);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export const triggerManager = TriggerManager.getInstance();
|