fix(web-test): резолвить корень сьюта подъёмом вверх, а не от переданного пути

Конфиг и хуки резолвились строго от каталога первого позиционного пути, поэтому
запуск подкаталога сьюта был невозможен: `test tests/app/00-smoke/` падал с
«No URL provided and no webtest.config.mjs found» — хотя спека прямо обещает
запуск подкаталога («Фильтр по пути с CLI»).

Опаснее отказа по URL были два молчаливых следствия: при `--url=` прогон
подкаталога терял `_hooks.mjs` и ехал по неподготовленному стенду без единого
предупреждения, а `_allure/` не находился. Плюс `file:` в отчёте считался от
переданного пути, из-за чего один и тот же тест получал разный ID в зависимости
от способа запуска и рвал историю Allure/JUnit.

Введён корень сьюта: подъём от каталога пути до первого `webtest.config.mjs`
ИЛИ `_hooks.mjs` (конфиг необязателен — сьют только с хуками иначе снова терял
бы подготовку), с ограничением подъёма каталогом `.git`/`.v8-project.json`, а
при их отсутствии — cwd. Граница ничего не выбирает, только останавливает, так
что ложная граница даёт «корень не найден», а не чужой корень. От найденного
корня берутся все пять ролей: конфиг, хуки, каталог отчёта, пути в отчёте,
`_allure/`.

Попутно: пути из разных сьютов в одном прогоне теперь отвергаются (раньше
молча выигрывал первый путь, и сьют B ехал по подготовке сьюта A); найденный
корень печатается в шапке; отсутствие корня — предупреждение в stderr;
диагностика говорит про корень сьюта, а не только про URL.

Проверено: 12/12 офлайн-кейсов резолвера; полный регресс 29/29 до и после —
`file`/`name`/`status` идентичны; `_suite-root/nested/` (сценарий, который
падал) проходит с подхваченными конфигом и хуками; `_hang/` 6/6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-20 14:41:16 +03:00
co-authored by Claude Opus 4.8
parent 9c7010a49e
commit 90d8263a05
10 changed files with 309 additions and 29 deletions
+9
View File
@@ -254,6 +254,15 @@ export default async function({
23 passed, 1 failed, 0 skipped (3m 42s)
```
### Прогон части набора
```
> Прогони только сценарии из папки 03-приходные-накладные
> Прогони только тесты с тегом контрагенты
```
Подмножество выбирается тремя способами: путём к подпапке или отдельному файлу, тегом (`--tags=`) или фильтром по имени теста (`--grep=`). Путь к подпапке работает наравне с остальными — конфиг и подготовка стенда всё равно берутся из корня набора (папки приложения в `tests/`), движок находит его сам, поднимаясь вверх. Отдельного URL или флагов для этого не нужно.
### Подробный отчёт
```
+23 -13
View File
@@ -29,12 +29,22 @@ node run.mjs test <dir|file>... [флаги]
| `--report=path` | (нет) | Записать машинный отчёт в файл (JSON или XML для `--format=junit`) |
| `--report=-` | (нет) | Машинный отчёт в stdout (`-` = stdout); человеческий прогресс уходит в stderr |
| `--format=fmt` | json | Формат отчёта: `json` / `allure` / `junit` |
| `--report-dir=path` | dirname(report) / testDir | Каталог для скриншотов, видео, Allure-результатов |
| `--report-dir=path` | dirname(report) / корень сьюта | Каталог для скриншотов, видео, Allure-результатов |
| `--screenshot=strategy` | on-failure | `on-failure` / `every-step` / `off` |
| `--record` | false | Записывать видео для каждого теста (mp4 в `--report-dir`) |
| `-- <hookArgs…>` | — | Всё после `--` пробрасывается в `_hooks.mjs` как `hookArgs` (см. §6.1) |
URL не передаётся позиционно — он берётся из `webtest.config.mjs` в каталоге тестов, а флаг `--url=` переопределяет URL дефолтного контекста. `webtest.config.mjs` и `_hooks.mjs` резолвятся от каталога **первого** пути, поэтому перечисляемые файлы должны лежать в одной папке сьюта.
URL не передаётся позиционно — он берётся из `webtest.config.mjs`, а флаг `--url=` переопределяет URL дефолтного контекста.
### Резолв корня сьюта
`webtest.config.mjs` и `_hooks.mjs` резолвятся не от переданного пути, а от **корня сьюта**: от каталога пути движок поднимается вверх до первого каталога, где лежит `webtest.config.mjs` или `_hooks.mjs`. Именно поэтому запуск подкаталога (`test tests/myapp/sales/`) и отдельного файла работает без `--url=`. Подъём ограничен каталогом с `.git` или `.v8-project.json` (сам каталог проверяется), а если их нет — текущим рабочим каталогом; выше поиск не идёт. Не нашли маркер — корнем считается переданный каталог (тогда хуков нет, и движок пишет об этом предупреждение в stderr).
Маркером служат **оба** файла, а не только конфиг: конфиг необязателен (§7), и сьют, у которого есть только `_hooks.mjs`, иначе молча остался бы без подготовки стенда.
Если переданные пути принадлежат разным сьютам (корни не совпали) — прогон не стартует: конфиг и хуки были бы взяты от первого пути, то есть чужие. Запускайте сьюты отдельно.
Найденный корень печатается в шапке прогона, рядом — переданные пути, если они от него отличаются.
### Валидация CLI
@@ -200,7 +210,7 @@ export default async function({ clerk, manager, step }) {
ctx.testInfo = {
name, // 'Навигация по разделам' (с подставленными params)
file, // '01-navigation.test.mjs' (basename)
filePath, // '01-navigation.test.mjs' (relative к testDir, разделитель '/')
filePath, // '01-navigation.test.mjs' (relative к корню сьюта, разделитель '/')
tags, // ['nav', 'smoke']
timeout, // 60000 (ms)
attempt, // 1..maxAttempts (1-based)
@@ -320,7 +330,7 @@ assert.noErrors(state, msg?)
## 6. Хуки
Все хуки определяются в `_hooks.mjs` в корне каталога тестов.
Все хуки определяются в `_hooks.mjs` в корне сьюта (§1 «Резолв корня сьюта»).
### Три уровня
@@ -451,7 +461,7 @@ node run.mjs test tests/myapp/ --bail -- --rebuild-stand --reload-data
## 7. Файл конфигурации
`webtest.config.mjs` в корне каталога тестов. Необязателен — если отсутствует, URL должен быть передан через CLI.
`webtest.config.mjs` в корне сьюта (§1 «Резолв корня сьюта»). Необязателен — если отсутствует, URL должен быть передан через CLI.
```js
export default {
@@ -782,7 +792,7 @@ await step('Кладовщик проверяет статус', async () => {
Движок всегда заполняет следующие метки (`labels`):
- **`tag`** — по одному на каждый элемент `mod.tags[]`. Готовая фильтрация в Allure-отчёте без дополнительной разметки.
- **`suite`** — `dirname(t.filePath)`. Тесты в корне `testDir` идут под `'root'`, тесты в подкаталоге `sales/` — под `'sales'`. Это даёт левую группировку отчёта без ручной разметки.
- **`suite`** — `dirname(t.filePath)`. Тесты в корне сьюта идут под `'root'`, тесты в подкаталоге `sales/` — под `'sales'`. Это даёт левую группировку отчёта без ручной разметки.
- **`severity`** — резолв в порядке приоритета:
1. `export const severity = 'critical'` в самом тесте, **если значение валидное** (одно из `blocker | critical | normal | minor | trivial`). Если экспорт задан, но значение невалидное — пункт пропускается и идём в (3); резолв через теги (пункт 2) при этом **не выполняется** (хотел бы автор иначе — он бы не объявлял `severity`).
2. Иначе **максимальный ранг** среди тегов теста (стандартные имена `blocker | critical | normal | minor | trivial` напрямую, либо через `config.severity`-маппинг).
@@ -792,9 +802,9 @@ await step('Кладовщик проверяет статус', async () => {
Пример: `tags: ['smoke', 'recording']` + `severity: { critical: ['smoke'], minor: ['recording'] }` → severity = `critical` (5 > 2).
#### Доп. файлы Allure через `<testDir>/_allure/`
#### Доп. файлы Allure через `<корень сьюта>/_allure/`
Движок ищет каталог `_allure/` рядом с тестами и копирует все его файлы в `reportDir` перед генерацией отчёта. Конвенция для статичной настройки Allure, для которой нет места внутри JSON-файла теста:
Движок ищет каталог `_allure/` в корне сьюта и копирует все его файлы в `reportDir` перед генерацией отчёта. Конвенция для статичной настройки Allure, для которой нет места внутри JSON-файла теста:
| Файл | Назначение |
|------|-----------|
@@ -922,7 +932,7 @@ export default async function({ fillFields, getFormState, assert }, { type, fiel
## 14. Обнаружение тестов
`testDir` (первый позиционный аргумент после URL) — каталог, в котором живут тесты. Сборщик рекурсивно обходит дерево и собирает файлы по правилам ниже.
Позиционные аргументы — пути к тестам; каталог, от которого считаются относительные пути в отчёте, — корень сьюта (§1 «Резолв корня сьюта»). Сборщик рекурсивно обходит дерево и собирает файлы по правилам ниже.
```
tests/myapp/
@@ -944,14 +954,13 @@ tests/myapp/
| Шаблон имени | Только `*.test.mjs` |
| Несколько путей | `node run.mjs test a.test.mjs b.test.mjs dir/` — наборы объединяются, дублируются и сортируются |
| Порядок | Сортировка по полному относительному пути (`sales/01` идёт до `warehouse/01`) |
| `file` в отчёте | `relative(testDir, file)` с разделителем `/`, например `sales/01-order-create.test.mjs` |
| `file` в отчёте | `relative(<корень сьюта>, file)` с разделителем `/`, например `sales/01-order-create.test.mjs` |
| Фильтр по пути с CLI | `node run.mjs test tests/myapp/sales/` запустит только подкаталог |
| Конкретный файл | `node run.mjs test tests/myapp/sales/01-order-create.test.mjs` |
### Чего НЕТ (сознательное упрощение)
- **`_hooks.mjs` на уровне подкаталога.** Движок ищет `_hooks.mjs` только в корне `testDir`. Подкаталоги свои хуки не получают.
- **`webtest.config.mjs` на уровне подкаталога.** Тоже только в корне.
- **`_hooks.mjs` / `webtest.config.mjs` на уровне подкаталога.** Оба берутся только из корня сьюта — того каталога, где лежит ближайший из них (§1 «Резолв корня сьюта»). Подкаталоги своих копий не получают; вложенный каталог со своим `webtest.config.mjs` — это уже отдельный сьют.
- **Многоуровневой Suite-разметки из дерева каталогов.** Allure-метка `suite` строится только по первому уровню (`dirname(filePath)`); более глубокую группировку делайте через `tags`.
- **Контекста по умолчанию на уровне подкаталога.** Каждый тест объявляет `context` / `contexts` сам; от пути контексты не наследуются.
@@ -1117,7 +1126,8 @@ JSON-отчёт (`tests[]`, полная структура — §9) для ка
| Термин | Определение |
|--------|-------------|
| **testDir** | Каталог тестов, переданный позиционным аргументом движку. Корень для discovery, `_hooks.mjs`, `webtest.config.mjs`, `_allure/`. |
| **Test path** | Путь к тесту или каталогу тестов, переданный позиционным аргументом. Корень только для discovery — что именно запускать. |
| **Suite root (корень сьюта)** | Каталог, найденный подъёмом от test path до первого `webtest.config.mjs` / `_hooks.mjs` (§1). От него берутся конфиг, хуки, `_allure/`, каталог отчёта по умолчанию и относительные пути `file` в отчёте. Не зависит от того, запустили сьют целиком или один его подкаталог, — поэтому ID теста в отчёте стабилен. |
| **Context (BrowserContext)** | Изолированная сессия Playwright. Куки/состояние/страница независимы. В рамках одного теста используется один или несколько контекстов. |
| **Active context** | Контекст, на котором сейчас оперируют функции browser-API. Переключается `setActiveContext`. |
| **Primary context** | Контекст, активный на входе в тест. Декларация (`mod.context` или `mod.contexts[0]`). Зафиксирован в `testInfo.primaryContext`. |