@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.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. package/tsup.config.ts +13 -0
@@ -0,0 +1,107 @@
1
+ // src/utils/selectors.ts
2
+ /**
3
+ * DOM selector utilities.
4
+ * - Accepts either a CSS selector or an XPath string (your backend sometimes sends XPath).
5
+ * - We keep the original string; resolution (CSS first, then XPath) happens at render time.
6
+ */
7
+
8
+ export function mapDomSelector(input: unknown): string {
9
+ if (typeof input !== "string") return "";
10
+ const sel = input.trim();
11
+ // We don't transform—just normalize whitespace so downstream can decide.
12
+ return sel;
13
+ }
14
+
15
+ /** Heuristic to detect if a string looks like XPath (vs CSS). */
16
+ export function isLikelyXPath(sel: string): boolean {
17
+ const s = sel.trim();
18
+ if (!s) return false;
19
+ // Simple checks that cover your samples like "/html[1]/body[1]/..."
20
+ return (
21
+ s.startsWith("/") ||
22
+ s.startsWith(".//") ||
23
+ s.startsWith("//") ||
24
+ s.includes("/@") ||
25
+ /\[\d+\]/.test(s)
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Optional convenience: resolve a selector against the DOM.
31
+ * Tries CSS first, then XPath. Safe to use anywhere.
32
+ */
33
+ export function resolveSelector<T extends Element = Element>(
34
+ sel: string,
35
+ root: Document | Element = document
36
+ ): T | null {
37
+ if (!sel || typeof sel !== "string") return null;
38
+
39
+ // Try CSS
40
+ try {
41
+ const cssEl = (root as Document | Element).querySelector(sel) as T | null;
42
+ if (cssEl) return cssEl;
43
+ } catch {
44
+ // ignore invalid CSS: we'll try XPath next
45
+ }
46
+
47
+ // Try XPath
48
+ try {
49
+ const doc = root instanceof Document ? root : root.ownerDocument ?? document;
50
+ const result = doc.evaluate(
51
+ sel,
52
+ root as Node,
53
+ null,
54
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
55
+ null
56
+ );
57
+ return (result.singleNodeValue as T) ?? null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Wait for an element to appear in the DOM with a timeout
65
+ */
66
+ export function waitForElement<T extends Element = Element>(
67
+ selector: string,
68
+ options: { timeout?: number; root?: Document | Element } = {}
69
+ ): Promise<T> {
70
+ const { timeout = 5000, root = document } = options;
71
+
72
+ return new Promise((resolve, reject) => {
73
+ // Check if element already exists
74
+ const existingElement = resolveSelector<T>(selector, root);
75
+ if (existingElement) {
76
+ resolve(existingElement);
77
+ return;
78
+ }
79
+
80
+ let timeoutId: number;
81
+ let observer: MutationObserver;
82
+
83
+ // Setup timeout
84
+ timeoutId = window.setTimeout(() => {
85
+ observer?.disconnect();
86
+ reject(new Error(`Element not found within timeout: ${selector}`));
87
+ }, timeout);
88
+
89
+ // Setup mutation observer
90
+ observer = new MutationObserver(() => {
91
+ const element = resolveSelector<T>(selector, root);
92
+ if (element) {
93
+ clearTimeout(timeoutId);
94
+ observer.disconnect();
95
+ resolve(element);
96
+ }
97
+ });
98
+
99
+ // Start observing
100
+ observer.observe(root as Node, {
101
+ childList: true,
102
+ subtree: true,
103
+ attributes: true,
104
+ attributeOldValue: false
105
+ });
106
+ });
107
+ }
@@ -0,0 +1,345 @@
1
+ // src/utils/stepExecutor.ts
2
+ // Step execution engine for trigger-based flow execution
3
+
4
+ import type {
5
+ ModalSequenceStep,
6
+ StepExecutionState,
7
+ StepExecutionContext,
8
+ FlowRunnerState
9
+ } from '../experiences/types';
10
+ import { resolveSelector } from './selectors';
11
+ import { normalizeTrigger } from './normalize';
12
+
13
+ /**
14
+ * Manages step execution state and trigger handling
15
+ */
16
+ export class StepExecutor {
17
+ private state: FlowRunnerState;
18
+ private steps: ModalSequenceStep[];
19
+ private flowId: string;
20
+
21
+ constructor(steps: ModalSequenceStep[], flowId: string) {
22
+ this.steps = steps;
23
+ this.flowId = flowId;
24
+ this.state = {
25
+ currentStepIndex: -1,
26
+ stepContexts: new Map(),
27
+ isMultiStep: steps.length > 1,
28
+ pageLoadTriggersExecuted: new Set()
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Initialize step contexts for all steps
34
+ */
35
+ public initializeSteps(): void {
36
+ console.debug(`[DAP] Initializing ${this.steps.length} steps for flow ${this.flowId}`);
37
+
38
+ this.steps.forEach((step, index) => {
39
+ const stepId = step.stepId || `step-${index + 1}`;
40
+ const hasElementTrigger = !!(step.elementSelector && step.elementTrigger);
41
+
42
+ const context: StepExecutionContext = {
43
+ stepIndex: index,
44
+ stepId: stepId,
45
+ state: "idle",
46
+ hasElementTrigger: hasElementTrigger,
47
+ triggerListeners: []
48
+ };
49
+
50
+ this.state.stepContexts.set(index, context);
51
+
52
+ console.debug(`[DAP] Step ${stepId} (${index}): hasElementTrigger=${hasElementTrigger}, trigger="${step.elementTrigger}"`);
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Start execution from a specific step index
58
+ */
59
+ public async startFromStep(startIndex: number): Promise<void> {
60
+ const clampedIndex = Math.max(0, Math.min(startIndex, this.steps.length - 1));
61
+ console.debug(`[DAP] Starting flow execution from step ${clampedIndex}`);
62
+
63
+ this.state.currentStepIndex = clampedIndex;
64
+ await this.executeStep(clampedIndex);
65
+ }
66
+
67
+ /**
68
+ * Execute a specific step based on its trigger configuration
69
+ */
70
+ private async executeStep(stepIndex: number): Promise<void> {
71
+ const step = this.steps[stepIndex];
72
+ const context = this.state.stepContexts.get(stepIndex);
73
+
74
+ if (!step || !context) {
75
+ console.error(`[DAP] Invalid step or context for index ${stepIndex}`);
76
+ return;
77
+ }
78
+
79
+ console.debug(`[DAP] Executing step ${context.stepId} with state ${context.state}`);
80
+
81
+ // Check if step has trigger configuration
82
+ if (context.hasElementTrigger && step.elementSelector && step.elementTrigger) {
83
+ await this.executeTriggeredStep(stepIndex, step, context);
84
+ } else {
85
+ // No trigger - render immediately with navigation controls
86
+ await this.executeImmediateStep(stepIndex, step, context);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Execute a step that requires a trigger
92
+ */
93
+ private async executeTriggeredStep(
94
+ stepIndex: number,
95
+ step: ModalSequenceStep,
96
+ context: StepExecutionContext
97
+ ): Promise<void> {
98
+
99
+ // Handle page load triggers specially
100
+ if (step.elementTrigger === "on page load") {
101
+ await this.handlePageLoadTrigger(stepIndex, step, context);
102
+ return;
103
+ }
104
+
105
+ // Find the target element
106
+ const targetElement = this.findTargetElement(step.elementSelector!);
107
+ if (!targetElement) {
108
+ console.warn(`[DAP] Element not found for selector "${step.elementSelector}", falling back to immediate execution`);
109
+ await this.executeImmediateStep(stepIndex, step, context);
110
+ return;
111
+ }
112
+
113
+ // Set up trigger listeners
114
+ context.targetElement = targetElement;
115
+ context.state = "waiting-for-trigger";
116
+
117
+ this.setupTriggerListeners(stepIndex, step, context, targetElement);
118
+
119
+ console.debug(`[DAP] Step ${context.stepId} is now waiting for trigger "${step.elementTrigger}" on element`, targetElement);
120
+ }
121
+
122
+ /**
123
+ * Execute a step immediately with navigation controls
124
+ */
125
+ private async executeImmediateStep(
126
+ stepIndex: number,
127
+ step: ModalSequenceStep,
128
+ context: StepExecutionContext
129
+ ): Promise<void> {
130
+ console.debug(`[DAP] Executing step ${context.stepId} immediately (no trigger)`);
131
+
132
+ context.state = "rendered";
133
+
134
+ // Render the step with navigation controls if multi-step
135
+ await this.renderStepWithNavigation(stepIndex, step, context);
136
+ }
137
+
138
+ /**
139
+ * Handle page load triggers with deduplication
140
+ */
141
+ private async handlePageLoadTrigger(
142
+ stepIndex: number,
143
+ step: ModalSequenceStep,
144
+ context: StepExecutionContext
145
+ ): Promise<void> {
146
+ const stepId = context.stepId;
147
+
148
+ // Check if this page load trigger has already been executed
149
+ if (this.state.pageLoadTriggersExecuted.has(stepId)) {
150
+ console.debug(`[DAP] Page load trigger for step ${stepId} already executed, skipping`);
151
+ return;
152
+ }
153
+
154
+ console.debug(`[DAP] Executing page load trigger for step ${stepId}`);
155
+
156
+ // Mark as executed for this page lifecycle
157
+ this.state.pageLoadTriggersExecuted.add(stepId);
158
+ context.state = "rendered";
159
+ context.lastTriggeredTime = Date.now();
160
+
161
+ // Render without navigation controls (triggered steps don't get navigation)
162
+ await this.renderStepWithoutNavigation(stepIndex, step, context);
163
+ }
164
+
165
+ /**
166
+ * Set up event listeners for triggered steps
167
+ */
168
+ private setupTriggerListeners(
169
+ stepIndex: number,
170
+ step: ModalSequenceStep,
171
+ context: StepExecutionContext,
172
+ targetElement: HTMLElement
173
+ ): void {
174
+ const normalizedTrigger = normalizeTrigger(step.elementTrigger);
175
+
176
+ const triggerHandler = (event: Event) => {
177
+ console.debug(`[DAP] Trigger "${normalizedTrigger}" fired for step ${context.stepId}`);
178
+
179
+ // Update context
180
+ context.state = "rendered";
181
+ context.lastTriggeredTime = Date.now();
182
+
183
+ // Clean up listeners before rendering
184
+ this.cleanupStepListeners(context);
185
+
186
+ // Render the step without navigation
187
+ this.renderStepWithoutNavigation(stepIndex, step, context);
188
+ };
189
+
190
+ // Map trigger types to event types
191
+ const eventType = this.mapTriggerToEventType(normalizedTrigger);
192
+ if (!eventType) {
193
+ console.warn(`[DAP] Unknown trigger type "${step.elementTrigger}", falling back to click`);
194
+ targetElement.addEventListener('click', triggerHandler);
195
+ context.triggerListeners!.push(() => targetElement.removeEventListener('click', triggerHandler));
196
+ } else {
197
+ eventType.forEach(eventName => {
198
+ targetElement.addEventListener(eventName, triggerHandler);
199
+ context.triggerListeners!.push(() => targetElement.removeEventListener(eventName, triggerHandler));
200
+ });
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Map trigger string to DOM event types
206
+ */
207
+ private mapTriggerToEventType(trigger: string): string[] | null {
208
+ switch (trigger.toLowerCase()) {
209
+ case 'click':
210
+ case 'on click':
211
+ return ['click'];
212
+ case 'hover':
213
+ case 'on hover':
214
+ return ['mouseenter'];
215
+ case 'focus':
216
+ case 'on focus':
217
+ return ['focus'];
218
+ case 'blur':
219
+ case 'on blur':
220
+ return ['blur'];
221
+ case 'change':
222
+ case 'on change':
223
+ return ['change'];
224
+ case 'input':
225
+ case 'on input':
226
+ return ['input'];
227
+ default:
228
+ return null;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Find target element using CSS selector or XPath
234
+ */
235
+ private findTargetElement(selector: string): HTMLElement | null {
236
+ try {
237
+ return resolveSelector(selector) as HTMLElement;
238
+ } catch (error) {
239
+ console.error(`[DAP] Error resolving selector "${selector}":`, error);
240
+ return null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Navigate to the next step
246
+ */
247
+ public async goToNextStep(): Promise<void> {
248
+ if (this.state.currentStepIndex < this.steps.length - 1) {
249
+ const currentContext = this.state.stepContexts.get(this.state.currentStepIndex);
250
+ if (currentContext) {
251
+ currentContext.state = "completed";
252
+ this.cleanupStepListeners(currentContext);
253
+ }
254
+
255
+ this.state.currentStepIndex++;
256
+ await this.executeStep(this.state.currentStepIndex);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Navigate to the previous step
262
+ */
263
+ public async goToPreviousStep(): Promise<void> {
264
+ if (this.state.currentStepIndex > 0) {
265
+ const currentContext = this.state.stepContexts.get(this.state.currentStepIndex);
266
+ if (currentContext) {
267
+ currentContext.state = "idle";
268
+ this.cleanupStepListeners(currentContext);
269
+ }
270
+
271
+ this.state.currentStepIndex--;
272
+ await this.executeStep(this.state.currentStepIndex);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Clean up event listeners for a step
278
+ */
279
+ private cleanupStepListeners(context: StepExecutionContext): void {
280
+ if (context.triggerListeners) {
281
+ context.triggerListeners.forEach(cleanup => cleanup());
282
+ context.triggerListeners = [];
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Clean up all resources
288
+ */
289
+ public cleanup(): void {
290
+ console.debug(`[DAP] Cleaning up step executor for flow ${this.flowId}`);
291
+
292
+ this.state.stepContexts.forEach(context => {
293
+ this.cleanupStepListeners(context);
294
+ });
295
+
296
+ this.state.stepContexts.clear();
297
+ this.state.pageLoadTriggersExecuted.clear();
298
+ }
299
+
300
+ /**
301
+ * Get current step context
302
+ */
303
+ public getCurrentStepContext(): StepExecutionContext | undefined {
304
+ return this.state.stepContexts.get(this.state.currentStepIndex);
305
+ }
306
+
307
+ /**
308
+ * Get step context by index
309
+ */
310
+ public getStepContext(index: number): StepExecutionContext | undefined {
311
+ return this.state.stepContexts.get(index);
312
+ }
313
+
314
+ /**
315
+ * Check if step has navigation controls (no trigger defined)
316
+ */
317
+ public stepHasNavigationControls(stepIndex: number): boolean {
318
+ const context = this.state.stepContexts.get(stepIndex);
319
+ return context ? !context.hasElementTrigger : true; // Default to true for safety
320
+ }
321
+
322
+ /**
323
+ * Placeholder for rendering step with navigation - to be implemented by modalSequence
324
+ */
325
+ private async renderStepWithNavigation(
326
+ stepIndex: number,
327
+ step: ModalSequenceStep,
328
+ context: StepExecutionContext
329
+ ): Promise<void> {
330
+ // This will be called by the modalSequence renderer
331
+ console.debug(`[DAP] Would render step ${context.stepId} with navigation controls`);
332
+ }
333
+
334
+ /**
335
+ * Placeholder for rendering step without navigation - to be implemented by modalSequence
336
+ */
337
+ private async renderStepWithoutNavigation(
338
+ stepIndex: number,
339
+ step: ModalSequenceStep,
340
+ context: StepExecutionContext
341
+ ): Promise<void> {
342
+ // This will be called by the modalSequence renderer
343
+ console.debug(`[DAP] Would render step ${context.stepId} without navigation controls`);
344
+ }
345
+ }
@@ -0,0 +1,149 @@
1
+ // src/utils/triggerNormalizer.ts
2
+ // Trigger normalization utilities for DAP SDK
3
+
4
+ import { resolveSelector } from './selectors';
5
+
6
+ /**
7
+ * Normalize backend trigger strings to DOM event types
8
+ */
9
+ export function normalizeTrigger(trigger: string | undefined | null): {
10
+ eventType: string;
11
+ isSynthetic: boolean;
12
+ } {
13
+ if (!trigger) {
14
+ console.warn('[DAP] No trigger specified, defaulting to click');
15
+ return { eventType: 'click', isSynthetic: false };
16
+ }
17
+
18
+ const normalizedTrigger = trigger.toLowerCase().trim();
19
+
20
+ console.debug(`[DAP] Normalizing trigger: "${trigger}" → processing "${normalizedTrigger}"`);
21
+
22
+ // Handle synthetic triggers (SDK controlled)
23
+ if (normalizedTrigger === 'on page load' || normalizedTrigger === 'page load' || normalizedTrigger === 'pageload') {
24
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → synthetic page load`);
25
+ return { eventType: 'pageload', isSynthetic: true };
26
+ }
27
+
28
+ // Handle hover triggers
29
+ if (normalizedTrigger === 'on hover' || normalizedTrigger === 'hover' || normalizedTrigger === 'mouseover') {
30
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "mouseenter"`);
31
+ return { eventType: 'mouseenter', isSynthetic: false };
32
+ }
33
+
34
+ // Handle click triggers
35
+ if (normalizedTrigger === 'on click' || normalizedTrigger === 'click') {
36
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "click"`);
37
+ return { eventType: 'click', isSynthetic: false };
38
+ }
39
+
40
+ // Handle focus triggers
41
+ if (normalizedTrigger === 'on focus' || normalizedTrigger === 'focus') {
42
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "focus"`);
43
+ return { eventType: 'focus', isSynthetic: false };
44
+ }
45
+
46
+ // Handle blur triggers
47
+ if (normalizedTrigger === 'on blur' || normalizedTrigger === 'blur') {
48
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "blur"`);
49
+ return { eventType: 'blur', isSynthetic: false };
50
+ }
51
+
52
+ // Handle input/typing triggers
53
+ if (normalizedTrigger === 'on input' || normalizedTrigger === 'input' || normalizedTrigger === 'typing') {
54
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "input"`);
55
+ return { eventType: 'input', isSynthetic: false };
56
+ }
57
+
58
+ // Handle change triggers
59
+ if (normalizedTrigger === 'on change' || normalizedTrigger === 'change') {
60
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "change"`);
61
+ return { eventType: 'change', isSynthetic: false };
62
+ }
63
+
64
+ // Handle keydown triggers
65
+ if (normalizedTrigger === 'on keydown' || normalizedTrigger === 'keydown') {
66
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "keydown"`);
67
+ return { eventType: 'keydown', isSynthetic: false };
68
+ }
69
+
70
+ // Handle keyup triggers
71
+ if (normalizedTrigger === 'on keyup' || normalizedTrigger === 'keyup') {
72
+ console.debug(`[DAP] Trigger normalized: "${trigger}" → "keyup"`);
73
+ return { eventType: 'keyup', isSynthetic: false };
74
+ }
75
+
76
+ // Default fallback
77
+ console.warn(`[DAP] Unknown trigger type: "${trigger}", defaulting to click`);
78
+ return { eventType: 'click', isSynthetic: false };
79
+ }
80
+
81
+ /**
82
+ * Wait for an element to exist in the DOM with timeout and retry
83
+ */
84
+ export function waitForElement(
85
+ selector: string,
86
+ options: {
87
+ timeout?: number;
88
+ interval?: number;
89
+ maxRetries?: number;
90
+ } = {}
91
+ ): Promise<Element> {
92
+ const {
93
+ timeout = 10000, // 10 seconds default
94
+ interval = 100, // Check every 100ms
95
+ maxRetries = timeout / interval
96
+ } = options;
97
+
98
+ return new Promise((resolve, reject) => {
99
+ let attempts = 0;
100
+
101
+ const checkElement = () => {
102
+ attempts++;
103
+ console.debug(`[DAP] Attempting to find element (${attempts}/${maxRetries}): ${selector}`);
104
+
105
+ try {
106
+ // Use resolveSelector utility which handles both CSS and XPath
107
+ const element = resolveSelector(selector);
108
+
109
+ if (element) {
110
+ console.debug(`[DAP] Element found after ${attempts} attempts: ${selector}`);
111
+ resolve(element);
112
+ return;
113
+ }
114
+ } catch (error) {
115
+ console.debug(`[DAP] Error finding element: ${error}`);
116
+ }
117
+
118
+ if (attempts >= maxRetries) {
119
+ console.warn(`[DAP] Element not found after ${attempts} attempts (${timeout}ms): ${selector}`);
120
+ reject(new Error(`Element not found: ${selector}`));
121
+ return;
122
+ }
123
+
124
+ setTimeout(checkElement, interval);
125
+ };
126
+
127
+ // Start checking immediately
128
+ checkElement();
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Evaluate XPath expression
134
+ */
135
+ function evaluateXPath(xpath: string): Element | null {
136
+ try {
137
+ const result = document.evaluate(
138
+ xpath,
139
+ document,
140
+ null,
141
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
142
+ null
143
+ );
144
+ return result.singleNodeValue as Element | null;
145
+ } catch (error) {
146
+ console.debug(`[DAP] XPath evaluation failed: ${error}`);
147
+ return null;
148
+ }
149
+ }