chrometools-mcp 2.5.0 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/COMPONENT_MAPPING_SPEC.md +1217 -0
  3. package/README.md +406 -38
  4. package/bridge/bridge-client.js +472 -0
  5. package/bridge/bridge-service.js +399 -0
  6. package/bridge/install.js +241 -0
  7. package/browser/browser-manager.js +107 -2
  8. package/browser/page-manager.js +226 -69
  9. package/docs/CHROME_EXTENSION.md +219 -0
  10. package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
  11. package/extension/background.js +643 -0
  12. package/extension/content.js +715 -0
  13. package/extension/icons/create-icons.js +164 -0
  14. package/extension/icons/icon128.png +0 -0
  15. package/extension/icons/icon16.png +0 -0
  16. package/extension/icons/icon48.png +0 -0
  17. package/extension/manifest.json +58 -0
  18. package/extension/popup/popup.css +437 -0
  19. package/extension/popup/popup.html +102 -0
  20. package/extension/popup/popup.js +415 -0
  21. package/extension/recorder-overlay.css +93 -0
  22. package/index.js +3347 -2901
  23. package/models/BaseInputModel.js +93 -0
  24. package/models/CheckboxGroupModel.js +199 -0
  25. package/models/CheckboxModel.js +103 -0
  26. package/models/ColorInputModel.js +53 -0
  27. package/models/DateInputModel.js +67 -0
  28. package/models/RadioGroupModel.js +126 -0
  29. package/models/RangeInputModel.js +60 -0
  30. package/models/SelectModel.js +97 -0
  31. package/models/TextInputModel.js +34 -0
  32. package/models/TextareaModel.js +59 -0
  33. package/models/TimeInputModel.js +49 -0
  34. package/models/index.js +122 -0
  35. package/package.json +3 -2
  36. package/pom/apom-converter.js +267 -0
  37. package/pom/apom-tree-converter.js +515 -0
  38. package/pom/element-id-generator.js +175 -0
  39. package/recorder/page-object-generator.js +16 -0
  40. package/recorder/scenario-executor.js +80 -2
  41. package/server/tool-definitions.js +839 -713
  42. package/server/tool-groups.js +1 -1
  43. package/server/tool-schemas.js +367 -326
  44. package/server/websocket-bridge.js +447 -0
  45. package/utils/selector-resolver.js +186 -0
  46. package/utils/ui-framework-detector.js +392 -0
  47. package/RELEASE_NOTES_v2.5.0.md +0 -109
  48. package/npm_publish_output.txt +0 -0
@@ -0,0 +1,515 @@
1
+ /**
2
+ * pom/apom-tree-converter.js
3
+ *
4
+ * Converts DOM to APOM Tree format with positioning information
5
+ * APOM v2: Tree-based structure with parent-child relationships
6
+ */
7
+
8
+ /**
9
+ * Build DOM tree starting from root element
10
+ * Runs in browser context via page.evaluate()
11
+ *
12
+ * @param {boolean} interactiveOnly - Only include interactive elements and their parents
13
+ * @returns {Object} APOM tree structure
14
+ */
15
+ function buildAPOMTree(interactiveOnly = true) {
16
+ const pageId = `page_${btoa(window.location.href).replace(/[^a-zA-Z0-9]/g, '').substring(0, 20)}_${Date.now()}`;
17
+
18
+ const result = {
19
+ pageId,
20
+ url: window.location.href,
21
+ title: document.title,
22
+ timestamp: Date.now(),
23
+ tree: null,
24
+ metadata: {
25
+ totalElements: 0,
26
+ interactiveCount: 0,
27
+ formCount: 0,
28
+ modalCount: 0,
29
+ maxDepth: 0
30
+ }
31
+ };
32
+
33
+ // Element ID counter
34
+ let idCounter = 0;
35
+ const elementIds = new WeakMap();
36
+ const interactiveElements = new WeakSet();
37
+
38
+ // First pass: mark all interactive elements
39
+ if (interactiveOnly) {
40
+ markInteractiveElements(document.body);
41
+ }
42
+
43
+ // Build tree from body
44
+ result.tree = buildNode(document.body, null, 0, []);
45
+
46
+ // Collect radio and checkbox groups for easier agent access
47
+ result.groups = collectInputGroups(result.tree);
48
+
49
+ return result;
50
+
51
+ /**
52
+ * Collect radio and checkbox groups from the tree
53
+ */
54
+ function collectInputGroups(tree) {
55
+ const radioGroups = {};
56
+ const checkboxGroups = {};
57
+
58
+ function traverse(node) {
59
+ if (!node) return;
60
+
61
+ // Check if this is a radio or checkbox input
62
+ if (node.type === 'input' && node.metadata) {
63
+ const { inputType, name, value, label, checked } = node.metadata;
64
+
65
+ if (inputType === 'radio' && name) {
66
+ if (!radioGroups[name]) {
67
+ radioGroups[name] = { type: 'radio', options: [] };
68
+ }
69
+ radioGroups[name].options.push({
70
+ id: node.id,
71
+ value: value || '',
72
+ label: label || value || '',
73
+ checked: checked || false
74
+ });
75
+ }
76
+
77
+ if (inputType === 'checkbox' && name) {
78
+ if (!checkboxGroups[name]) {
79
+ checkboxGroups[name] = { type: 'checkbox', options: [] };
80
+ }
81
+ checkboxGroups[name].options.push({
82
+ id: node.id,
83
+ value: value || '',
84
+ label: label || value || '',
85
+ checked: checked || false
86
+ });
87
+ }
88
+ }
89
+
90
+ // Traverse children
91
+ if (node.children) {
92
+ node.children.forEach(child => traverse(child));
93
+ }
94
+ }
95
+
96
+ traverse(tree);
97
+
98
+ return {
99
+ radio: Object.keys(radioGroups).length > 0 ? radioGroups : undefined,
100
+ checkbox: Object.keys(checkboxGroups).length > 0 ? checkboxGroups : undefined
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Mark interactive elements and their ancestors
106
+ */
107
+ function markInteractiveElements(root) {
108
+ const interactiveTags = new Set([
109
+ 'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'LABEL', 'FORM'
110
+ ]);
111
+
112
+ const interactiveRoles = new Set([
113
+ 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox',
114
+ 'menuitem', 'tab', 'switch', 'slider', 'searchbox'
115
+ ]);
116
+
117
+ // Find all interactive elements
118
+ const elements = root.querySelectorAll('*');
119
+ const interactiveList = [];
120
+
121
+ elements.forEach(el => {
122
+ const isInteractive =
123
+ interactiveTags.has(el.tagName) ||
124
+ interactiveRoles.has(el.getAttribute('role')) ||
125
+ el.hasAttribute('onclick') ||
126
+ el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1' ||
127
+ (el.tagName === 'DIV' && el.getAttribute('contenteditable') === 'true');
128
+
129
+ if (isInteractive && isVisible(el)) {
130
+ interactiveList.push(el);
131
+ }
132
+ });
133
+
134
+ // Mark interactive elements and all their ancestors
135
+ interactiveList.forEach(el => {
136
+ let current = el;
137
+ while (current && current !== document.body) {
138
+ interactiveElements.add(current);
139
+ current = current.parentElement;
140
+ }
141
+ });
142
+
143
+ // Always include body
144
+ interactiveElements.add(document.body);
145
+ }
146
+
147
+ /**
148
+ * Check if element is visible
149
+ */
150
+ function isVisible(el) {
151
+ if (!el.offsetParent && el !== document.body) return false;
152
+ const style = window.getComputedStyle(el);
153
+ return style.display !== 'none' &&
154
+ style.visibility !== 'hidden' &&
155
+ style.opacity !== '0';
156
+ }
157
+
158
+ /**
159
+ * Build node recursively
160
+ */
161
+ function buildNode(element, parentId, depth, path) {
162
+ // Skip if in interactive-only mode and element is not marked
163
+ if (interactiveOnly && !interactiveElements.has(element)) {
164
+ return null;
165
+ }
166
+
167
+ // Skip hidden elements
168
+ if (!isVisible(element)) {
169
+ return null;
170
+ }
171
+
172
+ // Generate unique ID
173
+ const id = generateElementId(element);
174
+ elementIds.set(element, id);
175
+
176
+ const currentPath = [...path, id];
177
+
178
+ // Get positioning info
179
+ const position = getPositionInfo(element);
180
+
181
+ // Determine element type
182
+ const elementType = determineElementType(element);
183
+
184
+ // Build node - minimize non-interactive parents
185
+ const isInteractive = elementType.isInteractive;
186
+
187
+ const node = {
188
+ id,
189
+ tag: element.tagName.toLowerCase(),
190
+ selector: generateSelector(element),
191
+ position,
192
+ children: []
193
+ };
194
+
195
+ // Add full info only for interactive elements
196
+ if (isInteractive) {
197
+ node.type = elementType.type;
198
+ node.bounds = getBounds(element);
199
+
200
+ // Add metadata based on element type
201
+ if (elementType.metadata) {
202
+ node.metadata = elementType.metadata;
203
+ }
204
+ } else {
205
+ // For containers (parents), keep it minimal
206
+ node.type = elementType.type;
207
+ }
208
+
209
+ // Update metadata counters
210
+ result.metadata.totalElements++;
211
+ if (elementType.isInteractive) {
212
+ result.metadata.interactiveCount++;
213
+ }
214
+ if (elementType.type === 'form') {
215
+ result.metadata.formCount++;
216
+ }
217
+ if (position.type === 'fixed' || position.type === 'absolute') {
218
+ if (position.zIndex >= 100) {
219
+ result.metadata.modalCount++;
220
+ }
221
+ }
222
+ if (depth > result.metadata.maxDepth) {
223
+ result.metadata.maxDepth = depth;
224
+ }
225
+
226
+ // Process children
227
+ for (const child of element.children) {
228
+ const childNode = buildNode(child, id, depth + 1, currentPath);
229
+ if (childNode) {
230
+ node.children.push(childNode);
231
+ }
232
+ }
233
+
234
+ return node;
235
+ }
236
+
237
+ /**
238
+ * Generate element ID
239
+ */
240
+ function generateElementId(element) {
241
+ const type = determineElementType(element).type;
242
+ const index = idCounter++;
243
+ return `${type}_${index}`;
244
+ }
245
+
246
+ /**
247
+ * Get positioning information
248
+ */
249
+ function getPositionInfo(element) {
250
+ const style = window.getComputedStyle(element);
251
+ const position = style.position;
252
+ const zIndex = style.zIndex === 'auto' ? 'auto' : parseInt(style.zIndex, 10);
253
+
254
+ // Check if creates stacking context
255
+ const isStacking =
256
+ position === 'fixed' ||
257
+ position === 'sticky' ||
258
+ (position === 'absolute' && zIndex !== 'auto') ||
259
+ (position === 'relative' && zIndex !== 'auto') ||
260
+ parseFloat(style.opacity) < 1 ||
261
+ style.transform !== 'none' ||
262
+ style.filter !== 'none' ||
263
+ style.perspective !== 'none' ||
264
+ style.clipPath !== 'none' ||
265
+ style.mask !== 'none' ||
266
+ style.mixBlendMode !== 'normal' ||
267
+ style.isolation === 'isolate';
268
+
269
+ return {
270
+ type: position,
271
+ zIndex: zIndex,
272
+ isStacking: isStacking,
273
+ // Additional positioning properties for modals/overlays detection
274
+ hasBackdrop: style.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
275
+ (position === 'fixed' || position === 'absolute'),
276
+ isFullscreen: element.offsetWidth >= window.innerWidth * 0.9 &&
277
+ element.offsetHeight >= window.innerHeight * 0.9
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Get element bounds
283
+ */
284
+ function getBounds(element) {
285
+ const rect = element.getBoundingClientRect();
286
+ return {
287
+ x: Math.round(rect.left),
288
+ y: Math.round(rect.top),
289
+ width: Math.round(rect.width),
290
+ height: Math.round(rect.height)
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Determine element type and metadata
296
+ */
297
+ function determineElementType(element) {
298
+ const tag = element.tagName.toLowerCase();
299
+ const type = element.type?.toLowerCase();
300
+ const role = element.getAttribute('role');
301
+
302
+ // Form
303
+ if (tag === 'form') {
304
+ return {
305
+ type: 'form',
306
+ isInteractive: true,
307
+ metadata: {
308
+ method: element.method?.toUpperCase() || 'GET',
309
+ action: element.action || '',
310
+ name: element.name || null
311
+ }
312
+ };
313
+ }
314
+
315
+ // Input fields
316
+ if (tag === 'input') {
317
+ const inputType = type || 'text';
318
+
319
+ // Get label text for radio/checkbox inputs
320
+ let labelText = null;
321
+ if (inputType === 'radio' || inputType === 'checkbox') {
322
+ // Try to find label by: 1) wrapping label, 2) label[for=id], 3) aria-label
323
+ const parentLabel = element.closest('label');
324
+ if (parentLabel) {
325
+ // Get text content excluding the input itself
326
+ labelText = parentLabel.textContent?.trim() || null;
327
+ } else if (element.id) {
328
+ const labelFor = document.querySelector(`label[for="${element.id}"]`);
329
+ if (labelFor) {
330
+ labelText = labelFor.textContent?.trim() || null;
331
+ }
332
+ }
333
+ if (!labelText) {
334
+ labelText = element.getAttribute('aria-label') || null;
335
+ }
336
+ }
337
+
338
+ return {
339
+ type: inputType === 'submit' || inputType === 'button' ? 'button' : 'input',
340
+ isInteractive: true,
341
+ metadata: {
342
+ inputType,
343
+ name: element.name || null,
344
+ placeholder: element.placeholder || null,
345
+ required: element.required || false,
346
+ disabled: element.disabled || false,
347
+ value: element.value || '',
348
+ checked: element.checked || undefined,
349
+ label: labelText,
350
+ min: element.min || undefined,
351
+ max: element.max || undefined,
352
+ pattern: element.pattern || undefined
353
+ }
354
+ };
355
+ }
356
+
357
+ // Textarea
358
+ if (tag === 'textarea') {
359
+ return {
360
+ type: 'textarea',
361
+ isInteractive: true,
362
+ metadata: {
363
+ name: element.name || null,
364
+ placeholder: element.placeholder || null,
365
+ required: element.required || false,
366
+ disabled: element.disabled || false,
367
+ rows: element.rows || undefined,
368
+ cols: element.cols || undefined,
369
+ maxLength: element.maxLength > 0 ? element.maxLength : undefined
370
+ }
371
+ };
372
+ }
373
+
374
+ // Select
375
+ if (tag === 'select') {
376
+ const options = Array.from(element.options).map(opt => ({
377
+ value: opt.value,
378
+ text: opt.textContent.trim(),
379
+ selected: opt.selected
380
+ }));
381
+
382
+ return {
383
+ type: 'select',
384
+ isInteractive: true,
385
+ metadata: {
386
+ name: element.name || null,
387
+ required: element.required || false,
388
+ disabled: element.disabled || false,
389
+ multiple: element.multiple || false,
390
+ size: element.size || undefined,
391
+ options,
392
+ selectedIndex: element.selectedIndex,
393
+ selectedValue: element.value || null
394
+ }
395
+ };
396
+ }
397
+
398
+ // Button
399
+ if (tag === 'button' || role === 'button') {
400
+ return {
401
+ type: 'button',
402
+ isInteractive: true,
403
+ metadata: {
404
+ buttonType: type || 'button',
405
+ text: element.textContent?.trim() || '',
406
+ disabled: element.disabled || false,
407
+ ariaLabel: element.getAttribute('aria-label') || null
408
+ }
409
+ };
410
+ }
411
+
412
+ // Link
413
+ if (tag === 'a') {
414
+ return {
415
+ type: 'link',
416
+ isInteractive: true,
417
+ metadata: {
418
+ href: element.href || null,
419
+ text: element.textContent?.trim() || '',
420
+ target: element.target || null,
421
+ rel: element.rel || null
422
+ }
423
+ };
424
+ }
425
+
426
+ // Label
427
+ if (tag === 'label') {
428
+ return {
429
+ type: 'label',
430
+ isInteractive: false,
431
+ metadata: {
432
+ for: element.htmlFor || null,
433
+ text: element.textContent?.trim() || ''
434
+ }
435
+ };
436
+ }
437
+
438
+ // Modal/Dialog
439
+ if (role === 'dialog' || role === 'alertdialog' || element.hasAttribute('aria-modal')) {
440
+ return {
441
+ type: 'modal',
442
+ isInteractive: false,
443
+ metadata: {
444
+ ariaModal: element.getAttribute('aria-modal') === 'true',
445
+ ariaLabel: element.getAttribute('aria-label') || null,
446
+ role: role
447
+ }
448
+ };
449
+ }
450
+
451
+ // Container with semantic role
452
+ if (role) {
453
+ return {
454
+ type: role,
455
+ isInteractive: false,
456
+ metadata: {
457
+ ariaLabel: element.getAttribute('aria-label') || null
458
+ }
459
+ };
460
+ }
461
+
462
+ // Generic container
463
+ return {
464
+ type: 'container',
465
+ isInteractive: false,
466
+ metadata: null
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Generate unique CSS selector
472
+ */
473
+ function generateSelector(element) {
474
+ // Use ID if available and unique
475
+ if (element.id && document.querySelectorAll(`#${element.id}`).length === 1) {
476
+ return `#${element.id}`;
477
+ }
478
+
479
+ // Build path from parent
480
+ const path = [];
481
+ let current = element;
482
+
483
+ while (current && current !== document.body) {
484
+ let selector = current.tagName.toLowerCase();
485
+
486
+ // Add nth-of-type if needed
487
+ if (current.parentElement) {
488
+ const siblings = Array.from(current.parentElement.children).filter(
489
+ el => el.tagName === current.tagName
490
+ );
491
+ if (siblings.length > 1) {
492
+ const index = siblings.indexOf(current) + 1;
493
+ selector += `:nth-of-type(${index})`;
494
+ }
495
+ }
496
+
497
+ path.unshift(selector);
498
+ current = current.parentElement;
499
+ }
500
+
501
+ return path.join(' > ');
502
+ }
503
+ }
504
+
505
+ // Export for use in both Node.js and browser context
506
+ if (typeof module !== 'undefined' && module.exports) {
507
+ module.exports = {
508
+ buildAPOMTree
509
+ };
510
+ }
511
+
512
+ // Make available globally in browser context
513
+ if (typeof window !== 'undefined') {
514
+ window.buildAPOMTree = buildAPOMTree;
515
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * pom/element-id-generator.js
3
+ *
4
+ * Генерация уникальных ID для элементов в Page Object Model
5
+ * Используется для создания стабильных идентификаторов элементов
6
+ */
7
+
8
+ /**
9
+ * Generate unique element ID based on priority:
10
+ * 1. data-testid attribute
11
+ * 2. id attribute
12
+ * 3. Semantic path (type + context + index)
13
+ *
14
+ * @param {Element} element - DOM element
15
+ * @param {string} type - Element type (input, button, select, etc.)
16
+ * @param {number} index - Element index among same type
17
+ * @returns {string} Unique element ID
18
+ */
19
+ function generateElementId(element, type, index) {
20
+ // Priority 1: data-testid
21
+ const testId = element.getAttribute('data-testid') || element.getAttribute('data-test');
22
+ if (testId) {
23
+ return `testid_${sanitizeId(testId)}`;
24
+ }
25
+
26
+ // Priority 2: id attribute
27
+ if (element.id) {
28
+ return `id_${sanitizeId(element.id)}`;
29
+ }
30
+
31
+ // Priority 3: Semantic path
32
+ const semanticPath = getSemanticPath(element);
33
+ const elementType = getElementType(element, type);
34
+
35
+ return `${elementType}_${semanticPath}_${index}`;
36
+ }
37
+
38
+ /**
39
+ * Get semantic path for element (form context, section, etc.)
40
+ *
41
+ * @param {Element} element - DOM element
42
+ * @returns {string} Semantic path
43
+ */
44
+ function getSemanticPath(element) {
45
+ const parts = [];
46
+
47
+ // Check if inside form
48
+ const form = element.closest('form');
49
+ if (form) {
50
+ const formId = form.id || form.name || 'form';
51
+ parts.push(sanitizeId(formId));
52
+ }
53
+
54
+ // Check if inside section/article/nav/header/footer
55
+ const section = element.closest('section, article, nav, header, footer, aside, main');
56
+ if (section && !form) {
57
+ const sectionTag = section.tagName.toLowerCase();
58
+ const sectionId = section.id || section.className.split(' ')[0] || sectionTag;
59
+ parts.push(sanitizeId(sectionId));
60
+ }
61
+
62
+ // If no context found, use 'page'
63
+ if (parts.length === 0) {
64
+ parts.push('page');
65
+ }
66
+
67
+ return parts.join('_');
68
+ }
69
+
70
+ /**
71
+ * Get element type string
72
+ *
73
+ * @param {Element} element - DOM element
74
+ * @param {string} providedType - Type provided by caller (optional)
75
+ * @returns {string} Element type
76
+ */
77
+ function getElementType(element, providedType) {
78
+ if (providedType) {
79
+ return providedType;
80
+ }
81
+
82
+ const tagName = element.tagName.toLowerCase();
83
+
84
+ // Handle input types
85
+ if (tagName === 'input') {
86
+ const inputType = element.type || 'text';
87
+ // Normalize checkbox/radio
88
+ if (inputType === 'checkbox') return 'checkbox';
89
+ if (inputType === 'radio') return 'radio';
90
+ if (inputType === 'submit' || inputType === 'button') return 'button';
91
+ return 'input';
92
+ }
93
+
94
+ // Handle other tags
95
+ if (tagName === 'textarea') return 'textarea';
96
+ if (tagName === 'select') return 'select';
97
+ if (tagName === 'button') return 'button';
98
+ if (tagName === 'a') return 'link';
99
+ if (tagName === 'form') return 'form';
100
+
101
+ // Role-based detection
102
+ const role = element.getAttribute('role');
103
+ if (role === 'button') return 'button';
104
+ if (role === 'link') return 'link';
105
+
106
+ return 'element';
107
+ }
108
+
109
+ /**
110
+ * Sanitize string for use in ID
111
+ *
112
+ * @param {string} str - String to sanitize
113
+ * @returns {string} Sanitized string
114
+ */
115
+ function sanitizeId(str) {
116
+ return str
117
+ .toLowerCase()
118
+ .replace(/[^a-z0-9_-]/g, '_') // Replace non-alphanumeric with underscore
119
+ .replace(/_+/g, '_') // Collapse multiple underscores
120
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
121
+ }
122
+
123
+ /**
124
+ * Generate element name from label, placeholder, or text
125
+ * Used for more descriptive IDs
126
+ *
127
+ * @param {Element} element - DOM element
128
+ * @returns {string} Element name
129
+ */
130
+ function generateElementName(element) {
131
+ // Try label
132
+ const label = element.labels && element.labels[0];
133
+ if (label) {
134
+ const labelText = label.textContent.trim();
135
+ if (labelText) {
136
+ return sanitizeId(labelText.substring(0, 30));
137
+ }
138
+ }
139
+
140
+ // Try placeholder
141
+ const placeholder = element.placeholder;
142
+ if (placeholder) {
143
+ return sanitizeId(placeholder.substring(0, 30));
144
+ }
145
+
146
+ // Try aria-label
147
+ const ariaLabel = element.getAttribute('aria-label');
148
+ if (ariaLabel) {
149
+ return sanitizeId(ariaLabel.substring(0, 30));
150
+ }
151
+
152
+ // Try name attribute
153
+ if (element.name) {
154
+ return sanitizeId(element.name);
155
+ }
156
+
157
+ // Try text content for buttons/links
158
+ const text = element.textContent?.trim();
159
+ if (text) {
160
+ return sanitizeId(text.substring(0, 30));
161
+ }
162
+
163
+ return '';
164
+ }
165
+
166
+ // Export for use in browser context (will be injected via eval)
167
+ if (typeof module !== 'undefined' && module.exports) {
168
+ module.exports = {
169
+ generateElementId,
170
+ getSemanticPath,
171
+ getElementType,
172
+ sanitizeId,
173
+ generateElementName
174
+ };
175
+ }
@@ -192,6 +192,7 @@ async function analyzePage(page) {
192
192
  }
193
193
 
194
194
  const elementInfo = {
195
+ id: `${generateElementName(el)}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // Unique ID for Page Object
195
196
  tag: el.tagName.toLowerCase(),
196
197
  type: el.type || 'element',
197
198
  selector: generateSelector(el),
@@ -215,6 +216,21 @@ async function analyzePage(page) {
215
216
  }
216
217
  };
217
218
 
219
+ // Add select options if element is a select
220
+ if (el.tagName === 'SELECT') {
221
+ elementInfo.options = Array.from(el.options).map((opt, idx) => ({
222
+ value: opt.value,
223
+ text: opt.textContent.trim(),
224
+ index: idx,
225
+ selected: opt.selected,
226
+ disabled: opt.disabled,
227
+ group: opt.parentElement.tagName === 'OPTGROUP' ? opt.parentElement.label : null
228
+ }));
229
+ elementInfo.selectedIndex = el.selectedIndex;
230
+ elementInfo.selectedValue = el.value;
231
+ elementInfo.selectedText = el.options[el.selectedIndex]?.textContent.trim() || null;
232
+ }
233
+
218
234
  elements.push(elementInfo);
219
235
  });
220
236