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