From e0197683e108a2ce1aa751ed0ff1308be90a1da2 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 13 May 2026 12:44:07 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20M7.1+M7.2=20=E2=80=94=20ctx.t?= =?UTF-8?q?estInfo=20+=20=D0=BF=D1=80=D0=BE=D0=B1=D1=80=D0=BE=D1=81=20cust?= =?UTF-8?q?om-=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ctx.testInfo (name/file/filePath/tags/timeout/attempt/maxAttempts/param/contexts/primaryContext) выставляется перед каждой попыткой, доступен в beforeEach/test/afterEach - ctx.testResult (status/duration/attempts/error/steps) доступен в afterEach - run.mjs:411 spread полного contextSpec (был whitelist {url, isolation}); CLI --url override сохраняет custom-поля через merge - webtest.config.mjs: displayName для a/b - spec §3 — подраздел «Метаданные теста», §6 — availability testInfo/testResult, §7 — рекомендация латинский ID + кириллический displayName - Full regression 18/18 ✓ (9m 9.8s) Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/run.mjs | 28 +++++++++-- docs/web-test-runner-spec.md | 66 ++++++++++++++++++++++--- tests/web-test/webtest.config.mjs | 7 ++- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index b3f74f3c..5c49c621 100644 --- a/.claude/skills/web-test/scripts/run.mjs +++ b/.claude/skills/web-test/scripts/run.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// web-test run v1.11 — CLI runner for 1C web client automation +// web-test run v1.12 — CLI runner for 1C web client automation // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * CLI runner for 1C web client automation. @@ -408,10 +408,10 @@ async function cmdTest(rawArgs) { const defaultIsolation = config.isolation || 'tab'; if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) { for (const [n, spec] of Object.entries(config.contexts)) { - contextSpecs[n] = { url: spec.url, isolation: spec.isolation }; + contextSpecs[n] = { ...spec }; } defaultContextName = config.defaultContext || Object.keys(config.contexts)[0]; - if (url) contextSpecs[defaultContextName] = { url }; // CLI override of default + if (url) contextSpecs[defaultContextName] = { ...contextSpecs[defaultContextName], url }; // CLI override of default url (preserve custom fields) } else { const fallbackUrl = url || config.url; if (!fallbackUrl) die('No URL provided and no webtest.config.mjs found'); @@ -572,6 +572,23 @@ async function cmdTest(rawArgs) { let stepIdx = 0; const t0 = Date.now(); + // testInfo — declarative metadata about the current test, visible + // to test body and hooks (beforeEach/afterEach). Overwritten on + // each attempt and each test (no delete, mirrors ctx.log/step lifecycle). + ctx.testInfo = { + name: t.name, + file: basename(t.file), + filePath: t.file, + tags: t.tags, + timeout: t.timeout, + attempt, + maxAttempts, + param: t.param, + contexts: Object.fromEntries(testContextNames.map(n => [n, contextSpecs[n]])), + primaryContext: testContextNames[0], + }; + ctx.testResult = null; // set right before afterEach + let videoFile = null; if (opts.record) { videoFile = resolve(reportDir, `${testIdx}-${slugify(t.name)}.mp4`); @@ -631,6 +648,8 @@ async function cmdTest(rawArgs) { // per-test teardown if (t.teardown) try { await t.teardown(ctx); } catch {} + // Expose testResult to afterEach (preliminary — full testResult assembled below). + ctx.testResult = { status: 'passed', duration: elapsed(t0), attempts: attempt, error: null, steps }; // afterEach if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {} // Built-in state reset across all contexts the test used @@ -661,6 +680,9 @@ async function cmdTest(rawArgs) { // per-test teardown (always) if (t.teardown) try { await t.teardown(ctx); } catch {} + // Expose preliminary testResult to afterEach (final testResult assembled below). + const errInfo = { message: e.message, step: e.onecError?.step, screenshot: shotFile }; + ctx.testResult = { status: 'failed', duration: elapsed(t0), attempts: attempt, error: errInfo, steps }; // afterEach (always) if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {} // Built-in state reset across all contexts the test used diff --git a/docs/web-test-runner-spec.md b/docs/web-test-runner-spec.md index 3210bc82..3bede21c 100644 --- a/docs/web-test-runner-spec.md +++ b/docs/web-test-runner-spec.md @@ -159,6 +159,50 @@ export default async function({ кладовщик, менеджер, step }) { - `assert.*` -- хелперы утверждений (см. раздел 5) - `log(...args)` -- добавить в вывод теста +### Метаданные теста (`ctx.testInfo`) + +Декларативная информация о текущем тесте. Раннер выставляет `ctx.testInfo` +перед каждой попыткой (до `beforeEach`), хук и тело теста могут читать. +Не предназначено для мутации. + +```js +ctx.testInfo = { + name, // 'Навигация по разделам' (с подставленными params) + file, // '01-navigation.test.mjs' (basename) + filePath, // '01-navigation.test.mjs' (relative к testDir) + tags, // ['nav', 'smoke'] + timeout, // 60000 (ms) + attempt, // 1..maxAttempts (1-based) + maxAttempts, // 1 + retry + param, // { ... } | undefined (для export const params) + contexts: { // объект, всегда 1+ ключей; зеркалит config.contexts + a: { url, isolation, ...customFields }, + b: { ... }, + }, + primaryContext, // 'a' — имя контекста, активного на входе в тест + // (= t.context для single, t.contexts[0] для multi) +} +``` + +Доступ к специфике контекста: `testInfo.contexts[testInfo.primaryContext].displayName`. +`primaryContext` — декларация теста, не зависит от runtime-состояния +`getActiveContext()` (которое может меняться внутри теста). + +### Результат теста в afterEach (`ctx.testResult`) + +Только в `afterEach`. До запуска теста — `null`. После — заполняется +раннером перед вызовом хука: + +```js +ctx.testResult = { + status, // 'passed' | 'failed' + duration, // ms + attempts, // фактически выполнено попыток (1..maxAttempts) + error, // { message, step?, screenshot? } | null + steps, // массив step-результатов +} +``` + ### Мульти-контекст При `export const contexts = ['a', 'b']`: @@ -282,8 +326,9 @@ assert.noErrors(state, msg) **Тестовый уровень** (с контекстом браузера): - `beforeAll(ctx)` -- после подключения, перед первым тестом - `afterAll(ctx)` -- после последнего теста, до отключения -- `beforeEach(ctx)` -- перед каждым тестом -- `afterEach(ctx)` -- после каждого теста +- `beforeEach(ctx)` -- перед каждым тестом. На входе уже доступен `ctx.testInfo` (см. §3). +- `afterEach(ctx)` -- после каждого теста. Дополнительно доступен `ctx.testResult` + с результатом завершившегося теста (status/duration/error/...). ### Порядок выполнения @@ -377,13 +422,16 @@ URL должен быть передан через CLI. ```js export default { - // Контексты: именованные URL для разных пользователей/ролей + // Контексты: именованные URL для разных пользователей/ролей. + // Рекомендация: латинский ID контекста (`clerk`, `manager`) + кириллический + // `displayName` для UI/слайдов. Любые custom-поля пробрасываются как есть + // и доступны хукам через `ctx.testInfo.contexts[name]` (см. §3). contexts: { - кладовщик: { url: 'http://localhost/app-clerk/ru_RU' }, - менеджер: { url: 'http://localhost/app-manager/ru_RU' }, - админ: { url: 'http://localhost/app-admin/ru_RU' }, + clerk: { url: 'http://localhost/app-clerk/ru_RU', displayName: 'Кладовщик' }, + manager: { url: 'http://localhost/app-manager/ru_RU', displayName: 'Менеджер' }, + admin: { url: 'http://localhost/app-admin/ru_RU', displayName: 'Админ' }, }, - defaultContext: 'кладовщик', + defaultContext: 'clerk', // Значения по умолчанию (переопределяются флагами CLI) timeout: 30000, @@ -393,6 +441,10 @@ export default { }; ``` +Кириллица в ID контекстов работает, но смешанный регистр затрудняет ergonomics +(`testInfo.contexts.кладовщик.displayName` vs `testInfo.contexts.clerk.displayName`). +Рекомендуем разделять технический ID и человекочитаемое имя. + **Упрощённая форма** (один контекст, без именованных): ```js diff --git a/tests/web-test/webtest.config.mjs b/tests/web-test/webtest.config.mjs index e89c48fe..c9bc04cf 100644 --- a/tests/web-test/webtest.config.mjs +++ b/tests/web-test/webtest.config.mjs @@ -7,8 +7,11 @@ // конфликтовать с ручной разведкой и работать поверх отдельного Apache на :9191. export default { contexts: { - a: { url: 'http://localhost:9191/webtest-runner/ru_RU' }, - b: { url: 'http://localhost:9191/webtest-runner/ru_RU' }, + // `displayName` — человекочитаемое имя контекста, видно хукам через + // testInfo.contexts[name].displayName (например для showTitleSlide). + // Custom-поля любого типа пробрасываются как есть. + a: { url: 'http://localhost:9191/webtest-runner/ru_RU', displayName: 'Пользователь A' }, + b: { url: 'http://localhost:9191/webtest-runner/ru_RU', displayName: 'Пользователь B' }, }, defaultContext: 'a', // isolation: 'tab' (default) — persistent context, tabs in one window, 1С extension loads.