Files
cc-1c-skills/docs/web-test-guide.md
T
Nick Shirokov b9a04b235f docs(web-test): document error stack, platformDialogs in guide
Add stack auto-fetch and platformDialogs detection to web-test-guide.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:53:33 +03:00

325 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Тестирование через веб-клиент 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 ──┘
```
## Сценарии использования
### Навигация и чтение данных
```
> Открой базу erp в браузере, перейди в раздел Склад и покажи какие команды там есть
```
Claude откроет браузер, перейдёт в раздел и покажет список команд.
```
> Открой список поступлений товаров и покажи первые 10 строк
```
Claude откроет список и прочитает таблицу.
### Поиск и открытие элементов
```
> Найди в списке номенклатуры товар "Вентилятор" и открой его карточку
```
Claude отфильтрует список, откроет найденный элемент двойным кликом и прочитает реквизиты формы.
```
> Открой справочник Контрагенты и найди "Торговый дом"
```
Claude может работать с иерархическими справочниками — поиск автоматически сглаживает иерархию.
### Создание документа
```
> Создай заказ клиента: организация "Андромеда Плюс", контрагент "Торговый дом Комплексный",
> добавь строку: номенклатура "Вентилятор", количество 5
```
Claude откроет форму создания, заполнит шапку и добавит строку в табличную часть.
### Работа с отчётами
```
> Открой отчёт "Остатки и доступность товаров",
> установи отбор Склад = "Склад бытовой техники", сформируй и прочитай результат
```
Claude заполнит фильтры отчёта по человекочитаемым именам (не надо знать технические имена DCS), нажмёт "Сформировать" и прочитает структурированные данные: заголовки, строки, итоги.
### Сравнение данных
```
> Сформируй отчёт по остаткам для "Склад бытовой техники" и "Западный склад",
> сравни итоги по доступным товарам
```
Claude напишет сценарий, который сформирует отчёт дважды с разными фильтрами и сравнит результаты.
### Проверка после загрузки расширения
```
> Загрузи расширение ТестОшибки и проверь через браузер, что при создании заказа клиента
> появляется ошибка "Тестовая ошибка из расширения"
```
Claude загрузит расширение через `/db-load-xml`, затем через `/web-test` откроет форму и проверит ожидаемое поведение.
### Открытие внешней обработки
```
> Открой обработку build/РедакторДвижений.epf в веб-клиенте и покажи что на форме
```
Claude откроет EPF через Ctrl+O, автоматически обработает диалог безопасности (если есть) и прочитает форму.
### Пошаговая отладка
```
> Запусти браузер на базе erp
> Перейди в раздел Продажи
> Посмотри что на форме
> Открой первый заказ
> Какие реквизиты заполнены?
```
Claude будет выполнять команды по одной, показывая состояние формы между шагами.
## Режимы работы
### Автономный режим (run)
Одна команда: открывает браузер → логинится → выполняет сценарий → закрывает браузер → завершает процесс. Не оставляет висящих процессов.
```bash
RUN=".claude/skills/web-test/scripts/run.mjs"
node $RUN run http://localhost:8081/erp scenario.js
```
Claude пишет `.js` файл со сценарием и запускает его. Ответ — JSON:
```json
{ "ok": true, "output": "...console.log output...", "elapsed": 12.3 }
```
При ошибке — автоматический скриншот (пока модалка ещё видна) и стек вызова:
```json
{ "ok": false, "error": "Тестовая проверка: запись запрещена", "screenshot": "error-shot.png",
"stack": { "raw": "...", "entries": [{"location": "Модуль(4)", "code": "ВызватьИсключение..."}] } }
```
Стек извлекается автоматически — через OpenReport (платформенные исключения) или "О программе" → "Информация для техподдержки" (ВызватьИсключение).
### Интерактивный режим (start/exec/stop)
Браузер остаётся открытым между командами. Состояние (открытые вкладки, формы) сохраняется.
```bash
node $RUN start http://localhost:8081/erp # запустить сессию (фоновый процесс)
cat <<'SCRIPT' | node $RUN exec - # выполнить скрипт
await navigateSection('Продажи');
SCRIPT
node $RUN shot current-state.png # скриншот
node $RUN stop # завершить сессию
```
### Когда какой
| Режим | Когда использовать |
|-------|-------------------|
| Автономный (`run`) | Готовый сценарий целиком, субагенты, CI |
| Интерактивный (`start/exec`) | Пошаговое исследование, отладка, разговор с пользователем |
## Пример: автономный сценарий
Сравнение остатков по двум складам — один файл, один запуск:
```js
// scenario-compare-stocks.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, '| Доступно:', report1.totals?.['Доступно']);
// 3. Второй склад
await fillFields({ 'Склад': 'Западный склад' });
await clickElement('Сформировать');
await wait(5);
const report2 = await readSpreadsheet();
console.log('=== Западный склад ===');
console.log('Строк:', report2.data?.length, '| Доступно:', report2.totals?.['Доступно']);
// 4. Сравнение
const parse = s => parseFloat((s || '0').replace(/\s/g, '').replace(',', '.'));
const diff = parse(report1.totals?.['Доступно']) - parse(report2.totals?.['Доступно']);
console.log('Разница:', diff.toFixed(0));
await closeForm({ save: false });
```
Запуск: `node $RUN run http://localhost:8081/erp scenario-compare-stocks.js`
## API
Все функции доступны как глобальные переменные в скриптах. `console.log()` выводит данные в ответ.
### Навигация
| Функция | Описание | Возвращает |
|---------|----------|------------|
| `navigateSection(name)` | Перейти в раздел (fuzzy match) | `{ sections, commands }` |
| `openCommand(name)` | Открыть команду из панели функций | form state |
| `navigateLink(path)` | Открыть по пути метаданных (`Документ.ЗаказКлиента`) | form state |
| `openFile(path)` | Открыть внешнюю обработку/отчёт (EPF/ERF) через «Файл → Открыть» | form state |
| `switchTab(name)` | Переключить открытую вкладку | form state |
### Чтение
| Функция | Описание | Возвращает |
|---------|----------|------------|
| `getFormState()` | Структура формы: поля, кнопки, таблица, фильтры, состояние окон | `{ form, formCount, openForms, 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 — подробнее
Основной способ «увидеть» что на экране:
- **form** — номер активной формы, `null` когда ничего не открыто (десктоп)
- **formCount** — количество открытых форм. `0` = десктоп. Работает даже если панель открытых окон скрыта
- **openForms** — `[0, 1, 2]` — номера всех открытых форм в DOM
- **modal** — `true` когда активная форма — модальный диалог, блокирующий интерфейс
- **openTabs** — `[{ name, active? }]` из панели открытых окон (только когда панель включена в настройках 1С)
- **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('Нет')`
- **platformDialogs** — `[{ type, title }]` — платформенные диалоги (О программе, Информация для техподдержки). Невидимы для обычного определения форм, но блокируют интерфейс. `closeForm()` закрывает их. Автоочистка через `dismissPendingErrors` перед каждым action
#### 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, opts?)` | Выбрать из справочника. search: текст или `{поле: значение}`. `{ type }` для составного типа | form state с `selected` |
| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка или `{ value, type }` для составного типа | form state |
| `deleteTableRow(row, {tab?})` | Удалить строку по индексу | form state |
| `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да". Возвращает `closed: true/false` | form state с `closed` |
| `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 для горячих клавиш и нестандартных операций |
| `startRecording(path, opts?)` | Начать запись видео (CDP screencast → ffmpeg → MP4) |
| `stopRecording()` | Остановить запись, вернуть `{ file, duration, size }` |
| `showCaption(text, opts?)` | Текстовая подпись поверх страницы (`speech` — текст озвучки) |
| `hideCaption()` | Убрать подпись |
| `showTitleSlide(text, opts?)` | Полноэкранный титульный слайд (`subtitle`, `background`, `speech`) |
| `hideTitleSlide()` | Убрать титульный слайд |
| `showImage(path, opts?)` | Полноэкранное изображение (`style`: blur/dark/light/full, `speech`) |
| `hideImage()` | Убрать изображение |
| `addNarration(videoPath, opts?)` | Озвучка видео по субтитрам (Edge TTS / ElevenLabs / OpenAI) |
| `getCaptions()` | Субтитры из текущей/последней записи |
| `isRecording()` | Идёт ли запись (boolean) |
| `setHighlight(on)` | Включить/выключить авто-выделение элементов при действиях |
| `isHighlightMode()` | Активен ли режим авто-выделения (boolean) |
| `highlight(text)` | Ручное выделение элемента (по имени, fuzzy match) |
| `unhighlight()` | Снять выделение |
## Клавиатурные сочетания
Через `getPage().keyboard.press()`:
| Клавиша | Контекст | Действие |
|---------|----------|----------|
| `F8` | Ссылочное поле | Создать новый элемент |
| `Shift+F4` | Ссылочное поле | Очистить значение |
| `F4` | Ссылочное поле | Форма выбора |
| `Alt+F` | Список/таблица | Расширенный поиск |
## Типичные ошибки
Все функции бросают исключение при ошибке (не возвращают `{ error }`). Сценарий прерывается на проблемном шаге с информативным сообщением. В интерактиве — `try/catch` для обработки.
| Проблема | Решение |
|----------|---------|
| `no form found` — форма не открыта | Добавьте `await wait(2)` после навигации |
| `not found. Available: ...` — элемент не найден | Проверьте имя через `getFormState()`, используйте вариант из Available |
| `fillFields: N of M field(s) failed` | Текст ошибки содержит список проблемных полей и доступные варианты |
| Пустой `readSpreadsheet()` | Увеличьте `await wait(N)` перед чтением |
## Особенности
- **Headed mode** — 1С требует видимый браузер, headless не поддерживается
- **Время запуска** — первое подключение к 1С занимает 30-60 секунд (ожидание встроено)
- **Fuzzy matching** — все поиски: точное совпадение → начало строки → вхождение. Буквы ё и е считаются эквивалентными
- **Clipboard paste** — поля заполняются через Ctrl+V (корректно триггерит события 1С)
- **Неразрывные пробелы** — 1С использует `\u00a0`, внутри API нормализация автоматическая
- **Ошибки** — все функции бросают исключение при ошибке (сценарий прерывается), `try/catch` для обработки
- **Панель разделов** — `navigateSection()` работает при любом расположении панели (сбоку, сверху), но требует режим «Картинка и текст» или «Текст». Режим «Только картинки» не поддерживается — API не может прочитать имена разделов из иконок
## Связанные навыки
- [Запись видеоинструкций](web-test-recording-guide.md) — запись видео, субтитры, подсветка, TTS-озвучка
- [Веб-публикация](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`