chrometools-mcp 3.5.6 → 3.6.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 +15 -0
- package/README.md +92 -5
- package/browser/browser-manager.js +26 -2
- package/browser/page-manager.js +89 -0
- package/element-finder-utils.js +38 -0
- package/index.js +208 -56
- package/package.json +1 -1
- package/server/tool-definitions.js +21 -0
- package/server/tool-groups.js +1 -1
- package/server/tool-schemas.js +9 -0
- package/utils/actions/click-action.js +38 -1
- package/utils/element-actions.js +8 -5
- package/utils/platform-utils.js +17 -2
- package/utils/post-click-diagnostics.js +21 -4
- package/specs/SEGM-537-UNBLOCKERS_PROGRESS.md +0 -94
- package/specs/SEGM-537-UNBLOCKERS_SPEC.md +0 -187
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.6.0] - 2026-06-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **iframe automation (`switchFrame` / `listFrames`)** — Automate pages whose UI lives in an `<iframe>`, including **cross-origin** ones (resolved over CDP, bypassing Same-Origin Policy). `switchFrame({ frameUrl | frameSelector })` sets an active frame so `click`, `type`, `hover`, `selectOption`, `pressKey`, `scrollTo`, `waitForElement`, `analyzePage`, `findElementsByText`, `smartFindElement`, `executeScript` all run inside it; call `switchFrame()` with no args to reset. `listFrames()` lists frames; `analyzePage` also reports a `frames` array when >1 frame exists. Active frame auto-resets on `navigateTo`
|
|
9
|
+
- **Browser connection env vars** — `CHROMETOOLS_BROWSER_WS_ENDPOINT` (connect directly to a CDP WebSocket), `CHROMETOOLS_DEBUG_PORT` (default 9222), `CHROMETOOLS_USER_DATA_DIR` (use a real/logged-in Chrome profile instead of a blank temp one), `CHROMETOOLS_CHROME_PATH`. Lets the server attach to your authenticated Chrome session — required for sites that need real cookies/cross-subdomain session
|
|
10
|
+
- **`smartFindElement({ minConfidence })`** — Confidence threshold (default 0.6) gating auto-`action`. If the best match is below it or too close to the runner-up, the action is skipped and candidates are returned with an `actionSkipped` reason. Candidate coverage broadened (`[onclick]`, `[role=menuitem]`, `[role=tab]`, nav links); scoring penalizes text that doesn't match the description and rewards nav/menu context
|
|
11
|
+
- **`click({ waitForRouteChange })`** — For SPAs: after click, waits for `location.pathname+search` to change and reports `routeChanged:true/false` (never fails the click on timeout)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **`executeScript` top-level `return`** — Now runs the snippet as-is first and only re-wraps in an async IIFE if the engine reports an "Illegal return statement". Handles `const x=…; return x`, callbacks containing `function`, and bare expressions — replaces the fragile `^return` + no-`function` heuristic
|
|
15
|
+
- **`navigateTo` network summary** — Lists only XHR/Fetch requests (static assets hidden and counted), capped at 12 with a `… N more` note, instead of dumping every chunk/CSS/font
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`findElementsByText({ action })` in iframes** — The action path resolved the selector against the main frame even after `switchFrame`, causing `Element not found for action`. Now resolves inside the active frame
|
|
19
|
+
|
|
5
20
|
## [3.5.6] - 2026-05-28
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -219,13 +219,14 @@ The Chrome Extension is **required** for scenario recording and other advanced f
|
|
|
219
219
|
- [Installation](#installation)
|
|
220
220
|
- [Chrome Extension Setup](#chrome-extension-setup)
|
|
221
221
|
- [AI Optimization Features](#ai-optimization-features)- [Scenario Recorder](#scenario-recorder) - Visual UI-based recording with smart optimization
|
|
222
|
-
- [Available Tools](#available-tools) - **
|
|
222
|
+
- [Available Tools](#available-tools) - **51+ Tools Total**
|
|
223
223
|
- [AI-Powered Tools](#ai-powered-tools) - smartFindElement, analyzePage, getElementDetails, findElementsByText
|
|
224
224
|
- [Core Tools](#1-core-tools) - ping, openBrowser
|
|
225
225
|
- [Interaction Tools](#2-interaction-tools) - click, type, scrollTo, selectOption, selectFromGroup, drag, scrollHorizontal
|
|
226
226
|
- [Inspection Tools](#3-inspection-tools) - getElement, getComputedCss, getBoxModel, screenshot
|
|
227
227
|
- [Advanced Tools](#4-advanced-tools) - executeScript, getConsoleLogs, listNetworkRequests, getNetworkRequest, filterNetworkRequests, hover, pressKey, setStyles, setViewport, getViewport, navigateTo
|
|
228
228
|
- [Tab Management Tools](#5-tab-management-tools) - listTabs, switchTab
|
|
229
|
+
- [Frame Tools](#5a-frame-tools-iframe-automation) - listFrames, switchFrame (cross-origin iframe automation)
|
|
229
230
|
- [Recorder Tools](#7-recorder-tools) - enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
|
|
230
231
|
- [API / Swagger Tools](#8-api--swagger-tools) - loadSwagger, generateApiModels
|
|
231
232
|
- [Typical Workflow Example](#typical-workflow-example)
|
|
@@ -357,6 +358,8 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
|
|
|
357
358
|
- **Parameters**:
|
|
358
359
|
- `description` (required): Natural language (e.g., "login button", "email field")
|
|
359
360
|
- `maxResults` (optional): Max candidates to return (default: 5)
|
|
361
|
+
- `minConfidence` (optional): Confidence threshold (0–1, default **0.6**) for auto-executing `action`. If the best match scores below this — or is too close to the runner-up — the action is **skipped** and candidates are returned with an `actionSkipped` reason instead. Prevents auto-clicking the wrong control (e.g. a primary form submit when you asked for a menu item). Lower it to act on weaker matches.
|
|
362
|
+
- **Candidate coverage**: besides `button`/`input`/`a`/`[role=button]`, also considers `[onclick]`, `[role=menuitem]`, `[role=tab]`, and links inside `nav`/`[role=navigation]`/`[role=menu]` — so menu items rendered as `div`/`span[onclick]` are found. Scoring penalizes candidates whose text doesn't match the description and rewards navigation/menu context.
|
|
360
363
|
- **Use case**: When you don't know the exact selector
|
|
361
364
|
- **Returns**: Ranked candidates with confidence scores, selectors, and reasoning
|
|
362
365
|
- **Example**:
|
|
@@ -519,6 +522,7 @@ Click an element with optional result screenshot. **PREFERRED**: Use APOM ID fro
|
|
|
519
522
|
- `networkWaitTimeout` (optional): Custom network wait timeout in ms (default: 10000). Only used if skipNetworkWait is false.
|
|
520
523
|
- `waitForSelector` (optional): CSS selector to wait for **after** the click — atomic click+wait. Use for dropdowns/popups that render into a React Portal and otherwise race with the next MCP call. Example: `click({ id: 'button_47', waitForSelector: '#menu-popup-root > div' })`.
|
|
521
524
|
- `waitTimeoutMs` (optional): Timeout for `waitForSelector` in ms (default: 2000). On timeout the click still succeeds but the result text reports `⚠️ WAIT_TIMEOUT`.
|
|
525
|
+
- `waitForRouteChange` (optional): For SPAs (React Router etc.). After the click, waits for `location.pathname + location.search` to change relative to before, then reports `Route changed: "/a" → "/b"` or `⚠️ ROUTE_UNCHANGED`. Because SPAs navigate via `history.pushState`, plain network diagnostics may not register the navigation — this makes "success" mean the view actually changed, not just that the click was delivered. Never fails the click on timeout. For view changes that don't alter the URL, use `waitForSelector` instead.
|
|
522
526
|
- `autoAnalyzeAfter` (optional): After click, automatically diff APOM and append the delta to the result text (e.g. `+3 appeared: button_42:"Статистика", button_43:"Настройки", link_44:"Удалить"`). New element ids are pre-registered so the next `click({ id })`/`type({ id })` call works **without an extra `analyzePage`**. Designed for the dropdown/menu pattern: one MCP call instead of three.
|
|
523
527
|
- **Use case**: Buttons, links, form submissions, Django admin forms
|
|
524
528
|
- **Returns**: Confirmation text + optional screenshot + network diagnostics
|
|
@@ -725,7 +729,8 @@ Execute arbitrary JavaScript in page context with optional screenshot.
|
|
|
725
729
|
- **Use case**: Complex interactions, custom manipulations
|
|
726
730
|
- **Returns**: Execution result + optional screenshot
|
|
727
731
|
- **Performance**: 2-10x faster without screenshot
|
|
728
|
-
- **Top-level `return`**:
|
|
732
|
+
- **Top-level `return`**: any snippet using a top-level `return` (e.g. `return document.title`, `const x = 1; return x`, or code that contains a `function` in a callback) just works — it's run as-is first, and only re-wrapped in an async IIFE if the engine reports an "Illegal return statement". Bare expressions like `document.title` still return their value. No manual `(() => { ... })()` wrapping needed.
|
|
733
|
+
- **Frames**: runs inside the active frame after `switchFrame` (see [Frame Tools](#5a-frame-tools-iframe-automation)); defaults to the main frame.
|
|
729
734
|
|
|
730
735
|
#### getConsoleLogs
|
|
731
736
|
Retrieve browser console logs (log, warn, error, etc.).
|
|
@@ -848,7 +853,8 @@ Navigate to different URL while keeping browser instance.
|
|
|
848
853
|
- `url` (required)
|
|
849
854
|
- `waitUntil` (optional): load event type
|
|
850
855
|
- **Use case**: Moving between pages in workflow
|
|
851
|
-
- **Returns**: New page title
|
|
856
|
+
- **Returns**: New page title. The post-navigation network summary lists only **XHR/Fetch** requests (static assets — JS chunks, CSS, fonts, images — are hidden and counted) and is capped at the first 12 with a `… N more` note, so a heavy SPA load doesn't bury the signal.
|
|
857
|
+
- **Note**: resets the active frame back to the main frame (see [Frame Tools](#5a-frame-tools-iframe-automation)).
|
|
852
858
|
|
|
853
859
|
### 5. Tab Management Tools
|
|
854
860
|
Tools for managing multiple browser tabs. New tabs opened via `window.open()`, `target="_blank"`, or user actions are automatically detected and tracked.
|
|
@@ -891,6 +897,45 @@ switchTab({ tab: 0 })
|
|
|
891
897
|
switchTab({ tab: "google.com" })
|
|
892
898
|
```
|
|
893
899
|
|
|
900
|
+
### 5a. Frame Tools (iframe automation)
|
|
901
|
+
|
|
902
|
+
Tools for automating pages whose UI lives inside an `<iframe>` — including **cross-origin** iframes (e.g. a widget hosted on a different subdomain). Cross-origin frames can't be reached from page JavaScript (Same-Origin Policy blocks `iframe.contentDocument`), but ChromeTools resolves them over CDP, so the SOP doesn't apply.
|
|
903
|
+
|
|
904
|
+
By default all tools operate on the **main frame**. After `switchFrame`, the element tools — `click`, `type`, `hover`, `selectOption`, `pressKey`, `scrollTo`, `waitForElement`, `analyzePage`, `findElementsByText`, `smartFindElement`, `executeScript` — run **inside the selected frame** until you reset. The active frame is reset automatically on `navigateTo`.
|
|
905
|
+
|
|
906
|
+
#### listFrames
|
|
907
|
+
List all frames on the current page so you can discover which one to switch into.
|
|
908
|
+
- **Parameters**: None
|
|
909
|
+
- **Returns**: `{ count, activeFrame, frames: [{ url, name, isMain }], hint }`
|
|
910
|
+
- **Use case**: Find a cross-origin iframe (e.g. `app.example.com`) before switching into it
|
|
911
|
+
|
|
912
|
+
#### switchFrame
|
|
913
|
+
Set the active frame for subsequent element tools. Call with **no arguments** to reset back to the main frame.
|
|
914
|
+
- **Parameters** (provide one, or none to reset):
|
|
915
|
+
- `frameUrl` (optional): substring matched against each frame's URL (e.g. `"app.example.com"`). Selects the first match.
|
|
916
|
+
- `frameSelector` (optional): CSS selector of the `<iframe>` element; its content frame becomes active.
|
|
917
|
+
- **Returns**: `{ active: <frame url>, matcher, frames }` (or `{ active: null }` on reset)
|
|
918
|
+
- **Use case**: Enter a cross-origin iframe to click/fill a form rendered there
|
|
919
|
+
|
|
920
|
+
```javascript
|
|
921
|
+
// Discover frames
|
|
922
|
+
listFrames()
|
|
923
|
+
// → { count: 2, frames: [ {url:".../app", isMain:true}, {url:"https://app.example.com/...", isMain:false} ] }
|
|
924
|
+
|
|
925
|
+
// Enter the cross-origin iframe
|
|
926
|
+
switchFrame({ frameUrl: "app.example.com" })
|
|
927
|
+
|
|
928
|
+
// Now element tools target the iframe
|
|
929
|
+
analyzePage() // returns the iframe's APOM tree
|
|
930
|
+
findElementsByText({ text: "Save" }) // searches inside the iframe
|
|
931
|
+
click({ id: "button_3" }) // clicks inside the iframe
|
|
932
|
+
|
|
933
|
+
// Back to the main document
|
|
934
|
+
switchFrame()
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
> `analyzePage` also includes a `frames` array in its output whenever the page has more than one frame, so the agent can discover iframes without a separate `listFrames` call.
|
|
938
|
+
|
|
894
939
|
### 6. Figma Tools
|
|
895
940
|
Design-to-code validation, file browsing, design system extraction, and comparison tools with automatic 3 MB compression.
|
|
896
941
|
|
|
@@ -1614,14 +1659,14 @@ Each tool definition is sent to the AI in every request, consuming context token
|
|
|
1614
1659
|
| Group | Description | Tools (count) |
|
|
1615
1660
|
|-------|-------------|---------------|
|
|
1616
1661
|
| `core` | Basic tools | `ping`, `openBrowser` (2) |
|
|
1617
|
-
| `interaction` | User interaction | `click`, `type`, `scrollTo`, `waitForElement`, `hover` (
|
|
1662
|
+
| `interaction` | User interaction & frames | `click`, `type`, `scrollTo`, `waitForElement`, `hover`, `selectOption`, `selectFromGroup`, `drag`, `scrollHorizontal`, `switchFrame`, `listFrames` (11) |
|
|
1618
1663
|
| `inspection` | Page inspection | `getComputedCss`, `getBoxModel`, `screenshot`, `saveScreenshot` (4) |
|
|
1619
1664
|
| `debug` | Debugging & network | `getConsoleLogs`, `listNetworkRequests`, `getNetworkRequest`, `filterNetworkRequests` (4) |
|
|
1620
1665
|
| `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `findElementsByText` (8) |
|
|
1621
1666
|
| `recorder` | Scenario recording | `enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`, `appendScenarioToFile`, `generatePageObject` (9) |
|
|
1622
1667
|
| `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette`, `convertFigmaToCode` (10) |
|
|
1623
1668
|
|
|
1624
|
-
**Total:**
|
|
1669
|
+
**Total:** 48 tools across 7 groups
|
|
1625
1670
|
|
|
1626
1671
|
**Configuration:**
|
|
1627
1672
|
|
|
@@ -1746,6 +1791,48 @@ To use Figma tools, you need to configure your Figma Personal Access Token.
|
|
|
1746
1791
|
|
|
1747
1792
|
---
|
|
1748
1793
|
|
|
1794
|
+
## Browser Connection (use your real, logged-in Chrome)
|
|
1795
|
+
|
|
1796
|
+
By default ChromeTools connects to a Chrome with remote debugging on port **9222**, and if none is found it launches a fresh Chrome with a **temporary profile** (no cookies, no logins). To automate a site that needs your **authenticated session** (and to reach cross-origin iframes that depend on it), point ChromeTools at your own Chrome via environment variables — all optional:
|
|
1797
|
+
|
|
1798
|
+
| Variable | Default | Purpose |
|
|
1799
|
+
|----------|---------|---------|
|
|
1800
|
+
| `CHROMETOOLS_BROWSER_WS_ENDPOINT` | _(unset)_ | Direct CDP WebSocket URL (e.g. `ws://127.0.0.1:9222/devtools/browser/<id>`). When set, ChromeTools connects straight to it, skipping port discovery and launch. |
|
|
1801
|
+
| `CHROMETOOLS_DEBUG_PORT` | `9222` | Remote-debugging port to discover/connect to (and to launch with). |
|
|
1802
|
+
| `CHROMETOOLS_USER_DATA_DIR` | `<temp>/chrome-mcp-profile` | Chrome profile used when launching a new instance. Point at a real (or cloned) profile to reuse its cookies/logins. |
|
|
1803
|
+
| `CHROMETOOLS_CHROME_PATH` | platform default | Path to the Chrome executable. |
|
|
1804
|
+
|
|
1805
|
+
**Quick path — attach to your already-logged-in Chrome:**
|
|
1806
|
+
|
|
1807
|
+
1. Fully close Chrome (a profile can't open a debug port while another Chrome holds it).
|
|
1808
|
+
2. Launch it with remote debugging on your normal profile:
|
|
1809
|
+
```bash
|
|
1810
|
+
# Windows
|
|
1811
|
+
chrome.exe --remote-debugging-port=9222
|
|
1812
|
+
# macOS
|
|
1813
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
|
|
1814
|
+
```
|
|
1815
|
+
3. Log into the target site in that Chrome. ChromeTools will `connect()` to it — cross-origin iframes load authenticated, and `listTabs` sees your real tabs.
|
|
1816
|
+
|
|
1817
|
+
```json
|
|
1818
|
+
{
|
|
1819
|
+
"mcpServers": {
|
|
1820
|
+
"chrometools": {
|
|
1821
|
+
"type": "stdio",
|
|
1822
|
+
"command": "npx",
|
|
1823
|
+
"args": ["chrometools-mcp"],
|
|
1824
|
+
"env": {
|
|
1825
|
+
"CHROMETOOLS_USER_DATA_DIR": "C:/Users/you/chrome-mcp-real-profile"
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
Without any of these variables, behavior is unchanged.
|
|
1833
|
+
|
|
1834
|
+
---
|
|
1835
|
+
|
|
1749
1836
|
## WSL Setup Guide
|
|
1750
1837
|
|
|
1751
1838
|
If you're using **Windows Subsystem for Linux (WSL)**, special configuration is required to display Chrome GUI windows.
|
|
@@ -9,7 +9,7 @@ import { spawn } from 'child_process';
|
|
|
9
9
|
import http from 'http';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { getChromePath,
|
|
12
|
+
import { getChromePath, getUserDataDir, isWSL, CHROME_DEBUG_PORT } from '../utils/platform-utils.js';
|
|
13
13
|
import { handleNewTab, openPages, lastPage } from './page-manager.js';
|
|
14
14
|
|
|
15
15
|
// Get extension path (use forward slashes for Chrome on Windows)
|
|
@@ -97,6 +97,30 @@ export async function getBrowser() {
|
|
|
97
97
|
let browser;
|
|
98
98
|
let endpoint;
|
|
99
99
|
|
|
100
|
+
// Highest priority: explicit CDP WebSocket endpoint (e.g. a real
|
|
101
|
+
// logged-in Chrome the user already launched). Skips port-discovery
|
|
102
|
+
// and spawning entirely — connect straight to the live session.
|
|
103
|
+
const wsEndpoint = process.env.CHROMETOOLS_BROWSER_WS_ENDPOINT;
|
|
104
|
+
if (wsEndpoint) {
|
|
105
|
+
browser = await puppeteer.connect({
|
|
106
|
+
browserWSEndpoint: wsEndpoint,
|
|
107
|
+
defaultViewport: null,
|
|
108
|
+
});
|
|
109
|
+
debugLog("Connected via CHROMETOOLS_BROWSER_WS_ENDPOINT:", wsEndpoint);
|
|
110
|
+
connectedToExistingChrome = true;
|
|
111
|
+
console.error("[chrometools-mcp] Connected to Chrome via CHROMETOOLS_BROWSER_WS_ENDPOINT.");
|
|
112
|
+
|
|
113
|
+
browser.on('disconnected', () => {
|
|
114
|
+
debugLog("Browser disconnected");
|
|
115
|
+
browserPromise = null;
|
|
116
|
+
connectedToExistingChrome = false;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
setupNewTabTracking(browser);
|
|
120
|
+
scheduleBridgeReconnect();
|
|
121
|
+
return browser;
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
// Try to connect to existing Chrome with remote debugging
|
|
101
125
|
try {
|
|
102
126
|
endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 2);
|
|
@@ -132,7 +156,7 @@ export async function getBrowser() {
|
|
|
132
156
|
|
|
133
157
|
// Launch new Chrome with remote debugging enabled
|
|
134
158
|
const chromePath = getChromePath();
|
|
135
|
-
const userDataDir =
|
|
159
|
+
const userDataDir = getUserDataDir();
|
|
136
160
|
|
|
137
161
|
debugLog("Chrome path:", chromePath);
|
|
138
162
|
debugLog("User data dir:", userDataDir);
|
package/browser/page-manager.js
CHANGED
|
@@ -346,6 +346,95 @@ export function setLastPage(page) {
|
|
|
346
346
|
lastPage = page;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
350
|
+
// Active frame state (P0-1: cross-origin iframe support)
|
|
351
|
+
//
|
|
352
|
+
// Tools default to the main frame. switchFrame() stores a *matcher* (not a Frame
|
|
353
|
+
// object — Frames get invalidated when the iframe re-navigates) keyed by Page.
|
|
354
|
+
// getTargetFrame() re-resolves the matcher against page.frames() on every call,
|
|
355
|
+
// so it survives iframe reloads. Cleared on top-level navigation.
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
const activeFrames = new WeakMap(); // Page -> { frameUrl?, frameSelector? }
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Set (or clear) the active frame matcher for a page.
|
|
361
|
+
* @param {Page} page
|
|
362
|
+
* @param {{frameUrl?: string, frameSelector?: string}|null} matcher - null resets to main frame
|
|
363
|
+
*/
|
|
364
|
+
export function setActiveFrame(page, matcher) {
|
|
365
|
+
if (!matcher || (!matcher.frameUrl && !matcher.frameSelector)) {
|
|
366
|
+
activeFrames.delete(page);
|
|
367
|
+
} else {
|
|
368
|
+
activeFrames.set(page, matcher);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Clear the active frame for a page (back to main frame).
|
|
374
|
+
* @param {Page} page
|
|
375
|
+
*/
|
|
376
|
+
export function clearActiveFrame(page) {
|
|
377
|
+
activeFrames.delete(page);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get the matcher currently stored for a page (or null).
|
|
382
|
+
* @param {Page} page
|
|
383
|
+
* @returns {{frameUrl?: string, frameSelector?: string}|null}
|
|
384
|
+
*/
|
|
385
|
+
export function getActiveFrameMatcher(page) {
|
|
386
|
+
return activeFrames.get(page) || null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolve the execution context (Frame) for a page based on its active matcher.
|
|
391
|
+
* Returns the main frame when no active frame is set. A Frame exposes the same
|
|
392
|
+
* surface tools rely on (.evaluate, .$, .$$, .click, .type, .waitForSelector),
|
|
393
|
+
* so handlers can use the returned context in place of `page` for DOM work.
|
|
394
|
+
* @param {Page} page
|
|
395
|
+
* @returns {Promise<Frame>} - Target Puppeteer Frame
|
|
396
|
+
*/
|
|
397
|
+
export async function getTargetFrame(page) {
|
|
398
|
+
const matcher = activeFrames.get(page);
|
|
399
|
+
if (!matcher) {
|
|
400
|
+
return page.mainFrame();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (matcher.frameSelector) {
|
|
404
|
+
const handle = await page.$(matcher.frameSelector);
|
|
405
|
+
const frame = handle ? await handle.contentFrame() : null;
|
|
406
|
+
if (!frame) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Active frame (selector "${matcher.frameSelector}") not found. Call listFrames() and switchFrame() again, or switchFrame() with no args to reset.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return frame;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// frameUrl: substring match against all frames
|
|
415
|
+
const frame = page.frames().find(f => f.url().includes(matcher.frameUrl));
|
|
416
|
+
if (!frame) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Active frame (url contains "${matcher.frameUrl}") not found. Call listFrames() and switchFrame() again, or switchFrame() with no args to reset.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return frame;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* List all frames on a page with metadata (for listFrames tool / analyzePage output).
|
|
426
|
+
* @param {Page} page
|
|
427
|
+
* @returns {Array<{url: string, name: string, isMain: boolean}>}
|
|
428
|
+
*/
|
|
429
|
+
export function listFramesForPage(page) {
|
|
430
|
+
const main = page.mainFrame();
|
|
431
|
+
return page.frames().map(f => ({
|
|
432
|
+
url: f.url(),
|
|
433
|
+
name: f.name() || '',
|
|
434
|
+
isMain: f === main,
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
|
|
349
438
|
/**
|
|
350
439
|
* Setup a new page with all monitoring (console, network, recorder)
|
|
351
440
|
* Used for both explicitly created pages and auto-detected new tabs
|
package/element-finder-utils.js
CHANGED
|
@@ -177,6 +177,12 @@ function analyzeButtonContextInPage(element) {
|
|
|
177
177
|
|
|
178
178
|
// Primary button indicators
|
|
179
179
|
isPrimary: /primary|main|btn-primary|button-primary/i.test(element.className || ''),
|
|
180
|
+
|
|
181
|
+
// Navigation/menu context (P1-3): menu items and nav links must be able to
|
|
182
|
+
// out-score a form's primary submit button when the user asked for navigation.
|
|
183
|
+
hasMenuRole: element.getAttribute('role') === 'menuitem' || element.getAttribute('role') === 'tab',
|
|
184
|
+
hasOnClick: element.hasAttribute('onclick'),
|
|
185
|
+
isNavContext: element.closest('nav, [role="navigation"], [role="menu"], [role="menubar"], [role="tablist"]') !== null,
|
|
180
186
|
};
|
|
181
187
|
|
|
182
188
|
// Check if it's the last button in form
|
|
@@ -217,6 +223,10 @@ function scoreSubmitButton(element, context, description) {
|
|
|
217
223
|
if (context.hasSubmitIcon) score += 5; // submit icon
|
|
218
224
|
if (context.isPrimary) score += 15; // primary button style
|
|
219
225
|
|
|
226
|
+
// Navigation/menu context bonus (P1-3)
|
|
227
|
+
if (context.hasMenuRole) score += 15; // role=menuitem/tab
|
|
228
|
+
if (context.isNavContext) score += 10; // inside nav/menu container
|
|
229
|
+
|
|
220
230
|
// Visibility bonus
|
|
221
231
|
if (context.isVisible) score += 10;
|
|
222
232
|
|
|
@@ -235,6 +245,22 @@ function scoreSubmitButton(element, context, description) {
|
|
|
235
245
|
score -= 20;
|
|
236
246
|
}
|
|
237
247
|
|
|
248
|
+
// Text-mismatch penalty (P1-3): when the description carries actual words but
|
|
249
|
+
// the element's text neither contains nor is contained by it AND it doesn't hit
|
|
250
|
+
// a submit keyword, it's almost certainly the wrong control (e.g. asked
|
|
251
|
+
// "Настройки", element says "Пополнить"). Without this, a primary form submit
|
|
252
|
+
// wins on structural points alone and gets auto-clicked.
|
|
253
|
+
if (descLower.trim().length > 0 && text.length > 0) {
|
|
254
|
+
const textMatchesDesc = text.includes(descLower) || descLower.includes(text);
|
|
255
|
+
if (!textMatchesDesc && !matchesSubmitKeyword(context.text, description)) {
|
|
256
|
+
// Strong enough to overcome a full house of structural submit points
|
|
257
|
+
// (type=submit + in-form + last + primary + visible ≈ +115), so a button
|
|
258
|
+
// whose text is unrelated to the request can't be auto-clicked. Submit
|
|
259
|
+
// keywords (Save/Войти/…) are exempt above, so real submits aren't harmed.
|
|
260
|
+
score -= 60;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
238
264
|
return score;
|
|
239
265
|
}
|
|
240
266
|
|
|
@@ -572,6 +598,18 @@ function explainScore(element, context, description, score) {
|
|
|
572
598
|
if (context.isLink && !matchesSubmitKeyword(context.text, description)) {
|
|
573
599
|
reasons.push('link without submit keyword (-20)');
|
|
574
600
|
}
|
|
601
|
+
if (context.hasMenuRole) reasons.push('menu/tab role (+15)');
|
|
602
|
+
if (context.isNavContext) reasons.push('nav/menu context (+10)');
|
|
603
|
+
{
|
|
604
|
+
const t = (context.text || '').toLowerCase();
|
|
605
|
+
const d = description.toLowerCase();
|
|
606
|
+
if (d.trim().length > 0 && t.length > 0) {
|
|
607
|
+
const m = t.includes(d) || d.includes(t);
|
|
608
|
+
if (!m && !matchesSubmitKeyword(context.text, description)) {
|
|
609
|
+
reasons.push('text ≠ description (-60)');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
575
613
|
|
|
576
614
|
// Common reasons
|
|
577
615
|
if (context.inForm) reasons.push('in form (+20)');
|