@qontinui/ui-bridge 0.3.0 → 0.3.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/ai/index.d.mts +312 -155
- package/dist/ai/index.d.ts +312 -155
- package/dist/ai/index.js +2363 -67
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/index.mjs +2328 -68
- package/dist/ai/index.mjs.map +1 -1
- package/dist/annotations/index.d.mts +218 -0
- package/dist/annotations/index.d.ts +218 -0
- package/dist/annotations/index.js +246 -0
- package/dist/annotations/index.js.map +1 -0
- package/dist/annotations/index.mjs +241 -0
- package/dist/annotations/index.mjs.map +1 -0
- package/dist/assertions-BSR3afVr.d.ts +161 -0
- package/dist/assertions-CTw1hfOx.d.mts +161 -0
- package/dist/babel-plugin/index.js +23 -34
- package/dist/babel-plugin/index.js.map +1 -1
- package/dist/babel-plugin/index.mjs +23 -34
- package/dist/babel-plugin/index.mjs.map +1 -1
- package/dist/browser-capture-Bms60T6f.d.mts +47 -0
- package/dist/browser-capture-CsTU29mb.d.ts +47 -0
- package/dist/control/index.d.mts +26 -7
- package/dist/control/index.d.ts +26 -7
- package/dist/control/index.js +276 -48
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +276 -48
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.d.mts +5 -3
- package/dist/debug/index.d.ts +5 -3
- package/dist/debug/index.js +925 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +924 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.mts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +4720 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4656 -174
- package/dist/index.mjs.map +1 -1
- package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
- package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
- package/dist/native/control/index.js +2 -7
- package/dist/native/control/index.js.map +1 -1
- package/dist/native/control/index.mjs +2 -7
- package/dist/native/control/index.mjs.map +1 -1
- package/dist/native/core/index.js.map +1 -1
- package/dist/native/core/index.mjs.map +1 -1
- package/dist/native/debug/index.js +23 -66
- package/dist/native/debug/index.js.map +1 -1
- package/dist/native/debug/index.mjs +23 -66
- package/dist/native/debug/index.mjs.map +1 -1
- package/dist/native/index.js +89 -131
- package/dist/native/index.js.map +1 -1
- package/dist/native/index.mjs +89 -131
- package/dist/native/index.mjs.map +1 -1
- package/dist/native/react/index.js +28 -52
- package/dist/native/react/index.js.map +1 -1
- package/dist/native/react/index.mjs +28 -52
- package/dist/native/react/index.mjs.map +1 -1
- package/dist/native/server/index.js +38 -13
- package/dist/native/server/index.js.map +1 -1
- package/dist/native/server/index.mjs +38 -13
- package/dist/native/server/index.mjs.map +1 -1
- package/dist/react/index.d.mts +107 -8
- package/dist/react/index.d.ts +107 -8
- package/dist/react/index.js +2194 -84
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +2194 -85
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
- package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
- package/dist/render-log/index.d.mts +1 -1
- package/dist/render-log/index.d.ts +1 -1
- package/dist/server/express.d.mts +5 -4
- package/dist/server/express.d.ts +5 -4
- package/dist/server/express.js +104 -2
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.mjs +104 -2
- package/dist/server/express.mjs.map +1 -1
- package/dist/server/handlers.d.mts +36 -5
- package/dist/server/handlers.d.ts +36 -5
- package/dist/server/handlers.js +3129 -224
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/handlers.mjs +3129 -224
- package/dist/server/handlers.mjs.map +1 -1
- package/dist/server/index.d.mts +7 -5
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.js +3215 -183
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +3215 -183
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/nextjs.d.mts +6 -4
- package/dist/server/nextjs.d.ts +6 -4
- package/dist/server/nextjs.js +106 -3
- package/dist/server/nextjs.js.map +1 -1
- package/dist/server/nextjs.mjs +106 -3
- package/dist/server/nextjs.mjs.map +1 -1
- package/dist/server/standalone.d.mts +6 -5
- package/dist/server/standalone.d.ts +6 -5
- package/dist/server/standalone.js +131 -5
- package/dist/server/standalone.js.map +1 -1
- package/dist/server/standalone.mjs +131 -5
- package/dist/server/standalone.mjs.map +1 -1
- package/dist/specs/index.d.mts +365 -0
- package/dist/specs/index.d.ts +365 -0
- package/dist/specs/index.js +2809 -0
- package/dist/specs/index.js.map +1 -0
- package/dist/specs/index.mjs +2786 -0
- package/dist/specs/index.mjs.map +1 -0
- package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
- package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
- package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
- package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
- package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
- package/dist/types-CebMQj76.d.ts +1275 -0
- package/dist/types-D_ypYl3T.d.mts +1275 -0
- package/dist/types-UBtp7R0u.d.mts +132 -0
- package/dist/types-UBtp7R0u.d.ts +132 -0
- package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
- package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
- package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
- package/package.json +18 -2
- package/dist/types-B5Q0GVo0.d.mts +0 -646
- package/dist/types-DfPqwU-i.d.ts +0 -646
package/dist/react/index.js
CHANGED
|
@@ -552,7 +552,9 @@ function generateDescription(input) {
|
|
|
552
552
|
}
|
|
553
553
|
parts.push(`"${name}"`);
|
|
554
554
|
}
|
|
555
|
-
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
555
|
+
const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
|
|
556
|
+
input.elementType || "element"
|
|
557
|
+
];
|
|
556
558
|
parts.push(typeWords[0]);
|
|
557
559
|
if (input.inputType && input.inputType !== "text") {
|
|
558
560
|
parts.push(`(${input.inputType})`);
|
|
@@ -773,12 +775,38 @@ var UIBridgeRegistry = class {
|
|
|
773
775
|
getState: () => getElementState(element),
|
|
774
776
|
getIdentifier: () => createElementIdentifier(element),
|
|
775
777
|
registeredAt: Date.now(),
|
|
776
|
-
mounted: true
|
|
778
|
+
mounted: true,
|
|
779
|
+
category: options.category ?? "interactive",
|
|
780
|
+
contentMetadata: options.contentMetadata
|
|
777
781
|
};
|
|
778
782
|
this.elements.set(actualId, registered);
|
|
779
783
|
this.emit("element:registered", { id: actualId, type, label: options.label });
|
|
780
784
|
return registered;
|
|
781
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Register a content (non-interactive) element
|
|
788
|
+
*/
|
|
789
|
+
registerContentElement(id, element, options) {
|
|
790
|
+
return this.registerElement(id, element, {
|
|
791
|
+
type: options.contentType,
|
|
792
|
+
label: options.label,
|
|
793
|
+
actions: [],
|
|
794
|
+
category: "content",
|
|
795
|
+
contentMetadata: options.contentMetadata
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Get all content (non-interactive) elements
|
|
800
|
+
*/
|
|
801
|
+
getAllContentElements() {
|
|
802
|
+
return Array.from(this.elements.values()).filter((el) => el.category === "content");
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get all interactive elements
|
|
806
|
+
*/
|
|
807
|
+
getAllInteractiveElements() {
|
|
808
|
+
return Array.from(this.elements.values()).filter((el) => el.category !== "content");
|
|
809
|
+
}
|
|
782
810
|
/**
|
|
783
811
|
* Unregister an element
|
|
784
812
|
*/
|
|
@@ -875,7 +903,9 @@ var UIBridgeRegistry = class {
|
|
|
875
903
|
scores.accessibility = result.similarity;
|
|
876
904
|
if (result.similarity > maxScore) {
|
|
877
905
|
maxScore = result.similarity;
|
|
878
|
-
matchReasons.push(
|
|
906
|
+
matchReasons.push(
|
|
907
|
+
`accessible name similarity: ${(result.similarity * 100).toFixed(0)}%`
|
|
908
|
+
);
|
|
879
909
|
}
|
|
880
910
|
}
|
|
881
911
|
}
|
|
@@ -1471,8 +1501,56 @@ var UIBridgeRegistry = class {
|
|
|
1471
1501
|
identifier: el.getIdentifier(),
|
|
1472
1502
|
state: el.getState(),
|
|
1473
1503
|
actions: el.actions,
|
|
1474
|
-
customActions: el.customActions ? Object.keys(el.customActions) : void 0
|
|
1504
|
+
customActions: el.customActions ? Object.keys(el.customActions) : void 0,
|
|
1505
|
+
category: el.category,
|
|
1506
|
+
contentMetadata: el.contentMetadata
|
|
1507
|
+
})),
|
|
1508
|
+
components: this.getAllComponents().map((comp) => ({
|
|
1509
|
+
id: comp.id,
|
|
1510
|
+
name: comp.name,
|
|
1511
|
+
description: comp.description,
|
|
1512
|
+
actions: comp.actions.map((a) => a.id),
|
|
1513
|
+
elementIds: comp.elementIds
|
|
1475
1514
|
})),
|
|
1515
|
+
workflows: this.getAllWorkflows().map((wf) => ({
|
|
1516
|
+
id: wf.id,
|
|
1517
|
+
name: wf.name,
|
|
1518
|
+
description: wf.description,
|
|
1519
|
+
stepCount: wf.steps.length
|
|
1520
|
+
}))
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Create a snapshot asynchronously, processing elements in batches to avoid
|
|
1525
|
+
* blocking the main thread. This prevents "Page Unresponsive" dialogs when
|
|
1526
|
+
* there are many registered elements (200-500+), since getState() and
|
|
1527
|
+
* getIdentifier() force layout/style recalculation for each element.
|
|
1528
|
+
*/
|
|
1529
|
+
async createSnapshotAsync(batchSize = 50) {
|
|
1530
|
+
const allElements = this.getAllElements();
|
|
1531
|
+
const elementSnapshots = [];
|
|
1532
|
+
for (let i = 0; i < allElements.length; i += batchSize) {
|
|
1533
|
+
const batch = allElements.slice(i, i + batchSize);
|
|
1534
|
+
for (const el of batch) {
|
|
1535
|
+
elementSnapshots.push({
|
|
1536
|
+
id: el.id,
|
|
1537
|
+
type: el.type,
|
|
1538
|
+
label: el.label,
|
|
1539
|
+
identifier: el.getIdentifier(),
|
|
1540
|
+
state: el.getState(),
|
|
1541
|
+
actions: el.actions,
|
|
1542
|
+
customActions: el.customActions ? Object.keys(el.customActions) : void 0,
|
|
1543
|
+
category: el.category,
|
|
1544
|
+
contentMetadata: el.contentMetadata
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (i + batchSize < allElements.length) {
|
|
1548
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
timestamp: Date.now(),
|
|
1553
|
+
elements: elementSnapshots,
|
|
1476
1554
|
components: this.getAllComponents().map((comp) => ({
|
|
1477
1555
|
id: comp.id,
|
|
1478
1556
|
name: comp.name,
|
|
@@ -1997,6 +2075,10 @@ function getElementState2(element) {
|
|
|
1997
2075
|
pointerEvents: style.pointerEvents
|
|
1998
2076
|
}
|
|
1999
2077
|
};
|
|
2078
|
+
const rawText = element.textContent?.trim();
|
|
2079
|
+
if (rawText) {
|
|
2080
|
+
state.textContent = rawText.replace(/\s+/g, " ").slice(0, 500);
|
|
2081
|
+
}
|
|
2000
2082
|
if (element instanceof HTMLInputElement) {
|
|
2001
2083
|
state.value = element.value;
|
|
2002
2084
|
if (element.type === "checkbox" || element.type === "radio") {
|
|
@@ -2038,9 +2120,25 @@ function createMouseEvent(type, element, options) {
|
|
|
2038
2120
|
clientY: rect.top + y
|
|
2039
2121
|
});
|
|
2040
2122
|
}
|
|
2123
|
+
function elementFromPointSafe(x, y) {
|
|
2124
|
+
if (typeof document.elementFromPoint === "function") {
|
|
2125
|
+
return document.elementFromPoint(x, y);
|
|
2126
|
+
}
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
function createMouseEventAt(type, clientX, clientY) {
|
|
2130
|
+
return new MouseEvent(type, {
|
|
2131
|
+
bubbles: true,
|
|
2132
|
+
cancelable: true,
|
|
2133
|
+
button: 0,
|
|
2134
|
+
clientX,
|
|
2135
|
+
clientY
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2041
2138
|
var DefaultActionExecutor = class {
|
|
2042
|
-
constructor(registry) {
|
|
2139
|
+
constructor(registry, consoleCapture) {
|
|
2043
2140
|
this.registry = registry;
|
|
2141
|
+
this.consoleCapture = consoleCapture;
|
|
2044
2142
|
}
|
|
2045
2143
|
/**
|
|
2046
2144
|
* Execute an action on an element
|
|
@@ -2077,11 +2175,19 @@ var DefaultActionExecutor = class {
|
|
|
2077
2175
|
};
|
|
2078
2176
|
}
|
|
2079
2177
|
}
|
|
2178
|
+
const actionStartTime = Date.now();
|
|
2080
2179
|
const result = await this.performAction(element, request.action, request.params);
|
|
2180
|
+
let consoleErrors;
|
|
2181
|
+
if (this.consoleCapture) {
|
|
2182
|
+
await sleep(50);
|
|
2183
|
+
const errors = this.consoleCapture.getConsoleSince(actionStartTime);
|
|
2184
|
+
if (errors.length > 0) consoleErrors = errors;
|
|
2185
|
+
}
|
|
2081
2186
|
return {
|
|
2082
2187
|
success: true,
|
|
2083
2188
|
elementState: getElementState2(element),
|
|
2084
2189
|
result,
|
|
2190
|
+
consoleErrors,
|
|
2085
2191
|
durationMs: performance.now() - startTime,
|
|
2086
2192
|
timestamp: Date.now(),
|
|
2087
2193
|
requestId: request.requestId,
|
|
@@ -2173,47 +2279,76 @@ var DefaultActionExecutor = class {
|
|
|
2173
2279
|
const rootEl = document.querySelector(options.root);
|
|
2174
2280
|
if (rootEl) root = rootEl;
|
|
2175
2281
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
const
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
if (
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2282
|
+
if (!options?.contentOnly) {
|
|
2283
|
+
const interactiveSelectors = [
|
|
2284
|
+
"a[href]",
|
|
2285
|
+
"button",
|
|
2286
|
+
"input",
|
|
2287
|
+
"select",
|
|
2288
|
+
"textarea",
|
|
2289
|
+
"[onclick]",
|
|
2290
|
+
'[role="button"]',
|
|
2291
|
+
'[role="link"]',
|
|
2292
|
+
'[role="checkbox"]',
|
|
2293
|
+
'[role="radio"]',
|
|
2294
|
+
'[role="menuitem"]',
|
|
2295
|
+
'[role="tab"]',
|
|
2296
|
+
'[role="switch"]',
|
|
2297
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
2298
|
+
'[contenteditable="true"]',
|
|
2299
|
+
"[data-ui-element]",
|
|
2300
|
+
"[data-ui-id]",
|
|
2301
|
+
"[data-testid]"
|
|
2302
|
+
];
|
|
2303
|
+
const selector = options?.selector || interactiveSelectors.join(", ");
|
|
2304
|
+
const foundElements = root.querySelectorAll(selector);
|
|
2305
|
+
for (const el of foundElements) {
|
|
2306
|
+
if (options?.limit && elements.length >= options.limit) break;
|
|
2307
|
+
const state = getElementState2(el);
|
|
2308
|
+
if (!options?.includeHidden && !state.visible) continue;
|
|
2309
|
+
if (options?.types) {
|
|
2310
|
+
const type = this.inferElementType(el);
|
|
2311
|
+
if (!options.types.includes(type)) continue;
|
|
2312
|
+
}
|
|
2313
|
+
const registered = this.registry.findByDOMElement(el);
|
|
2314
|
+
elements.push({
|
|
2315
|
+
id: registered?.id || this.getElementId(el),
|
|
2316
|
+
type: registered?.type || this.inferElementType(el),
|
|
2317
|
+
label: registered?.label || this.getElementLabel(el),
|
|
2318
|
+
tagName: el.tagName.toLowerCase(),
|
|
2319
|
+
role: el.getAttribute("role") || void 0,
|
|
2320
|
+
accessibleName: this.getAccessibleName(el),
|
|
2321
|
+
actions: registered?.actions || this.inferActions(el),
|
|
2322
|
+
state,
|
|
2323
|
+
registered: !!registered,
|
|
2324
|
+
category: registered?.category || "interactive",
|
|
2325
|
+
contentMetadata: registered?.contentMetadata
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (options?.includeContent || options?.contentOnly) {
|
|
2330
|
+
const contentElements = this.registry.getAllContentElements();
|
|
2331
|
+
for (const el of contentElements) {
|
|
2332
|
+
if (options?.limit && elements.length >= options.limit) break;
|
|
2333
|
+
const state = el.getState();
|
|
2334
|
+
if (!options?.includeHidden && !state.visible) continue;
|
|
2335
|
+
if (options?.contentRole && el.contentMetadata?.contentRole !== options.contentRole) {
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
elements.push({
|
|
2339
|
+
id: el.id,
|
|
2340
|
+
type: el.type,
|
|
2341
|
+
label: el.label,
|
|
2342
|
+
tagName: el.element.tagName.toLowerCase(),
|
|
2343
|
+
role: el.element.getAttribute("role") || void 0,
|
|
2344
|
+
accessibleName: el.label || state.textContent?.trim(),
|
|
2345
|
+
actions: [],
|
|
2346
|
+
state,
|
|
2347
|
+
registered: true,
|
|
2348
|
+
category: "content",
|
|
2349
|
+
contentMetadata: el.contentMetadata
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2217
2352
|
}
|
|
2218
2353
|
return {
|
|
2219
2354
|
elements,
|
|
@@ -2243,7 +2378,9 @@ var DefaultActionExecutor = class {
|
|
|
2243
2378
|
type: el.type,
|
|
2244
2379
|
label: el.label,
|
|
2245
2380
|
actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
|
|
2246
|
-
state: el.getState()
|
|
2381
|
+
state: el.getState(),
|
|
2382
|
+
category: el.category,
|
|
2383
|
+
contentMetadata: el.contentMetadata
|
|
2247
2384
|
})),
|
|
2248
2385
|
components: components.map((comp) => ({
|
|
2249
2386
|
id: comp.id,
|
|
@@ -2327,6 +2464,14 @@ var DefaultActionExecutor = class {
|
|
|
2327
2464
|
return this.performCheck(element, false);
|
|
2328
2465
|
case "toggle":
|
|
2329
2466
|
return this.performToggle(element);
|
|
2467
|
+
case "drag":
|
|
2468
|
+
return this.performDrag(element, params);
|
|
2469
|
+
case "setValue":
|
|
2470
|
+
return this.performSetValue(element, params);
|
|
2471
|
+
case "submit":
|
|
2472
|
+
return this.performSubmit(element);
|
|
2473
|
+
case "reset":
|
|
2474
|
+
return this.performReset(element);
|
|
2330
2475
|
default: {
|
|
2331
2476
|
const registered = this.registry.findByDOMElement(element);
|
|
2332
2477
|
if (registered?.customActions?.[action]) {
|
|
@@ -2356,15 +2501,26 @@ var DefaultActionExecutor = class {
|
|
|
2356
2501
|
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
2357
2502
|
throw new Error("Type action requires an input or textarea element");
|
|
2358
2503
|
}
|
|
2504
|
+
const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
2505
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
2359
2506
|
element.focus();
|
|
2360
2507
|
if (options?.clear) {
|
|
2361
|
-
|
|
2508
|
+
if (nativeSetter) {
|
|
2509
|
+
nativeSetter.call(element, "");
|
|
2510
|
+
} else {
|
|
2511
|
+
element.value = "";
|
|
2512
|
+
}
|
|
2362
2513
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2363
2514
|
}
|
|
2364
2515
|
const text = options?.text || "";
|
|
2365
2516
|
const delay = options?.delay || 0;
|
|
2366
2517
|
for (const char of text) {
|
|
2367
|
-
element.value
|
|
2518
|
+
const current = element.value;
|
|
2519
|
+
if (nativeSetter) {
|
|
2520
|
+
nativeSetter.call(element, current + char);
|
|
2521
|
+
} else {
|
|
2522
|
+
element.value = current + char;
|
|
2523
|
+
}
|
|
2368
2524
|
if (options?.triggerEvents !== false) {
|
|
2369
2525
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2370
2526
|
}
|
|
@@ -2378,7 +2534,13 @@ var DefaultActionExecutor = class {
|
|
|
2378
2534
|
}
|
|
2379
2535
|
performClear(element) {
|
|
2380
2536
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
2381
|
-
element.
|
|
2537
|
+
const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
2538
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
2539
|
+
if (nativeSetter) {
|
|
2540
|
+
nativeSetter.call(element, "");
|
|
2541
|
+
} else {
|
|
2542
|
+
element.value = "";
|
|
2543
|
+
}
|
|
2382
2544
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2383
2545
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2384
2546
|
}
|
|
@@ -2460,6 +2622,150 @@ var DefaultActionExecutor = class {
|
|
|
2460
2622
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2461
2623
|
}
|
|
2462
2624
|
}
|
|
2625
|
+
performSetValue(element, params) {
|
|
2626
|
+
const value = params?.value;
|
|
2627
|
+
if (value === void 0) {
|
|
2628
|
+
throw new Error('setValue requires a "value" parameter');
|
|
2629
|
+
}
|
|
2630
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
2631
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
2632
|
+
element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
|
|
2633
|
+
"value"
|
|
2634
|
+
)?.set;
|
|
2635
|
+
if (nativeSetter) {
|
|
2636
|
+
nativeSetter.call(element, value);
|
|
2637
|
+
} else {
|
|
2638
|
+
element.value = value;
|
|
2639
|
+
}
|
|
2640
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2641
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2642
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
2643
|
+
element.value = value;
|
|
2644
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
performSubmit(element) {
|
|
2648
|
+
const form = element instanceof HTMLFormElement ? element : element.closest("form");
|
|
2649
|
+
if (form) {
|
|
2650
|
+
const submitEvent = new Event("submit", { bubbles: true, cancelable: true });
|
|
2651
|
+
if (form.dispatchEvent(submitEvent)) {
|
|
2652
|
+
form.requestSubmit();
|
|
2653
|
+
}
|
|
2654
|
+
} else {
|
|
2655
|
+
throw new Error("No form found for submit action");
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
performReset(element) {
|
|
2659
|
+
const form = element instanceof HTMLFormElement ? element : element.closest("form");
|
|
2660
|
+
if (form) {
|
|
2661
|
+
form.reset();
|
|
2662
|
+
form.dispatchEvent(new Event("reset", { bubbles: true }));
|
|
2663
|
+
} else {
|
|
2664
|
+
throw new Error("No form found for reset action");
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Perform a drag operation by dispatching a sequence of mouse events.
|
|
2669
|
+
*
|
|
2670
|
+
* Follows the same composite pattern as the qontinui core library:
|
|
2671
|
+
* mousedown on source → wait → mousemove × N along path → mouseup on target.
|
|
2672
|
+
*
|
|
2673
|
+
* Optionally dispatches HTML5 drag events (dragstart/dragover/drop/dragend)
|
|
2674
|
+
* for apps that use the HTML5 Drag and Drop API instead of mouse events.
|
|
2675
|
+
*/
|
|
2676
|
+
async performDrag(sourceElement, options) {
|
|
2677
|
+
const sourceRect = sourceElement.getBoundingClientRect();
|
|
2678
|
+
const sourceX = sourceRect.left + (options?.sourceOffset?.x ?? sourceRect.width / 2);
|
|
2679
|
+
const sourceY = sourceRect.top + (options?.sourceOffset?.y ?? sourceRect.height / 2);
|
|
2680
|
+
let targetX;
|
|
2681
|
+
let targetY;
|
|
2682
|
+
if (options?.targetPosition) {
|
|
2683
|
+
targetX = options.targetPosition.x;
|
|
2684
|
+
targetY = options.targetPosition.y;
|
|
2685
|
+
} else if (options?.target) {
|
|
2686
|
+
const targetElement = this.resolveTargetElement(options.target);
|
|
2687
|
+
if (!targetElement) {
|
|
2688
|
+
throw new Error(`Drag target element not found: ${JSON.stringify(options.target)}`);
|
|
2689
|
+
}
|
|
2690
|
+
const targetRect = targetElement.getBoundingClientRect();
|
|
2691
|
+
targetX = targetRect.left + (options?.targetOffset?.x ?? targetRect.width / 2);
|
|
2692
|
+
targetY = targetRect.top + (options?.targetOffset?.y ?? targetRect.height / 2);
|
|
2693
|
+
} else {
|
|
2694
|
+
throw new Error("Drag requires either target or targetPosition");
|
|
2695
|
+
}
|
|
2696
|
+
const steps = options?.steps ?? 10;
|
|
2697
|
+
const holdDelay = options?.holdDelay ?? 100;
|
|
2698
|
+
const releaseDelay = options?.releaseDelay ?? 50;
|
|
2699
|
+
sourceElement.dispatchEvent(createMouseEventAt("mousedown", sourceX, sourceY));
|
|
2700
|
+
const canHTML5 = options?.html5 && typeof DragEvent !== "undefined";
|
|
2701
|
+
if (canHTML5) {
|
|
2702
|
+
sourceElement.dispatchEvent(
|
|
2703
|
+
new DragEvent("dragstart", {
|
|
2704
|
+
bubbles: true,
|
|
2705
|
+
cancelable: true,
|
|
2706
|
+
clientX: sourceX,
|
|
2707
|
+
clientY: sourceY
|
|
2708
|
+
})
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
if (holdDelay > 0) {
|
|
2712
|
+
await sleep(holdDelay);
|
|
2713
|
+
}
|
|
2714
|
+
for (let i = 1; i <= steps; i++) {
|
|
2715
|
+
const progress = i / steps;
|
|
2716
|
+
const currentX = sourceX + (targetX - sourceX) * progress;
|
|
2717
|
+
const currentY = sourceY + (targetY - sourceY) * progress;
|
|
2718
|
+
const dispatchTarget = elementFromPointSafe(currentX, currentY) || sourceElement;
|
|
2719
|
+
dispatchTarget.dispatchEvent(createMouseEventAt("mousemove", currentX, currentY));
|
|
2720
|
+
if (canHTML5) {
|
|
2721
|
+
dispatchTarget.dispatchEvent(
|
|
2722
|
+
new DragEvent("dragover", {
|
|
2723
|
+
bubbles: true,
|
|
2724
|
+
cancelable: true,
|
|
2725
|
+
clientX: currentX,
|
|
2726
|
+
clientY: currentY
|
|
2727
|
+
})
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
const dropTarget = elementFromPointSafe(targetX, targetY) || sourceElement;
|
|
2732
|
+
dropTarget.dispatchEvent(createMouseEventAt("mouseup", targetX, targetY));
|
|
2733
|
+
if (canHTML5) {
|
|
2734
|
+
dropTarget.dispatchEvent(
|
|
2735
|
+
new DragEvent("drop", {
|
|
2736
|
+
bubbles: true,
|
|
2737
|
+
cancelable: true,
|
|
2738
|
+
clientX: targetX,
|
|
2739
|
+
clientY: targetY
|
|
2740
|
+
})
|
|
2741
|
+
);
|
|
2742
|
+
sourceElement.dispatchEvent(
|
|
2743
|
+
new DragEvent("dragend", {
|
|
2744
|
+
bubbles: true,
|
|
2745
|
+
cancelable: true,
|
|
2746
|
+
clientX: targetX,
|
|
2747
|
+
clientY: targetY
|
|
2748
|
+
})
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (releaseDelay > 0) {
|
|
2752
|
+
await sleep(releaseDelay);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Resolve a drag target element from a target descriptor.
|
|
2757
|
+
*/
|
|
2758
|
+
resolveTargetElement(target) {
|
|
2759
|
+
if (target.elementId) {
|
|
2760
|
+
const registered = this.registry.getElement(target.elementId);
|
|
2761
|
+
if (registered?.element) return registered.element;
|
|
2762
|
+
return findElementByIdentifier(target.elementId);
|
|
2763
|
+
}
|
|
2764
|
+
if (target.selector) {
|
|
2765
|
+
return document.querySelector(target.selector);
|
|
2766
|
+
}
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2463
2769
|
getElementId(element) {
|
|
2464
2770
|
return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
2465
2771
|
}
|
|
@@ -2555,8 +2861,442 @@ var DefaultActionExecutor = class {
|
|
|
2555
2861
|
}
|
|
2556
2862
|
}
|
|
2557
2863
|
};
|
|
2558
|
-
function createActionExecutor(registry) {
|
|
2559
|
-
return new DefaultActionExecutor(registry);
|
|
2864
|
+
function createActionExecutor(registry, consoleCapture) {
|
|
2865
|
+
return new DefaultActionExecutor(registry, consoleCapture);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/specs/types.ts
|
|
2869
|
+
var SPEC_CONFIG_VERSION = "1.0.0";
|
|
2870
|
+
var VALID_ASSERTION_TYPES = [
|
|
2871
|
+
"visible",
|
|
2872
|
+
"hidden",
|
|
2873
|
+
"enabled",
|
|
2874
|
+
"disabled",
|
|
2875
|
+
"focused",
|
|
2876
|
+
"checked",
|
|
2877
|
+
"unchecked",
|
|
2878
|
+
"hasText",
|
|
2879
|
+
"containsText",
|
|
2880
|
+
"hasValue",
|
|
2881
|
+
"hasClass",
|
|
2882
|
+
"exists",
|
|
2883
|
+
"notExists",
|
|
2884
|
+
"count",
|
|
2885
|
+
"attribute",
|
|
2886
|
+
"cssProperty"
|
|
2887
|
+
];
|
|
2888
|
+
var VALID_SPEC_CATEGORIES = [
|
|
2889
|
+
"element-presence",
|
|
2890
|
+
"accessibility",
|
|
2891
|
+
"form-validation",
|
|
2892
|
+
"state-consistency",
|
|
2893
|
+
"modal-dialog",
|
|
2894
|
+
"navigation",
|
|
2895
|
+
"cross-page-consistency",
|
|
2896
|
+
"custom"
|
|
2897
|
+
];
|
|
2898
|
+
var VALID_SPEC_SEVERITIES = [
|
|
2899
|
+
"critical",
|
|
2900
|
+
"warning",
|
|
2901
|
+
"info"
|
|
2902
|
+
];
|
|
2903
|
+
var VALID_SPEC_SOURCES = [
|
|
2904
|
+
"auto",
|
|
2905
|
+
"manual",
|
|
2906
|
+
"ai-generated"
|
|
2907
|
+
];
|
|
2908
|
+
|
|
2909
|
+
// src/specs/validator.ts
|
|
2910
|
+
function isValidAssertionType(value) {
|
|
2911
|
+
return typeof value === "string" && VALID_ASSERTION_TYPES.includes(value);
|
|
2912
|
+
}
|
|
2913
|
+
function isValidSpecCategory(value) {
|
|
2914
|
+
return typeof value === "string" && VALID_SPEC_CATEGORIES.includes(value);
|
|
2915
|
+
}
|
|
2916
|
+
function isValidSpecSeverity(value) {
|
|
2917
|
+
return typeof value === "string" && VALID_SPEC_SEVERITIES.includes(value);
|
|
2918
|
+
}
|
|
2919
|
+
function isValidSpecSource(value) {
|
|
2920
|
+
return typeof value === "string" && VALID_SPEC_SOURCES.includes(value);
|
|
2921
|
+
}
|
|
2922
|
+
function validateSpecAssertion(data, path = "assertion") {
|
|
2923
|
+
const errors = [];
|
|
2924
|
+
if (!data || typeof data !== "object") {
|
|
2925
|
+
errors.push({ path, message: "must be an object" });
|
|
2926
|
+
return errors;
|
|
2927
|
+
}
|
|
2928
|
+
const obj = data;
|
|
2929
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
|
2930
|
+
errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
|
|
2931
|
+
}
|
|
2932
|
+
if (typeof obj.description !== "string") {
|
|
2933
|
+
errors.push({ path: `${path}.description`, message: "must be a string" });
|
|
2934
|
+
}
|
|
2935
|
+
if (!isValidSpecCategory(obj.category)) {
|
|
2936
|
+
errors.push({
|
|
2937
|
+
path: `${path}.category`,
|
|
2938
|
+
message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
if (!isValidSpecSeverity(obj.severity)) {
|
|
2942
|
+
errors.push({
|
|
2943
|
+
path: `${path}.severity`,
|
|
2944
|
+
message: `must be one of: ${VALID_SPEC_SEVERITIES.join(", ")}`
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
if (!obj.target || typeof obj.target !== "object") {
|
|
2948
|
+
errors.push({ path: `${path}.target`, message: "must be an object" });
|
|
2949
|
+
} else {
|
|
2950
|
+
const target = obj.target;
|
|
2951
|
+
if (target.type === "elementId") {
|
|
2952
|
+
if (typeof target.elementId !== "string" || target.elementId.length === 0) {
|
|
2953
|
+
errors.push({ path: `${path}.target.elementId`, message: "must be a non-empty string" });
|
|
2954
|
+
}
|
|
2955
|
+
} else if (target.type === "search") {
|
|
2956
|
+
if (!target.criteria || typeof target.criteria !== "object") {
|
|
2957
|
+
errors.push({ path: `${path}.target.criteria`, message: "must be an object" });
|
|
2958
|
+
}
|
|
2959
|
+
} else {
|
|
2960
|
+
errors.push({ path: `${path}.target.type`, message: 'must be "elementId" or "search"' });
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
if (!isValidAssertionType(obj.assertionType)) {
|
|
2964
|
+
errors.push({
|
|
2965
|
+
path: `${path}.assertionType`,
|
|
2966
|
+
message: `must be one of: ${VALID_ASSERTION_TYPES.join(", ")}`
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
if (!isValidSpecSource(obj.source)) {
|
|
2970
|
+
errors.push({
|
|
2971
|
+
path: `${path}.source`,
|
|
2972
|
+
message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
if (typeof obj.reviewed !== "boolean") {
|
|
2976
|
+
errors.push({ path: `${path}.reviewed`, message: "must be a boolean" });
|
|
2977
|
+
}
|
|
2978
|
+
if (typeof obj.enabled !== "boolean") {
|
|
2979
|
+
errors.push({ path: `${path}.enabled`, message: "must be a boolean" });
|
|
2980
|
+
}
|
|
2981
|
+
if (obj.timeout !== void 0 && (typeof obj.timeout !== "number" || obj.timeout < 0)) {
|
|
2982
|
+
errors.push({ path: `${path}.timeout`, message: "must be a non-negative number" });
|
|
2983
|
+
}
|
|
2984
|
+
return errors;
|
|
2985
|
+
}
|
|
2986
|
+
function validateSpecGroup(data, path = "group") {
|
|
2987
|
+
const errors = [];
|
|
2988
|
+
if (!data || typeof data !== "object") {
|
|
2989
|
+
errors.push({ path, message: "must be an object" });
|
|
2990
|
+
return errors;
|
|
2991
|
+
}
|
|
2992
|
+
const obj = data;
|
|
2993
|
+
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
|
2994
|
+
errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
|
|
2995
|
+
}
|
|
2996
|
+
if (typeof obj.name !== "string") {
|
|
2997
|
+
errors.push({ path: `${path}.name`, message: "must be a string" });
|
|
2998
|
+
}
|
|
2999
|
+
if (typeof obj.description !== "string") {
|
|
3000
|
+
errors.push({ path: `${path}.description`, message: "must be a string" });
|
|
3001
|
+
}
|
|
3002
|
+
if (!isValidSpecCategory(obj.category)) {
|
|
3003
|
+
errors.push({
|
|
3004
|
+
path: `${path}.category`,
|
|
3005
|
+
message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
if (!isValidSpecSource(obj.source)) {
|
|
3009
|
+
errors.push({
|
|
3010
|
+
path: `${path}.source`,
|
|
3011
|
+
message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
if (!Array.isArray(obj.assertions)) {
|
|
3015
|
+
errors.push({ path: `${path}.assertions`, message: "must be an array" });
|
|
3016
|
+
} else {
|
|
3017
|
+
for (let i = 0; i < obj.assertions.length; i++) {
|
|
3018
|
+
errors.push(...validateSpecAssertion(obj.assertions[i], `${path}.assertions[${i}]`));
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return errors;
|
|
3022
|
+
}
|
|
3023
|
+
function validateSpecConfig(data) {
|
|
3024
|
+
const errors = [];
|
|
3025
|
+
if (!data || typeof data !== "object") {
|
|
3026
|
+
return { valid: false, errors: [{ path: "", message: "must be an object" }] };
|
|
3027
|
+
}
|
|
3028
|
+
const obj = data;
|
|
3029
|
+
if (obj.version !== SPEC_CONFIG_VERSION) {
|
|
3030
|
+
errors.push({ path: "version", message: `must be "${SPEC_CONFIG_VERSION}"` });
|
|
3031
|
+
}
|
|
3032
|
+
if (obj.description !== void 0 && typeof obj.description !== "string") {
|
|
3033
|
+
errors.push({ path: "description", message: "must be a string if provided" });
|
|
3034
|
+
}
|
|
3035
|
+
if (!Array.isArray(obj.groups)) {
|
|
3036
|
+
errors.push({ path: "groups", message: "must be an array" });
|
|
3037
|
+
} else {
|
|
3038
|
+
for (let i = 0; i < obj.groups.length; i++) {
|
|
3039
|
+
errors.push(...validateSpecGroup(obj.groups[i], `groups[${i}]`));
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (obj.assertions !== void 0) {
|
|
3043
|
+
if (!Array.isArray(obj.assertions)) {
|
|
3044
|
+
errors.push({ path: "assertions", message: "must be an array if provided" });
|
|
3045
|
+
} else {
|
|
3046
|
+
for (let i = 0; i < obj.assertions.length; i++) {
|
|
3047
|
+
errors.push(...validateSpecAssertion(obj.assertions[i], `assertions[${i}]`));
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
if (obj.metadata !== void 0 && (typeof obj.metadata !== "object" || obj.metadata === null)) {
|
|
3052
|
+
errors.push({ path: "metadata", message: "must be an object if provided" });
|
|
3053
|
+
}
|
|
3054
|
+
return { valid: errors.length === 0, errors };
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/specs/store.ts
|
|
3058
|
+
var SpecStore = class {
|
|
3059
|
+
constructor() {
|
|
3060
|
+
this.configs = /* @__PURE__ */ new Map();
|
|
3061
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
3062
|
+
}
|
|
3063
|
+
// ---------------------------------------------------------------------------
|
|
3064
|
+
// CRUD — Config Level
|
|
3065
|
+
// ---------------------------------------------------------------------------
|
|
3066
|
+
load(specId, config) {
|
|
3067
|
+
this.configs.set(specId, config);
|
|
3068
|
+
this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
|
|
3069
|
+
}
|
|
3070
|
+
unload(specId) {
|
|
3071
|
+
const existed = this.configs.delete(specId);
|
|
3072
|
+
if (existed) {
|
|
3073
|
+
this.emit({ type: "spec:unloaded", specId, timestamp: Date.now() });
|
|
3074
|
+
}
|
|
3075
|
+
return existed;
|
|
3076
|
+
}
|
|
3077
|
+
get(specId) {
|
|
3078
|
+
return this.configs.get(specId);
|
|
3079
|
+
}
|
|
3080
|
+
has(specId) {
|
|
3081
|
+
return this.configs.has(specId);
|
|
3082
|
+
}
|
|
3083
|
+
getIds() {
|
|
3084
|
+
return Array.from(this.configs.keys());
|
|
3085
|
+
}
|
|
3086
|
+
getAll() {
|
|
3087
|
+
return new Map(this.configs);
|
|
3088
|
+
}
|
|
3089
|
+
get count() {
|
|
3090
|
+
return this.configs.size;
|
|
3091
|
+
}
|
|
3092
|
+
clear() {
|
|
3093
|
+
this.configs.clear();
|
|
3094
|
+
this.emit({ type: "spec:cleared", timestamp: Date.now() });
|
|
3095
|
+
}
|
|
3096
|
+
// ---------------------------------------------------------------------------
|
|
3097
|
+
// CRUD — Group Level
|
|
3098
|
+
// ---------------------------------------------------------------------------
|
|
3099
|
+
addGroup(specId, group) {
|
|
3100
|
+
const config = this.configs.get(specId);
|
|
3101
|
+
if (!config) return false;
|
|
3102
|
+
config.groups.push(group);
|
|
3103
|
+
this.emit({ type: "spec:group-added", specId, groupId: group.id, timestamp: Date.now() });
|
|
3104
|
+
return true;
|
|
3105
|
+
}
|
|
3106
|
+
removeGroup(specId, groupId) {
|
|
3107
|
+
const config = this.configs.get(specId);
|
|
3108
|
+
if (!config) return false;
|
|
3109
|
+
const idx = config.groups.findIndex((g) => g.id === groupId);
|
|
3110
|
+
if (idx === -1) return false;
|
|
3111
|
+
config.groups.splice(idx, 1);
|
|
3112
|
+
this.emit({ type: "spec:group-removed", specId, groupId, timestamp: Date.now() });
|
|
3113
|
+
return true;
|
|
3114
|
+
}
|
|
3115
|
+
getGroup(specId, groupId) {
|
|
3116
|
+
const config = this.configs.get(specId);
|
|
3117
|
+
if (!config) return void 0;
|
|
3118
|
+
return config.groups.find((g) => g.id === groupId);
|
|
3119
|
+
}
|
|
3120
|
+
// ---------------------------------------------------------------------------
|
|
3121
|
+
// CRUD — Assertion Level
|
|
3122
|
+
// ---------------------------------------------------------------------------
|
|
3123
|
+
addAssertion(specId, groupId, assertion) {
|
|
3124
|
+
const config = this.configs.get(specId);
|
|
3125
|
+
if (!config) return false;
|
|
3126
|
+
if (groupId) {
|
|
3127
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
3128
|
+
if (!group) return false;
|
|
3129
|
+
group.assertions.push(assertion);
|
|
3130
|
+
} else {
|
|
3131
|
+
if (!config.assertions) config.assertions = [];
|
|
3132
|
+
config.assertions.push(assertion);
|
|
3133
|
+
}
|
|
3134
|
+
this.emit({
|
|
3135
|
+
type: "spec:assertion-added",
|
|
3136
|
+
specId,
|
|
3137
|
+
groupId: groupId ?? void 0,
|
|
3138
|
+
assertionId: assertion.id,
|
|
3139
|
+
timestamp: Date.now()
|
|
3140
|
+
});
|
|
3141
|
+
return true;
|
|
3142
|
+
}
|
|
3143
|
+
removeAssertion(specId, groupId, assertionId) {
|
|
3144
|
+
const config = this.configs.get(specId);
|
|
3145
|
+
if (!config) return false;
|
|
3146
|
+
let removed = false;
|
|
3147
|
+
if (groupId) {
|
|
3148
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
3149
|
+
if (group) {
|
|
3150
|
+
const idx = group.assertions.findIndex((a) => a.id === assertionId);
|
|
3151
|
+
if (idx !== -1) {
|
|
3152
|
+
group.assertions.splice(idx, 1);
|
|
3153
|
+
removed = true;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
} else if (config.assertions) {
|
|
3157
|
+
const idx = config.assertions.findIndex((a) => a.id === assertionId);
|
|
3158
|
+
if (idx !== -1) {
|
|
3159
|
+
config.assertions.splice(idx, 1);
|
|
3160
|
+
removed = true;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
if (removed) {
|
|
3164
|
+
this.emit({
|
|
3165
|
+
type: "spec:assertion-removed",
|
|
3166
|
+
specId,
|
|
3167
|
+
groupId: groupId ?? void 0,
|
|
3168
|
+
assertionId,
|
|
3169
|
+
timestamp: Date.now()
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
return removed;
|
|
3173
|
+
}
|
|
3174
|
+
toggleAssertion(specId, groupId, assertionId) {
|
|
3175
|
+
const assertion = this.findAssertion(specId, groupId, assertionId);
|
|
3176
|
+
if (!assertion) return false;
|
|
3177
|
+
assertion.enabled = !assertion.enabled;
|
|
3178
|
+
this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
|
|
3179
|
+
return true;
|
|
3180
|
+
}
|
|
3181
|
+
markReviewed(specId, groupId, assertionId) {
|
|
3182
|
+
const assertion = this.findAssertion(specId, groupId, assertionId);
|
|
3183
|
+
if (!assertion) return false;
|
|
3184
|
+
assertion.reviewed = !assertion.reviewed;
|
|
3185
|
+
this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
|
|
3186
|
+
return true;
|
|
3187
|
+
}
|
|
3188
|
+
// ---------------------------------------------------------------------------
|
|
3189
|
+
// Queries
|
|
3190
|
+
// ---------------------------------------------------------------------------
|
|
3191
|
+
getAllAssertions() {
|
|
3192
|
+
const result = [];
|
|
3193
|
+
for (const config of this.configs.values()) {
|
|
3194
|
+
for (const group of config.groups) {
|
|
3195
|
+
result.push(...group.assertions);
|
|
3196
|
+
}
|
|
3197
|
+
if (config.assertions) {
|
|
3198
|
+
result.push(...config.assertions);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
return result;
|
|
3202
|
+
}
|
|
3203
|
+
filterAssertions(opts) {
|
|
3204
|
+
return this.getAllAssertions().filter((a) => {
|
|
3205
|
+
if (opts.categories && !opts.categories.includes(a.category)) return false;
|
|
3206
|
+
if (opts.severities && !opts.severities.includes(a.severity)) return false;
|
|
3207
|
+
if (opts.enabledOnly && !a.enabled) return false;
|
|
3208
|
+
if (opts.reviewedOnly && !a.reviewed) return false;
|
|
3209
|
+
return true;
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
// ---------------------------------------------------------------------------
|
|
3213
|
+
// Coverage
|
|
3214
|
+
// ---------------------------------------------------------------------------
|
|
3215
|
+
getCoverage(allElementIds) {
|
|
3216
|
+
const specifiedIdSet = /* @__PURE__ */ new Set();
|
|
3217
|
+
for (const assertion of this.getAllAssertions()) {
|
|
3218
|
+
if (assertion.target.type === "elementId") {
|
|
3219
|
+
specifiedIdSet.add(assertion.target.elementId);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
const specifiedIds = [];
|
|
3223
|
+
const unspecifiedIds = [];
|
|
3224
|
+
for (const id of allElementIds) {
|
|
3225
|
+
if (specifiedIdSet.has(id)) {
|
|
3226
|
+
specifiedIds.push(id);
|
|
3227
|
+
} else {
|
|
3228
|
+
unspecifiedIds.push(id);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
const total = allElementIds.length;
|
|
3232
|
+
return {
|
|
3233
|
+
totalElements: total,
|
|
3234
|
+
specifiedElements: specifiedIds.length,
|
|
3235
|
+
coveragePercent: total > 0 ? specifiedIds.length / total * 100 : 0,
|
|
3236
|
+
specifiedIds,
|
|
3237
|
+
unspecifiedIds,
|
|
3238
|
+
timestamp: Date.now()
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
// ---------------------------------------------------------------------------
|
|
3242
|
+
// Import / Export
|
|
3243
|
+
// ---------------------------------------------------------------------------
|
|
3244
|
+
importConfig(specId, config) {
|
|
3245
|
+
const result = validateSpecConfig(config);
|
|
3246
|
+
if (!result.valid) return false;
|
|
3247
|
+
this.configs.set(specId, config);
|
|
3248
|
+
this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
|
|
3249
|
+
return true;
|
|
3250
|
+
}
|
|
3251
|
+
exportConfig(specId) {
|
|
3252
|
+
const config = this.configs.get(specId);
|
|
3253
|
+
if (!config) return void 0;
|
|
3254
|
+
return {
|
|
3255
|
+
...config,
|
|
3256
|
+
version: SPEC_CONFIG_VERSION,
|
|
3257
|
+
metadata: {
|
|
3258
|
+
...config.metadata,
|
|
3259
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3260
|
+
}
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
// ---------------------------------------------------------------------------
|
|
3264
|
+
// Events
|
|
3265
|
+
// ---------------------------------------------------------------------------
|
|
3266
|
+
on(listener) {
|
|
3267
|
+
this.listeners.add(listener);
|
|
3268
|
+
return () => {
|
|
3269
|
+
this.listeners.delete(listener);
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
emit(event) {
|
|
3273
|
+
for (const listener of this.listeners) {
|
|
3274
|
+
try {
|
|
3275
|
+
listener(event);
|
|
3276
|
+
} catch {
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
// ---------------------------------------------------------------------------
|
|
3281
|
+
// Private Helpers
|
|
3282
|
+
// ---------------------------------------------------------------------------
|
|
3283
|
+
findAssertion(specId, groupId, assertionId) {
|
|
3284
|
+
const config = this.configs.get(specId);
|
|
3285
|
+
if (!config) return void 0;
|
|
3286
|
+
if (groupId) {
|
|
3287
|
+
const group = config.groups.find((g) => g.id === groupId);
|
|
3288
|
+
if (!group) return void 0;
|
|
3289
|
+
return group.assertions.find((a) => a.id === assertionId);
|
|
3290
|
+
}
|
|
3291
|
+
return config.assertions?.find((a) => a.id === assertionId);
|
|
3292
|
+
}
|
|
3293
|
+
};
|
|
3294
|
+
var globalStore = null;
|
|
3295
|
+
function getGlobalSpecStore() {
|
|
3296
|
+
if (!globalStore) {
|
|
3297
|
+
globalStore = new SpecStore();
|
|
3298
|
+
}
|
|
3299
|
+
return globalStore;
|
|
2560
3300
|
}
|
|
2561
3301
|
|
|
2562
3302
|
// src/control/workflow-engine.ts
|
|
@@ -3588,34 +4328,690 @@ var MetricsCollector = class {
|
|
|
3588
4328
|
function createMetricsCollector(options) {
|
|
3589
4329
|
return new MetricsCollector(options);
|
|
3590
4330
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
4331
|
+
|
|
4332
|
+
// src/debug/browser-capture-types.ts
|
|
4333
|
+
var DEFAULT_CAPTURE_CONFIG = {
|
|
4334
|
+
console: true,
|
|
4335
|
+
network: true,
|
|
4336
|
+
navigation: true,
|
|
4337
|
+
longTasks: true,
|
|
4338
|
+
longAnimationFrames: true,
|
|
4339
|
+
resourceErrors: true,
|
|
4340
|
+
wsDisconnections: true,
|
|
4341
|
+
hmr: true,
|
|
4342
|
+
webVitals: false,
|
|
4343
|
+
memory: false,
|
|
4344
|
+
memoryIntervalMs: 3e4,
|
|
4345
|
+
maxEntries: 200
|
|
4346
|
+
};
|
|
4347
|
+
|
|
4348
|
+
// src/debug/captures/console.ts
|
|
4349
|
+
function argsToMessage(args) {
|
|
4350
|
+
return args.map((a) => {
|
|
4351
|
+
if (a instanceof Error) return a.message;
|
|
4352
|
+
if (typeof a === "string") return a;
|
|
4353
|
+
try {
|
|
4354
|
+
return JSON.stringify(a);
|
|
4355
|
+
} catch {
|
|
4356
|
+
return String(a);
|
|
3613
4357
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
4358
|
+
}).join(" ");
|
|
4359
|
+
}
|
|
4360
|
+
function extractStack(args) {
|
|
4361
|
+
const err = args.find((a) => a instanceof Error);
|
|
4362
|
+
return err?.stack;
|
|
4363
|
+
}
|
|
4364
|
+
function makeEvent(level, message, stack) {
|
|
4365
|
+
return {
|
|
4366
|
+
type: "console",
|
|
4367
|
+
timestamp: Date.now(),
|
|
4368
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4369
|
+
level,
|
|
4370
|
+
message,
|
|
4371
|
+
stack
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
function installConsoleCapture(emit) {
|
|
4375
|
+
const originalError = console.error;
|
|
4376
|
+
const originalWarn = console.warn;
|
|
4377
|
+
console.error = (...args) => {
|
|
4378
|
+
emit(makeEvent("error", argsToMessage(args), extractStack(args)));
|
|
4379
|
+
originalError.apply(console, args);
|
|
4380
|
+
};
|
|
4381
|
+
console.warn = (...args) => {
|
|
4382
|
+
emit(makeEvent("warn", argsToMessage(args), extractStack(args)));
|
|
4383
|
+
originalWarn.apply(console, args);
|
|
4384
|
+
};
|
|
4385
|
+
const rejectionHandler = (event) => {
|
|
4386
|
+
const reason = event.reason;
|
|
4387
|
+
const message = reason instanceof Error ? reason.message : String(reason ?? "Unhandled rejection");
|
|
4388
|
+
const stack = reason instanceof Error ? reason.stack : void 0;
|
|
4389
|
+
emit(makeEvent("unhandledrejection", message, stack));
|
|
4390
|
+
};
|
|
4391
|
+
if (typeof window !== "undefined") {
|
|
4392
|
+
window.addEventListener("unhandledrejection", rejectionHandler);
|
|
4393
|
+
}
|
|
4394
|
+
return () => {
|
|
4395
|
+
console.error = originalError;
|
|
4396
|
+
console.warn = originalWarn;
|
|
4397
|
+
if (typeof window !== "undefined") {
|
|
4398
|
+
window.removeEventListener("unhandledrejection", rejectionHandler);
|
|
3616
4399
|
}
|
|
3617
|
-
|
|
3618
|
-
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/debug/captures/network.ts
|
|
4404
|
+
var DEFAULT_IGNORE = ["/api/dev-debug/", "/api/ui-bridge/", "localhost:9876"];
|
|
4405
|
+
function installNetworkCapture(emit, options) {
|
|
4406
|
+
if (typeof window === "undefined" || typeof window.fetch !== "function") {
|
|
4407
|
+
return () => {
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
const originalFetch = window.fetch;
|
|
4411
|
+
const ignorePatterns = options?.ignorePatterns ?? DEFAULT_IGNORE;
|
|
4412
|
+
function shouldIgnore(url) {
|
|
4413
|
+
return ignorePatterns.some((p) => url.includes(p));
|
|
4414
|
+
}
|
|
4415
|
+
function getMethod(input, init) {
|
|
4416
|
+
if (init?.method) return init.method.toUpperCase();
|
|
4417
|
+
if (input instanceof Request) return input.method.toUpperCase();
|
|
4418
|
+
return "GET";
|
|
4419
|
+
}
|
|
4420
|
+
function getUrl(input) {
|
|
4421
|
+
if (typeof input === "string") return input;
|
|
4422
|
+
if (input instanceof URL) return input.href;
|
|
4423
|
+
if (input instanceof Request) return input.url;
|
|
4424
|
+
return String(input);
|
|
4425
|
+
}
|
|
4426
|
+
window.fetch = async function patchedFetch(input, init) {
|
|
4427
|
+
const requestUrl = getUrl(input);
|
|
4428
|
+
if (shouldIgnore(requestUrl)) {
|
|
4429
|
+
return originalFetch.call(window, input, init);
|
|
4430
|
+
}
|
|
4431
|
+
const method = getMethod(input, init);
|
|
4432
|
+
const start = performance.now();
|
|
4433
|
+
try {
|
|
4434
|
+
const response = await originalFetch.call(window, input, init);
|
|
4435
|
+
const durationMs = Math.round(performance.now() - start);
|
|
4436
|
+
if (response.status >= 400) {
|
|
4437
|
+
const event = {
|
|
4438
|
+
type: "network",
|
|
4439
|
+
timestamp: Date.now(),
|
|
4440
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4441
|
+
method,
|
|
4442
|
+
requestUrl,
|
|
4443
|
+
status: response.status,
|
|
4444
|
+
statusText: response.statusText,
|
|
4445
|
+
durationMs,
|
|
4446
|
+
kind: "http-error"
|
|
4447
|
+
};
|
|
4448
|
+
emit(event);
|
|
4449
|
+
}
|
|
4450
|
+
return response;
|
|
4451
|
+
} catch (err) {
|
|
4452
|
+
const durationMs = Math.round(performance.now() - start);
|
|
4453
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
4454
|
+
let kind = "network-error";
|
|
4455
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
4456
|
+
kind = "abort";
|
|
4457
|
+
} else if (errorMessage.includes("CORS") || errorMessage.includes("cross-origin")) {
|
|
4458
|
+
kind = "cors";
|
|
4459
|
+
} else if (errorMessage.includes("timeout") || errorMessage.includes("timed out")) {
|
|
4460
|
+
kind = "timeout";
|
|
4461
|
+
}
|
|
4462
|
+
const event = {
|
|
4463
|
+
type: "network",
|
|
4464
|
+
timestamp: Date.now(),
|
|
4465
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4466
|
+
method,
|
|
4467
|
+
requestUrl,
|
|
4468
|
+
durationMs,
|
|
4469
|
+
kind,
|
|
4470
|
+
errorMessage
|
|
4471
|
+
};
|
|
4472
|
+
emit(event);
|
|
4473
|
+
throw err;
|
|
4474
|
+
}
|
|
4475
|
+
};
|
|
4476
|
+
return () => {
|
|
4477
|
+
window.fetch = originalFetch;
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
// src/debug/captures/navigation.ts
|
|
4482
|
+
function installNavigationCapture(emit) {
|
|
4483
|
+
if (typeof window === "undefined" || typeof history === "undefined") {
|
|
4484
|
+
return () => {
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
let lastUrl = window.location.href;
|
|
4488
|
+
function emitNav(to, trigger) {
|
|
4489
|
+
const from = lastUrl;
|
|
4490
|
+
lastUrl = to;
|
|
4491
|
+
if (from === to) return;
|
|
4492
|
+
emit({
|
|
4493
|
+
type: "navigation",
|
|
4494
|
+
timestamp: Date.now(),
|
|
4495
|
+
url: to,
|
|
4496
|
+
from,
|
|
4497
|
+
to,
|
|
4498
|
+
trigger
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
const originalPushState = history.pushState;
|
|
4502
|
+
const originalReplaceState = history.replaceState;
|
|
4503
|
+
history.pushState = function(...args) {
|
|
4504
|
+
originalPushState.apply(this, args);
|
|
4505
|
+
emitNav(window.location.href, "pushState");
|
|
4506
|
+
};
|
|
4507
|
+
history.replaceState = function(...args) {
|
|
4508
|
+
originalReplaceState.apply(this, args);
|
|
4509
|
+
emitNav(window.location.href, "replaceState");
|
|
4510
|
+
};
|
|
4511
|
+
const popstateHandler = () => {
|
|
4512
|
+
emitNav(window.location.href, "popstate");
|
|
4513
|
+
};
|
|
4514
|
+
window.addEventListener("popstate", popstateHandler);
|
|
4515
|
+
return () => {
|
|
4516
|
+
history.pushState = originalPushState;
|
|
4517
|
+
history.replaceState = originalReplaceState;
|
|
4518
|
+
window.removeEventListener("popstate", popstateHandler);
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
// src/debug/captures/long-tasks.ts
|
|
4523
|
+
function installLongTaskCapture(emit) {
|
|
4524
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
|
|
4525
|
+
return () => {
|
|
4526
|
+
};
|
|
4527
|
+
}
|
|
4528
|
+
try {
|
|
4529
|
+
const observer = new PerformanceObserver((list) => {
|
|
4530
|
+
for (const entry of list.getEntries()) {
|
|
4531
|
+
emit({
|
|
4532
|
+
type: "long-task",
|
|
4533
|
+
timestamp: Date.now(),
|
|
4534
|
+
url: window.location.href,
|
|
4535
|
+
durationMs: Math.round(entry.duration)
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4539
|
+
observer.observe({ type: "longtask", buffered: true });
|
|
4540
|
+
return () => {
|
|
4541
|
+
observer.disconnect();
|
|
4542
|
+
};
|
|
4543
|
+
} catch {
|
|
4544
|
+
return () => {
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
// src/debug/captures/resource-errors.ts
|
|
4550
|
+
var TRACKED_TAGS = /* @__PURE__ */ new Set(["IMG", "SCRIPT", "LINK"]);
|
|
4551
|
+
function installResourceErrorCapture(emit) {
|
|
4552
|
+
if (typeof window === "undefined") {
|
|
4553
|
+
return () => {
|
|
4554
|
+
};
|
|
4555
|
+
}
|
|
4556
|
+
const handler = (event) => {
|
|
4557
|
+
const target = event.target;
|
|
4558
|
+
if (!target || !target.tagName) return;
|
|
4559
|
+
if (!TRACKED_TAGS.has(target.tagName)) return;
|
|
4560
|
+
const resourceUrl = target.src || target.src || target.href || "";
|
|
4561
|
+
if (!resourceUrl) return;
|
|
4562
|
+
emit({
|
|
4563
|
+
type: "resource-error",
|
|
4564
|
+
timestamp: Date.now(),
|
|
4565
|
+
url: window.location.href,
|
|
4566
|
+
resourceUrl,
|
|
4567
|
+
tagName: target.tagName
|
|
4568
|
+
});
|
|
4569
|
+
};
|
|
4570
|
+
window.addEventListener("error", handler, true);
|
|
4571
|
+
return () => {
|
|
4572
|
+
window.removeEventListener("error", handler, true);
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
// src/debug/captures/web-vitals.ts
|
|
4577
|
+
function installWebVitalsCapture(emit) {
|
|
4578
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
|
|
4579
|
+
return () => {
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
const observers = [];
|
|
4583
|
+
try {
|
|
4584
|
+
const lcpObserver = new PerformanceObserver((list) => {
|
|
4585
|
+
const entries = list.getEntries();
|
|
4586
|
+
const last = entries[entries.length - 1];
|
|
4587
|
+
if (last) {
|
|
4588
|
+
emit({
|
|
4589
|
+
type: "web-vital",
|
|
4590
|
+
timestamp: Date.now(),
|
|
4591
|
+
url: window.location.href,
|
|
4592
|
+
metric: "LCP",
|
|
4593
|
+
value: Math.round(last.startTime)
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
});
|
|
4597
|
+
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
|
|
4598
|
+
observers.push(lcpObserver);
|
|
4599
|
+
} catch {
|
|
4600
|
+
}
|
|
4601
|
+
try {
|
|
4602
|
+
let clsValue = 0;
|
|
4603
|
+
const clsObserver = new PerformanceObserver((list) => {
|
|
4604
|
+
for (const entry of list.getEntries()) {
|
|
4605
|
+
if (!entry.hadRecentInput) {
|
|
4606
|
+
clsValue += entry.value ?? 0;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
emit({
|
|
4610
|
+
type: "web-vital",
|
|
4611
|
+
timestamp: Date.now(),
|
|
4612
|
+
url: window.location.href,
|
|
4613
|
+
metric: "CLS",
|
|
4614
|
+
value: Math.round(clsValue * 1e3) / 1e3
|
|
4615
|
+
});
|
|
4616
|
+
});
|
|
4617
|
+
clsObserver.observe({ type: "layout-shift", buffered: true });
|
|
4618
|
+
observers.push(clsObserver);
|
|
4619
|
+
} catch {
|
|
4620
|
+
}
|
|
4621
|
+
return () => {
|
|
4622
|
+
for (const obs of observers) {
|
|
4623
|
+
obs.disconnect();
|
|
4624
|
+
}
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
// src/debug/captures/memory.ts
|
|
4629
|
+
function installMemoryCapture(emit, intervalMs = 3e4) {
|
|
4630
|
+
if (typeof window === "undefined") {
|
|
4631
|
+
return () => {
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
const perf = performance;
|
|
4635
|
+
if (!perf.memory) {
|
|
4636
|
+
return () => {
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
4639
|
+
const tick = () => {
|
|
4640
|
+
const mem = perf.memory;
|
|
4641
|
+
if (!mem) return;
|
|
4642
|
+
emit({
|
|
4643
|
+
type: "memory",
|
|
4644
|
+
timestamp: Date.now(),
|
|
4645
|
+
url: window.location.href,
|
|
4646
|
+
usedJSHeapSize: mem.usedJSHeapSize,
|
|
4647
|
+
totalJSHeapSize: mem.totalJSHeapSize,
|
|
4648
|
+
jsHeapSizeLimit: mem.jsHeapSizeLimit
|
|
4649
|
+
});
|
|
4650
|
+
};
|
|
4651
|
+
tick();
|
|
4652
|
+
const id = setInterval(tick, intervalMs);
|
|
4653
|
+
return () => {
|
|
4654
|
+
clearInterval(id);
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
// src/debug/captures/hmr.ts
|
|
4659
|
+
var HMR_PATH_PATTERNS = ["/_next/webpack-hmr", "/__turbopack_hmr", "/_next/turbopack-hmr"];
|
|
4660
|
+
function isHmrUrl(url) {
|
|
4661
|
+
return HMR_PATH_PATTERNS.some((p) => url.includes(p));
|
|
4662
|
+
}
|
|
4663
|
+
function makeEvent2(level, message, moduleName, loc) {
|
|
4664
|
+
return {
|
|
4665
|
+
type: "hmr",
|
|
4666
|
+
level,
|
|
4667
|
+
message,
|
|
4668
|
+
moduleName,
|
|
4669
|
+
loc,
|
|
4670
|
+
timestamp: Date.now(),
|
|
4671
|
+
url: typeof window !== "undefined" ? window.location.href : ""
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
function processHmrMessage(data, emit) {
|
|
4675
|
+
try {
|
|
4676
|
+
const msg = JSON.parse(data);
|
|
4677
|
+
if (Array.isArray(msg.errors)) {
|
|
4678
|
+
for (const err of msg.errors) {
|
|
4679
|
+
emit(
|
|
4680
|
+
makeEvent2(
|
|
4681
|
+
"error",
|
|
4682
|
+
typeof err === "string" ? err : err.message ?? String(err),
|
|
4683
|
+
err.moduleName ?? err.moduleIdentifier,
|
|
4684
|
+
err.loc ? String(err.loc) : void 0
|
|
4685
|
+
)
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (Array.isArray(msg.warnings)) {
|
|
4690
|
+
for (const warn of msg.warnings) {
|
|
4691
|
+
emit(
|
|
4692
|
+
makeEvent2(
|
|
4693
|
+
"warning",
|
|
4694
|
+
typeof warn === "string" ? warn : warn.message ?? String(warn),
|
|
4695
|
+
warn.moduleName ?? warn.moduleIdentifier,
|
|
4696
|
+
warn.loc ? String(warn.loc) : void 0
|
|
4697
|
+
)
|
|
4698
|
+
);
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
if (msg.action === "serverError" && msg.errorJSON) {
|
|
4702
|
+
try {
|
|
4703
|
+
const err = JSON.parse(msg.errorJSON);
|
|
4704
|
+
emit(makeEvent2("error", err.message ?? String(err)));
|
|
4705
|
+
} catch {
|
|
4706
|
+
emit(makeEvent2("error", msg.errorJSON));
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
if ((msg.action === "turbopack-message" || msg.type === "turbopack-message") && msg.data?.diagnostics) {
|
|
4710
|
+
for (const diag of msg.data.diagnostics) {
|
|
4711
|
+
emit(
|
|
4712
|
+
makeEvent2(
|
|
4713
|
+
diag.category === "warning" ? "warning" : "error",
|
|
4714
|
+
diag.message ?? String(diag),
|
|
4715
|
+
diag.filePath,
|
|
4716
|
+
diag.line != null ? `${diag.line}:${diag.column ?? 0}` : void 0
|
|
4717
|
+
)
|
|
4718
|
+
);
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
} catch {
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
function installWebSocketCapture(emit, cleanups) {
|
|
4725
|
+
if (!window.WebSocket) return;
|
|
4726
|
+
const OriginalWebSocket = window.WebSocket;
|
|
4727
|
+
const trackedSockets = [];
|
|
4728
|
+
const PatchedWebSocket = function(url, protocols) {
|
|
4729
|
+
const ws = new OriginalWebSocket(url, protocols);
|
|
4730
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
4731
|
+
if (isHmrUrl(urlStr)) {
|
|
4732
|
+
ws.addEventListener("message", (event) => {
|
|
4733
|
+
if (typeof event.data === "string") {
|
|
4734
|
+
processHmrMessage(event.data, emit);
|
|
4735
|
+
}
|
|
4736
|
+
});
|
|
4737
|
+
trackedSockets.push(ws);
|
|
4738
|
+
}
|
|
4739
|
+
return ws;
|
|
4740
|
+
};
|
|
4741
|
+
PatchedWebSocket.prototype = OriginalWebSocket.prototype;
|
|
4742
|
+
Object.defineProperty(PatchedWebSocket, "CONNECTING", { value: OriginalWebSocket.CONNECTING });
|
|
4743
|
+
Object.defineProperty(PatchedWebSocket, "OPEN", { value: OriginalWebSocket.OPEN });
|
|
4744
|
+
Object.defineProperty(PatchedWebSocket, "CLOSING", { value: OriginalWebSocket.CLOSING });
|
|
4745
|
+
Object.defineProperty(PatchedWebSocket, "CLOSED", { value: OriginalWebSocket.CLOSED });
|
|
4746
|
+
window.WebSocket = PatchedWebSocket;
|
|
4747
|
+
cleanups.push(() => {
|
|
4748
|
+
window.WebSocket = OriginalWebSocket;
|
|
4749
|
+
for (const ws of trackedSockets) {
|
|
4750
|
+
ws.close();
|
|
4751
|
+
}
|
|
4752
|
+
trackedSockets.length = 0;
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
function installEventSourceCapture(emit, cleanups) {
|
|
4756
|
+
if (!window.EventSource) return;
|
|
4757
|
+
const OriginalEventSource = window.EventSource;
|
|
4758
|
+
const trackedSources = [];
|
|
4759
|
+
const messageHandler = (event) => {
|
|
4760
|
+
if (typeof event.data === "string") {
|
|
4761
|
+
processHmrMessage(event.data, emit);
|
|
4762
|
+
}
|
|
4763
|
+
};
|
|
4764
|
+
const PatchedEventSource = function(url, init) {
|
|
4765
|
+
const es = new OriginalEventSource(url, init);
|
|
4766
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
4767
|
+
if (isHmrUrl(urlStr)) {
|
|
4768
|
+
es.addEventListener("message", messageHandler);
|
|
4769
|
+
trackedSources.push(es);
|
|
4770
|
+
}
|
|
4771
|
+
return es;
|
|
4772
|
+
};
|
|
4773
|
+
PatchedEventSource.prototype = OriginalEventSource.prototype;
|
|
4774
|
+
Object.defineProperty(PatchedEventSource, "CONNECTING", {
|
|
4775
|
+
value: OriginalEventSource.CONNECTING
|
|
4776
|
+
});
|
|
4777
|
+
Object.defineProperty(PatchedEventSource, "OPEN", { value: OriginalEventSource.OPEN });
|
|
4778
|
+
Object.defineProperty(PatchedEventSource, "CLOSED", { value: OriginalEventSource.CLOSED });
|
|
4779
|
+
window.EventSource = PatchedEventSource;
|
|
4780
|
+
cleanups.push(() => {
|
|
4781
|
+
window.EventSource = OriginalEventSource;
|
|
4782
|
+
for (const es of trackedSources) {
|
|
4783
|
+
es.close();
|
|
4784
|
+
}
|
|
4785
|
+
trackedSources.length = 0;
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
function installHmrCapture(emit) {
|
|
4789
|
+
if (typeof window === "undefined") return () => {
|
|
4790
|
+
};
|
|
4791
|
+
const cleanups = [];
|
|
4792
|
+
installWebSocketCapture(emit, cleanups);
|
|
4793
|
+
installEventSourceCapture(emit, cleanups);
|
|
4794
|
+
return () => {
|
|
4795
|
+
for (const cleanup of cleanups) {
|
|
4796
|
+
cleanup();
|
|
4797
|
+
}
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
// src/debug/captures/long-animation-frames.ts
|
|
4802
|
+
function installLoafCapture(emit) {
|
|
4803
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
|
|
4804
|
+
return () => {
|
|
4805
|
+
};
|
|
4806
|
+
}
|
|
4807
|
+
try {
|
|
4808
|
+
const observer = new PerformanceObserver((list) => {
|
|
4809
|
+
for (const entry of list.getEntries()) {
|
|
4810
|
+
const scripts = (entry.scripts ?? []).map((s) => {
|
|
4811
|
+
const script = s;
|
|
4812
|
+
return {
|
|
4813
|
+
invoker: script.invoker ?? "",
|
|
4814
|
+
sourceURL: script.sourceURL ?? "",
|
|
4815
|
+
sourceFunctionName: script.sourceFunctionName ?? "",
|
|
4816
|
+
sourceCharPosition: script.sourceCharPosition ?? 0,
|
|
4817
|
+
duration: Math.round(script.duration ?? 0)
|
|
4818
|
+
};
|
|
4819
|
+
});
|
|
4820
|
+
emit({
|
|
4821
|
+
type: "long-animation-frame",
|
|
4822
|
+
timestamp: Date.now(),
|
|
4823
|
+
url: window.location.href,
|
|
4824
|
+
durationMs: Math.round(entry.duration),
|
|
4825
|
+
blockingDurationMs: Math.round(
|
|
4826
|
+
entry.blockingDuration ?? 0
|
|
4827
|
+
),
|
|
4828
|
+
scripts
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
observer.observe({ type: "long-animation-frame", buffered: true });
|
|
4833
|
+
return () => {
|
|
4834
|
+
observer.disconnect();
|
|
4835
|
+
};
|
|
4836
|
+
} catch {
|
|
4837
|
+
return () => {
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
// src/debug/browser-capture.ts
|
|
4843
|
+
var BrowserEventCapture = class {
|
|
4844
|
+
constructor(config) {
|
|
4845
|
+
this.buffer = [];
|
|
4846
|
+
this.installed = false;
|
|
4847
|
+
this.cleanups = [];
|
|
4848
|
+
this.onEvent = null;
|
|
4849
|
+
this.config = config ?? {};
|
|
4850
|
+
this.maxEntries = config?.maxEntries ?? DEFAULT_CAPTURE_CONFIG.maxEntries;
|
|
4851
|
+
}
|
|
4852
|
+
setOnEvent(cb) {
|
|
4853
|
+
this.onEvent = cb;
|
|
4854
|
+
}
|
|
4855
|
+
/**
|
|
4856
|
+
* Install all enabled capture sub-modules.
|
|
4857
|
+
* Safe to call multiple times (no-ops if already installed).
|
|
4858
|
+
*/
|
|
4859
|
+
install() {
|
|
4860
|
+
if (this.installed) return;
|
|
4861
|
+
const cfg = { ...DEFAULT_CAPTURE_CONFIG, ...this.config };
|
|
4862
|
+
const emit = (event) => {
|
|
4863
|
+
this.buffer.push(event);
|
|
4864
|
+
this.trim();
|
|
4865
|
+
this.onEvent?.(event);
|
|
4866
|
+
};
|
|
4867
|
+
if (cfg.console) {
|
|
4868
|
+
this.cleanups.push(installConsoleCapture(emit));
|
|
4869
|
+
}
|
|
4870
|
+
if (cfg.network) {
|
|
4871
|
+
this.cleanups.push(installNetworkCapture(emit, cfg.networkOptions));
|
|
4872
|
+
}
|
|
4873
|
+
if (cfg.navigation) {
|
|
4874
|
+
this.cleanups.push(installNavigationCapture(emit));
|
|
4875
|
+
}
|
|
4876
|
+
if (cfg.longTasks) {
|
|
4877
|
+
this.cleanups.push(installLongTaskCapture(emit));
|
|
4878
|
+
}
|
|
4879
|
+
if (cfg.longAnimationFrames) {
|
|
4880
|
+
this.cleanups.push(installLoafCapture(emit));
|
|
4881
|
+
}
|
|
4882
|
+
if (cfg.resourceErrors) {
|
|
4883
|
+
this.cleanups.push(installResourceErrorCapture(emit));
|
|
4884
|
+
}
|
|
4885
|
+
if (cfg.webVitals) {
|
|
4886
|
+
this.cleanups.push(installWebVitalsCapture(emit));
|
|
4887
|
+
}
|
|
4888
|
+
if (cfg.memory) {
|
|
4889
|
+
this.cleanups.push(installMemoryCapture(emit, cfg.memoryIntervalMs));
|
|
4890
|
+
}
|
|
4891
|
+
if (cfg.hmr) {
|
|
4892
|
+
this.cleanups.push(installHmrCapture(emit));
|
|
4893
|
+
}
|
|
4894
|
+
this.installed = true;
|
|
4895
|
+
}
|
|
4896
|
+
/**
|
|
4897
|
+
* Uninstall all capture sub-modules.
|
|
4898
|
+
*/
|
|
4899
|
+
uninstall() {
|
|
4900
|
+
if (!this.installed) return;
|
|
4901
|
+
for (const cleanup of this.cleanups) {
|
|
4902
|
+
cleanup();
|
|
4903
|
+
}
|
|
4904
|
+
this.cleanups = [];
|
|
4905
|
+
this.installed = false;
|
|
4906
|
+
}
|
|
4907
|
+
// -------------------------------------------------------------------------
|
|
4908
|
+
// Manual event reporting (for events that can't be auto-captured)
|
|
4909
|
+
// -------------------------------------------------------------------------
|
|
4910
|
+
reportReactError(error, errorInfo) {
|
|
4911
|
+
const event = {
|
|
4912
|
+
type: "react-error",
|
|
4913
|
+
timestamp: Date.now(),
|
|
4914
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4915
|
+
message: error.message,
|
|
4916
|
+
stack: error.stack,
|
|
4917
|
+
componentStack: errorInfo.componentStack
|
|
4918
|
+
};
|
|
4919
|
+
this.buffer.push(event);
|
|
4920
|
+
this.trim();
|
|
4921
|
+
this.onEvent?.(event);
|
|
4922
|
+
}
|
|
4923
|
+
reportWsStateChange(prev, next, reconnectAttempt) {
|
|
4924
|
+
if (next === "disconnected" || next === "error") {
|
|
4925
|
+
const event = {
|
|
4926
|
+
type: "ws-disconnection",
|
|
4927
|
+
timestamp: Date.now(),
|
|
4928
|
+
url: typeof window !== "undefined" ? window.location.href : "",
|
|
4929
|
+
previousState: prev,
|
|
4930
|
+
newState: next,
|
|
4931
|
+
reconnectAttempt
|
|
4932
|
+
};
|
|
4933
|
+
this.buffer.push(event);
|
|
4934
|
+
this.trim();
|
|
4935
|
+
this.onEvent?.(event);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
// -------------------------------------------------------------------------
|
|
4939
|
+
// Query methods
|
|
4940
|
+
// -------------------------------------------------------------------------
|
|
4941
|
+
getSince(ts) {
|
|
4942
|
+
return this.buffer.filter((e) => e.timestamp >= ts);
|
|
4943
|
+
}
|
|
4944
|
+
getRecent(n = 50) {
|
|
4945
|
+
return this.buffer.slice(-n);
|
|
4946
|
+
}
|
|
4947
|
+
getByType(type) {
|
|
4948
|
+
return this.buffer.filter((e) => e.type === type);
|
|
4949
|
+
}
|
|
4950
|
+
/**
|
|
4951
|
+
* Get console errors since a timestamp (backward-compat for ActionExecutor).
|
|
4952
|
+
*/
|
|
4953
|
+
getConsoleSince(ts) {
|
|
4954
|
+
return this.buffer.filter((e) => (e.type === "console" || e.type === "hmr") && e.timestamp >= ts).map((e) => ({
|
|
4955
|
+
timestamp: e.timestamp,
|
|
4956
|
+
level: e.type === "hmr" ? e.level === "warning" ? "warn" : e.level : e.level,
|
|
4957
|
+
message: e.message,
|
|
4958
|
+
stack: e.stack
|
|
4959
|
+
}));
|
|
4960
|
+
}
|
|
4961
|
+
/**
|
|
4962
|
+
* Get recent console errors (backward-compat for ActionExecutor).
|
|
4963
|
+
*/
|
|
4964
|
+
getConsoleRecent(n = 50) {
|
|
4965
|
+
return this.buffer.filter((e) => e.type === "console" || e.type === "hmr").slice(-n).map((e) => ({
|
|
4966
|
+
timestamp: e.timestamp,
|
|
4967
|
+
level: e.type === "hmr" ? e.level === "warning" ? "warn" : e.level : e.level,
|
|
4968
|
+
message: e.message,
|
|
4969
|
+
stack: e.stack
|
|
4970
|
+
}));
|
|
4971
|
+
}
|
|
4972
|
+
clear() {
|
|
4973
|
+
this.buffer = [];
|
|
4974
|
+
}
|
|
4975
|
+
trim() {
|
|
4976
|
+
if (this.buffer.length > this.maxEntries) {
|
|
4977
|
+
this.buffer = this.buffer.slice(-this.maxEntries);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
};
|
|
4981
|
+
var UIBridgeContext = react.createContext(null);
|
|
4982
|
+
function UIBridgeProvider({
|
|
4983
|
+
children,
|
|
4984
|
+
features = {},
|
|
4985
|
+
config = {},
|
|
4986
|
+
onEvent,
|
|
4987
|
+
onBrowserEvent,
|
|
4988
|
+
browserCaptureConfig
|
|
4989
|
+
}) {
|
|
4990
|
+
const registryRef = react.useRef(null);
|
|
4991
|
+
const renderLogRef = react.useRef(null);
|
|
4992
|
+
const metricsRef = react.useRef(null);
|
|
4993
|
+
const browserCaptureRef = react.useRef(null);
|
|
4994
|
+
const wsClientRef = react.useRef(null);
|
|
4995
|
+
const [wsConnectionState, setWsConnectionState] = react.useState("disconnected");
|
|
4996
|
+
const prevWsStateRef = react.useRef("disconnected");
|
|
4997
|
+
if (!registryRef.current) {
|
|
4998
|
+
registryRef.current = new UIBridgeRegistry({
|
|
4999
|
+
verbose: config.verbose,
|
|
5000
|
+
onEvent
|
|
5001
|
+
});
|
|
5002
|
+
setGlobalRegistry(registryRef.current);
|
|
5003
|
+
if (features.renderLog) {
|
|
5004
|
+
renderLogRef.current = createRenderLogManager({
|
|
5005
|
+
maxEntries: config.maxLogEntries
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
if (features.debug) {
|
|
5009
|
+
metricsRef.current = createMetricsCollector();
|
|
5010
|
+
}
|
|
5011
|
+
browserCaptureRef.current = new BrowserEventCapture(browserCaptureConfig);
|
|
5012
|
+
browserCaptureRef.current.install();
|
|
5013
|
+
if (config.websocket) {
|
|
5014
|
+
const wsPort = config.websocketPort || config.serverPort || 9876;
|
|
3619
5015
|
const wsUrl = `ws://localhost:${wsPort}`;
|
|
3620
5016
|
wsClientRef.current = createWSClient({
|
|
3621
5017
|
url: wsUrl,
|
|
@@ -3625,12 +5021,27 @@ function UIBridgeProvider({
|
|
|
3625
5021
|
pingInterval: 3e4
|
|
3626
5022
|
});
|
|
3627
5023
|
}
|
|
5024
|
+
if (typeof window !== "undefined") {
|
|
5025
|
+
const w = window;
|
|
5026
|
+
if (!w.__UI_BRIDGE__) {
|
|
5027
|
+
w.__UI_BRIDGE__ = {};
|
|
5028
|
+
}
|
|
5029
|
+
w.__UI_BRIDGE__.specs = {
|
|
5030
|
+
getGlobalSpecStore
|
|
5031
|
+
};
|
|
5032
|
+
w.__UI_BRIDGE__.browserCapture = browserCaptureRef.current;
|
|
5033
|
+
w.__UI_BRIDGE__.consoleCapture = browserCaptureRef.current;
|
|
5034
|
+
}
|
|
3628
5035
|
}
|
|
3629
5036
|
const registry = registryRef.current;
|
|
3630
5037
|
const renderLog = renderLogRef.current || void 0;
|
|
3631
5038
|
const metrics = metricsRef.current || void 0;
|
|
3632
5039
|
const wsClient = wsClientRef.current || void 0;
|
|
3633
|
-
const
|
|
5040
|
+
const browserCapture = browserCaptureRef.current || void 0;
|
|
5041
|
+
const executor = react.useMemo(
|
|
5042
|
+
() => createActionExecutor(registry, browserCapture),
|
|
5043
|
+
[registry, browserCapture]
|
|
5044
|
+
);
|
|
3634
5045
|
const workflowEngine = react.useMemo(
|
|
3635
5046
|
() => createWorkflowEngine(registry, executor),
|
|
3636
5047
|
[registry, executor]
|
|
@@ -3657,13 +5068,21 @@ function UIBridgeProvider({
|
|
|
3657
5068
|
react.useEffect(() => {
|
|
3658
5069
|
if (!wsClient) return;
|
|
3659
5070
|
const unsubscribe = wsClient.onConnectionChange((state) => {
|
|
5071
|
+
const prev = prevWsStateRef.current;
|
|
5072
|
+
prevWsStateRef.current = state;
|
|
3660
5073
|
setWsConnectionState(state);
|
|
5074
|
+
browserCaptureRef.current?.reportWsStateChange(prev, state);
|
|
3661
5075
|
});
|
|
3662
5076
|
return unsubscribe;
|
|
3663
5077
|
}, [wsClient]);
|
|
5078
|
+
react.useEffect(() => {
|
|
5079
|
+
browserCaptureRef.current?.setOnEvent(onBrowserEvent ?? null);
|
|
5080
|
+
}, [onBrowserEvent]);
|
|
3664
5081
|
react.useEffect(() => {
|
|
3665
5082
|
return () => {
|
|
3666
5083
|
renderLog?.stop();
|
|
5084
|
+
browserCaptureRef.current?.setOnEvent(null);
|
|
5085
|
+
browserCaptureRef.current?.uninstall();
|
|
3667
5086
|
wsClient?.disconnect();
|
|
3668
5087
|
resetGlobalRegistry();
|
|
3669
5088
|
};
|
|
@@ -3698,6 +5117,10 @@ function UIBridgeProvider({
|
|
|
3698
5117
|
const getElements = react.useCallback(() => registry.getAllElements(), [registry]);
|
|
3699
5118
|
const getComponents = react.useCallback(() => registry.getAllComponents(), [registry]);
|
|
3700
5119
|
const createSnapshot = react.useCallback(() => registry.createSnapshot(), [registry]);
|
|
5120
|
+
const createSnapshotAsync = react.useCallback(
|
|
5121
|
+
(batchSize) => registry.createSnapshotAsync(batchSize),
|
|
5122
|
+
[registry]
|
|
5123
|
+
);
|
|
3701
5124
|
const on = react.useCallback(
|
|
3702
5125
|
(type, listener) => registry.on(type, listener),
|
|
3703
5126
|
[registry]
|
|
@@ -3720,6 +5143,7 @@ function UIBridgeProvider({
|
|
|
3720
5143
|
getElements,
|
|
3721
5144
|
getComponents,
|
|
3722
5145
|
createSnapshot,
|
|
5146
|
+
createSnapshotAsync,
|
|
3723
5147
|
on,
|
|
3724
5148
|
off,
|
|
3725
5149
|
initialized: true,
|
|
@@ -3741,6 +5165,7 @@ function UIBridgeProvider({
|
|
|
3741
5165
|
getElements,
|
|
3742
5166
|
getComponents,
|
|
3743
5167
|
createSnapshot,
|
|
5168
|
+
createSnapshotAsync,
|
|
3744
5169
|
on,
|
|
3745
5170
|
off,
|
|
3746
5171
|
wsConnect,
|
|
@@ -3980,6 +5405,20 @@ function useUIBridge() {
|
|
|
3980
5405
|
}
|
|
3981
5406
|
return context.createSnapshot();
|
|
3982
5407
|
}, [context]);
|
|
5408
|
+
const createSnapshotAsync = react.useCallback(
|
|
5409
|
+
async (batchSize) => {
|
|
5410
|
+
if (!context) {
|
|
5411
|
+
return {
|
|
5412
|
+
timestamp: Date.now(),
|
|
5413
|
+
elements: [],
|
|
5414
|
+
components: [],
|
|
5415
|
+
workflows: []
|
|
5416
|
+
};
|
|
5417
|
+
}
|
|
5418
|
+
return context.createSnapshotAsync(batchSize);
|
|
5419
|
+
},
|
|
5420
|
+
[context]
|
|
5421
|
+
);
|
|
3983
5422
|
const executeAction = react.useCallback(
|
|
3984
5423
|
async (elementId, request) => {
|
|
3985
5424
|
if (!context) {
|
|
@@ -4101,6 +5540,7 @@ function useUIBridge() {
|
|
|
4101
5540
|
components,
|
|
4102
5541
|
workflows,
|
|
4103
5542
|
createSnapshot,
|
|
5543
|
+
createSnapshotAsync,
|
|
4104
5544
|
executeAction,
|
|
4105
5545
|
executeComponentAction,
|
|
4106
5546
|
find,
|
|
@@ -4516,6 +5956,305 @@ function useNavigationPath(targetStates) {
|
|
|
4516
5956
|
return bridge.registry.findPath(targetStates);
|
|
4517
5957
|
}, [bridge, targetStates]);
|
|
4518
5958
|
}
|
|
5959
|
+
|
|
5960
|
+
// src/react/content-discovery.ts
|
|
5961
|
+
var CONTENT_SELECTORS = [
|
|
5962
|
+
"h1",
|
|
5963
|
+
"h2",
|
|
5964
|
+
"h3",
|
|
5965
|
+
"h4",
|
|
5966
|
+
"h5",
|
|
5967
|
+
"h6",
|
|
5968
|
+
"p",
|
|
5969
|
+
"li",
|
|
5970
|
+
"td",
|
|
5971
|
+
"th",
|
|
5972
|
+
"label:not([for])",
|
|
5973
|
+
"figcaption",
|
|
5974
|
+
"caption",
|
|
5975
|
+
"blockquote",
|
|
5976
|
+
"pre",
|
|
5977
|
+
"code",
|
|
5978
|
+
"dd",
|
|
5979
|
+
"dt",
|
|
5980
|
+
'[role="heading"]',
|
|
5981
|
+
'[role="status"]',
|
|
5982
|
+
'[role="alert"]',
|
|
5983
|
+
"[aria-live]",
|
|
5984
|
+
"legend",
|
|
5985
|
+
"summary",
|
|
5986
|
+
"[data-content-role]"
|
|
5987
|
+
];
|
|
5988
|
+
var CONTENT_EXCLUDE_SELECTORS = [
|
|
5989
|
+
"script",
|
|
5990
|
+
"style",
|
|
5991
|
+
"noscript",
|
|
5992
|
+
"template",
|
|
5993
|
+
'[aria-hidden="true"]',
|
|
5994
|
+
"[data-no-register]",
|
|
5995
|
+
".sr-only",
|
|
5996
|
+
".visually-hidden"
|
|
5997
|
+
];
|
|
5998
|
+
function getDirectTextContent(element) {
|
|
5999
|
+
let text = "";
|
|
6000
|
+
for (const node of element.childNodes) {
|
|
6001
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
6002
|
+
text += node.textContent || "";
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
return text.trim();
|
|
6006
|
+
}
|
|
6007
|
+
var SEMANTIC_CONTENT_TAGS = /* @__PURE__ */ new Set([
|
|
6008
|
+
"h1",
|
|
6009
|
+
"h2",
|
|
6010
|
+
"h3",
|
|
6011
|
+
"h4",
|
|
6012
|
+
"h5",
|
|
6013
|
+
"h6",
|
|
6014
|
+
"p",
|
|
6015
|
+
"li",
|
|
6016
|
+
"td",
|
|
6017
|
+
"th",
|
|
6018
|
+
"label",
|
|
6019
|
+
"figcaption",
|
|
6020
|
+
"caption",
|
|
6021
|
+
"blockquote",
|
|
6022
|
+
"pre",
|
|
6023
|
+
"code",
|
|
6024
|
+
"dd",
|
|
6025
|
+
"dt",
|
|
6026
|
+
"legend",
|
|
6027
|
+
"summary"
|
|
6028
|
+
]);
|
|
6029
|
+
function isNoise(element) {
|
|
6030
|
+
const text = getDirectTextContent(element);
|
|
6031
|
+
const tag = element.tagName.toLowerCase();
|
|
6032
|
+
if (!text && element.children.length > 0 && !SEMANTIC_CONTENT_TAGS.has(tag) && !element.hasAttribute("data-content-role")) {
|
|
6033
|
+
return true;
|
|
6034
|
+
}
|
|
6035
|
+
const fullText = element.textContent?.trim() || "";
|
|
6036
|
+
if (fullText.length === 1 && !/\w/.test(fullText)) {
|
|
6037
|
+
return true;
|
|
6038
|
+
}
|
|
6039
|
+
return false;
|
|
6040
|
+
}
|
|
6041
|
+
function isContentVisible(element) {
|
|
6042
|
+
const style = window.getComputedStyle(element);
|
|
6043
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
6044
|
+
return false;
|
|
6045
|
+
}
|
|
6046
|
+
if (parseFloat(style.opacity) === 0) {
|
|
6047
|
+
return false;
|
|
6048
|
+
}
|
|
6049
|
+
const rect = element.getBoundingClientRect();
|
|
6050
|
+
return rect.width > 0 && rect.height > 0;
|
|
6051
|
+
}
|
|
6052
|
+
function shouldRegisterContent(element, options = {}, registeredIds) {
|
|
6053
|
+
const minTextLength = options.minTextLength ?? 1;
|
|
6054
|
+
const excludeSelectors = [
|
|
6055
|
+
...CONTENT_EXCLUDE_SELECTORS,
|
|
6056
|
+
...options.excludeContentSelectors || []
|
|
6057
|
+
];
|
|
6058
|
+
for (const sel of excludeSelectors) {
|
|
6059
|
+
if (element.matches(sel)) {
|
|
6060
|
+
return false;
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
if (!isContentVisible(element)) {
|
|
6064
|
+
return false;
|
|
6065
|
+
}
|
|
6066
|
+
const text = element.textContent?.trim() || "";
|
|
6067
|
+
if (text.length < minTextLength) {
|
|
6068
|
+
return false;
|
|
6069
|
+
}
|
|
6070
|
+
if (isNoise(element)) {
|
|
6071
|
+
return false;
|
|
6072
|
+
}
|
|
6073
|
+
if (isInteractiveElement(element)) {
|
|
6074
|
+
return false;
|
|
6075
|
+
}
|
|
6076
|
+
const id = generateContentId(element);
|
|
6077
|
+
if (registeredIds.has(id)) {
|
|
6078
|
+
return false;
|
|
6079
|
+
}
|
|
6080
|
+
if (options.contentRoles && options.contentRoles.length > 0) {
|
|
6081
|
+
const metadata = inferContentMetadata(element);
|
|
6082
|
+
if (!options.contentRoles.includes(metadata.contentRole)) {
|
|
6083
|
+
return false;
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
return true;
|
|
6087
|
+
}
|
|
6088
|
+
function isInteractiveElement(element) {
|
|
6089
|
+
const tag = element.tagName.toLowerCase();
|
|
6090
|
+
const interactiveTags = ["button", "input", "select", "textarea", "a"];
|
|
6091
|
+
if (interactiveTags.includes(tag)) return true;
|
|
6092
|
+
const role = element.getAttribute("role");
|
|
6093
|
+
const interactiveRoles = [
|
|
6094
|
+
"button",
|
|
6095
|
+
"link",
|
|
6096
|
+
"checkbox",
|
|
6097
|
+
"radio",
|
|
6098
|
+
"menuitem",
|
|
6099
|
+
"tab",
|
|
6100
|
+
"switch",
|
|
6101
|
+
"slider",
|
|
6102
|
+
"spinbutton",
|
|
6103
|
+
"combobox",
|
|
6104
|
+
"listbox",
|
|
6105
|
+
"option",
|
|
6106
|
+
"textbox"
|
|
6107
|
+
];
|
|
6108
|
+
if (role && interactiveRoles.includes(role)) return true;
|
|
6109
|
+
if (element.getAttribute("contenteditable") === "true") return true;
|
|
6110
|
+
if (element.hasAttribute("data-ui-element")) return true;
|
|
6111
|
+
return false;
|
|
6112
|
+
}
|
|
6113
|
+
function inferContentType(element) {
|
|
6114
|
+
const explicitRole = element.getAttribute("data-content-role");
|
|
6115
|
+
if (explicitRole) return roleToContentType(explicitRole);
|
|
6116
|
+
const tag = element.tagName.toLowerCase();
|
|
6117
|
+
const role = element.getAttribute("role");
|
|
6118
|
+
if (role === "heading") return "heading";
|
|
6119
|
+
if (role === "status") return "status-message";
|
|
6120
|
+
if (role === "alert") return "status-message";
|
|
6121
|
+
if (/^h[1-6]$/.test(tag)) return "heading";
|
|
6122
|
+
if (tag === "p") return "paragraph";
|
|
6123
|
+
if (tag === "li") return "list-item";
|
|
6124
|
+
if (tag === "td") return "table-cell";
|
|
6125
|
+
if (tag === "th") return "table-header";
|
|
6126
|
+
if (tag === "label") return "label";
|
|
6127
|
+
if (tag === "figcaption" || tag === "caption") return "caption";
|
|
6128
|
+
if (tag === "blockquote") return "blockquote";
|
|
6129
|
+
if (tag === "pre" || tag === "code") return "code-block";
|
|
6130
|
+
if (tag === "dd") return "description-text";
|
|
6131
|
+
if (tag === "dt") return "label";
|
|
6132
|
+
if (tag === "legend") return "label";
|
|
6133
|
+
if (tag === "summary") return "label";
|
|
6134
|
+
if (element.hasAttribute("aria-live")) return "status-message";
|
|
6135
|
+
const classList = element.className?.toLowerCase() || "";
|
|
6136
|
+
if (classList.includes("badge")) return "badge";
|
|
6137
|
+
if (classList.includes("status")) return "status-message";
|
|
6138
|
+
if (classList.includes("metric") || classList.includes("stat")) return "metric-value";
|
|
6139
|
+
return "content-generic";
|
|
6140
|
+
}
|
|
6141
|
+
function roleToContentType(role) {
|
|
6142
|
+
const map = {
|
|
6143
|
+
heading: "heading",
|
|
6144
|
+
"body-text": "paragraph",
|
|
6145
|
+
"list-item": "list-item",
|
|
6146
|
+
"table-cell": "table-cell",
|
|
6147
|
+
"table-header": "table-header",
|
|
6148
|
+
label: "label",
|
|
6149
|
+
caption: "caption",
|
|
6150
|
+
quote: "blockquote",
|
|
6151
|
+
code: "code-block",
|
|
6152
|
+
badge: "badge",
|
|
6153
|
+
status: "status-message",
|
|
6154
|
+
metric: "metric-value",
|
|
6155
|
+
description: "description-text",
|
|
6156
|
+
navigation: "nav-text",
|
|
6157
|
+
generic: "content-generic"
|
|
6158
|
+
};
|
|
6159
|
+
return map[role] ?? "content-generic";
|
|
6160
|
+
}
|
|
6161
|
+
function contentTypeToRole(contentType) {
|
|
6162
|
+
const map = {
|
|
6163
|
+
heading: "heading",
|
|
6164
|
+
paragraph: "body-text",
|
|
6165
|
+
"list-item": "list-item",
|
|
6166
|
+
"table-cell": "table-cell",
|
|
6167
|
+
"table-header": "table-header",
|
|
6168
|
+
label: "label",
|
|
6169
|
+
caption: "caption",
|
|
6170
|
+
blockquote: "quote",
|
|
6171
|
+
"code-block": "code",
|
|
6172
|
+
badge: "badge",
|
|
6173
|
+
"status-message": "status",
|
|
6174
|
+
"metric-value": "metric",
|
|
6175
|
+
"description-text": "description",
|
|
6176
|
+
"nav-text": "navigation",
|
|
6177
|
+
"content-generic": "generic"
|
|
6178
|
+
};
|
|
6179
|
+
return map[contentType];
|
|
6180
|
+
}
|
|
6181
|
+
function inferContentMetadata(element) {
|
|
6182
|
+
const contentType = inferContentType(element);
|
|
6183
|
+
const explicitRole = element.getAttribute("data-content-role");
|
|
6184
|
+
const contentRole = explicitRole ? explicitRole : contentTypeToRole(contentType);
|
|
6185
|
+
const metadata = {
|
|
6186
|
+
contentRole
|
|
6187
|
+
};
|
|
6188
|
+
const explicitLevel = element.getAttribute("data-content-level");
|
|
6189
|
+
const tag = element.tagName.toLowerCase();
|
|
6190
|
+
const role = element.getAttribute("role");
|
|
6191
|
+
if (explicitLevel) {
|
|
6192
|
+
metadata.headingLevel = parseInt(explicitLevel, 10);
|
|
6193
|
+
} else if (/^h([1-6])$/.test(tag)) {
|
|
6194
|
+
metadata.headingLevel = parseInt(tag[1], 10);
|
|
6195
|
+
} else if (role === "heading") {
|
|
6196
|
+
const ariaLevel = element.getAttribute("aria-level");
|
|
6197
|
+
metadata.headingLevel = ariaLevel ? parseInt(ariaLevel, 10) : 2;
|
|
6198
|
+
}
|
|
6199
|
+
if (element.hasAttribute("aria-live") || role === "status" || role === "alert") {
|
|
6200
|
+
metadata.dynamic = true;
|
|
6201
|
+
}
|
|
6202
|
+
if (tag === "td" || tag === "th") {
|
|
6203
|
+
const row = element.closest("tr");
|
|
6204
|
+
const table = element.closest("table");
|
|
6205
|
+
if (row && table) {
|
|
6206
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
6207
|
+
const rowIndex = rows.indexOf(row);
|
|
6208
|
+
const cells = Array.from(row.children);
|
|
6209
|
+
const colIndex = cells.indexOf(element);
|
|
6210
|
+
metadata.structuralContext = `table > row ${rowIndex} > col ${colIndex}`;
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
if (metadata.dynamic) {
|
|
6214
|
+
const text = element.textContent?.trim() || "";
|
|
6215
|
+
if (text.length > 10) {
|
|
6216
|
+
metadata.stableTextPrefix = text.substring(0, 10);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
return metadata;
|
|
6220
|
+
}
|
|
6221
|
+
function slugify(text, maxLength = 30) {
|
|
6222
|
+
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, maxLength).replace(/-+$/, "");
|
|
6223
|
+
}
|
|
6224
|
+
function generateContentId(element) {
|
|
6225
|
+
const explicitId = element.getAttribute("data-content-id");
|
|
6226
|
+
if (explicitId) return explicitId;
|
|
6227
|
+
const contentType = inferContentType(element);
|
|
6228
|
+
const tag = element.tagName.toLowerCase();
|
|
6229
|
+
const contentLabel = element.getAttribute("data-content-label");
|
|
6230
|
+
if (contentType === "heading") {
|
|
6231
|
+
const level = element.getAttribute("data-content-level") || (/^h([1-6])$/.test(tag) ? tag[1] : element.getAttribute("aria-level") || "2");
|
|
6232
|
+
const text = contentLabel || element.textContent?.trim() || "";
|
|
6233
|
+
const slug = slugify(text);
|
|
6234
|
+
return `heading-${level}-${slug || "untitled"}`;
|
|
6235
|
+
}
|
|
6236
|
+
if (contentType === "table-cell" || contentType === "table-header") {
|
|
6237
|
+
const row = element.closest("tr");
|
|
6238
|
+
const table = element.closest("table");
|
|
6239
|
+
if (row && table) {
|
|
6240
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
6241
|
+
const rowIndex = rows.indexOf(row);
|
|
6242
|
+
const cells = Array.from(row.children);
|
|
6243
|
+
const colIndex = cells.indexOf(element);
|
|
6244
|
+
const tableAnchor = table.getAttribute("data-ui-id") || table.getAttribute("data-testid") || table.id || "table";
|
|
6245
|
+
return `cell-r${rowIndex}-c${colIndex}-${tableAnchor}`;
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
const parent = element.parentElement;
|
|
6249
|
+
const anchorId = parent?.getAttribute("data-ui-id") || parent?.getAttribute("data-testid") || parent?.id || "";
|
|
6250
|
+
const siblings = parent ? Array.from(parent.querySelectorAll(`:scope > ${tag}`)) : [];
|
|
6251
|
+
const siblingIndex = siblings.indexOf(element);
|
|
6252
|
+
const textSlug = slugify(contentLabel || element.textContent?.trim() || "", 20);
|
|
6253
|
+
const anchor = anchorId ? slugify(anchorId, 15) : textSlug;
|
|
6254
|
+
return `content-${contentType}-${anchor || "unknown"}-${siblingIndex >= 0 ? siblingIndex : 0}`;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
// src/react/useAutoRegister.ts
|
|
4519
6258
|
var INTERACTIVE_SELECTORS2 = [
|
|
4520
6259
|
"a[href]",
|
|
4521
6260
|
"button",
|
|
@@ -4710,12 +6449,17 @@ function useAutoRegister(options = {}) {
|
|
|
4710
6449
|
excludeSelectors = [],
|
|
4711
6450
|
generateId: customGenerateId,
|
|
4712
6451
|
onRegister,
|
|
4713
|
-
onUnregister
|
|
6452
|
+
onUnregister,
|
|
6453
|
+
contentDiscovery
|
|
4714
6454
|
} = options;
|
|
6455
|
+
const contentEnabled = contentDiscovery?.enabled !== false;
|
|
4715
6456
|
const bridge = useUIBridgeOptional();
|
|
4716
6457
|
const registeredElementsRef = react.useRef(/* @__PURE__ */ new Map());
|
|
6458
|
+
const registeredContentElementsRef = react.useRef(/* @__PURE__ */ new Map());
|
|
4717
6459
|
const pendingRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
6460
|
+
const pendingContentRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
4718
6461
|
const debounceTimeoutRef = react.useRef(null);
|
|
6462
|
+
const contentDebounceTimeoutRef = react.useRef(null);
|
|
4719
6463
|
const shouldRegister = react.useCallback(
|
|
4720
6464
|
(element) => {
|
|
4721
6465
|
if (!includeHidden && !isElementVisible2(element)) {
|
|
@@ -4783,6 +6527,41 @@ function useAutoRegister(options = {}) {
|
|
|
4783
6527
|
},
|
|
4784
6528
|
[bridge, onUnregister]
|
|
4785
6529
|
);
|
|
6530
|
+
const registerContentElement = react.useCallback(
|
|
6531
|
+
(element) => {
|
|
6532
|
+
if (!bridge?.registry || registeredContentElementsRef.current.has(element)) {
|
|
6533
|
+
return;
|
|
6534
|
+
}
|
|
6535
|
+
const maxElements = contentDiscovery?.maxContentElements ?? 500;
|
|
6536
|
+
if (registeredContentElementsRef.current.size >= maxElements) {
|
|
6537
|
+
return;
|
|
6538
|
+
}
|
|
6539
|
+
const id = generateContentId(element);
|
|
6540
|
+
const existing = bridge.registry.getElement(id);
|
|
6541
|
+
if (existing) {
|
|
6542
|
+
return;
|
|
6543
|
+
}
|
|
6544
|
+
const contentType = inferContentType(element);
|
|
6545
|
+
const metadata = inferContentMetadata(element);
|
|
6546
|
+
const label = element.getAttribute("data-content-label") || element.textContent?.trim().substring(0, 50) || void 0;
|
|
6547
|
+
bridge.registry.registerContentElement(id, element, {
|
|
6548
|
+
contentType,
|
|
6549
|
+
contentMetadata: metadata,
|
|
6550
|
+
label
|
|
6551
|
+
});
|
|
6552
|
+
registeredContentElementsRef.current.set(element, id);
|
|
6553
|
+
},
|
|
6554
|
+
[bridge, contentDiscovery?.maxContentElements]
|
|
6555
|
+
);
|
|
6556
|
+
const unregisterContentElement = react.useCallback(
|
|
6557
|
+
(element) => {
|
|
6558
|
+
const id = registeredContentElementsRef.current.get(element);
|
|
6559
|
+
if (!id || !bridge?.registry) return;
|
|
6560
|
+
bridge.registry.unregisterElement(id);
|
|
6561
|
+
registeredContentElementsRef.current.delete(element);
|
|
6562
|
+
},
|
|
6563
|
+
[bridge]
|
|
6564
|
+
);
|
|
4786
6565
|
const processPendingRegistrations = react.useCallback(() => {
|
|
4787
6566
|
pendingRegistrationsRef.current.forEach((element) => {
|
|
4788
6567
|
if (shouldRegister(element)) {
|
|
@@ -4791,6 +6570,15 @@ function useAutoRegister(options = {}) {
|
|
|
4791
6570
|
});
|
|
4792
6571
|
pendingRegistrationsRef.current.clear();
|
|
4793
6572
|
}, [shouldRegister, registerElement]);
|
|
6573
|
+
const processPendingContentRegistrations = react.useCallback(() => {
|
|
6574
|
+
const registeredIds = new Set(registeredContentElementsRef.current.values());
|
|
6575
|
+
pendingContentRegistrationsRef.current.forEach((element) => {
|
|
6576
|
+
if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
|
|
6577
|
+
registerContentElement(element);
|
|
6578
|
+
}
|
|
6579
|
+
});
|
|
6580
|
+
pendingContentRegistrationsRef.current.clear();
|
|
6581
|
+
}, [contentDiscovery, registerContentElement]);
|
|
4794
6582
|
const queueRegistration = react.useCallback(
|
|
4795
6583
|
(element) => {
|
|
4796
6584
|
pendingRegistrationsRef.current.add(element);
|
|
@@ -4801,6 +6589,20 @@ function useAutoRegister(options = {}) {
|
|
|
4801
6589
|
},
|
|
4802
6590
|
[debounceMs, processPendingRegistrations]
|
|
4803
6591
|
);
|
|
6592
|
+
const queueContentRegistration = react.useCallback(
|
|
6593
|
+
(element) => {
|
|
6594
|
+
pendingContentRegistrationsRef.current.add(element);
|
|
6595
|
+
if (contentDebounceTimeoutRef.current) {
|
|
6596
|
+
clearTimeout(contentDebounceTimeoutRef.current);
|
|
6597
|
+
}
|
|
6598
|
+
const contentDebounceMs = contentDiscovery?.contentDebounceMs ?? 250;
|
|
6599
|
+
contentDebounceTimeoutRef.current = setTimeout(
|
|
6600
|
+
processPendingContentRegistrations,
|
|
6601
|
+
contentDebounceMs
|
|
6602
|
+
);
|
|
6603
|
+
},
|
|
6604
|
+
[contentDiscovery?.contentDebounceMs, processPendingContentRegistrations]
|
|
6605
|
+
);
|
|
4804
6606
|
const scanAndRegister = react.useCallback(
|
|
4805
6607
|
(rootElement) => {
|
|
4806
6608
|
const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
|
|
@@ -4810,8 +6612,28 @@ function useAutoRegister(options = {}) {
|
|
|
4810
6612
|
queueRegistration(element);
|
|
4811
6613
|
}
|
|
4812
6614
|
});
|
|
6615
|
+
if (contentEnabled) {
|
|
6616
|
+
const contentSelectors = [
|
|
6617
|
+
...CONTENT_SELECTORS,
|
|
6618
|
+
...contentDiscovery?.includeContentSelectors || []
|
|
6619
|
+
].join(", ");
|
|
6620
|
+
const contentElements = rootElement.querySelectorAll(contentSelectors);
|
|
6621
|
+
const registeredIds = new Set(registeredContentElementsRef.current.values());
|
|
6622
|
+
contentElements.forEach((element) => {
|
|
6623
|
+
if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
|
|
6624
|
+
queueContentRegistration(element);
|
|
6625
|
+
}
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
4813
6628
|
},
|
|
4814
|
-
[
|
|
6629
|
+
[
|
|
6630
|
+
includeSelectors,
|
|
6631
|
+
shouldRegister,
|
|
6632
|
+
queueRegistration,
|
|
6633
|
+
contentEnabled,
|
|
6634
|
+
contentDiscovery,
|
|
6635
|
+
queueContentRegistration
|
|
6636
|
+
]
|
|
4815
6637
|
);
|
|
4816
6638
|
const handleMutations = react.useCallback(
|
|
4817
6639
|
(mutations) => {
|
|
@@ -4829,6 +6651,22 @@ function useAutoRegister(options = {}) {
|
|
|
4829
6651
|
queueRegistration(descendant);
|
|
4830
6652
|
}
|
|
4831
6653
|
});
|
|
6654
|
+
if (contentEnabled) {
|
|
6655
|
+
const contentSelectors = [
|
|
6656
|
+
...CONTENT_SELECTORS,
|
|
6657
|
+
...contentDiscovery?.includeContentSelectors || []
|
|
6658
|
+
].join(", ");
|
|
6659
|
+
const registeredIds = new Set(registeredContentElementsRef.current.values());
|
|
6660
|
+
if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
|
|
6661
|
+
queueContentRegistration(element);
|
|
6662
|
+
}
|
|
6663
|
+
const contentDescendants = element.querySelectorAll(contentSelectors);
|
|
6664
|
+
contentDescendants.forEach((descendant) => {
|
|
6665
|
+
if (shouldRegisterContent(descendant, contentDiscovery, registeredIds)) {
|
|
6666
|
+
queueContentRegistration(descendant);
|
|
6667
|
+
}
|
|
6668
|
+
});
|
|
6669
|
+
}
|
|
4832
6670
|
}
|
|
4833
6671
|
});
|
|
4834
6672
|
mutation.removedNodes.forEach((node) => {
|
|
@@ -4837,17 +6675,32 @@ function useAutoRegister(options = {}) {
|
|
|
4837
6675
|
if (registeredElementsRef.current.has(element)) {
|
|
4838
6676
|
unregisterElement(element);
|
|
4839
6677
|
}
|
|
6678
|
+
if (registeredContentElementsRef.current.has(element)) {
|
|
6679
|
+
unregisterContentElement(element);
|
|
6680
|
+
}
|
|
4840
6681
|
const descendants = element.querySelectorAll("*");
|
|
4841
6682
|
descendants.forEach((descendant) => {
|
|
4842
6683
|
if (registeredElementsRef.current.has(descendant)) {
|
|
4843
6684
|
unregisterElement(descendant);
|
|
4844
6685
|
}
|
|
6686
|
+
if (registeredContentElementsRef.current.has(descendant)) {
|
|
6687
|
+
unregisterContentElement(descendant);
|
|
6688
|
+
}
|
|
4845
6689
|
});
|
|
4846
6690
|
}
|
|
4847
6691
|
});
|
|
4848
6692
|
});
|
|
4849
6693
|
},
|
|
4850
|
-
[
|
|
6694
|
+
[
|
|
6695
|
+
shouldRegister,
|
|
6696
|
+
queueRegistration,
|
|
6697
|
+
unregisterElement,
|
|
6698
|
+
includeSelectors,
|
|
6699
|
+
contentEnabled,
|
|
6700
|
+
contentDiscovery,
|
|
6701
|
+
queueContentRegistration,
|
|
6702
|
+
unregisterContentElement
|
|
6703
|
+
]
|
|
4851
6704
|
);
|
|
4852
6705
|
react.useEffect(() => {
|
|
4853
6706
|
if (!enabled || !bridge?.registry) return;
|
|
@@ -4863,10 +6716,17 @@ function useAutoRegister(options = {}) {
|
|
|
4863
6716
|
if (debounceTimeoutRef.current) {
|
|
4864
6717
|
clearTimeout(debounceTimeoutRef.current);
|
|
4865
6718
|
}
|
|
6719
|
+
if (contentDebounceTimeoutRef.current) {
|
|
6720
|
+
clearTimeout(contentDebounceTimeoutRef.current);
|
|
6721
|
+
}
|
|
4866
6722
|
registeredElementsRef.current.forEach((id, _element) => {
|
|
4867
6723
|
bridge.registry.unregisterElement(id);
|
|
4868
6724
|
});
|
|
4869
6725
|
registeredElementsRef.current.clear();
|
|
6726
|
+
registeredContentElementsRef.current.forEach((id, _element) => {
|
|
6727
|
+
bridge.registry.unregisterElement(id);
|
|
6728
|
+
});
|
|
6729
|
+
registeredContentElementsRef.current.clear();
|
|
4870
6730
|
};
|
|
4871
6731
|
}, [enabled, bridge, root, scanAndRegister, handleMutations]);
|
|
4872
6732
|
}
|
|
@@ -4881,7 +6741,8 @@ function AutoRegisterProvider({
|
|
|
4881
6741
|
excludeSelectors = [],
|
|
4882
6742
|
generateId: generateId4,
|
|
4883
6743
|
onRegister,
|
|
4884
|
-
onUnregister
|
|
6744
|
+
onUnregister,
|
|
6745
|
+
contentDiscovery
|
|
4885
6746
|
}) {
|
|
4886
6747
|
const containerRef = react.useRef(null);
|
|
4887
6748
|
useAutoRegister({
|
|
@@ -4894,7 +6755,8 @@ function AutoRegisterProvider({
|
|
|
4894
6755
|
excludeSelectors,
|
|
4895
6756
|
generateId: generateId4,
|
|
4896
6757
|
onRegister,
|
|
4897
|
-
onUnregister
|
|
6758
|
+
onUnregister,
|
|
6759
|
+
contentDiscovery
|
|
4898
6760
|
});
|
|
4899
6761
|
if (scopeToChildren) {
|
|
4900
6762
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: { display: "contents" }, children });
|
|
@@ -4902,6 +6764,253 @@ function AutoRegisterProvider({
|
|
|
4902
6764
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
4903
6765
|
}
|
|
4904
6766
|
|
|
6767
|
+
// src/annotations/types.ts
|
|
6768
|
+
var ANNOTATION_CONFIG_VERSION = "1.0.0";
|
|
6769
|
+
|
|
6770
|
+
// src/annotations/store.ts
|
|
6771
|
+
var AnnotationStore = class {
|
|
6772
|
+
constructor() {
|
|
6773
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6774
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
6775
|
+
}
|
|
6776
|
+
/**
|
|
6777
|
+
* Get an annotation by element ID.
|
|
6778
|
+
*/
|
|
6779
|
+
get(elementId) {
|
|
6780
|
+
return this.store.get(elementId);
|
|
6781
|
+
}
|
|
6782
|
+
/**
|
|
6783
|
+
* Get all annotations as a record.
|
|
6784
|
+
*/
|
|
6785
|
+
getAll() {
|
|
6786
|
+
const result = {};
|
|
6787
|
+
for (const [id, annotation] of this.store) {
|
|
6788
|
+
result[id] = annotation;
|
|
6789
|
+
}
|
|
6790
|
+
return result;
|
|
6791
|
+
}
|
|
6792
|
+
/**
|
|
6793
|
+
* Set an annotation for an element. Auto-sets `updatedAt`.
|
|
6794
|
+
*/
|
|
6795
|
+
set(elementId, annotation) {
|
|
6796
|
+
const updated = {
|
|
6797
|
+
...annotation,
|
|
6798
|
+
updatedAt: Date.now()
|
|
6799
|
+
};
|
|
6800
|
+
this.store.set(elementId, updated);
|
|
6801
|
+
this.emit({
|
|
6802
|
+
type: "annotation:set",
|
|
6803
|
+
elementId,
|
|
6804
|
+
annotation: updated,
|
|
6805
|
+
timestamp: Date.now()
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
/**
|
|
6809
|
+
* Delete an annotation by element ID.
|
|
6810
|
+
*
|
|
6811
|
+
* @returns true if the annotation existed and was deleted
|
|
6812
|
+
*/
|
|
6813
|
+
delete(elementId) {
|
|
6814
|
+
const existed = this.store.delete(elementId);
|
|
6815
|
+
if (existed) {
|
|
6816
|
+
this.emit({
|
|
6817
|
+
type: "annotation:deleted",
|
|
6818
|
+
elementId,
|
|
6819
|
+
timestamp: Date.now()
|
|
6820
|
+
});
|
|
6821
|
+
}
|
|
6822
|
+
return existed;
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* Check if an annotation exists for an element.
|
|
6826
|
+
*/
|
|
6827
|
+
has(elementId) {
|
|
6828
|
+
return this.store.has(elementId);
|
|
6829
|
+
}
|
|
6830
|
+
/**
|
|
6831
|
+
* Get the number of stored annotations.
|
|
6832
|
+
*/
|
|
6833
|
+
get count() {
|
|
6834
|
+
return this.store.size;
|
|
6835
|
+
}
|
|
6836
|
+
/**
|
|
6837
|
+
* Clear all annotations.
|
|
6838
|
+
*/
|
|
6839
|
+
clear() {
|
|
6840
|
+
this.store.clear();
|
|
6841
|
+
this.emit({
|
|
6842
|
+
type: "annotation:cleared",
|
|
6843
|
+
timestamp: Date.now()
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
/**
|
|
6847
|
+
* Import annotations from a config object.
|
|
6848
|
+
*
|
|
6849
|
+
* Merges with existing annotations (new values overwrite per element ID).
|
|
6850
|
+
*
|
|
6851
|
+
* @returns Number of annotations imported
|
|
6852
|
+
*
|
|
6853
|
+
* @example
|
|
6854
|
+
* ```ts
|
|
6855
|
+
* const config: AnnotationConfig = {
|
|
6856
|
+
* version: '1.0.0',
|
|
6857
|
+
* annotations: {
|
|
6858
|
+
* 'btn-1': { description: 'Submit button', tags: ['form'] },
|
|
6859
|
+
* 'input-1': { description: 'Name field' },
|
|
6860
|
+
* },
|
|
6861
|
+
* };
|
|
6862
|
+
* const count = store.importConfig(config); // 2
|
|
6863
|
+
* ```
|
|
6864
|
+
*/
|
|
6865
|
+
importConfig(config) {
|
|
6866
|
+
let count = 0;
|
|
6867
|
+
for (const [id, annotation] of Object.entries(config.annotations)) {
|
|
6868
|
+
this.store.set(id, {
|
|
6869
|
+
...annotation,
|
|
6870
|
+
updatedAt: annotation.updatedAt ?? Date.now()
|
|
6871
|
+
});
|
|
6872
|
+
count++;
|
|
6873
|
+
}
|
|
6874
|
+
this.emit({
|
|
6875
|
+
type: "annotation:imported",
|
|
6876
|
+
count,
|
|
6877
|
+
timestamp: Date.now()
|
|
6878
|
+
});
|
|
6879
|
+
return count;
|
|
6880
|
+
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Export all annotations as a config object.
|
|
6883
|
+
*
|
|
6884
|
+
* The returned object can be serialized to JSON and saved to a file,
|
|
6885
|
+
* then later re-imported with {@link importConfig}.
|
|
6886
|
+
*
|
|
6887
|
+
* @param metadata - Optional metadata to include (appName, description, etc.)
|
|
6888
|
+
* @returns AnnotationConfig with all current annotations
|
|
6889
|
+
*
|
|
6890
|
+
* @example
|
|
6891
|
+
* ```ts
|
|
6892
|
+
* const config = store.exportConfig({ appName: 'MyApp' });
|
|
6893
|
+
* // config.version === '1.0.0'
|
|
6894
|
+
* // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
|
|
6895
|
+
* // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
|
|
6896
|
+
*
|
|
6897
|
+
* // Save to file
|
|
6898
|
+
* fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
|
|
6899
|
+
* ```
|
|
6900
|
+
*/
|
|
6901
|
+
exportConfig(metadata) {
|
|
6902
|
+
return {
|
|
6903
|
+
version: ANNOTATION_CONFIG_VERSION,
|
|
6904
|
+
annotations: this.getAll(),
|
|
6905
|
+
metadata: {
|
|
6906
|
+
...metadata,
|
|
6907
|
+
exportedAt: Date.now()
|
|
6908
|
+
}
|
|
6909
|
+
};
|
|
6910
|
+
}
|
|
6911
|
+
/**
|
|
6912
|
+
* Compute annotation coverage against a set of known element IDs.
|
|
6913
|
+
*
|
|
6914
|
+
* Compares the store's annotations against the provided list of element IDs
|
|
6915
|
+
* to determine what percentage of elements have been annotated.
|
|
6916
|
+
*
|
|
6917
|
+
* @param allElementIds - Array of all known element IDs in the UI
|
|
6918
|
+
* @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
|
|
6919
|
+
*
|
|
6920
|
+
* @example
|
|
6921
|
+
* ```ts
|
|
6922
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
6923
|
+
* store.set('input-1', { description: 'Name' });
|
|
6924
|
+
*
|
|
6925
|
+
* const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
|
|
6926
|
+
* // coverage.totalElements === 4
|
|
6927
|
+
* // coverage.annotatedElements === 2
|
|
6928
|
+
* // coverage.coveragePercent === 50
|
|
6929
|
+
* // coverage.annotatedIds === ['btn-1', 'input-1']
|
|
6930
|
+
* // coverage.unannotatedIds === ['input-2', 'link-1']
|
|
6931
|
+
* ```
|
|
6932
|
+
*/
|
|
6933
|
+
getCoverage(allElementIds) {
|
|
6934
|
+
const annotatedIds = [];
|
|
6935
|
+
const unannotatedIds = [];
|
|
6936
|
+
for (const id of allElementIds) {
|
|
6937
|
+
if (this.store.has(id)) {
|
|
6938
|
+
annotatedIds.push(id);
|
|
6939
|
+
} else {
|
|
6940
|
+
unannotatedIds.push(id);
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
const total = allElementIds.length;
|
|
6944
|
+
return {
|
|
6945
|
+
totalElements: total,
|
|
6946
|
+
annotatedElements: annotatedIds.length,
|
|
6947
|
+
coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
|
|
6948
|
+
annotatedIds,
|
|
6949
|
+
unannotatedIds,
|
|
6950
|
+
timestamp: Date.now()
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6953
|
+
/**
|
|
6954
|
+
* Subscribe to annotation events.
|
|
6955
|
+
*
|
|
6956
|
+
* The listener is called whenever annotations are set, deleted, imported,
|
|
6957
|
+
* or cleared. Returns an unsubscribe function to stop listening.
|
|
6958
|
+
*
|
|
6959
|
+
* @param listener - Callback function receiving {@link AnnotationEvent} objects
|
|
6960
|
+
* @returns Unsubscribe function - call it to remove the listener
|
|
6961
|
+
*
|
|
6962
|
+
* @example
|
|
6963
|
+
* ```ts
|
|
6964
|
+
* const unsubscribe = store.on((event) => {
|
|
6965
|
+
* if (event.type === 'annotation:set') {
|
|
6966
|
+
* console.log(`Element ${event.elementId} annotated:`, event.annotation);
|
|
6967
|
+
* }
|
|
6968
|
+
* });
|
|
6969
|
+
*
|
|
6970
|
+
* store.set('btn-1', { description: 'Submit' });
|
|
6971
|
+
* // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
|
|
6972
|
+
*
|
|
6973
|
+
* unsubscribe(); // Stop listening
|
|
6974
|
+
* ```
|
|
6975
|
+
*/
|
|
6976
|
+
on(listener) {
|
|
6977
|
+
this.listeners.add(listener);
|
|
6978
|
+
return () => {
|
|
6979
|
+
this.listeners.delete(listener);
|
|
6980
|
+
};
|
|
6981
|
+
}
|
|
6982
|
+
/**
|
|
6983
|
+
* Emit an event to all listeners.
|
|
6984
|
+
*/
|
|
6985
|
+
emit(event) {
|
|
6986
|
+
for (const listener of this.listeners) {
|
|
6987
|
+
try {
|
|
6988
|
+
listener(event);
|
|
6989
|
+
} catch {
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
};
|
|
6994
|
+
var globalStore2 = null;
|
|
6995
|
+
function getGlobalAnnotationStore() {
|
|
6996
|
+
if (!globalStore2) {
|
|
6997
|
+
globalStore2 = new AnnotationStore();
|
|
6998
|
+
}
|
|
6999
|
+
return globalStore2;
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
// src/react/useUIAnnotation.ts
|
|
7003
|
+
function useUIAnnotation(elementId, annotation) {
|
|
7004
|
+
const serializedRef = react.useRef("");
|
|
7005
|
+
react.useEffect(() => {
|
|
7006
|
+
const serialized = JSON.stringify(annotation);
|
|
7007
|
+
if (serialized !== serializedRef.current) {
|
|
7008
|
+
serializedRef.current = serialized;
|
|
7009
|
+
getGlobalAnnotationStore().set(elementId, annotation);
|
|
7010
|
+
}
|
|
7011
|
+
}, [elementId, annotation]);
|
|
7012
|
+
}
|
|
7013
|
+
|
|
4905
7014
|
exports.AutoRegisterProvider = AutoRegisterProvider;
|
|
4906
7015
|
exports.UIBridgeProvider = UIBridgeProvider;
|
|
4907
7016
|
exports.useActiveStates = useActiveStates;
|
|
@@ -4911,6 +7020,7 @@ exports.useCanNavigateTo = useCanNavigateTo;
|
|
|
4911
7020
|
exports.useNavigationPath = useNavigationPath;
|
|
4912
7021
|
exports.useStateSnapshot = useStateSnapshot;
|
|
4913
7022
|
exports.useTransitions = useTransitions;
|
|
7023
|
+
exports.useUIAnnotation = useUIAnnotation;
|
|
4914
7024
|
exports.useUIBridge = useUIBridge;
|
|
4915
7025
|
exports.useUIBridgeContext = useUIBridgeContext;
|
|
4916
7026
|
exports.useUIBridgeOptional = useUIBridgeOptional;
|