@zibby/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- package/templates/register-nodes.js +24 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Enrichment Pipeline Component
|
|
3
|
+
* Each enricher adds specific data to events without affecting others
|
|
4
|
+
*/
|
|
5
|
+
export class EventEnricher {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.enabled = config.enabled !== false; // Default enabled
|
|
9
|
+
this.priority = config.priority || 50; // Default priority
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get enricher name for logging
|
|
14
|
+
*/
|
|
15
|
+
getName() {
|
|
16
|
+
throw new Error('EventEnricher.getName() must be implemented');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if this enricher can run (dependencies met, etc)
|
|
21
|
+
* @param {Object} context - { page, element, event, session }
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
canEnrich(_context) {
|
|
25
|
+
return this.enabled;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enrich the event with additional data
|
|
30
|
+
* @param {Object} event - The MCP event to enrich
|
|
31
|
+
* @param {Object} context - { page, element, ref }
|
|
32
|
+
* @returns {Promise<Object>} - Enriched data to merge into event
|
|
33
|
+
*/
|
|
34
|
+
async enrich(_event, _context) {
|
|
35
|
+
throw new Error('EventEnricher.enrich() must be implemented');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handle errors gracefully (never break the pipeline)
|
|
40
|
+
* @param {Error} error
|
|
41
|
+
* @param {Object} event
|
|
42
|
+
*/
|
|
43
|
+
handleError(error, event) {
|
|
44
|
+
console.warn(`[${this.getName()}] Enrichment failed for event ${event.type}:`, error.message);
|
|
45
|
+
return null; // Return null = skip this enrichment
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default EventEnricher;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility Enricher - Captures accessibility tree data and structural hash
|
|
3
|
+
*/
|
|
4
|
+
import { EventEnricher } from '../base.js';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
|
|
7
|
+
export class AccessibilityEnricher extends EventEnricher {
|
|
8
|
+
getName() {
|
|
9
|
+
return 'AccessibilityEnricher';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getPriority() {
|
|
13
|
+
return 100; // Highest priority - most stable identifier
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
canEnrich(context) {
|
|
17
|
+
if (!this.enabled) return false;
|
|
18
|
+
if (!context.element) return false;
|
|
19
|
+
if (!context.event) return false;
|
|
20
|
+
|
|
21
|
+
return ['click', 'fill', 'type', 'selectOption', 'hover'].includes(context.event.type);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async enrich(event, context) {
|
|
25
|
+
try {
|
|
26
|
+
const { page, element } = context;
|
|
27
|
+
|
|
28
|
+
// Get accessibility snapshot of entire page
|
|
29
|
+
const snapshot = await page.accessibility.snapshot();
|
|
30
|
+
|
|
31
|
+
// Find the target element in AX tree
|
|
32
|
+
const axNode = await this.findAxNode(element, snapshot);
|
|
33
|
+
if (!axNode) return null;
|
|
34
|
+
|
|
35
|
+
// Get parent and siblings
|
|
36
|
+
const axContext = await this.getAxContext(axNode, snapshot);
|
|
37
|
+
|
|
38
|
+
// Calculate structural hash (hash of AX subtree)
|
|
39
|
+
const axTreeHash = this.hashAxSubtree(axNode);
|
|
40
|
+
const axPathHash = this.hashAxPath(axContext.path);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
accessibility: {
|
|
44
|
+
role: axNode.role,
|
|
45
|
+
name: axNode.name || '',
|
|
46
|
+
level: axContext.level,
|
|
47
|
+
parent: axContext.parent,
|
|
48
|
+
siblings: axContext.siblings,
|
|
49
|
+
axTreeHash,
|
|
50
|
+
axPathHash
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return this.handleError(error, event);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find AX node for given element
|
|
60
|
+
*/
|
|
61
|
+
async findAxNode(element, snapshot) {
|
|
62
|
+
// Get element's aria attributes for matching
|
|
63
|
+
const ariaProps = await element.evaluate(el => ({
|
|
64
|
+
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
65
|
+
name: el.getAttribute('aria-label') || el.textContent?.trim() || '',
|
|
66
|
+
tagName: el.tagName.toLowerCase()
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// Search AX tree for matching node
|
|
70
|
+
return this.searchAxTree(snapshot, ariaProps);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recursively search AX tree
|
|
75
|
+
*/
|
|
76
|
+
searchAxTree(node, props) {
|
|
77
|
+
if (!node) return null;
|
|
78
|
+
|
|
79
|
+
// Match by role and name
|
|
80
|
+
if (node.role === props.role &&
|
|
81
|
+
(node.name || '').includes(props.name.substring(0, 20))) {
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Search children
|
|
86
|
+
if (node.children) {
|
|
87
|
+
for (const child of node.children) {
|
|
88
|
+
const found = this.searchAxTree(child, props);
|
|
89
|
+
if (found) return found;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get context (parent, siblings, path)
|
|
98
|
+
*/
|
|
99
|
+
getAxContext(node, snapshot) {
|
|
100
|
+
const context = {
|
|
101
|
+
level: 0,
|
|
102
|
+
parent: null,
|
|
103
|
+
siblings: [],
|
|
104
|
+
path: []
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Walk up tree to find parent and siblings
|
|
108
|
+
const parent = this.findParent(node, snapshot);
|
|
109
|
+
if (parent) {
|
|
110
|
+
context.parent = { role: parent.role, name: parent.name };
|
|
111
|
+
context.siblings = (parent.children || [])
|
|
112
|
+
.filter(c => c !== node)
|
|
113
|
+
.map(c => ({ role: c.role, name: c.name }))
|
|
114
|
+
.slice(0, 3); // Limit siblings
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calculate level (depth in tree)
|
|
118
|
+
context.level = this.calculateLevel(node, snapshot);
|
|
119
|
+
|
|
120
|
+
// Build path from root to node
|
|
121
|
+
context.path = this.buildPath(node, snapshot);
|
|
122
|
+
|
|
123
|
+
return context;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
findParent(node, tree, current = tree) {
|
|
127
|
+
if (!current || !current.children) return null;
|
|
128
|
+
|
|
129
|
+
if (current.children.includes(node)) {
|
|
130
|
+
return current;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const child of current.children) {
|
|
134
|
+
const found = this.findParent(node, tree, child);
|
|
135
|
+
if (found) return found;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
calculateLevel(node, tree, current = tree, level = 0) {
|
|
142
|
+
if (current === node) return level;
|
|
143
|
+
|
|
144
|
+
if (current.children) {
|
|
145
|
+
for (const child of current.children) {
|
|
146
|
+
const found = this.calculateLevel(node, tree, child, level + 1);
|
|
147
|
+
if (found >= 0) return found;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return -1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
buildPath(node, tree, current = tree, path = []) {
|
|
155
|
+
if (current === node) {
|
|
156
|
+
return [...path, { role: current.role, name: current.name }];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (current.children) {
|
|
160
|
+
for (const child of current.children) {
|
|
161
|
+
const found = this.buildPath(node, tree, child, [
|
|
162
|
+
...path,
|
|
163
|
+
{ role: current.role, name: current.name }
|
|
164
|
+
]);
|
|
165
|
+
if (found) return found;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Hash AX subtree for stability detection
|
|
174
|
+
*/
|
|
175
|
+
hashAxSubtree(node) {
|
|
176
|
+
const structure = JSON.stringify({
|
|
177
|
+
role: node.role,
|
|
178
|
+
name: node.name,
|
|
179
|
+
children: (node.children || []).map(c => ({
|
|
180
|
+
role: c.role,
|
|
181
|
+
name: c.name
|
|
182
|
+
}))
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return crypto.createHash('md5').update(structure).digest('hex').substring(0, 12);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Hash AX path from root to node
|
|
190
|
+
*/
|
|
191
|
+
hashAxPath(path) {
|
|
192
|
+
const pathStr = path.map(p => `${p.role}:${p.name}`).join('/');
|
|
193
|
+
return crypto.createHash('md5').update(pathStr).digest('hex').substring(0, 12);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default AccessibilityEnricher;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Enricher - Captures DOM path, XPath, and element attributes
|
|
3
|
+
* This file contains browser code executed via element.evaluate() where browser globals are available
|
|
4
|
+
*/
|
|
5
|
+
/* global document, window */
|
|
6
|
+
import { EventEnricher } from '../base.js';
|
|
7
|
+
|
|
8
|
+
export class DOMEnricher extends EventEnricher {
|
|
9
|
+
getName() {
|
|
10
|
+
return 'DOMEnricher';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getPriority() {
|
|
14
|
+
return 85; // High priority - structural data
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
canEnrich(context) {
|
|
18
|
+
if (!this.enabled) return false;
|
|
19
|
+
if (!context.element) return false;
|
|
20
|
+
if (!context.event) return false;
|
|
21
|
+
|
|
22
|
+
return ['click', 'fill', 'type', 'selectOption', 'hover'].includes(context.event.type);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async enrich(event, context) {
|
|
26
|
+
try {
|
|
27
|
+
const { element } = context;
|
|
28
|
+
|
|
29
|
+
// Get DOM path, XPath, and attributes
|
|
30
|
+
const domData = await element.evaluate(el => {
|
|
31
|
+
// Build CSS path
|
|
32
|
+
const getCssPath = (innerEl) => {
|
|
33
|
+
const path = [];
|
|
34
|
+
let current = innerEl;
|
|
35
|
+
|
|
36
|
+
while (current && current !== document.body) {
|
|
37
|
+
let selector = current.tagName.toLowerCase();
|
|
38
|
+
|
|
39
|
+
// Add nth-child if there are siblings of same type
|
|
40
|
+
const parent = current.parentElement;
|
|
41
|
+
if (parent) {
|
|
42
|
+
const siblings = Array.from(parent.children).filter(
|
|
43
|
+
child => child.tagName === current.tagName
|
|
44
|
+
);
|
|
45
|
+
if (siblings.length > 1) {
|
|
46
|
+
const index = siblings.indexOf(current) + 1;
|
|
47
|
+
selector += `:nth-child(${index})`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
path.unshift(selector);
|
|
52
|
+
current = current.parentElement;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `body > ${path.join(' > ')}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Build XPath
|
|
59
|
+
const getXPath = (innerEl) => {
|
|
60
|
+
const path = [];
|
|
61
|
+
let current = innerEl;
|
|
62
|
+
|
|
63
|
+
while (current && current !== document.body) {
|
|
64
|
+
let index = 1;
|
|
65
|
+
let sibling = current.previousSibling;
|
|
66
|
+
|
|
67
|
+
while (sibling) {
|
|
68
|
+
if (sibling.nodeType === 1 && sibling.tagName === current.tagName) {
|
|
69
|
+
index++;
|
|
70
|
+
}
|
|
71
|
+
sibling = sibling.previousSibling;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tag = current.tagName.toLowerCase();
|
|
75
|
+
path.unshift(`${tag}[${index}]`);
|
|
76
|
+
current = current.parentElement;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return `/html/body/${path.join('/')}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Get all attributes
|
|
83
|
+
const attributes = {};
|
|
84
|
+
for (const attr of el.attributes) {
|
|
85
|
+
attributes[attr.name] = attr.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get computed style (only essential properties)
|
|
89
|
+
const computed = window.getComputedStyle(el);
|
|
90
|
+
const state = {
|
|
91
|
+
display: computed.display,
|
|
92
|
+
visibility: computed.visibility,
|
|
93
|
+
opacity: computed.opacity,
|
|
94
|
+
pointerEvents: computed.pointerEvents
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Calculate depth
|
|
98
|
+
let depth = 0;
|
|
99
|
+
let parent = el.parentElement;
|
|
100
|
+
while (parent) {
|
|
101
|
+
depth++;
|
|
102
|
+
parent = parent.parentElement;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
path: getCssPath(el),
|
|
107
|
+
xpath: getXPath(el),
|
|
108
|
+
depth,
|
|
109
|
+
parent: el.parentElement ? el.parentElement.tagName.toLowerCase() : null,
|
|
110
|
+
tagName: el.tagName.toLowerCase(),
|
|
111
|
+
attributes,
|
|
112
|
+
state: {
|
|
113
|
+
visible: computed.display !== 'none' && computed.visibility !== 'hidden',
|
|
114
|
+
enabled: !el.disabled,
|
|
115
|
+
focused: document.activeElement === el,
|
|
116
|
+
...state
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
dom: {
|
|
123
|
+
path: domData.path,
|
|
124
|
+
xpath: domData.xpath,
|
|
125
|
+
depth: domData.depth,
|
|
126
|
+
parent: domData.parent,
|
|
127
|
+
selector: this.buildSmartSelector(domData)
|
|
128
|
+
},
|
|
129
|
+
attributes: domData.attributes,
|
|
130
|
+
state: domData.state
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return this.handleError(error, event);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build a smart CSS selector from DOM data
|
|
139
|
+
*/
|
|
140
|
+
buildSmartSelector(domData) {
|
|
141
|
+
let selector = domData.tagName;
|
|
142
|
+
|
|
143
|
+
// Add ID if available
|
|
144
|
+
if (domData.attributes.id) {
|
|
145
|
+
return `#${domData.attributes.id}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add data-test-id if available
|
|
149
|
+
if (domData.attributes['data-test-id']) {
|
|
150
|
+
return `[data-test-id="${domData.attributes['data-test-id']}"]`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add class if available
|
|
154
|
+
if (domData.attributes.class) {
|
|
155
|
+
const classes = domData.attributes.class.split(' ')
|
|
156
|
+
.filter(c => c && !c.match(/^(active|focus|hover|disabled)/)); // Skip state classes
|
|
157
|
+
if (classes.length > 0) {
|
|
158
|
+
selector += `.${classes.slice(0, 2).join('.')}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Add parent context
|
|
163
|
+
if (domData.parent) {
|
|
164
|
+
selector = `${domData.parent} > ${selector}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return selector;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default DOMEnricher;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page State Enricher - Captures network and page stability state
|
|
3
|
+
* This file contains browser code executed via page.evaluate() where browser globals are available
|
|
4
|
+
*/
|
|
5
|
+
/* global document, MutationObserver */
|
|
6
|
+
import { EventEnricher } from '../base.js';
|
|
7
|
+
|
|
8
|
+
export class PageStateEnricher extends EventEnricher {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
super(config);
|
|
11
|
+
this.pendingRequests = new Set();
|
|
12
|
+
this.setupNetworkTracking = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getName() {
|
|
16
|
+
return 'PageStateEnricher';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getPriority() {
|
|
20
|
+
return 95; // Very high priority - critical for timing
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
canEnrich(context) {
|
|
24
|
+
return this.enabled && context.page;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Setup network request tracking (call once per page)
|
|
29
|
+
*/
|
|
30
|
+
async setupTracking(page) {
|
|
31
|
+
if (this.setupNetworkTracking) return;
|
|
32
|
+
|
|
33
|
+
page.on('request', (request) => {
|
|
34
|
+
if (['document', 'xhr', 'fetch'].includes(request.resourceType())) {
|
|
35
|
+
this.pendingRequests.add(request.url());
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
page.on('requestfinished', (request) => {
|
|
40
|
+
this.pendingRequests.delete(request.url());
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
page.on('requestfailed', (request) => {
|
|
44
|
+
this.pendingRequests.delete(request.url());
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.setupNetworkTracking = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async enrich(event, context) {
|
|
51
|
+
try {
|
|
52
|
+
const { page } = context;
|
|
53
|
+
|
|
54
|
+
// Setup tracking if not done
|
|
55
|
+
await this.setupTracking(page);
|
|
56
|
+
|
|
57
|
+
// Get page load state
|
|
58
|
+
const pageState = await page.evaluate(() => ({
|
|
59
|
+
readyState: document.readyState,
|
|
60
|
+
domContentLoaded: document.readyState !== 'loading',
|
|
61
|
+
loadComplete: document.readyState === 'complete',
|
|
62
|
+
url: document.location.href
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Check if DOM is stable (no mutations for 500ms)
|
|
66
|
+
const domStable = await this.checkDOMStability(page);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
page: {
|
|
70
|
+
networkIdle: this.pendingRequests.size === 0,
|
|
71
|
+
pendingRequests: this.pendingRequests.size,
|
|
72
|
+
domStable,
|
|
73
|
+
...pageState
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return this.handleError(error, event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if DOM hasn't mutated for 500ms
|
|
83
|
+
*/
|
|
84
|
+
async checkDOMStability(page, timeoutMs = 500) {
|
|
85
|
+
try {
|
|
86
|
+
const stable = await page.evaluate((timeout) => {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
let timer;
|
|
89
|
+
let mutations = 0;
|
|
90
|
+
|
|
91
|
+
const observer = new MutationObserver(() => {
|
|
92
|
+
mutations++;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
timer = setTimeout(() => {
|
|
95
|
+
observer.disconnect();
|
|
96
|
+
resolve(mutations === 0);
|
|
97
|
+
}, timeout);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
observer.observe(document.body, {
|
|
101
|
+
childList: true,
|
|
102
|
+
subtree: true,
|
|
103
|
+
attributes: true
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Initial timer
|
|
107
|
+
timer = setTimeout(() => {
|
|
108
|
+
observer.disconnect();
|
|
109
|
+
resolve(true);
|
|
110
|
+
}, timeout);
|
|
111
|
+
});
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
|
|
114
|
+
return stable;
|
|
115
|
+
} catch (_error) {
|
|
116
|
+
return false; // Assume not stable on error
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reset tracking for new page navigation
|
|
122
|
+
*/
|
|
123
|
+
reset() {
|
|
124
|
+
this.pendingRequests.clear();
|
|
125
|
+
this.setupNetworkTracking = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default PageStateEnricher;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Enricher - Captures element bounding box and viewport state
|
|
3
|
+
* This file contains browser code executed via element.evaluate() where browser globals are available
|
|
4
|
+
*/
|
|
5
|
+
/* global window */
|
|
6
|
+
import { EventEnricher } from '../base.js';
|
|
7
|
+
|
|
8
|
+
export class PositionEnricher extends EventEnricher {
|
|
9
|
+
getName() {
|
|
10
|
+
return 'PositionEnricher';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getPriority() {
|
|
14
|
+
return 90; // High priority - cheap and useful
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
canEnrich(context) {
|
|
18
|
+
// Only enrich interactive actions that have an element
|
|
19
|
+
if (!this.enabled) return false;
|
|
20
|
+
if (!context.element) return false;
|
|
21
|
+
if (!context.event) return false;
|
|
22
|
+
|
|
23
|
+
return ['click', 'fill', 'type', 'selectOption', 'hover'].includes(context.event.type);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async enrich(event, context) {
|
|
27
|
+
try {
|
|
28
|
+
const { page, element } = context;
|
|
29
|
+
|
|
30
|
+
// Get bounding box
|
|
31
|
+
const box = await element.boundingBox();
|
|
32
|
+
if (!box) return null; // Element not visible
|
|
33
|
+
|
|
34
|
+
// Get viewport state
|
|
35
|
+
const viewport = await page.evaluate(() => ({
|
|
36
|
+
scrollX: window.scrollX,
|
|
37
|
+
scrollY: window.scrollY,
|
|
38
|
+
width: window.innerWidth,
|
|
39
|
+
height: window.innerHeight
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Check if in viewport
|
|
43
|
+
const inViewport = (
|
|
44
|
+
box.y >= viewport.scrollY &&
|
|
45
|
+
box.y + box.height <= viewport.scrollY + viewport.height &&
|
|
46
|
+
box.x >= 0 &&
|
|
47
|
+
box.x + box.width <= viewport.width
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
position: {
|
|
52
|
+
boundingBox: box,
|
|
53
|
+
viewport,
|
|
54
|
+
inViewport,
|
|
55
|
+
centerPoint: {
|
|
56
|
+
x: Math.round(box.x + box.width / 2),
|
|
57
|
+
y: Math.round(box.y + box.height / 2)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return this.handleError(error, event);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default PositionEnricher;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Enrichment System
|
|
3
|
+
* Modular pipeline for adding data to MCP events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { EventEnricher } from './base.js';
|
|
7
|
+
export { EnrichmentPipeline } from './pipeline.js';
|
|
8
|
+
|
|
9
|
+
// Enrichers
|
|
10
|
+
export { PositionEnricher } from './enrichers/position-enricher.js';
|
|
11
|
+
export { AccessibilityEnricher } from './enrichers/accessibility-enricher.js';
|
|
12
|
+
export { PageStateEnricher } from './enrichers/page-state-enricher.js';
|
|
13
|
+
export { DOMEnricher } from './enrichers/dom-enricher.js';
|
|
14
|
+
export { MCPRefEnricher } from './mcp-ref-enricher.js';
|
|
15
|
+
export { TraceTextEnricher } from './trace-text-enricher.js';
|
|
16
|
+
|
|
17
|
+
import { EnrichmentPipeline } from './pipeline.js';
|
|
18
|
+
import { PositionEnricher } from './enrichers/position-enricher.js';
|
|
19
|
+
import { AccessibilityEnricher } from './enrichers/accessibility-enricher.js';
|
|
20
|
+
import { PageStateEnricher } from './enrichers/page-state-enricher.js';
|
|
21
|
+
import { DOMEnricher } from './enrichers/dom-enricher.js';
|
|
22
|
+
import { MCPRefEnricher } from './mcp-ref-enricher.js';
|
|
23
|
+
import { TraceTextEnricher } from './trace-text-enricher.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a default enrichment pipeline with all enrichers
|
|
27
|
+
* @param {Object} config - Configuration options
|
|
28
|
+
* @param {boolean} config.enablePosition - Enable position enricher (default: true)
|
|
29
|
+
* @param {boolean} config.enableAccessibility - Enable accessibility enricher (default: true)
|
|
30
|
+
* @param {boolean} config.enablePageState - Enable page state enricher (default: true)
|
|
31
|
+
* @param {boolean} config.enableDOM - Enable DOM enricher (default: true)
|
|
32
|
+
* @returns {EnrichmentPipeline}
|
|
33
|
+
*/
|
|
34
|
+
export function createDefaultPipeline(config = {}) {
|
|
35
|
+
const pipeline = new EnrichmentPipeline(config);
|
|
36
|
+
|
|
37
|
+
// Register enrichers (they'll auto-sort by priority)
|
|
38
|
+
// MCPRefEnricher has highest priority (200) - captures exact MCP refs
|
|
39
|
+
if (config.enableMCPRef !== false) {
|
|
40
|
+
pipeline.register(new MCPRefEnricher(config));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// TraceTextEnricher (190) - extracts ACTUAL text from trace (for Chinese/multi-language)
|
|
44
|
+
if (config.enableTraceText !== false) {
|
|
45
|
+
pipeline.register(new TraceTextEnricher(config));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (config.enableAccessibility !== false) {
|
|
49
|
+
pipeline.register(new AccessibilityEnricher(config));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config.enablePageState !== false) {
|
|
53
|
+
pipeline.register(new PageStateEnricher(config));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.enablePosition !== false) {
|
|
57
|
+
pipeline.register(new PositionEnricher(config));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.enableDOM !== false) {
|
|
61
|
+
pipeline.register(new DOMEnricher(config));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return pipeline;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a minimal pipeline (only critical enrichers)
|
|
69
|
+
* @param {Object} config
|
|
70
|
+
* @returns {EnrichmentPipeline}
|
|
71
|
+
*/
|
|
72
|
+
export function createMinimalPipeline(config = {}) {
|
|
73
|
+
const pipeline = new EnrichmentPipeline(config);
|
|
74
|
+
|
|
75
|
+
// Only most critical enrichers
|
|
76
|
+
pipeline.register(new AccessibilityEnricher(config));
|
|
77
|
+
pipeline.register(new PageStateEnricher(config));
|
|
78
|
+
|
|
79
|
+
return pipeline;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a custom pipeline
|
|
84
|
+
* @param {Array} enrichers - Array of enricher instances
|
|
85
|
+
* @param {Object} config
|
|
86
|
+
* @returns {EnrichmentPipeline}
|
|
87
|
+
*/
|
|
88
|
+
export function createCustomPipeline(enrichers, config = {}) {
|
|
89
|
+
const pipeline = new EnrichmentPipeline(config);
|
|
90
|
+
|
|
91
|
+
for (const enricher of enrichers) {
|
|
92
|
+
pipeline.register(enricher);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return pipeline;
|
|
96
|
+
}
|