@qontinui/ui-bridge 0.1.1
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/dist/control/index.d.mts +134 -0
- package/dist/control/index.d.ts +134 -0
- package/dist/control/index.js +924 -0
- package/dist/control/index.js.map +1 -0
- package/dist/control/index.mjs +919 -0
- package/dist/control/index.mjs.map +1 -0
- package/dist/core/index.d.mts +52 -0
- package/dist/core/index.d.ts +52 -0
- package/dist/core/index.js +1424 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +1409 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/debug/index.d.mts +93 -0
- package/dist/debug/index.d.ts +93 -0
- package/dist/debug/index.js +673 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/debug/index.mjs +664 -0
- package/dist/debug/index.mjs.map +1 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +4719 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4665 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metrics-BCG7z7Aq.d.mts +147 -0
- package/dist/metrics-QCnK0EFw.d.ts +147 -0
- package/dist/react/index.d.mts +786 -0
- package/dist/react/index.d.ts +786 -0
- package/dist/react/index.js +4312 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +4290 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/registry-CT6BVVKr.d.mts +253 -0
- package/dist/registry-D4mQ01B3.d.ts +253 -0
- package/dist/render-log/index.d.mts +340 -0
- package/dist/render-log/index.d.ts +340 -0
- package/dist/render-log/index.js +702 -0
- package/dist/render-log/index.js.map +1 -0
- package/dist/render-log/index.mjs +695 -0
- package/dist/render-log/index.mjs.map +1 -0
- package/dist/types-BDkXy5si.d.ts +354 -0
- package/dist/types-BpvpStn3.d.mts +802 -0
- package/dist/types-BpvpStn3.d.ts +802 -0
- package/dist/types-DdJD9yw5.d.mts +354 -0
- package/dist/websocket-client-B2LC9CYc.d.mts +124 -0
- package/dist/websocket-client-DupH0X7B.d.ts +124 -0
- package/package.json +83 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
// src/core/element-identifier.ts
|
|
2
|
+
function findElementByIdentifier(identifier, root = document) {
|
|
3
|
+
if (typeof identifier === "string") {
|
|
4
|
+
const byUiId = root.querySelector(`[data-ui-id="${identifier}"]`);
|
|
5
|
+
if (byUiId) return byUiId;
|
|
6
|
+
const byTestId = root.querySelector(`[data-testid="${identifier}"]`);
|
|
7
|
+
if (byTestId) return byTestId;
|
|
8
|
+
const byAwasId = root.querySelector(`[data-awas-element="${identifier}"]`);
|
|
9
|
+
if (byAwasId) return byAwasId;
|
|
10
|
+
const byId = root.querySelector(`#${CSS.escape(identifier)}`);
|
|
11
|
+
if (byId) return byId;
|
|
12
|
+
try {
|
|
13
|
+
const bySelector = root.querySelector(identifier);
|
|
14
|
+
if (bySelector) return bySelector;
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const result = document.evaluate(
|
|
19
|
+
identifier,
|
|
20
|
+
root,
|
|
21
|
+
null,
|
|
22
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
23
|
+
null
|
|
24
|
+
);
|
|
25
|
+
if (result.singleNodeValue instanceof HTMLElement) {
|
|
26
|
+
return result.singleNodeValue;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (identifier.uiId) {
|
|
33
|
+
const el = root.querySelector(`[data-ui-id="${identifier.uiId}"]`);
|
|
34
|
+
if (el) return el;
|
|
35
|
+
}
|
|
36
|
+
if (identifier.testId) {
|
|
37
|
+
const el = root.querySelector(`[data-testid="${identifier.testId}"]`);
|
|
38
|
+
if (el) return el;
|
|
39
|
+
}
|
|
40
|
+
if (identifier.awasId) {
|
|
41
|
+
const el = root.querySelector(`[data-awas-element="${identifier.awasId}"]`);
|
|
42
|
+
if (el) return el;
|
|
43
|
+
}
|
|
44
|
+
if (identifier.htmlId) {
|
|
45
|
+
const el = root.querySelector(`#${CSS.escape(identifier.htmlId)}`);
|
|
46
|
+
if (el) return el;
|
|
47
|
+
}
|
|
48
|
+
if (identifier.selector) {
|
|
49
|
+
try {
|
|
50
|
+
const el = root.querySelector(identifier.selector);
|
|
51
|
+
if (el) return el;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (identifier.xpath) {
|
|
56
|
+
try {
|
|
57
|
+
const result = document.evaluate(
|
|
58
|
+
identifier.xpath,
|
|
59
|
+
root,
|
|
60
|
+
null,
|
|
61
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
62
|
+
null
|
|
63
|
+
);
|
|
64
|
+
if (result.singleNodeValue instanceof HTMLElement) {
|
|
65
|
+
return result.singleNodeValue;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/control/action-executor.ts
|
|
74
|
+
var DEFAULT_WAIT_OPTIONS = {
|
|
75
|
+
visible: true,
|
|
76
|
+
enabled: true,
|
|
77
|
+
focused: false,
|
|
78
|
+
state: {},
|
|
79
|
+
timeout: 1e4,
|
|
80
|
+
interval: 100
|
|
81
|
+
};
|
|
82
|
+
function getElementState(element) {
|
|
83
|
+
const rect = element.getBoundingClientRect();
|
|
84
|
+
const style = window.getComputedStyle(element);
|
|
85
|
+
const state = {
|
|
86
|
+
visible: isVisible(element, rect, style),
|
|
87
|
+
enabled: !isDisabled(element),
|
|
88
|
+
focused: document.activeElement === element,
|
|
89
|
+
rect: {
|
|
90
|
+
x: rect.x,
|
|
91
|
+
y: rect.y,
|
|
92
|
+
width: rect.width,
|
|
93
|
+
height: rect.height,
|
|
94
|
+
top: rect.top,
|
|
95
|
+
right: rect.right,
|
|
96
|
+
bottom: rect.bottom,
|
|
97
|
+
left: rect.left
|
|
98
|
+
},
|
|
99
|
+
computedStyles: {
|
|
100
|
+
display: style.display,
|
|
101
|
+
visibility: style.visibility,
|
|
102
|
+
opacity: style.opacity,
|
|
103
|
+
pointerEvents: style.pointerEvents
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (element instanceof HTMLInputElement) {
|
|
107
|
+
state.value = element.value;
|
|
108
|
+
if (element.type === "checkbox" || element.type === "radio") {
|
|
109
|
+
state.checked = element.checked;
|
|
110
|
+
}
|
|
111
|
+
} else if (element instanceof HTMLTextAreaElement) {
|
|
112
|
+
state.value = element.value;
|
|
113
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
114
|
+
state.value = element.value;
|
|
115
|
+
state.selectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value);
|
|
116
|
+
}
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
function isVisible(element, rect, style) {
|
|
120
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
121
|
+
if (style.display === "none") return false;
|
|
122
|
+
if (style.visibility === "hidden") return false;
|
|
123
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
124
|
+
return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
|
|
125
|
+
}
|
|
126
|
+
function isDisabled(element) {
|
|
127
|
+
if ("disabled" in element && element.disabled) return true;
|
|
128
|
+
if (element.getAttribute("aria-disabled") === "true") return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
function sleep(ms) {
|
|
132
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
133
|
+
}
|
|
134
|
+
function createMouseEvent(type, element, options) {
|
|
135
|
+
const rect = element.getBoundingClientRect();
|
|
136
|
+
const x = options?.position?.x ?? rect.width / 2;
|
|
137
|
+
const y = options?.position?.y ?? rect.height / 2;
|
|
138
|
+
return new MouseEvent(type, {
|
|
139
|
+
bubbles: true,
|
|
140
|
+
cancelable: true,
|
|
141
|
+
view: window,
|
|
142
|
+
button: options?.button === "right" ? 2 : options?.button === "middle" ? 1 : 0,
|
|
143
|
+
clientX: rect.left + x,
|
|
144
|
+
clientY: rect.top + y
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
var DefaultActionExecutor = class {
|
|
148
|
+
constructor(registry) {
|
|
149
|
+
this.registry = registry;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Execute an action on an element
|
|
153
|
+
*/
|
|
154
|
+
async executeAction(elementId, request) {
|
|
155
|
+
const startTime = performance.now();
|
|
156
|
+
let waitDurationMs = 0;
|
|
157
|
+
try {
|
|
158
|
+
const registered = this.registry.getElement(elementId);
|
|
159
|
+
let element = registered?.element ?? null;
|
|
160
|
+
if (!element) {
|
|
161
|
+
element = findElementByIdentifier(elementId);
|
|
162
|
+
}
|
|
163
|
+
if (!element) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: `Element not found: ${elementId}`,
|
|
167
|
+
durationMs: performance.now() - startTime,
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
requestId: request.requestId
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (request.waitOptions) {
|
|
173
|
+
const waitResult = await this.waitForElement(element, request.waitOptions);
|
|
174
|
+
waitDurationMs = waitResult.waitedMs;
|
|
175
|
+
if (!waitResult.met) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: waitResult.error || "Wait condition not met",
|
|
179
|
+
durationMs: performance.now() - startTime,
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
requestId: request.requestId,
|
|
182
|
+
waitDurationMs
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const result = await this.performAction(element, request.action, request.params);
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
elementState: getElementState(element),
|
|
190
|
+
result,
|
|
191
|
+
durationMs: performance.now() - startTime,
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
requestId: request.requestId,
|
|
194
|
+
waitDurationMs
|
|
195
|
+
};
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
201
|
+
durationMs: performance.now() - startTime,
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
requestId: request.requestId,
|
|
204
|
+
waitDurationMs
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Execute an action on a component
|
|
210
|
+
*/
|
|
211
|
+
async executeComponentAction(componentId, request) {
|
|
212
|
+
const startTime = performance.now();
|
|
213
|
+
try {
|
|
214
|
+
const component = this.registry.getComponent(componentId);
|
|
215
|
+
if (!component) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
error: `Component not found: ${componentId}`,
|
|
219
|
+
durationMs: performance.now() - startTime,
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
requestId: request.requestId
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const action = component.actions.find((a) => a.id === request.action);
|
|
225
|
+
if (!action) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error: `Action not found: ${request.action}`,
|
|
229
|
+
durationMs: performance.now() - startTime,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
requestId: request.requestId
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const result = await action.handler(request.params);
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
result,
|
|
238
|
+
durationMs: performance.now() - startTime,
|
|
239
|
+
timestamp: Date.now(),
|
|
240
|
+
requestId: request.requestId
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
error: error instanceof Error ? error.message : String(error),
|
|
246
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
247
|
+
durationMs: performance.now() - startTime,
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
requestId: request.requestId
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Wait for a condition on an element
|
|
255
|
+
*/
|
|
256
|
+
async waitFor(elementId, options) {
|
|
257
|
+
const registered = this.registry.getElement(elementId);
|
|
258
|
+
let element = registered?.element ?? null;
|
|
259
|
+
if (!element) {
|
|
260
|
+
element = findElementByIdentifier(elementId);
|
|
261
|
+
}
|
|
262
|
+
if (!element) {
|
|
263
|
+
return {
|
|
264
|
+
met: false,
|
|
265
|
+
waitedMs: 0,
|
|
266
|
+
error: `Element not found: ${elementId}`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return this.waitForElement(element, options);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Find controllable elements
|
|
273
|
+
*/
|
|
274
|
+
async find(options) {
|
|
275
|
+
const startTime = performance.now();
|
|
276
|
+
const elements = [];
|
|
277
|
+
let root = document.body;
|
|
278
|
+
if (options?.root) {
|
|
279
|
+
const rootEl = document.querySelector(options.root);
|
|
280
|
+
if (rootEl) root = rootEl;
|
|
281
|
+
}
|
|
282
|
+
const interactiveSelectors = [
|
|
283
|
+
"a[href]",
|
|
284
|
+
"button",
|
|
285
|
+
"input",
|
|
286
|
+
"select",
|
|
287
|
+
"textarea",
|
|
288
|
+
"[onclick]",
|
|
289
|
+
'[role="button"]',
|
|
290
|
+
'[role="link"]',
|
|
291
|
+
'[role="checkbox"]',
|
|
292
|
+
'[role="radio"]',
|
|
293
|
+
'[role="menuitem"]',
|
|
294
|
+
'[role="tab"]',
|
|
295
|
+
'[role="switch"]',
|
|
296
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
297
|
+
'[contenteditable="true"]',
|
|
298
|
+
"[data-ui-id]",
|
|
299
|
+
"[data-testid]"
|
|
300
|
+
];
|
|
301
|
+
const selector = options?.selector || interactiveSelectors.join(", ");
|
|
302
|
+
const foundElements = root.querySelectorAll(selector);
|
|
303
|
+
for (const el of foundElements) {
|
|
304
|
+
if (options?.limit && elements.length >= options.limit) break;
|
|
305
|
+
const state = getElementState(el);
|
|
306
|
+
if (!options?.includeHidden && !state.visible) continue;
|
|
307
|
+
if (options?.types) {
|
|
308
|
+
const type = this.inferElementType(el);
|
|
309
|
+
if (!options.types.includes(type)) continue;
|
|
310
|
+
}
|
|
311
|
+
const registered = this.registry.findByDOMElement(el);
|
|
312
|
+
elements.push({
|
|
313
|
+
id: registered?.id || this.getElementId(el),
|
|
314
|
+
type: registered?.type || this.inferElementType(el),
|
|
315
|
+
label: registered?.label || this.getElementLabel(el),
|
|
316
|
+
tagName: el.tagName.toLowerCase(),
|
|
317
|
+
role: el.getAttribute("role") || void 0,
|
|
318
|
+
accessibleName: this.getAccessibleName(el),
|
|
319
|
+
actions: registered?.actions || this.inferActions(el),
|
|
320
|
+
state,
|
|
321
|
+
registered: !!registered
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
elements,
|
|
326
|
+
total: elements.length,
|
|
327
|
+
durationMs: performance.now() - startTime,
|
|
328
|
+
timestamp: Date.now()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Discover controllable elements
|
|
333
|
+
* @deprecated Use find() instead
|
|
334
|
+
*/
|
|
335
|
+
async discover(options) {
|
|
336
|
+
return this.find(options);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get control snapshot
|
|
340
|
+
*/
|
|
341
|
+
async getSnapshot() {
|
|
342
|
+
const elements = this.registry.getAllElements();
|
|
343
|
+
const components = this.registry.getAllComponents();
|
|
344
|
+
const workflows = this.registry.getAllWorkflows();
|
|
345
|
+
return {
|
|
346
|
+
timestamp: Date.now(),
|
|
347
|
+
elements: elements.map((el) => ({
|
|
348
|
+
id: el.id,
|
|
349
|
+
type: el.type,
|
|
350
|
+
label: el.label,
|
|
351
|
+
actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
|
|
352
|
+
state: el.getState()
|
|
353
|
+
})),
|
|
354
|
+
components: components.map((comp) => ({
|
|
355
|
+
id: comp.id,
|
|
356
|
+
name: comp.name,
|
|
357
|
+
actions: comp.actions.map((a) => a.id)
|
|
358
|
+
})),
|
|
359
|
+
workflows: workflows.map((wf) => ({
|
|
360
|
+
id: wf.id,
|
|
361
|
+
name: wf.name,
|
|
362
|
+
stepCount: wf.steps.length
|
|
363
|
+
})),
|
|
364
|
+
activeRuns: []
|
|
365
|
+
// Workflow engine manages this
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Wait for element conditions
|
|
370
|
+
*/
|
|
371
|
+
async waitForElement(element, options) {
|
|
372
|
+
const opts = { ...DEFAULT_WAIT_OPTIONS, ...options };
|
|
373
|
+
const startTime = performance.now();
|
|
374
|
+
const deadline = startTime + opts.timeout;
|
|
375
|
+
while (Date.now() < deadline) {
|
|
376
|
+
const state = getElementState(element);
|
|
377
|
+
let allMet = true;
|
|
378
|
+
if (opts.visible && !state.visible) allMet = false;
|
|
379
|
+
if (opts.enabled && !state.enabled) allMet = false;
|
|
380
|
+
if (opts.focused && !state.focused) allMet = false;
|
|
381
|
+
if (opts.state) {
|
|
382
|
+
for (const [key, value] of Object.entries(opts.state)) {
|
|
383
|
+
if (state[key] !== value) {
|
|
384
|
+
allMet = false;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (allMet) {
|
|
390
|
+
return {
|
|
391
|
+
met: true,
|
|
392
|
+
waitedMs: performance.now() - startTime,
|
|
393
|
+
state
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
await sleep(opts.interval);
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
met: false,
|
|
400
|
+
waitedMs: performance.now() - startTime,
|
|
401
|
+
state: getElementState(element),
|
|
402
|
+
error: `Timeout waiting for conditions after ${opts.timeout}ms`
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Perform an action on an element
|
|
407
|
+
*/
|
|
408
|
+
async performAction(element, action, params) {
|
|
409
|
+
switch (action) {
|
|
410
|
+
case "click":
|
|
411
|
+
return this.performClick(element, params);
|
|
412
|
+
case "doubleClick":
|
|
413
|
+
return this.performDoubleClick(element, params);
|
|
414
|
+
case "rightClick":
|
|
415
|
+
return this.performRightClick(element, params);
|
|
416
|
+
case "type":
|
|
417
|
+
return this.performType(element, params);
|
|
418
|
+
case "clear":
|
|
419
|
+
return this.performClear(element);
|
|
420
|
+
case "select":
|
|
421
|
+
return this.performSelect(element, params);
|
|
422
|
+
case "focus":
|
|
423
|
+
return this.performFocus(element);
|
|
424
|
+
case "blur":
|
|
425
|
+
return this.performBlur(element);
|
|
426
|
+
case "hover":
|
|
427
|
+
return this.performHover(element);
|
|
428
|
+
case "scroll":
|
|
429
|
+
return this.performScroll(element, params);
|
|
430
|
+
case "check":
|
|
431
|
+
return this.performCheck(element, true);
|
|
432
|
+
case "uncheck":
|
|
433
|
+
return this.performCheck(element, false);
|
|
434
|
+
case "toggle":
|
|
435
|
+
return this.performToggle(element);
|
|
436
|
+
default: {
|
|
437
|
+
const registered = this.registry.findByDOMElement(element);
|
|
438
|
+
if (registered?.customActions?.[action]) {
|
|
439
|
+
return registered.customActions[action].handler(params);
|
|
440
|
+
}
|
|
441
|
+
throw new Error(`Unknown action: ${action}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
performClick(element, options) {
|
|
446
|
+
element.dispatchEvent(createMouseEvent("mousedown", element, options));
|
|
447
|
+
element.dispatchEvent(createMouseEvent("mouseup", element, options));
|
|
448
|
+
element.dispatchEvent(createMouseEvent("click", element, options));
|
|
449
|
+
}
|
|
450
|
+
performDoubleClick(element, options) {
|
|
451
|
+
this.performClick(element, options);
|
|
452
|
+
this.performClick(element, options);
|
|
453
|
+
element.dispatchEvent(createMouseEvent("dblclick", element, options));
|
|
454
|
+
}
|
|
455
|
+
performRightClick(element, options) {
|
|
456
|
+
const opts = { ...options, button: "right" };
|
|
457
|
+
element.dispatchEvent(createMouseEvent("mousedown", element, opts));
|
|
458
|
+
element.dispatchEvent(createMouseEvent("mouseup", element, opts));
|
|
459
|
+
element.dispatchEvent(createMouseEvent("contextmenu", element, opts));
|
|
460
|
+
}
|
|
461
|
+
async performType(element, options) {
|
|
462
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
463
|
+
throw new Error("Type action requires an input or textarea element");
|
|
464
|
+
}
|
|
465
|
+
element.focus();
|
|
466
|
+
if (options?.clear) {
|
|
467
|
+
element.value = "";
|
|
468
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
469
|
+
}
|
|
470
|
+
const text = options?.text || "";
|
|
471
|
+
const delay = options?.delay || 0;
|
|
472
|
+
for (const char of text) {
|
|
473
|
+
element.value += char;
|
|
474
|
+
if (options?.triggerEvents !== false) {
|
|
475
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
476
|
+
}
|
|
477
|
+
if (delay > 0) {
|
|
478
|
+
await sleep(delay);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (options?.triggerEvents !== false) {
|
|
482
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
performClear(element) {
|
|
486
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
487
|
+
element.value = "";
|
|
488
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
489
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
performSelect(element, options) {
|
|
493
|
+
if (!(element instanceof HTMLSelectElement)) {
|
|
494
|
+
throw new Error("Select action requires a select element");
|
|
495
|
+
}
|
|
496
|
+
const values = Array.isArray(options?.value) ? options.value : [options?.value];
|
|
497
|
+
if (!options?.additive) {
|
|
498
|
+
for (const option of element.options) {
|
|
499
|
+
option.selected = false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (const option of element.options) {
|
|
503
|
+
const matchValue = options?.byLabel ? option.text : option.value;
|
|
504
|
+
if (values.includes(matchValue)) {
|
|
505
|
+
option.selected = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
509
|
+
}
|
|
510
|
+
performFocus(element) {
|
|
511
|
+
element.focus();
|
|
512
|
+
element.dispatchEvent(new FocusEvent("focus", { bubbles: true }));
|
|
513
|
+
}
|
|
514
|
+
performBlur(element) {
|
|
515
|
+
element.blur();
|
|
516
|
+
element.dispatchEvent(new FocusEvent("blur", { bubbles: true }));
|
|
517
|
+
}
|
|
518
|
+
performHover(element) {
|
|
519
|
+
element.dispatchEvent(createMouseEvent("mouseenter", element));
|
|
520
|
+
element.dispatchEvent(createMouseEvent("mouseover", element));
|
|
521
|
+
}
|
|
522
|
+
performScroll(element, options) {
|
|
523
|
+
if (options?.toElement) {
|
|
524
|
+
const target = document.querySelector(options.toElement);
|
|
525
|
+
if (target) {
|
|
526
|
+
target.scrollIntoView({ behavior: options.smooth ? "smooth" : "auto" });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (options?.position) {
|
|
531
|
+
element.scrollTo({
|
|
532
|
+
left: options.position.x,
|
|
533
|
+
top: options.position.y,
|
|
534
|
+
behavior: options.smooth ? "smooth" : "auto"
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const amount = options?.amount || 100;
|
|
539
|
+
const direction = options?.direction || "down";
|
|
540
|
+
switch (direction) {
|
|
541
|
+
case "up":
|
|
542
|
+
element.scrollBy({ top: -amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
543
|
+
break;
|
|
544
|
+
case "down":
|
|
545
|
+
element.scrollBy({ top: amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
546
|
+
break;
|
|
547
|
+
case "left":
|
|
548
|
+
element.scrollBy({ left: -amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
549
|
+
break;
|
|
550
|
+
case "right":
|
|
551
|
+
element.scrollBy({ left: amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
performCheck(element, checked) {
|
|
556
|
+
if (element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio")) {
|
|
557
|
+
if (element.checked !== checked) {
|
|
558
|
+
element.checked = checked;
|
|
559
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
performToggle(element) {
|
|
564
|
+
if (element instanceof HTMLInputElement && element.type === "checkbox") {
|
|
565
|
+
element.checked = !element.checked;
|
|
566
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
getElementId(element) {
|
|
570
|
+
return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
571
|
+
}
|
|
572
|
+
getElementLabel(element) {
|
|
573
|
+
return element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent?.trim().substring(0, 50) || void 0;
|
|
574
|
+
}
|
|
575
|
+
getAccessibleName(element) {
|
|
576
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
577
|
+
if (ariaLabel) return ariaLabel;
|
|
578
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
579
|
+
if (labelledBy) {
|
|
580
|
+
const labels = labelledBy.split(" ").map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
|
|
581
|
+
if (labels.length > 0) return labels.join(" ");
|
|
582
|
+
}
|
|
583
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
584
|
+
if (element.id) {
|
|
585
|
+
const label = document.querySelector(`label[for="${element.id}"]`);
|
|
586
|
+
if (label) return label.textContent?.trim();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return element.getAttribute("title") || element.textContent?.trim().substring(0, 50) || void 0;
|
|
590
|
+
}
|
|
591
|
+
inferElementType(element) {
|
|
592
|
+
const tagName = element.tagName.toLowerCase();
|
|
593
|
+
const role = element.getAttribute("role");
|
|
594
|
+
if (role) {
|
|
595
|
+
switch (role) {
|
|
596
|
+
case "button":
|
|
597
|
+
return "button";
|
|
598
|
+
case "textbox":
|
|
599
|
+
return "input";
|
|
600
|
+
case "checkbox":
|
|
601
|
+
return "checkbox";
|
|
602
|
+
case "radio":
|
|
603
|
+
return "radio";
|
|
604
|
+
case "link":
|
|
605
|
+
return "link";
|
|
606
|
+
case "listbox":
|
|
607
|
+
case "combobox":
|
|
608
|
+
return "select";
|
|
609
|
+
case "menu":
|
|
610
|
+
return "menu";
|
|
611
|
+
case "menuitem":
|
|
612
|
+
return "menuitem";
|
|
613
|
+
case "tab":
|
|
614
|
+
return "tab";
|
|
615
|
+
case "dialog":
|
|
616
|
+
return "dialog";
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
switch (tagName) {
|
|
620
|
+
case "button":
|
|
621
|
+
return "button";
|
|
622
|
+
case "input": {
|
|
623
|
+
const type = element.type;
|
|
624
|
+
if (type === "checkbox") return "checkbox";
|
|
625
|
+
if (type === "radio") return "radio";
|
|
626
|
+
if (type === "submit" || type === "button") return "button";
|
|
627
|
+
return "input";
|
|
628
|
+
}
|
|
629
|
+
case "textarea":
|
|
630
|
+
return "textarea";
|
|
631
|
+
case "select":
|
|
632
|
+
return "select";
|
|
633
|
+
case "a":
|
|
634
|
+
return "link";
|
|
635
|
+
case "form":
|
|
636
|
+
return "form";
|
|
637
|
+
default:
|
|
638
|
+
return "custom";
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
inferActions(element) {
|
|
642
|
+
const type = this.inferElementType(element);
|
|
643
|
+
const baseActions = ["focus", "blur", "hover"];
|
|
644
|
+
switch (type) {
|
|
645
|
+
case "button":
|
|
646
|
+
return [...baseActions, "click", "doubleClick", "rightClick"];
|
|
647
|
+
case "input":
|
|
648
|
+
return [...baseActions, "click", "type", "clear"];
|
|
649
|
+
case "textarea":
|
|
650
|
+
return [...baseActions, "click", "type", "clear"];
|
|
651
|
+
case "select":
|
|
652
|
+
return [...baseActions, "click", "select"];
|
|
653
|
+
case "checkbox":
|
|
654
|
+
return [...baseActions, "click", "check", "uncheck", "toggle"];
|
|
655
|
+
case "radio":
|
|
656
|
+
return [...baseActions, "click", "check"];
|
|
657
|
+
case "link":
|
|
658
|
+
return [...baseActions, "click"];
|
|
659
|
+
default:
|
|
660
|
+
return [...baseActions, "click"];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
function createActionExecutor(registry) {
|
|
665
|
+
return new DefaultActionExecutor(registry);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/control/workflow-engine.ts
|
|
669
|
+
function generateRunId() {
|
|
670
|
+
return `run-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
671
|
+
}
|
|
672
|
+
var DefaultWorkflowEngine = class {
|
|
673
|
+
constructor(registry, executor) {
|
|
674
|
+
this.registry = registry;
|
|
675
|
+
this.executor = executor;
|
|
676
|
+
this.activeRuns = /* @__PURE__ */ new Map();
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Run a workflow
|
|
680
|
+
*/
|
|
681
|
+
async run(workflowId, request) {
|
|
682
|
+
const workflow = this.registry.getWorkflow(workflowId);
|
|
683
|
+
if (!workflow) {
|
|
684
|
+
return {
|
|
685
|
+
workflowId,
|
|
686
|
+
runId: generateRunId(),
|
|
687
|
+
status: "failed",
|
|
688
|
+
steps: [],
|
|
689
|
+
totalSteps: 0,
|
|
690
|
+
success: false,
|
|
691
|
+
error: `Workflow not found: ${workflowId}`,
|
|
692
|
+
startedAt: Date.now(),
|
|
693
|
+
completedAt: Date.now(),
|
|
694
|
+
durationMs: 0
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const runId = generateRunId();
|
|
698
|
+
const state = {
|
|
699
|
+
workflowId,
|
|
700
|
+
runId,
|
|
701
|
+
workflow,
|
|
702
|
+
request,
|
|
703
|
+
status: "running",
|
|
704
|
+
steps: [],
|
|
705
|
+
currentStep: 0,
|
|
706
|
+
startedAt: Date.now()
|
|
707
|
+
};
|
|
708
|
+
this.activeRuns.set(runId, state);
|
|
709
|
+
try {
|
|
710
|
+
await this.executeWorkflow(state);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
state.status = "failed";
|
|
713
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
714
|
+
}
|
|
715
|
+
state.completedAt = Date.now();
|
|
716
|
+
state.durationMs = state.completedAt - state.startedAt;
|
|
717
|
+
state.success = state.status === "completed" && state.steps.every((s) => s.success);
|
|
718
|
+
setTimeout(() => {
|
|
719
|
+
this.activeRuns.delete(runId);
|
|
720
|
+
}, 6e4);
|
|
721
|
+
return this.buildResponse(state);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Get workflow run status
|
|
725
|
+
*/
|
|
726
|
+
async getRunStatus(runId) {
|
|
727
|
+
const state = this.activeRuns.get(runId);
|
|
728
|
+
if (!state) return null;
|
|
729
|
+
return this.buildResponse(state);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Cancel a running workflow
|
|
733
|
+
*/
|
|
734
|
+
async cancel(runId) {
|
|
735
|
+
const state = this.activeRuns.get(runId);
|
|
736
|
+
if (!state || state.status !== "running") return false;
|
|
737
|
+
state.status = "cancelled";
|
|
738
|
+
state.completedAt = Date.now();
|
|
739
|
+
state.durationMs = state.completedAt - state.startedAt;
|
|
740
|
+
state.error = "Workflow cancelled by user";
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* List active runs
|
|
745
|
+
*/
|
|
746
|
+
async listActiveRuns() {
|
|
747
|
+
return Array.from(this.activeRuns.values()).filter((state) => state.status === "running").map((state) => this.buildResponse(state));
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Execute a workflow
|
|
751
|
+
*/
|
|
752
|
+
async executeWorkflow(state) {
|
|
753
|
+
const { workflow, request } = state;
|
|
754
|
+
const params = { ...workflow.defaultParams, ...request?.params };
|
|
755
|
+
let startIndex = 0;
|
|
756
|
+
if (request?.startStep) {
|
|
757
|
+
const idx = workflow.steps.findIndex((s) => s.id === request.startStep);
|
|
758
|
+
if (idx >= 0) startIndex = idx;
|
|
759
|
+
}
|
|
760
|
+
let stopIndex = workflow.steps.length;
|
|
761
|
+
if (request?.stopStep) {
|
|
762
|
+
const idx = workflow.steps.findIndex((s) => s.id === request.stopStep);
|
|
763
|
+
if (idx >= 0) stopIndex = idx + 1;
|
|
764
|
+
}
|
|
765
|
+
for (let i = startIndex; i < stopIndex; i++) {
|
|
766
|
+
if (state.status === "cancelled") break;
|
|
767
|
+
state.currentStep = i;
|
|
768
|
+
const step = workflow.steps[i];
|
|
769
|
+
const stepResult = await this.executeStep(step, params, request?.stepTimeout);
|
|
770
|
+
state.steps.push(stepResult);
|
|
771
|
+
if (!stepResult.success) {
|
|
772
|
+
state.status = "failed";
|
|
773
|
+
state.error = stepResult.error;
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
state.status = "completed";
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Execute a single step
|
|
781
|
+
*/
|
|
782
|
+
async executeStep(step, params, timeout) {
|
|
783
|
+
const startTime = performance.now();
|
|
784
|
+
try {
|
|
785
|
+
const timeoutPromise = timeout ? new Promise(
|
|
786
|
+
(_, reject) => setTimeout(() => reject(new Error("Step timeout")), timeout)
|
|
787
|
+
) : null;
|
|
788
|
+
const executePromise = this.executeStepInternal(step, params);
|
|
789
|
+
const result = timeoutPromise ? await Promise.race([executePromise, timeoutPromise]) : await executePromise;
|
|
790
|
+
return {
|
|
791
|
+
stepId: step.id,
|
|
792
|
+
stepType: step.type,
|
|
793
|
+
success: true,
|
|
794
|
+
result,
|
|
795
|
+
durationMs: performance.now() - startTime,
|
|
796
|
+
timestamp: Date.now()
|
|
797
|
+
};
|
|
798
|
+
} catch (error) {
|
|
799
|
+
return {
|
|
800
|
+
stepId: step.id,
|
|
801
|
+
stepType: step.type,
|
|
802
|
+
success: false,
|
|
803
|
+
error: error instanceof Error ? error.message : String(error),
|
|
804
|
+
durationMs: performance.now() - startTime,
|
|
805
|
+
timestamp: Date.now()
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Execute step internal logic
|
|
811
|
+
*/
|
|
812
|
+
async executeStepInternal(step, params) {
|
|
813
|
+
const resolvedParams = this.interpolateParams(step.params || {}, params);
|
|
814
|
+
switch (step.type) {
|
|
815
|
+
case "element-action":
|
|
816
|
+
if (!step.target || !step.action) {
|
|
817
|
+
throw new Error("Element action requires target and action");
|
|
818
|
+
}
|
|
819
|
+
return this.executor.executeAction(step.target, {
|
|
820
|
+
action: step.action,
|
|
821
|
+
params: resolvedParams,
|
|
822
|
+
waitOptions: step.waitOptions
|
|
823
|
+
});
|
|
824
|
+
case "component-action":
|
|
825
|
+
if (!step.target || !step.action) {
|
|
826
|
+
throw new Error("Component action requires target and action");
|
|
827
|
+
}
|
|
828
|
+
return this.executor.executeComponentAction(step.target, {
|
|
829
|
+
action: step.action,
|
|
830
|
+
params: resolvedParams
|
|
831
|
+
});
|
|
832
|
+
case "wait": {
|
|
833
|
+
if (!step.target) {
|
|
834
|
+
throw new Error("Wait step requires target");
|
|
835
|
+
}
|
|
836
|
+
const waitResult = await this.executor.waitFor(step.target, step.waitOptions || {});
|
|
837
|
+
if (!waitResult.met) {
|
|
838
|
+
throw new Error(waitResult.error || "Wait condition not met");
|
|
839
|
+
}
|
|
840
|
+
return waitResult;
|
|
841
|
+
}
|
|
842
|
+
case "assert":
|
|
843
|
+
if (!step.target || !step.expectedState) {
|
|
844
|
+
throw new Error("Assert step requires target and expectedState");
|
|
845
|
+
}
|
|
846
|
+
return this.performAssertion(step.target, step.expectedState);
|
|
847
|
+
case "custom":
|
|
848
|
+
if (!step.handler) {
|
|
849
|
+
throw new Error("Custom step requires handler");
|
|
850
|
+
}
|
|
851
|
+
return step.handler();
|
|
852
|
+
default:
|
|
853
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Perform state assertion
|
|
858
|
+
*/
|
|
859
|
+
async performAssertion(target, expectedState) {
|
|
860
|
+
const snapshot = await this.executor.getSnapshot();
|
|
861
|
+
const element = snapshot.elements.find((e) => e.id === target);
|
|
862
|
+
if (!element) {
|
|
863
|
+
throw new Error(`Element not found for assertion: ${target}`);
|
|
864
|
+
}
|
|
865
|
+
const differences = [];
|
|
866
|
+
for (const [key, expected] of Object.entries(expectedState)) {
|
|
867
|
+
const actual = element.state[key];
|
|
868
|
+
if (actual !== expected) {
|
|
869
|
+
differences.push(`${key}: expected ${expected}, got ${actual}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (differences.length > 0) {
|
|
873
|
+
throw new Error(`Assertion failed:
|
|
874
|
+
${differences.join("\n")}`);
|
|
875
|
+
}
|
|
876
|
+
return { passed: true, differences };
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Interpolate parameters with {{param}} syntax
|
|
880
|
+
*/
|
|
881
|
+
interpolateParams(stepParams, workflowParams) {
|
|
882
|
+
const result = {};
|
|
883
|
+
for (const [key, value] of Object.entries(stepParams)) {
|
|
884
|
+
if (typeof value === "string") {
|
|
885
|
+
result[key] = value.replace(/\{\{(\w+)\}\}/g, (_, name) => {
|
|
886
|
+
return String(workflowParams[name] ?? "");
|
|
887
|
+
});
|
|
888
|
+
} else {
|
|
889
|
+
result[key] = value;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Build response from state
|
|
896
|
+
*/
|
|
897
|
+
buildResponse(state) {
|
|
898
|
+
return {
|
|
899
|
+
workflowId: state.workflowId,
|
|
900
|
+
runId: state.runId,
|
|
901
|
+
status: state.status,
|
|
902
|
+
steps: [...state.steps],
|
|
903
|
+
currentStep: state.currentStep,
|
|
904
|
+
totalSteps: state.workflow.steps.length,
|
|
905
|
+
success: state.success,
|
|
906
|
+
error: state.error,
|
|
907
|
+
startedAt: state.startedAt,
|
|
908
|
+
completedAt: state.completedAt,
|
|
909
|
+
durationMs: state.durationMs
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
function createWorkflowEngine(registry, executor) {
|
|
914
|
+
return new DefaultWorkflowEngine(registry, executor);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export { DefaultActionExecutor, DefaultWorkflowEngine, createActionExecutor, createWorkflowEngine };
|
|
918
|
+
//# sourceMappingURL=index.mjs.map
|
|
919
|
+
//# sourceMappingURL=index.mjs.map
|