Files
cc-1c-skills/docs/web-test-guide.md
T
Nick Shirokov f9439dce6d 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 <noreply@anthropic.com>
2026-02-28 18:05:35 +03:00

25 KiB
Raw Blame History

Тестирование через веб-клиент 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 попыток — остановиться и сообщить что найдено, не зацикливаться

Связанные навыки