chrometools-mcp 3.5.4 → 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.
- package/CHANGELOG.md +25 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -1217
- package/README.md +13 -4
- package/SPEC-swagger-api-tools.md +3101 -3101
- package/browser/page-manager.js +7 -0
- package/index.js +199 -14
- package/models/DATEPICKER_IMPLEMENTATION.md +543 -543
- package/models/ModelRegistry.js +115 -115
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +56 -1
- package/server/tool-definitions.js +10 -5
- package/server/tool-schemas.js +10 -5
- package/specs/SEGM-537-UNBLOCKERS_PROGRESS.md +94 -0
- package/specs/SEGM-537-UNBLOCKERS_SPEC.md +187 -0
- package/utils/actions/click-action.js +76 -8
- package/SPEC-IMPROVEMENTS.md +0 -173
- package/SPEC-pom-integration.md +0 -227
|
@@ -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
|
|
@@ -40,14 +44,16 @@ export async function executeClickAction(page, element, options = {}) {
|
|
|
40
44
|
// This decides the click path — Puppeteer's click() doesn't always throw on interception.
|
|
41
45
|
//
|
|
42
46
|
// Path A (element covered by another, e.g. small button under <a routerLink>):
|
|
43
|
-
// →
|
|
47
|
+
// → Full DOM event sequence — bypasses coordinate hit-testing, fires pointer+mouse+click
|
|
44
48
|
//
|
|
45
49
|
// Path B (element is topmost — normal case):
|
|
46
50
|
// Tier 1: Puppeteer native click (trusted CDP events)
|
|
47
51
|
// Tier 2: page.mouse.click at coordinates (trusted CDP, no interception check)
|
|
48
|
-
// Tier 3:
|
|
52
|
+
// Tier 3: Full DOM event sequence (untrusted, last resort)
|
|
49
53
|
// Trusted CDP events (Tier 1 & 2) are critical for Angular/Zone.js apps where
|
|
50
54
|
// untrusted .click() triggers change detection mid-dispatch, destroying *ngFor elements.
|
|
55
|
+
let clickMethod = 'unknown';
|
|
56
|
+
|
|
51
57
|
const clickWithTimeout = async (timeoutMs = 5000) => {
|
|
52
58
|
const withTimeout = (promise) => Promise.race([
|
|
53
59
|
promise,
|
|
@@ -67,8 +73,22 @@ export async function executeClickAction(page, element, options = {}) {
|
|
|
67
73
|
|
|
68
74
|
if (intercepted) {
|
|
69
75
|
// Path A: Element is covered (e.g., small button under <a routerLink>)
|
|
70
|
-
// Coordinate clicks would hit the covering element —
|
|
71
|
-
|
|
76
|
+
// Coordinate clicks would hit the covering element — dispatch full event sequence
|
|
77
|
+
// directly on the element to bypass hit-testing while still triggering framework handlers
|
|
78
|
+
clickMethod = 'dom-sequence-intercepted';
|
|
79
|
+
await element.evaluate(el => {
|
|
80
|
+
const rect = el.getBoundingClientRect();
|
|
81
|
+
const x = rect.left + rect.width / 2;
|
|
82
|
+
const y = rect.top + rect.height / 2;
|
|
83
|
+
const common = { bubbles: true, cancelable: true, clientX: x, clientY: y, view: window };
|
|
84
|
+
const pOpts = { ...common, pointerId: 1, pointerType: 'mouse', isPrimary: true, width: 1, height: 1 };
|
|
85
|
+
el.dispatchEvent(new PointerEvent('pointerdown', pOpts));
|
|
86
|
+
el.dispatchEvent(new MouseEvent('mousedown', common));
|
|
87
|
+
if (el.focus) el.focus();
|
|
88
|
+
el.dispatchEvent(new PointerEvent('pointerup', pOpts));
|
|
89
|
+
el.dispatchEvent(new MouseEvent('mouseup', common));
|
|
90
|
+
el.dispatchEvent(new MouseEvent('click', common));
|
|
91
|
+
});
|
|
72
92
|
return;
|
|
73
93
|
}
|
|
74
94
|
|
|
@@ -76,6 +96,7 @@ export async function executeClickAction(page, element, options = {}) {
|
|
|
76
96
|
// Tier 1: Puppeteer native click
|
|
77
97
|
try {
|
|
78
98
|
await withTimeout(element.click());
|
|
99
|
+
clickMethod = 'puppeteer-native';
|
|
79
100
|
return;
|
|
80
101
|
} catch (e) { /* fall through to Tier 2 */ }
|
|
81
102
|
|
|
@@ -84,16 +105,51 @@ export async function executeClickAction(page, element, options = {}) {
|
|
|
84
105
|
const box = await element.boundingBox();
|
|
85
106
|
if (box) {
|
|
86
107
|
await withTimeout(page.mouse.click(box.x + box.width / 2, box.y + box.height / 2));
|
|
108
|
+
clickMethod = 'cdp-coordinates';
|
|
87
109
|
return;
|
|
88
110
|
}
|
|
89
111
|
} catch (e) { /* fall through to Tier 3 */ }
|
|
90
112
|
|
|
91
|
-
// Tier 3:
|
|
92
|
-
|
|
113
|
+
// Tier 3: Full DOM event sequence — untrusted, last resort
|
|
114
|
+
// Dispatches pointer+mouse+click events for compatibility with UI frameworks
|
|
115
|
+
// that rely on the full browser event sequence (e.g., components with pressed states)
|
|
116
|
+
clickMethod = 'dom-sequence-fallback';
|
|
117
|
+
await element.evaluate(el => {
|
|
118
|
+
const rect = el.getBoundingClientRect();
|
|
119
|
+
const x = rect.left + rect.width / 2;
|
|
120
|
+
const y = rect.top + rect.height / 2;
|
|
121
|
+
const common = { bubbles: true, cancelable: true, clientX: x, clientY: y, view: window };
|
|
122
|
+
const pOpts = { ...common, pointerId: 1, pointerType: 'mouse', isPrimary: true, width: 1, height: 1 };
|
|
123
|
+
el.dispatchEvent(new PointerEvent('pointerdown', pOpts));
|
|
124
|
+
el.dispatchEvent(new MouseEvent('mousedown', common));
|
|
125
|
+
if (el.focus) el.focus();
|
|
126
|
+
el.dispatchEvent(new PointerEvent('pointerup', pOpts));
|
|
127
|
+
el.dispatchEvent(new MouseEvent('mouseup', common));
|
|
128
|
+
el.dispatchEvent(new MouseEvent('click', common));
|
|
129
|
+
});
|
|
93
130
|
};
|
|
94
131
|
|
|
95
132
|
await clickWithTimeout();
|
|
96
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
|
+
|
|
97
153
|
// Check if element was detached from DOM during click (Angular *ngFor + Zone.js pattern)
|
|
98
154
|
let elementDetached = false;
|
|
99
155
|
try {
|
|
@@ -174,11 +230,23 @@ export async function executeClickAction(page, element, options = {}) {
|
|
|
174
230
|
hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
|
|
175
231
|
}
|
|
176
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
|
+
|
|
177
242
|
// 4. Add diagnostics to output
|
|
178
243
|
const diagnosticsText = formatDiagnosticsForAI(diagnostics);
|
|
179
244
|
|
|
245
|
+
// Include click method in output for diagnostics (only non-default methods)
|
|
246
|
+
const methodNote = clickMethod !== 'puppeteer-native' ? ` [${clickMethod}]` : '';
|
|
247
|
+
|
|
180
248
|
const content = [
|
|
181
|
-
{ type: "text", text: `Clicked: ${identifier}${hintsText}${diagnosticsText}` }
|
|
249
|
+
{ type: "text", text: `Clicked: ${identifier}${methodNote}${hintsText}${diagnosticsText}` }
|
|
182
250
|
];
|
|
183
251
|
|
|
184
252
|
// Only add screenshot if requested — lightweight JPEG for action confirmation
|
package/SPEC-IMPROVEMENTS.md
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
# TEMP: Spec — Action System Improvements
|
|
2
|
-
|
|
3
|
-
> This is a temporary spec document. Delete after implementation is complete.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
After unifying standalone tools with shared action handlers (commit 9ab791a), the following improvements were identified during comprehensive testing across 5 sites (Google, herokuapp, React TodoMVC, Angular TodoMVC, Construction Tracker).
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. Tab Targeting Fix (Critical)
|
|
12
|
-
|
|
13
|
-
### Problem
|
|
14
|
-
When multiple tabs are open, `analyzePage` can target one tab while `executeModelAction`/`executeScript` target another. This causes "ModelRegistry is not defined" errors because the model registry is injected into a different page.
|
|
15
|
-
|
|
16
|
-
### Root Cause
|
|
17
|
-
`getLastOpenPage()` and the extension bridge can return different page references. There's no single source of truth for "current active page".
|
|
18
|
-
|
|
19
|
-
### Solution
|
|
20
|
-
- Track `lastAnalyzedPageId` after each `analyzePage` call
|
|
21
|
-
- In `executeModelAction`, use the same page that was last analyzed (not `getLastOpenPage()`)
|
|
22
|
-
- Add `pageId` validation: if the page URL changed since last analysis, warn the user
|
|
23
|
-
- Fallback: if no analyzed page exists, use `getLastOpenPage()` as before
|
|
24
|
-
|
|
25
|
-
### Files
|
|
26
|
-
- `index.js` — add page tracking logic in analyzePage and executeModelAction handlers
|
|
27
|
-
|
|
28
|
-
### Acceptance Criteria
|
|
29
|
-
- `executeModelAction` works reliably after `analyzePage` even with multiple tabs open
|
|
30
|
-
- No "ModelRegistry is not defined" errors when tools target the same page
|
|
31
|
-
- Clear error message when page context is stale (navigated away)
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## 2. `pressKey` Tool (High Priority)
|
|
36
|
-
|
|
37
|
-
### Problem
|
|
38
|
-
No tool exists for pressing keyboard keys (Enter, Escape, Tab, Arrow keys, etc.). Current workarounds:
|
|
39
|
-
- Include `\n` in `type()` text — fragile, doesn't work for Escape/Tab
|
|
40
|
-
- `executeScript` with `dispatchEvent(new KeyboardEvent(...))` — untrusted events, Angular/React may ignore
|
|
41
|
-
|
|
42
|
-
### Solution
|
|
43
|
-
New MCP tool: `pressKey`
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
Parameters:
|
|
47
|
-
- key: string (required) — Key name: "Enter", "Escape", "Tab", "ArrowDown", etc.
|
|
48
|
-
- modifiers: string[] (optional) — ["Shift", "Control", "Alt", "Meta"]
|
|
49
|
-
- repeat: number (optional, default: 1) — Number of times to press
|
|
50
|
-
- id: string (optional) — APOM element ID to focus before pressing
|
|
51
|
-
- selector: string (optional) — CSS selector to focus before pressing
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Implementation: `page.keyboard.press(key)` with optional element focus first.
|
|
55
|
-
|
|
56
|
-
### Files
|
|
57
|
-
- `server/tool-schemas.js` — add `PressKeySchema`
|
|
58
|
-
- `server/tool-definitions.js` — add tool definition
|
|
59
|
-
- `index.js` — add handler
|
|
60
|
-
- `utils/actions/pressKey-action.js` — shared handler (for potential executeModelAction use)
|
|
61
|
-
- `utils/actions/index.js` — export
|
|
62
|
-
|
|
63
|
-
### Acceptance Criteria
|
|
64
|
-
- `pressKey({ key: "Enter" })` submits forms in Angular/React
|
|
65
|
-
- `pressKey({ key: "Escape" })` closes modals
|
|
66
|
-
- `pressKey({ key: "Tab" })` moves focus
|
|
67
|
-
- `pressKey({ key: "a", modifiers: ["Control"] })` selects all text
|
|
68
|
-
- Works with APOM ID to focus element first
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## 3. Post-Action Diagnostics for All Actions (Medium Priority)
|
|
73
|
-
|
|
74
|
-
### Problem
|
|
75
|
-
Post-action diagnostics (network requests, console errors, AI hints, navigation detection) only run after `click` and `type`. Other actions (hover, screenshot, selectOption, scrollTo) return bare results without diagnostics.
|
|
76
|
-
|
|
77
|
-
### Current Architecture
|
|
78
|
-
`click-action.js` and `type-action.js` call `runPostClickDiagnostics()` internally. Other handlers don't.
|
|
79
|
-
|
|
80
|
-
### Solution
|
|
81
|
-
Option A (preferred): Add diagnostics at the **standalone tool level** (in `index.js`), not inside action handlers. This way:
|
|
82
|
-
- Action handlers remain pure (return action result only)
|
|
83
|
-
- Standalone tools wrap with diagnostics
|
|
84
|
-
- `executeModelAction` also wraps with diagnostics
|
|
85
|
-
|
|
86
|
-
Option B: Create `withDiagnostics(handler)` wrapper function.
|
|
87
|
-
|
|
88
|
-
### Implementation (Option A)
|
|
89
|
-
1. Extract diagnostics from click/type tools into `utils/post-action-diagnostics.js`
|
|
90
|
-
2. In standalone tool handlers and `executeModelAction`, call diagnostics after action
|
|
91
|
-
3. Merge diagnostics result with action result
|
|
92
|
-
|
|
93
|
-
### Files
|
|
94
|
-
- `utils/post-action-diagnostics.js` — extracted diagnostics logic
|
|
95
|
-
- `index.js` — wrap action calls with diagnostics in standalone tools and executeModelAction
|
|
96
|
-
- `utils/actions/click-action.js` — remove internal diagnostics call
|
|
97
|
-
- `utils/actions/type-action.js` — remove internal diagnostics call
|
|
98
|
-
|
|
99
|
-
### Acceptance Criteria
|
|
100
|
-
- All 7 action types return diagnostics when called via standalone tools
|
|
101
|
-
- All 7 action types return diagnostics when called via `executeModelAction`
|
|
102
|
-
- Diagnostics are optional (can be disabled for performance)
|
|
103
|
-
- No duplicate diagnostics (click/type don't run them twice)
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## 4. Missing Action Handlers (Medium Priority)
|
|
108
|
-
|
|
109
|
-
### 4a. `executeDatePickerAction`
|
|
110
|
-
|
|
111
|
-
DatePicker model is registered with actions `SetDate`, `SetDateTime`, `SetTime`, `clear`, but handler `executeDatePickerAction` is NOT implemented. Calling any DatePicker action via `executeModelAction` will throw "not implemented".
|
|
112
|
-
|
|
113
|
-
**Strategy**: Use the underlying input's native date setting:
|
|
114
|
-
1. Find the input inside the datepicker container
|
|
115
|
-
2. Set value via `element.evaluate()` with framework-specific event dispatch
|
|
116
|
-
3. For Ant Design (`ant-picker`): click to open, type date, press Enter
|
|
117
|
-
4. For native `<input type="date">`: set `.value` directly
|
|
118
|
-
|
|
119
|
-
### 4b. `append` and `clear` for TextArea
|
|
120
|
-
|
|
121
|
-
TextArea model maps `append` and `clear` to `executeTypeAction`, but `executeTypeAction` doesn't handle these action names specially. Need to:
|
|
122
|
-
- `append`: type without clearing (pass `clearFirst: false`)
|
|
123
|
-
- `clear`: clear field without typing (set value to "")
|
|
124
|
-
|
|
125
|
-
### 4c. `setValue` for Range and Color
|
|
126
|
-
|
|
127
|
-
Range and Color models map `setValue` to `executeTypeAction`, which doesn't handle range/color inputs correctly. Need:
|
|
128
|
-
- Range: `element.evaluate(el => { el.value = X; el.dispatchEvent(...) })`
|
|
129
|
-
- Color: `element.evaluate(el => { el.value = '#hex'; el.dispatchEvent(...) })`
|
|
130
|
-
|
|
131
|
-
### Files
|
|
132
|
-
- `utils/actions/datePicker-action.js` — new handler
|
|
133
|
-
- `utils/actions/type-action.js` — handle `append`/`clear` action names
|
|
134
|
-
- `utils/actions/setValue-action.js` — new handler for range/color
|
|
135
|
-
- `utils/actions/index.js` — exports
|
|
136
|
-
- `index.js` — add to actionHandlers map
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
## 5. Page Reference Caching (Low Priority, depends on #1)
|
|
141
|
-
|
|
142
|
-
### Problem
|
|
143
|
-
Every tool call runs `getLastOpenPage()` independently. In multi-tab scenarios, this can return different pages between related operations.
|
|
144
|
-
|
|
145
|
-
### Solution
|
|
146
|
-
Cache the page reference from the last `analyzePage` call. Use it for subsequent operations until:
|
|
147
|
-
- A new `analyzePage` is called
|
|
148
|
-
- A `navigateTo` or `switchTab` is called
|
|
149
|
-
- The page navigates away (detected via CDP event)
|
|
150
|
-
|
|
151
|
-
This is a deeper architectural change that builds on fix #1.
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## Priority Order
|
|
156
|
-
|
|
157
|
-
1. **pressKey tool** — quick win, high impact, no architectural changes
|
|
158
|
-
2. **Tab targeting fix** — critical bug, moderate complexity
|
|
159
|
-
3. **Missing action handlers** (4b: append/clear, 4c: setValue) — quick wins
|
|
160
|
-
4. **Post-action diagnostics unification** — moderate complexity, high polish
|
|
161
|
-
5. **executeDatePickerAction** — complex, framework-specific logic
|
|
162
|
-
6. **Page reference caching** — architectural, depends on #2
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## Testing Plan
|
|
167
|
-
|
|
168
|
-
After each improvement:
|
|
169
|
-
1. Test on Google (textarea, search, autocomplete)
|
|
170
|
-
2. Test on the-internet.herokuapp.com (checkboxes, dropdown, inputs)
|
|
171
|
-
3. Test on React TodoMVC (type + Enter, check, navigation)
|
|
172
|
-
4. Test on Angular TodoMVC (type + Enter, check, navigation)
|
|
173
|
-
5. Test on Construction Tracker (React/Ant Design — datepicker, tabs, forms)
|