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.
@@ -20,13 +20,16 @@ async function takeActionScreenshot(page, clip) {
20
20
  };
21
21
  }
22
22
 
23
- // Helper function to execute actions on elements
24
- export async function executeElementAction(page, selector, action) {
23
+ // Helper function to execute actions on elements.
24
+ // `frame` (P0-1) is the DOM context used for element resolution/evaluation —
25
+ // pass the active iframe's Frame to act inside it. Defaults to `page` (main frame).
26
+ // Page-level operations (keyboard, screenshot) always use `page`.
27
+ export async function executeElementAction(page, selector, action, frame = page) {
25
28
  if (!action || !action.type) {
26
29
  return null;
27
30
  }
28
31
 
29
- const element = await page.$(selector);
32
+ const element = await frame.$(selector);
30
33
  if (!element) {
31
34
  throw new Error(`Element not found for action: ${selector}`);
32
35
  }
@@ -83,7 +86,7 @@ export async function executeElementAction(page, selector, action) {
83
86
  case 'scrollTo':
84
87
  await element.scrollIntoView({ behavior: 'auto' });
85
88
  await new Promise(resolve => setTimeout(resolve, action.waitAfter || 300));
86
- const position = await page.evaluate(() => ({
89
+ const position = await frame.evaluate(() => ({
87
90
  x: window.scrollX,
88
91
  y: window.scrollY
89
92
  }));
@@ -128,7 +131,7 @@ export async function executeElementAction(page, selector, action) {
128
131
  for (const style of action.styles) {
129
132
  stylesObject[style.name] = style.value;
130
133
  }
131
- await page.evaluate((sel, styles) => {
134
+ await frame.evaluate((sel, styles) => {
132
135
  const el = document.querySelector(sel);
133
136
  if (el) {
134
137
  Object.entries(styles).forEach(([key, value]) => {
@@ -30,6 +30,10 @@ export const isWindows = process.platform === 'win32' || isWSL;
30
30
  * @returns {string} - Path to Chrome executable
31
31
  */
32
32
  export function getChromePath() {
33
+ // Explicit override wins on every platform
34
+ if (process.env.CHROMETOOLS_CHROME_PATH) {
35
+ return process.env.CHROMETOOLS_CHROME_PATH;
36
+ }
33
37
  if (process.platform === 'win32') {
34
38
  // Native Windows
35
39
  return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
@@ -57,6 +61,17 @@ export function getTempDir() {
57
61
  }
58
62
 
59
63
  /**
60
- * Chrome remote debugging port
64
+ * Get the Chrome user-data-dir used when launching a new instance.
65
+ * Override with CHROMETOOLS_USER_DATA_DIR to point at a real/cloned profile
66
+ * that already holds the user's login session/cookies.
67
+ * @returns {string} - Path to Chrome user data directory
68
+ */
69
+ export function getUserDataDir() {
70
+ return process.env.CHROMETOOLS_USER_DATA_DIR || `${getTempDir()}/chrome-mcp-profile`;
71
+ }
72
+
73
+ /**
74
+ * Chrome remote debugging port.
75
+ * Override with CHROMETOOLS_DEBUG_PORT (defaults to 9222).
61
76
  */
62
- export const CHROME_DEBUG_PORT = 9222;
77
+ export const CHROME_DEBUG_PORT = parseInt(process.env.CHROMETOOLS_DEBUG_PORT, 10) || 9222;
@@ -298,21 +298,38 @@ export function formatDiagnosticsForAI(diagnostics) {
298
298
  const netActivity = diagnostics.networkActivity;
299
299
  const trackedRequests = netActivity.trackedRequests || [];
300
300
 
301
- // Show requests detected within 200ms after action
302
- if (trackedRequests.length > 0) {
303
- output += `\n\n📡 Network requests (${trackedRequests.length}):`;
304
- trackedRequests.forEach((req, idx) => {
301
+ // Show requests detected within 200ms after action.
302
+ // P1-6: filter to data requests (XHR/Fetch) and drop static assets — a single
303
+ // SPA navigation pulls dozens of JS chunks/CSS/fonts that drown the signal.
304
+ // Cap the printed list and summarize the rest.
305
+ const STATIC_TYPES = new Set(['Script', 'Stylesheet', 'Font', 'Image', 'Media', 'Manifest', 'Other']);
306
+ const MAX_SHOWN = 12;
307
+ const dataRequests = trackedRequests.filter(req => {
308
+ if (!req.type) return true; // no type info → keep (bridge/legacy)
309
+ return !STATIC_TYPES.has(req.type);
310
+ });
311
+ const staticDropped = trackedRequests.length - dataRequests.length;
312
+
313
+ if (dataRequests.length > 0) {
314
+ const shown = dataRequests.slice(0, MAX_SHOWN);
315
+ output += `\n\n📡 Network requests (XHR/Fetch: ${dataRequests.length}${staticDropped > 0 ? `, ${staticDropped} static asset(s) hidden` : ''}):`;
316
+ shown.forEach((req, idx) => {
305
317
  const statusIcon = req.status === 'pending' ? '⏳' :
306
318
  (req.status === 'completed' || (typeof req.status === 'number' && req.status < 400) ? '✓' : '✗');
307
319
  const statusText = req.statusText || req.status || 'pending';
308
320
  output += `\n ${idx + 1}. ${statusIcon} ${req.method} ${req.url}`;
309
321
  output += `\n → Status: ${statusText}`;
310
322
  });
323
+ if (dataRequests.length > MAX_SHOWN) {
324
+ output += `\n … ${dataRequests.length - MAX_SHOWN} more`;
325
+ }
311
326
 
312
327
  // Show if some requests are still pending after timeout
313
328
  if (netActivity.stillPending > 0) {
314
329
  output += `\n\n⏳ ${netActivity.stillPending} request(s) still pending after ${Math.round(netActivity.waitedMs)}ms timeout`;
315
330
  }
331
+ } else if (staticDropped > 0) {
332
+ output += `\n\n📡 No XHR/Fetch requests detected within 200ms (${staticDropped} static asset(s) hidden)`;
316
333
  } else {
317
334
  output += '\n\n📡 No network requests detected within 200ms';
318
335
  }
@@ -1,94 +0,0 @@
1
- # Progress: SEGM-537 QA Unblockers
2
-
3
- **Status**: NEEDS APPROVAL
4
- **Spec**: [SEGM-537-UNBLOCKERS_SPEC.md](./SEGM-537-UNBLOCKERS_SPEC.md)
5
- **Created**: 2026-05-28
6
-
7
- ## Phase 0 — Approval & alignment
8
-
9
- - [ ] Пользователь одобрил scope (все 5 блокеров)
10
- - [ ] Пользователь одобрил дизайн API (имена параметров, дефолты)
11
- - [ ] Дать команду «погнали, начни с Phase 1»
12
-
13
- ## Phase 1 — Блокер #1: portal scan расширение
14
-
15
- - [x] Расширить `pom/apom-tree-converter.js` — добавить `portalSelectors` параметр + третий проход (~120)
16
- - [x] ~~Переименовать `forceMarkModalTree`~~ — переиспользована as-is, переименование избыточно
17
- - [x] Прокинуть `includePortals` / `portalSelectors` через `page.evaluate` в `index.js`
18
- - [x] Обновить `server/tool-schemas.js` + `server/tool-definitions.js` для `analyzePage`
19
- - [x] `npm run build` — Syntax validation passed
20
- - [ ] Verification: бенчмарк Google (требует запуска MCP — пользователь должен перезапустить и прогнать)
21
- - [ ] Verification: ручной тест на QA-стенде (требует доступа QA — отдаём после рестарта MCP)
22
- - [x] Обновить `README.md` — секция `analyzePage`
23
-
24
- ## Phase 2 — Блокер #2: click + waitForSelector
25
-
26
- - [x] Добавить `waitForSelector` / `waitTimeoutMs` в `executeClickAction` + `click` handler
27
- - [x] Возвращать `appearedInMs` в результате (или `⚠️ WAIT_TIMEOUT` сообщение)
28
- - [x] Обновить `server/tool-schemas.js` + `server/tool-definitions.js` для `click`
29
- - [x] `npm run build` — Syntax validation passed
30
- - [ ] Verification: на стенде клик «три точки» с `waitForSelector` (требует рестарта MCP)
31
- - [ ] Verification: несуществующий селектор → таймаут (требует рестарта MCP)
32
- - [x] Обновить `README.md` — секция `click`
33
-
34
- ## Phase 3 — Блокер #4: ModelRegistry stale error
35
-
36
- - [x] **Root cause fix**: `quickRegisterElements` теперь инжектит models code (раньше не делал → ReferenceError при auto-refresh после navigation)
37
- - [x] User-friendly fallback: catch на `ReferenceError: ModelRegistry is not defined` в `resolveSelector` → сообщение «APOM registry stale, call analyzePage()»
38
- - [x] `npm run build` — Syntax validation passed
39
- - [ ] Verification: воспроизвести (analyzePage → navigate → click) — требует рестарта MCP
40
-
41
- ## Phase 4 — Блокер #3: screenshot без selector
42
-
43
- - [x] Ослабить `.refine` в `ScreenshotSchema` (запрещаем только конфликт, не отсутствие)
44
- - [x] Без id/selector — viewport screenshot через `processScreenshot`
45
- - [x] Обновить `server/tool-schemas.js` + `server/tool-definitions.js`
46
- - [x] `npm run build` — Syntax validation passed
47
- - [ ] Verification: `screenshot()` без аргументов возвращает viewport — требует рестарта MCP
48
- - [x] Обновить `README.md` — секция `screenshot`
49
-
50
- ## Phase 5 — Блокер #5: executeScript auto-IIFE
51
-
52
- - [x] Препроцессор скрипта в `executeScript` handler (index.js)
53
- - [x] Regex: оборачивает только если `^return[\s;]` И код не содержит `function ...`
54
- - [x] `npm run build` — Syntax validation passed
55
- - [x] Verification (offline): 5 канонических случаев работают корректно (`return document.title`, ` return 42;`, IIFE, plain expr, `function ... return`)
56
- - [x] Обновить `README.md` — секция `executeScript`
57
-
58
- ## Phase 7 — click autoAnalyzeAfter
59
-
60
- - [x] Helper `getApomSnapshot(page)` в index.js
61
- - [x] Pre/post snapshot в click handler с регистрацией новых id через `quickRegisterElements`
62
- - [x] Дельта `+N appeared: id:"text"` / `-N disappeared` / `No APOM changes` в результате click
63
- - [x] Schema + tool-definitions + README
64
- - [x] `npm run build` — passed
65
- - [x] Verified на стенде: Phase 7 механика работает (snapshots + diff + content append). На SEGM-стенде дельта пустая из-за Phase 8 проблемы (popup не попадает в дерево).
66
-
67
- ## Phase 8 — In-tree popup detection
68
-
69
- Открыт **при e2e-тесте Phase 7**: popup-меню стенда (Popper-style) рендерится внутри 0-height wrapper и не попадает в APOM tree, потому что `isVisible` отбрасывает wrapper.
70
-
71
- - [x] Расширение portal-scan в `pom/apom-tree-converter.js` — новый блок после id-portalSelectors
72
- - [x] `findPositionedPopup(el, depth=3)` — рекурсивный поиск absolute/fixed-positioned child с реальными bounds внутри 0×0 wrapper
73
- - [x] force-mark найденного popup через существующий `forceMarkModalTree`
74
- - [x] Используется тот же flag `portalInclude` (default `true`) — opt-out возможен через `includePortals: false`
75
- - [x] README обновлён
76
- - [x] `npm run build` — passed
77
- - [ ] Verification на SEGM-стенде — требует рестарта MCP
78
-
79
- ## Phase 6 — Финализация сессии
80
-
81
- - [ ] Прогон всех verification на реальном QA-стенде SEGM-537 (если доступ есть)
82
- - [ ] Спросить пользователя: bump version + CHANGELOG entry?
83
- - [ ] Если YES — single CHANGELOG entry для всей сессии, версия += patch (3.5.4 → 3.5.5)
84
- - [ ] Отдать QA на повтор сценария из отчёта
85
-
86
- ## Verification status: per-phase checks
87
-
88
- Каждая phase имеет свои verification-шаги выше. Не двигаться к следующей пока текущая не зелёная.
89
-
90
- ## Открытые вопросы (повторяю из spec)
91
-
92
- 1. Дефолтные id-портал-селекторы (`#menu-popup-root`, `#modal-root`, `#tooltip-root`, `#popover-root`) — норм или хотите другой набор?
93
- 2. `keepOpen: true` для `click` — пропускаем, делаем только `waitForSelector`. Если QA увидит что попап всё равно закрывается — добавим в Phase 2.5.
94
- 3. Авто-IIFE для `executeScript` — допустимый риск перехвата `return` в случаях, где `return` намеренно внутри функции? Митигируем regex'ом на top-level.
@@ -1,187 +0,0 @@
1
- # Spec: SEGM-537 QA Unblockers
2
-
3
- **Status**: DRAFT — waiting for user approval
4
- **Created**: 2026-05-28
5
- **Triggered by**: QA-отчёт SEGM-537 (Bulk Creatives Upload) — пять блокеров ChromeTools MCP не дали довести QA до фичи
6
-
7
- ## Motivation
8
-
9
- При прогоне QA SEGM-537 на стенде `selfservice.segmento.mts-corp.ru` ChromeTools MCP заблокировал прогресс на шаге «открыть меню действий → Настройки». QA довёл до этапа 1 из 7 и зафиксировал пять конкретных проблем в инструменте. Фича сама не проверена — нужно разблокировать инструмент.
10
-
11
- ## Goals
12
-
13
- Разблокировать сценарий: открыть React Portal попап (`#menu-popup-root`), дождаться его появления после клика, кликнуть пункт меню, продолжить QA-сценарий. Попутно убрать раздражающие шероховатости (`screenshot` без аргументов, понятная ошибка про устаревший APOM `id`, поведение `executeScript` с top-level `return`).
14
-
15
- ## Non-goals
16
-
17
- - Не переписываем `analyzePage` целиком — расширяем существующую portal-логику в `pom/apom-tree-converter.js:88-115`.
18
- - Не делаем «универсальный» wait для произвольных DOM-изменений — добавляем только `waitForSelector` как опцию к `click`.
19
- - Не лезем в код QA-стенда (Замечание #6 из отчёта — про прямые URL `/app/campaign/settings/77130` — не наша зона).
20
-
21
- ---
22
-
23
- ## Блокер #1 — analyzePage не видит React Portals (menu/tooltip)
24
-
25
- ### Текущее поведение
26
-
27
- `pom/apom-tree-converter.js:88-115` сканирует прямых детей `<body>` по жёсткому списку CSS-классов framework-портал-контейнеров (`ant-modal-root`, `MuiDialog-root`, `mantine-Modal-root` и т.п.) — это про **модальные диалоги**. Меню/dropdown/tooltip-порталы у проектов часто рендерятся в `<div id="menu-popup-root">` / `<div id="tooltip-root">` (см. `client/index.html` стенда SEGM-537) — эти контейнеры детектор пропускает.
28
-
29
- ### Целевое поведение
30
-
31
- `analyzePage` принимает опциональный параметр `includePortals` (default `true`):
32
-
33
- ```js
34
- analyzePage({ url?, includePortals?: boolean = true, portalSelectors?: string[] })
35
- ```
36
-
37
- - `includePortals: true` — по умолчанию включаем порталы.
38
- - `portalSelectors` — массив CSS-селекторов (default: `['#modal-root', '#menu-popup-root', '#tooltip-root', '#popover-root', '[data-portal]']`), любые `body > selector` контейнеры force-включаются в APOM tree с compact-форматом, как уже делается для модалок.
39
- - Существующая логика модалок-фреймворков сохраняется без изменений (не ломаем уже работающий ant/MUI detection).
40
-
41
- ### Реализация
42
-
43
- 1. В `pom/apom-tree-converter.js` рядом с `portalPatterns` (~89) добавить `idPortalSelectors` — массив дефолтов, мерж с user-провайдеными.
44
- 2. В пасс «scan body direct children» (~107) добавить второй проход: `document.querySelectorAll(idPortalSelectors.join(','))` → force-include через существующую `forceIncludeModalSubtree()` (или вынести в `forceIncludePortalSubtree`, более широкое имя).
45
- 3. Прокинуть `includePortals` / `portalSelectors` через `page.evaluate` в `index.js:~2400`.
46
- 4. В `tool-schemas.js` добавить два параметра с описанием и дефолтами.
47
-
48
- ### Verification
49
-
50
- - На стенде с открытым меню действий (`document.querySelector('#menu-popup-root').children.length > 0`) `analyzePage` возвращает элементы попапа в дереве.
51
- - Бенчмарк из CLAUDE.md (Google Search) — размер вывода не вырастает на > 5KB при отсутствии порталов (контейнеры пустые → ничего не добавляется).
52
- - `findElementsByText` — проверить что на тексте «Настройки» внутри открытого попапа возвращает > 0 (по отчёту QA там может быть отдельный баг, верифицировать).
53
-
54
- ---
55
-
56
- ## Блокер #2 — атомарный click + ожидание появления попапа
57
-
58
- ### Текущее поведение
59
-
60
- `click({ id })` отправляет `mousePressed` + `mouseReleased` и возвращает управление. Попап-меню часто закрывается по `mousedown` outside (focus-out) при следующем действии MCP, поэтому к моменту `analyzePage`/`executeScript` контента в `#menu-popup-root` уже нет (см. отчёт: `childCount: 0`).
61
-
62
- ### Целевое поведение
63
-
64
- ```js
65
- click({
66
- id | selector,
67
- waitForSelector?: string, // дождаться появления селектора после клика
68
- waitTimeoutMs?: number = 2000, // таймаут ожидания
69
- })
70
- ```
71
-
72
- - После `mouseReleased` запускается `page.waitForSelector(waitForSelector, { timeout: waitTimeoutMs, visible: true })`.
73
- - Возвращает `{ clicked: true, appearedAfter?: 'selector', appearedInMs: number }` либо понятную ошибку `WAIT_TIMEOUT: selector did not appear within Xms after click`.
74
- - Не двигаем курсор после клика (это уже так, но проверить — отчёт упоминает «фокус-аут» как причину закрытия).
75
-
76
- ### NOT добавляем
77
-
78
- `keepOpen: true` (из отчёта) — не понятно как это реализовать без хака на уровне страницы. Если `waitForSelector` решает проблему — `keepOpen` не нужен. Если QA увидит, что попап всё равно закрывается — вернёмся к вопросу.
79
-
80
- ### Реализация
81
-
82
- 1. В `index.js` (`click` handler ~) после `await page.mouse.click(...)` — условное `await page.waitForSelector(opts.waitForSelector, ...)`.
83
- 2. Замерить время `Date.now() - start` для `appearedInMs`.
84
- 3. В `tool-schemas.js` добавить `waitForSelector`, `waitTimeoutMs`.
85
-
86
- ### Verification
87
-
88
- - На стенде кликнуть «три точки» с `waitForSelector: '#menu-popup-root > div'` → попап остаётся открытым, селектор найден.
89
- - Кликнуть с заведомо несуществующим селектором → понятная ошибка с таймаутом.
90
-
91
- ---
92
-
93
- ## Блокер #3 — screenshot без selector
94
-
95
- ### Текущее поведение
96
-
97
- ```
98
- Either 'id' or 'selector' must be provided, but not both
99
- ```
100
-
101
- Обязателен один из двух параметров.
102
-
103
- ### Целевое поведение
104
-
105
- Оба параметра опциональны. Если ни `id`, ни `selector` не переданы — делается **viewport screenshot** (как `saveScreenshot('viewport')`).
106
-
107
- ### Реализация
108
-
109
- В `index.js` (`screenshot` handler) ослабить валидацию, в дефолтной ветке использовать `page.screenshot({ fullPage: false })` (либо `fullPage: true` если будет отдельный параметр).
110
-
111
- ### Verification
112
-
113
- `screenshot()` без аргументов возвращает viewport-картинку.
114
-
115
- ---
116
-
117
- ## Блокер #4 — `ModelRegistry is not defined` после навигации
118
-
119
- ### Текущее поведение
120
-
121
- После навигации/перерисовки страницы повторный `click({ id: 'button_47' })` иногда падает с `ModelRegistry is not defined` (browser-side глобал сбрасывается, id из старого `analyzePage` устарел).
122
-
123
- ### Целевое поведение
124
-
125
- `click` (и `executeModelAction`) ловят `ReferenceError: ModelRegistry is not defined` и возвращают понятную ошибку:
126
-
127
- ```
128
- APOM registry stale (navigation/reload occurred). Call analyzePage() to refresh element ids.
129
- ```
130
-
131
- ### Реализация
132
-
133
- В `index.js` (action handlers ~672) обернуть `page.evaluate` в try/catch, маппить `ReferenceError` на бизнес-сообщение.
134
-
135
- ### NOT делаем
136
-
137
- Авто-вызов `analyzePage` — потенциально дорого, плюс пользователь может не ожидать перерасчёт состояния.
138
-
139
- ### Verification
140
-
141
- Воспроизвести: `analyzePage` → `navigateTo(другая страница)` → `click({ id: 'button_47' })` → получить новое сообщение, не `ReferenceError`.
142
-
143
- ---
144
-
145
- ## Блокер #5 — executeScript падает на top-level `return`
146
-
147
- ### Текущее поведение
148
-
149
- ```js
150
- executeScript({ script: 'return 42;' }) // Illegal return statement
151
- ```
152
-
153
- Юзеру приходится оборачивать в IIFE: `(() => { return 42; })();`.
154
-
155
- ### Целевое поведение
156
-
157
- Авто-оборачивание: если переданный `script` содержит top-level `return` (regex `/^\s*return\s|;\s*return\s/`), оборачиваем в `(async () => { <script> })()` перед `page.evaluate`.
158
-
159
- ### Реализация
160
-
161
- В `index.js` (`executeScript` handler) — препроцессор кода.
162
-
163
- ### Verification
164
-
165
- - `executeScript({ script: 'return document.title' })` → возвращает title.
166
- - `executeScript({ script: '(() => { return 42; })()' })` — продолжает работать (уже завёрнут).
167
- - `executeScript({ script: 'document.title' })` — продолжает работать.
168
-
169
- ---
170
-
171
- ## Affected files
172
-
173
- - `pom/apom-tree-converter.js` — Блокер #1 (portal scan расширение)
174
- - `index.js` — Блокеры #1, #2, #3, #4, #5 (handler-ы + page.evaluate args)
175
- - `tool-schemas.js` — Блокеры #1, #2, #3 (новые параметры в JSON Schema)
176
- - `README.md` — документация новых параметров (обязательно по CLAUDE.md)
177
- - `CHANGELOG.md` — одна запись в конце сессии (только по запросу пользователя)
178
-
179
- ## Out of scope / открытые вопросы
180
-
181
- 1. **#1**: список дефолтных id-портал-селекторов — взят с потолка по практике (`#menu-popup-root` есть у QA-стенда, `#modal-root`/`#tooltip-root`/`#popover-root` — типовые имена). Возможно, стоит сделать без дефолтов и требовать явный массив? Решение: оставить дефолты, чтобы не ломать существующих юзеров.
182
- 2. **#2**: нужен ли `waitForSelector` отдельным параметром или интегрировать в существующий `waitForElement`-pattern? Решение: отдельный параметр для атомарности (одна MCP-вызов вместо двух).
183
- 3. **#5**: авто-IIFE может сломать скрипты, где `return` намеренно внутри функции. Митигируем regex'ом который ищет именно top-level (не внутри `function (){ return }`). Если окажется хрупким — откатимся к документации.
184
-
185
- ## Verification (end-to-end на QA-стенде)
186
-
187
- После реализации QA повторяет сценарий из отчёта начиная с шага 1 плана продолжения. Успех = пройти до шага 7 (загрузка ZIP) без воркараундов через `executeScript`.