diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index 106b8cb5..4b157073 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -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 нормализация автоматическая ## Связанные навыки