chrometools-mcp 2.4.2 → 2.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,126 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.5.0] - 2026-01-21
6
+
7
+ ### Added
8
+ - **`selectOption` tool** - Select options in HTML dropdown elements with intelligent priority-based selection
9
+ - Parameters: `selector` (required), `value`, `text`, or `index` (specify at least one)
10
+ - Selection priority: value → text → index (tries value first, falls back to text, then index)
11
+ - Automatically triggers `input` and `change` events for React and other frameworks
12
+ - Returns selected option details (value, text, index)
13
+ - Location: `index.js:911-979`, schemas in `server/tool-schemas.js:40-45`, definitions in `server/tool-definitions.js:234-247`, tool group in `server/tool-groups.js:10`
14
+
15
+ - **`drag` tool** - Drag element by mouse (click-hold-move-release) in any direction
16
+ - Parameters: `selector` (required), `direction` (required: 'up', 'down', 'left', 'right', 'up-left', 'up-right', 'down-left', 'down-right'), `distance` (optional, default: 100), `duration` (optional, default: 500ms)
17
+ - Emulates real mouse drag: moves to element center, presses button, drags, releases button
18
+ - Supports 8 directions including 4 diagonal directions for maximum flexibility
19
+ - Use for: interactive maps (Google Maps, Leaflet), Gantt charts, SVG diagrams, canvas, drag-to-pan interfaces
20
+ - NOT for: standard overflow scrollbars (use `scrollTo` or `scrollHorizontal` instead)
21
+ - Location: `index.js:982-1091`, schemas in `server/tool-schemas.js:47-53`, definitions in `server/tool-definitions.js:248-261`, tool group in `server/tool-groups.js:10`
22
+
23
+ - **`scrollHorizontal` tool** - Scroll element horizontally for tables, carousels, and wide content
24
+ - Parameters: `selector` (required), `direction` (required: 'left' or 'right'), `amount` (required: pixels or 'full'), `behavior` (optional: 'auto' or 'smooth')
25
+ - Supports precise pixel-based scrolling or 'full' to scroll to the end
26
+ - Returns detailed scroll state: position, total width, visible width, and scroll availability (canScrollLeft, canScrollRight)
27
+ - Uses native `scrollTo` API with smooth/auto behavior options
28
+ - Location: `index.js:1055-1117`, schemas in `server/tool-schemas.js:55-60`, definitions in `server/tool-definitions.js:262-275`, tool group in `server/tool-groups.js:10`
29
+
30
+ ### Fixed
31
+ - **🔥 CRITICAL: Fixed `drag` tool implementation** - Now correctly emulates mouse drag instead of changing scrollLeft/scrollTop
32
+ - Problem: Previous implementation used `scrollLeft`/`scrollTop` animation which only works for `overflow: auto/scroll` containers
33
+ - Impact: **Did not work with custom drag-to-scroll interfaces** like:
34
+ - ❌ Interactive maps (Google Maps, Leaflet, Mapbox)
35
+ - ❌ Gantt charts and timeline diagrams (SVG-based)
36
+ - ❌ Canvas elements with pan/zoom
37
+ - ❌ Custom drag handlers (React DnD, interact.js)
38
+ - Solution: Complete rewrite using Puppeteer's `page.mouse` API:
39
+ 1. Finds element center position
40
+ 2. Moves mouse to center (`page.mouse.move`)
41
+ 3. Presses mouse button (`page.mouse.down`)
42
+ 4. Drags to target position with smooth motion (`page.mouse.move` with steps)
43
+ 5. Releases mouse button (`page.mouse.up`)
44
+ - Result: **Now works with ANY drag-scrollable element** including SVG diagrams, maps, and custom implementations
45
+ - Location: `index.js:982-1091`, updated description in `README.md:277-285`
46
+ - Reported by: User testing on Gantt chart with `<svg class="gantt">` element
47
+
48
+ - **Fixed `analyzePage` crash with `includeAll: true` on SVG elements** - Now handles both HTML and SVG className types
49
+ - Problem: `className.split is not a function` error when page contains SVG elements
50
+ - Cause: SVG elements have `className` as `SVGAnimatedString` object (with `.baseVal` property), not a string
51
+ - Solution: Added type checking - uses `className.baseVal` for SVG elements, direct string for HTML
52
+ - Location: `index.js:2126-2137`
53
+
54
+ - **🔥 CRITICAL: Fixed Tailwind CSS selector generation bug** - `getUniqueSelectorInPage` now works correctly with Tailwind/utility-first CSS frameworks
55
+ - Problem: Generated invalid CSS selectors like `button.hover:bg-blue-700` containing special characters (`:`, `/`, `[]`)
56
+ - Impact: **ALL AI-powered tools failed** with `SyntaxError: invalid selector` on Tailwind/styled-components apps:
57
+ - ❌ `analyzePage` - couldn't read page state
58
+ - ❌ `findElementsByText` - couldn't find elements by text
59
+ - ❌ `smartFindElement` - couldn't find elements by description
60
+ - ❌ `getAllInteractiveElements` - couldn't list interactive elements
61
+ - Solution: Complete rewrite of selector generation logic with intelligent filtering:
62
+ 1. **New priority hierarchy** (most reliable first):
63
+ - `#id` (ID attribute)
64
+ - `[data-testid="..."]` (test IDs, very common in modern apps)
65
+ - `[data-*="..."]` (other data attributes)
66
+ - `[aria-label="..."]` (accessibility labels)
67
+ - `[role="..."]` (ARIA roles)
68
+ - `[name="..."]` (form element names)
69
+ - `tag.semantic-class` (non-Tailwind classes only)
70
+ - `tag:nth-of-type(n)` (fallback with path)
71
+ 2. **Tailwind class filtering** - New `isTailwindClass()` function detects and excludes:
72
+ - Variant classes with `:` (hover:, focus:, md:, lg:, etc.)
73
+ - Fraction classes with `/` (w-1/2, space-x-1/2)
74
+ - Arbitrary values with `[]` (bg-[#1da1f2], w-[500px])
75
+ - 60+ common Tailwind prefixes (bg-, text-, p-, m-, flex-, etc.)
76
+ 3. **CSS.escape() integration** - All selectors properly escaped (with fallback for old browsers)
77
+ 4. **Semantic attribute prioritization** - Prefers stable, meaningful selectors over utility classes
78
+ - Result: **Unblocks testing of ALL modern apps** using Tailwind, styled-components, CSS modules, Emotion, etc.
79
+ - Location: `element-finder-utils.js:316-509` (complete rewrite, ~200 lines)
80
+ - Reported by: AI agent encountering `SyntaxError` on every tool call in React+Tailwind app
81
+
82
+ ### Changed
83
+ - **Improved tool descriptions for better AI agent behavior** - Prevents premature use of `executeScript`
84
+ - `click` - Emphasized as PRIMARY tool for clicking, works with React/Vue/Angular synthetic events
85
+ - `type` - Emphasized as PRIMARY tool for input, updates React hooks and Vue reactive data correctly
86
+ - `executeScript` - ⚠️ Marked as LAST RESORT with strict warnings, never use for clicking/typing/reading
87
+ - `findElementsByText` - Highlighted as alternative to executeScript for finding elements
88
+ - `analyzePage` - Emphasized as PRIMARY tool for reading page state, more efficient than executeScript
89
+ - Location: `server/tool-definitions.js:31,45,162,510,489`
90
+
91
+ - **Added "Tool Usage Priority" section to README** - Clear hierarchy preventing executeScript abuse
92
+ - Three workflows: Clicking/Interaction, Filling Forms, Reading Page State
93
+ - Each shows specialized tools first (click, type, analyzePage), executeScript last
94
+ - Explains why specialized tools work with React/Vue/Angular while executeScript may fail
95
+ - Location: `README.md:116-147`
96
+
97
+ - **`analyzePage` enhancement** - Now detects and reports HTML select elements with all available options
98
+ - Select fields in forms and inputs sections now include `options` array with value, text, index, selected, and disabled status
99
+ - Includes `selectedIndex`, `selectedValue`, and `selectedText` for current selection
100
+ - Enables AI agents to see all dropdown options without additional queries
101
+ - Makes `selectOption` tool usage more intelligent and reliable
102
+ - Location: `index.js:1632-1660` (forms), `index.js:1691-1713` (inputs)
103
+
104
+ - **Tool groups** - Added 3 new tools to `interaction` group: `selectOption`, `dragScroll`, `scrollHorizontal`
105
+ - Total interaction tools: 8 (was 5)
106
+ - Total tools in project: 44+ (was 40+)
107
+ - Location: `server/tool-groups.js:10`
108
+
109
+ - **`convertFigmaToCode` tool** - Convert Figma designs to React/Tailwind code with AI assistance
110
+ - Parameters: `figmaToken` (optional), `fileKey` (required), `nodeId` (required), `framework` (optional: 'react', 'react-typescript', 'html'), `includeComments` (optional, default: true)
111
+ - Fetches design structure (layout, colors, typography, spacing) and rendered image at 2x scale
112
+ - Returns AI-optimized instruction prompt with simplified JSON structure and framework-specific guidelines
113
+ - Supports React (JavaScript), React (TypeScript), and pure HTML with Tailwind CSS
114
+ - Generates clean, semantic code with proper spacing, accessibility, and component structure
115
+ - Uses existing Figma token mechanism (from parameter or FIGMA_TOKEN env var)
116
+ - Location: `index.js:1676-1779`, schemas in `server/tool-schemas.js:225-231`, definitions in `server/tool-definitions.js:448-462`, tool group in `server/tool-groups.js:53`, helper in `figma-tools.js:381-499`
117
+
118
+ - **`simplifyNode` helper** - New function in figma-tools.js for code generation
119
+ - Recursively extracts essential design properties from Figma node structure
120
+ - Captures: layout (flexbox), dimensions, padding/gaps, colors (fills/strokes), effects (shadows), typography, border radius
121
+ - Filters out invisible elements and rounds numeric values for cleaner output
122
+ - Used by `convertFigmaToCode` to provide AI with actionable design data
123
+ - Location: `figma-tools.js:381-499`
124
+
5
125
  ## [2.4.2] - 2026-01-05
6
126
 
7
127
  ### Added
package/README.md CHANGED
@@ -8,10 +8,10 @@ MCP server for Chrome automation using Puppeteer with persistent browser session
8
8
  - [Usage](#usage)
9
9
  - [AI Optimization Features](#ai-optimization-features) ⭐ **NEW**
10
10
  - [Scenario Recorder](#scenario-recorder) ⭐ **NEW** - Visual UI-based recording with smart optimization
11
- - [Available Tools](#available-tools) - **40+ Tools Total**
11
+ - [Available Tools](#available-tools) - **44+ Tools Total**
12
12
  - [AI-Powered Tools](#ai-powered-tools) ⭐ **NEW** - smartFindElement, analyzePage, getAllInteractiveElements, findElementsByText
13
13
  - [Core Tools](#1-core-tools) - ping, openBrowser
14
- - [Interaction Tools](#2-interaction-tools) - click, type, scrollTo
14
+ - [Interaction Tools](#2-interaction-tools) - click, type, scrollTo, selectOption, dragScroll, scrollHorizontal
15
15
  - [Inspection Tools](#3-inspection-tools) - getElement, getComputedCss, getBoxModel, screenshot
16
16
  - [Advanced Tools](#4-advanced-tools) - executeScript, getConsoleLogs, listNetworkRequests, getNetworkRequest, filterNetworkRequests, hover, setStyles, setViewport, getViewport, navigateTo
17
17
  - [Recorder Tools](#6-recorder-tools) ⭐ **NEW** - enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
@@ -113,6 +113,39 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
113
113
 
114
114
  ## Available Tools
115
115
 
116
+ ### ⚠️ Tool Usage Priority
117
+
118
+ **CRITICAL: Always use specialized tools first. Never jump to `executeScript` as first choice.**
119
+
120
+ #### For Clicking/Interaction
121
+ 1. ✅ **`click()`** - PRIMARY tool for all clicks
122
+ - Works correctly with React/Vue/Angular synthetic events
123
+ - Handles button clicks, link navigation, form submissions
124
+ 2. ✅ **`findElementsByText()` + action** - When selector is unknown, find by text
125
+ 3. ⚠️ **`executeScript()`** - LAST RESORT, only if above failed
126
+
127
+ #### For Filling Forms
128
+ 1. ✅ **`type()`** - PRIMARY tool for all text input
129
+ - Properly updates React hooks, Vue reactive data
130
+ - Auto-clears field before typing (configurable)
131
+ 2. ⚠️ **`executeScript()`** - LAST RESORT, only if above failed
132
+
133
+ #### For Reading Page State
134
+ 1. ✅ **`analyzePage()`** - PRIMARY tool for reading page content
135
+ - Gets forms, inputs, buttons, links with current values
136
+ - Use `refresh: true` after interactions to see updated state
137
+ - Efficient: 2-5k tokens vs screenshot 15-25k
138
+ 2. ✅ **`findElementsByText()`** - Find specific elements by visible text
139
+ 3. ✅ **`getElement()`** - Get HTML of specific element
140
+ 4. ⚠️ **`executeScript()`** - LAST RESORT, only if above failed
141
+
142
+ **Why specialized tools matter:**
143
+ - ✅ Trigger proper browser events (click, input, change)
144
+ - ✅ Work with React/Vue/Angular synthetic event systems
145
+ - ✅ Update framework state correctly (React hooks, Vue reactivity)
146
+ - ✅ Handle animations, navigation, and async updates
147
+ - ❌ `executeScript` bypasses framework events and may fail silently
148
+
116
149
  ### AI-Powered Tools
117
150
 
118
151
  #### smartFindElement ⭐
@@ -229,6 +262,40 @@ Scroll page to bring element into view.
229
262
  - **Use case**: Lazy loading, sticky elements, visibility checks
230
263
  - **Returns**: Final scroll position
231
264
 
265
+ #### selectOption
266
+ Select option in dropdown (HTML select elements). Automatically detected by analyzePage with all available options.
267
+ - **Parameters**:
268
+ - `selector` (required): CSS selector for select element
269
+ - `value` (optional): Option value attribute (priority 1)
270
+ - `text` (optional): Option text content (priority 2)
271
+ - `index` (optional): Option index, 0-based (priority 3)
272
+ - **Use case**: Form dropdowns, filtering, selection menus
273
+ - **Returns**: Selected option details (value, text, index)
274
+ - **Selection priority**: If multiple parameters specified, tries value → text → index
275
+ - **AI Integration**: Use `analyzePage` to see all available options with their values, text, and indices
276
+
277
+ #### drag
278
+ Drag element by mouse (click-hold-move-release). Simulates real mouse drag, not scrollbar scrolling.
279
+ - **Parameters**:
280
+ - `selector` (required): CSS selector for element to drag
281
+ - `direction` (required): 'up', 'down', 'left', 'right', 'up-left', 'up-right', 'down-left', 'down-right'
282
+ - `distance` (optional): Distance in pixels (default: 100)
283
+ - `duration` (optional): Drag duration in milliseconds (default: 500)
284
+ - **Use case**: Interactive maps (Google Maps, Leaflet), Gantt charts, SVG diagrams, canvas elements, sliders, drag-to-pan interfaces
285
+ - **How it works**: Moves mouse to element center, presses mouse button, drags to target position, releases button
286
+ - **NOT for**: Standard overflow scrollbars (use `scrollTo` or `scrollHorizontal` instead)
287
+ - **Returns**: Start/end mouse positions and drag delta
288
+
289
+ #### scrollHorizontal
290
+ Scroll element horizontally (for tables, carousels, wide content).
291
+ - **Parameters**:
292
+ - `selector` (required): CSS selector for element to scroll
293
+ - `direction` (required): 'left' or 'right'
294
+ - `amount` (required): Number of pixels to scroll, or 'full' to scroll to the end
295
+ - `behavior` (optional): 'auto' or 'smooth' (default: 'auto')
296
+ - **Use case**: Wide tables, image carousels, horizontally scrollable containers
297
+ - **Returns**: Scroll state (position, total width, visible width, scroll availability)
298
+
232
299
  ### 3. Inspection Tools
233
300
 
234
301
  #### getElement
@@ -472,6 +539,27 @@ Extract complete color palette with usage statistics.
472
539
  - Usage examples (where the color is used)
473
540
  - Sorted by usage frequency
474
541
 
542
+ #### convertFigmaToCode ⭐ NEW
543
+ Convert Figma designs to React/Tailwind code with AI assistance.
544
+ - **Parameters**:
545
+ - `figmaToken` (optional): Figma API token
546
+ - `fileKey` (required): Figma file key
547
+ - `nodeId` (required): Frame/component ID (formats: '123:456' or '123-456')
548
+ - `framework` (optional): 'react', 'react-typescript', or 'html' (default: 'react')
549
+ - `includeComments` (optional): Include code comments (default: true)
550
+ - **Use case**: Rapid prototyping, design-to-code workflow, implementing Figma designs
551
+ - **How it works**:
552
+ 1. Fetches design structure (layout, colors, typography, spacing)
553
+ 2. Gets rendered design image at 2x resolution
554
+ 3. Returns AI-optimized instructions with simplified JSON structure
555
+ 4. AI generates clean React/Tailwind code matching the design
556
+ - **Returns**: Formatted instruction prompt containing:
557
+ - Design image reference
558
+ - Simplified JSON structure with layout, styling, text properties
559
+ - Framework-specific guidelines (React components, TypeScript types, Tailwind classes)
560
+ - Quality requirements (semantic HTML, accessibility, accurate spacing)
561
+ - **Best for**: UI components, landing pages, card designs, navigation bars
562
+
475
563
  #### getFigmaFrame
476
564
  Export and download a Figma frame as PNG/JPG image with automatic compression.
477
565
  - **Parameters**:
@@ -997,9 +1085,9 @@ Each tool definition is sent to the AI in every request, consuming context token
997
1085
  | `debug` | Debugging & network | `getConsoleLogs`, `listNetworkRequests`, `getNetworkRequest`, `filterNetworkRequests` (4) |
998
1086
  | `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `getAllInteractiveElements`, `findElementsByText` (9) |
999
1087
  | `recorder` | Scenario recording | `enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`, `appendScenarioToFile`, `generatePageObject` (9) |
1000
- | `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette` (9) |
1088
+ | `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette`, `convertFigmaToCode` (10) |
1001
1089
 
1002
- **Total:** 43 tools across 7 groups
1090
+ **Total:** 44 tools across 7 groups
1003
1091
 
1004
1092
  **Configuration:**
1005
1093
 
@@ -0,0 +1,109 @@
1
+ # Release v2.5.0 - Major Update: 4 New Tools + Critical Fixes
2
+
3
+ ## 🎉 New Tools (4)
4
+
5
+ ### 1. `selectOption` - Dropdown Selection
6
+ Select options in HTML dropdown elements with intelligent priority-based selection.
7
+ - **Parameters**: `selector`, `value`, `text`, or `index`
8
+ - **Priority**: value → text → index
9
+ - **React/Vue support**: Automatically triggers input/change events
10
+ - Perfect for automated form filling
11
+
12
+ ### 2. `drag` - Mouse Drag Emulation
13
+ Drag elements by mouse (click-hold-move-release) in any direction.
14
+ - **8 directions**: up, down, left, right, + 4 diagonals
15
+ - **Use cases**: Interactive maps, Gantt charts, SVG diagrams, canvas
16
+ - **How it works**: Puppeteer's `page.mouse` API for real drag events
17
+ - **NOT for**: Standard overflow scrollbars (use `scrollTo` instead)
18
+
19
+ ### 3. `scrollHorizontal` - Horizontal Scrolling
20
+ Scroll elements horizontally for tables, carousels, and wide content.
21
+ - **Directions**: left, right
22
+ - **Amount**: Pixels or 'full' to scroll to end
23
+ - **Behavior**: smooth or auto
24
+ - Returns detailed scroll state
25
+
26
+ ### 4. `convertFigmaToCode` ⭐
27
+ Convert Figma designs to React+Tailwind code.
28
+ - **Frameworks**: React, React TypeScript, HTML
29
+ - **Styling**: Tailwind CSS optimized
30
+ - **Input**: Figma file key + node ID
31
+ - **Output**: Clean, semantic component code with AI-generated instructions
32
+
33
+ ## 🔥 Critical Fixes
34
+
35
+ ### Fixed Tailwind CSS Selector Generation Bug
36
+ **Impact**: ALL AI tools failed on Tailwind/styled-components apps with `SyntaxError: invalid selector`
37
+
38
+ **Affected tools**:
39
+ - ❌ `analyzePage` - couldn't read page state
40
+ - ❌ `findElementsByText` - couldn't find elements
41
+ - ❌ `smartFindElement` - couldn't locate elements
42
+ - ❌ `getAllInteractiveElements` - couldn't list interactive elements
43
+
44
+ **Solution**:
45
+ - New `isTailwindClass()` function filters utility classes with `:`, `/`, `[]`
46
+ - Smart priority: id → data-testid → aria-label → semantic classes
47
+ - CSS.escape() for all selectors
48
+ - **Result**: ✅ Unblocks testing of ALL modern apps (Tailwind, styled-components, CSS modules, Emotion)
49
+
50
+ ### Fixed `drag` Tool Implementation
51
+ Previously used `scrollLeft`/`scrollTop` which only works with `overflow: auto/scroll` containers.
52
+
53
+ **Now works with**:
54
+ - ✅ Interactive maps (Google Maps, Leaflet, Mapbox)
55
+ - ✅ Gantt charts and SVG diagrams
56
+ - ✅ Canvas elements with pan/zoom
57
+ - ✅ Custom drag handlers (React DnD, interact.js)
58
+
59
+ ### Fixed `analyzePage` SVG Crash
60
+ Fixed `className.split is not a function` error when page contains SVG elements.
61
+ - Now handles both HTML (string) and SVG (SVGAnimatedString) className types
62
+
63
+ ## 🔧 Improvements
64
+
65
+ ### Better AI Agent Behavior
66
+ - Improved tool descriptions to prevent premature `executeScript` usage
67
+ - `click`, `type`, `analyzePage` emphasized as PRIMARY tools
68
+ - `executeScript` marked as ⚠️ LAST RESORT
69
+ - New "Tool Usage Priority" section in README
70
+
71
+ ### Enhanced `analyzePage`
72
+ - Now detects HTML select elements with all options
73
+ - Returns: value, text, index, selected, disabled status
74
+ - Enables smarter `selectOption` usage
75
+
76
+ ## 📊 Stats
77
+
78
+ - **Total tools**: 44 (was 40)
79
+ - **Files changed**: 9
80
+ - **Lines added**: +957
81
+ - **Lines removed**: -48
82
+
83
+ ## 🚀 Installation
84
+
85
+ ```bash
86
+ npx chrometools-mcp
87
+ ```
88
+
89
+ Or update in your MCP config:
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "chrometools": {
94
+ "command": "npx",
95
+ "args": ["chrometools-mcp"]
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## 🙏 Contributors
102
+
103
+ Special thanks to users who reported issues:
104
+ - Tailwind CSS selector bug reporter
105
+ - Gantt chart drag testing feedback
106
+
107
+ ---
108
+
109
+ **Full Changelog**: https://github.com/docentovich/chrometools-mcp/blob/main/CHANGELOG.md
@@ -305,73 +305,181 @@ function isSafeSelectorValue(value) {
305
305
  return !/["'\\[\]{}()]/.test(value);
306
306
  }
307
307
 
308
+ /**
309
+ * Check if a class name is a Tailwind CSS utility class
310
+ * Tailwind classes contain special characters that break CSS selectors:
311
+ * - Colons (:) for variants like hover:, focus:, md:, etc.
312
+ * - Slashes (/) for fractions like w-1/2
313
+ * - Brackets ([]) for arbitrary values like bg-[#1da1f2]
314
+ * - Dots (.) in decimal values
315
+ */
316
+ function isTailwindClass(className) {
317
+ if (!className || typeof className !== 'string') return false;
318
+
319
+ // Check for special characters that indicate Tailwind variants/utilities
320
+ if (/[:\/\[\]]/.test(className)) {
321
+ return true;
322
+ }
323
+
324
+ // Common Tailwind utility prefixes
325
+ const tailwindPrefixes = [
326
+ 'bg-', 'text-', 'border-', 'rounded-', 'shadow-', 'font-', 'leading-',
327
+ 'flex-', 'grid-', 'p-', 'px-', 'py-', 'pt-', 'pb-', 'pl-', 'pr-',
328
+ 'm-', 'mx-', 'my-', 'mt-', 'mb-', 'ml-', 'mr-', 'space-',
329
+ 'w-', 'h-', 'min-', 'max-', 'gap-', 'inset-', 'top-', 'right-', 'bottom-', 'left-',
330
+ 'justify-', 'items-', 'content-', 'self-', 'place-',
331
+ 'overflow-', 'opacity-', 'cursor-', 'transition-', 'transform-',
332
+ 'duration-', 'ease-', 'delay-', 'animate-', 'scale-', 'rotate-', 'translate-',
333
+ 'z-', 'order-', 'col-', 'row-', 'auto-', 'tracking-', 'select-',
334
+ 'sr-', 'not-', 'pointer-', 'resize-', 'list-', 'appearance-',
335
+ 'underline', 'line-through', 'no-underline', 'uppercase', 'lowercase', 'capitalize',
336
+ 'italic', 'not-italic', 'ordinal', 'slashed-zero', 'lining-nums', 'oldstyle-nums',
337
+ 'proportional-nums', 'tabular-nums', 'diagonal-fractions', 'stacked-fractions',
338
+ 'hidden', 'block', 'inline', 'flex', 'grid', 'table', 'contents',
339
+ 'absolute', 'relative', 'fixed', 'sticky', 'static'
340
+ ];
341
+
342
+ // Check if className starts with known Tailwind prefix
343
+ return tailwindPrefixes.some(prefix => className.startsWith(prefix));
344
+ }
345
+
308
346
  /**
309
347
  * Generate unique CSS selector for an element
348
+ * Priority: id → data-testid → data-* → aria-label → role → semantic classes → nth-child
349
+ * Filters out Tailwind CSS classes to avoid invalid selectors
310
350
  */
311
351
  function getUniqueSelectorInPage(element) {
312
- // Try ID first
313
- if (element.id) {
314
- return `#${element.id}`;
352
+ const tagName = element.tagName.toLowerCase();
353
+
354
+ // 1. Try ID first (highest priority)
355
+ if (element.id && isSafeSelectorValue(element.id)) {
356
+ try {
357
+ return `#${CSS.escape(element.id)}`;
358
+ } catch (e) {
359
+ // CSS.escape not available in old browsers, use simple escape
360
+ return `#${element.id.replace(/[^\w-]/g, '\\$&')}`;
361
+ }
315
362
  }
316
363
 
317
- // Try unique class combination
318
- if (element.className) {
319
- const classes = element.className.split(' ').filter(c => c.trim());
320
- if (classes.length > 0) {
321
- const selector = `${element.tagName.toLowerCase()}.${classes.join('.')}`;
364
+ // 2. Try data-testid (very common in modern apps)
365
+ if (element.dataset && element.dataset.testid && isSafeSelectorValue(element.dataset.testid)) {
366
+ const selector = `[data-testid="${element.dataset.testid}"]`;
367
+ try {
322
368
  if (document.querySelectorAll(selector).length === 1) {
323
369
  return selector;
324
370
  }
325
- // Try with first class only
326
- const firstClassSelector = `${element.tagName.toLowerCase()}.${classes[0]}`;
327
- if (document.querySelectorAll(firstClassSelector).length === 1) {
328
- return firstClassSelector;
371
+ } catch (e) {
372
+ // Invalid selector, continue
373
+ }
374
+ }
375
+
376
+ // 3. Try other data-* attributes (only if values are safe)
377
+ const dataAttrs = Array.from(element.attributes)
378
+ .filter(attr => attr.name.startsWith('data-') &&
379
+ attr.name !== 'data-testid' &&
380
+ isSafeSelectorValue(attr.value))
381
+ .slice(0, 2);
382
+
383
+ for (const attr of dataAttrs) {
384
+ const selector = `${tagName}[${attr.name}="${attr.value}"]`;
385
+ try {
386
+ if (document.querySelectorAll(selector).length === 1) {
387
+ return selector;
329
388
  }
389
+ } catch (e) {
390
+ continue;
330
391
  }
331
392
  }
332
393
 
333
- // Try name attribute (only if value is safe)
334
- if (element.name && isSafeSelectorValue(element.name)) {
335
- const selector = `${element.tagName.toLowerCase()}[name="${element.name}"]`;
394
+ // 4. Try aria-label
395
+ const ariaLabel = element.getAttribute('aria-label');
396
+ if (ariaLabel && isSafeSelectorValue(ariaLabel)) {
397
+ const selector = `${tagName}[aria-label="${ariaLabel}"]`;
336
398
  try {
337
399
  if (document.querySelectorAll(selector).length === 1) {
338
400
  return selector;
339
401
  }
340
402
  } catch (e) {
341
- // Invalid selector, skip
403
+ // Invalid selector, continue
342
404
  }
343
405
  }
344
406
 
345
- // Try data attributes (only if values are safe)
346
- const dataAttrs = Array.from(element.attributes)
347
- .filter(attr => attr.name.startsWith('data-') && isSafeSelectorValue(attr.value))
348
- .slice(0, 2);
407
+ // 5. Try role attribute
408
+ const role = element.getAttribute('role');
409
+ if (role && isSafeSelectorValue(role)) {
410
+ const selector = `${tagName}[role="${role}"]`;
411
+ try {
412
+ if (document.querySelectorAll(selector).length === 1) {
413
+ return selector;
414
+ }
415
+ } catch (e) {
416
+ // Invalid selector, continue
417
+ }
418
+ }
349
419
 
350
- for (const attr of dataAttrs) {
351
- const selector = `${element.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`;
420
+ // 6. Try name attribute (common for form inputs)
421
+ if (element.name && isSafeSelectorValue(element.name)) {
422
+ const selector = `${tagName}[name="${element.name}"]`;
352
423
  try {
353
424
  if (document.querySelectorAll(selector).length === 1) {
354
425
  return selector;
355
426
  }
356
427
  } catch (e) {
357
- // Invalid selector, skip
358
- continue;
428
+ // Invalid selector, continue
429
+ }
430
+ }
431
+
432
+ // 7. Try semantic classes (filter out Tailwind)
433
+ if (element.className && typeof element.className === 'string') {
434
+ const classes = element.className.split(' ')
435
+ .filter(c => c.trim() && !isTailwindClass(c))
436
+ .slice(0, 3); // Limit to 3 classes max
437
+
438
+ if (classes.length > 0) {
439
+ try {
440
+ // Try with all filtered classes
441
+ const escapedClasses = classes.map(c => {
442
+ try {
443
+ return CSS.escape(c);
444
+ } catch (e) {
445
+ return c.replace(/[^\w-]/g, '\\$&');
446
+ }
447
+ });
448
+ const selector = `${tagName}.${escapedClasses.join('.')}`;
449
+ if (document.querySelectorAll(selector).length === 1) {
450
+ return selector;
451
+ }
452
+
453
+ // Try with first class only
454
+ const firstClassSelector = `${tagName}.${escapedClasses[0]}`;
455
+ if (document.querySelectorAll(firstClassSelector).length === 1) {
456
+ return firstClassSelector;
457
+ }
458
+ } catch (e) {
459
+ // Invalid selector, continue to fallback
460
+ }
359
461
  }
360
462
  }
361
463
 
362
- // Fallback: nth-child
464
+ // 8. Fallback: nth-of-type with path
363
465
  let current = element;
364
466
  const path = [];
365
467
 
366
468
  while (current && current.tagName) {
367
469
  let selector = current.tagName.toLowerCase();
368
470
 
369
- if (current.id) {
370
- selector = `#${current.id}`;
471
+ // Stop at element with ID
472
+ if (current.id && isSafeSelectorValue(current.id)) {
473
+ try {
474
+ selector = `#${CSS.escape(current.id)}`;
475
+ } catch (e) {
476
+ selector = `#${current.id.replace(/[^\w-]/g, '\\$&')}`;
477
+ }
371
478
  path.unshift(selector);
372
479
  break;
373
480
  }
374
481
 
482
+ // Calculate nth-of-type
375
483
  let sibling = current;
376
484
  let nth = 1;
377
485
 
@@ -382,7 +490,9 @@ function getUniqueSelectorInPage(element) {
382
490
  }
383
491
  }
384
492
 
385
- if (nth > 1) {
493
+ // Only add nth-of-type if there are multiple siblings of same type
494
+ if (nth > 1 || (current.parentElement &&
495
+ Array.from(current.parentElement.children).filter(c => c.tagName === current.tagName).length > 1)) {
386
496
  selector += `:nth-of-type(${nth})`;
387
497
  }
388
498