From 588382cec10aac0357d9824c37c026c94af07c4a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 13 May 2026 14:35:20 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20M7.4=20=E2=80=94=20testlevel-?= =?UTF-8?q?=D1=85=D1=83=D0=BA=D0=B8=20+=2000-hooks=20=D0=B8=D0=BD=D0=B4?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _hooks.mjs v0.3: добавлены beforeAll/afterAll/beforeEach/afterEach (counter-only) и shared `_state` (счётчики + events log). tests/web-test/00-hooks.test.mjs (новый, 4 шага, 0s) — индикатор порядка вызовов: проверяет beforeAll===1, beforeEach для текущего теста, доступность ctx.testInfo, afterEach < beforeEach. Multi-context хуки оставлены one-shot. Разведка beforeAll: navigateSection не нужен, 1С после входа уже на дефолтной секции. Регресс 19/19 ✓ (9m 12.7s). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/web-test/00-hooks.test.mjs | 55 ++++++++++++++++++++++++++++++++ tests/web-test/_hooks.mjs | 50 ++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/web-test/00-hooks.test.mjs diff --git a/tests/web-test/00-hooks.test.mjs b/tests/web-test/00-hooks.test.mjs new file mode 100644 index 00000000..52e9e5d1 --- /dev/null +++ b/tests/web-test/00-hooks.test.mjs @@ -0,0 +1,55 @@ +// 00-hooks.test.mjs — индикатор покрытия testlevel-хуков (M7.4). +// +// Тест запускается ПЕРВЫМ (алфавитно), импортирует shared `_state` из +// `_hooks.mjs` и проверяет: +// - `beforeAll` отработал ровно один раз ДО любого теста. +// - `beforeEach` уже отработал для самого 00-hooks (счётчик === 1). +// - `testInfo` доступен внутри тела (через ctx). +// - `afterEach` для 00-hooks ещё не вызывался — `afterEach < beforeEach`. +// - Последнее событие — `beforeEach:00-hooks.test.mjs`. +// +// `afterAll` проверить из теста невозможно (он зовётся после всех тестов). +// Покрывается косвенно: финальный run должен показать `afterAll = 1` в +// summary log (см. ctx.log в этом тесте). + +import { _state } from './_hooks.mjs'; + +export const name = 'Хуки testlevel — индикатор порядка вызовов'; +export const tags = ['hooks', 'smoke']; +export const timeout = 10000; + +export default async function ({ step, assert, log, testInfo }) { + + await step('beforeAll отработал ровно один раз', () => { + assert.equal(_state.beforeAll, 1, `beforeAll=${_state.beforeAll}, ожидался 1`); + assert.equal(_state.afterAll, 0, `afterAll=${_state.afterAll}, ожидался 0 (вызывается после всех тестов)`); + }); + + await step('beforeEach отработал для этого теста', () => { + assert.ok(_state.beforeEach >= 1, `beforeEach=${_state.beforeEach}, ожидался >= 1`); + const last = _state.events[_state.events.length - 1]; + assert.ok(typeof last === 'string' && last.startsWith('beforeEach:'), + `последнее событие должно быть beforeEach:..., но это "${last}"`); + assert.ok(last.includes('00-hooks'), + `последнее beforeEach должно ссылаться на 00-hooks, а не "${last}"`); + }); + + await step('testInfo доступен в теле теста', () => { + assert.equal(testInfo.file, '00-hooks.test.mjs', `testInfo.file=${testInfo.file}`); + assert.ok(Array.isArray(testInfo.tags), 'testInfo.tags должен быть массивом'); + assert.includes(testInfo.tags, 'hooks', 'testInfo.tags должен содержать "hooks"'); + assert.equal(testInfo.attempt, 1, `attempt=${testInfo.attempt}`); + assert.equal(typeof testInfo.primaryContext, 'string', 'primaryContext должен быть строкой'); + }); + + await step('afterEach для этого теста ещё не вызывался', () => { + // В теле теста afterEach НЕ должен быть вызван ни разу для текущего теста. + // Если 00-hooks запущен первым (что и ожидается), afterEach === 0. + // Tolerance: проверяем относительное неравенство, чтобы тест не сломался + // если кто-то добавит ещё один тест с алфавитно меньшим именем. + assert.ok(_state.afterEach < _state.beforeEach, + `afterEach (${_state.afterEach}) должен быть строго меньше beforeEach (${_state.beforeEach}) в теле теста`); + }); + + log(`hooks indicator: beforeAll=${_state.beforeAll}, beforeEach=${_state.beforeEach}, afterEach=${_state.afterEach}, events.length=${_state.events.length}`); +} diff --git a/tests/web-test/_hooks.mjs b/tests/web-test/_hooks.mjs index fd0601d6..52a21416 100644 --- a/tests/web-test/_hooks.mjs +++ b/tests/web-test/_hooks.mjs @@ -1,4 +1,4 @@ -// _hooks.mjs v0.2 — автономный тестовый стенд для web-test +// _hooks.mjs v0.3 — автономный тестовый стенд для web-test + testlevel-хуки // // `prepare()` поднимает изолированный стенд по smart-логике: // 1) Если нужно пересоздавать БД (config-rebuild или --reload-data) — web-stop @@ -293,3 +293,51 @@ export async function cleanup({ log }) { // или следующий запуск с --rebuild-stand. log('cleanup: stand left running (use /web-stop or run with `-- --rebuild-stand` to reset)'); } + +// ── Testlevel hooks (M7.4) ──────────────────────────────────────────────────── +// +// Shared mutable state, импортируется индикатором `00-hooks.test.mjs` для +// проверки порядка вызовов. Хуки — counter-only, никакой реальной работы: +// `prepare()` уже подготовил стенд, дефолтное приземление 1С после входа +// уже показывает панель разделов (разведка 2026-05-13: navigateSection +// в beforeAll не нужен). +// +// `events` — последовательность строк, по которой индикатор восстанавливает +// порядок (`beforeAll`, `beforeEach:01-navigation.test.mjs`, ...). + +export const _state = { + beforeAll: 0, + afterAll: 0, + beforeEach: 0, + afterEach: 0, + events: [], + lastTestResult: null, +}; + +export async function beforeAll(_ctx) { + _state.beforeAll++; + _state.events.push('beforeAll'); +} + +export async function afterAll(_ctx) { + _state.afterAll++; + _state.events.push('afterAll'); +} + +export async function beforeEach(ctx) { + _state.beforeEach++; + _state.events.push(`beforeEach:${ctx.testInfo?.file || '?'}`); +} + +export async function afterEach(ctx) { + _state.afterEach++; + // Снимок testResult без тяжёлого steps[]: индикатор проверяет только + // status/duration/attempts/error. + if (ctx.testResult) { + const { status, duration, attempts, error } = ctx.testResult; + _state.lastTestResult = { status, duration, attempts, error }; + } else { + _state.lastTestResult = null; + } + _state.events.push(`afterEach:${ctx.testInfo?.file || '?'}:${ctx.testResult?.status || '?'}`); +}