mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
docs(web-test): restructure guide — scenarios first, then technical details
User-facing examples and use cases at the top (navigation, search, documents, reports, extensions, debugging), followed by modes explanation, autonomous scenario example, then API reference tables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+191
-399
@@ -22,471 +22,263 @@
|
||||
└── правки → /db-load-xml → /db-update ──┘
|
||||
```
|
||||
|
||||
## Сценарии использования
|
||||
|
||||
### Навигация и чтение данных
|
||||
|
||||
```
|
||||
> Открой базу erp в браузере, перейди в раздел Склад и покажи какие команды там есть
|
||||
```
|
||||
|
||||
Claude откроет браузер, перейдёт в раздел и покажет список команд.
|
||||
|
||||
```
|
||||
> Открой список поступлений товаров и покажи первые 10 строк
|
||||
```
|
||||
|
||||
Claude откроет список и прочитает таблицу.
|
||||
|
||||
### Поиск и открытие элементов
|
||||
|
||||
```
|
||||
> Найди в списке номенклатуры товар "Вентилятор" и открой его карточку
|
||||
```
|
||||
|
||||
Claude отфильтрует список, откроет найденный элемент двойным кликом и прочитает реквизиты формы.
|
||||
|
||||
```
|
||||
> Открой справочник Контрагенты и найди "Торговый дом"
|
||||
```
|
||||
|
||||
Claude может работать с иерархическими справочниками — поиск автоматически сглаживает иерархию.
|
||||
|
||||
### Создание документа
|
||||
|
||||
```
|
||||
> Создай заказ клиента: организация "Андромеда Плюс", контрагент "Торговый дом Комплексный",
|
||||
> добавь строку: номенклатура "Вентилятор", количество 5
|
||||
```
|
||||
|
||||
Claude откроет форму создания, заполнит шапку и добавит строку в табличную часть.
|
||||
|
||||
### Работа с отчётами
|
||||
|
||||
```
|
||||
> Открой отчёт "Остатки и доступность товаров",
|
||||
> установи отбор Склад = "Склад бытовой техники", сформируй и прочитай результат
|
||||
```
|
||||
|
||||
Claude заполнит фильтры отчёта по человекочитаемым именам (не надо знать технические имена DCS), нажмёт "Сформировать" и прочитает структурированные данные: заголовки, строки, итоги.
|
||||
|
||||
### Сравнение данных
|
||||
|
||||
```
|
||||
> Сформируй отчёт по остаткам для "Склад бытовой техники" и "Западный склад",
|
||||
> сравни итоги по доступным товарам
|
||||
```
|
||||
|
||||
Claude напишет сценарий, который сформирует отчёт дважды с разными фильтрами и сравнит результаты.
|
||||
|
||||
### Проверка после загрузки расширения
|
||||
|
||||
```
|
||||
> Загрузи расширение ТестОшибки и проверь через браузер, что при создании заказа клиента
|
||||
> появляется ошибка "Тестовая ошибка из расширения"
|
||||
```
|
||||
|
||||
Claude загрузит расширение через `/db-load-xml`, затем через `/web-test` откроет форму и проверит ожидаемое поведение.
|
||||
|
||||
### Пошаговая отладка
|
||||
|
||||
```
|
||||
> Запусти браузер на базе erp
|
||||
> Перейди в раздел Продажи
|
||||
> Посмотри что на форме
|
||||
> Открой первый заказ
|
||||
> Какие реквизиты заполнены?
|
||||
```
|
||||
|
||||
Claude будет выполнять команды по одной, показывая состояние формы между шагами.
|
||||
|
||||
## Режимы работы
|
||||
|
||||
### Автономный режим (run)
|
||||
|
||||
Одна команда: открывает браузер → логинится → выполняет сценарий → закрывает браузер → завершает процесс.
|
||||
Одна команда: открывает браузер → логинится → выполняет сценарий → закрывает браузер → завершает процесс. Не оставляет висящих процессов.
|
||||
|
||||
```bash
|
||||
RUN=".claude/skills/web-test/scripts/run.mjs"
|
||||
node $RUN run http://localhost:8081/erp scenario.js
|
||||
```
|
||||
|
||||
**Когда использовать**: готовый сценарий, который нужно выполнить целиком. Идеален для субагентов и CI — не оставляет висящих процессов.
|
||||
|
||||
Claude пишет `.js` файл со сценарием, запускает его и получает результат. Все функции API доступны как глобальные переменные, `console.log()` выводит данные в ответ.
|
||||
|
||||
Можно также передать скрипт через 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
|
||||
```
|
||||
|
||||
Ответ — JSON:
|
||||
Claude пишет `.js` файл со сценарием и запускает его. Ответ — JSON:
|
||||
```json
|
||||
{ "ok": true, "output": "Columns: [...]\nFirst 5 rows: [...]", "elapsed": 12.3 }
|
||||
{ "ok": true, "output": "...console.log output...", "elapsed": 12.3 }
|
||||
```
|
||||
|
||||
При ошибке — автоматический скриншот:
|
||||
```json
|
||||
{ "ok": false, "error": "Element not found: Созздать", "screenshot": "error-shot.png", "elapsed": 5.1 }
|
||||
{ "ok": false, "error": "Element not found", "screenshot": "error-shot.png" }
|
||||
```
|
||||
|
||||
### Интерактивный режим (start/exec/stop)
|
||||
|
||||
Браузер остаётся открытым между командами. Удобно для пошаговой отладки и исследования интерфейса.
|
||||
Браузер остаётся открытым между командами. Состояние (открытые вкладки, формы) сохраняется.
|
||||
|
||||
```bash
|
||||
# 1. Запустить сессию (фоновый процесс с HTTP-сервером)
|
||||
node $RUN start http://localhost:8081/erp
|
||||
|
||||
# 2. Выполнять команды по очереди
|
||||
cat <<'SCRIPT' | node $RUN exec -
|
||||
node $RUN start http://localhost:8081/erp # запустить сессию (фоновый процесс)
|
||||
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
|
||||
node $RUN shot current-state.png # скриншот
|
||||
node $RUN stop # завершить сессию
|
||||
```
|
||||
|
||||
**Когда использовать**: пошаговое исследование интерфейса, отладка сценария, когда нужно видеть состояние формы между шагами.
|
||||
### Когда какой
|
||||
|
||||
Ключевое отличие: `start` запускает HTTP-сервер в фоне, `exec` отправляет скрипты через HTTP POST. Состояние браузера (открытые вкладки, формы) сохраняется между вызовами `exec`.
|
||||
| Режим | Когда использовать |
|
||||
|-------|-------------------|
|
||||
| Автономный (`run`) | Готовый сценарий целиком, субагенты, CI |
|
||||
| Интерактивный (`start/exec`) | Пошаговое исследование, отладка, разговор с пользователем |
|
||||
|
||||
## API
|
||||
## Пример: автономный сценарий
|
||||
|
||||
### Навигация
|
||||
|
||||
#### `navigateSection(name)` → `{ navigated, sections, commands }`
|
||||
|
||||
Переход в раздел верхнего уровня (Продажи, Склад и доставка, Закупки, ...). Fuzzy match по имени.
|
||||
Сравнение остатков по двум складам — один файл, один запуск:
|
||||
|
||||
```js
|
||||
const r = await navigateSection('Склад');
|
||||
// r.sections = [{ name: 'Главное', active: false }, { name: 'Склад и доставка', active: true }, ...]
|
||||
// r.commands = ['Отчеты по складу', 'Поступление товаров и услуг', ...]
|
||||
```
|
||||
|
||||
Возвращает список команд текущего раздела — используйте для выбора следующего действия.
|
||||
|
||||
#### `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": "Организация", "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
|
||||
// === Сценарий: сравнение остатков по двум складам ===
|
||||
// scenario-compare-stocks.js
|
||||
|
||||
// 1. Открыть отчёт
|
||||
await navigateSection('Склад и доставка');
|
||||
await openCommand('Отчеты по складу');
|
||||
await clickElement('Остатки и доступность товаров', { dblclick: true });
|
||||
|
||||
// 2. Первый склад — "Склад бытовой техники"
|
||||
// 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['Доступно']);
|
||||
console.log('Строк:', report1.data?.length, '| Доступно:', report1.totals?.['Доступно']);
|
||||
|
||||
// 3. Второй склад — "Западный склад"
|
||||
// 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['Доступно']);
|
||||
console.log('=== Западный склад ===');
|
||||
console.log('Строк:', report2.data?.length, '| Доступно:', 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)}`);
|
||||
const diff = parse(report1.totals?.['Доступно']) - parse(report2.totals?.['Доступно']);
|
||||
console.log('Разница:', diff.toFixed(0));
|
||||
|
||||
// 5. Закрыть отчёт
|
||||
await closeForm({ save: false });
|
||||
console.log('done');
|
||||
```
|
||||
|
||||
Запуск:
|
||||
```bash
|
||||
node $RUN run http://localhost:8081/erp scenario-compare-stocks.js
|
||||
```
|
||||
Запуск: `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
|
||||
}
|
||||
```
|
||||
## API
|
||||
|
||||
## Типичные ошибки и решения
|
||||
Все функции доступны как глобальные переменные в скриптах. `console.log()` выводит данные в ответ.
|
||||
|
||||
| Проблема | Причина | Решение |
|
||||
|----------|---------|---------|
|
||||
| `no_form` | Форма не открыта или не загрузилась | Добавьте `await wait(2)` после навигации |
|
||||
| `not_found` для элемента | Fuzzy match не нашёл | Проверьте точное имя через `getFormState()` |
|
||||
| Зацикливание поиска | Элемент не существует в базе | Навык остановится после 2 попыток — прочитайте что есть через `readTable()` |
|
||||
| Пустой `readSpreadsheet()` | Отчёт не успел сформироваться | Увеличьте `await wait(N)` перед чтением |
|
||||
| Clipboard paste не работает | Фокус не на поле ввода | `clickElement` перед `fillFields` на нужной области |
|
||||
### Навигация
|
||||
|
||||
| Функция | Описание | Возвращает |
|
||||
|---------|----------|------------|
|
||||
| `navigateSection(name)` | Перейти в раздел (fuzzy match) | `{ sections, commands }` |
|
||||
| `openCommand(name)` | Открыть команду из панели функций | form state |
|
||||
| `navigateLink(path)` | Открыть по пути метаданных (`Документ.ЗаказКлиента`) | form state |
|
||||
| `switchTab(name)` | Переключить открытую вкладку | form state |
|
||||
|
||||
### Чтение
|
||||
|
||||
| Функция | Описание | Возвращает |
|
||||
|---------|----------|------------|
|
||||
| `getFormState()` | Структура формы: поля, кнопки, таблица, фильтры | `{ fields, buttons, tabs, table, filters, reportSettings? }` |
|
||||
| `readTable({ maxRows?, offset? })` | Данные таблицы с пагинацией | `{ columns, rows: [{col: val}], total }` |
|
||||
| `readSpreadsheet()` | Результат отчёта | `{ title?, headers?, data?, totals?, total }` |
|
||||
| `getSections()` | Разделы и команды | `{ activeSection, sections, commands }` |
|
||||
| `getPageState()` | Разделы + открытые вкладки | `{ activeSection, activeTab, sections, tabs }` |
|
||||
|
||||
#### getFormState — подробнее
|
||||
|
||||
Основной способ «увидеть» что на экране:
|
||||
|
||||
- **fields** — `[{ name, value, label?, actions?, required? }]`. `actions` = select/clear/open. `required: true` = незаполненное обязательное поле
|
||||
- **table** — `{ name, columns, rowCount }` (метаданные; для данных — `readTable()`)
|
||||
- **reportSettings** — DCS-фильтры в читаемом виде: `[{ name: "Склад", enabled: true, value: "..." }]`
|
||||
- **errorModal** — 1С показала ошибку
|
||||
- **confirmation** — диалог Да/Нет, вызовите `clickElement('Да')` или `clickElement('Нет')`
|
||||
|
||||
#### readTable — подробнее
|
||||
|
||||
Каждая строка — объект `{ columnName: value }`. Специальные поля для иерархии и дерева:
|
||||
|
||||
- `_kind: 'group'` — группа в иерархическом списке
|
||||
- `_tree: 'expanded'|'collapsed'` — состояние узла дерева
|
||||
- `_level: N` — уровень вложенности
|
||||
- На объекте результата: `hierarchical: true`, `viewMode: 'tree'`
|
||||
|
||||
### Действия
|
||||
|
||||
| Функция | Описание | Возвращает |
|
||||
|---------|----------|------------|
|
||||
| `clickElement(text, {dblclick?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия из списка | form state или `{ submenu }` |
|
||||
| `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры) | `{ filled: [{field, ok, method}], form }` |
|
||||
| `selectValue(field, search)` | Выбрать из справочника (dropdown или форма подбора) | form state с `selected` |
|
||||
| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку таблицы. `add: true` = новая, `row: N` = редактирование | form state |
|
||||
| `deleteTableRow(row, {tab?})` | Удалить строку по индексу | form state |
|
||||
| `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да" | form state |
|
||||
| `filterList(text, {field?, exact?})` | Фильтр списка. Без field = все колонки, с field = расширенный поиск | form state |
|
||||
| `unfilterList({field?})` | Снять фильтры (все или конкретный) | form state |
|
||||
|
||||
#### fillFields — типы полей
|
||||
|
||||
| Значение | Тип поля | Метод |
|
||||
|----------|---------|--------|
|
||||
| `'Андромеда Плюс'` | Ссылочное | clipboard paste + typeahead |
|
||||
| `'5000'` | Текст | clipboard paste |
|
||||
| `'true'` / `'да'` | Чекбокс | toggle |
|
||||
| `'Оплата поставщику'` | Радио | fuzzy match по меткам |
|
||||
| `'Склад бытовой техники'` (DCS) | Фильтр отчёта | авто-включение чекбокса + заполнение |
|
||||
|
||||
### Утилиты
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `screenshot()` | Скриншот (PNG Buffer) |
|
||||
| `wait(seconds)` | Пауза, возвращает form state |
|
||||
| `getPage()` | Сырой Playwright Page для горячих клавиш и нестандартных операций |
|
||||
|
||||
## Клавиатурные сочетания
|
||||
|
||||
Полезные горячие клавиши 1С, доступные через `getPage().keyboard.press()`:
|
||||
Через `getPage().keyboard.press()`:
|
||||
|
||||
| Клавиша | Контекст | Действие |
|
||||
|---------|----------|----------|
|
||||
| `F8` | Ссылочное поле в фокусе | Создать новый элемент справочника |
|
||||
| `Shift+F4` | Ссылочное поле в фокусе | Очистить значение поля |
|
||||
| `F4` | Ссылочное поле в фокусе | Открыть форму выбора |
|
||||
| `Alt+F` | Форма списка/таблицы | Расширенный поиск |
|
||||
| `Escape` | Любая форма | Закрыть текущую форму |
|
||||
| `F8` | Ссылочное поле | Создать новый элемент |
|
||||
| `Shift+F4` | Ссылочное поле | Очистить значение |
|
||||
| `F4` | Ссылочное поле | Форма выбора |
|
||||
| `Alt+F` | Список/таблица | Расширенный поиск |
|
||||
|
||||
## Типичные ошибки
|
||||
|
||||
| Проблема | Решение |
|
||||
|----------|---------|
|
||||
| `no_form` — форма не открыта | Добавьте `await wait(2)` после навигации |
|
||||
| `not_found` — элемент не найден | Проверьте имя через `getFormState()` |
|
||||
| Зацикливание поиска | Элемент не существует — навык остановится после 2 попыток |
|
||||
| Пустой `readSpreadsheet()` | Увеличьте `await wait(N)` перед чтением |
|
||||
|
||||
## Особенности
|
||||
|
||||
- **Headed mode** — 1С требует видимый браузер, headless не поддерживается
|
||||
- **Время запуска** — первое подключение к 1С занимает 30-60 секунд (ожидание встроено в `start`/`run`)
|
||||
- **Fuzzy matching** — все поиски по имени: точное совпадение → начало строки → вхождение подстроки
|
||||
- **Clipboard paste** — все текстовые поля заполняются через Ctrl+V (единственный способ корректно триггерить события 1С)
|
||||
- **Неразрывные пробелы** — 1С использует `\u00a0` вместо обычных пробелов. Внутри API нормализация автоматическая
|
||||
- **Anti-loop** — если элемент не найден после 2 попыток — остановиться и сообщить что найдено, не зацикливаться
|
||||
- **Время запуска** — первое подключение к 1С занимает 30-60 секунд (ожидание встроено)
|
||||
- **Fuzzy matching** — все поиски: точное совпадение → начало строки → вхождение
|
||||
- **Clipboard paste** — поля заполняются через Ctrl+V (корректно триггерит события 1С)
|
||||
- **Неразрывные пробелы** — 1С использует `\u00a0`, внутри API нормализация автоматическая
|
||||
|
||||
## Связанные навыки
|
||||
|
||||
|
||||
Reference in New Issue
Block a user