- 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 <noreply@anthropic.com>
25 KiB
Тестирование через веб-клиент 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)
Одна команда: открывает браузер → логинится → выполняет сценарий → закрывает браузер → завершает процесс.
RUN=".claude/skills/web-test/scripts/run.mjs"
node $RUN run http://localhost:8081/erp scenario.js
Когда использовать: готовый сценарий, который нужно выполнить целиком. Идеален для субагентов и CI — не оставляет висящих процессов.
Claude пишет .js файл со сценарием, запускает его и получает результат. Все функции API доступны как глобальные переменные, console.log() выводит данные в ответ.
Можно также передать скрипт через stdin:
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:
{ "ok": true, "output": "Columns: [...]\nFirst 5 rows: [...]", "elapsed": 12.3 }
При ошибке — автоматический скриншот:
{ "ok": false, "error": "Element not found: Созздать", "screenshot": "error-shot.png", "elapsed": 5.1 }
Интерактивный режим (start/exec/stop)
Браузер остаётся открытым между командами. Удобно для пошаговой отладки и исследования интерфейса.
# 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 по имени.
const r = await navigateSection('Склад');
// r.sections = [{ name: 'Главное', active: false }, { name: 'Склад и доставка', active: true }, ...]
// r.commands = ['Отчеты по складу', 'Поступление товаров и услуг', ...]
Возвращает список команд текущего раздела — используйте для выбора следующего действия.
openCommand(name) → form state
Открытие команды из панели функций. Возвращает состояние открывшейся формы (поля, таблица, кнопки).
const form = await openCommand('Заказы клиентов');
// form.table = { name: 'Список', columns: ['Номер', 'Дата', 'Статус', ...], rowCount: 20 }
// form.buttons = ['Создать', 'Найти', 'Ещё', ...]
// form.filters = [{ name: 'Организация', active: false }, ...]
navigateLink(path) → form state
Открытие объекта по пути метаданных через диалог Shift+F11. Обходит навигацию по разделам — полезно для регистров, журналов и любых форм с известным путём.
await navigateLink('Документ.ЗаказКлиента'); // список документов
await navigateLink('Справочник.Номенклатура'); // справочник
await navigateLink('РегистрНакопления.ТоварыНаСкладах'); // регистр
switchTab(name) → form state
Переключение между уже открытыми вкладками. Fuzzy match.
await switchTab('Заказы клиентов'); // вернуться на открытую ранее вкладку
Чтение состояния
getFormState() → { fields, buttons, tabs, table, filters, reportSettings? }
Основной способ «увидеть» что на экране. Возвращает структуру текущей формы.
const form = await getFormState();
fields — массив полей формы:
[
{ "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 — метаданные таблицы (не данные!):
{ "name": "Товары", "columns": ["Номенклатура", "Количество", "Цена", "Сумма"], "rowCount": 3 }
Для чтения строк таблицы вызовите readTable().
reportSettings — фильтры DCS-отчётов в читаемом виде:
[
{ "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 }.
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
Пагинация — для больших списков:
const page1 = await readTable({ maxRows: 50 }); // строки 0-49
const page2 = await readTable({ maxRows: 50, offset: 50 }); // строки 50-99
Иерархия и дерево — специальные поля строк:
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) — результат отчёта после нажатия "Сформировать".
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.
Одиночный клик — нажатие кнопки или выбор строки в списке:
await clickElement('Создать'); // кнопка
await clickElement('Товары'); // вкладка формы
await clickElement('0000-000039'); // выбрать строку (НЕ открывает)
Двойной клик — открытие элемента из списка:
await clickElement('0000-000039', { dblclick: true }); // открывает документ
Одиночный клик только выделяет строку. Чтобы открыть — всегда { dblclick: true }.
Подменю — если клик открывает меню, возвращается submenu:
const r = await clickElement('Ещё');
// r.submenu = ['Расширенный поиск', 'Настройки', 'Изменить форму', ...]
await clickElement('Расширенный поиск'); // выбрать пункт
Дерево — клик по иконке раскрывает/сворачивает узел.
fillFields({ name: value }) → { filled, form }
Заполнение полей формы по метке (fuzzy match). Автоматически определяет тип поля.
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-фильтры отчётов — человекочитаемые метки вместо технических имён. Чекбокс «Использование» включается автоматически:
await fillFields({
'Склад': 'Склад бытовой техники', // включит чекбокс + заполнит значение
'Номенклатура': 'Вентилятор' // то же: включит + заполнит
});
selectValue(field, search) → form state с selected
Выбор значения из справочника через выпадающий список или форму подбора. Надёжнее чем fillFields для ссылочных полей, где нужен точный выбор из каталога.
const r = await selectValue('Контрагент', 'Торговый дом');
// r.selected = { field: 'Контрагент', search: 'Торговый дом', method: 'form' }
Обрабатывает три сценария:
- Dropdown — выпадающий список с вариантами → клик по совпадению
- История + "Показать все" — dropdown с историей → переход в форму подбора
- Форма подбора — отдельное окно с поиском → ввод текста + двойной клик
Также поддерживает DCS-метки — автоматически включает чекбокс.
fillTableRow(fields, opts) → form state
Заполнение строки табличной части. Навигация между ячейками через Tab.
// Добавить новую строку:
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 индексу.
await deleteTableRow(0, { tab: 'Товары' });
closeForm({ save? }) → form state
Закрытие текущей формы через Escape.
await closeForm({ save: false }); // закрыть без сохранения (автоматически "Нет")
await closeForm({ save: true }); // закрыть с сохранением (автоматически "Да")
await closeForm(); // если появится диалог — вернёт confirmation
Предпочтительнее чем clickElement('×') — кнопки закрытия на вкладках неоднозначны.
filterList(text, opts?) → form state
Фильтрация списка. Два режима:
// Простой — поиск по всем колонкам:
await filterList('КП00-000018');
// Расширенный — поиск по конкретному полю:
await filterList('Конфетпром', { field: 'Наименование' });
// Точное совпадение:
await filterList('Конфетпром ООО', { field: 'Наименование', exact: true });
Работает с иерархическими списками (справочники с группами) — автоматически переключает в плоский режим для поиска.
unfilterList({ field? }) → form state
Снятие фильтров:
await unfilterList(); // снять все
await unfilterList({ field: 'Наименование' }); // снять конкретный
Утилиты
screenshot() → PNG Buffer
Скриншот текущего состояния браузера.
wait(seconds) → form state
Ожидание N секунд. Возвращает состояние формы после паузы. Используйте для длительных операций (формирование отчёта, проведение документа).
getPage() → Playwright Page
Доступ к сырому объекту Playwright для нестандартных операций:
const page = getPage();
await page.keyboard.press('F8'); // создать новый элемент справочника
await page.keyboard.press('Shift+F4'); // очистить ссылочное поле
Пример: сложный автономный сценарий
Сценарий для субагента: проверить отчёт «Остатки и доступность товаров» по двум складам и сравнить итоги.
// === Сценарий: сравнение остатков по двум складам ===
// 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');
Запуск:
node $RUN run http://localhost:8081/erp scenario-compare-stocks.js
Результат:
{
"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-publish,/web-info,/web-stop,/web-unpublish - Базы данных —
/db-load-xml,/db-update,/db-run - Расширения —
/cfe-init,/cfe-borrow,/cfe-patch-method