@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,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
|
+
}
|