From f9439dce6d765ca1752244a95fd50662cc31ea3d Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 28 Feb 2026 18:05:35 +0300 Subject: [PATCH] docs(web-test): expand guide with detailed API, modes, and complex scenario - Detailed description of autonomous vs interactive modes with examples - Full API reference with signatures, return shapes, and code samples - Complex autonomous scenario: compare stock reports across two warehouses - Troubleshooting table for common errors - Keyboard shortcuts reference Co-Authored-By: Claude Opus 4.6 --- docs/web-test-guide.md | 513 ++++++++++++++++++++++++++++++++++------- 1 file changed, 430 insertions(+), 83 deletions(-) diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index c0995102..106b8cb5 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -22,124 +22,471 @@ └── правки → /db-load-xml → /db-update ──┘ ``` -### Два режима работы +## Режимы работы -**Автономный** — одна команда запускает браузер, выполняет сценарий и закрывает: -``` -> Открой список заказов клиентов в ERP, найди заказ КП00-000018, открой и прочитай реквизиты -``` -Claude напишет `.js` файл со сценарием и запустит `node $RUN run script.js`. +### Автономный режим (run) -**Интерактивный** — пошаговая работа через живую сессию: -``` -> Запусти браузер на базе erp -> Перейди в раздел Продажи -> Открой Заказы клиентов -> Прочитай таблицу -``` -Claude запустит `node $RUN start `, затем выполнит каждый шаг через `node $RUN exec`. +Одна команда: открывает браузер → логинится → выполняет сценарий → закрывает браузер → завершает процесс. -## Сценарии использования - -### Навигация и чтение данных - -``` -> Открой список контрагентов в ERP и покажи первые 10 записей +```bash +RUN=".claude/skills/web-test/scripts/run.mjs" +node $RUN run http://localhost:8081/erp scenario.js ``` -Claude перейдёт в нужный раздел, откроет список и прочитает таблицу. +**Когда использовать**: готовый сценарий, который нужно выполнить целиком. Идеален для субагентов и CI — не оставляет висящих процессов. -### Создание документа +Claude пишет `.js` файл со сценарием, запускает его и получает результат. Все функции API доступны как глобальные переменные, `console.log()` выводит данные в ответ. -``` -> Создай заказ клиента: организация "Андромеда Плюс", контрагент "Торговый дом Комплексный", -> добавь строку: номенклатура "Вентилятор", количество 5 +Можно также передать скрипт через stdin: +```bash +cat <<'SCRIPT' | node $RUN run http://localhost:8081/erp - +await navigateSection('Продажи'); +const form = await openCommand('Заказы клиентов'); +console.log('Columns:', JSON.stringify(form.table?.columns)); +const data = await readTable({ maxRows: 5 }); +console.log('First 5 rows:', JSON.stringify(data.rows, null, 2)); +SCRIPT ``` -Claude откроет форму создания, заполнит реквизиты шапки, добавит строку в табличную часть. - -### Работа с отчётами - -``` -> Открой отчёт "Остатки и доступность товаров", -> установи отбор Склад = "Склад бытовой техники", сформируй и прочитай результат +Ответ — JSON: +```json +{ "ok": true, "output": "Columns: [...]\nFirst 5 rows: [...]", "elapsed": 12.3 } ``` -Claude заполнит фильтры отчёта (DCS-настройки) по человекочитаемым именам, нажмёт "Сформировать" и прочитает табличный документ. - -### Поиск и фильтрация - -``` -> Найди в списке номенклатуры товар "Вентилятор" и открой его карточку +При ошибке — автоматический скриншот: +```json +{ "ok": false, "error": "Element not found: Созздать", "screenshot": "error-shot.png", "elapsed": 5.1 } ``` -Claude отфильтрует список, откроет найденный элемент двойным кликом, прочитает форму. +### Интерактивный режим (start/exec/stop) -### Проверка после загрузки расширения +Браузер остаётся открытым между командами. Удобно для пошаговой отладки и исследования интерфейса. -``` -> Загрузи расширение ТестОшибки и проверь через браузер, что при создании заказа клиента -> появляется ошибка "Тестовая ошибка из расширения" +```bash +# 1. Запустить сессию (фоновый процесс с HTTP-сервером) +node $RUN start http://localhost:8081/erp + +# 2. Выполнять команды по очереди +cat <<'SCRIPT' | node $RUN exec - +await navigateSection('Продажи'); +SCRIPT + +cat <<'SCRIPT' | node $RUN exec - +const form = await openCommand('Заказы клиентов'); +console.log(JSON.stringify(form, null, 2)); +SCRIPT + +# 3. Сделать скриншот в любой момент +node $RUN shot current-state.png + +# 4. Завершить сессию +node $RUN stop ``` -Claude загрузит расширение через `/db-load-xml`, затем через `/web-test` откроет форму заказа и проверит ожидаемое поведение. +**Когда использовать**: пошаговое исследование интерфейса, отладка сценария, когда нужно видеть состояние формы между шагами. -## API: что умеет навык +Ключевое отличие: `start` запускает HTTP-сервер в фоне, `exec` отправляет скрипты через HTTP POST. Состояние браузера (открытые вкладки, формы) сохраняется между вызовами `exec`. + +## API ### Навигация -| Функция | Что делает | -|---------|------------| -| `navigateSection(name)` | Переход в раздел (Продажи, Склад и доставка, ...) | -| `openCommand(name)` | Открытие команды из панели функций | -| `navigateLink(url)` | Открытие по пути метаданных (`Документ.ЗаказКлиента`) | -| `switchTab(name)` | Переключение между открытыми вкладками | +#### `navigateSection(name)` → `{ navigated, sections, commands }` -### Чтение - -| Функция | Что делает | -|---------|------------| -| `getFormState()` | Структура формы: поля, кнопки, вкладки, фильтры, DCS-настройки отчёта | -| `readTable()` | Данные таблицы с пагинацией, поддержка дерева и иерархии | -| `readSpreadsheet()` | Табличный документ отчёта: заголовки, данные, итоги | -| `getSections()` | Разделы и команды | - -### Действия - -| Функция | Что делает | -|---------|------------| -| `clickElement(text)` | Клик по кнопке, ссылке, вкладке, строке таблицы | -| `fillFields({...})` | Заполнение полей формы (текст, чекбокс, радио, ссылочные) | -| `selectValue(field, search)` | Выбор из справочника через форму подбора | -| `fillTableRow({...})` | Заполнение строки табличной части | -| `filterList(text)` | Фильтрация списка (простая и расширенная) | -| `closeForm()` | Закрытие формы с управлением диалогом подтверждения | - -### DCS-отчёты (фильтры по меткам) - -Фильтры отчётов можно задавать человекочитаемыми именами вместо технических: +Переход в раздел верхнего уровня (Продажи, Склад и доставка, Закупки, ...). Fuzzy match по имени. ```js -// Вместо: 'КомпоновщикНастроекПользовательскиеНастройкиЭлемент3Значение' -// Просто: -await fillFields({ 'Склад': 'Склад бытовой техники' }); +const r = await navigateSection('Склад'); +// r.sections = [{ name: 'Главное', active: false }, { name: 'Склад и доставка', active: true }, ...] +// r.commands = ['Отчеты по складу', 'Поступление товаров и услуг', ...] ``` -`getFormState()` возвращает `reportSettings` — фильтры в читаемом виде: +Возвращает список команд текущего раздела — используйте для выбора следующего действия. + +#### `openCommand(name)` → form state + +Открытие команды из панели функций. Возвращает состояние открывшейся формы (поля, таблица, кнопки). + +```js +const form = await openCommand('Заказы клиентов'); +// form.table = { name: 'Список', columns: ['Номер', 'Дата', 'Статус', ...], rowCount: 20 } +// form.buttons = ['Создать', 'Найти', 'Ещё', ...] +// form.filters = [{ name: 'Организация', active: false }, ...] +``` + +#### `navigateLink(path)` → form state + +Открытие объекта по пути метаданных через диалог Shift+F11. Обходит навигацию по разделам — полезно для регистров, журналов и любых форм с известным путём. + +```js +await navigateLink('Документ.ЗаказКлиента'); // список документов +await navigateLink('Справочник.Номенклатура'); // справочник +await navigateLink('РегистрНакопления.ТоварыНаСкладах'); // регистр +``` + +#### `switchTab(name)` → form state + +Переключение между уже открытыми вкладками. Fuzzy match. + +```js +await switchTab('Заказы клиентов'); // вернуться на открытую ранее вкладку +``` + +### Чтение состояния + +#### `getFormState()` → `{ fields, buttons, tabs, table, filters, reportSettings? }` + +Основной способ «увидеть» что на экране. Возвращает структуру текущей формы. + +```js +const form = await getFormState(); +``` + +**fields** — массив полей формы: ```json [ - { "name": "Склад", "enabled": true, "value": "Склад бытовой техники" }, + { "name": "Организация", "value": "Андромеда Плюс", "label": "Организация:", "actions": ["select", "clear", "open"] }, + { "name": "Дата", "value": "28.02.2026", "label": "от:" }, + { "name": "Проведен", "value": true, "label": "Проведен" }, + { "name": "Сумма", "value": "150 000,00", "required": true } +] +``` +- `actions` — доступные действия (select = выбор из справочника, clear = очистка, open = открыть связанный объект) +- `required: true` — незаполненное обязательное поле (подсвечено красным) + +**table** — метаданные таблицы (не данные!): +```json +{ "name": "Товары", "columns": ["Номенклатура", "Количество", "Цена", "Сумма"], "rowCount": 3 } +``` +Для чтения строк таблицы вызовите `readTable()`. + +**reportSettings** — фильтры DCS-отчётов в читаемом виде: +```json +[ + { "name": "Склад", "enabled": true, "value": "Склад бытовой техники", "actions": ["select"] }, { "name": "Номенклатура", "enabled": false, "value": "" } ] ``` +Вместо технических имён вроде `КомпоновщикНастроекПользовательскиеНастройкиЭлемент3Значение`. + +**errorModal** — если есть, 1С показала ошибку. Прочитайте сообщение и решите что делать. + +**confirmation** — диалог Да/Нет. Вызовите `clickElement('Да')` или `clickElement('Нет')`. + +#### `readTable({ maxRows?, offset? })` → `{ columns, rows, total, shown, offset }` + +Чтение данных таблицы (список документов, табличная часть, любой грид). Каждая строка — объект `{ columnName: value }`. + +```js +const t = await readTable({ maxRows: 10 }); +// t.columns = ['Номер', 'Дата', 'Контрагент', 'Сумма'] +// t.rows = [ +// { 'Номер': '0000-000039', 'Дата': '01.10.2022', 'Контрагент': 'Фирма "LIGHT"', 'Сумма': '657 600,00' }, +// ... +// ] +// t.total = 20, t.shown = 10, t.offset = 0 +``` + +**Пагинация** — для больших списков: +```js +const page1 = await readTable({ maxRows: 50 }); // строки 0-49 +const page2 = await readTable({ maxRows: 50, offset: 50 }); // строки 50-99 +``` + +**Иерархия и дерево** — специальные поля строк: +```js +const t = await readTable(); +// t.hierarchical = true — список с группами +// t.viewMode = 'tree' — режим дерева +// Строки: +// { 'Наименование': 'Электроника', _kind: 'group' } — группа +// { 'Наименование': 'Вентилятор', _tree: 'expanded', _level: 1 } — узел дерева +// { 'Наименование': 'Модель А', _level: 2 } — вложенный элемент +``` + +#### `readSpreadsheet()` → `{ title?, headers?, data?, totals?, total }` + +Чтение табличного документа (SpreadsheetDocument) — результат отчёта после нажатия "Сформировать". + +```js +await clickElement('Сформировать'); +await wait(5); // отчёт формируется +const report = await readSpreadsheet(); +// report.title = "Остатки и доступность товаров" +// report.headers = ["Артикул", "Номенклатура", "Ед. изм.", "В наличии", "Доступно"] +// report.data = [ +// { "Артикул": "В-789", "Номенклатура": "Вентилятор BINATONE", "В наличии": "14,000", "Доступно": "2,000" }, +// ... +// ] +// report.totals = { "В наличии": "903,000", "Доступно": "797,000" } +// report.total = 28 +``` + +Если заголовки не распознаны, возвращает сырые строки: `{ rows: string[][], total }`. + +#### `getSections()` → `{ activeSection, sections, commands }` + +Разделы и команды текущего раздела без навигации. + +#### `getCommands()` → `string[]` + +Только команды текущего раздела. + +#### `getPageState()` → `{ activeSection, activeTab, sections, tabs }` + +Полное состояние страницы: разделы + все открытые вкладки. + +### Действия + +#### `clickElement(text, { dblclick? })` → form state + +Клик по кнопке, гиперссылке, вкладке формы, строке таблицы. Fuzzy match. + +**Одиночный клик** — нажатие кнопки или выбор строки в списке: +```js +await clickElement('Создать'); // кнопка +await clickElement('Товары'); // вкладка формы +await clickElement('0000-000039'); // выбрать строку (НЕ открывает) +``` + +**Двойной клик** — открытие элемента из списка: +```js +await clickElement('0000-000039', { dblclick: true }); // открывает документ +``` +Одиночный клик только выделяет строку. Чтобы открыть — всегда `{ dblclick: true }`. + +**Подменю** — если клик открывает меню, возвращается `submenu`: +```js +const r = await clickElement('Ещё'); +// r.submenu = ['Расширенный поиск', 'Настройки', 'Изменить форму', ...] +await clickElement('Расширенный поиск'); // выбрать пункт +``` + +**Дерево** — клик по иконке раскрывает/сворачивает узел. + +#### `fillFields({ name: value })` → `{ filled, form }` + +Заполнение полей формы по метке (fuzzy match). Автоматически определяет тип поля. + +```js +const result = await fillFields({ + 'Организация': 'Андромеда Плюс', // ссылочное поле — typeahead через clipboard paste + 'Сумма': '5000', // текст — clipboard paste + 'Оплачено': 'true', // чекбокс — toggle (также: 'false', 'да', 'нет') + 'Вид операции': 'Оплата поставщику' // радио — fuzzy match по меткам вариантов +}); +// result.filled = [ +// { field: 'Организация', ok: true, value: 'Андромеда Плюс', method: 'typeahead' }, +// { field: 'Сумма', ok: true, value: '5000', method: 'paste' }, +// { field: 'Оплачено', ok: true, value: 'true', method: 'toggle' }, +// { field: 'Вид операции', ok: true, value: 'Оплата поставщику', method: 'radio' } +// ] +// result.form = { ... текущее состояние формы ... } +``` + +**DCS-фильтры отчётов** — человекочитаемые метки вместо технических имён. Чекбокс «Использование» включается автоматически: +```js +await fillFields({ + 'Склад': 'Склад бытовой техники', // включит чекбокс + заполнит значение + 'Номенклатура': 'Вентилятор' // то же: включит + заполнит +}); +``` + +#### `selectValue(field, search)` → form state с `selected` + +Выбор значения из справочника через выпадающий список или форму подбора. Надёжнее чем `fillFields` для ссылочных полей, где нужен точный выбор из каталога. + +```js +const r = await selectValue('Контрагент', 'Торговый дом'); +// r.selected = { field: 'Контрагент', search: 'Торговый дом', method: 'form' } +``` + +Обрабатывает три сценария: +- **Dropdown** — выпадающий список с вариантами → клик по совпадению +- **История + "Показать все"** — dropdown с историей → переход в форму подбора +- **Форма подбора** — отдельное окно с поиском → ввод текста + двойной клик + +Также поддерживает DCS-метки — автоматически включает чекбокс. + +#### `fillTableRow(fields, opts)` → form state + +Заполнение строки табличной части. Навигация между ячейками через Tab. + +```js +// Добавить новую строку: +await fillTableRow( + { 'Номенклатура': 'Бумага А4', 'Количество': '10', 'Цена': '500' }, + { tab: 'Товары', add: true } +); + +// Редактировать существующую строку: +await fillTableRow( + { 'Количество': '20' }, + { tab: 'Товары', row: 0 } // 0-based индекс +); +``` + +- `tab` — имя вкладки с таблицей (если их несколько) +- `add: true` — нажать "Добавить" перед заполнением +- `row: N` — двойной клик по строке N для редактирования +- Порядок полей определяется конфигурацией формы 1С (Tab-порядок) +- Ссылочные ячейки автоматически определяются по popup автодополнения + +#### `deleteTableRow(row, { tab? })` → form state + +Удаление строки по 0-based индексу. + +```js +await deleteTableRow(0, { tab: 'Товары' }); +``` + +#### `closeForm({ save? })` → form state + +Закрытие текущей формы через Escape. + +```js +await closeForm({ save: false }); // закрыть без сохранения (автоматически "Нет") +await closeForm({ save: true }); // закрыть с сохранением (автоматически "Да") +await closeForm(); // если появится диалог — вернёт confirmation +``` + +Предпочтительнее чем `clickElement('×')` — кнопки закрытия на вкладках неоднозначны. + +#### `filterList(text, opts?)` → form state + +Фильтрация списка. Два режима: + +```js +// Простой — поиск по всем колонкам: +await filterList('КП00-000018'); + +// Расширенный — поиск по конкретному полю: +await filterList('Конфетпром', { field: 'Наименование' }); + +// Точное совпадение: +await filterList('Конфетпром ООО', { field: 'Наименование', exact: true }); +``` + +Работает с иерархическими списками (справочники с группами) — автоматически переключает в плоский режим для поиска. + +#### `unfilterList({ field? })` → form state + +Снятие фильтров: +```js +await unfilterList(); // снять все +await unfilterList({ field: 'Наименование' }); // снять конкретный +``` + +### Утилиты + +#### `screenshot()` → PNG Buffer + +Скриншот текущего состояния браузера. + +#### `wait(seconds)` → form state + +Ожидание N секунд. Возвращает состояние формы после паузы. Используйте для длительных операций (формирование отчёта, проведение документа). + +#### `getPage()` → Playwright Page + +Доступ к сырому объекту Playwright для нестандартных операций: +```js +const page = getPage(); +await page.keyboard.press('F8'); // создать новый элемент справочника +await page.keyboard.press('Shift+F4'); // очистить ссылочное поле +``` + +## Пример: сложный автономный сценарий + +Сценарий для субагента: проверить отчёт «Остатки и доступность товаров» по двум складам и сравнить итоги. + +```js +// === Сценарий: сравнение остатков по двум складам === + +// 1. Открыть отчёт +await navigateSection('Склад и доставка'); +await openCommand('Отчеты по складу'); +await clickElement('Остатки и доступность товаров', { dblclick: true }); + +// 2. Первый склад — "Склад бытовой техники" +await fillFields({ 'Склад': 'Склад бытовой техники' }); +await clickElement('Сформировать'); +await wait(5); + +const report1 = await readSpreadsheet(); +console.log('=== Склад бытовой техники ==='); +console.log('Строк:', report1.data?.length || 0); +if (report1.totals) console.log('Итого В наличии:', report1.totals['В наличии']); +if (report1.totals) console.log('Итого Доступно:', report1.totals['Доступно']); + +// 3. Второй склад — "Западный склад" +await fillFields({ 'Склад': 'Западный склад' }); +await clickElement('Сформировать'); +await wait(5); + +const report2 = await readSpreadsheet(); +console.log('\n=== Западный склад ==='); +console.log('Строк:', report2.data?.length || 0); +if (report2.totals) console.log('Итого В наличии:', report2.totals['В наличии']); +if (report2.totals) console.log('Итого Доступно:', report2.totals['Доступно']); + +// 4. Сравнение +console.log('\n=== Сравнение ==='); +const parse = s => parseFloat((s || '0').replace(/\s/g, '').replace(',', '.')); +const avail1 = parse(report1.totals?.['Доступно']); +const avail2 = parse(report2.totals?.['Доступно']); +console.log(`Бытовой техники: ${avail1}, Западный: ${avail2}`); +console.log(`Разница: ${(avail1 - avail2).toFixed(0)}`); + +// 5. Закрыть отчёт +await closeForm({ save: false }); +console.log('done'); +``` + +Запуск: +```bash +node $RUN run http://localhost:8081/erp scenario-compare-stocks.js +``` + +Результат: +```json +{ + "ok": true, + "output": "=== Склад бытовой техники ===\nСтрок: 28\nИтого В наличии: 903,000\nИтого Доступно: 797,000\n\n=== Западный склад ===\nСтрок: 15\nИтого В наличии: 420,000\nИтого Доступно: 350,000\n\n=== Сравнение ===\nБытовой техники: 797, Западный: 350\nРазница: 447\ndone", + "elapsed": 45.2 +} +``` + +## Типичные ошибки и решения + +| Проблема | Причина | Решение | +|----------|---------|---------| +| `no_form` | Форма не открыта или не загрузилась | Добавьте `await wait(2)` после навигации | +| `not_found` для элемента | Fuzzy match не нашёл | Проверьте точное имя через `getFormState()` | +| Зацикливание поиска | Элемент не существует в базе | Навык остановится после 2 попыток — прочитайте что есть через `readTable()` | +| Пустой `readSpreadsheet()` | Отчёт не успел сформироваться | Увеличьте `await wait(N)` перед чтением | +| Clipboard paste не работает | Фокус не на поле ввода | `clickElement` перед `fillFields` на нужной области | + +## Клавиатурные сочетания + +Полезные горячие клавиши 1С, доступные через `getPage().keyboard.press()`: + +| Клавиша | Контекст | Действие | +|---------|----------|----------| +| `F8` | Ссылочное поле в фокусе | Создать новый элемент справочника | +| `Shift+F4` | Ссылочное поле в фокусе | Очистить значение поля | +| `F4` | Ссылочное поле в фокусе | Открыть форму выбора | +| `Alt+F` | Форма списка/таблицы | Расширенный поиск | +| `Escape` | Любая форма | Закрыть текущую форму | ## Особенности - **Headed mode** — 1С требует видимый браузер, headless не поддерживается -- **Время запуска** — первое подключение к 1С занимает 30-60 секунд -- **Fuzzy matching** — все поиски по имени: точное совпадение → начало строки → вхождение -- **Clipboard paste** — все поля заполняются через Ctrl+V (единственный способ корректно триггерить события 1С) -- **Anti-loop** — если элемент не найден после 2 попыток, навык сообщает что найдено вместо бесконечных ретраев +- **Время запуска** — первое подключение к 1С занимает 30-60 секунд (ожидание встроено в `start`/`run`) +- **Fuzzy matching** — все поиски по имени: точное совпадение → начало строки → вхождение подстроки +- **Clipboard paste** — все текстовые поля заполняются через Ctrl+V (единственный способ корректно триггерить события 1С) +- **Неразрывные пробелы** — 1С использует `\u00a0` вместо обычных пробелов. Внутри API нормализация автоматическая +- **Anti-loop** — если элемент не найден после 2 попыток — остановиться и сообщить что найдено, не зацикливаться ## Связанные навыки