chrometools-mcp 2.4.2 → 3.1.2
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 +540 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +494 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/element-finder-utils.js +138 -28
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/figma-tools.js +120 -0
- package/index.js +3347 -2518
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -656
- package/server/tool-groups.js +3 -2
- package/server/tool-schemas.js +367 -296
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
|
@@ -0,0 +1,1756 @@
|
|
|
1
|
+
# Agent Page Object Model (APOM) API - Концепция и спецификация
|
|
2
|
+
|
|
3
|
+
> **ВАЖНО: Терминология**
|
|
4
|
+
>
|
|
5
|
+
> В chrometools-mcp существуют **два разных** инструмента с похожими названиями, но **разным назначением**:
|
|
6
|
+
>
|
|
7
|
+
> 1. **Test Page Object (generatePageObject)** - существующий инструмент
|
|
8
|
+
> - **Назначение**: Экспорт структуры страницы в автотесты (Playwright/Selenium)
|
|
9
|
+
> - **Для кого**: QA-инженеры, разработчики автотестов
|
|
10
|
+
> - **Формат**: Генерация кода классов для test frameworks
|
|
11
|
+
> - **Файл**: `recorder/page-object-generator.js`
|
|
12
|
+
> - **Сохраняется**: ✅ Остаётся без изменений
|
|
13
|
+
>
|
|
14
|
+
> 2. **Agent Page Object Model (APOM)** - новый API (эта спецификация)
|
|
15
|
+
> - **Назначение**: Промежуточная модель коммуникации AI-агента с MCP chrometools
|
|
16
|
+
> - **Для кого**: AI-агенты (Claude, ChatGPT, etc.)
|
|
17
|
+
> - **Формат**: JSON-модель страницы с element IDs и actions
|
|
18
|
+
> - **Цель**: Упростить взаимодействие агента с браузером через объектную модель
|
|
19
|
+
> - **Новые инструменты**: `getPageObject`, `performAction`, `updatePageObject`, `queryElements`
|
|
20
|
+
>
|
|
21
|
+
> **Терминология в этом документе:**
|
|
22
|
+
> - **APOM (Agent Page Object Model)** = модель для AI-агентов
|
|
23
|
+
> - **Test Page Object** = существующий инструмент для автотестов
|
|
24
|
+
|
|
25
|
+
## Содержание
|
|
26
|
+
|
|
27
|
+
1. [Общая концепция](#общая-концепция)
|
|
28
|
+
2. [Архитектура решения](#архитектура-решения)
|
|
29
|
+
3. [Модели элементов](#модели-элементов)
|
|
30
|
+
4. [API инструментов](#api-инструментов)
|
|
31
|
+
5. [План разработки](#план-разработки)
|
|
32
|
+
6. [Примеры использования](#примеры-использования)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Общая концепция
|
|
37
|
+
|
|
38
|
+
### Проблема
|
|
39
|
+
|
|
40
|
+
Текущая архитектура chrometools-mcp требует от AI агента работать с низкоуровневыми селекторами и отдельными командами для каждого действия. Это приводит к:
|
|
41
|
+
|
|
42
|
+
1. **Множественным запросам**: для получения информации о странице и последующих действий
|
|
43
|
+
2. **Потере контекста**: между вызовами инструментов элементы могут измениться
|
|
44
|
+
3. **Ограниченной семантике**: агент не знает, какие операции доступны для конкретного элемента
|
|
45
|
+
4. **Избыточным токенам**: получение HTML для простых операций
|
|
46
|
+
|
|
47
|
+
### Решение: Agent Page Object Model (APOM) API
|
|
48
|
+
|
|
49
|
+
Предлагается создать объектную модель для AI-агентов, где:
|
|
50
|
+
|
|
51
|
+
1. **Один инструмент возвращает полную модель страницы** с уникальными идентификаторами элементов
|
|
52
|
+
2. **Каждый элемент содержит метаданные** о доступных операциях (actions)
|
|
53
|
+
3. **Последующие команды работают с ID элементов**, а не с селекторами
|
|
54
|
+
4. **Модели типизированы** в зависимости от типа элемента (input, form, button, link и т.д.)
|
|
55
|
+
5. **ID элементов валидны в течение сессии** страницы (до перезагрузки/навигации)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Архитектура решения
|
|
60
|
+
|
|
61
|
+
### 1. Инструменты верхнего уровня
|
|
62
|
+
|
|
63
|
+
> **🔄 ВАЖНОЕ АРХИТЕКТУРНОЕ РЕШЕНИЕ (v3.0.0)**
|
|
64
|
+
>
|
|
65
|
+
> **analyzePage → getPageObject (переименование + расширение)**
|
|
66
|
+
>
|
|
67
|
+
> В v3.0.0 существующий инструмент `analyzePage` будет переименован и расширен:
|
|
68
|
+
> - **Старое название:** `analyzePage` (v1.0 - v2.x)
|
|
69
|
+
> - **Новое название:** `getPageObject` (v3.0.0+)
|
|
70
|
+
>
|
|
71
|
+
> **Что изменится:**
|
|
72
|
+
> - ✅ Автоматическая генерация уникальных ID для каждого элемента
|
|
73
|
+
> - ✅ Автоматическая регистрация элементов в реестре (использует `utils/selector-resolver.js`)
|
|
74
|
+
> - ✅ Возвращает `pageId` для валидации
|
|
75
|
+
> - ✅ Группировка элементов по типу/секциям
|
|
76
|
+
> - ✅ Сохранение всех существующих возможностей `analyzePage`
|
|
77
|
+
>
|
|
78
|
+
> **Обратная совместимость:**
|
|
79
|
+
> - Параметр `legacy: true` вернёт старый формат без ID (для миграции)
|
|
80
|
+
> - `analyzePage` будет работать как алиас для `getPageObject({ legacy: true })`
|
|
81
|
+
>
|
|
82
|
+
> **Миграция:**
|
|
83
|
+
> ```javascript
|
|
84
|
+
> // Старый код (v2.x):
|
|
85
|
+
> const analysis = analyzePage()
|
|
86
|
+
> click({ selector: "#email" })
|
|
87
|
+
>
|
|
88
|
+
> // Новый код (v3.0.0):
|
|
89
|
+
> const page = getPageObject()
|
|
90
|
+
> click({ selector: "input_email_0" }) // или "#email" (backward compatible)
|
|
91
|
+
> ```
|
|
92
|
+
|
|
93
|
+
#### `getPageObject` - получение объектной модели страницы (APOM)
|
|
94
|
+
|
|
95
|
+
**Статус:** 🚧 Планируется в v3.0.0 (переименование + расширение `analyzePage`)
|
|
96
|
+
|
|
97
|
+
**Назначение:** Главный инструмент APOM API — получение полной модели страницы с автоматической генерацией ID и регистрацией элементов
|
|
98
|
+
|
|
99
|
+
**Параметры:**
|
|
100
|
+
```typescript
|
|
101
|
+
{
|
|
102
|
+
// Существующие параметры из analyzePage:
|
|
103
|
+
includeAll?: boolean, // Включить все видимые элементы (default: false)
|
|
104
|
+
|
|
105
|
+
// Новые параметры APOM:
|
|
106
|
+
refresh?: boolean, // Пересчитать модель (default: false)
|
|
107
|
+
generateIds?: boolean, // Генерировать уникальные ID (default: true в v3.0.0)
|
|
108
|
+
registerElements?: boolean, // Автоматически регистрировать элементы (default: true)
|
|
109
|
+
groupBy?: 'type' | 'section' | 'flat', // Группировка элементов (default: 'type')
|
|
110
|
+
maxElements?: number, // Лимит элементов (default: 200)
|
|
111
|
+
|
|
112
|
+
// Обратная совместимость:
|
|
113
|
+
legacy?: boolean // Вернуть старый формат analyzePage (default: false)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Возвращает:**
|
|
118
|
+
```typescript
|
|
119
|
+
{
|
|
120
|
+
pageId: string, // Уникальный ID страницы (для валидации)
|
|
121
|
+
url: string,
|
|
122
|
+
title: string,
|
|
123
|
+
timestamp: number, // Когда создана модель
|
|
124
|
+
|
|
125
|
+
elements: {
|
|
126
|
+
[elementId: string]: PageElement // Карта элементов
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Группировки для удобства
|
|
130
|
+
groups: {
|
|
131
|
+
inputs?: ElementGroup,
|
|
132
|
+
buttons?: ElementGroup,
|
|
133
|
+
links?: ElementGroup,
|
|
134
|
+
forms?: FormGroup[],
|
|
135
|
+
sections?: SectionGroup[]
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
metadata: {
|
|
139
|
+
totalElements: number,
|
|
140
|
+
interactiveCount: number,
|
|
141
|
+
formCount: number
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### `performAction` - выполнение действия над элементом 🚧 **НЕ РЕАЛИЗОВАН**
|
|
147
|
+
|
|
148
|
+
**Параметры:**
|
|
149
|
+
```typescript
|
|
150
|
+
{
|
|
151
|
+
pageId: string, // Валидация что страница не изменилась
|
|
152
|
+
elementId: string, // ID элемента из getPageObject
|
|
153
|
+
action: ActionType, // Тип действия (зависит от элемента)
|
|
154
|
+
params?: ActionParams, // Параметры действия
|
|
155
|
+
screenshot?: boolean // Сделать скриншот после действия
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Примеры actions:**
|
|
160
|
+
```typescript
|
|
161
|
+
// Для input элемента
|
|
162
|
+
{ action: 'type', params: { text: 'username' } }
|
|
163
|
+
{ action: 'clear' }
|
|
164
|
+
{ action: 'focus' }
|
|
165
|
+
|
|
166
|
+
// Для button
|
|
167
|
+
{ action: 'click' }
|
|
168
|
+
{ action: 'hover' }
|
|
169
|
+
|
|
170
|
+
// Для любого элемента
|
|
171
|
+
{ action: 'setStyles', params: { styles: [{name: 'color', value: 'red'}] } }
|
|
172
|
+
{ action: 'scrollTo' }
|
|
173
|
+
|
|
174
|
+
// Для select
|
|
175
|
+
{ action: 'selectOption', params: { value: 'option1' } }
|
|
176
|
+
|
|
177
|
+
// Для form
|
|
178
|
+
{ action: 'submit' }
|
|
179
|
+
{ action: 'fillForm', params: { fields: {...} } }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `updatePageObject` - обновление части модели 🚧 **НЕ РЕАЛИЗОВАН**
|
|
183
|
+
|
|
184
|
+
**Параметры:**
|
|
185
|
+
```typescript
|
|
186
|
+
{
|
|
187
|
+
pageId: string,
|
|
188
|
+
elementIds?: string[], // Обновить только эти элементы (или все)
|
|
189
|
+
includeNew?: boolean // Добавить новые элементы (default: true)
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Использование:** после динамических изменений на странице (AJAX, React re-renders)
|
|
194
|
+
|
|
195
|
+
#### `queryElements` - поиск элементов в модели 🚧 **НЕ РЕАЛИЗОВАН**
|
|
196
|
+
|
|
197
|
+
**Параметры:**
|
|
198
|
+
```typescript
|
|
199
|
+
{
|
|
200
|
+
pageId: string,
|
|
201
|
+
query: {
|
|
202
|
+
type?: ElementType | ElementType[],
|
|
203
|
+
text?: string, // Поиск по тексту (substring, case-insensitive)
|
|
204
|
+
attributes?: Record<string, string>, // Поиск по атрибутам
|
|
205
|
+
visible?: boolean,
|
|
206
|
+
inForm?: boolean,
|
|
207
|
+
parentId?: string // Дочерние элементы
|
|
208
|
+
},
|
|
209
|
+
limit?: number // default: 20
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Возвращает:** массив `ElementId[]`
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### 2. Модели элементов
|
|
218
|
+
|
|
219
|
+
Базовая модель для всех элементов:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface PageElement {
|
|
223
|
+
id: string, // Уникальный ID в рамках модели
|
|
224
|
+
type: ElementType, // Тип элемента
|
|
225
|
+
selector: string, // CSS селектор для Puppeteer
|
|
226
|
+
|
|
227
|
+
// Метаданные
|
|
228
|
+
tagName: string,
|
|
229
|
+
text?: string,
|
|
230
|
+
visible: boolean,
|
|
231
|
+
enabled: boolean,
|
|
232
|
+
|
|
233
|
+
// Геометрия
|
|
234
|
+
bounds?: {
|
|
235
|
+
x: number,
|
|
236
|
+
y: number,
|
|
237
|
+
width: number,
|
|
238
|
+
height: number
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Атрибуты
|
|
242
|
+
attributes: Record<string, string>,
|
|
243
|
+
|
|
244
|
+
// Доступные действия
|
|
245
|
+
actions: Action[],
|
|
246
|
+
|
|
247
|
+
// Стили (опционально при includeStyles: true)
|
|
248
|
+
styles?: Record<string, string>,
|
|
249
|
+
|
|
250
|
+
// Родитель/дети
|
|
251
|
+
parentId?: string,
|
|
252
|
+
childIds?: string[]
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### 2.1. Input элементы
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
interface InputElement extends PageElement {
|
|
260
|
+
type: 'input',
|
|
261
|
+
|
|
262
|
+
inputType: 'text' | 'password' | 'email' | 'number' | 'tel' | 'search' | 'url' | 'date' | ...,
|
|
263
|
+
value: string,
|
|
264
|
+
placeholder?: string,
|
|
265
|
+
required: boolean,
|
|
266
|
+
disabled: boolean,
|
|
267
|
+
readonly: boolean,
|
|
268
|
+
maxLength?: number,
|
|
269
|
+
pattern?: string,
|
|
270
|
+
|
|
271
|
+
// Валидация
|
|
272
|
+
validation: {
|
|
273
|
+
required: boolean,
|
|
274
|
+
pattern?: string,
|
|
275
|
+
minLength?: number,
|
|
276
|
+
maxLength?: number,
|
|
277
|
+
min?: number, // для type=number/date
|
|
278
|
+
max?: number,
|
|
279
|
+
step?: number
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// Состояние валидации
|
|
283
|
+
validationState?: {
|
|
284
|
+
valid: boolean,
|
|
285
|
+
message?: string
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Доступные действия
|
|
289
|
+
actions: [
|
|
290
|
+
{ type: 'type', params: { text: string, delay?: number, clearFirst?: boolean } },
|
|
291
|
+
{ type: 'clear' },
|
|
292
|
+
{ type: 'focus' },
|
|
293
|
+
{ type: 'blur' },
|
|
294
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
295
|
+
{ type: 'scrollTo' }
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### 2.2. Textarea элементы
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
interface TextareaElement extends PageElement {
|
|
304
|
+
type: 'textarea',
|
|
305
|
+
|
|
306
|
+
value: string,
|
|
307
|
+
placeholder?: string,
|
|
308
|
+
required: boolean,
|
|
309
|
+
disabled: boolean,
|
|
310
|
+
readonly: boolean,
|
|
311
|
+
maxLength?: number,
|
|
312
|
+
rows?: number,
|
|
313
|
+
cols?: number,
|
|
314
|
+
|
|
315
|
+
validation: {
|
|
316
|
+
required: boolean,
|
|
317
|
+
minLength?: number,
|
|
318
|
+
maxLength?: number
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
actions: [
|
|
322
|
+
{ type: 'type', params: { text: string, clearFirst?: boolean } },
|
|
323
|
+
{ type: 'clear' },
|
|
324
|
+
{ type: 'focus' },
|
|
325
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
326
|
+
{ type: 'scrollTo' }
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### 2.3. Select элементы
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
interface SelectElement extends PageElement {
|
|
335
|
+
type: 'select',
|
|
336
|
+
|
|
337
|
+
// Базовые свойства
|
|
338
|
+
multiple: boolean, // Множественный выбор
|
|
339
|
+
required: boolean, // Обязательное поле
|
|
340
|
+
disabled: boolean, // Элемент заблокирован
|
|
341
|
+
readonly: boolean, // Только для чтения (если поддерживается)
|
|
342
|
+
size?: number, // Количество видимых опций (для multiple)
|
|
343
|
+
|
|
344
|
+
// Опции
|
|
345
|
+
options: Array<{
|
|
346
|
+
value: string, // Значение опции (атрибут value)
|
|
347
|
+
text: string, // Отображаемый текст
|
|
348
|
+
index: number, // Индекс опции (0-based)
|
|
349
|
+
selected: boolean, // Выбрана ли опция
|
|
350
|
+
disabled: boolean, // Заблокирована ли опция
|
|
351
|
+
group?: string, // Название группы (optgroup label), если есть
|
|
352
|
+
groupIndex?: number // Индекс в группе (для optgroup)
|
|
353
|
+
}>,
|
|
354
|
+
|
|
355
|
+
// Текущий выбор
|
|
356
|
+
selectedValues: string[], // Массив выбранных значений (value)
|
|
357
|
+
selectedTexts: string[], // Массив выбранных текстов (для отображения)
|
|
358
|
+
selectedIndices: number[], // Массив индексов выбранных опций
|
|
359
|
+
selectedValue: string | null, // Первое выбранное значение (для удобства)
|
|
360
|
+
selectedText: string | null, // Первый выбранный текст (для удобства)
|
|
361
|
+
selectedIndex: number, // Первый выбранный индекс (-1 если ничего не выбрано)
|
|
362
|
+
|
|
363
|
+
// Группировка (optgroup)
|
|
364
|
+
hasGroups: boolean, // Есть ли группы (optgroup)
|
|
365
|
+
groups?: Array<{
|
|
366
|
+
label: string, // Название группы
|
|
367
|
+
disabled: boolean, // Группа заблокирована
|
|
368
|
+
optionIndices: number[] // Индексы опций в этой группе
|
|
369
|
+
}>,
|
|
370
|
+
|
|
371
|
+
// UI Framework информация (v2.6.0+)
|
|
372
|
+
uiFramework?: {
|
|
373
|
+
name: string, // 'mui' | 'antd' | 'chakra' | 'bootstrap' | 'vuetify' | 'semantic' | null
|
|
374
|
+
version?: string, // Версия библиотеки (если определена)
|
|
375
|
+
component?: string, // Название компонента ('Select', 'Dropdown', etc.)
|
|
376
|
+
customDropdown: boolean, // true если кастомный dropdown (не нативный <select>)
|
|
377
|
+
expanded?: boolean, // Развернут ли dropdown (если кастомный)
|
|
378
|
+
searchable?: boolean // Поддерживает ли поиск (если кастомный)
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
// Валидация
|
|
382
|
+
validation: {
|
|
383
|
+
required: boolean,
|
|
384
|
+
customValidity?: string // Кастомное сообщение валидации
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
// Метаданные
|
|
388
|
+
name?: string, // Атрибут name
|
|
389
|
+
form?: string, // ID формы (атрибут form)
|
|
390
|
+
autocomplete?: string, // Атрибут autocomplete
|
|
391
|
+
|
|
392
|
+
// Доступные действия
|
|
393
|
+
actions: [
|
|
394
|
+
{ type: 'selectOption', params: { value?: string, text?: string, index?: number } },
|
|
395
|
+
{ type: 'selectMultiple', params: { values: string[] } }, // для multiple
|
|
396
|
+
{ type: 'deselectOption', params: { value?: string, text?: string, index?: number } }, // для multiple
|
|
397
|
+
{ type: 'deselectAll' }, // для multiple
|
|
398
|
+
{ type: 'focus' },
|
|
399
|
+
{ type: 'blur' },
|
|
400
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
401
|
+
{ type: 'scrollTo' }
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Особенности Select элементов:**
|
|
407
|
+
|
|
408
|
+
1. **Нативные vs Кастомные:**
|
|
409
|
+
- Нативные `<select>` элементы всегда возвращают полную информацию об опциях
|
|
410
|
+
- Кастомные dropdown (MUI, Ant Design, etc.) могут иметь `uiFramework.customDropdown: true`
|
|
411
|
+
- Для кастомных dropdown опции могут быть недоступны, если dropdown не раскрыт
|
|
412
|
+
|
|
413
|
+
2. **Группировка опций (optgroup):**
|
|
414
|
+
- Если используется `<optgroup>`, каждая опция получает поле `group` с названием группы
|
|
415
|
+
- Массив `groups` содержит информацию о всех группах
|
|
416
|
+
- `groupIndex` указывает позицию опции внутри группы
|
|
417
|
+
|
|
418
|
+
3. **Множественный выбор (multiple):**
|
|
419
|
+
- `selectedValues`, `selectedTexts`, `selectedIndices` содержат все выбранные элементы
|
|
420
|
+
- Для удобства `selectedValue`, `selectedText`, `selectedIndex` содержат первый выбранный элемент
|
|
421
|
+
|
|
422
|
+
4. **UI Framework Detection (v2.6.0+):**
|
|
423
|
+
- Автоматически определяется используемая UI-библиотека
|
|
424
|
+
- Для MUI Select, Ant Design Select, Chakra Select, etc. заполняется `uiFramework`
|
|
425
|
+
- `customDropdown: true` означает, что элемент не является нативным `<select>`
|
|
426
|
+
|
|
427
|
+
**Примеры:**
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// Нативный <select>
|
|
431
|
+
{
|
|
432
|
+
type: 'select',
|
|
433
|
+
multiple: false,
|
|
434
|
+
options: [
|
|
435
|
+
{ value: 'US', text: 'United States', index: 0, selected: true, disabled: false },
|
|
436
|
+
{ value: 'UK', text: 'United Kingdom', index: 1, selected: false, disabled: false }
|
|
437
|
+
],
|
|
438
|
+
selectedValue: 'US',
|
|
439
|
+
selectedText: 'United States',
|
|
440
|
+
selectedIndex: 0,
|
|
441
|
+
hasGroups: false,
|
|
442
|
+
uiFramework: null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Select с optgroup
|
|
446
|
+
{
|
|
447
|
+
type: 'select',
|
|
448
|
+
options: [
|
|
449
|
+
{ value: 'us', text: 'United States', index: 0, selected: false, group: 'North America', groupIndex: 0 },
|
|
450
|
+
{ value: 'ca', text: 'Canada', index: 1, selected: false, group: 'North America', groupIndex: 1 },
|
|
451
|
+
{ value: 'uk', text: 'United Kingdom', index: 2, selected: true, group: 'Europe', groupIndex: 0 }
|
|
452
|
+
],
|
|
453
|
+
hasGroups: true,
|
|
454
|
+
groups: [
|
|
455
|
+
{ label: 'North America', disabled: false, optionIndices: [0, 1] },
|
|
456
|
+
{ label: 'Europe', disabled: false, optionIndices: [2] }
|
|
457
|
+
],
|
|
458
|
+
selectedValue: 'uk'
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// MUI Select (кастомный)
|
|
462
|
+
{
|
|
463
|
+
type: 'select',
|
|
464
|
+
options: [
|
|
465
|
+
{ value: '1', text: 'Option 1', index: 0, selected: true }
|
|
466
|
+
],
|
|
467
|
+
uiFramework: {
|
|
468
|
+
name: 'mui',
|
|
469
|
+
version: '5.x',
|
|
470
|
+
component: 'Select',
|
|
471
|
+
customDropdown: true,
|
|
472
|
+
expanded: false,
|
|
473
|
+
searchable: false
|
|
474
|
+
},
|
|
475
|
+
selectedValue: '1'
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### 2.4. Button элементы
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
interface ButtonElement extends PageElement {
|
|
483
|
+
type: 'button',
|
|
484
|
+
|
|
485
|
+
buttonType: 'button' | 'submit' | 'reset',
|
|
486
|
+
disabled: boolean,
|
|
487
|
+
|
|
488
|
+
// Контекст
|
|
489
|
+
formId?: string, // ID формы если кнопка внутри формы
|
|
490
|
+
role?: string, // ARIA role
|
|
491
|
+
ariaLabel?: string,
|
|
492
|
+
|
|
493
|
+
actions: [
|
|
494
|
+
{ type: 'click', params: { waitForNavigation?: boolean } },
|
|
495
|
+
{ type: 'hover' },
|
|
496
|
+
{ type: 'focus' },
|
|
497
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
498
|
+
{ type: 'scrollTo' }
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### 2.5. Link элементы
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
interface LinkElement extends PageElement {
|
|
507
|
+
type: 'link',
|
|
508
|
+
|
|
509
|
+
href: string,
|
|
510
|
+
target?: '_blank' | '_self' | '_parent' | '_top',
|
|
511
|
+
download?: string,
|
|
512
|
+
rel?: string,
|
|
513
|
+
|
|
514
|
+
actions: [
|
|
515
|
+
{ type: 'click', params: { waitForNavigation?: boolean } },
|
|
516
|
+
{ type: 'hover' },
|
|
517
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
518
|
+
{ type: 'scrollTo' }
|
|
519
|
+
]
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### 2.6. Form элементы (композитная модель)
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
interface FormElement extends PageElement {
|
|
527
|
+
type: 'form',
|
|
528
|
+
|
|
529
|
+
method: 'GET' | 'POST',
|
|
530
|
+
action?: string,
|
|
531
|
+
enctype?: string,
|
|
532
|
+
|
|
533
|
+
// Поля формы (группированные)
|
|
534
|
+
fields: {
|
|
535
|
+
inputs: Record<string, InputElement>,
|
|
536
|
+
textareas: Record<string, TextareaElement>,
|
|
537
|
+
selects: Record<string, SelectElement>,
|
|
538
|
+
checkboxes: Record<string, CheckboxElement>,
|
|
539
|
+
radios: Record<string, RadioElement>
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
// Кнопки формы
|
|
543
|
+
submitButtons: ButtonElement[],
|
|
544
|
+
resetButtons: ButtonElement[],
|
|
545
|
+
|
|
546
|
+
// Валидация всей формы
|
|
547
|
+
validation: {
|
|
548
|
+
valid: boolean, // Валидна ли форма в целом
|
|
549
|
+
requiredFields: string[], // ID обязательных полей
|
|
550
|
+
invalidFields: string[] // ID невалидных полей
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
actions: [
|
|
554
|
+
{
|
|
555
|
+
type: 'fillForm',
|
|
556
|
+
params: {
|
|
557
|
+
fields: Record<fieldId, string>, // Заполнить несколько полей
|
|
558
|
+
submit?: boolean // Автоматически отправить
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
{ type: 'submit', params: { waitForNavigation?: boolean } },
|
|
562
|
+
{ type: 'reset' },
|
|
563
|
+
{ type: 'validateForm' }, // Запустить HTML5 валидацию
|
|
564
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
565
|
+
{ type: 'scrollTo' }
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
#### 2.7. Checkbox/Radio элементы
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
interface CheckboxElement extends PageElement {
|
|
574
|
+
type: 'checkbox',
|
|
575
|
+
|
|
576
|
+
checked: boolean,
|
|
577
|
+
required: boolean,
|
|
578
|
+
disabled: boolean,
|
|
579
|
+
value: string,
|
|
580
|
+
|
|
581
|
+
// Для связанных радио
|
|
582
|
+
name?: string, // Группа радио-кнопок
|
|
583
|
+
|
|
584
|
+
actions: [
|
|
585
|
+
{ type: 'toggle' }, // Переключить
|
|
586
|
+
{ type: 'check' }, // Установить checked
|
|
587
|
+
{ type: 'uncheck' }, // Снять checked
|
|
588
|
+
{ type: 'click' },
|
|
589
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
590
|
+
{ type: 'scrollTo' }
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
interface RadioElement extends PageElement {
|
|
595
|
+
type: 'radio',
|
|
596
|
+
|
|
597
|
+
checked: boolean,
|
|
598
|
+
required: boolean,
|
|
599
|
+
disabled: boolean,
|
|
600
|
+
value: string,
|
|
601
|
+
name: string, // Группа радио
|
|
602
|
+
|
|
603
|
+
// Другие опции в группе
|
|
604
|
+
groupOptions: Array<{
|
|
605
|
+
elementId: string,
|
|
606
|
+
value: string,
|
|
607
|
+
text?: string,
|
|
608
|
+
checked: boolean
|
|
609
|
+
}>,
|
|
610
|
+
|
|
611
|
+
actions: [
|
|
612
|
+
{ type: 'select' }, // Выбрать эту опцию
|
|
613
|
+
{ type: 'click' },
|
|
614
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
615
|
+
{ type: 'scrollTo' }
|
|
616
|
+
]
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
#### 2.8. Generic/Non-interactive элементы
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
interface GenericElement extends PageElement {
|
|
624
|
+
type: 'generic',
|
|
625
|
+
|
|
626
|
+
role?: string, // ARIA role
|
|
627
|
+
ariaLabel?: string,
|
|
628
|
+
|
|
629
|
+
// Интерактивность
|
|
630
|
+
clickable: boolean, // Имеет onclick или cursor:pointer
|
|
631
|
+
hoverable: boolean, // Имеет hover эффекты
|
|
632
|
+
|
|
633
|
+
actions: [
|
|
634
|
+
{ type: 'click' }?, // Только если clickable
|
|
635
|
+
{ type: 'hover' }?, // Только если hoverable
|
|
636
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
637
|
+
{ type: 'scrollTo' },
|
|
638
|
+
{ type: 'getComputedCss', params: { category?: string } },
|
|
639
|
+
{ type: 'getBoxModel' }
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
#### 2.9. Section элементы (группировка)
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
interface SectionElement extends PageElement {
|
|
648
|
+
type: 'section',
|
|
649
|
+
|
|
650
|
+
sectionType: 'header' | 'nav' | 'main' | 'article' | 'aside' | 'footer' | 'form' | 'div',
|
|
651
|
+
role?: string,
|
|
652
|
+
|
|
653
|
+
// Дочерние элементы
|
|
654
|
+
childIds: string[],
|
|
655
|
+
|
|
656
|
+
// Семантика
|
|
657
|
+
label?: string, // aria-label или heading внутри
|
|
658
|
+
landmark?: string, // ARIA landmark role
|
|
659
|
+
|
|
660
|
+
actions: [
|
|
661
|
+
{ type: 'setStyles', params: { styles: StylePair[] } },
|
|
662
|
+
{ type: 'scrollTo' }
|
|
663
|
+
]
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
### 3. Генерация ID элементов
|
|
670
|
+
|
|
671
|
+
#### Стратегия присвоения ID:
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
function generateElementId(element: Element, index: number): string {
|
|
675
|
+
// Приоритет:
|
|
676
|
+
// 1. data-testid
|
|
677
|
+
if (element.dataset.testid) {
|
|
678
|
+
return `testid:${element.dataset.testid}`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 2. id атрибут
|
|
682
|
+
if (element.id) {
|
|
683
|
+
return `id:${element.id}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 3. Семантический путь + тип + индекс
|
|
687
|
+
const path = getSemanticPath(element);
|
|
688
|
+
const type = getElementType(element);
|
|
689
|
+
return `${type}:${path}:${index}`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Примеры ID:
|
|
693
|
+
// - testid:login-button
|
|
694
|
+
// - id:email-input
|
|
695
|
+
// - input:form[name="login"]:0
|
|
696
|
+
// - button:header>nav:2
|
|
697
|
+
// - link:footer:5
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
#### Валидация ID
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
interface ElementIdValidator {
|
|
704
|
+
pageId: string, // Берется из page.url() + timestamp
|
|
705
|
+
elementIds: Set<string>, // Валидные ID в рамках модели
|
|
706
|
+
|
|
707
|
+
validate(elementId: string): boolean {
|
|
708
|
+
return this.elementIds.has(elementId);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
При выполнении действия проверяется:
|
|
714
|
+
1. `pageId` совпадает (страница не перезагружалась)
|
|
715
|
+
2. `elementId` существует в модели
|
|
716
|
+
|
|
717
|
+
Если валидация не прошла - возвращается ошибка с предложением вызвать `updatePageObject` или `getPageObject`.
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
### 4. Группировка элементов
|
|
722
|
+
|
|
723
|
+
#### По типу (default: `groupBy: 'type'`)
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
{
|
|
727
|
+
groups: {
|
|
728
|
+
inputs: {
|
|
729
|
+
count: 5,
|
|
730
|
+
elementIds: ['input:form[0]:0', 'input:form[0]:1', ...]
|
|
731
|
+
},
|
|
732
|
+
buttons: { count: 3, elementIds: [...] },
|
|
733
|
+
links: { count: 10, elementIds: [...] },
|
|
734
|
+
forms: [
|
|
735
|
+
{
|
|
736
|
+
formId: 'form:0',
|
|
737
|
+
fields: { inputs: [...], selects: [...] },
|
|
738
|
+
submitButtons: [...]
|
|
739
|
+
}
|
|
740
|
+
]
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
#### По секциям (`groupBy: 'section'`)
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
{
|
|
749
|
+
groups: {
|
|
750
|
+
sections: [
|
|
751
|
+
{
|
|
752
|
+
sectionId: 'section:header',
|
|
753
|
+
label: 'Site Header',
|
|
754
|
+
childIds: ['link:header:0', 'button:header:0', ...]
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
sectionId: 'section:main>form',
|
|
758
|
+
label: 'Login Form',
|
|
759
|
+
childIds: ['input:form[0]:0', 'input:form[0]:1', 'button:form[0]:0']
|
|
760
|
+
}
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
#### Плоская (`groupBy: 'flat'`)
|
|
767
|
+
|
|
768
|
+
Просто карта `elements: { [id]: element }` без группировки.
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## API инструментов
|
|
773
|
+
|
|
774
|
+
### Полная спецификация инструментов
|
|
775
|
+
|
|
776
|
+
#### 1. `getPageObject`
|
|
777
|
+
|
|
778
|
+
**Описание:** Получить объектную модель текущей страницы
|
|
779
|
+
|
|
780
|
+
**Параметры:**
|
|
781
|
+
```typescript
|
|
782
|
+
{
|
|
783
|
+
refresh?: boolean, // Пересчитать модель (default: false)
|
|
784
|
+
includeNonInteractive?: boolean, // Включить статичные элементы (default: false)
|
|
785
|
+
includeStyles?: boolean, // Включить computed styles для каждого элемента (default: false)
|
|
786
|
+
groupBy?: 'type' | 'section' | 'flat', // Группировка элементов (default: 'type')
|
|
787
|
+
maxElements?: number // Лимит элементов (default: 200)
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Возвращает:** `PageObjectModel` (см. раздел 2)
|
|
792
|
+
|
|
793
|
+
**Кэширование:** результат кэшируется по URL до `refresh: true` или навигации
|
|
794
|
+
|
|
795
|
+
**Примеры использования:**
|
|
796
|
+
```javascript
|
|
797
|
+
// Базовое использование
|
|
798
|
+
const page = await getPageObject();
|
|
799
|
+
console.log(page.groups.forms); // Все формы
|
|
800
|
+
|
|
801
|
+
// С дополнительными элементами
|
|
802
|
+
const page = await getPageObject({
|
|
803
|
+
includeNonInteractive: true,
|
|
804
|
+
groupBy: 'section'
|
|
805
|
+
});
|
|
806
|
+
console.log(page.groups.sections); // Элементы по секциям страницы
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
#### 2. `performAction`
|
|
812
|
+
|
|
813
|
+
**Описание:** Выполнить действие над элементом по его ID
|
|
814
|
+
|
|
815
|
+
**Параметры:**
|
|
816
|
+
```typescript
|
|
817
|
+
{
|
|
818
|
+
pageId: string, // ID страницы из getPageObject
|
|
819
|
+
elementId: string, // ID элемента из getPageObject
|
|
820
|
+
action: ActionType, // Тип действия
|
|
821
|
+
params?: object, // Параметры действия (зависят от типа)
|
|
822
|
+
screenshot?: boolean, // Сделать скриншот после (default: false)
|
|
823
|
+
waitAfter?: number // Ждать N мс после действия (default: 500)
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**Поддерживаемые действия:**
|
|
828
|
+
|
|
829
|
+
| Action Type | Параметры | Применимо к |
|
|
830
|
+
|------------|-----------|-------------|
|
|
831
|
+
| `click` | `{ waitForNavigation?: boolean }` | button, link, generic (clickable) |
|
|
832
|
+
| `type` | `{ text: string, delay?: number, clearFirst?: boolean }` | input, textarea |
|
|
833
|
+
| `clear` | - | input, textarea |
|
|
834
|
+
| `focus` | - | input, textarea, select, button |
|
|
835
|
+
| `blur` | - | input, textarea |
|
|
836
|
+
| `hover` | - | любой видимый элемент |
|
|
837
|
+
| `scrollTo` | `{ behavior?: 'auto' \| 'smooth' }` | любой элемент |
|
|
838
|
+
| `selectOption` | `{ value?: string, text?: string, index?: number }` | select |
|
|
839
|
+
| `toggle` | - | checkbox |
|
|
840
|
+
| `check` | - | checkbox |
|
|
841
|
+
| `uncheck` | - | checkbox |
|
|
842
|
+
| `select` | - | radio |
|
|
843
|
+
| `submit` | `{ waitForNavigation?: boolean }` | form |
|
|
844
|
+
| `reset` | - | form |
|
|
845
|
+
| `fillForm` | `{ fields: Record<fieldId, value>, submit?: boolean }` | form |
|
|
846
|
+
| `validateForm` | - | form |
|
|
847
|
+
| `setStyles` | `{ styles: Array<{name, value}> }` | любой элемент |
|
|
848
|
+
| `getComputedCss` | `{ category?: 'layout'\|'typography'\|... }` | любой элемент |
|
|
849
|
+
| `getBoxModel` | - | любой элемент |
|
|
850
|
+
|
|
851
|
+
**Возвращает:**
|
|
852
|
+
```typescript
|
|
853
|
+
{
|
|
854
|
+
success: boolean,
|
|
855
|
+
message?: string, // Сообщение об ошибке или успехе
|
|
856
|
+
result?: any, // Результат действия (напр. CSS для getComputedCss)
|
|
857
|
+
screenshot?: string, // Base64 PNG если screenshot: true
|
|
858
|
+
pageUpdated?: boolean // true если страница изменилась (навигация, reload)
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
**Примеры:**
|
|
863
|
+
```javascript
|
|
864
|
+
// Заполнить input
|
|
865
|
+
await performAction({
|
|
866
|
+
pageId: page.pageId,
|
|
867
|
+
elementId: 'input:form[0]:0',
|
|
868
|
+
action: 'type',
|
|
869
|
+
params: { text: 'user@example.com' }
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Кликнуть кнопку
|
|
873
|
+
await performAction({
|
|
874
|
+
pageId: page.pageId,
|
|
875
|
+
elementId: 'button:form[0]:0',
|
|
876
|
+
action: 'click',
|
|
877
|
+
params: { waitForNavigation: true },
|
|
878
|
+
screenshot: true
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Заполнить всю форму
|
|
882
|
+
await performAction({
|
|
883
|
+
pageId: page.pageId,
|
|
884
|
+
elementId: 'form:0',
|
|
885
|
+
action: 'fillForm',
|
|
886
|
+
params: {
|
|
887
|
+
fields: {
|
|
888
|
+
'input:form[0]:0': 'user@example.com',
|
|
889
|
+
'input:form[0]:1': 'password123',
|
|
890
|
+
'select:form[0]:0': 'option1'
|
|
891
|
+
},
|
|
892
|
+
submit: true
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
#### 3. `updatePageObject`
|
|
900
|
+
|
|
901
|
+
**Описание:** Обновить модель страницы (после динамических изменений)
|
|
902
|
+
|
|
903
|
+
**Параметры:**
|
|
904
|
+
```typescript
|
|
905
|
+
{
|
|
906
|
+
pageId: string, // ID страницы
|
|
907
|
+
elementIds?: string[], // Обновить только эти элементы (или все)
|
|
908
|
+
includeNew?: boolean, // Добавить новые элементы (default: true)
|
|
909
|
+
removeDeleted?: boolean // Удалить несуществующие элементы (default: true)
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**Возвращает:** обновленный `PageObjectModel`
|
|
914
|
+
|
|
915
|
+
**Использование:** после AJAX запросов, React re-renders, динамических изменений DOM
|
|
916
|
+
|
|
917
|
+
**Пример:**
|
|
918
|
+
```javascript
|
|
919
|
+
// После клика, который загрузил новые элементы
|
|
920
|
+
await performAction({ ... });
|
|
921
|
+
|
|
922
|
+
// Обновить модель
|
|
923
|
+
const updatedPage = await updatePageObject({
|
|
924
|
+
pageId: page.pageId,
|
|
925
|
+
includeNew: true
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
console.log(updatedPage.metadata.totalElements); // Новое количество
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
#### 4. `queryElements`
|
|
934
|
+
|
|
935
|
+
**Описание:** Найти элементы в модели по критериям
|
|
936
|
+
|
|
937
|
+
**Параметры:**
|
|
938
|
+
```typescript
|
|
939
|
+
{
|
|
940
|
+
pageId: string,
|
|
941
|
+
query: {
|
|
942
|
+
type?: ElementType | ElementType[], // Фильтр по типу
|
|
943
|
+
text?: string, // Поиск по тексту (substring)
|
|
944
|
+
attributes?: Record<string, string>, // Фильтр по атрибутам
|
|
945
|
+
visible?: boolean, // Только видимые/невидимые
|
|
946
|
+
enabled?: boolean, // Только enabled
|
|
947
|
+
inForm?: boolean, // Только внутри форм
|
|
948
|
+
parentId?: string, // Дочерние элементы
|
|
949
|
+
hasAction?: ActionType // Элементы с определенным действием
|
|
950
|
+
},
|
|
951
|
+
limit?: number, // default: 20
|
|
952
|
+
offset?: number // Пагинация (default: 0)
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Возвращает:**
|
|
957
|
+
```typescript
|
|
958
|
+
{
|
|
959
|
+
elementIds: string[],
|
|
960
|
+
total: number, // Всего найдено
|
|
961
|
+
hasMore: boolean // Есть еще результаты
|
|
962
|
+
}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
**Примеры:**
|
|
966
|
+
```javascript
|
|
967
|
+
// Найти все кнопки submit
|
|
968
|
+
const { elementIds } = await queryElements({
|
|
969
|
+
pageId: page.pageId,
|
|
970
|
+
query: {
|
|
971
|
+
type: 'button',
|
|
972
|
+
attributes: { type: 'submit' }
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// Найти input с текстом "email"
|
|
977
|
+
const { elementIds } = await queryElements({
|
|
978
|
+
pageId: page.pageId,
|
|
979
|
+
query: {
|
|
980
|
+
type: 'input',
|
|
981
|
+
text: 'email'
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Найти все элементы в форме
|
|
986
|
+
const { elementIds } = await queryElements({
|
|
987
|
+
pageId: page.pageId,
|
|
988
|
+
query: {
|
|
989
|
+
parentId: 'form:0'
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
#### 5. `getElementDetails`
|
|
997
|
+
|
|
998
|
+
**Описание:** Получить детальную информацию об элементе
|
|
999
|
+
|
|
1000
|
+
**Параметры:**
|
|
1001
|
+
```typescript
|
|
1002
|
+
{
|
|
1003
|
+
pageId: string,
|
|
1004
|
+
elementId: string,
|
|
1005
|
+
includeStyles?: boolean, // Включить computed styles (default: false)
|
|
1006
|
+
includeBoxModel?: boolean, // Включить box model (default: false)
|
|
1007
|
+
includeChildren?: boolean // Включить детали дочерних элементов (default: false)
|
|
1008
|
+
}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
**Возвращает:** полный объект `PageElement` с запрошенными деталями
|
|
1012
|
+
|
|
1013
|
+
**Использование:** когда нужна детальная информация об одном элементе (избегаем передачи всей модели)
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## План разработки
|
|
1018
|
+
|
|
1019
|
+
### Фаза 1: Базовая инфраструктура (Week 1)
|
|
1020
|
+
|
|
1021
|
+
#### 1.1. Модуль генерации ID элементов
|
|
1022
|
+
**Файл:** `pom/element-id-generator.js`
|
|
1023
|
+
|
|
1024
|
+
**Задачи:**
|
|
1025
|
+
- [ ] Функция `generateElementId(element, index)` с приоритетом testid > id > semantic path
|
|
1026
|
+
- [ ] Функция `getSemanticPath(element)` для построения пути
|
|
1027
|
+
- [ ] Функция `getElementType(element)` для определения типа
|
|
1028
|
+
- [ ] Unit тесты для генерации ID
|
|
1029
|
+
|
|
1030
|
+
**Зависимости:** нет
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
#### 1.2. Модуль валидации ID
|
|
1035
|
+
**Файл:** `pom/element-id-validator.js`
|
|
1036
|
+
|
|
1037
|
+
**Задачи:**
|
|
1038
|
+
- [ ] Класс `ElementIdValidator` с методами validate/add/remove
|
|
1039
|
+
- [ ] Генерация `pageId` на основе URL + timestamp
|
|
1040
|
+
- [ ] Обработка невалидных ID с рекомендациями
|
|
1041
|
+
- [ ] Unit тесты
|
|
1042
|
+
|
|
1043
|
+
**Зависимости:** 1.1
|
|
1044
|
+
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
#### 1.3. Модуль создания моделей элементов
|
|
1048
|
+
**Файл:** `pom/element-model-factory.js`
|
|
1049
|
+
|
|
1050
|
+
**Задачи:**
|
|
1051
|
+
- [ ] Функции создания моделей для каждого типа элемента:
|
|
1052
|
+
- `createInputElement(element, id, selector)`
|
|
1053
|
+
- `createButtonElement(...)`
|
|
1054
|
+
- `createSelectElement(...)`
|
|
1055
|
+
- `createFormElement(...)`
|
|
1056
|
+
- и т.д. (10+ типов)
|
|
1057
|
+
- [ ] Извлечение метаданных (bounds, attributes, validation)
|
|
1058
|
+
- [ ] Определение доступных actions для каждого типа
|
|
1059
|
+
- [ ] Unit тесты
|
|
1060
|
+
|
|
1061
|
+
**Зависимости:** 1.1
|
|
1062
|
+
|
|
1063
|
+
---
|
|
1064
|
+
|
|
1065
|
+
### Фаза 2: Инструмент getPageObject (Week 2)
|
|
1066
|
+
|
|
1067
|
+
#### 2.1. Сбор элементов со страницы
|
|
1068
|
+
**Файл:** `pom/page-scanner.js`
|
|
1069
|
+
|
|
1070
|
+
**Задачи:**
|
|
1071
|
+
- [ ] Функция `scanPage(page, options)` - обход DOM через page.evaluate()
|
|
1072
|
+
- [ ] Сбор интерактивных элементов (input, button, select, a, textarea, form)
|
|
1073
|
+
- [ ] Опциональный сбор неинтерактивных элементов
|
|
1074
|
+
- [ ] Извлечение атрибутов, bounds, visibility для каждого элемента
|
|
1075
|
+
- [ ] Построение иерархии (parent/child relationships)
|
|
1076
|
+
- [ ] Интеграционные тесты
|
|
1077
|
+
|
|
1078
|
+
**Зависимости:** 1.1, 1.3
|
|
1079
|
+
|
|
1080
|
+
---
|
|
1081
|
+
|
|
1082
|
+
#### 2.2. Группировка элементов
|
|
1083
|
+
**Файл:** `pom/element-grouper.js`
|
|
1084
|
+
|
|
1085
|
+
**Задачи:**
|
|
1086
|
+
- [ ] Группировка по типу (`groupBy: 'type'`)
|
|
1087
|
+
- [ ] Группировка по секциям (`groupBy: 'section'`) - поиск header/nav/main/footer
|
|
1088
|
+
- [ ] Плоская структура (`groupBy: 'flat'`)
|
|
1089
|
+
- [ ] Специальная обработка форм (fields + buttons)
|
|
1090
|
+
- [ ] Unit тесты
|
|
1091
|
+
|
|
1092
|
+
**Зависимости:** 1.3
|
|
1093
|
+
|
|
1094
|
+
---
|
|
1095
|
+
|
|
1096
|
+
#### 2.3. Инструмент getPageObject
|
|
1097
|
+
**Файл:** `pom/tools/get-page-object.js`
|
|
1098
|
+
|
|
1099
|
+
**Задачи:**
|
|
1100
|
+
- [ ] Реализация MCP tool handler
|
|
1101
|
+
- [ ] Интеграция с кэшированием (pageAnalysisCache)
|
|
1102
|
+
- [ ] Поддержка параметров (refresh, includeNonInteractive, groupBy, maxElements)
|
|
1103
|
+
- [ ] Обработка ошибок
|
|
1104
|
+
- [ ] Документация
|
|
1105
|
+
- [ ] Интеграционные тесты
|
|
1106
|
+
|
|
1107
|
+
**Zod схема:**
|
|
1108
|
+
```javascript
|
|
1109
|
+
getPageObject: z.object({
|
|
1110
|
+
refresh: z.boolean().optional(),
|
|
1111
|
+
includeNonInteractive: z.boolean().optional(),
|
|
1112
|
+
includeStyles: z.boolean().optional(),
|
|
1113
|
+
groupBy: z.enum(['type', 'section', 'flat']).optional(),
|
|
1114
|
+
maxElements: z.number().optional()
|
|
1115
|
+
})
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
**Зависимости:** 2.1, 2.2, 1.2
|
|
1119
|
+
|
|
1120
|
+
---
|
|
1121
|
+
|
|
1122
|
+
### Фаза 3: Инструмент performAction (Week 3)
|
|
1123
|
+
|
|
1124
|
+
#### 3.1. Модуль выполнения действий
|
|
1125
|
+
**Файл:** `pom/action-executor.js`
|
|
1126
|
+
|
|
1127
|
+
**Задачи:**
|
|
1128
|
+
- [ ] Функция `executeAction(page, element, action, params)` - роутинг по типу действия
|
|
1129
|
+
- [ ] Реализация всех типов действий:
|
|
1130
|
+
- **click** - через element.click()
|
|
1131
|
+
- **type** - через element.type() с clearFirst
|
|
1132
|
+
- **clear** - через triple-click + backspace
|
|
1133
|
+
- **focus/blur** - через element.focus()
|
|
1134
|
+
- **hover** - через element.hover()
|
|
1135
|
+
- **scrollTo** - через element.scrollIntoView()
|
|
1136
|
+
- **selectOption** - через page.select()
|
|
1137
|
+
- **toggle/check/uncheck** - для checkbox
|
|
1138
|
+
- **select** - для radio (найти по name и кликнуть)
|
|
1139
|
+
- **submit** - через form.submit() или кнопка submit
|
|
1140
|
+
- **reset** - через form.reset()
|
|
1141
|
+
- **fillForm** - итерация по полям + submit
|
|
1142
|
+
- **validateForm** - вызов reportValidity()
|
|
1143
|
+
- **setStyles** - через page.evaluate()
|
|
1144
|
+
- **getComputedCss** - через CDP
|
|
1145
|
+
- **getBoxModel** - через CDP
|
|
1146
|
+
- [ ] Поддержка waitForNavigation для click/submit
|
|
1147
|
+
- [ ] Поддержка screenshot после действия
|
|
1148
|
+
- [ ] Обработка ошибок (элемент не найден, не видим, disabled)
|
|
1149
|
+
- [ ] Unit + интеграционные тесты
|
|
1150
|
+
|
|
1151
|
+
**Зависимости:** нет (использует Puppeteer API)
|
|
1152
|
+
|
|
1153
|
+
---
|
|
1154
|
+
|
|
1155
|
+
#### 3.2. Инструмент performAction
|
|
1156
|
+
**Файл:** `pom/tools/perform-action.js`
|
|
1157
|
+
|
|
1158
|
+
**Задачи:**
|
|
1159
|
+
- [ ] Реализация MCP tool handler
|
|
1160
|
+
- [ ] Валидация pageId и elementId через ElementIdValidator
|
|
1161
|
+
- [ ] Получение селектора из модели по elementId
|
|
1162
|
+
- [ ] Вызов action-executor
|
|
1163
|
+
- [ ] Обработка результата (success, message, result, screenshot)
|
|
1164
|
+
- [ ] Определение изменения страницы (pageUpdated)
|
|
1165
|
+
- [ ] Документация
|
|
1166
|
+
- [ ] Интеграционные тесты
|
|
1167
|
+
|
|
1168
|
+
**Zod схема:**
|
|
1169
|
+
```javascript
|
|
1170
|
+
performAction: z.object({
|
|
1171
|
+
pageId: z.string(),
|
|
1172
|
+
elementId: z.string(),
|
|
1173
|
+
action: z.enum(['click', 'type', 'clear', 'focus', 'blur', 'hover', 'scrollTo',
|
|
1174
|
+
'selectOption', 'toggle', 'check', 'uncheck', 'select',
|
|
1175
|
+
'submit', 'reset', 'fillForm', 'validateForm',
|
|
1176
|
+
'setStyles', 'getComputedCss', 'getBoxModel']),
|
|
1177
|
+
params: z.record(z.any()).optional(),
|
|
1178
|
+
screenshot: z.boolean().optional(),
|
|
1179
|
+
waitAfter: z.number().optional()
|
|
1180
|
+
})
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
**Зависимости:** 3.1, 1.2, 2.3 (для получения модели)
|
|
1184
|
+
|
|
1185
|
+
---
|
|
1186
|
+
|
|
1187
|
+
### Фаза 4: Дополнительные инструменты (Week 4)
|
|
1188
|
+
|
|
1189
|
+
#### 4.1. Инструмент updatePageObject
|
|
1190
|
+
**Файл:** `pom/tools/update-page-object.js`
|
|
1191
|
+
|
|
1192
|
+
**Задачи:**
|
|
1193
|
+
- [ ] Реализация MCP tool handler
|
|
1194
|
+
- [ ] Валидация pageId
|
|
1195
|
+
- [ ] Обновление конкретных элементов (по elementIds) или всех
|
|
1196
|
+
- [ ] Добавление новых элементов (includeNew)
|
|
1197
|
+
- [ ] Удаление несуществующих (removeDeleted)
|
|
1198
|
+
- [ ] Обновление кэша
|
|
1199
|
+
- [ ] Возврат обновленной модели
|
|
1200
|
+
- [ ] Документация
|
|
1201
|
+
- [ ] Интеграционные тесты
|
|
1202
|
+
|
|
1203
|
+
**Zod схема:**
|
|
1204
|
+
```javascript
|
|
1205
|
+
updatePageObject: z.object({
|
|
1206
|
+
pageId: z.string(),
|
|
1207
|
+
elementIds: z.array(z.string()).optional(),
|
|
1208
|
+
includeNew: z.boolean().optional(),
|
|
1209
|
+
removeDeleted: z.boolean().optional()
|
|
1210
|
+
})
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
**Зависимости:** 2.1, 2.2, 1.2
|
|
1214
|
+
|
|
1215
|
+
---
|
|
1216
|
+
|
|
1217
|
+
#### 4.2. Инструмент queryElements
|
|
1218
|
+
**Файл:** `pom/tools/query-elements.js`
|
|
1219
|
+
|
|
1220
|
+
**Задачи:**
|
|
1221
|
+
- [ ] Реализация MCP tool handler
|
|
1222
|
+
- [ ] Валидация pageId
|
|
1223
|
+
- [ ] Получение модели из кэша
|
|
1224
|
+
- [ ] Фильтрация элементов по критериям:
|
|
1225
|
+
- type (поддержка массива типов)
|
|
1226
|
+
- text (substring, case-insensitive)
|
|
1227
|
+
- attributes (partial match)
|
|
1228
|
+
- visible/enabled
|
|
1229
|
+
- inForm (проверка parentId)
|
|
1230
|
+
- parentId (дочерние элементы)
|
|
1231
|
+
- hasAction (проверка actions)
|
|
1232
|
+
- [ ] Пагинация (limit/offset)
|
|
1233
|
+
- [ ] Возврат elementIds + metadata
|
|
1234
|
+
- [ ] Документация
|
|
1235
|
+
- [ ] Unit + интеграционные тесты
|
|
1236
|
+
|
|
1237
|
+
**Zod схема:**
|
|
1238
|
+
```javascript
|
|
1239
|
+
queryElements: z.object({
|
|
1240
|
+
pageId: z.string(),
|
|
1241
|
+
query: z.object({
|
|
1242
|
+
type: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1243
|
+
text: z.string().optional(),
|
|
1244
|
+
attributes: z.record(z.string()).optional(),
|
|
1245
|
+
visible: z.boolean().optional(),
|
|
1246
|
+
enabled: z.boolean().optional(),
|
|
1247
|
+
inForm: z.boolean().optional(),
|
|
1248
|
+
parentId: z.string().optional(),
|
|
1249
|
+
hasAction: z.string().optional()
|
|
1250
|
+
}),
|
|
1251
|
+
limit: z.number().optional(),
|
|
1252
|
+
offset: z.number().optional()
|
|
1253
|
+
})
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
**Зависимости:** 2.3, 1.2
|
|
1257
|
+
|
|
1258
|
+
---
|
|
1259
|
+
|
|
1260
|
+
#### 4.3. Инструмент getElementDetails
|
|
1261
|
+
**Файл:** `pom/tools/get-element-details.js`
|
|
1262
|
+
|
|
1263
|
+
**Задачи:**
|
|
1264
|
+
- [ ] Реализация MCP tool handler
|
|
1265
|
+
- [ ] Валидация pageId и elementId
|
|
1266
|
+
- [ ] Получение базовой модели элемента
|
|
1267
|
+
- [ ] Опциональное добавление computed styles (через CDP)
|
|
1268
|
+
- [ ] Опциональное добавление box model (через CDP)
|
|
1269
|
+
- [ ] Опциональное добавление деталей дочерних элементов
|
|
1270
|
+
- [ ] Документация
|
|
1271
|
+
- [ ] Интеграционные тесты
|
|
1272
|
+
|
|
1273
|
+
**Zod схема:**
|
|
1274
|
+
```javascript
|
|
1275
|
+
getElementDetails: z.object({
|
|
1276
|
+
pageId: z.string(),
|
|
1277
|
+
elementId: z.string(),
|
|
1278
|
+
includeStyles: z.boolean().optional(),
|
|
1279
|
+
includeBoxModel: z.boolean().optional(),
|
|
1280
|
+
includeChildren: z.boolean().optional()
|
|
1281
|
+
})
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
**Зависимости:** 2.3, 1.2
|
|
1285
|
+
|
|
1286
|
+
---
|
|
1287
|
+
|
|
1288
|
+
### Фаза 5: Интеграция и документация (Week 5)
|
|
1289
|
+
|
|
1290
|
+
#### 5.1. Интеграция в основной MCP server
|
|
1291
|
+
**Файл:** `index.js`, `tools/tool-schemas.js`, `server/tool-definitions.js`
|
|
1292
|
+
|
|
1293
|
+
**Задачи:**
|
|
1294
|
+
- [ ] Добавить импорты всех POM инструментов
|
|
1295
|
+
- [ ] Зарегистрировать инструменты в MCP server
|
|
1296
|
+
- [ ] Добавить Zod схемы в tool-schemas.js
|
|
1297
|
+
- [ ] Добавить определения в tool-definitions.js
|
|
1298
|
+
- [ ] Добавить новую группу инструментов 'pom' в tool-groups.js
|
|
1299
|
+
- [ ] Интеграционные тесты для всего MCP сервера
|
|
1300
|
+
|
|
1301
|
+
**Зависимости:** 2.3, 3.2, 4.1, 4.2, 4.3
|
|
1302
|
+
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
#### 5.2. Документация
|
|
1306
|
+
**Файлы:** `README.md`, `CHANGELOG.md`, `docs/POM_API.md`
|
|
1307
|
+
|
|
1308
|
+
**Задачи:**
|
|
1309
|
+
- [ ] Обновить README.md:
|
|
1310
|
+
- Добавить секцию "Page Object Model API"
|
|
1311
|
+
- Описание концепции
|
|
1312
|
+
- Список инструментов с кратким описанием
|
|
1313
|
+
- Примеры базового использования
|
|
1314
|
+
- Обновить счетчик инструментов (было ~50, стало ~55)
|
|
1315
|
+
- [ ] Обновить CHANGELOG.md:
|
|
1316
|
+
- Новая версия (например, 3.0.0 - major change)
|
|
1317
|
+
- Секция "Added" со списком 5 новых инструментов
|
|
1318
|
+
- Краткое описание концепции POM
|
|
1319
|
+
- [ ] Создать подробную документацию `docs/POM_API.md`:
|
|
1320
|
+
- Полное описание концепции
|
|
1321
|
+
- Спецификация всех инструментов
|
|
1322
|
+
- Примеры использования для распространенных сценариев
|
|
1323
|
+
- Comparison с существующими инструментами (когда использовать POM API vs классические инструменты)
|
|
1324
|
+
- Best practices
|
|
1325
|
+
- [ ] Обновить package.json версию
|
|
1326
|
+
|
|
1327
|
+
**Зависимости:** 5.1
|
|
1328
|
+
|
|
1329
|
+
---
|
|
1330
|
+
|
|
1331
|
+
#### 5.3. Примеры и тесты
|
|
1332
|
+
**Файлы:** `examples/pom-examples.js`, `tests/pom-integration.test.js`
|
|
1333
|
+
|
|
1334
|
+
**Задачи:**
|
|
1335
|
+
- [ ] Создать файл с примерами использования:
|
|
1336
|
+
- Пример 1: Получение модели и заполнение формы
|
|
1337
|
+
- Пример 2: Поиск элементов и клики
|
|
1338
|
+
- Пример 3: Обновление модели после AJAX
|
|
1339
|
+
- Пример 4: Работа с сложными формами
|
|
1340
|
+
- Пример 5: Изменение стилей элементов
|
|
1341
|
+
- [ ] End-to-end тесты:
|
|
1342
|
+
- Тест на реальной странице с формой
|
|
1343
|
+
- Тест на динамическом сайте (React/Vue)
|
|
1344
|
+
- Тест валидации ID
|
|
1345
|
+
- Тест обработки ошибок
|
|
1346
|
+
- [ ] Performance тесты:
|
|
1347
|
+
- Время генерации модели для больших страниц
|
|
1348
|
+
- Время выполнения действий
|
|
1349
|
+
- Память (размер модели)
|
|
1350
|
+
|
|
1351
|
+
**Зависимости:** 5.1
|
|
1352
|
+
|
|
1353
|
+
---
|
|
1354
|
+
|
|
1355
|
+
## Примеры использования
|
|
1356
|
+
|
|
1357
|
+
### Пример 1: Базовое использование - вход в систему
|
|
1358
|
+
|
|
1359
|
+
```javascript
|
|
1360
|
+
// Шаг 1: Получить модель страницы
|
|
1361
|
+
const page = await getPageObject();
|
|
1362
|
+
|
|
1363
|
+
console.log(page.groups.forms);
|
|
1364
|
+
// [
|
|
1365
|
+
// {
|
|
1366
|
+
// formId: 'form:0',
|
|
1367
|
+
// fields: {
|
|
1368
|
+
// inputs: {
|
|
1369
|
+
// 'input:form[0]:0': { inputType: 'email', placeholder: 'Email', ... },
|
|
1370
|
+
// 'input:form[0]:1': { inputType: 'password', placeholder: 'Password', ... }
|
|
1371
|
+
// }
|
|
1372
|
+
// },
|
|
1373
|
+
// submitButtons: [
|
|
1374
|
+
// { id: 'button:form[0]:0', text: 'Sign In', ... }
|
|
1375
|
+
// ]
|
|
1376
|
+
// }
|
|
1377
|
+
// ]
|
|
1378
|
+
|
|
1379
|
+
// Шаг 2: Заполнить форму одной командой
|
|
1380
|
+
await performAction({
|
|
1381
|
+
pageId: page.pageId,
|
|
1382
|
+
elementId: 'form:0',
|
|
1383
|
+
action: 'fillForm',
|
|
1384
|
+
params: {
|
|
1385
|
+
fields: {
|
|
1386
|
+
'input:form[0]:0': 'user@example.com',
|
|
1387
|
+
'input:form[0]:1': 'password123'
|
|
1388
|
+
},
|
|
1389
|
+
submit: true
|
|
1390
|
+
},
|
|
1391
|
+
screenshot: true
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
// Альтернатива: заполнить поля по отдельности
|
|
1395
|
+
await performAction({
|
|
1396
|
+
pageId: page.pageId,
|
|
1397
|
+
elementId: 'input:form[0]:0',
|
|
1398
|
+
action: 'type',
|
|
1399
|
+
params: { text: 'user@example.com' }
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
await performAction({
|
|
1403
|
+
pageId: page.pageId,
|
|
1404
|
+
elementId: 'input:form[0]:1',
|
|
1405
|
+
action: 'type',
|
|
1406
|
+
params: { text: 'password123' }
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
await performAction({
|
|
1410
|
+
pageId: page.pageId,
|
|
1411
|
+
elementId: 'button:form[0]:0',
|
|
1412
|
+
action: 'click',
|
|
1413
|
+
params: { waitForNavigation: true }
|
|
1414
|
+
});
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
---
|
|
1418
|
+
|
|
1419
|
+
### Пример 2: Поиск элементов и взаимодействие
|
|
1420
|
+
|
|
1421
|
+
```javascript
|
|
1422
|
+
// Шаг 1: Получить модель
|
|
1423
|
+
const page = await getPageObject({ groupBy: 'section' });
|
|
1424
|
+
|
|
1425
|
+
// Шаг 2: Найти все ссылки в навигации
|
|
1426
|
+
const navSection = page.groups.sections.find(s => s.sectionType === 'nav');
|
|
1427
|
+
const navLinks = navSection.childIds.filter(id =>
|
|
1428
|
+
page.elements[id].type === 'link'
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
console.log(navLinks);
|
|
1432
|
+
// ['link:header>nav:0', 'link:header>nav:1', 'link:header>nav:2']
|
|
1433
|
+
|
|
1434
|
+
// Шаг 3: Найти конкретную ссылку по тексту
|
|
1435
|
+
const { elementIds } = await queryElements({
|
|
1436
|
+
pageId: page.pageId,
|
|
1437
|
+
query: {
|
|
1438
|
+
type: 'link',
|
|
1439
|
+
text: 'Products',
|
|
1440
|
+
parentId: navSection.sectionId
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
// Шаг 4: Кликнуть на найденную ссылку
|
|
1445
|
+
await performAction({
|
|
1446
|
+
pageId: page.pageId,
|
|
1447
|
+
elementId: elementIds[0],
|
|
1448
|
+
action: 'click',
|
|
1449
|
+
params: { waitForNavigation: true }
|
|
1450
|
+
});
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1453
|
+
---
|
|
1454
|
+
|
|
1455
|
+
### Пример 3: Работа с динамическим контентом
|
|
1456
|
+
|
|
1457
|
+
```javascript
|
|
1458
|
+
// Шаг 1: Получить начальную модель
|
|
1459
|
+
let page = await getPageObject();
|
|
1460
|
+
|
|
1461
|
+
console.log(page.metadata.totalElements); // 50
|
|
1462
|
+
|
|
1463
|
+
// Шаг 2: Кликнуть на кнопку "Load More"
|
|
1464
|
+
const { elementIds } = await queryElements({
|
|
1465
|
+
pageId: page.pageId,
|
|
1466
|
+
query: {
|
|
1467
|
+
type: 'button',
|
|
1468
|
+
text: 'Load More'
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
await performAction({
|
|
1473
|
+
pageId: page.pageId,
|
|
1474
|
+
elementId: elementIds[0],
|
|
1475
|
+
action: 'click',
|
|
1476
|
+
waitAfter: 1000
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// Шаг 3: Обновить модель после загрузки новых элементов
|
|
1480
|
+
page = await updatePageObject({
|
|
1481
|
+
pageId: page.pageId,
|
|
1482
|
+
includeNew: true
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
console.log(page.metadata.totalElements); // 75 (добавилось 25 элементов)
|
|
1486
|
+
|
|
1487
|
+
// Шаг 4: Найти новые элементы
|
|
1488
|
+
const newElements = Object.keys(page.elements).filter(id =>
|
|
1489
|
+
!oldElements.includes(id)
|
|
1490
|
+
);
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
---
|
|
1494
|
+
|
|
1495
|
+
### Пример 4: Изменение стилей элементов
|
|
1496
|
+
|
|
1497
|
+
```javascript
|
|
1498
|
+
// Шаг 1: Получить модель
|
|
1499
|
+
const page = await getPageObject();
|
|
1500
|
+
|
|
1501
|
+
// Шаг 2: Найти главный заголовок
|
|
1502
|
+
const { elementIds } = await queryElements({
|
|
1503
|
+
pageId: page.pageId,
|
|
1504
|
+
query: {
|
|
1505
|
+
type: 'generic',
|
|
1506
|
+
text: 'Welcome',
|
|
1507
|
+
attributes: { tagName: 'h1' }
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// Шаг 3: Изменить стили заголовка
|
|
1512
|
+
await performAction({
|
|
1513
|
+
pageId: page.pageId,
|
|
1514
|
+
elementId: elementIds[0],
|
|
1515
|
+
action: 'setStyles',
|
|
1516
|
+
params: {
|
|
1517
|
+
styles: [
|
|
1518
|
+
{ name: 'color', value: 'red' },
|
|
1519
|
+
{ name: 'font-size', value: '48px' },
|
|
1520
|
+
{ name: 'font-weight', value: 'bold' }
|
|
1521
|
+
]
|
|
1522
|
+
},
|
|
1523
|
+
screenshot: true
|
|
1524
|
+
});
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
---
|
|
1528
|
+
|
|
1529
|
+
### Пример 5: Валидация формы
|
|
1530
|
+
|
|
1531
|
+
```javascript
|
|
1532
|
+
// Шаг 1: Получить модель с формой
|
|
1533
|
+
const page = await getPageObject();
|
|
1534
|
+
|
|
1535
|
+
const form = page.groups.forms[0];
|
|
1536
|
+
console.log(form.validation);
|
|
1537
|
+
// {
|
|
1538
|
+
// valid: false,
|
|
1539
|
+
// requiredFields: ['input:form[0]:0', 'input:form[0]:1'],
|
|
1540
|
+
// invalidFields: []
|
|
1541
|
+
// }
|
|
1542
|
+
|
|
1543
|
+
// Шаг 2: Попытаться отправить пустую форму (для показа ошибок)
|
|
1544
|
+
await performAction({
|
|
1545
|
+
pageId: page.pageId,
|
|
1546
|
+
elementId: form.formId,
|
|
1547
|
+
action: 'validateForm'
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// Шаг 3: Получить детали полей с ошибками
|
|
1551
|
+
for (const fieldId of form.validation.requiredFields) {
|
|
1552
|
+
const details = await getElementDetails({
|
|
1553
|
+
pageId: page.pageId,
|
|
1554
|
+
elementId: fieldId
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
console.log(details.validationState);
|
|
1558
|
+
// { valid: false, message: 'Please fill out this field.' }
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Шаг 4: Заполнить поля
|
|
1562
|
+
await performAction({
|
|
1563
|
+
pageId: page.pageId,
|
|
1564
|
+
elementId: form.formId,
|
|
1565
|
+
action: 'fillForm',
|
|
1566
|
+
params: {
|
|
1567
|
+
fields: {
|
|
1568
|
+
'input:form[0]:0': 'user@example.com',
|
|
1569
|
+
'input:form[0]:1': 'password123'
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
// Шаг 5: Обновить модель и проверить валидацию
|
|
1575
|
+
const updatedPage = await updatePageObject({
|
|
1576
|
+
pageId: page.pageId,
|
|
1577
|
+
elementIds: [form.formId]
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
const updatedForm = updatedPage.elements[form.formId];
|
|
1581
|
+
console.log(updatedForm.validation);
|
|
1582
|
+
// { valid: true, requiredFields: [...], invalidFields: [] }
|
|
1583
|
+
|
|
1584
|
+
// Шаг 6: Отправить форму
|
|
1585
|
+
await performAction({
|
|
1586
|
+
pageId: page.pageId,
|
|
1587
|
+
elementId: form.formId,
|
|
1588
|
+
action: 'submit',
|
|
1589
|
+
params: { waitForNavigation: true }
|
|
1590
|
+
});
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
---
|
|
1594
|
+
|
|
1595
|
+
### Пример 6: Работа с checkbox и radio
|
|
1596
|
+
|
|
1597
|
+
```javascript
|
|
1598
|
+
// Шаг 1: Получить модель
|
|
1599
|
+
const page = await getPageObject();
|
|
1600
|
+
|
|
1601
|
+
// Шаг 2: Найти все checkbox
|
|
1602
|
+
const { elementIds: checkboxIds } = await queryElements({
|
|
1603
|
+
pageId: page.pageId,
|
|
1604
|
+
query: { type: 'checkbox' }
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// Шаг 3: Включить все checkbox
|
|
1608
|
+
for (const id of checkboxIds) {
|
|
1609
|
+
await performAction({
|
|
1610
|
+
pageId: page.pageId,
|
|
1611
|
+
elementId: id,
|
|
1612
|
+
action: 'check'
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Шаг 4: Найти radio группу
|
|
1617
|
+
const { elementIds: radioIds } = await queryElements({
|
|
1618
|
+
pageId: page.pageId,
|
|
1619
|
+
query: {
|
|
1620
|
+
type: 'radio',
|
|
1621
|
+
attributes: { name: 'subscription' }
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// Шаг 5: Получить детали одного radio (чтобы увидеть все опции группы)
|
|
1626
|
+
const radioDetails = await getElementDetails({
|
|
1627
|
+
pageId: page.pageId,
|
|
1628
|
+
elementId: radioIds[0]
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
console.log(radioDetails.groupOptions);
|
|
1632
|
+
// [
|
|
1633
|
+
// { elementId: 'radio:form[0]:0', value: 'free', text: 'Free', checked: true },
|
|
1634
|
+
// { elementId: 'radio:form[0]:1', value: 'pro', text: 'Pro', checked: false },
|
|
1635
|
+
// { elementId: 'radio:form[0]:2', value: 'enterprise', text: 'Enterprise', checked: false }
|
|
1636
|
+
// ]
|
|
1637
|
+
|
|
1638
|
+
// Шаг 6: Выбрать опцию "Pro"
|
|
1639
|
+
const proOption = radioDetails.groupOptions.find(o => o.value === 'pro');
|
|
1640
|
+
await performAction({
|
|
1641
|
+
pageId: page.pageId,
|
|
1642
|
+
elementId: proOption.elementId,
|
|
1643
|
+
action: 'select'
|
|
1644
|
+
});
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
---
|
|
1648
|
+
|
|
1649
|
+
## Comparison: POM API vs Классические инструменты
|
|
1650
|
+
|
|
1651
|
+
### Когда использовать POM API:
|
|
1652
|
+
|
|
1653
|
+
✅ **Сложные формы** - один вызов `fillForm` вместо множества `type` команд
|
|
1654
|
+
✅ **Многошаговые взаимодействия** - получить модель один раз, использовать ID многократно
|
|
1655
|
+
✅ **Динамический контент** - `updatePageObject` для обновления модели после AJAX
|
|
1656
|
+
✅ **Исследование страницы** - `queryElements` для поиска элементов по критериям
|
|
1657
|
+
✅ **Валидация форм** - детальная информация о required fields и validation state
|
|
1658
|
+
✅ **Контекстные действия** - знание о том, какие действия доступны для элемента
|
|
1659
|
+
|
|
1660
|
+
### Когда использовать классические инструменты:
|
|
1661
|
+
|
|
1662
|
+
✅ **Простые одиночные действия** - быстрый `click(selector)` или `type(selector, text)`
|
|
1663
|
+
✅ **Известные селекторы** - если уже знаете точный CSS селектор
|
|
1664
|
+
✅ **Легковесные операции** - не нужна полная модель страницы
|
|
1665
|
+
✅ **Скриншоты и инспекция** - `screenshot`, `getComputedCss`, `getBoxModel` остаются отдельными инструментами
|
|
1666
|
+
|
|
1667
|
+
### Гибридный подход:
|
|
1668
|
+
|
|
1669
|
+
Можно комбинировать оба подхода:
|
|
1670
|
+
|
|
1671
|
+
```javascript
|
|
1672
|
+
// Использовать POM для сложного взаимодействия
|
|
1673
|
+
const page = await getPageObject();
|
|
1674
|
+
await performAction({ elementId: 'form:0', action: 'fillForm', ... });
|
|
1675
|
+
|
|
1676
|
+
// Использовать классические инструменты для быстрого скриншота
|
|
1677
|
+
await screenshot({ selector: 'body' });
|
|
1678
|
+
|
|
1679
|
+
// Использовать POM для поиска элемента
|
|
1680
|
+
const { elementIds } = await queryElements({ query: { text: 'Submit' } });
|
|
1681
|
+
|
|
1682
|
+
// Использовать классический click (если нужен просто клик)
|
|
1683
|
+
await click({ selector: `[data-testid="submit-button"]` });
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
---
|
|
1687
|
+
|
|
1688
|
+
## Расширения и будущие улучшения
|
|
1689
|
+
|
|
1690
|
+
### Версия 3.1 (будущее):
|
|
1691
|
+
|
|
1692
|
+
1. **Поддержка Shadow DOM** - работа с Web Components
|
|
1693
|
+
2. **Поддержка iframe** - вложенные документы
|
|
1694
|
+
3. **Accessibility tree** - доступ к accessibility информации
|
|
1695
|
+
4. **Event listeners** - информация о прикрепленных обработчиках событий
|
|
1696
|
+
5. **Performance metrics** - время рендеринга элементов
|
|
1697
|
+
|
|
1698
|
+
### Версия 3.2 (будущее):
|
|
1699
|
+
|
|
1700
|
+
1. **Smart actions** - AI предложения действий на основе контекста
|
|
1701
|
+
2. **Visual regression** - сравнение скриншотов элементов
|
|
1702
|
+
3. **Element snapshots** - сохранение и восстановление состояния элементов
|
|
1703
|
+
4. **Batch operations** - выполнение множественных действий одной командой
|
|
1704
|
+
|
|
1705
|
+
---
|
|
1706
|
+
|
|
1707
|
+
## Технические требования
|
|
1708
|
+
|
|
1709
|
+
### Производительность:
|
|
1710
|
+
|
|
1711
|
+
- Генерация модели для страницы со 100 элементами: < 500ms
|
|
1712
|
+
- Выполнение действия по ID: < 100ms
|
|
1713
|
+
- Обновление модели (частичное): < 200ms
|
|
1714
|
+
- Размер модели в JSON: < 500KB для 100 элементов
|
|
1715
|
+
|
|
1716
|
+
### Совместимость:
|
|
1717
|
+
|
|
1718
|
+
- Puppeteer 24.x+
|
|
1719
|
+
- Node.js 18+
|
|
1720
|
+
- Chrome/Chromium 120+
|
|
1721
|
+
|
|
1722
|
+
### Обратная совместимость:
|
|
1723
|
+
|
|
1724
|
+
- Все существующие инструменты продолжают работать без изменений
|
|
1725
|
+
- POM API - это дополнение, а не замена
|
|
1726
|
+
|
|
1727
|
+
---
|
|
1728
|
+
|
|
1729
|
+
## Заключение
|
|
1730
|
+
|
|
1731
|
+
Agent Page Object Model (APOM) API - это мощное дополнение к chrometools-mcp, которое позволяет AI агентам работать с браузером как с объектной моделью, а не набором команд.
|
|
1732
|
+
|
|
1733
|
+
**Не путать с:** Существующий инструмент `generatePageObject` остаётся без изменений и продолжает генерировать Test Page Objects для автотестов (Playwright/Selenium).
|
|
1734
|
+
|
|
1735
|
+
**Ключевые преимущества:**
|
|
1736
|
+
|
|
1737
|
+
1. ⚡ **Меньше запросов** - получить модель один раз, использовать многократно
|
|
1738
|
+
2. 🎯 **Семантика** - знание о типах элементов и доступных действиях
|
|
1739
|
+
3. 🔒 **Валидация** - проверка ID элементов и состояния страницы
|
|
1740
|
+
4. 📊 **Структура** - группировка элементов по типу, секциям, формам
|
|
1741
|
+
5. 🚀 **Производительность** - кэширование и инкрементальные обновления
|
|
1742
|
+
6. 💪 **Мощь** - сложные операции (fillForm, validateForm) одной командой
|
|
1743
|
+
|
|
1744
|
+
**Roadmap:**
|
|
1745
|
+
|
|
1746
|
+
- Week 1-2: Базовая инфраструктура + getPageObject
|
|
1747
|
+
- Week 3: performAction
|
|
1748
|
+
- Week 4: updatePageObject, queryElements, getElementDetails
|
|
1749
|
+
- Week 5: Интеграция, документация, тесты
|
|
1750
|
+
|
|
1751
|
+
**Версия:** 3.0.0 (major release)
|
|
1752
|
+
|
|
1753
|
+
---
|
|
1754
|
+
|
|
1755
|
+
Документация создана: 2026-01-24
|
|
1756
|
+
Версия: 1.0.0
|