# Тестирование через веб-клиент 1С Навык `/web-test` автоматизирует действия в веб-клиенте 1С через Playwright — навигация по разделам, заполнение форм, чтение таблиц и отчётов, фильтрация списков. Замыкает цикл: правка исходников → загрузка → обновление → публикация → **автоматическое тестирование**. ## Навык | Навык | Скрипт | Описание | |-------|:------:|----------| | `/web-test` | `.mjs` (Node.js) | Автоматизация 1С через браузер — навигация, формы, таблицы, отчёты | ## Предусловия - База опубликована через Apache (`/web-publish`) - Node.js 18+ установлен - Зависимости установлены: `cd .claude/skills/web-test/scripts && npm install` ## Рабочий цикл ``` /web-publish → /web-test → результат ↑ | └── правки → /db-load-xml → /db-update ──┘ ``` ## Режимы работы ### Автономный режим (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: ```json { "ok": true, "output": "Columns: [...]\nFirst 5 rows: [...]", "elapsed": 12.3 } ``` При ошибке — автоматический скриншот: ```json { "ok": false, "error": "Element not found: Созздать", "screenshot": "error-shot.png", "elapsed": 5.1 } ``` ### Интерактивный режим (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 ``` **Когда использовать**: пошаговое исследование интерфейса, отладка сценария, когда нужно видеть состояние формы между шагами. Ключевое отличие: `start` запускает HTTP-сервер в фоне, `exec` отправляет скрипты через HTTP POST. Состояние браузера (открытые вкладки, формы) сохраняется между вызовами `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 // === Сценарий: сравнение остатков по двум складам === // 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 секунд (ожидание встроено в `start`/`run`) - **Fuzzy matching** — все поиски по имени: точное совпадение → начало строки → вхождение подстроки - **Clipboard paste** — все текстовые поля заполняются через Ctrl+V (единственный способ корректно триггерить события 1С) - **Неразрывные пробелы** — 1С использует `\u00a0` вместо обычных пробелов. Внутри API нормализация автоматическая - **Anti-loop** — если элемент не найден после 2 попыток — остановиться и сообщить что найдено, не зацикливаться ## Связанные навыки - [Веб-публикация](web-guide.md) — `/web-publish`, `/web-info`, `/web-stop`, `/web-unpublish` - [Базы данных](db-guide.md) — `/db-load-xml`, `/db-update`, `/db-run` - [Расширения](cfe-guide.md) — `/cfe-init`, `/cfe-borrow`, `/cfe-patch-method`