chrometools-mcp 2.4.2 → 3.1.2
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 +540 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +494 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/element-finder-utils.js +138 -28
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/figma-tools.js +120 -0
- package/index.js +3347 -2518
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -656
- package/server/tool-groups.js +3 -2
- package/server/tool-schemas.js +367 -296
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Framework Detector
|
|
3
|
+
* Detects and extracts information from various UI component libraries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect UI framework used by an element
|
|
8
|
+
* @param {HTMLElement} element - DOM element to analyze
|
|
9
|
+
* @returns {Object} Framework information
|
|
10
|
+
*/
|
|
11
|
+
function detectUIFramework(element) {
|
|
12
|
+
const classes = Array.from(element.classList || []);
|
|
13
|
+
const dataAttrs = Array.from(element.attributes || [])
|
|
14
|
+
.filter(attr => attr.name.startsWith('data-'))
|
|
15
|
+
.map(attr => attr.name);
|
|
16
|
+
|
|
17
|
+
// Material-UI (MUI) detection
|
|
18
|
+
if (classes.some(c => c.startsWith('Mui')) ||
|
|
19
|
+
classes.some(c => c.startsWith('MuiInput')) ||
|
|
20
|
+
classes.some(c => c.startsWith('MuiSelect')) ||
|
|
21
|
+
classes.some(c => c.startsWith('MuiButton'))) {
|
|
22
|
+
return {
|
|
23
|
+
name: 'mui',
|
|
24
|
+
version: detectMUIVersion(element),
|
|
25
|
+
component: detectMUIComponent(element, classes)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ant Design detection
|
|
30
|
+
if (classes.some(c => c.startsWith('ant-')) ||
|
|
31
|
+
dataAttrs.includes('data-antd')) {
|
|
32
|
+
return {
|
|
33
|
+
name: 'antd',
|
|
34
|
+
version: null, // Can be enhanced
|
|
35
|
+
component: detectAntDComponent(element, classes)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Chakra UI detection
|
|
40
|
+
if (classes.some(c => c.startsWith('chakra-')) ||
|
|
41
|
+
dataAttrs.includes('data-chakra-component')) {
|
|
42
|
+
return {
|
|
43
|
+
name: 'chakra',
|
|
44
|
+
version: null,
|
|
45
|
+
component: element.getAttribute('data-chakra-component') || 'unknown'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Bootstrap detection
|
|
50
|
+
if (classes.some(c => c.startsWith('form-')) ||
|
|
51
|
+
classes.some(c => c.startsWith('btn-')) ||
|
|
52
|
+
classes.includes('form-control') ||
|
|
53
|
+
classes.includes('form-select')) {
|
|
54
|
+
return {
|
|
55
|
+
name: 'bootstrap',
|
|
56
|
+
version: detectBootstrapVersion(element),
|
|
57
|
+
component: detectBootstrapComponent(element, classes)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Vuetify detection
|
|
62
|
+
if (classes.some(c => c.startsWith('v-')) ||
|
|
63
|
+
dataAttrs.includes('data-v-')) {
|
|
64
|
+
return {
|
|
65
|
+
name: 'vuetify',
|
|
66
|
+
version: null,
|
|
67
|
+
component: detectVuetifyComponent(element, classes)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Semantic UI detection
|
|
72
|
+
if (classes.includes('ui') &&
|
|
73
|
+
(classes.includes('input') || classes.includes('dropdown') || classes.includes('button'))) {
|
|
74
|
+
return {
|
|
75
|
+
name: 'semantic-ui',
|
|
76
|
+
version: null,
|
|
77
|
+
component: detectSemanticUIComponent(element, classes)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default: vanilla HTML
|
|
82
|
+
return {
|
|
83
|
+
name: 'vanilla',
|
|
84
|
+
version: null,
|
|
85
|
+
component: element.tagName.toLowerCase()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect MUI version
|
|
91
|
+
*/
|
|
92
|
+
function detectMUIVersion(element) {
|
|
93
|
+
// MUI v5 uses different class naming than v4
|
|
94
|
+
const classes = Array.from(element.classList || []);
|
|
95
|
+
|
|
96
|
+
// v5: MuiInputBase-root, MuiOutlinedInput-root
|
|
97
|
+
if (classes.some(c => c.match(/^Mui[A-Z][a-z]+Base-/))) {
|
|
98
|
+
return '5.x';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// v4: MuiInput-root, MuiSelect-root
|
|
102
|
+
if (classes.some(c => c.match(/^Mui[A-Z][a-z]+-root$/))) {
|
|
103
|
+
return '4.x';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detect MUI component type
|
|
111
|
+
*/
|
|
112
|
+
function detectMUIComponent(element, classes) {
|
|
113
|
+
if (classes.some(c => c.includes('TextField'))) return 'TextField';
|
|
114
|
+
if (classes.some(c => c.includes('Select'))) return 'Select';
|
|
115
|
+
if (classes.some(c => c.includes('Autocomplete'))) return 'Autocomplete';
|
|
116
|
+
if (classes.some(c => c.includes('Button'))) return 'Button';
|
|
117
|
+
if (classes.some(c => c.includes('Checkbox'))) return 'Checkbox';
|
|
118
|
+
if (classes.some(c => c.includes('Radio'))) return 'Radio';
|
|
119
|
+
if (classes.some(c => c.includes('Switch'))) return 'Switch';
|
|
120
|
+
return 'unknown';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Detect Ant Design component type
|
|
125
|
+
*/
|
|
126
|
+
function detectAntDComponent(element, classes) {
|
|
127
|
+
if (classes.includes('ant-input')) return 'Input';
|
|
128
|
+
if (classes.includes('ant-select')) return 'Select';
|
|
129
|
+
if (classes.includes('ant-picker')) return 'DatePicker';
|
|
130
|
+
if (classes.includes('ant-btn')) return 'Button';
|
|
131
|
+
if (classes.includes('ant-checkbox')) return 'Checkbox';
|
|
132
|
+
if (classes.includes('ant-radio')) return 'Radio';
|
|
133
|
+
if (classes.includes('ant-switch')) return 'Switch';
|
|
134
|
+
return 'unknown';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Detect Bootstrap version
|
|
139
|
+
*/
|
|
140
|
+
function detectBootstrapVersion(element) {
|
|
141
|
+
// Check for Bootstrap 5 specific classes
|
|
142
|
+
const classes = Array.from(element.classList || []);
|
|
143
|
+
if (classes.includes('form-select')) return '5.x';
|
|
144
|
+
|
|
145
|
+
// Bootstrap 4 and earlier use 'custom-select'
|
|
146
|
+
if (classes.includes('custom-select')) return '4.x';
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Detect Bootstrap component type
|
|
153
|
+
*/
|
|
154
|
+
function detectBootstrapComponent(element, classes) {
|
|
155
|
+
if (classes.includes('form-control')) {
|
|
156
|
+
if (element.tagName === 'SELECT') return 'Select';
|
|
157
|
+
if (element.tagName === 'TEXTAREA') return 'Textarea';
|
|
158
|
+
return 'Input';
|
|
159
|
+
}
|
|
160
|
+
if (classes.includes('form-select')) return 'Select';
|
|
161
|
+
if (classes.includes('form-check-input')) {
|
|
162
|
+
if (element.type === 'checkbox') return 'Checkbox';
|
|
163
|
+
if (element.type === 'radio') return 'Radio';
|
|
164
|
+
}
|
|
165
|
+
if (classes.some(c => c.startsWith('btn'))) return 'Button';
|
|
166
|
+
return 'unknown';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Detect Vuetify component type
|
|
171
|
+
*/
|
|
172
|
+
function detectVuetifyComponent(element, classes) {
|
|
173
|
+
if (classes.includes('v-text-field')) return 'TextField';
|
|
174
|
+
if (classes.includes('v-select')) return 'Select';
|
|
175
|
+
if (classes.includes('v-autocomplete')) return 'Autocomplete';
|
|
176
|
+
if (classes.includes('v-btn')) return 'Button';
|
|
177
|
+
if (classes.includes('v-checkbox')) return 'Checkbox';
|
|
178
|
+
if (classes.includes('v-radio')) return 'Radio';
|
|
179
|
+
if (classes.includes('v-switch')) return 'Switch';
|
|
180
|
+
return 'unknown';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Detect Semantic UI component type
|
|
185
|
+
*/
|
|
186
|
+
function detectSemanticUIComponent(element, classes) {
|
|
187
|
+
if (classes.includes('input')) return 'Input';
|
|
188
|
+
if (classes.includes('dropdown')) return 'Dropdown';
|
|
189
|
+
if (classes.includes('button')) return 'Button';
|
|
190
|
+
if (classes.includes('checkbox')) return 'Checkbox';
|
|
191
|
+
if (classes.includes('radio')) return 'Radio';
|
|
192
|
+
return 'unknown';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract options from a select/dropdown element
|
|
197
|
+
* Handles both native <select> and custom UI framework dropdowns
|
|
198
|
+
*/
|
|
199
|
+
function extractSelectOptions(element) {
|
|
200
|
+
const framework = detectUIFramework(element);
|
|
201
|
+
|
|
202
|
+
switch (framework.name) {
|
|
203
|
+
case 'vanilla':
|
|
204
|
+
return extractVanillaSelectOptions(element);
|
|
205
|
+
|
|
206
|
+
case 'mui':
|
|
207
|
+
return extractMUISelectOptions(element);
|
|
208
|
+
|
|
209
|
+
case 'antd':
|
|
210
|
+
return extractAntDSelectOptions(element);
|
|
211
|
+
|
|
212
|
+
case 'chakra':
|
|
213
|
+
return extractChakraSelectOptions(element);
|
|
214
|
+
|
|
215
|
+
case 'bootstrap':
|
|
216
|
+
return extractVanillaSelectOptions(element); // Bootstrap uses native select
|
|
217
|
+
|
|
218
|
+
case 'vuetify':
|
|
219
|
+
return extractVuetifySelectOptions(element);
|
|
220
|
+
|
|
221
|
+
case 'semantic-ui':
|
|
222
|
+
return extractSemanticUISelectOptions(element);
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
return extractVanillaSelectOptions(element);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract options from vanilla HTML <select>
|
|
231
|
+
*/
|
|
232
|
+
function extractVanillaSelectOptions(element) {
|
|
233
|
+
if (element.tagName !== 'SELECT') return null;
|
|
234
|
+
|
|
235
|
+
const options = [];
|
|
236
|
+
let currentGroup = null;
|
|
237
|
+
|
|
238
|
+
Array.from(element.children).forEach((child, idx) => {
|
|
239
|
+
if (child.tagName === 'OPTGROUP') {
|
|
240
|
+
currentGroup = child.label;
|
|
241
|
+
Array.from(child.children).forEach((opt, subIdx) => {
|
|
242
|
+
if (opt.tagName === 'OPTION') {
|
|
243
|
+
options.push({
|
|
244
|
+
value: opt.value,
|
|
245
|
+
text: opt.textContent.trim(),
|
|
246
|
+
index: options.length,
|
|
247
|
+
selected: opt.selected,
|
|
248
|
+
disabled: opt.disabled,
|
|
249
|
+
group: currentGroup
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
} else if (child.tagName === 'OPTION') {
|
|
254
|
+
options.push({
|
|
255
|
+
value: child.value,
|
|
256
|
+
text: child.textContent.trim(),
|
|
257
|
+
index: idx,
|
|
258
|
+
selected: child.selected,
|
|
259
|
+
disabled: child.disabled,
|
|
260
|
+
group: null
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
options,
|
|
267
|
+
selectedIndex: element.selectedIndex,
|
|
268
|
+
selectedValue: element.value,
|
|
269
|
+
selectedText: element.options[element.selectedIndex]?.textContent.trim() || null,
|
|
270
|
+
multiple: element.multiple
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extract options from MUI Select
|
|
276
|
+
*/
|
|
277
|
+
function extractMUISelectOptions(element) {
|
|
278
|
+
// MUI Select renders native select or custom menu
|
|
279
|
+
const nativeSelect = element.querySelector('select');
|
|
280
|
+
if (nativeSelect) {
|
|
281
|
+
return extractVanillaSelectOptions(nativeSelect);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// For custom MUI Select, options are in menu (not always rendered)
|
|
285
|
+
// We need to look for data attributes or aria-attributes
|
|
286
|
+
const selectedValue = element.getAttribute('aria-activedescendant') ||
|
|
287
|
+
element.querySelector('input')?.value || '';
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
options: [], // Options may not be in DOM until dropdown opens
|
|
291
|
+
selectedValue,
|
|
292
|
+
selectedText: element.textContent.trim(),
|
|
293
|
+
note: 'MUI Select options may require opening the dropdown to extract'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Extract options from Ant Design Select
|
|
299
|
+
*/
|
|
300
|
+
function extractAntDSelectOptions(element) {
|
|
301
|
+
// Ant Design Select stores value in hidden input
|
|
302
|
+
const input = element.querySelector('.ant-select-selection-item');
|
|
303
|
+
const selectedText = input?.textContent.trim() || '';
|
|
304
|
+
|
|
305
|
+
// Options are rendered in dropdown (may not be in DOM)
|
|
306
|
+
const dropdown = document.querySelector('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
|
|
307
|
+
const options = [];
|
|
308
|
+
|
|
309
|
+
if (dropdown) {
|
|
310
|
+
dropdown.querySelectorAll('.ant-select-item-option').forEach((opt, idx) => {
|
|
311
|
+
options.push({
|
|
312
|
+
value: opt.getAttribute('data-value') || opt.textContent.trim(),
|
|
313
|
+
text: opt.textContent.trim(),
|
|
314
|
+
index: idx,
|
|
315
|
+
selected: opt.classList.contains('ant-select-item-option-selected'),
|
|
316
|
+
disabled: opt.classList.contains('ant-select-item-option-disabled'),
|
|
317
|
+
group: null
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
options,
|
|
324
|
+
selectedText,
|
|
325
|
+
note: 'Ant Design Select options may require opening the dropdown to extract'
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Extract options from Chakra UI Select
|
|
331
|
+
*/
|
|
332
|
+
function extractChakraSelectOptions(element) {
|
|
333
|
+
// Chakra UI typically uses native select
|
|
334
|
+
const nativeSelect = element.querySelector('select');
|
|
335
|
+
if (nativeSelect) {
|
|
336
|
+
return extractVanillaSelectOptions(nativeSelect);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
options: [],
|
|
341
|
+
note: 'Chakra UI Select options need specific extraction logic'
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Extract options from Vuetify Select
|
|
347
|
+
*/
|
|
348
|
+
function extractVuetifySelectOptions(element) {
|
|
349
|
+
// Vuetify stores selected value in data attributes or input
|
|
350
|
+
const input = element.querySelector('input');
|
|
351
|
+
const selectedText = input?.value || '';
|
|
352
|
+
|
|
353
|
+
// Options are in menu (may not be rendered)
|
|
354
|
+
return {
|
|
355
|
+
options: [],
|
|
356
|
+
selectedText,
|
|
357
|
+
note: 'Vuetify Select options may require opening the dropdown to extract'
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Extract options from Semantic UI Dropdown
|
|
363
|
+
*/
|
|
364
|
+
function extractSemanticUISelectOptions(element) {
|
|
365
|
+
const options = [];
|
|
366
|
+
|
|
367
|
+
element.querySelectorAll('.item').forEach((item, idx) => {
|
|
368
|
+
options.push({
|
|
369
|
+
value: item.getAttribute('data-value') || item.textContent.trim(),
|
|
370
|
+
text: item.textContent.trim(),
|
|
371
|
+
index: idx,
|
|
372
|
+
selected: item.classList.contains('selected'),
|
|
373
|
+
disabled: item.classList.contains('disabled'),
|
|
374
|
+
group: null
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const selectedText = element.querySelector('.text')?.textContent.trim() || '';
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
options,
|
|
382
|
+
selectedText
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Export for use in page context
|
|
387
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
388
|
+
module.exports = {
|
|
389
|
+
detectUIFramework,
|
|
390
|
+
extractSelectOptions
|
|
391
|
+
};
|
|
392
|
+
}
|