chrometools-mcp 3.2.4 → 3.2.10
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 +120 -0
- package/README.md +30 -13
- package/README.ru.md +24 -3
- package/browser/page-manager.js +31 -0
- package/index.js +55 -54
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +28 -7
- package/server/tool-definitions.js +1 -11
- package/server/tool-groups.js +0 -1
- package/server/tool-schemas.js +1 -5
- package/utils/post-click-diagnostics.js +277 -0
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
|
+
## [3.2.10] - 2026-01-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Network request deduplication** — Fixed duplicate pending requests in diagnostics
|
|
9
|
+
- Prevented same requestId from being added multiple times during redirects/retries
|
|
10
|
+
- Added deduplication check in Network.requestWillBeSent event handler
|
|
11
|
+
- Updates existing request instead of creating duplicate entry
|
|
12
|
+
- Example: example.com showed 4 pending (2 URLs × 2 duplicates) → now shows 2 pending (correct count)
|
|
13
|
+
- **Memory leak prevention** — Limited networkRequests array growth
|
|
14
|
+
- Keeps maximum 500 most recent network requests in memory
|
|
15
|
+
- Automatically removes oldest requests when limit exceeded
|
|
16
|
+
- Prevents unbounded memory growth during long browser sessions
|
|
17
|
+
- Example: After 100 page navigations, memory stays bounded
|
|
18
|
+
|
|
19
|
+
## [3.2.9] - 2026-01-29
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **navigateTo diagnostics** — Post-navigation diagnostics for navigateTo tool
|
|
23
|
+
- Detects chrome-error:// pages (unreachable servers, DNS failures)
|
|
24
|
+
- Waits 20s for slow page loads and network requests
|
|
25
|
+
- Reports JS console errors and network errors after navigation
|
|
26
|
+
- Shows pending requests if page loads slowly
|
|
27
|
+
- Same comprehensive diagnostics as click tool
|
|
28
|
+
- Example: Navigate to offline backend → instant error report instead of silent failure
|
|
29
|
+
- **openBrowser diagnostics** — Post-navigation diagnostics for openBrowser tool
|
|
30
|
+
- Same comprehensive diagnostics as navigateTo and click
|
|
31
|
+
- Critical for first action in session - shows errors immediately
|
|
32
|
+
- Detects chrome-error:// pages on initial load
|
|
33
|
+
- Reports network errors, console errors, pending requests
|
|
34
|
+
- Example: Open unreachable backend → instant error report with details
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **Diagnostics naming** — Renamed for clarity and universal use
|
|
38
|
+
- "POST-CLICK DIAGNOSTICS" → "POST-ACTION DIAGNOSTICS"
|
|
39
|
+
- Function parameters: beforeClickTimestamp → beforeActionTimestamp
|
|
40
|
+
- Comments updated to reflect use in both click and navigate actions
|
|
41
|
+
- File remains post-click-diagnostics.js for backward compatibility
|
|
42
|
+
|
|
43
|
+
## [3.2.8] - 2026-01-29
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- **Network wait timeout** — Increased from 5s to 20s for slow APIs
|
|
47
|
+
- Gives slow backend APIs time to complete before timeout
|
|
48
|
+
- AI gets complete success/error status instead of "pending unknown"
|
|
49
|
+
- Pending requests after 20s are reported with details (URL, method, runtime)
|
|
50
|
+
- Clear warning: "Status unknown - may complete successfully or fail"
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- **Click timeout on network errors** — No more 30s timeout when backend unreachable
|
|
54
|
+
- Detects chrome-error:// pages (ERR_CONNECTION_REFUSED, DNS_PROBE_FINISHED_NXDOMAIN, etc.)
|
|
55
|
+
- Returns error details immediately after 500ms diagnostic wait
|
|
56
|
+
- Shows error code and suggestion: "Backend likely not running or unreachable"
|
|
57
|
+
- Reduces diagnosis from 3 API calls to 1
|
|
58
|
+
- Example: Form submits to localhost:8001 (not running) → instant error report instead of 30s timeout
|
|
59
|
+
- **Network request tracking** — Now tracks ALL requests triggered by click, not just pending at 500ms
|
|
60
|
+
- Filters requests by timestamp (only those started AFTER click)
|
|
61
|
+
- Catches slow-starting requests that begin after initial 500ms wait
|
|
62
|
+
- Shows accurate count: completed/pending/total requests
|
|
63
|
+
- Prevents false "No network requests triggered" when requests start late
|
|
64
|
+
- **Delayed error collection** — Errors from requests that complete during maxWait are now captured
|
|
65
|
+
- Added 100ms delay after network wait before collecting errors
|
|
66
|
+
- Catches errors from requests that finish right as timeout expires
|
|
67
|
+
- Network summary shows: "⚠️ Network: 2 OK, 1 failed" when errors present
|
|
68
|
+
- Ensures AI sees errors even if request completes at edge of timeout window
|
|
69
|
+
- **Pending request reporting** — AI now sees details about slow/hanging requests
|
|
70
|
+
- Lists pending requests with URL, method, and elapsed time
|
|
71
|
+
- Suggests backend performance check or network connectivity issues
|
|
72
|
+
- Example: "POST /api/slow - Running for: 20145ms"
|
|
73
|
+
|
|
74
|
+
## [3.2.7] - 2026-01-29
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
- **Post-click diagnostics** — Click tool now automatically detects and reports errors
|
|
78
|
+
- Waits 500ms after click to capture async events
|
|
79
|
+
- Detects pending network requests and waits for completion (up to 5s)
|
|
80
|
+
- Collects JavaScript console errors and network errors
|
|
81
|
+
- **Error limit**: Max 15 console errors + 15 network errors to prevent spam
|
|
82
|
+
- Shows omitted error count if limit exceeded
|
|
83
|
+
- Returns diagnostics in click response for immediate AI feedback
|
|
84
|
+
- Prevents AI from making blind follow-up requests when errors occur
|
|
85
|
+
- New module: `utils/post-click-diagnostics.js`
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
- **Click behavior** — Enhanced UX for AI agents
|
|
89
|
+
- Click now includes network activity summary (requests completed, timing)
|
|
90
|
+
- Errors displayed immediately in click response
|
|
91
|
+
- AI can see what broke without additional tool calls
|
|
92
|
+
- Better error context: timestamp, location, status codes
|
|
93
|
+
- Smart error limiting prevents overwhelming AI with hundreds of errors
|
|
94
|
+
|
|
95
|
+
## [3.2.6] - 2026-01-28
|
|
96
|
+
|
|
97
|
+
### Removed
|
|
98
|
+
- **getAllInteractiveElements tool** — Removed redundant tool, fully replaced by analyzePage (54 → 53 tools)
|
|
99
|
+
- `analyzePage` provides superior functionality: hierarchical tree, element registration, APOM IDs, metadata
|
|
100
|
+
- `getAllInteractiveElements` only returned flat list with CSS selectors
|
|
101
|
+
- Affected files: `index.js`, `server/tool-definitions.js`, `server/tool-schemas.js`, `server/tool-groups.js`, `README.md`
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
- **analyzePage visibility detection** — Fixed critical bug where analyzePage returned tree: null with interactiveCount: 0 on Angular Material pages
|
|
105
|
+
- Changed `isVisible()` check from `offsetParent` to `offsetWidth/offsetHeight > 0`
|
|
106
|
+
- Now correctly detects elements inside `position: fixed` containers (Angular Material overlays, dialogs, selects)
|
|
107
|
+
- Handles `position: sticky` elements properly
|
|
108
|
+
- Testing on my-autotests.segmento.ru: interactiveCount increased from 0 → 329 elements
|
|
109
|
+
- Affected file: `pom/apom-tree-converter.js`
|
|
110
|
+
- **type() text corruption** — Fixed text input corruption (duplicated/swapped characters)
|
|
111
|
+
- Changed default keystroke delay from 0ms to 30ms
|
|
112
|
+
- Prevents character corruption on fast-reacting inputs (Google Search, autocomplete fields)
|
|
113
|
+
- Example: "puppeteer automation" no longer becomes "ppuuppppeetteeeerr baruotwosmeart"
|
|
114
|
+
- Affected file: `index.js:454`
|
|
115
|
+
|
|
116
|
+
## [3.2.5] - 2026-01-28
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
- **CSS selector validation** — Fixed analyzePage crash when elements have numeric IDs
|
|
120
|
+
- Added validation to skip IDs starting with digits (e.g., `id="301178"`)
|
|
121
|
+
- CSS selectors don't support IDs starting with numbers (per CSS specification)
|
|
122
|
+
- Added try-catch for invalid selector edge cases
|
|
123
|
+
- Affected file: `pom/apom-tree-converter.js`
|
|
124
|
+
|
|
5
125
|
## [3.2.4] - 2026-01-27
|
|
6
126
|
|
|
7
127
|
### Performance
|
package/README.md
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
# chrometools-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 🌐 [Русская версия README](./README.ru.md)
|
|
4
|
+
|
|
5
|
+
**AI-powered Chrome automation through natural language.** No more fighting with CSS selectors, XPath expressions, or brittle test scripts. Just tell your AI assistant what you want to do on a web page, and ChromeTools MCP makes it happen.
|
|
6
|
+
|
|
7
|
+
## Why ChromeTools MCP?
|
|
8
|
+
|
|
9
|
+
**For AI Agents & Developers:**
|
|
10
|
+
- 🎯 **54 specialized tools** for browser automation - from simple clicks to Figma comparisons
|
|
11
|
+
- 🧠 **APOM (Agent Page Object Model)** - AI-friendly page representation (~8-10k tokens vs 15-25k for screenshots)
|
|
12
|
+
- 🔄 **Persistent browser sessions** - pages stay open between commands for iterative workflows
|
|
13
|
+
- ⚡ **Framework-aware** - handles React, Vue, Angular events and state updates automatically
|
|
14
|
+
- 📸 **Visual testing** - compare designs pixel-by-pixel with Figma integration
|
|
15
|
+
- 🎬 **Scenario recording** - record browser actions, replay them, or export as Playwright/Selenium tests
|
|
16
|
+
- 🌍 **Cross-platform** - works seamlessly on Windows, WSL, Linux, and macOS
|
|
17
|
+
|
|
18
|
+
**Perfect for:**
|
|
19
|
+
- 🤖 Building AI agents that interact with web applications
|
|
20
|
+
- 🧪 Automated testing without writing code - let AI generate tests from scenarios
|
|
21
|
+
- 🔍 Web scraping and data extraction with natural language instructions
|
|
22
|
+
- 🎨 Design validation - compare implemented UI with Figma designs
|
|
23
|
+
- 🚀 Rapid prototyping - test user flows by describing them to AI
|
|
24
|
+
- 📊 Monitoring and health checks for web applications
|
|
25
|
+
|
|
26
|
+
Stop writing brittle automation scripts. Start describing what you want in plain English.
|
|
4
27
|
|
|
5
28
|
## Installation
|
|
6
29
|
|
|
@@ -152,7 +175,7 @@ The Chrome Extension is **required** for scenario recording and other advanced f
|
|
|
152
175
|
**Step 3:** Download and Extract the Extension
|
|
153
176
|
|
|
154
177
|
**Option A - Download from GitHub (Recommended):**
|
|
155
|
-
1. Download the extension archive: [chrome-extension.zip](https://github.com/
|
|
178
|
+
1. Download the extension archive: [chrome-extension.zip](https://github.com/docentovich/chrometools-mcp/raw/main/chrome-extension.zip)
|
|
156
179
|
2. Extract the ZIP file to a folder on your computer
|
|
157
180
|
3. Remember the extraction path (you'll need it in the next step)
|
|
158
181
|
|
|
@@ -197,7 +220,7 @@ The Chrome Extension is **required** for scenario recording and other advanced f
|
|
|
197
220
|
- [Chrome Extension Setup](#chrome-extension-setup)
|
|
198
221
|
- [AI Optimization Features](#ai-optimization-features)- [Scenario Recorder](#scenario-recorder) - Visual UI-based recording with smart optimization
|
|
199
222
|
- [Available Tools](#available-tools) - **46+ Tools Total**
|
|
200
|
-
- [AI-Powered Tools](#ai-powered-tools) - smartFindElement, analyzePage, getElementDetails,
|
|
223
|
+
- [AI-Powered Tools](#ai-powered-tools) - smartFindElement, analyzePage, getElementDetails, findElementsByText
|
|
201
224
|
- [Core Tools](#1-core-tools) - ping, openBrowser
|
|
202
225
|
- [Interaction Tools](#2-interaction-tools) - click, type, scrollTo, selectOption, selectFromGroup, drag, scrollHorizontal
|
|
203
226
|
- [Inspection Tools](#3-inspection-tools) - getElement, getComputedCss, getBoxModel, screenshot
|
|
@@ -238,7 +261,7 @@ AI: smartFindElement("login button")
|
|
|
238
261
|
1. **`analyzePage`** - 🔥 **USE FREQUENTLY** - Get current page state after loads, clicks, submissions (cached, use refresh:true)
|
|
239
262
|
2. **`smartFindElement`** - Natural language element search with multilingual support
|
|
240
263
|
3. **AI Hints** - Automatic context in all tools (page type, available actions, suggestions)
|
|
241
|
-
4. **
|
|
264
|
+
4. **Text search** - `findElementsByText` for finding elements by visible text
|
|
242
265
|
|
|
243
266
|
**Performance:** 3-5x faster, 5-10x fewer requests
|
|
244
267
|
|
|
@@ -438,12 +461,6 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
|
|
|
438
461
|
getElementDetails({ id: "container_123", analyzeChildren: true, refresh: true }) // Analyze modal contents with children tree
|
|
439
462
|
```
|
|
440
463
|
|
|
441
|
-
#### getAllInteractiveElements
|
|
442
|
-
Get all clickable/fillable elements with their selectors.
|
|
443
|
-
- **Parameters**:
|
|
444
|
-
- `includeHidden` (optional): Include hidden elements (default: false)
|
|
445
|
-
- **Returns**: Array of all interactive elements with selectors and metadata
|
|
446
|
-
|
|
447
464
|
#### findElementsByText
|
|
448
465
|
Find elements by their visible text content.
|
|
449
466
|
- **Parameters**:
|
|
@@ -1431,11 +1448,11 @@ Each tool definition is sent to the AI in every request, consuming context token
|
|
|
1431
1448
|
| `interaction` | User interaction | `click`, `type`, `scrollTo`, `waitForElement`, `hover` (5) |
|
|
1432
1449
|
| `inspection` | Page inspection | `getComputedCss`, `getBoxModel`, `screenshot`, `saveScreenshot` (4) |
|
|
1433
1450
|
| `debug` | Debugging & network | `getConsoleLogs`, `listNetworkRequests`, `getNetworkRequest`, `filterNetworkRequests` (4) |
|
|
1434
|
-
| `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `
|
|
1451
|
+
| `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `findElementsByText` (8) |
|
|
1435
1452
|
| `recorder` | Scenario recording | `enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`, `appendScenarioToFile`, `generatePageObject` (9) |
|
|
1436
1453
|
| `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette`, `convertFigmaToCode` (10) |
|
|
1437
1454
|
|
|
1438
|
-
**Total:**
|
|
1455
|
+
**Total:** 42 tools across 7 groups
|
|
1439
1456
|
|
|
1440
1457
|
**Configuration:**
|
|
1441
1458
|
|
|
@@ -1603,7 +1620,7 @@ npx @modelcontextprotocol/inspector node index.js
|
|
|
1603
1620
|
- Interaction: click, type, scrollTo, selectOption, selectFromGroup, drag, scrollHorizontal
|
|
1604
1621
|
- Inspection: getElement, getComputedCss, getBoxModel, screenshot, saveScreenshot
|
|
1605
1622
|
- Advanced: executeScript, getConsoleLogs, listNetworkRequests, getNetworkRequest, filterNetworkRequests, hover, setStyles, setViewport, getViewport, navigateTo, waitForElement
|
|
1606
|
-
- AI-Powered: smartFindElement, analyzePage, getElementDetails (with children analysis),
|
|
1623
|
+
- AI-Powered: smartFindElement, analyzePage, getElementDetails (with children analysis), findElementsByText - Recorder: enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
|
|
1607
1624
|
- Figma: getFigmaFrame, compareFigmaToElement, getFigmaSpecs, parseFigmaUrl, listFigmaPages, searchFigmaFrames, getFigmaComponents, getFigmaStyles, getFigmaColorPalette, convertFigmaToCode
|
|
1608
1625
|
- **UI Framework Detection**: Automatic detection of MUI, Ant Design, Chakra UI, Bootstrap, Vuetify, Semantic UI- **Smart Dropdown Handling**: Extracts options from both native `<select>` and custom UI framework components- **APOM (Agent Page Object Model)**: Automatic element ID assignment for reliable interaction - `analyzePage()` returns elements with unique IDs (e.g., `input_20`, `button_45`)
|
|
1609
1626
|
- Use `id` parameter in click/type/hover/selectOption for stable targeting
|
package/README.ru.md
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
# chrometools-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 🌐 [English version](./README.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Автоматизация Chrome через естественный язык для ИИ.** Забудьте о борьбе с CSS селекторами, XPath выражениями и хрупкими тестовыми скриптами. Просто скажите своему ИИ-помощнику, что вы хотите сделать на веб-странице, и ChromeTools MCP сделает это.
|
|
6
|
+
|
|
7
|
+
## Зачем нужен ChromeTools MCP?
|
|
8
|
+
|
|
9
|
+
**Для ИИ-агентов и разработчиков:**
|
|
10
|
+
- 🎯 **54 специализированных инструмента** для автоматизации браузера — от простых кликов до сравнения с Figma
|
|
11
|
+
- 🧠 **APOM (Agent Page Object Model)** — представление страницы для ИИ (~8-10k токенов против 15-25k для скриншотов)
|
|
12
|
+
- 🔄 **Постоянные сессии браузера** — страницы остаются открытыми между командами для итеративной работы
|
|
13
|
+
- ⚡ **Поддержка фреймворков** — автоматически обрабатывает события и состояние React, Vue, Angular
|
|
14
|
+
- 📸 **Визуальное тестирование** — попиксельное сравнение дизайна с макетами Figma
|
|
15
|
+
- 🎬 **Запись сценариев** — записывайте действия в браузере, воспроизводите их или экспортируйте в Playwright/Selenium
|
|
16
|
+
- 🌍 **Кросс-платформенность** — работает на Windows, WSL, Linux и macOS
|
|
17
|
+
|
|
18
|
+
**Идеально для:**
|
|
19
|
+
- 🤖 Создания ИИ-агентов, взаимодействующих с веб-приложениями
|
|
20
|
+
- 🧪 Автоматизированного тестирования без написания кода — пусть ИИ генерирует тесты из сценариев
|
|
21
|
+
- 🔍 Парсинга веб-страниц и извлечения данных с помощью естественного языка
|
|
22
|
+
- 🎨 Валидации дизайна — сравнение реализованного UI с дизайном в Figma
|
|
23
|
+
- 🚀 Быстрого прототипирования — тестирование пользовательских сценариев через их описание
|
|
24
|
+
- 📊 Мониторинга и проверки работоспособности веб-приложений
|
|
25
|
+
|
|
26
|
+
Перестаньте писать хрупкие скрипты автоматизации. Начните описывать желаемое на обычном языке.
|
|
6
27
|
|
|
7
28
|
## Установка
|
|
8
29
|
|
|
@@ -91,7 +112,7 @@ npx chrometools-mcp
|
|
|
91
112
|
**Шаг 3:** Скачайте и распакуйте расширение
|
|
92
113
|
|
|
93
114
|
**Вариант A - Скачать с GitHub (Рекомендуется):**
|
|
94
|
-
1. Скачайте архив расширения: [chrome-extension.zip](https://github.com/
|
|
115
|
+
1. Скачайте архив расширения: [chrome-extension.zip](https://github.com/docentovich/chrometools-mcp/raw/main/chrome-extension.zip)
|
|
95
116
|
2. Распакуйте ZIP файл в папку на вашем компьютере
|
|
96
117
|
3. Запомните путь распаковки (он понадобится на следующем шаге)
|
|
97
118
|
|
package/browser/page-manager.js
CHANGED
|
@@ -30,6 +30,21 @@ export const consoleLogs = [];
|
|
|
30
30
|
// Network requests storage
|
|
31
31
|
export const networkRequests = [];
|
|
32
32
|
|
|
33
|
+
// Maximum number of network requests to keep in memory (prevent unbounded growth)
|
|
34
|
+
const MAX_NETWORK_REQUESTS = 500;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clean old network requests to prevent memory leak
|
|
38
|
+
* Keeps only the most recent MAX_NETWORK_REQUESTS requests
|
|
39
|
+
*/
|
|
40
|
+
function cleanOldNetworkRequests() {
|
|
41
|
+
if (networkRequests.length > MAX_NETWORK_REQUESTS) {
|
|
42
|
+
// Remove oldest requests (keep most recent)
|
|
43
|
+
const removeCount = networkRequests.length - MAX_NETWORK_REQUESTS;
|
|
44
|
+
networkRequests.splice(0, removeCount);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
// Page analysis cache
|
|
34
49
|
export const pageAnalysisCache = new Map();
|
|
35
50
|
|
|
@@ -52,6 +67,19 @@ export async function setupNetworkMonitoring(page) {
|
|
|
52
67
|
|
|
53
68
|
client.on('Network.requestWillBeSent', (event) => {
|
|
54
69
|
const timestamp = new Date().toISOString();
|
|
70
|
+
|
|
71
|
+
// Check if request already exists (prevent duplicates from redirects/retries)
|
|
72
|
+
const existingReq = networkRequests.find(r => r.requestId === event.requestId);
|
|
73
|
+
if (existingReq) {
|
|
74
|
+
// Update existing request instead of creating duplicate
|
|
75
|
+
existingReq.url = event.request.url;
|
|
76
|
+
existingReq.method = event.request.method;
|
|
77
|
+
existingReq.headers = event.request.headers;
|
|
78
|
+
existingReq.postData = event.request.postData;
|
|
79
|
+
existingReq.timestamp = timestamp;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
networkRequests.push({
|
|
56
84
|
requestId: event.requestId,
|
|
57
85
|
url: event.request.url,
|
|
@@ -64,6 +92,9 @@ export async function setupNetworkMonitoring(page) {
|
|
|
64
92
|
status: 'pending',
|
|
65
93
|
documentURL: event.documentURL
|
|
66
94
|
});
|
|
95
|
+
|
|
96
|
+
// Clean old requests to prevent unbounded memory growth
|
|
97
|
+
cleanOldNetworkRequests();
|
|
67
98
|
});
|
|
68
99
|
|
|
69
100
|
client.on('Network.responseReceived', (event) => {
|
package/index.js
CHANGED
|
@@ -54,6 +54,8 @@ import {getToolsFromGroups, getAllGroupNames} from './server/tool-groups.js';
|
|
|
54
54
|
import {executeElementAction} from './utils/element-actions.js';
|
|
55
55
|
// Import hints generator
|
|
56
56
|
import {generateClickHints, generateNavigationHints} from './utils/hints-generator.js';
|
|
57
|
+
// Import post-click diagnostics
|
|
58
|
+
import {runPostClickDiagnostics, formatDiagnosticsForAI} from './utils/post-click-diagnostics.js';
|
|
57
59
|
|
|
58
60
|
// Import Recorder modules
|
|
59
61
|
// Note: injectRecorder removed - now using Chrome Extension
|
|
@@ -313,12 +315,22 @@ async function executeToolInternal(name, args) {
|
|
|
313
315
|
|
|
314
316
|
if (name === "openBrowser") {
|
|
315
317
|
const validatedArgs = schemas.OpenBrowserSchema.parse(args);
|
|
318
|
+
|
|
319
|
+
// Capture timestamp BEFORE opening for diagnostics
|
|
320
|
+
const beforeOpenTimestamp = Date.now();
|
|
321
|
+
|
|
316
322
|
const page = await getOrCreatePage(validatedArgs.url);
|
|
317
323
|
const title = await page.title();
|
|
318
324
|
|
|
325
|
+
// Run post-navigation diagnostics (same as navigateTo)
|
|
326
|
+
const diagnostics = await runPostClickDiagnostics(page, beforeOpenTimestamp);
|
|
327
|
+
|
|
319
328
|
// Generate AI hints
|
|
320
329
|
const hints = await generateNavigationHints(page, validatedArgs.url);
|
|
321
330
|
|
|
331
|
+
// Format diagnostics for output
|
|
332
|
+
const diagnosticsText = formatDiagnosticsForAI(diagnostics);
|
|
333
|
+
|
|
322
334
|
// Check if extension is connected
|
|
323
335
|
const extensionConnected = isExtensionConnected();
|
|
324
336
|
const usedExistingChrome = isConnectedToExistingChrome();
|
|
@@ -329,11 +341,20 @@ async function executeToolInternal(name, args) {
|
|
|
329
341
|
extensionNote = `\n\n⚠️ EXTENSION NOT CONNECTED\nConnected to existing Chrome - extension needs manual installation.\n${instructions.installSteps.join('\n')}\n\nAlternative: ${instructions.alternativeFix}`;
|
|
330
342
|
}
|
|
331
343
|
|
|
344
|
+
let hintsText = '\n\n** AI HINTS **';
|
|
345
|
+
hintsText += `\nPage type: ${hints.pageType}`;
|
|
346
|
+
if (hints.availableActions.length > 0) {
|
|
347
|
+
hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
|
|
348
|
+
}
|
|
349
|
+
if (hints.suggestedNext.length > 0) {
|
|
350
|
+
hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
332
353
|
return {
|
|
333
354
|
content: [
|
|
334
355
|
{
|
|
335
356
|
type: "text",
|
|
336
|
-
text: `Browser opened successfully!\nURL: ${validatedArgs.url}\nPage title: ${title}\n\nBrowser remains open for interaction
|
|
357
|
+
text: `Browser opened successfully!\nURL: ${validatedArgs.url}\nPage title: ${title}\n\nBrowser remains open for interaction.${hintsText}${diagnosticsText}${extensionNote}`,
|
|
337
358
|
},
|
|
338
359
|
],
|
|
339
360
|
};
|
|
@@ -380,6 +401,9 @@ async function executeToolInternal(name, args) {
|
|
|
380
401
|
throw new Error(`Element not found: ${identifier}`);
|
|
381
402
|
}
|
|
382
403
|
|
|
404
|
+
// Capture timestamp BEFORE click for error filtering
|
|
405
|
+
const beforeClickTimestamp = Date.now();
|
|
406
|
+
|
|
383
407
|
// Try multiple click methods for better reliability
|
|
384
408
|
try {
|
|
385
409
|
// Method 1: Puppeteer click (most reliable for most cases)
|
|
@@ -395,11 +419,15 @@ async function executeToolInternal(name, args) {
|
|
|
395
419
|
await element.evaluate(el => el.click());
|
|
396
420
|
}
|
|
397
421
|
}
|
|
398
|
-
await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 1500));
|
|
399
422
|
|
|
400
|
-
//
|
|
423
|
+
// NEW POST-CLICK PATTERN:
|
|
424
|
+
// 1. Run post-click diagnostics (waits 500ms, checks pending requests, collects errors)
|
|
425
|
+
const diagnostics = await runPostClickDiagnostics(page, beforeClickTimestamp);
|
|
426
|
+
|
|
427
|
+
// 2. Generate AI hints after click
|
|
401
428
|
const hints = await generateClickHints(page, identifier);
|
|
402
429
|
|
|
430
|
+
// 3. Format output with hints and diagnostics
|
|
403
431
|
let hintsText = '\n\n** AI HINTS **';
|
|
404
432
|
if (hints.modalOpened) hintsText += '\nModal opened - interact with it or close';
|
|
405
433
|
if (hints.newElements.length > 0) {
|
|
@@ -409,8 +437,11 @@ async function executeToolInternal(name, args) {
|
|
|
409
437
|
hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
|
|
410
438
|
}
|
|
411
439
|
|
|
440
|
+
// 4. Add diagnostics to output
|
|
441
|
+
const diagnosticsText = formatDiagnosticsForAI(diagnostics);
|
|
442
|
+
|
|
412
443
|
const content = [
|
|
413
|
-
{ type: "text", text: `Clicked: ${identifier}${hintsText}` }
|
|
444
|
+
{ type: "text", text: `Clicked: ${identifier}${hintsText}${diagnosticsText}` }
|
|
414
445
|
];
|
|
415
446
|
|
|
416
447
|
// Only add screenshot if requested
|
|
@@ -451,7 +482,7 @@ async function executeToolInternal(name, args) {
|
|
|
451
482
|
// Use input model to handle the element appropriately
|
|
452
483
|
const model = await getInputModel(element, page);
|
|
453
484
|
const options = {
|
|
454
|
-
delay: validatedArgs.delay
|
|
485
|
+
delay: validatedArgs.delay !== undefined ? validatedArgs.delay : 30,
|
|
455
486
|
clearFirst: validatedArgs.clearFirst !== undefined ? validatedArgs.clearFirst : true,
|
|
456
487
|
};
|
|
457
488
|
|
|
@@ -1343,6 +1374,9 @@ async function executeToolInternal(name, args) {
|
|
|
1343
1374
|
browserOpened = true;
|
|
1344
1375
|
}
|
|
1345
1376
|
|
|
1377
|
+
// Capture timestamp BEFORE navigation for diagnostics
|
|
1378
|
+
const beforeNavTimestamp = Date.now();
|
|
1379
|
+
|
|
1346
1380
|
// Navigate to the new URL (skip if we just created page with this URL)
|
|
1347
1381
|
if (!browserOpened) {
|
|
1348
1382
|
await page.goto(validatedArgs.url, { waitUntil: validatedArgs.waitUntil || 'networkidle2' });
|
|
@@ -1350,17 +1384,32 @@ async function executeToolInternal(name, args) {
|
|
|
1350
1384
|
|
|
1351
1385
|
const title = await page.title();
|
|
1352
1386
|
|
|
1387
|
+
// Run post-navigation diagnostics (same as post-click)
|
|
1388
|
+
const diagnostics = await runPostClickDiagnostics(page, beforeNavTimestamp);
|
|
1389
|
+
|
|
1353
1390
|
// Generate AI hints
|
|
1354
1391
|
const hints = await generateNavigationHints(page, validatedArgs.url);
|
|
1355
1392
|
|
|
1393
|
+
// Format diagnostics for output
|
|
1394
|
+
const diagnosticsText = formatDiagnosticsForAI(diagnostics);
|
|
1395
|
+
|
|
1356
1396
|
const message = browserOpened
|
|
1357
1397
|
? `Browser opened and navigated to: ${validatedArgs.url}`
|
|
1358
1398
|
: `Navigated to: ${validatedArgs.url}`;
|
|
1359
1399
|
|
|
1400
|
+
let hintsText = '\n\n** AI HINTS **';
|
|
1401
|
+
hintsText += `\nPage type: ${hints.pageType}`;
|
|
1402
|
+
if (hints.availableActions.length > 0) {
|
|
1403
|
+
hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
|
|
1404
|
+
}
|
|
1405
|
+
if (hints.suggestedNext.length > 0) {
|
|
1406
|
+
hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1360
1409
|
return {
|
|
1361
1410
|
content: [{
|
|
1362
1411
|
type: "text",
|
|
1363
|
-
text: `${message}\nPage title: ${title}
|
|
1412
|
+
text: `${message}\nPage title: ${title}${hintsText}${diagnosticsText}`
|
|
1364
1413
|
}],
|
|
1365
1414
|
};
|
|
1366
1415
|
}
|
|
@@ -2250,54 +2299,6 @@ Start coding now.`;
|
|
|
2250
2299
|
};
|
|
2251
2300
|
}
|
|
2252
2301
|
|
|
2253
|
-
if (name === "getAllInteractiveElements") {
|
|
2254
|
-
const validatedArgs = schemas.GetAllInteractiveElementsSchema.parse(args);
|
|
2255
|
-
const page = await getLastOpenPage();
|
|
2256
|
-
|
|
2257
|
-
const elements = await page.evaluate((includeHidden, utilsCode) => {
|
|
2258
|
-
eval(utilsCode);
|
|
2259
|
-
|
|
2260
|
-
const results = [];
|
|
2261
|
-
const selector = 'button, a[href], input, select, textarea, [onclick], [role="button"], [tabindex]:not([tabindex="-1"])';
|
|
2262
|
-
|
|
2263
|
-
document.querySelectorAll(selector).forEach(el => {
|
|
2264
|
-
const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
|
|
2265
|
-
|
|
2266
|
-
if (!includeHidden && !isVisible) return;
|
|
2267
|
-
|
|
2268
|
-
const text = (el.textContent || el.value || el.getAttribute('aria-label') || el.placeholder || '').trim();
|
|
2269
|
-
|
|
2270
|
-
results.push({
|
|
2271
|
-
selector: getUniqueSelectorInPage(el),
|
|
2272
|
-
type: el.tagName.toLowerCase(),
|
|
2273
|
-
text: text.substring(0, 100),
|
|
2274
|
-
visible: isVisible,
|
|
2275
|
-
attributes: {
|
|
2276
|
-
id: el.id || null,
|
|
2277
|
-
class: el.className || null,
|
|
2278
|
-
role: el.getAttribute('role') || null,
|
|
2279
|
-
type: el.type || null,
|
|
2280
|
-
}
|
|
2281
|
-
});
|
|
2282
|
-
});
|
|
2283
|
-
|
|
2284
|
-
return results;
|
|
2285
|
-
}, validatedArgs.includeHidden || false, elementFinderUtils);
|
|
2286
|
-
|
|
2287
|
-
return {
|
|
2288
|
-
content: [{
|
|
2289
|
-
type: 'text',
|
|
2290
|
-
text: JSON.stringify({
|
|
2291
|
-
count: elements.length,
|
|
2292
|
-
elements,
|
|
2293
|
-
hints: {
|
|
2294
|
-
suggestion: 'Use these selectors directly with click, type, or other tools'
|
|
2295
|
-
}
|
|
2296
|
-
}, null, 2)
|
|
2297
|
-
}]
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
2302
|
if (name === "findElementsByText") {
|
|
2302
2303
|
const validatedArgs = schemas.FindElementsByTextSchema.parse(args);
|
|
2303
2304
|
const page = await getLastOpenPage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrometools-mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.10",
|
|
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",
|
|
@@ -231,13 +231,26 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
231
231
|
|
|
232
232
|
/**
|
|
233
233
|
* Check if element is visible
|
|
234
|
+
* More reliable check that works with position:fixed elements (Angular Material, etc.)
|
|
234
235
|
*/
|
|
235
236
|
function isVisible(el) {
|
|
236
|
-
|
|
237
|
+
// Check dimensions first (works for fixed position elements)
|
|
238
|
+
if (el.offsetWidth === 0 || el.offsetHeight === 0) return false;
|
|
239
|
+
|
|
240
|
+
// Check computed styles
|
|
237
241
|
const style = window.getComputedStyle(el);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
if (style.display === 'none' ||
|
|
243
|
+
style.visibility === 'hidden' ||
|
|
244
|
+
style.opacity === '0') {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// For body element, always consider visible if dimensions > 0
|
|
249
|
+
if (el === document.body) return true;
|
|
250
|
+
|
|
251
|
+
// Additional check: element should be in viewport or have offsetParent
|
|
252
|
+
// This handles elements inside position:fixed containers (Angular Material)
|
|
253
|
+
return el.offsetParent !== null || style.position === 'fixed' || style.position === 'sticky';
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
/**
|
|
@@ -682,9 +695,17 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
682
695
|
* Excludes framework-specific dynamic attributes (React, Vue, Angular)
|
|
683
696
|
*/
|
|
684
697
|
function generateSelector(element) {
|
|
685
|
-
// Use ID if available and unique
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
// Use ID if available, valid (not starting with digit), and unique
|
|
699
|
+
// CSS selectors don't support IDs starting with digits (e.g., #301178 is invalid)
|
|
700
|
+
if (element.id && !/^[0-9]/.test(element.id)) {
|
|
701
|
+
try {
|
|
702
|
+
const selector = `#${CSS.escape(element.id)}`;
|
|
703
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
704
|
+
return selector;
|
|
705
|
+
}
|
|
706
|
+
} catch (e) {
|
|
707
|
+
// Invalid selector, continue to other strategies
|
|
708
|
+
}
|
|
688
709
|
}
|
|
689
710
|
|
|
690
711
|
// Try to find stable class name (excluding framework-specific dynamic classes)
|
|
@@ -49,7 +49,7 @@ export const toolDefinitions = [
|
|
|
49
49
|
id: { type: "string", description: "APOM element ID from analyzePage (e.g., 'input_20'). Either id or selector required." },
|
|
50
50
|
selector: { type: "string", description: "CSS selector (e.g., '#email'). Either id or selector required." },
|
|
51
51
|
text: { type: "string", description: "Text to type" },
|
|
52
|
-
delay: { type: "number", description: "Keystroke delay ms (default:
|
|
52
|
+
delay: { type: "number", description: "Keystroke delay ms (default: 30)" },
|
|
53
53
|
clearFirst: { type: "boolean", description: "Clear first (default: true)" },
|
|
54
54
|
},
|
|
55
55
|
required: ["text"],
|
|
@@ -503,16 +503,6 @@ export const toolDefinitions = [
|
|
|
503
503
|
required: ["id"],
|
|
504
504
|
},
|
|
505
505
|
},
|
|
506
|
-
{
|
|
507
|
-
name: "getAllInteractiveElements",
|
|
508
|
-
description: "Get all interactive elements with selectors. For understanding available actions.",
|
|
509
|
-
inputSchema: {
|
|
510
|
-
type: "object",
|
|
511
|
-
properties: {
|
|
512
|
-
includeHidden: { type: "boolean", description: "Include hidden (default: false)" },
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
506
|
{
|
|
517
507
|
name: "findElementsByText",
|
|
518
508
|
description: "Find elements by visible text content and get their selectors. Use this INSTEAD of executeScript when you need to find elements. Returns working selectors that can be used with click/type tools. Can optionally perform actions directly.",
|
package/server/tool-groups.js
CHANGED
package/server/tool-schemas.js
CHANGED
|
@@ -29,7 +29,7 @@ export const TypeSchema = z.object({
|
|
|
29
29
|
id: z.string().optional().describe("APOM element ID from analyzePage (e.g., 'input_20', 'input_33'). Mutually exclusive with selector."),
|
|
30
30
|
selector: z.string().optional().describe("CSS selector for input element. Mutually exclusive with id."),
|
|
31
31
|
text: z.string().describe("Text to type"),
|
|
32
|
-
delay: z.number().optional().describe("Delay between keystrokes in ms (default:
|
|
32
|
+
delay: z.number().optional().describe("Delay between keystrokes in ms (default: 30)"),
|
|
33
33
|
clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
|
|
34
34
|
}).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
|
|
35
35
|
message: "Either 'id' or 'selector' must be provided, but not both"
|
|
@@ -269,10 +269,6 @@ export const GetElementDetailsSchema = z.object({
|
|
|
269
269
|
refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
-
export const GetAllInteractiveElementsSchema = z.object({
|
|
273
|
-
includeHidden: z.boolean().optional().describe("Include hidden elements (default: false)"),
|
|
274
|
-
});
|
|
275
|
-
|
|
276
272
|
export const FindElementsByTextSchema = z.object({
|
|
277
273
|
text: z.string().describe("Text to search for in elements"),
|
|
278
274
|
exact: z.boolean().optional().describe("Exact match only (default: false)"),
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Action Diagnostics
|
|
3
|
+
* Collects errors and waits for network requests after user actions (click, navigation, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { consoleLogs, networkRequests } from '../browser/page-manager.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wait for pending network requests to complete
|
|
10
|
+
* @param {number} beforeActionTimestamp - Timestamp before action to track new requests
|
|
11
|
+
* @param {number} initialWaitMs - Initial wait time before checking (default: 500ms)
|
|
12
|
+
* @param {number} maxWaitMs - Maximum time to wait for requests (default: 5000ms)
|
|
13
|
+
* @returns {Promise<{pendingFound: boolean, waitedMs: number, completedRequests: number, totalRequests: number}>}
|
|
14
|
+
*/
|
|
15
|
+
export async function waitForPendingRequests(beforeActionTimestamp, initialWaitMs = 500, maxWaitMs = 5000) {
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
|
|
18
|
+
// Step 1: Wait initial period to let requests start
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, initialWaitMs));
|
|
20
|
+
|
|
21
|
+
// Step 2: Get requests that started AFTER action
|
|
22
|
+
const getPostActionRequests = () => {
|
|
23
|
+
const cutoffDate = new Date(beforeActionTimestamp).toISOString();
|
|
24
|
+
return networkRequests.filter(req => req.timestamp >= cutoffDate);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Step 3: Check for pending requests (from post-action requests)
|
|
28
|
+
const checkPending = () => {
|
|
29
|
+
return getPostActionRequests().filter(req => req.status === 'pending');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let pending = checkPending();
|
|
33
|
+
let allPostActionRequests = getPostActionRequests();
|
|
34
|
+
const initialPendingCount = pending.length;
|
|
35
|
+
|
|
36
|
+
// Step 4: If there are pending requests OR new requests appeared, wait for completion
|
|
37
|
+
if (pending.length > 0 || allPostActionRequests.length > 0) {
|
|
38
|
+
// Wait for pending requests to complete (with timeout)
|
|
39
|
+
while (pending.length > 0 && (Date.now() - startTime) < maxWaitMs) {
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms
|
|
41
|
+
pending = checkPending();
|
|
42
|
+
allPostActionRequests = getPostActionRequests(); // Update total count
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const finalRequests = getPostActionRequests();
|
|
47
|
+
const completedRequests = finalRequests.filter(req => req.status === 'completed' || (typeof req.status === 'number'));
|
|
48
|
+
const pendingRequests = pending.map(req => ({
|
|
49
|
+
url: req.url,
|
|
50
|
+
method: req.method,
|
|
51
|
+
timestamp: req.timestamp
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
pendingFound: initialPendingCount > 0,
|
|
56
|
+
waitedMs: Date.now() - startTime,
|
|
57
|
+
completedRequests: completedRequests.length,
|
|
58
|
+
stillPending: pending.length,
|
|
59
|
+
pendingRequests: pendingRequests,
|
|
60
|
+
totalRequests: finalRequests.length
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collect errors from console logs and network requests
|
|
66
|
+
* @param {number} sinceTimestamp - Only collect errors after this timestamp (default: collect recent errors)
|
|
67
|
+
* @param {number} maxConsoleErrors - Maximum console errors to return (default: 15)
|
|
68
|
+
* @param {number} maxNetworkErrors - Maximum network errors to return (default: 15)
|
|
69
|
+
* @returns {Object} Object with consoleErrors and networkErrors arrays
|
|
70
|
+
*/
|
|
71
|
+
export function collectErrors(sinceTimestamp = null, maxConsoleErrors = 15, maxNetworkErrors = 15) {
|
|
72
|
+
const errors = {
|
|
73
|
+
consoleErrors: [],
|
|
74
|
+
networkErrors: [],
|
|
75
|
+
jsExceptions: [],
|
|
76
|
+
consoleErrorsOmitted: 0,
|
|
77
|
+
networkErrorsOmitted: 0
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// If no timestamp provided, look back 10 seconds
|
|
81
|
+
const cutoffTime = sinceTimestamp || (Date.now() - 10000);
|
|
82
|
+
const cutoffDate = new Date(cutoffTime).toISOString();
|
|
83
|
+
|
|
84
|
+
// Collect console errors (with limit)
|
|
85
|
+
let consoleErrorCount = 0;
|
|
86
|
+
consoleLogs.forEach(log => {
|
|
87
|
+
if (log.type === 'error') {
|
|
88
|
+
// Check if error is recent
|
|
89
|
+
const logTime = new Date(log.timestamp || 0).toISOString();
|
|
90
|
+
if (!sinceTimestamp || logTime >= cutoffDate) {
|
|
91
|
+
if (consoleErrorCount < maxConsoleErrors) {
|
|
92
|
+
errors.consoleErrors.push({
|
|
93
|
+
message: log.text,
|
|
94
|
+
timestamp: log.timestamp,
|
|
95
|
+
location: log.location || 'unknown'
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
errors.consoleErrorsOmitted++;
|
|
99
|
+
}
|
|
100
|
+
consoleErrorCount++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Collect network errors (failed requests, with limit)
|
|
106
|
+
let networkErrorCount = 0;
|
|
107
|
+
networkRequests.forEach(req => {
|
|
108
|
+
if (req.status === 'failed' || (typeof req.status === 'number' && req.status >= 400)) {
|
|
109
|
+
// Check if error is recent
|
|
110
|
+
const reqTime = req.timestamp;
|
|
111
|
+
if (!sinceTimestamp || reqTime >= cutoffDate) {
|
|
112
|
+
if (networkErrorCount < maxNetworkErrors) {
|
|
113
|
+
errors.networkErrors.push({
|
|
114
|
+
url: req.url,
|
|
115
|
+
method: req.method,
|
|
116
|
+
status: req.status,
|
|
117
|
+
statusText: req.statusText,
|
|
118
|
+
errorText: req.errorText,
|
|
119
|
+
timestamp: req.timestamp
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
errors.networkErrorsOmitted++;
|
|
123
|
+
}
|
|
124
|
+
networkErrorCount++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return errors;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Full post-action diagnostics: wait for requests and collect errors
|
|
134
|
+
* @param {Page} page - Puppeteer page instance
|
|
135
|
+
* @param {number} beforeActionTimestamp - Timestamp before action (to filter errors)
|
|
136
|
+
* @returns {Promise<Object>} Diagnostics result with errors and network info
|
|
137
|
+
*/
|
|
138
|
+
export async function runPostClickDiagnostics(page, beforeActionTimestamp) {
|
|
139
|
+
// Wait for network requests (passing timestamp to track post-action requests)
|
|
140
|
+
// maxWait = 20s to give slow APIs time to complete
|
|
141
|
+
const networkInfo = await waitForPendingRequests(beforeActionTimestamp, 500, 20000);
|
|
142
|
+
|
|
143
|
+
// Small delay to let pending requests update their error status
|
|
144
|
+
// (handles case where request completes with error right after maxWait expires)
|
|
145
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
146
|
+
|
|
147
|
+
// Check for chrome error page (ERR_CONNECTION_REFUSED, etc.)
|
|
148
|
+
const url = page.url();
|
|
149
|
+
let chromeErrorInfo = null;
|
|
150
|
+
if (url.startsWith('chrome-error://')) {
|
|
151
|
+
chromeErrorInfo = await page.evaluate(() => {
|
|
152
|
+
const errorCode = document.querySelector('#error-code');
|
|
153
|
+
const suggestionText = document.querySelector('.suggestions');
|
|
154
|
+
return {
|
|
155
|
+
errorCode: errorCode?.textContent || 'UNKNOWN_ERROR',
|
|
156
|
+
suggestion: suggestionText?.textContent?.trim() || 'Connection failed'
|
|
157
|
+
};
|
|
158
|
+
}).catch(() => ({ errorCode: 'PAGE_LOAD_ERROR', suggestion: 'Navigation failed' }));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Collect errors that occurred after the action (including errors from just-completed requests)
|
|
162
|
+
const errors = collectErrors(beforeActionTimestamp);
|
|
163
|
+
|
|
164
|
+
// Combine into diagnostics report
|
|
165
|
+
const diagnostics = {
|
|
166
|
+
networkActivity: {
|
|
167
|
+
hadPendingRequests: networkInfo.pendingFound,
|
|
168
|
+
completedRequests: networkInfo.completedRequests,
|
|
169
|
+
stillPending: networkInfo.stillPending,
|
|
170
|
+
pendingRequests: networkInfo.pendingRequests,
|
|
171
|
+
totalRequests: networkInfo.totalRequests,
|
|
172
|
+
waitedMs: networkInfo.waitedMs
|
|
173
|
+
},
|
|
174
|
+
chromeError: chromeErrorInfo,
|
|
175
|
+
errors: {
|
|
176
|
+
consoleErrors: errors.consoleErrors,
|
|
177
|
+
networkErrors: errors.networkErrors,
|
|
178
|
+
consoleErrorsOmitted: errors.consoleErrorsOmitted,
|
|
179
|
+
networkErrorsOmitted: errors.networkErrorsOmitted,
|
|
180
|
+
totalErrors: errors.consoleErrors.length + errors.networkErrors.length
|
|
181
|
+
},
|
|
182
|
+
hasErrors: (errors.consoleErrors.length + errors.networkErrors.length) > 0 || chromeErrorInfo !== null
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return diagnostics;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Format diagnostics for AI-friendly output
|
|
190
|
+
* @param {Object} diagnostics - Diagnostics object from runPostClickDiagnostics
|
|
191
|
+
* @returns {string} Formatted text for AI
|
|
192
|
+
*/
|
|
193
|
+
export function formatDiagnosticsForAI(diagnostics) {
|
|
194
|
+
let output = '\n\n** POST-ACTION DIAGNOSTICS **';
|
|
195
|
+
|
|
196
|
+
// Chrome error page (connection refused, DNS failed, etc.)
|
|
197
|
+
if (diagnostics.chromeError) {
|
|
198
|
+
output += `\n\n🔴 CRITICAL: Navigation Failed`;
|
|
199
|
+
output += `\n Error: ${diagnostics.chromeError.errorCode}`;
|
|
200
|
+
output += `\n Suggestion: ${diagnostics.chromeError.suggestion}`;
|
|
201
|
+
output += `\n → Backend likely not running or unreachable`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Network activity
|
|
205
|
+
const netActivity = diagnostics.networkActivity;
|
|
206
|
+
if (netActivity.totalRequests > 0) {
|
|
207
|
+
const errorCount = diagnostics.errors.networkErrors.length;
|
|
208
|
+
const successCount = netActivity.completedRequests - errorCount;
|
|
209
|
+
|
|
210
|
+
// Show warning if there are pending requests after timeout
|
|
211
|
+
if (netActivity.stillPending > 0) {
|
|
212
|
+
output += `\n⚠️ Network: ${successCount} OK, ${errorCount} failed, ${netActivity.stillPending} PENDING`;
|
|
213
|
+
output += `\n ⏱️ Timeout: Stopped waiting after ${netActivity.waitedMs}ms`;
|
|
214
|
+
output += `\n → ${netActivity.stillPending} request(s) still running - status unknown`;
|
|
215
|
+
output += `\n → May complete successfully or fail - cannot determine outcome`;
|
|
216
|
+
} else if (errorCount > 0) {
|
|
217
|
+
output += `\n⚠️ Network: ${successCount} OK, ${errorCount} failed (${netActivity.waitedMs}ms)`;
|
|
218
|
+
} else {
|
|
219
|
+
output += `\n✓ Network: ${netActivity.completedRequests} completed (${netActivity.waitedMs}ms)`;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
output += '\n✓ No network requests triggered';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Errors
|
|
226
|
+
if (diagnostics.errors.totalErrors > 0) {
|
|
227
|
+
output += `\n\n⚠️ ERRORS DETECTED (${diagnostics.errors.totalErrors} total):`;
|
|
228
|
+
|
|
229
|
+
// Console errors
|
|
230
|
+
if (diagnostics.errors.consoleErrors.length > 0) {
|
|
231
|
+
output += `\n\nJavaScript Console Errors (${diagnostics.errors.consoleErrors.length}):`;
|
|
232
|
+
diagnostics.errors.consoleErrors.forEach((err, idx) => {
|
|
233
|
+
output += `\n ${idx + 1}. ${err.message}`;
|
|
234
|
+
if (err.location && err.location !== 'unknown') {
|
|
235
|
+
output += ` [${err.location}]`;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
// Show if some errors were omitted
|
|
239
|
+
if (diagnostics.errors.consoleErrorsOmitted > 0) {
|
|
240
|
+
output += `\n ... and ${diagnostics.errors.consoleErrorsOmitted} more console error(s) (omitted to prevent spam)`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Network errors
|
|
245
|
+
if (diagnostics.errors.networkErrors.length > 0) {
|
|
246
|
+
output += `\n\nNetwork Errors (${diagnostics.errors.networkErrors.length}):`;
|
|
247
|
+
diagnostics.errors.networkErrors.forEach((err, idx) => {
|
|
248
|
+
output += `\n ${idx + 1}. ${err.method} ${err.url}`;
|
|
249
|
+
output += `\n Status: ${err.status}${err.statusText ? ' ' + err.statusText : ''}`;
|
|
250
|
+
if (err.errorText) {
|
|
251
|
+
output += `\n Error: ${err.errorText}`;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// Show if some errors were omitted
|
|
255
|
+
if (diagnostics.errors.networkErrorsOmitted > 0) {
|
|
256
|
+
output += `\n ... and ${diagnostics.errors.networkErrorsOmitted} more network error(s) (omitted to prevent spam)`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
output += '\n✓ No errors detected';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Pending requests (if any still running after timeout)
|
|
264
|
+
if (netActivity.stillPending > 0 && netActivity.pendingRequests.length > 0) {
|
|
265
|
+
output += `\n\n⏳ PENDING REQUESTS (${netActivity.stillPending} still running):`;
|
|
266
|
+
netActivity.pendingRequests.forEach((req, idx) => {
|
|
267
|
+
output += `\n ${idx + 1}. ${req.method} ${req.url}`;
|
|
268
|
+
const elapsed = Date.now() - new Date(req.timestamp).getTime();
|
|
269
|
+
output += `\n Running for: ${elapsed}ms`;
|
|
270
|
+
});
|
|
271
|
+
output += `\n\n💡 Suggestion: These requests may be slow or hanging`;
|
|
272
|
+
output += `\n → Check backend performance or network connectivity`;
|
|
273
|
+
output += `\n → Consider using getNetworkRequest() to monitor progress`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return output;
|
|
277
|
+
}
|