From 2347859bdda9b422015aef0003f468906527d55c Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 5 Apr 2026 14:37:47 +0300 Subject: [PATCH] =?UTF-8?q?docs(web-test):=20=D1=81=D0=BF=D0=B5=D1=86?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20test=20runne?= =?UTF-8?q?r=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B3=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=81=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Единый механизм для внутреннего регресса browser.mjs API и пользовательского регресса 1С-приложений. Паттерны Playwright Test. Содержание: CLI, формат тестов, контексты, хуки, assertions, step(), отчёты (JSON/Allure/JUnit), синтетическая конфигурация, дорожная карта. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/web-test-runner-spec.md | 730 +++++++++++++++++++++++++++++++++++ 1 file changed, 730 insertions(+) create mode 100644 docs/web-test-runner-spec.md diff --git a/docs/web-test-runner-spec.md b/docs/web-test-runner-spec.md new file mode 100644 index 00000000..54538f17 --- /dev/null +++ b/docs/web-test-runner-spec.md @@ -0,0 +1,730 @@ +# web-test runner: спецификация + +Версия: 0.1 (черновик) +Дата: 2026-04-05 + +## Обзор + +Единый механизм регрессионного тестирования веб-клиента 1С. +Два сценария использования, один инструмент: + +1. **Внутренний регресс** -- тестирование API browser.mjs для безопасного рефакторинга +2. **Пользовательский регресс** -- тестирование 1С-приложений (доработанных типовых или разработанных с нуля) + +Принцип: если удобно для пользовательского регресса, подходит и для внутреннего. + +Паттерны следуют конвенциям Playwright Test (обёртки шагов, хуки, утверждения). + +--- + +## 1. Командная строка + +``` +node run.mjs test [url] [флаги] +``` + +| Флаг | По умолчанию | Описание | +|------|-------------|----------| +| `--tags=smoke,crud` | (все) | Фильтр тестов по тегам (пересечение) | +| `--grep=pattern` | (все) | Фильтр тестов по имени (регулярное выражение) | +| `--bail` | false | Остановиться при первом падении | +| `--retry=N` | 0 | Повторить упавшие тесты N раз | +| `--timeout=ms` | 30000 | Таймаут на тест (мс) | +| `--report=path` | (нет) | Записать JSON-отчёт в файл | +| `--format=fmt` | json | Формат отчёта: `json`, `allure`, `junit` | +| `--report-dir=path` | (нет) | Каталог для результатов Allure | +| `--screenshot=strategy` | on-failure | `on-failure` / `every-step` / `off` | +| `--record` | false | Записывать видео для каждого теста | + +URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. + +### Режим выполнения + +In-process (не через HTTP). Раннер: +1. Загружает конфиг (если есть) +2. Обнаруживает файлы `*.test.mjs` +3. Импортирует каждый модуль, извлекает метаданные +4. Фильтрует по тегам/grep/only +5. Группирует по контексту, сортирует по алфавиту внутри группы +6. Подключается к 1С (`browser.connect(url)`) +7. Выполняет тесты последовательно +8. Отключается, выводит результаты + +--- + +## 2. Формат тест-модуля + +Каждый файл `*.test.mjs` -- ES-модуль. + +### Экспорты + +| Экспорт | Тип | Обязателен | По умолчанию | Описание | +|---------|-----|-----------|-------------|----------| +| `name` | `string` | да | -- | Читаемое имя теста | +| `default` | `async function(ctx)` | да | -- | Тело теста | +| `tags` | `string[]` | нет | `[]` | Теги для фильтрации | +| `timeout` | `number` | нет | 30000 | Таймаут теста (мс) | +| `skip` | `boolean \| string` | нет | false | Пропустить тест (строка = причина) | +| `only` | `boolean` | нет | false | Запустить только этот тест (отладка) | +| `context` | `string` | нет | defaultContext | Имя контекста из конфига | +| `contexts` | `string[]` | нет | -- | Мульти-пользовательский процессный тест | +| `params` | `object[]` | нет | -- | Параметризация (будущее) | +| `setup` | `async function(ctx)` | нет | -- | Подготовка перед тестом | +| `teardown` | `async function(ctx)` | нет | -- | Очистка после теста (выполняется всегда) | + +### Пример: тест с одним контекстом + +```js +export const name = 'CRUD справочника Контрагенты'; +export const tags = ['smoke', 'crud', 'catalog']; +export const timeout = 45000; + +export default async function({ navigateSection, openCommand, clickElement, + fillFields, readTable, closeForm, getFormState, assert, step, log }) { + + await step('Открыть список', async () => { + await navigateSection('Склад'); + await openCommand('Контрагенты'); + }); + + await step('Создать элемент', async () => { + await clickElement('Создать'); + await fillFields({ 'Наименование': 'Тест-' + Date.now() }); + await clickElement('Записать и закрыть'); + }); + + await step('Проверить в списке', async () => { + const table = await readTable(); + assert.tableHasRow(table, r => r['Наименование']?.startsWith('Тест-')); + log('Элемент найден в списке'); + }); +} +``` + +### Пример: мульти-контекстный процессный тест + +```js +export const name = 'Согласование приходной накладной'; +export const contexts = ['кладовщик', 'менеджер']; +export const tags = ['process']; + +export default async function({ кладовщик, менеджер, step }) { + + await step('Кладовщик создаёт накладную', async () => { + await кладовщик.navigateSection('Склад'); + await кладовщик.openCommand('Приходные накладные'); + await кладовщик.clickElement('Создать'); + await кладовщик.fillFields({ 'Контрагент': 'ООО Поставщик' }); + await кладовщик.clickElement('Записать'); + }); + + await step('Менеджер утверждает', async () => { + await менеджер.navigateSection('Согласование'); + await менеджер.openCommand('На утверждении'); + await менеджер.clickElement('ООО Поставщик', { dblclick: true }); + await менеджер.clickElement('Утвердить'); + }); +} +``` + +--- + +## 3. Объект контекста + +Каждая тестовая функция получает объект контекста `ctx`: + +### API браузера (все экспорты browser.mjs) + +Все функции обёрнуты авто-обнаружением ошибок (как в `executeScript`): +- При модальной/всплывающей ошибке 1С: скриншот + `fetchErrorStack` + throw +- Обёрнутые ACTION_FNS: `clickElement`, `fillFields`, `fillField`, `selectValue`, + `fillTableRow`, `deleteTableRow`, `openCommand`, `navigateSection`, + `navigateLink`, `openFile`, `closeForm`, `filterList`, `unfilterList` + +Полный список доступных функций: + +**Навигация:** `navigateSection`, `openCommand`, `switchTab`, `navigateLink`, `openFile` +**Состояние:** `getFormState`, `getPageState`, `getSections`, `getCommands` +**Таблицы:** `readTable`, `readSpreadsheet`, `fillTableRow`, `deleteTableRow` +**Поля:** `fillFields`, `fillField`, `selectValue` +**Действия:** `clickElement`, `closeForm`, `filterList`, `unfilterList` +**Запись:** `startRecording`, `stopRecording`, `isRecording`, `addNarration`, `getCaptions` +**Презентация:** `showCaption`, `hideCaption`, `highlight`, `unhighlight`, `showTitleSlide`, `showImage` +**Утилиты:** `screenshot`, `wait`, `getPage`, `getSession` + +### Тестовые утилиты + +- `step(name, fn)` -- обёртка шага (см. раздел 4) +- `assert.*` -- хелперы утверждений (см. раздел 5) +- `log(...args)` -- добавить в вывод теста + +### Мульти-контекст + +При `export const contexts = ['a', 'b']`: +- `ctx.a` и `ctx.b` -- отдельные объекты контекста, каждый с полным API браузера +- `ctx.step` и `ctx.assert` остаются на верхнем уровне + +--- + +## 4. step(name, fn) -- обёртка шага + +```js +await step('Имя шага', async () => { + // тело шага +}); +``` + +Поведение: +- Записывает метку `start` перед `fn()` +- Записывает метку `stop` после `fn()` (успех или ошибка) +- При ошибке: устанавливает `status: 'failed'`, прикрепляет сообщение, пробрасывает исключение +- При успехе: устанавливает `status: 'passed'` +- Если стратегия скриншотов `every-step`: делает скриншот после `fn()` +- Вложенные шаги поддерживаются (шаг внутри шага) +- Напрямую маппится на шаги Allure + +Структура данных шага (для отчётов): + +```js +{ + name: 'Имя шага', + start: 1712345678000, // мс от эпохи + stop: 1712345679200, + status: 'passed' | 'failed', + error: 'сообщение' | undefined, + screenshot: 'путь' | undefined, + steps: [] // вложенные шаги +} +``` + +Реализация (~15 строк): + +```js +async function step(name, fn) { + const s = { name, start: Date.now(), status: 'passed', steps: [] }; + const parent = currentSteps; + parent.push(s); + const prev = currentSteps; + currentSteps = s.steps; + try { + await fn(); + } catch (e) { + s.status = 'failed'; + s.error = e.message; + throw e; + } finally { + s.stop = Date.now(); + currentSteps = prev; + } +} +``` + +--- + +## 5. Утверждения (assertions) + +Простые хелперы утверждений. Без зависимостей. Бросают `AssertionError` со +свойствами `.actual`, `.expected`, `.message`. + +### Общие + +```js +assert.ok(value, msg) // истинность +assert.equal(actual, expected, msg) // === +assert.notEqual(actual, expected, msg) // !== +assert.deepEqual(actual, expected, msg) // сравнение через JSON +assert.includes(haystack, needle, msg) // string/array .includes() +assert.match(string, regex, msg) // проверка регулярным выражением +assert.throws(asyncFn, msg) // ожидает исключение +``` + +### Специфичные для 1С + +```js +assert.formHasField(state, fieldName, msg) +// проверяет наличие state.fields[fieldName] + +assert.formTitle(state, expected, msg) +// проверяет state.title === expected (или includes) + +assert.tableHasRow(table, predicate, msg) +// predicate: объект (частичное совпадение) или функция +// объект: assert.tableHasRow(table, { 'Наименование': 'Тест' }) +// функция: assert.tableHasRow(table, r => r['Сумма'] > 100) + +assert.tableRowCount(table, expected, msg) +// проверяет table.rows.length === expected + +assert.noErrors(state, msg) +// проверяет !state.errors +``` + +--- + +## 6. Хуки + +Все хуки определяются в `_hooks.mjs` в корне каталога тестов. + +### Два уровня + +**Инфраструктурный уровень** (без браузера): +- `prepare()` -- до подключения (восстановление БД, публикация, загрузка данных) +- `cleanup()` -- после отключения (удаление публикации, очистка) + +**Тестовый уровень** (с контекстом браузера): +- `beforeAll(ctx)` -- после подключения, перед первым тестом +- `afterAll(ctx)` -- после последнего теста, до отключения +- `beforeEach(ctx)` -- перед каждым тестом +- `afterEach(ctx)` -- после каждого теста + +### Порядок выполнения + +``` +prepare() // без браузера + browser.connect(url) + beforeAll(ctx) // браузер готов + beforeEach(ctx) + test.setup(ctx) // подготовка теста + test.default(ctx) // тело теста + test.teardown(ctx) // очистка теста (всегда) + afterEach(ctx) // всегда + [встроенный сброс] // всегда + ...следующий тест... + afterAll(ctx) + browser.disconnect() +cleanup() // без браузера +``` + +### Встроенный сброс состояния + +После каждого теста (после `afterEach`) раннер гарантирует чистое состояние: + +```js +await dismissPendingErrors(); +while (есть открытые формы) { + await closeForm({ save: false }); +} +``` + +Это гарантирует, что каждый тест стартует с чистого рабочего стола, +независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). + +### Пример _hooks.mjs + +```js +import { execSync } from 'child_process'; + +export async function prepare() { + execSync('powershell.exe -File scripts/restore-db.ps1'); + execSync('powershell.exe -File scripts/publish.ps1'); +} + +export async function cleanup() { + execSync('powershell.exe -File scripts/unpublish.ps1'); +} + +export async function beforeAll({ navigateSection }) { + await navigateSection('Склад'); +} + +export async function afterEach({ closeForm }) { + // пользовательская очистка после теста (необязательно, встроенный сброс тоже сработает) +} +``` + +--- + +## 7. Файл конфигурации + +`webtest.config.mjs` в корне каталога тестов. Необязателен -- если отсутствует, +URL должен быть передан через CLI. + +```js +export default { + // Контексты: именованные URL для разных пользователей/ролей + contexts: { + кладовщик: { url: 'http://localhost/app-clerk/ru_RU' }, + менеджер: { url: 'http://localhost/app-manager/ru_RU' }, + админ: { url: 'http://localhost/app-admin/ru_RU' }, + }, + defaultContext: 'кладовщик', + + // Значения по умолчанию (переопределяются флагами CLI) + timeout: 30000, + retries: 0, + screenshot: 'on-failure', // 'every-step' | 'off' + record: false, +}; +``` + +**Упрощённая форма** (один контекст, без именованных): + +```js +export default { + url: 'http://localhost/app/ru_RU', + timeout: 30000, +}; +``` + +Флаги CLI всегда переопределяют значения конфига. + +--- + +## 8. Контексты + +### Одиночный контекст (по умолчанию) + +Большинство тестов. Один браузер, один пользователь. Тест получает плоский контекст со всем API. + +```js +export const context = 'кладовщик'; // необязательно, используется defaultContext +export default async function({ clickElement, fillFields, ... }) { } +``` + +### Группировка по контексту + +Раннер группирует тесты по значению `context`, минимизирует переподключения: +1. Собрать все тесты, сгруппировать по имени контекста +2. Для каждой группы: подключиться -> выполнить тесты -> отключиться +3. Внутри группы тесты выполняются по алфавиту + +### Мульти-контекст (процессные тесты) + +```js +export const contexts = ['кладовщик', 'менеджер']; +export default async function({ кладовщик, менеджер, step, assert }) { } +``` + +Каждый именованный контекст -- полноценный объект API. Тест оркестрирует переключение. + +**Этапы реализации:** +- Этап 1: последовательное переподключение (отключиться от одного URL, подключиться к другому) +- Этап 2: параллельные браузеры (после рефакторинга browser.mjs в `createContext()`) + +--- + +## 9. Отчёты + +### JSON (нативный, по умолчанию) + +```json +{ + "runner": "web-test", + "url": "http://localhost/app/ru_RU", + "startedAt": "2026-04-05T10:00:00.000Z", + "finishedAt": "2026-04-05T10:05:30.000Z", + "duration": 330.0, + "summary": { + "total": 25, + "passed": 23, + "failed": 1, + "skipped": 1 + }, + "tests": [ + { + "name": "CRUD справочника Контрагенты", + "file": "02-catalog-crud.test.mjs", + "tags": ["smoke", "crud"], + "context": "кладовщик", + "status": "passed", + "duration": 12.3, + "attempts": 1, + "steps": [ + { + "name": "Открыть список", + "start": 1712345678000, + "stop": 1712345679200, + "status": "passed", + "steps": [] + } + ], + "output": "Элемент найден в списке", + "error": null, + "screenshot": null + }, + { + "name": "Обязательное поле", + "file": "10-validation.test.mjs", + "tags": ["validation"], + "context": "кладовщик", + "status": "failed", + "duration": 8.1, + "attempts": 2, + "steps": [ + { + "name": "Сохранить пустую форму", + "start": 1712345700000, + "stop": 1712345708100, + "status": "failed", + "error": "Ожидалось модальное окно ошибки, но форма сохранилась" + } + ], + "output": "", + "error": { + "message": "Ожидалось модальное окно ошибки, но форма сохранилась", + "step": "Сохранить пустую форму", + "screenshot": "error-shot-10.png" + }, + "screenshot": "error-shot-10.png" + } + ] +} +``` + +### Allure (`--format=allure --report-dir=allure-results/`) + +Отдельные JSON-файлы для каждого теста в каталоге `allure-results/`: + +```json +{ + "uuid": "сгенерированный-uuid", + "name": "CRUD справочника", + "fullName": "02-catalog-crud.test.mjs", + "status": "passed", + "stage": "finished", + "start": 1712345678000, + "stop": 1712345690300, + "labels": [ + { "name": "tag", "value": "smoke" }, + { "name": "tag", "value": "crud" } + ], + "steps": [ + { + "name": "Открыть список", + "status": "passed", + "start": 1712345678000, + "stop": 1712345679200, + "steps": [] + } + ], + "attachments": [ + { + "name": "Скриншот при падении", + "source": "uuid-attachment.png", + "type": "image/png" + } + ] +} +``` + +Скриншоты/видео копируются в `allure-results/` с уникальными именами. + +### JUnit XML (`--format=junit`) + +```xml + + + + + + + Стек вызовов... + + Скриншот: error-shot-10.png + + + +``` + +--- + +## 10. Консольный вывод + +``` +web-test -- http://localhost/app/ru_RU +Запуск 25 тестов из tests/web-test/ + + ✓ Навигация по разделам (2.1s) + ✓ CRUD справочника Контрагенты (12.3s) + ├ Открыть список (1.2s) + ├ Создать элемент (8.0s) + └ Проверить в списке (3.1s) + ✗ Обязательное поле (8.1s) + ├ Открыть форму (2.0s) + └ ✗ Сохранить пустую форму (6.1s) + Ожидалось модальное окно ошибки, но форма сохранилась + скриншот: error-shot-10.png + ○ Составной тип (skip: не реализовано) + +23 passed, 1 failed, 1 skipped (2m 0.5s) +``` + +Шаги показываются для упавших тестов (всегда) и для успешных (в verbose-режиме). + +--- + +## 11. Скриншоты и видео + +### Стратегия скриншотов + +| Стратегия | Поведение | +|-----------|----------| +| `on-failure` (по умолчанию) | Скриншот при падении теста, прикрепляется к ошибке | +| `every-step` | Скриншот в конце каждого `step()`, плюс при падении | +| `off` | Без автоматических скриншотов | + +Скриншоты сохраняются в каталог отчёта по шаблону `{индекс-теста}-{имя-шага}.png`. + +### Видеозапись + +При включённом `--record`: +- `startRecording()` перед каждым тестом +- `stopRecording()` после каждого теста +- Видео сохраняется как `{индекс-теста}-{имя-теста}.mp4` +- Прикрепляется к отчёту (Allure: вложение видео) + +--- + +## 12. Сброс состояния + +Встроенный механизм, выполняется после `afterEach` (и `teardown`) каждого теста: + +```js +async function resetState(ctx) { + // 1. Убрать все ожидающие диалоги ошибок/всплывающие уведомления + try { await ctx.dismissPendingErrors(); } catch {} + + // 2. Закрыть все открытые формы до рабочего стола + for (let i = 0; i < 10; i++) { + const state = await ctx.getFormState(); + if (!state.form) break; + try { await ctx.closeForm({ save: false }); } catch { break; } + } +} +``` + +Гарантирует, что каждый тест стартует с чистого рабочего стола, +независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). + +--- + +## 13. Параметризация (будущее) + +Формат зарезервирован, реализация отложена. + +```js +export const name = 'Заполнение поля {type}'; +export const params = [ + { type: 'String', field: 'Наименование', value: 'Тест' }, + { type: 'Number', field: 'Цена', value: '100.50' }, + { type: 'Date', field: 'ДатаПоступления', value: '01.01.2024' }, + { type: 'Boolean', field: 'Активен', value: true }, +]; + +export default async function({ fillFields, getFormState, assert }, { type, field, value }) { + await fillFields({ [field]: value }); + const state = await getFormState(); + assert.equal(state.fields[field]?.value, String(value)); +} +``` + +В отчётах каждый набор параметров отображается как отдельный тест: +- "Заполнение поля String" +- "Заполнение поля Number" +- "Заполнение поля Date" +- "Заполнение поля Boolean" + +--- + +## 14. buildContext() -- рефакторинг executeScript + +Извлечь из `executeScript()` в `run.mjs` (строки 104-214): + +**Что извлечь:** +- Сбор всех экспортов `browser.*` в объект +- Обёртка ACTION_FNS авто-обнаружением ошибок (проверка модальных/всплывающих после каждого вызова) +- Захват скриншота до того, как `fetchErrorStack` закроет модальное окно ошибки +- Вызов `fetchErrorStack` для модальных ошибок +- Заглушки `noRecord` для функций записи/озвучки + +**Сигнатура новой функции:** +```js +function buildContext({ noRecord = false } = {}) -> object +``` + +**Использование после рефакторинга:** +- `executeScript()` вызывает `buildContext()` + `new AsyncFunction(...)` (поведение не меняется) +- `cmdTest()` вызывает `buildContext()` + `import()` + `mod.default(ctx)` (новое поведение) + +--- + +## 15. Синтетическая тестовая конфигурация + +### Текущие объекты base-config + +| Объект | Поля | Форма | +|--------|------|-------| +| Справочник Контрагенты | ИНН (String 12), Телефон (String 20) | ФормаЭлемента: 3 поля ввода | +| Справочник Номенклатура | Артикул (String 25), ЕдиницаИзмерения (String 10) | -- | +| Перечисление ВидыНоменклатуры | Товар, Услуга, Работа | -- | +| Документ ПриходнаяНакладная | Контрагент (String); ТЧ Товары (4 колонки) | ФормаДокумента | +| РН ОстаткиТоваров | Изм: Номенклатура; Рес: Количество, Сумма | -- | +| РС КурсыВалют | Изм: Валюта; Рес: Курс, Кратность | -- | +| Константа ОсновнаяВалюта | String 10 | -- | +| Отчёт ОстаткиТоваров | Схема СКД | -- | +| Подсистема Склад | все объекты | -- | +| Роль Кладовщик | права Read/View | -- | + +### Что нужно добавить + +| Изменение | Зачем (какой API тестируем) | +|-----------|---------------------------| +| Номенклатура: +Цена (Number 15.2) | fillFields -- число | +| Номенклатура: +Активен (Boolean) | fillFields -- флажок | +| Номенклатура: +ВидНоменклатуры (EnumRef) | fillFields -- ссылка на перечисление | +| Номенклатура: +ДатаПоступления (Date) | fillFields -- дата | +| Номенклатура: +Комментарий (String неограниченная) | fillFields -- многострочный текст | +| Номенклатура: FillChecking на Наименование | Тест ошибки валидации | +| Номенклатура: hierarchical=true | clickElement expand/collapse | +| Номенклатура: Форма с 2 вкладками (Основное / Дополнительно) | switchTab | +| ПриходнаяНакладная.Контрагент -> CatalogRef.Контрагенты | selectValue (ссылочное поле) | +| +Подсистема Администрирование (КурсыВалют, ОсновнаяВалюта) | navigateSection между разделами | +| Роль: полные права (не только Read/View) | CRUD без ограничений | + +### Способ сборки + +Интеграционный тест `build-webtest-config.test.mjs` собирает конфигурацию через +пайплайн навыков (cf-init -> meta-compile -> form-compile -> ...). +Результат кэшируется в `.cache/webtest-config/`. +Первый запуск требует: загрузку в 1С (`db-load-xml`) + веб-публикацию (`web-publish`). + +--- + +## 16. Каталог тест-кейсов + +Расположение: `tests/web-test/` + +| # | Файл | Теги | Покрытие API | +|---|------|------|-------------| +| 01 | navigation.test.mjs | nav, smoke | navigateSection, getPageState, getSections, getCommands | +| 02 | catalog-crud.test.mjs | crud, catalog, smoke | openCommand, fillFields, clickElement, closeForm, readTable, getFormState | +| 03 | field-types.test.mjs | fields | fillFields (строка, число, дата, булево, перечисление) на Номенклатуре | +| 04 | reference-field.test.mjs | fields, select | selectValue на ПриходнаяНакладная.Контрагент | +| 05 | table-operations.test.mjs | table, smoke | readTable, fillTableRow, deleteTableRow | +| 06 | document-workflow.test.mjs | doc, smoke | Создание документа, заполнение шапки + ТЧ, проведение, отмена | +| 07 | tabs.test.mjs | tabs | switchTab на форме Номенклатуры | +| 08 | hierarchy.test.mjs | hierarchy | clickElement с expand/collapse на Номенклатуре | +| 09 | filter-list.test.mjs | filter | filterList, unfilterList, расширенный фильтр по полю | +| 10 | validation.test.mjs | validation | Ошибка обязательного поля, подтверждение при закрытии | +| 11 | report.test.mjs | report | Открыть отчёт, задать параметры, сформировать, readSpreadsheet | +| 12 | form-state.test.mjs | state | getFormState: поля, кнопки, таблицы | +| 13 | screenshots.test.mjs | util | screenshot(), wait() | + +~30 тест-кейсов, покрывающих все основные области API browser.mjs. + +--- + +## 17. Дорожная карта реализации + +| # | Задача | Результат | Зависимости | +|---|--------|-----------|-------------| +| 1 | Архитектурная спецификация | `docs/web-test-runner-spec.md` (этот файл) | -- | +| 2 | Рефакторинг buildContext() | run.mjs: извлечение из executeScript | спека | +| 3 | Ядро cmdTest() | run.mjs: обнаружение, импорт, выполнение, консольный вывод, JSON-отчёт | #2 | +| 4 | Утверждения + обёртка step() | run.mjs: assert.*, step(name, fn) | #3 | +| 5 | Хуки (prepare/cleanup + before/after) | run.mjs: поддержка _hooks.mjs | #3 | +| 6 | Файл конфигурации + контексты | run.mjs: webtest.config.mjs, маршрутизация контекстов | #3 | +| 7 | Форматы отчётов (Allure, JUnit) | run.mjs: --format=allure/junit | #3 | +| 8 | Синтетическая конфигурация | integration/build-webtest-config.test.mjs | спека | +| 9 | Smoke-тесты (01-06) | tests/web-test/01-06*.test.mjs | #3, #8 | +| 10 | Остальные тесты (07-13) | tests/web-test/07-13*.test.mjs | #9 |