docs(web-test): полный sync спеки + contexts[] в testResult

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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-13 17:11:51 +03:00
parent eb87be5c04
commit 1eff62de42
2 changed files with 178 additions and 111 deletions
+11 -7
View File
@@ -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 };
}
}
+167 -104
View File
@@ -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] <dir|file> [флаги]
| `--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`) |
| `-- <hookArgs...>` | -- | Всё после `--` пробрасывается в `_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.<id>.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<name, slot>`. Переключение между слотами:
`_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 |