chrometools-mcp 3.5.5 → 3.5.6

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.
@@ -0,0 +1,187 @@
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`.
@@ -17,6 +17,8 @@ import { processScreenshot } from '../screenshot-processor.js';
17
17
  * @param {boolean} options.screenshot - Whether to capture screenshot after click
18
18
  * @param {boolean} options.skipNetworkWait - Skip waiting for network requests
19
19
  * @param {number} options.networkWaitTimeout - Network wait timeout in ms
20
+ * @param {string} options.waitForSelector - CSS selector to wait for after click (e.g., dropdown menu opening)
21
+ * @param {number} options.waitTimeoutMs - Timeout for waitForSelector in ms (default: 2000)
20
22
  * @returns {Promise<Object>} Result with content array
21
23
  */
22
24
  export async function executeClickAction(page, element, options = {}) {
@@ -24,7 +26,9 @@ export async function executeClickAction(page, element, options = {}) {
24
26
  identifier = 'element',
25
27
  screenshot = false,
26
28
  skipNetworkWait = false,
27
- networkWaitTimeout = 3000
29
+ networkWaitTimeout = 3000,
30
+ waitForSelector = null,
31
+ waitTimeoutMs = 2000
28
32
  } = options;
29
33
 
30
34
  // Capture timestamp and URL BEFORE click for diagnostics
@@ -127,6 +131,25 @@ export async function executeClickAction(page, element, options = {}) {
127
131
 
128
132
  await clickWithTimeout();
129
133
 
134
+ // Atomic click + wait: if caller asked to wait for a selector to appear after click,
135
+ // do it BEFORE diagnostics. Useful for dropdowns/popups that render into a portal —
136
+ // without atomic wait, the next MCP call may race against the popup closing.
137
+ let waitResult = null;
138
+ if (waitForSelector) {
139
+ const waitStart = Date.now();
140
+ try {
141
+ await page.waitForSelector(waitForSelector, { timeout: waitTimeoutMs, visible: true });
142
+ waitResult = { appeared: true, selector: waitForSelector, appearedInMs: Date.now() - waitStart };
143
+ } catch (e) {
144
+ waitResult = {
145
+ appeared: false,
146
+ selector: waitForSelector,
147
+ timedOutAfterMs: Date.now() - waitStart,
148
+ timeoutMs: waitTimeoutMs
149
+ };
150
+ }
151
+ }
152
+
130
153
  // Check if element was detached from DOM during click (Angular *ngFor + Zone.js pattern)
131
154
  let elementDetached = false;
132
155
  try {
@@ -207,6 +230,15 @@ export async function executeClickAction(page, element, options = {}) {
207
230
  hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
208
231
  }
209
232
 
233
+ // Surface waitForSelector outcome to the caller (success or timeout)
234
+ if (waitResult) {
235
+ if (waitResult.appeared) {
236
+ hintsText += `\nWait: "${waitResult.selector}" appeared in ${waitResult.appearedInMs}ms`;
237
+ } else {
238
+ hintsText += `\n⚠️ WAIT_TIMEOUT: "${waitResult.selector}" did not appear within ${waitResult.timeoutMs}ms after click`;
239
+ }
240
+ }
241
+
210
242
  // 4. Add diagnostics to output
211
243
  const diagnosticsText = formatDiagnosticsForAI(diagnostics);
212
244