From 1eff62de42bc1c85156527977f0b13697ff07066 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 13 May 2026 17:11:51 +0300 Subject: [PATCH] =?UTF-8?q?docs(web-test):=20=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20sync=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B8=20+=20cont?= =?UTF-8?q?exts[]=20=D0=B2=20testResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec.md v0.2 (последний sync 2026-05-13): §1 CLI: добавлены --report-dir и `--` separator в таблицу флагов. §1 «Режим выполнения»: убрана несуществующая «группировка по контексту», описана реальная алфавитная модель + lazy ensureContext. §2 пример multi-context: latin ID контекстов вместо кириллицы (clerk/manager) + showcase closeContext в финальном шаге. §3 список API расширен: контексты (createContext/closeContext/setActive/ listContexts/hasContext/getActiveContext), overlay-helpers (hideTitleSlide/ hideImage/setHighlight/isHighlightMode), error-helpers (dismissPendingErrors/ fetchErrorStack). §6 пример _hooks.mjs: убран mock-навигация в beforeAll, добавлены примеры afterOpenContext/beforeCloseContext, afterEach показывает testResult. §8 переписан раздел «Реализация в browser.mjs» (мульти-контекст уже live) + новая таблица режимов изоляции tab/window. §9 JSON example: поле "context" → "contexts": [...] (массив). §10: убрано упоминание несуществующего verbose-режима. §13 «Параметризация»: убран статус «будущее», описана реальная семантика T6 (template name, param 2-м аргументом, testInfo.param). §14 buildContext: переписан под done-состояние + scoped-вариант. §16 каталог тест-кейсов: 13 → 19 файлов (multi-context, recording, errors-stack, tree-form, misc, hooks). §17 дорожная карта: 10 → 18 пунктов, M4–M8 включены. run.mjs: - testResult получил поле contexts: [...names] во всех ветках (passed/failed/skipped/context-setup-failed). Раннер передаёт declaredContexts из единой точки до if(skip), чтобы skip-результаты тоже несли структурную информацию. Регресс 19/19 ✓ (9m 8.7s) после --rebuild-stand. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/run.mjs | 18 +- docs/web-test-runner-spec.md | 271 +++++++++++++++--------- 2 files changed, 178 insertions(+), 111 deletions(-) diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index 5c94b190..129cfcc3 100644 --- a/.claude/skills/web-test/scripts/run.mjs +++ b/.claude/skills/web-test/scripts/run.mjs @@ -571,25 +571,29 @@ async function cmdTest(rawArgs) { let testIdx = 0; for (const t of filtered) { testIdx++; + // Declared contexts — нужны и в skip-ветке, и в основной, чтобы все + // testResult-записи в отчёте всегда содержали `contexts` поле. + const declaredContexts = t.contexts && t.contexts.length + ? t.contexts + : [t.context || defaultContextName]; + if (t.skip) { const reason = typeof t.skip === 'string' ? t.skip : ''; W.write(` \u25CB ${t.name}${reason ? ` (skip: ${reason})` : ' (skip)'}\n`); - results.push({ name: t.name, file: t.file, tags: t.tags, status: 'skipped', duration: 0, attempts: 0, steps: [], output: '', error: null, screenshot: null }); + results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'skipped', duration: 0, attempts: 0, steps: [], output: '', error: null, screenshot: null }); skipCount++; continue; } // Resolve test's contexts: multi (t.contexts) or single (t.context || default). // Lazy-create them and set active to the primary one. - const testContextNames = t.contexts && t.contexts.length - ? t.contexts - : [t.context || defaultContextName]; + const testContextNames = declaredContexts; try { for (const cn of testContextNames) await ensureContext(cn); await browser.setActiveContext(testContextNames[0]); } catch (e) { W.write(` ✗ ${t.name} (context setup failed: ${e.message})\n`); - results.push({ name: t.name, file: t.file, tags: t.tags, status: 'failed', duration: 0, attempts: 0, steps: [], output: '', error: { message: e.message }, screenshot: null }); + results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'failed', duration: 0, attempts: 0, steps: [], output: '', error: { message: e.message }, screenshot: null }); failCount++; if (opts.bail) break; continue; @@ -697,7 +701,7 @@ async function cmdTest(rawArgs) { try { await browser.stopRecording(); } catch {} } const dur = elapsed(t0); - testResult = { name: t.name, file: t.file, tags: t.tags, status: 'passed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: null, screenshot: null, video: videoFile }; + testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, status: 'passed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: null, screenshot: null, video: videoFile }; lastError = null; break; @@ -731,7 +735,7 @@ async function cmdTest(rawArgs) { } lastError = { message: e.message, step: e.onecError?.step, screenshot: shotFile }; const dur = elapsed(t0); - testResult = { name: t.name, file: t.file, tags: t.tags, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: lastError, screenshot: shotFile, video: videoFile }; + testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: lastError, screenshot: shotFile, video: videoFile }; } } diff --git a/docs/web-test-runner-spec.md b/docs/web-test-runner-spec.md index a8ffd36e..90faee48 100644 --- a/docs/web-test-runner-spec.md +++ b/docs/web-test-runner-spec.md @@ -1,7 +1,7 @@ # web-test runner: спецификация -Версия: 0.1 (черновик) -Дата: 2026-04-05 +Версия: 0.2 +Дата: 2026-05-13 (последний sync) ## Обзор @@ -30,26 +30,29 @@ node run.mjs test [url] [флаги] | `--bail` | false | Остановиться при первом падении | | `--retry=N` | 0 | Повторить упавшие тесты N раз | | `--timeout=ms` | 30000 | Таймаут на тест (мс) | -| `--report=path` | (нет) | Записать JSON-отчёт в файл | -| `--format=fmt` | json | Формат отчёта: `json`, `allure`, `junit` | -| `--report-dir=path` | (нет) | Каталог для результатов Allure | +| `--report=path` | (нет) | Записать JSON-отчёт в файл (или XML для `--format=junit`) | +| `--format=fmt` | json | Формат отчёта: `json` / `allure` / `junit` | +| `--report-dir=path` | dirname(report) / testDir | Каталог для скриншотов, видео, Allure-результатов | | `--screenshot=strategy` | on-failure | `on-failure` / `every-step` / `off` | -| `--record` | false | Записывать видео для каждого теста | +| `--record` | false | Записывать видео для каждого теста (mp4 в `--report-dir`) | +| `-- ` | -- | Всё после `--` пробрасывается в `_hooks.mjs` как `hookArgs` (см. §6.1) | -URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. +URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. CLI URL переопределяет URL дефолтного контекста. ### Режим выполнения In-process (не через HTTP). Раннер: -1. Загружает конфиг (если есть) -2. Обнаруживает файлы `*.test.mjs` -3. Импортирует каждый модуль, извлекает метаданные -4. Фильтрует по тегам/grep/only -5. Группирует по контексту, сортирует по алфавиту внутри группы -6. Запускает браузер (`chromium.launch()`) -7. Создаёт BrowserContext + page для каждого используемого контекста (лениво) -8. Выполняет тесты последовательно, переключая активный контекст -9. Закрывает все контексты и браузер, выводит результаты +1. Загружает конфиг (если есть). +2. Обнаруживает файлы `*.test.mjs`, читает каждый, извлекает метаданные. +3. Фильтрует по `--tags`/`--grep`/`only`. Параметризованные тесты разворачиваются. +4. Запускает браузер и default-контекст (`chromium.launch()` или `launchPersistentContext` + в зависимости от `isolation`). +5. Тесты выполняются последовательно **в алфавитном порядке имён файлов** + (внутри файла — в порядке экспорта). +6. Для каждого теста: лениво создаёт нужные BrowserContext-ы (`ensureContext`), + переключает активный, прогоняет хуки и тело, делает встроенный reset. +7. По завершении: финальный teardown контекстов с `beforeCloseContext`-хуками, + `disconnect()`, `cleanup()`. --- @@ -104,26 +107,33 @@ export default async function({ navigateSection, openCommand, clickElement, ### Пример: мульти-контекстный процессный тест +Рекомендация: латинский ID контекста + кириллический `displayName` в +`webtest.config.mjs.contexts..displayName` (см. §7). + ```js export const name = 'Согласование приходной накладной'; -export const contexts = ['кладовщик', 'менеджер']; +export const contexts = ['clerk', 'manager']; export const tags = ['process']; -export default async function({ кладовщик, менеджер, step }) { +export default async function({ clerk, manager, step }) { await step('Кладовщик создаёт накладную', async () => { - await кладовщик.navigateSection('Склад'); - await кладовщик.openCommand('Приходные накладные'); - await кладовщик.clickElement('Создать'); - await кладовщик.fillFields({ 'Контрагент': 'ООО Поставщик' }); - await кладовщик.clickElement('Записать'); + await clerk.navigateSection('Склад'); + await clerk.openCommand('Приходные накладные'); + await clerk.clickElement('Создать'); + await clerk.fillFields({ 'Контрагент': 'ООО Поставщик' }); + await clerk.clickElement('Записать'); }); await step('Менеджер утверждает', async () => { - await менеджер.navigateSection('Согласование'); - await менеджер.openCommand('На утверждении'); - await менеджер.clickElement('ООО Поставщик', { dblclick: true }); - await менеджер.clickElement('Утвердить'); + await manager.navigateSection('Согласование'); + await manager.openCommand('На утверждении'); + await manager.clickElement('ООО Поставщик', { dblclick: true }); + await manager.clickElement('Утвердить'); + }); + + await step('Освобождаем контекст clerk', async () => { + await manager.closeContext('clerk'); // освободить лицензию 1С }); } ``` @@ -149,8 +159,12 @@ export default async function({ кладовщик, менеджер, step }) { **Таблицы:** `readTable`, `readSpreadsheet`, `fillTableRow`, `deleteTableRow` **Поля:** `fillFields`, `fillField`, `selectValue` **Действия:** `clickElement`, `closeForm`, `filterList`, `unfilterList` +**Ошибки:** `dismissPendingErrors`, `fetchErrorStack` +**Контексты:** `createContext`, `setActiveContext`, `closeContext`, `listContexts`, +`hasContext`, `getActiveContext` **Запись:** `startRecording`, `stopRecording`, `isRecording`, `addNarration`, `getCaptions` -**Презентация:** `showCaption`, `hideCaption`, `highlight`, `unhighlight`, `showTitleSlide`, `showImage` +**Презентация:** `showCaption`, `hideCaption`, `showTitleSlide`, `hideTitleSlide`, +`showImage`, `hideImage`, `highlight`, `unhighlight`, `setHighlight`, `isHighlightMode` **Утилиты:** `screenshot`, `wait`, `getPage`, `getSession` ### Тестовые утилиты @@ -401,12 +415,26 @@ export async function cleanup({ log }) { execSync('powershell.exe -File scripts/unpublish.ps1'); } -export async function beforeAll({ navigateSection }) { - await navigateSection('Склад'); +export async function beforeAll(ctx) { + // По умолчанию 1С после входа уже показывает дефолтную секцию — навигация + // в beforeAll обычно не нужна. Хук удобен для счётчиков, телеметрии, + // общего setup'а который должен случиться один раз для всего прогона. } -export async function afterEach({ closeForm }) { - // пользовательская очистка после теста (необязательно, встроенный сброс тоже сработает) +export async function afterEach(ctx) { + // Доступен ctx.testResult — { status, duration, attempts, error, steps }. + // Встроенный сброс состояния выполняется ПОСЛЕ afterEach автоматически. +} + +export async function afterOpenContext(ctx, name, spec) { + // Контекст name создан. spec — config.contexts[name]. Удобно для + // persistent DOM-overlay'я с displayName (видно в видео какая вкладка к + // какому пользователю относится). +} + +export async function beforeCloseContext(ctx, name, spec) { + // Контекст name вот-вот закроется. Срабатывает и при ctx.closeContext + // из теста, и в финальном teardown раннера. } ``` @@ -508,15 +536,20 @@ export const context = 'кладовщик'; // необязательно, и export default async function({ clickElement, fillFields, ... }) { } ``` -### Группировка по контексту +### Порядок выполнения и переключение контекста -Раннер группирует тесты по значению `context`: -1. Собрать все тесты, определить набор уникальных контекстов -2. Создать BrowserContext + page для каждого используемого контекста -3. Для каждой группы тестов: переключить активный context, выполнить тесты -4. Внутри группы тесты выполняются по алфавиту +Раннер НЕ группирует тесты по контексту. Порядок выполнения — алфавитный +по именам файлов (плюс порядок экспорта внутри файла). Для каждого теста: +1. Через `ensureContext(name)` создаются BrowserContext-ы, упомянутые в + `t.context` / `t.contexts` (если ещё не созданы). +2. `setActiveContext(testContextNames[0])` — активный контекст = первый + объявленный (для single — `t.context || defaultContext`, для multi — + `t.contexts[0]`). +3. После теста встроенный сброс пробегает по всем использованным контекстам. -Контексты создаются лениво (при первом обращении) и живут до конца прогона. +Контексты живут между тестами: переключение через `setActiveContext` — +дешёвое, новый login не требуется. Закрываются явно (`closeContext`) или +финальным teardown'ом перед `disconnect()`. ### Мульти-контекст (процессные тесты) @@ -550,18 +583,34 @@ await step('Кладовщик проверяет статус', async () => { }); ``` -### Влияние на browser.mjs +### Реализация в browser.mjs -Текущий browser.mjs хранит `page`, `browser`, `session` как module-level переменные. -Для мульти-контекста необходимо: -- Уметь создавать несколько `BrowserContext` + `page` в одном `browser` -- Хранить карту контекстов `{ name → { context, page, session } }` -- Переключать текущий `page` при смене активного контекста -- API-функции раб��тают с текущим активным `page` +`browser.mjs` хранит активный слот в module-level `page`/`browser`/`sessionPrefix`/`seanceId`, +зеркалит его из Map `contexts: Map`. Переключение между слотами: +`_saveActiveSlot()` сохраняет module-level → slot, `_activateSlot(name)` +загружает slot → module-level. Это держит API-функции (`clickElement`, +`fillFields` и т.д.) plain — они работают с текущим активным `page`, +не зная про множественность контекстов. -Это промежуточный шаг к полному `createContext()` из Фазы 3 роадмапа, -но значительно проще -- не требует рефакторинга всех функций browser.mjs, -только управление текущим page. +Публичный контекстный API: +- `createContext(name, url, { isolation, extensionPath })` — создаёт BrowserContext + и navigate'ит на URL. +- `setActiveContext(name)` — переключает активный слот, при активной записи + flush'ит хвост старой страницы и переподключает screencast к новой. +- `closeContext(name)` — logout + close (page для `tab`, BrowserContext для + `window`), удаляет из реестра. Throw если `name === active`. +- `listContexts()` / `hasContext(name)` / `getActiveContext()` — read-only. + +### Режимы изоляции + +`isolation` (per-context или config-level): + +| Режим | Реализация | Окна | Cookies | 1С-расширение | +|-------|-----------|------|---------|---------------| +| `'tab'` (default) | `launchPersistentContext` + `newPage()` per context | 1 окно, N вкладок | shared by path | загружается надёжно | +| `'window'` | `chromium.launch()` + `newContext()` per context | N окон | полная изоляция | может не загружаться | + +Смешивать режимы в одном прогоне нельзя — `createContext` бросает явную ошибку. --- @@ -587,7 +636,7 @@ await step('Кладовщик проверяет статус', async () => { "name": "CRUD справочника Контрагенты", "file": "02-catalog-crud.test.mjs", "tags": ["smoke", "crud"], - "context": "кладовщик", + "contexts": ["clerk"], "status": "passed", "duration": 12.3, "attempts": 1, @@ -608,7 +657,7 @@ await step('Кладовщик проверяет статус', async () => { "name": "Обязательное поле", "file": "10-validation.test.mjs", "tags": ["validation"], - "context": "кладовщик", + "contexts": ["clerk"], "status": "failed", "duration": 8.1, "attempts": 2, @@ -711,7 +760,9 @@ web-test -- http://localhost/app/ru_RU 23 passed, 1 failed, 1 skipped (2m 0.5s) ``` -Шаги показываются для упавших тестов (всегда) и для успешных (в verbose-режиме). +Для passed-тестов выводится одна строка `✓ name (duration)`. Шаги печатаются +только для упавших — после строки `✗`, с отступом, плюс сообщение ошибки и +путь к скриншоту. Полная картина по шагам — в JSON-отчёте (`--report=...`). --- @@ -760,9 +811,7 @@ async function resetState(ctx) { --- -## 13. Параметризация (будущее) - -Формат зарезервирован, реализация отложена. +## 13. Параметризация ```js export const name = 'Заполнение поля {type}'; @@ -780,33 +829,33 @@ export default async function({ fillFields, getFormState, assert }, { type, fiel } ``` -В отчётах каждый набор параметров отображается как отдельный тест: -- "Заполнение поля String" -- "Заполнение поля Number" -- "Заполнение поля Date" -- "Заполнение поля Boolean" +Параметры разворачиваются в отдельные тесты на этапе discovery. Имя +формируется подстановкой через шаблон `{key}` в `mod.name`; если шаблона +нет — суффикс `[index]`. Тест получает `param` вторым аргументом +(`default(ctx, param)`). В отчётах каждый набор — отдельная запись со +своим `name` и `param` в testInfo. `ctx.testInfo.param` доступен в теле +теста и хуках. --- -## 14. buildContext() -- рефакторинг executeScript +## 14. buildContext() -Извлечь из `executeScript()` в `run.mjs` (строки 104-214): +Общая фабрика контекста, используется и `executeScript()` (для `exec`/`run`/`start`), +и `cmdTest()` (для `test`). -**Что извлечь:** -- Сбор всех экспортов `browser.*` в объект -- Обёртка ACTION_FNS авто-обнаружением ошибок (проверка модальных/всплывающих после каждого вызова) -- Захват скриншота до того, как `fetchErrorStack` закроет модальное окно ошибки -- Вызов `fetchErrorStack` для модальных ошибок -- Заглушки `noRecord` для функций записи/озвучки +**Что делает:** +- Собирает все экспорты `browser.*` в плоский объект. +- Оборачивает ACTION_FNS авто-обнаружением 1С-ошибок: после каждого вызова + проверяет `state.errors.modal`/`balloon`, делает скриншот ДО того, как + `fetchErrorStack` закроет модалку, вызывает `fetchErrorStack` для modal-ошибок, + бросает исключение со структурированным `err.onecError = { step, args, errors, formState, stack, screenshot }`. +- Подмешивает заглушки `noRecord` (для функций записи/озвучки в exec-режиме). -**Сигнатура новой функции:** -```js -function buildContext({ noRecord = false } = {}) -> object -``` +**Сигнатура:** `function buildContext({ noRecord = false } = {}) -> object` -**Использование после рефакторинга:** -- `executeScript()` вызывает `buildContext()` + `new AsyncFunction(...)` (поведение не меняется) -- `cmdTest()` вызывает `buildContext()` + `import()` + `mod.default(ctx)` (новое поведение) +**Scoped-вариант** (`buildScopedContext(name)`): тот же `buildContext()`, +но каждый вызов функции префиксится `await browser.setActiveContext(name)`. +Используется для мульти-контекстных тестов (`ctx.a`/`ctx.b`). --- @@ -854,39 +903,53 @@ function buildContext({ noRecord = false } = {}) -> object ## 16. Каталог тест-кейсов -Расположение: `tests/web-test/` +Расположение: `tests/web-test/`. По состоянию на 2026-05-13: 19 файлов. -| # | Файл | Теги | Покрытие 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() | +| # | Файл | Теги | Покрытие | +|---|------|------|----------| +| 00 | hooks.test.mjs | hooks, smoke | индикатор порядка beforeAll/beforeEach/afterEach + testInfo + afterOpenContext | +| 01 | navigation.test.mjs | nav, smoke | navigateSection, getPageState, navigateLink, switchTab, errors | +| 02 | crud.test.mjs | crud, smoke | openCommand, fillFields, clickElement, closeForm, save-confirm flow | +| 03 | fillfields.test.mjs | fields | text/checkbox/date/dropdown/reference/radio/clear + composite + direct-edit-form | +| 04 | selectvalue.test.mjs | fields, select | dropdown / форма выбора / auto-history / clear | +| 05 | table.test.mjs | table, smoke | fillTableRow/deleteTableRow/tab-loop/checkbox/clear | +| 06 | document.test.mjs | doc, smoke | создание+проведение документа | +| 07 | tabs.test.mjs | tabs | switchTab + errors | +| 08 | hierarchy.test.mjs | hierarchy | groups expand + tree-grid view-mode switch | +| 09 | filter.test.mjs | filter | simple-search/advanced-column/exact/date/reference/unfilter-all/unfilter-specific | +| 10 | validation.test.mjs | validation | сообщения + exception modal (fetchErrorStack Path 1) | +| 11 | report.test.mjs | report | DCS form + быстрый фильтр + readSpreadsheet + drill-down | +| 12 | formstate.test.mjs | state | fields/buttons/tables/openForms/subordinate-nav/platformDialogs | +| 13 | misc.test.mjs | misc | openFile EPF + security confirm | +| 14 | errors-stack.test.mjs | errors | fetchErrorStack Path 1 + dismiss-modal + dismiss-platform | +| 14 | multi-context-routing.test.mjs | multi-context | single test → non-default context | +| 15 | multi-context-handover.test.mjs | multi-context | ctx.a creates → ctx.b sees → closeContext(b) + edge throw | +| 15 | recording.test.mjs | record | startRecording/stopRecording/captions/narration/overlays | +| 16 | tree-form.test.mjs | tree, table | FormDataTree edit (ДеревоНоменклатуры) | -~30 тест-кейсов, покрывающих все основные области API browser.mjs. +Полный регресс — **19/19** (~9 минут на warm-стенде). --- ## 17. Дорожная карта реализации -| # | Задача | Результат | Зависимости | Статус | -|---|--------|-----------|-------------|--------| -| 1 | Архитектурная спецификация | `docs/web-test-runner-spec.md` (этот файл) | -- | done 2026-04-05 | -| 2 | Рефакторинг buildContext() | run.mjs: извлечение из executeScript | спека | done 2026-04-05 | -| 3 | Ядро cmdTest() | run.mjs: обнаружение, импорт, выполнение, консольный вывод, JSON-отчёт | #2 | done 2026-04-05 | -| 4 | Утверждения + обёртка step() | run.mjs: assert.*, step(name, fn) | #3 | done 2026-04-05 | -| 5 | Хуки (prepare/cleanup + before/after) | run.mjs: поддержка _hooks.mjs | #3 | done 2026-04-05 | -| 6 | Файл конфигурации + контексты | run.mjs: webtest.config.mjs, BrowserContext'ы, маршрутизация | #3 | config done, BrowserContext pending | -| 7 | Форматы отчётов (Allure, JUnit) | run.mjs: --format=allure/junit | #3 | -- | -| 8 | Синтетическая конфигурация | integration/build-webtest-config.test.mjs | спека | done 2026-04-05 | -| 9 | Smoke-тесты (01-06) | tests/web-test/01-06*.test.mjs | #3, #8 | -- | -| 10 | Остальные тесты (07-13) | tests/web-test/07-13*.test.mjs | #9 | -- | +| # | Задача | Результат | Статус | +|---|--------|-----------|--------| +| 1 | Архитектурная спецификация | `docs/web-test-runner-spec.md` (этот файл) | done 2026-04-05 | +| 2 | Рефакторинг buildContext() | run.mjs: извлечение из executeScript | done 2026-04-05 | +| 3 | Ядро cmdTest() | run.mjs: обнаружение, импорт, выполнение, JSON-отчёт | done 2026-04-05 | +| 4 | Утверждения + обёртка step() | run.mjs: assert.*, step(name, fn) | done 2026-04-05 | +| 5 | Хуки (prepare/cleanup + before/after) | run.mjs: поддержка `_hooks.mjs` | done 2026-04-05 | +| 6 | Файл конфигурации + BrowserContext-ы | webtest.config.mjs, мульти-контекст | done 2026-05-10 (T4 + T4.5/4.6) | +| 7 | Форматы отчётов (Allure, JUnit) | --format=allure/junit | done 2026-05-03 (T2/T3) | +| 8 | Синтетическая конфигурация | `build-webtest-config.test.mjs` | done 2026-04-05 + M1 расширения 2026-05-01 | +| 9 | Smoke-тесты P0 (~18 кейсов) | `tests/web-test/01-12*.test.mjs` | done 2026-05-04 (M2) | +| 10 | Регресс P1 (~15 кейсов) | расширение 02/03/04/05/09/12 | done 2026-05-10 (M3) | +| 11 | M4: расширенный регресс P2 | validation/errors/recording/hierarchy/openFile | done 2026-05-11 | +| 12 | M5-pre: расширение синтетики | tree-form, composite, textEdit, history, unfilter | done 2026-05-12 | +| 13 | M6: автономный стенд через `_hooks.mjs` | prepare(): config-rebuild/data-reload/EPF + smart Apache | done 2026-05-12 (MVP) | +| 14 | M7.1/M7.2: ctx.testInfo + custom-поля контекстов | спека §3 + run.mjs | done 2026-05-13 | +| 15 | M7.3: Headless-режим | `--headless` CLI + config | deferred (1С-specific блокеры в headless) | +| 16 | M7.4: 4 testlevel-хука + индикатор | `_hooks.mjs` v0.3 + 00-hooks.test.mjs | done 2026-05-13 | +| 17 | M7.5: title slide bonus | `beforeEach` под isRecording() | done 2026-05-13 | +| 18 | M8: per-context lifecycle | closeContext + afterOpenContext/beforeCloseContext | done 2026-05-13 |