chrometools-mcp 3.5.1 → 3.5.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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.5.2] - 2026-02-16
6
+
7
+ ### Added
8
+ - **Modal/Dialog detection (React Portals)** — `analyzePage` now detects modals rendered via React Portals (antd, MUI, Bootstrap, Chakra, Element UI, Headless UI, Radix, Mantine). ModalModel class in Element Model system with `role="dialog"` / `aria-modal="true"` matching. Portal wrapper ancestors are force-included in APOM tree with compact format. Modal metadata includes title and action buttons
9
+ - **TxtInp `clear` action** — TextInput model now supports `executeModelAction(action: "clear")` for clearing pre-filled form fields
10
+
11
+ ### Fixed
12
+ - **React controlled input clearing** — `type(clearFirst: true)` now works correctly with React/Vue/Angular controlled inputs (antd `<Input>`, MUI `<TextField>`, etc.). Uses native `HTMLInputElement.prototype.value` setter to bypass framework value trackers that ignored programmatic `el.value = ''` changes. Applied to both TextInputModel and TextareaModel
13
+ - **"ModelRegistry is not defined" error** — Fixed sporadic ReferenceError when calling `executeModelAction` or `click` after page navigation. Bare `ModelRegistry` identifier was inaccessible after `eval()` in strict mode contexts; changed to `window.ModelRegistry` reference
14
+ - **Modal output bloat** — ModalModel now only matches actual dialog elements (`role="dialog"`), not framework wrapper divs (`ant-modal-root`, `ant-modal-wrap`). Reduces `modalCount` from 3 to 1 per modal and forces wrapper ancestors to compact container format
15
+
5
16
  ## [3.5.1] - 2026-02-16
6
17
 
7
18
  ### Fixed
package/README.md CHANGED
@@ -335,6 +335,14 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
335
335
  - Example: `executeModelAction({id: "input_34", action: "check"})`
336
336
  - Example: `executeModelAction({selector: ".datepicker", action: "SetDate", params: {date: "2024-03-15"}})`
337
337
  - See `models/` directory for available models and actions
338
+ - Available models: TxtInp, Sel, Btn, Chk, Radio, TxtArea, Link, Range, DatePicker, DateInp, FileInp, ColorInp, **Modal**, default
339
+
340
+ #### Modal/Dialog Support
341
+ - **Automatic detection**: APOM detects modals rendered via React Portals (antd, MUI, Bootstrap, Chakra, Mantine, Element UI, Headless UI, Radix)
342
+ - **Detection methods**: `role="dialog"`, `aria-modal="true"`, framework-specific CSS classes
343
+ - **Animation-proof**: Modal elements are included even during CSS appear animations (opacity: 0)
344
+ - **Rich metadata**: Modal nodes include `title` and `actions` (button labels) in metadata
345
+ - **In APOM tree**: Modals appear as `type: "dialog"` with `model: "Modal"`, containing all interactive children
338
346
 
339
347
  **Why specialized tools matter:**
340
348
  - ✅ Trigger proper browser events (click, input, change)
package/index.js CHANGED
@@ -584,7 +584,7 @@ async function executeToolInternal(name, args) {
584
584
 
585
585
  // Initialize registry if needed
586
586
  const registry = window.__MODEL_REGISTRY__ || (() => {
587
- const reg = new ModelRegistry();
587
+ const reg = new window.ModelRegistry();
588
588
  if (window.ELEMENT_MODELS_CLASSES) {
589
589
  reg.registerAll(window.ELEMENT_MODELS_CLASSES);
590
590
  }
@@ -42,13 +42,22 @@ export class TextInputModel extends BaseInputModel {
42
42
  try {
43
43
  // Method 1: Try Puppeteer typing (works for most cases)
44
44
  try {
45
- // Focus and clear using JS (most reliable)
45
+ // Focus and clear using native setter (works with React/Vue/Angular controlled inputs)
46
46
  await withTimeout(
47
47
  () => this.element.evaluate((el, shouldClear) => {
48
48
  el.focus();
49
49
  el.click();
50
50
  if (shouldClear) {
51
- el.value = '';
51
+ // Use native HTMLInputElement setter to bypass React's value tracker
52
+ // React overrides the value setter and ignores programmatic changes via el.value = ''
53
+ const nativeSetter = Object.getOwnPropertyDescriptor(
54
+ window.HTMLInputElement.prototype, 'value'
55
+ )?.set;
56
+ if (nativeSetter) {
57
+ nativeSetter.call(el, '');
58
+ } else {
59
+ el.value = '';
60
+ }
52
61
  el.dispatchEvent(new Event('input', { bubbles: true }));
53
62
  el.dispatchEvent(new Event('change', { bubbles: true }));
54
63
  }
@@ -78,11 +87,20 @@ export class TextInputModel extends BaseInputModel {
78
87
  // Fall through to JS method
79
88
  }
80
89
 
81
- // Method 2: Fallback to direct JS value setting
90
+ // Method 2: Fallback to direct JS value setting (with React-compatible native setter)
82
91
  await withTimeout(
83
92
  () => this.element.evaluate((el, newValue, shouldClear) => {
84
93
  el.focus();
85
- el.value = shouldClear ? newValue : el.value + newValue;
94
+ const finalValue = shouldClear ? newValue : el.value + newValue;
95
+ // Use native setter to bypass React's value tracker
96
+ const nativeSetter = Object.getOwnPropertyDescriptor(
97
+ window.HTMLInputElement.prototype, 'value'
98
+ )?.set;
99
+ if (nativeSetter) {
100
+ nativeSetter.call(el, finalValue);
101
+ } else {
102
+ el.value = finalValue;
103
+ }
86
104
  el.dispatchEvent(new Event('input', { bubbles: true }));
87
105
  el.dispatchEvent(new Event('change', { bubbles: true }));
88
106
  }, value, clearFirst),
@@ -54,7 +54,15 @@ export class TextareaModel extends BaseInputModel {
54
54
  await withTimeout(
55
55
  () => this.element.evaluate(el => {
56
56
  el.focus();
57
- el.value = '';
57
+ // Use native setter to bypass React's value tracker
58
+ const nativeSetter = Object.getOwnPropertyDescriptor(
59
+ window.HTMLTextAreaElement.prototype, 'value'
60
+ )?.set;
61
+ if (nativeSetter) {
62
+ nativeSetter.call(el, '');
63
+ } else {
64
+ el.value = '';
65
+ }
58
66
  el.dispatchEvent(new Event('input', { bubbles: true }));
59
67
  el.dispatchEvent(new Event('change', { bubbles: true }));
60
68
  }),
@@ -82,11 +90,19 @@ export class TextareaModel extends BaseInputModel {
82
90
  // Fall through to JS method
83
91
  }
84
92
 
85
- // Method 2: Fallback to direct JS value setting
93
+ // Method 2: Fallback to direct JS value setting (with React-compatible native setter)
86
94
  await withTimeout(
87
95
  () => this.element.evaluate((el, newValue, shouldClear) => {
88
96
  el.focus();
89
- el.value = shouldClear ? newValue : el.value + newValue;
97
+ const finalValue = shouldClear ? newValue : el.value + newValue;
98
+ const nativeSetter = Object.getOwnPropertyDescriptor(
99
+ window.HTMLTextAreaElement.prototype, 'value'
100
+ )?.set;
101
+ if (nativeSetter) {
102
+ nativeSetter.call(el, finalValue);
103
+ } else {
104
+ el.value = finalValue;
105
+ }
90
106
  el.dispatchEvent(new Event('input', { bubbles: true }));
91
107
  el.dispatchEvent(new Event('change', { bubbles: true }));
92
108
  }, value, clearFirst),
package/models/index.js CHANGED
@@ -19,7 +19,7 @@ class TextInputModel extends ElementModel {
19
19
  }
20
20
 
21
21
  getActions() {
22
- return ['type', 'click', 'hover', 'screenshot'];
22
+ return ['type', 'clear', 'click', 'hover', 'screenshot'];
23
23
  }
24
24
 
25
25
  matches(element, elementType) {
@@ -32,6 +32,7 @@ class TextInputModel extends ElementModel {
32
32
  getActionHandler(actionName) {
33
33
  const handlers = {
34
34
  'type': 'executeTypeAction',
35
+ 'clear': 'executeTypeAction',
35
36
  'click': 'executeClickAction',
36
37
  'hover': 'executeHoverAction',
37
38
  'screenshot': 'executeScreenshotAction'
@@ -381,6 +382,42 @@ class ColorInputModel extends ElementModel {
381
382
  }
382
383
  }
383
384
 
385
+ /**
386
+ * Modal/Dialog Model
387
+ * Handles: Modal dialogs, popups, overlays (React Portals, framework modals)
388
+ * Detects elements rendered via portals outside the main React tree
389
+ */
390
+ class ModalModel extends ElementModel {
391
+ getName() {
392
+ return 'Modal';
393
+ }
394
+
395
+ getActions() {
396
+ return ['screenshot', 'close', 'scrollTo'];
397
+ }
398
+
399
+ getPriority() {
400
+ return 200; // High priority — check before containers
401
+ }
402
+
403
+ matches(element, elementType) {
404
+ // Only match actual dialog elements, not framework wrappers
405
+ // Framework wrappers are detected separately for portal inclusion
406
+ if (element.getAttribute('role') === 'dialog') return true;
407
+ if (element.getAttribute('aria-modal') === 'true') return true;
408
+ return false;
409
+ }
410
+
411
+ getActionHandler(actionName) {
412
+ const handlers = {
413
+ 'screenshot': 'executeScreenshotAction',
414
+ 'close': 'executeClickAction',
415
+ 'scrollTo': 'executeScrollToAction'
416
+ };
417
+ return handlers[actionName] || null;
418
+ }
419
+ }
420
+
384
421
  /**
385
422
  * Default Model (fallback for non-interactive elements)
386
423
  */
@@ -424,6 +461,7 @@ const MODELS = [
424
461
  DateInputModel,
425
462
  FileInputModel,
426
463
  ColorInputModel,
464
+ ModalModel,
427
465
  DefaultModel
428
466
  ];
429
467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "3.5.1",
3
+ "version": "3.5.2",
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",
@@ -16,7 +16,7 @@ function initializeModelRegistry() {
16
16
  }
17
17
 
18
18
  // Create and populate registry
19
- const registry = new ModelRegistry();
19
+ const registry = new (window.ModelRegistry || ModelRegistry)();
20
20
 
21
21
  // Register all models (order doesn't matter, priority is handled internally)
22
22
  if (typeof window !== 'undefined' && window.ELEMENT_MODELS_CLASSES) {
@@ -73,12 +73,51 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
73
73
  let idCounter = 0;
74
74
  const elementIds = new WeakMap();
75
75
  const interactiveElements = new WeakSet();
76
+ const modalElements = new WeakSet(); // Elements inside modal portals (skip visibility checks)
77
+ const modalAncestors = new WeakSet(); // Portal wrapper ancestors (force compact format)
76
78
 
77
79
  // First pass: mark all interactive elements
78
80
  if (interactiveOnly) {
79
81
  markInteractiveElements(document.body);
80
82
  }
81
83
 
84
+ // Second pass: detect modal/dialog portals and force-mark for inclusion
85
+ // Modals are rendered via React Portals outside the main tree and may have
86
+ // opacity: 0 during animation — force-include them and all their descendants
87
+ if (interactiveOnly) {
88
+ // Framework-specific portal container patterns (used only for detection, not model assignment)
89
+ const portalPatterns = [
90
+ 'ant-modal-root', 'ant-modal-wrap',
91
+ 'MuiDialog-root', 'MuiModal-root',
92
+ 'modal-dialog',
93
+ 'chakra-modal__content-container',
94
+ 'el-dialog__wrapper', 'el-overlay-dialog',
95
+ 'headlessui-dialog',
96
+ 'radix-dialog',
97
+ 'mantine-Modal-root',
98
+ ];
99
+ function isPortalElement(el) {
100
+ if (el.getAttribute('role') === 'dialog') return true;
101
+ if (el.getAttribute('aria-modal') === 'true') return true;
102
+ const classes = el.className || '';
103
+ if (typeof classes !== 'string') return false;
104
+ return portalPatterns.some(p => classes.includes(p));
105
+ }
106
+
107
+ // Scan body direct children for framework-specific portal roots
108
+ for (const child of document.body.children) {
109
+ if (isPortalElement(child)) {
110
+ forceMarkModalTree(child);
111
+ }
112
+ }
113
+ // Also find deeper dialog elements (some frameworks nest portals)
114
+ document.querySelectorAll('[role="dialog"], [aria-modal="true"]').forEach(el => {
115
+ if (!modalElements.has(el)) {
116
+ forceMarkModalTree(el);
117
+ }
118
+ });
119
+ }
120
+
82
121
  // Build tree from body
83
122
  result.tree = buildNode(document.body, null, 0, []);
84
123
 
@@ -408,6 +447,31 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
408
447
  interactiveElements.add(document.body);
409
448
  }
410
449
 
450
+ /**
451
+ * Force-mark modal portal element and all its descendants for inclusion in APOM tree.
452
+ * Modal portals (React Portals) are rendered outside the main tree and may have
453
+ * opacity: 0 during CSS animations — this ensures they're always included.
454
+ */
455
+ function forceMarkModalTree(element) {
456
+ modalElements.add(element);
457
+ interactiveElements.add(element);
458
+ // Mark all descendants — inputs, buttons, etc. inside the modal
459
+ element.querySelectorAll('*').forEach(el => {
460
+ modalElements.add(el);
461
+ interactiveElements.add(el);
462
+ });
463
+ // Mark ancestors up to body (so the path from body to modal is traversed)
464
+ // Must add to modalElements for isVisible() bypass (0x0 dimensions, opacity:0)
465
+ // Also mark as modalAncestors to force compact format (portal wrappers are pass-through)
466
+ let current = element.parentElement;
467
+ while (current && current !== document.body) {
468
+ modalElements.add(current);
469
+ modalAncestors.add(current);
470
+ interactiveElements.add(current);
471
+ current = current.parentElement;
472
+ }
473
+ }
474
+
411
475
  /**
412
476
  * Check if element is in viewport
413
477
  */
@@ -431,7 +495,11 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
431
495
  */
432
496
  function isVisible(el) {
433
497
  // Check dimensions first (works for fixed position elements)
434
- if (el.offsetWidth === 0 || el.offsetHeight === 0) return false;
498
+ // Exception: modal portal wrapper divs may have 0x0 dimensions
499
+ // while their visible content (dialog, inputs, buttons) does not
500
+ if (el.offsetWidth === 0 || el.offsetHeight === 0) {
501
+ if (!modalElements.has(el)) return false;
502
+ }
435
503
 
436
504
  // Check computed styles
437
505
  const style = window.getComputedStyle(el);
@@ -439,11 +507,12 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
439
507
  return false;
440
508
  }
441
509
 
442
- // Check opacity, but allow exceptions for inputs that are commonly styled with opacity:0
443
- // (checkboxes, radios, file inputs often use opacity:0 with custom visual overlay)
510
+ // Check opacity, but allow exceptions for:
511
+ // - inputs styled with opacity:0 (checkboxes, radios, file inputs with custom overlay)
512
+ // - elements inside modal portals (opacity: 0 during CSS appear animation)
444
513
  const tag = el.tagName.toLowerCase();
445
514
  const isStylableInput = tag === 'input' && ['checkbox', 'radio', 'file'].includes(el.type);
446
- if (style.opacity === '0' && !isStylableInput) {
515
+ if (style.opacity === '0' && !isStylableInput && !modalElements.has(el)) {
447
516
  return false;
448
517
  }
449
518
 
@@ -457,7 +526,8 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
457
526
 
458
527
  // Additional check: element should be in viewport or have offsetParent
459
528
  // This handles elements inside position:fixed containers (Angular Material)
460
- return el.offsetParent !== null || style.position === 'fixed' || style.position === 'sticky';
529
+ // Exception: modal portal elements may lack offsetParent
530
+ return el.offsetParent !== null || style.position === 'fixed' || style.position === 'sticky' || modalElements.has(el);
461
531
  }
462
532
 
463
533
  /**
@@ -505,8 +575,31 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
505
575
  }
506
576
  }
507
577
 
578
+ // Modal containers: promote to interactive node with metadata
579
+ if (modelName === 'Modal') {
580
+ elementType.isInteractive = true;
581
+ elementType.type = 'dialog';
582
+ // Extract modal title
583
+ const titleEl = element.querySelector(
584
+ '.ant-modal-title, .MuiDialogTitle-root, [class*="modal-title"], [class*="dialog-title"], .modal-header h5, .modal-header h4'
585
+ );
586
+ const titleText = titleEl ? titleEl.textContent.trim().substring(0, 100) : null;
587
+ // Extract action buttons
588
+ const buttons = element.querySelectorAll('button');
589
+ const actions = Array.from(buttons)
590
+ .map(b => b.textContent.trim())
591
+ .filter(t => t && t.length > 0 && t.length < 30);
592
+ elementType.metadata = {
593
+ ...(elementType.metadata || {}),
594
+ ...(titleText ? { title: titleText } : {}),
595
+ ...(actions.length ? { actions: actions.slice(0, 5) } : {})
596
+ };
597
+ }
598
+
508
599
  // Build node - minimize non-interactive parents
509
- const isInteractive = elementType.isInteractive;
600
+ // Modal ancestors (portal wrappers) are forced to compact format —
601
+ // they have onclick handlers (close on outside click) but are just pass-through containers
602
+ const isInteractive = elementType.isInteractive && !modalAncestors.has(element);
510
603
 
511
604
  // Build node structure based on mode
512
605
  let node;
@@ -562,16 +655,14 @@ function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
562
655
 
563
656
  // Update metadata counters
564
657
  result.metadata.totalElements++;
565
- if (elementType.isInteractive) {
658
+ if (isInteractive) {
566
659
  result.metadata.interactiveCount++;
567
660
  }
568
661
  if (elementType.type === 'form') {
569
662
  result.metadata.formCount++;
570
663
  }
571
- if (position && (position.type === 'fixed' || position.type === 'absolute')) {
572
- if (position.zIndex && position.zIndex >= 100) {
573
- result.metadata.modalCount++;
574
- }
664
+ if (modelName === 'Modal') {
665
+ result.metadata.modalCount++;
575
666
  }
576
667
  if (depth > result.metadata.maxDepth) {
577
668
  result.metadata.maxDepth = depth;