chrometools-mcp 3.5.4 → 3.5.6
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/CHANGELOG.md +25 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -1217
- package/README.md +13 -4
- package/SPEC-swagger-api-tools.md +3101 -3101
- package/browser/page-manager.js +7 -0
- package/index.js +199 -14
- package/models/DATEPICKER_IMPLEMENTATION.md +543 -543
- package/models/ModelRegistry.js +115 -115
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +56 -1
- package/server/tool-definitions.js +10 -5
- package/server/tool-schemas.js +10 -5
- package/specs/SEGM-537-UNBLOCKERS_PROGRESS.md +94 -0
- package/specs/SEGM-537-UNBLOCKERS_SPEC.md +187 -0
- package/utils/actions/click-action.js +76 -8
- package/SPEC-IMPROVEMENTS.md +0 -173
- package/SPEC-pom-integration.md +0 -227
package/models/ModelRegistry.js
CHANGED
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model Registry - Strategy Pattern Implementation
|
|
3
|
-
* Manages registration and selection of element models
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class ModelRegistry {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.models = [];
|
|
9
|
-
this.modelsMap = null; // Cached models -> actions map
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Register a model class (not instance)
|
|
14
|
-
* @param {Class} ModelClass - Element model class
|
|
15
|
-
*/
|
|
16
|
-
register(ModelClass) {
|
|
17
|
-
const instance = new ModelClass();
|
|
18
|
-
this.models.push(instance);
|
|
19
|
-
|
|
20
|
-
// Sort by priority (higher first)
|
|
21
|
-
this.models.sort((a, b) => b.getPriority() - a.getPriority());
|
|
22
|
-
|
|
23
|
-
// Invalidate cache
|
|
24
|
-
this.modelsMap = null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Register multiple models at once
|
|
29
|
-
* @param {Array<Class>} modelClasses - Array of model classes
|
|
30
|
-
*/
|
|
31
|
-
registerAll(modelClasses) {
|
|
32
|
-
for (const ModelClass of modelClasses) {
|
|
33
|
-
this.register(ModelClass);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Find matching model for element (Strategy Pattern)
|
|
39
|
-
* @param {HTMLElement} element - DOM element
|
|
40
|
-
* @param {Object} elementType - Result from determineElementType()
|
|
41
|
-
* @returns {ElementModel} Matching model instance
|
|
42
|
-
*/
|
|
43
|
-
findModel(element, elementType) {
|
|
44
|
-
for (const model of this.models) {
|
|
45
|
-
if (model.matches(element, elementType)) {
|
|
46
|
-
return model;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// This should never happen if DefaultModel is registered
|
|
51
|
-
throw new Error('No matching model found (is DefaultModel registered?)');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get models map for APOM output (cached)
|
|
56
|
-
* @returns {Object} Map of model names to actions
|
|
57
|
-
*/
|
|
58
|
-
getModelsMap() {
|
|
59
|
-
if (this.modelsMap === null) {
|
|
60
|
-
this.modelsMap = {};
|
|
61
|
-
for (const model of this.models) {
|
|
62
|
-
this.modelsMap[model.getName()] = model.getActions();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return this.modelsMap;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get model by name
|
|
70
|
-
* @param {string} modelName - Model name
|
|
71
|
-
* @returns {ElementModel|null} Model instance or null
|
|
72
|
-
*/
|
|
73
|
-
getModelByName(modelName) {
|
|
74
|
-
return this.models.find(m => m.getName() === modelName) || null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get action handler name for element action
|
|
79
|
-
* @param {HTMLElement} element - Target element
|
|
80
|
-
* @param {Object} elementType - Element type info
|
|
81
|
-
* @param {string} actionName - Action to execute
|
|
82
|
-
* @returns {Object} { handlerName: string, modelName: string } or { error: string }
|
|
83
|
-
*/
|
|
84
|
-
getActionHandler(element, elementType, actionName) {
|
|
85
|
-
const model = this.findModel(element, elementType);
|
|
86
|
-
|
|
87
|
-
// Check if action is available
|
|
88
|
-
const actions = model.getActions();
|
|
89
|
-
if (!actions.includes(actionName)) {
|
|
90
|
-
return {
|
|
91
|
-
error: `Action "${actionName}" not available for model "${model.getName()}". Available: ${actions.join(', ')}`
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const handlerName = model.getActionHandler(actionName);
|
|
96
|
-
if (!handlerName) {
|
|
97
|
-
return {
|
|
98
|
-
error: `Action "${actionName}" has no handler in model "${model.getName()}"`
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
handlerName,
|
|
104
|
-
modelName: model.getName()
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Export for both Node.js and browser
|
|
110
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
111
|
-
module.exports = ModelRegistry;
|
|
112
|
-
}
|
|
113
|
-
if (typeof window !== 'undefined') {
|
|
114
|
-
window.ModelRegistry = ModelRegistry;
|
|
115
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Model Registry - Strategy Pattern Implementation
|
|
3
|
+
* Manages registration and selection of element models
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ModelRegistry {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.models = [];
|
|
9
|
+
this.modelsMap = null; // Cached models -> actions map
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register a model class (not instance)
|
|
14
|
+
* @param {Class} ModelClass - Element model class
|
|
15
|
+
*/
|
|
16
|
+
register(ModelClass) {
|
|
17
|
+
const instance = new ModelClass();
|
|
18
|
+
this.models.push(instance);
|
|
19
|
+
|
|
20
|
+
// Sort by priority (higher first)
|
|
21
|
+
this.models.sort((a, b) => b.getPriority() - a.getPriority());
|
|
22
|
+
|
|
23
|
+
// Invalidate cache
|
|
24
|
+
this.modelsMap = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register multiple models at once
|
|
29
|
+
* @param {Array<Class>} modelClasses - Array of model classes
|
|
30
|
+
*/
|
|
31
|
+
registerAll(modelClasses) {
|
|
32
|
+
for (const ModelClass of modelClasses) {
|
|
33
|
+
this.register(ModelClass);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find matching model for element (Strategy Pattern)
|
|
39
|
+
* @param {HTMLElement} element - DOM element
|
|
40
|
+
* @param {Object} elementType - Result from determineElementType()
|
|
41
|
+
* @returns {ElementModel} Matching model instance
|
|
42
|
+
*/
|
|
43
|
+
findModel(element, elementType) {
|
|
44
|
+
for (const model of this.models) {
|
|
45
|
+
if (model.matches(element, elementType)) {
|
|
46
|
+
return model;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// This should never happen if DefaultModel is registered
|
|
51
|
+
throw new Error('No matching model found (is DefaultModel registered?)');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get models map for APOM output (cached)
|
|
56
|
+
* @returns {Object} Map of model names to actions
|
|
57
|
+
*/
|
|
58
|
+
getModelsMap() {
|
|
59
|
+
if (this.modelsMap === null) {
|
|
60
|
+
this.modelsMap = {};
|
|
61
|
+
for (const model of this.models) {
|
|
62
|
+
this.modelsMap[model.getName()] = model.getActions();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return this.modelsMap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get model by name
|
|
70
|
+
* @param {string} modelName - Model name
|
|
71
|
+
* @returns {ElementModel|null} Model instance or null
|
|
72
|
+
*/
|
|
73
|
+
getModelByName(modelName) {
|
|
74
|
+
return this.models.find(m => m.getName() === modelName) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get action handler name for element action
|
|
79
|
+
* @param {HTMLElement} element - Target element
|
|
80
|
+
* @param {Object} elementType - Element type info
|
|
81
|
+
* @param {string} actionName - Action to execute
|
|
82
|
+
* @returns {Object} { handlerName: string, modelName: string } or { error: string }
|
|
83
|
+
*/
|
|
84
|
+
getActionHandler(element, elementType, actionName) {
|
|
85
|
+
const model = this.findModel(element, elementType);
|
|
86
|
+
|
|
87
|
+
// Check if action is available
|
|
88
|
+
const actions = model.getActions();
|
|
89
|
+
if (!actions.includes(actionName)) {
|
|
90
|
+
return {
|
|
91
|
+
error: `Action "${actionName}" not available for model "${model.getName()}". Available: ${actions.join(', ')}`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handlerName = model.getActionHandler(actionName);
|
|
96
|
+
if (!handlerName) {
|
|
97
|
+
return {
|
|
98
|
+
error: `Action "${actionName}" has no handler in model "${model.getName()}"`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
handlerName,
|
|
104
|
+
modelName: model.getName()
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Export for both Node.js and browser
|
|
110
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
111
|
+
module.exports = ModelRegistry;
|
|
112
|
+
}
|
|
113
|
+
if (typeof window !== 'undefined') {
|
|
114
|
+
window.ModelRegistry = ModelRegistry;
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrometools-mcp",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.6",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -37,9 +37,16 @@ function initializeModelRegistry() {
|
|
|
37
37
|
*
|
|
38
38
|
* @param {boolean} interactiveOnly - Only include interactive elements and their parents
|
|
39
39
|
* @param {boolean} viewportOnly - Only include elements visible in current viewport
|
|
40
|
+
* @param {Object} portalOpts - Portal scan options: { include: boolean, selectors: string[] }
|
|
41
|
+
* When include=true, force-includes contents of React Portal containers
|
|
42
|
+
* (e.g. #menu-popup-root, #tooltip-root) that live outside main React root.
|
|
40
43
|
* @returns {Object} APOM tree structure
|
|
41
44
|
*/
|
|
42
|
-
function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
|
|
45
|
+
function buildAPOMTree(interactiveOnly = true, viewportOnly = false, portalOpts = undefined) {
|
|
46
|
+
const portalInclude = portalOpts ? portalOpts.include !== false : true;
|
|
47
|
+
const portalSelectors = (portalOpts && Array.isArray(portalOpts.selectors) && portalOpts.selectors.length > 0)
|
|
48
|
+
? portalOpts.selectors
|
|
49
|
+
: ['#modal-root', '#menu-popup-root', '#tooltip-root', '#popover-root', '[data-portal]'];
|
|
43
50
|
const pageId = `page_${btoa(window.location.href).replace(/[^a-zA-Z0-9]/g, '').substring(0, 20)}_${Date.now()}`;
|
|
44
51
|
|
|
45
52
|
// Initialize model registry (Strategy Pattern setup)
|
|
@@ -116,6 +123,54 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
|
|
|
116
123
|
forceMarkModalTree(el);
|
|
117
124
|
}
|
|
118
125
|
});
|
|
126
|
+
|
|
127
|
+
// Generic portal containers (menus, tooltips, popovers) — opt-in by selector list.
|
|
128
|
+
// Unlike framework modals, these are app-defined wrappers (e.g. #menu-popup-root from
|
|
129
|
+
// client/index.html). When non-empty, force-include their subtree.
|
|
130
|
+
if (portalInclude && portalSelectors.length > 0) {
|
|
131
|
+
try {
|
|
132
|
+
document.querySelectorAll(portalSelectors.join(',')).forEach(container => {
|
|
133
|
+
if (!container.children || container.children.length === 0) return;
|
|
134
|
+
for (const child of container.children) {
|
|
135
|
+
if (!modalElements.has(child)) {
|
|
136
|
+
forceMarkModalTree(child);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Invalid selector in portalSelectors — skip silently, do not break analyzePage
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// In-tree popups (Popper/Tippy/FloatingUI/custom contextMenu pattern). Some libs
|
|
146
|
+
// render the popup inside a 0-height inline wrapper and then absolute-position it
|
|
147
|
+
// out of the wrapper's box. Default isVisible() drops the wrapper (height: 0) and
|
|
148
|
+
// every popup descendant is lost. Detect: a 0×0 wrapper whose subtree contains a
|
|
149
|
+
// positioned (absolute/fixed) child with real bounds — force-mark that child.
|
|
150
|
+
if (portalInclude) {
|
|
151
|
+
function findPositionedPopup(el, maxDepth) {
|
|
152
|
+
if (maxDepth <= 0) return null;
|
|
153
|
+
for (const child of el.children) {
|
|
154
|
+
if (modalElements.has(child)) continue;
|
|
155
|
+
const cs = window.getComputedStyle(child);
|
|
156
|
+
if ((cs.position === 'absolute' || cs.position === 'fixed') &&
|
|
157
|
+
child.offsetWidth > 0 && child.offsetHeight > 0) {
|
|
158
|
+
return child;
|
|
159
|
+
}
|
|
160
|
+
const deeper = findPositionedPopup(child, maxDepth - 1);
|
|
161
|
+
if (deeper) return deeper;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const allEls = document.body.querySelectorAll('*');
|
|
167
|
+
for (const el of allEls) {
|
|
168
|
+
// Only zero-sized wrappers with children are candidates — cheap filter
|
|
169
|
+
if ((el.offsetHeight !== 0 && el.offsetWidth !== 0) || el.children.length === 0) continue;
|
|
170
|
+
const popup = findPositionedPopup(el, 3);
|
|
171
|
+
if (popup) forceMarkModalTree(popup);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
119
174
|
}
|
|
120
175
|
|
|
121
176
|
// Build tree from body
|
|
@@ -37,6 +37,9 @@ export const toolDefinitions = [
|
|
|
37
37
|
waitAfter: { type: "number", description: "Wait ms (default: 1500)" },
|
|
38
38
|
screenshot: { type: "boolean", description: "Screenshot (default: false)" },
|
|
39
39
|
timeout: { type: "number", description: "Max wait ms (default: 30000)" },
|
|
40
|
+
waitForSelector: { type: "string", description: "CSS selector to wait for after click (atomic click+wait). Use for dropdowns/popups that render into portals." },
|
|
41
|
+
waitTimeoutMs: { type: "number", description: "Timeout for waitForSelector in ms (default: 2000)." },
|
|
42
|
+
autoAnalyzeAfter: { type: "boolean", description: "After click, diff APOM and append '+N appeared: id:\"text\"' delta to result. New ids are pre-registered for follow-up clicks. Use for dropdowns/menus opening with new options." },
|
|
40
43
|
},
|
|
41
44
|
},
|
|
42
45
|
},
|
|
@@ -92,18 +95,18 @@ export const toolDefinitions = [
|
|
|
92
95
|
},
|
|
93
96
|
{
|
|
94
97
|
name: "screenshot",
|
|
95
|
-
description: "Capture element image (5-10k tokens). Use analyzePage for form data/validation (8-10k tokens).",
|
|
98
|
+
description: "Capture element image (5-10k tokens), or full viewport when no id/selector is given. Use analyzePage for form data/validation (8-10k tokens).",
|
|
96
99
|
inputSchema: {
|
|
97
100
|
type: "object",
|
|
98
101
|
properties: {
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
id: { type: "string", description: "APOM element ID. Mutually exclusive with selector. Omit both for viewport screenshot." },
|
|
103
|
+
selector: { type: "string", description: "CSS selector. Mutually exclusive with id. Omit both for viewport screenshot." },
|
|
104
|
+
padding: { type: "number", description: "Padding px (default: 0). Ignored for viewport." },
|
|
101
105
|
maxWidth: { type: "number", description: "Max width px (default: 1024, null=original)" },
|
|
102
106
|
maxHeight: { type: "number", description: "Max height px (default: 8000, null=original)" },
|
|
103
107
|
quality: { type: "number", minimum: 1, maximum: 100, description: "JPEG quality (default: 40)" },
|
|
104
108
|
format: { type: "string", enum: ["png", "jpeg", "auto"], description: "Format (default: jpeg)" },
|
|
105
109
|
},
|
|
106
|
-
required: ["selector"],
|
|
107
110
|
},
|
|
108
111
|
},
|
|
109
112
|
{
|
|
@@ -150,7 +153,7 @@ export const toolDefinitions = [
|
|
|
150
153
|
},
|
|
151
154
|
{
|
|
152
155
|
name: "executeScript",
|
|
153
|
-
description: "⚠️ LAST RESORT tool - use ONLY when ALL specialized tools failed. NEVER use for: clicking (use click), typing (use type), reading page (use analyzePage), finding elements (use findElementsByText). May break React/Vue/Angular synthetic events. ALWAYS try specialized tools first.",
|
|
156
|
+
description: "⚠️ LAST RESORT tool - use ONLY when ALL specialized tools failed. NEVER use for: clicking (use click), typing (use type), scrolling (use scrollTo), reading page elements (use analyzePage), finding elements (use findElementsByText), fetching API data (use listNetworkRequests + getNetworkRequest). May break React/Vue/Angular synthetic events. ALWAYS try specialized tools first.",
|
|
154
157
|
inputSchema: {
|
|
155
158
|
type: "object",
|
|
156
159
|
properties: {
|
|
@@ -532,6 +535,8 @@ Examples:
|
|
|
532
535
|
groupBy: { type: "string", description: "Group elements: 'type' or 'flat' (default: 'type')", enum: ["type", "flat"] },
|
|
533
536
|
viewportOnly: { type: "boolean", description: "Only analyze elements in current viewport (default: false). Reduces output for long pages." },
|
|
534
537
|
diff: { type: "boolean", description: "Return only changes since last analysis: {added, removed, changed} (default: false)." },
|
|
538
|
+
includePortals: { type: "boolean", description: "Include React Portal contents — menus, tooltips, popovers outside main root (default: true). Without this, dropdown items are invisible." },
|
|
539
|
+
portalSelectors: { type: "array", items: { type: "string" }, description: "Custom portal root CSS selectors. Default: ['#modal-root', '#menu-popup-root', '#tooltip-root', '#popover-root', '[data-portal]']." },
|
|
535
540
|
},
|
|
536
541
|
},
|
|
537
542
|
},
|
package/server/tool-schemas.js
CHANGED
|
@@ -23,6 +23,9 @@ export const ClickSchema = z.object({
|
|
|
23
23
|
timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
|
|
24
24
|
skipNetworkWait: z.boolean().optional().describe("Skip waiting for network requests (default: false). Use for forms with long-polling/WebSockets to avoid timeouts."),
|
|
25
25
|
networkWaitTimeout: z.number().optional().describe("Maximum time to wait for network requests in ms (default: 3000). Only used if skipNetworkWait is false."),
|
|
26
|
+
waitForSelector: z.string().optional().describe("CSS selector to wait for after click — atomic click+wait. Useful for dropdowns/popups in portals (e.g. '#menu-popup-root > div') that otherwise race against the next MCP call."),
|
|
27
|
+
waitTimeoutMs: z.number().optional().describe("Timeout for waitForSelector in ms (default: 2000)."),
|
|
28
|
+
autoAnalyzeAfter: z.boolean().optional().describe("After click, automatically diff APOM state and append a delta to the result: '+N appeared: id1:\"text\", id2:\"text\"'. New ids are re-registered so callers can use them directly in the next click/type call without an extra analyzePage. Use for dropdowns and menus that reveal new options on click."),
|
|
26
29
|
}).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
|
|
27
30
|
message: "Either 'id' or 'selector' must be provided, but not both"
|
|
28
31
|
});
|
|
@@ -110,15 +113,15 @@ export const SetStylesSchema = z.object({
|
|
|
110
113
|
|
|
111
114
|
// Screenshot tools
|
|
112
115
|
export const ScreenshotSchema = z.object({
|
|
113
|
-
id: z.string().optional().describe("APOM element ID from analyzePage (e.g., 'div_20'). Mutually exclusive with selector."),
|
|
114
|
-
selector: z.string().optional().describe("CSS selector for element to screenshot. Mutually exclusive with id."),
|
|
115
|
-
padding: z.number().optional().describe("Padding around element in pixels (default: 0)"),
|
|
116
|
+
id: z.string().optional().describe("APOM element ID from analyzePage (e.g., 'div_20'). Mutually exclusive with selector. If neither id nor selector is provided, captures full viewport."),
|
|
117
|
+
selector: z.string().optional().describe("CSS selector for element to screenshot. Mutually exclusive with id. If neither id nor selector is provided, captures full viewport."),
|
|
118
|
+
padding: z.number().optional().describe("Padding around element in pixels (default: 0). Ignored for viewport screenshot."),
|
|
116
119
|
maxWidth: z.number().nullable().optional().describe("Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)"),
|
|
117
120
|
maxHeight: z.number().nullable().optional().describe("Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)"),
|
|
118
121
|
quality: z.number().min(1).max(100).optional().describe("JPEG quality 1-100 (default: 40)"),
|
|
119
122
|
format: z.enum(['png', 'jpeg', 'auto']).optional().describe("Image format (default: 'jpeg')"),
|
|
120
|
-
}).refine(data =>
|
|
121
|
-
message: "
|
|
123
|
+
}).refine(data => !(data.id && data.selector), {
|
|
124
|
+
message: "Provide only one of 'id' or 'selector' (or neither for a viewport screenshot)"
|
|
122
125
|
});
|
|
123
126
|
|
|
124
127
|
export const SaveScreenshotSchema = z.object({
|
|
@@ -289,6 +292,8 @@ export const AnalyzePageSchema = z.object({
|
|
|
289
292
|
groupBy: z.enum(['type', 'flat']).optional().describe("Group elements by type or return flat structure (default: 'type')"),
|
|
290
293
|
viewportOnly: z.boolean().optional().describe("Only analyze elements visible in current viewport (default: false). Reduces output for long pages."),
|
|
291
294
|
diff: z.boolean().optional().describe("Return only changes since last analysis: {added, removed, changed} (default: false). Useful after clicks to see what changed."),
|
|
295
|
+
includePortals: z.boolean().optional().describe("Include contents of React Portal containers that live outside main React root (default: true). Covers menus, tooltips, popovers rendered via portals — without this, dropdown contents are invisible to analyzePage."),
|
|
296
|
+
portalSelectors: z.array(z.string()).optional().describe("CSS selectors of portal root containers to scan (default: ['#modal-root', '#menu-popup-root', '#tooltip-root', '#popover-root', '[data-portal]']). Provide custom list when the app uses different portal element ids."),
|
|
292
297
|
});
|
|
293
298
|
|
|
294
299
|
export const GetElementDetailsSchema = z.object({
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Progress: SEGM-537 QA Unblockers
|
|
2
|
+
|
|
3
|
+
**Status**: NEEDS APPROVAL
|
|
4
|
+
**Spec**: [SEGM-537-UNBLOCKERS_SPEC.md](./SEGM-537-UNBLOCKERS_SPEC.md)
|
|
5
|
+
**Created**: 2026-05-28
|
|
6
|
+
|
|
7
|
+
## Phase 0 — Approval & alignment
|
|
8
|
+
|
|
9
|
+
- [ ] Пользователь одобрил scope (все 5 блокеров)
|
|
10
|
+
- [ ] Пользователь одобрил дизайн API (имена параметров, дефолты)
|
|
11
|
+
- [ ] Дать команду «погнали, начни с Phase 1»
|
|
12
|
+
|
|
13
|
+
## Phase 1 — Блокер #1: portal scan расширение
|
|
14
|
+
|
|
15
|
+
- [x] Расширить `pom/apom-tree-converter.js` — добавить `portalSelectors` параметр + третий проход (~120)
|
|
16
|
+
- [x] ~~Переименовать `forceMarkModalTree`~~ — переиспользована as-is, переименование избыточно
|
|
17
|
+
- [x] Прокинуть `includePortals` / `portalSelectors` через `page.evaluate` в `index.js`
|
|
18
|
+
- [x] Обновить `server/tool-schemas.js` + `server/tool-definitions.js` для `analyzePage`
|
|
19
|
+
- [x] `npm run build` — Syntax validation passed
|
|
20
|
+
- [ ] Verification: бенчмарк Google (требует запуска MCP — пользователь должен перезапустить и прогнать)
|
|
21
|
+
- [ ] Verification: ручной тест на QA-стенде (требует доступа QA — отдаём после рестарта MCP)
|
|
22
|
+
- [x] Обновить `README.md` — секция `analyzePage`
|
|
23
|
+
|
|
24
|
+
## Phase 2 — Блокер #2: click + waitForSelector
|
|
25
|
+
|
|
26
|
+
- [x] Добавить `waitForSelector` / `waitTimeoutMs` в `executeClickAction` + `click` handler
|
|
27
|
+
- [x] Возвращать `appearedInMs` в результате (или `⚠️ WAIT_TIMEOUT` сообщение)
|
|
28
|
+
- [x] Обновить `server/tool-schemas.js` + `server/tool-definitions.js` для `click`
|
|
29
|
+
- [x] `npm run build` — Syntax validation passed
|
|
30
|
+
- [ ] Verification: на стенде клик «три точки» с `waitForSelector` (требует рестарта MCP)
|
|
31
|
+
- [ ] Verification: несуществующий селектор → таймаут (требует рестарта MCP)
|
|
32
|
+
- [x] Обновить `README.md` — секция `click`
|
|
33
|
+
|
|
34
|
+
## Phase 3 — Блокер #4: ModelRegistry stale error
|
|
35
|
+
|
|
36
|
+
- [x] **Root cause fix**: `quickRegisterElements` теперь инжектит models code (раньше не делал → ReferenceError при auto-refresh после navigation)
|
|
37
|
+
- [x] User-friendly fallback: catch на `ReferenceError: ModelRegistry is not defined` в `resolveSelector` → сообщение «APOM registry stale, call analyzePage()»
|
|
38
|
+
- [x] `npm run build` — Syntax validation passed
|
|
39
|
+
- [ ] Verification: воспроизвести (analyzePage → navigate → click) — требует рестарта MCP
|
|
40
|
+
|
|
41
|
+
## Phase 4 — Блокер #3: screenshot без selector
|
|
42
|
+
|
|
43
|
+
- [x] Ослабить `.refine` в `ScreenshotSchema` (запрещаем только конфликт, не отсутствие)
|
|
44
|
+
- [x] Без id/selector — viewport screenshot через `processScreenshot`
|
|
45
|
+
- [x] Обновить `server/tool-schemas.js` + `server/tool-definitions.js`
|
|
46
|
+
- [x] `npm run build` — Syntax validation passed
|
|
47
|
+
- [ ] Verification: `screenshot()` без аргументов возвращает viewport — требует рестарта MCP
|
|
48
|
+
- [x] Обновить `README.md` — секция `screenshot`
|
|
49
|
+
|
|
50
|
+
## Phase 5 — Блокер #5: executeScript auto-IIFE
|
|
51
|
+
|
|
52
|
+
- [x] Препроцессор скрипта в `executeScript` handler (index.js)
|
|
53
|
+
- [x] Regex: оборачивает только если `^return[\s;]` И код не содержит `function ...`
|
|
54
|
+
- [x] `npm run build` — Syntax validation passed
|
|
55
|
+
- [x] Verification (offline): 5 канонических случаев работают корректно (`return document.title`, ` return 42;`, IIFE, plain expr, `function ... return`)
|
|
56
|
+
- [x] Обновить `README.md` — секция `executeScript`
|
|
57
|
+
|
|
58
|
+
## Phase 7 — click autoAnalyzeAfter
|
|
59
|
+
|
|
60
|
+
- [x] Helper `getApomSnapshot(page)` в index.js
|
|
61
|
+
- [x] Pre/post snapshot в click handler с регистрацией новых id через `quickRegisterElements`
|
|
62
|
+
- [x] Дельта `+N appeared: id:"text"` / `-N disappeared` / `No APOM changes` в результате click
|
|
63
|
+
- [x] Schema + tool-definitions + README
|
|
64
|
+
- [x] `npm run build` — passed
|
|
65
|
+
- [x] Verified на стенде: Phase 7 механика работает (snapshots + diff + content append). На SEGM-стенде дельта пустая из-за Phase 8 проблемы (popup не попадает в дерево).
|
|
66
|
+
|
|
67
|
+
## Phase 8 — In-tree popup detection
|
|
68
|
+
|
|
69
|
+
Открыт **при e2e-тесте Phase 7**: popup-меню стенда (Popper-style) рендерится внутри 0-height wrapper и не попадает в APOM tree, потому что `isVisible` отбрасывает wrapper.
|
|
70
|
+
|
|
71
|
+
- [x] Расширение portal-scan в `pom/apom-tree-converter.js` — новый блок после id-portalSelectors
|
|
72
|
+
- [x] `findPositionedPopup(el, depth=3)` — рекурсивный поиск absolute/fixed-positioned child с реальными bounds внутри 0×0 wrapper
|
|
73
|
+
- [x] force-mark найденного popup через существующий `forceMarkModalTree`
|
|
74
|
+
- [x] Используется тот же flag `portalInclude` (default `true`) — opt-out возможен через `includePortals: false`
|
|
75
|
+
- [x] README обновлён
|
|
76
|
+
- [x] `npm run build` — passed
|
|
77
|
+
- [ ] Verification на SEGM-стенде — требует рестарта MCP
|
|
78
|
+
|
|
79
|
+
## Phase 6 — Финализация сессии
|
|
80
|
+
|
|
81
|
+
- [ ] Прогон всех verification на реальном QA-стенде SEGM-537 (если доступ есть)
|
|
82
|
+
- [ ] Спросить пользователя: bump version + CHANGELOG entry?
|
|
83
|
+
- [ ] Если YES — single CHANGELOG entry для всей сессии, версия += patch (3.5.4 → 3.5.5)
|
|
84
|
+
- [ ] Отдать QA на повтор сценария из отчёта
|
|
85
|
+
|
|
86
|
+
## Verification status: per-phase checks
|
|
87
|
+
|
|
88
|
+
Каждая phase имеет свои verification-шаги выше. Не двигаться к следующей пока текущая не зелёная.
|
|
89
|
+
|
|
90
|
+
## Открытые вопросы (повторяю из spec)
|
|
91
|
+
|
|
92
|
+
1. Дефолтные id-портал-селекторы (`#menu-popup-root`, `#modal-root`, `#tooltip-root`, `#popover-root`) — норм или хотите другой набор?
|
|
93
|
+
2. `keepOpen: true` для `click` — пропускаем, делаем только `waitForSelector`. Если QA увидит что попап всё равно закрывается — добавим в Phase 2.5.
|
|
94
|
+
3. Авто-IIFE для `executeScript` — допустимый риск перехвата `return` в случаях, где `return` намеренно внутри функции? Митигируем regex'ом на top-level.
|