feat(web-test): test-раннер пишет человеческий отчёт в stdout, JSON по --report=-

Команда `test` приведена к поведению тест-раннеров (jest/pytest/playwright):
человеческий отчёт со сводкой в последней строке идёт в stdout, а машинный
JSON/JUnit — опционально через `--report=-` (Unix-конвенция `-` = stdout),
при этом прогресс уезжает в stderr. Убран безусловный дамп JSON в stdout,
из-за которого `test … | tail` хоронил сводку под отчётом.

- test.mjs: writer выбирается по режиму (--report=- → stderr-прогресс);
  развилка `-` в обеих ветках записи (json и junit), чтобы не плодить файл "-";
  валидация: --report=- несовместимо с --format=allure (каталог, не поток).
- util.mjs: строка --report=- в справке.
- Документация (spec/guide/regress/README) приведена к фактическому
  английскому выводу и описывает матрицу потоков stdout/stderr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-31 14:17:52 +03:00
parent f424d2ac70
commit 547f336cf8
6 changed files with 48 additions and 19 deletions
+2 -1
View File
@@ -366,10 +366,11 @@ node $RUN test tests/<app-name>/ --tags=smoke # by tag
node $RUN test tests/<app-name>/ --grep='накладн' # by name regex
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
node $RUN test tests/<app-name>/ --report=allure-results --format=allure --report-dir=allure-results
node $RUN test tests/<app-name>/ --report=- # machine JSON to stdout, progress to stderr
node $RUN test tests/<app-name>/ -- --rebuild-stand # after `--` → hookArgs
```
Default report is JSON when `--report=` is given. Allure needs `--format=allure` + a directory. JUnit similarly with `--format=junit`.
**Output contract.** `test` behaves like a test runner: by default the human report (with the summary as the last line) goes to **stdout** — read the tail of stdout + exit code. The machine report is opt-in via `--report`: `--report=path` writes it to a file (default JSON; XML for `--format=junit`), `--report=-` writes it to stdout while progress moves to stderr. Allure needs `--format=allure` + a directory (`-` is invalid for allure). For detailed triage use `--report=path` or `--report=-`. **In `--report=-` mode never use `2>&1`** — it merges stderr progress into the stdout JSON. (In the default mode there is no JSON in stdout, so `… | tail` is safe.)
### Allure static config — `_allure/`
@@ -1,4 +1,4 @@
// web-test cli/commands/test v1.0 — regression test runner
// web-test cli/commands/test v1.1 — regression test runner
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { resolve, dirname, basename, relative } from 'path';
@@ -96,9 +96,15 @@ export async function cmdTest(rawArgs) {
if (opts.format === 'junit' && !opts.report) {
die('--format=junit requires --report=path.xml');
}
// `--report=-` means "machine report to stdout" (Unix `-` convention).
// Only meaningful for streamable formats (json/junit); allure is a directory.
const reportToStdout = opts.report === '-';
if (reportToStdout && opts.format === 'allure') {
die('--report=- (stdout) is not valid with --format=allure: allure emits a directory of files, not a single stream. Use --report-dir=<dir> instead.');
}
const reportDir = opts.reportDir
? resolve(opts.reportDir)
: (opts.report ? dirname(resolve(opts.report)) : testDir);
: (opts.report && !reportToStdout ? dirname(resolve(opts.report)) : testDir);
if (opts.screenshot !== 'off') {
try { mkdirSync(reportDir, { recursive: true }); } catch {}
}
@@ -154,8 +160,9 @@ export async function cmdTest(rawArgs) {
hooks = await import('file:///' + hooksPath.replace(/\\/g, '/'));
}
// Console header
const W = process.stderr;
// Human-readable report goes to stdout (test-runner convention: jest/pytest/playwright).
// In `--report -` mode the machine JSON/XML takes over stdout, so progress moves to stderr.
const W = reportToStdout ? process.stderr : process.stdout;
W.write(`\nweb-test -- ${url}\n`);
W.write(`Running ${filtered.length} tests from ${relative(process.cwd(), testDir).replace(/\\/g, '/') || '.'}/\n\n`);
@@ -418,13 +425,14 @@ export async function cmdTest(rawArgs) {
summary: { total: results.length, passed: passCount, failed: failCount, skipped: skipCount },
tests: results,
};
out(report);
if (opts.format === 'allure') {
writeAllure(results, reportDir, severityIndex);
syncAllureExtras(testDir, reportDir);
} else if (opts.format === 'junit') {
writeFileSync(resolve(opts.report), buildJUnit(report, testDir));
if (reportToStdout) process.stdout.write(buildJUnit(report, testDir) + '\n');
else writeFileSync(resolve(opts.report), buildJUnit(report, testDir));
} else if (reportToStdout) {
out(report);
} else if (opts.report) {
writeFileSync(resolve(opts.report), JSON.stringify(report, null, 2));
}
+3 -2
View File
@@ -1,4 +1,4 @@
// web-test cli/util v1.0 — generic helpers for CLI commands
// web-test cli/util v1.1 — generic helpers for CLI commands
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
export function out(obj) {
@@ -100,7 +100,8 @@ Options for test:
--bail Stop on first failure
--retry=N Retry failed tests N times
--timeout=ms Per-test timeout (default: 30000)
--report=path Write JSON report to file
--report=path Write machine report (JSON/JUnit) to file
--report=- Write machine report to stdout (progress moves to stderr)
--report-dir=path Directory for screenshots and other artifacts
--screenshot=mode on-failure (default) | every-step | off
--format=fmt json (default) | allure | junit
+2 -2
View File
@@ -241,9 +241,9 @@ export default async function({
✗ Проведение приходной накладной (12.7s)
└ Заполнить табличную часть (5.2s)
Не найден столбец "Цена" в табличной части "Товары"
скриншот: tests/учёт-поступлений/error-shot.png
screenshot: tests/учёт-поступлений/error-shot.png
23 пройдено, 1 упал, 0 пропущено (3 мин 42 с)
23 passed, 1 failed, 0 skipped (3m 42s)
```
### Подробный отчёт
+24 -6
View File
@@ -22,7 +22,8 @@ node run.mjs test [url] <dir|file> [флаги]
| `--bail` | false | Остановиться при первом падении |
| `--retry=N` | 0 | Повторить упавшие тесты N раз |
| `--timeout=ms` | 30000 | Таймаут на тест (мс) |
| `--report=path` | (нет) | Записать отчёт в файл (JSON или XML для `--format=junit`) |
| `--report=path` | (нет) | Записать машинный отчёт в файл (JSON или XML для `--format=junit`) |
| `--report=-` | (нет) | Машинный отчёт в stdout (`-` = stdout); человеческий прогресс уходит в stderr |
| `--format=fmt` | json | Формат отчёта: `json` / `allure` / `junit` |
| `--report-dir=path` | dirname(report) / testDir | Каталог для скриншотов, видео, Allure-результатов |
| `--screenshot=strategy` | on-failure | `on-failure` / `every-step` / `off` |
@@ -35,7 +36,20 @@ URL необязателен, если в каталоге тестов есть
- `--screenshot=<v>` принимается только `on-failure | every-step | off`; при невалидном значении движок выводит ошибку и завершается с ненулевым кодом до старта прогона.
- `--format=<v>` принимается только `json | allure | junit`; иначе — завершение с ошибкой.
- `--format=junit` требует `--report=<path>` (иначе некуда писать XML); иначе — завершение с ошибкой.
- `--format=junit` требует `--report=<path>` (иначе некуда писать XML); иначе — завершение с ошибкой. Значение `-` (stdout) для junit допустимо.
- `--report=-` (stdout) несовместимо с `--format=allure`: allure пишет каталог, а не поток; иначе — завершение с ошибкой.
### Потоки вывода (stdout / stderr)
`test` ведёт себя как тест-раннер (jest/pytest/playwright): человеческий отчёт со сводкой в конце идёт в **stdout**. Машинный отчёт (JSON/JUnit) включается отдельно флагом `--report`.
| Запуск | stdout | stderr | Файл |
|--------|--------|--------|------|
| `test …` (дефолт) | человеческий отчёт, **сводка последней строкой** | — | — |
| `test … --report=file` | человеческий отчёт (виден прогресс + сводка) | — | JSON/JUnit в файл |
| `test … --report=-` | **чистый машинный отчёт** (JSON или JUnit-XML) | человеческий прогресс | — |
| `--format=allure …` | человеческий отчёт | — | артефакты allure в каталоге |
| любой | — | — | exit code **0/1** всегда |
### Режим выполнения
@@ -753,9 +767,11 @@ await step('Кладовщик проверяет статус', async () => {
## 10. Консольный вывод
Вывод — на английском (статусы `passed/failed/skipped` зеркалят ключи JSON-отчёта и Allure/JUnit):
```
web-test http://localhost/app/ru_RU
Запуск 25 тестов из tests/myapp/
web-test -- http://localhost/app/ru_RU
Running 25 tests from tests/myapp/
✓ Навигация по разделам (2.1s)
✓ CRUD справочника Контрагенты (12.3s)
@@ -766,13 +782,15 @@ web-test — http://localhost/app/ru_RU
├ Открыть форму (2.0s)
└ ✗ Сохранить пустую форму (6.1s)
Ожидалось модальное окно ошибки, но форма сохранилась
скриншот: error-shot-10.png
screenshot: error-shot-10.png
○ Составной тип (skip: не реализовано)
23 passed, 1 failed, 1 skipped (2m 0.5s)
```
Для passed-тестов выводится одна строка `✓ name (duration)`. Шаги печатаются только для упавших — после строки `✗`, с отступом, плюс сообщение ошибки и путь к скриншоту. Полная картина по шагам — в JSON-отчёте (`--report=…`).
Для passed-тестов выводится одна строка `✓ name (duration)`. Шаги печатаются только для упавших — после строки `✗`, с отступом, плюс сообщение ошибки и путь к скриншоту (`screenshot:`). Полная картина по шагам — в машинном отчёте (`--report=…` или `--report=-`).
По умолчанию этот отчёт идёт в **stdout** и заканчивается строкой сводки (`N passed, M failed, K skipped (Xs)`) — модель читает хвост stdout + exit code. В режиме `--report=-` (Unix-конвенция `-` = stdout) stdout занимает чистый машинный отчёт (JSON/JUnit), а человеческий прогресс уходит в stderr.
---
+2 -1
View File
@@ -31,7 +31,8 @@ Exit code: 0 = все прошли, 1 = есть падения.
| `--bail` | Остановиться на первой ошибке |
| `--retry=N` | Перепрогон упавших тестов N раз |
| `--timeout=ms` | Таймаут одного теста (default 30000) |
| `--report=path` | Сохранить отчёт в файл |
| `--report=path` | Сохранить машинный отчёт в файл |
| `--report=-` | Машинный отчёт в stdout (прогресс → stderr) |
| `--format=json\|allure\|junit` | Формат отчёта |
| `--report-dir=path` | Корень для Allure/JUnit артефактов |
| `--screenshot=on-failure\|every-step\|off` | Когда снимать скриншоты |