From c1a0a54971a8966c6a2457b71e62b85309b56ed9 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 3 May 2026 16:19:52 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20--record=20=D0=B8=20export=20?= =?UTF-8?q?const=20params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раннер v1.7. T5 --record: startRecording перед каждым тестом, stopRecording после (и в passed, и в failed ветке). Файл {reportDir}/{testIdx}-{slug}.mp4. testResult.video содержит путь. В Allure — attachment типа video/mp4. config.record читается тоже. Использует существующую инфраструктуру browser.mjs. T6 export const params: материализация в N тестов на этапе discovery. Имя через {key}-шаблон в mod.name (например 'demo {type}'); если шаблона нет — суффикс [index]. Тест-функция получает param как второй аргумент: default(ctx, param). В отчёте каждый набор — отдельная test entry с собственным uuid в Allure / testcase в JUnit. Live-проверка: - params: 2 теста с именами demo A / demo B из шаблона. - record: mp4 91KB на 6-секундном тесте, путь в JSON и Allure attachment video/mp4. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/run.mjs | 56 ++++++++++++++++++------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index 945fbcb3..2ee1eeb6 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.6 — CLI runner for 1C web client automation +// web-test run v1.7 — CLI runner for 1C web client automation // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * CLI runner for 1C web client automation. @@ -336,7 +336,7 @@ function cmdStatus() { async function cmdTest(rawArgs) { // Parse flags - const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null }; + const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false }; let tags = null, grep = null; const positional = []; for (const a of rawArgs) { @@ -349,6 +349,7 @@ async function cmdTest(rawArgs) { else if (a.startsWith('--format=')) opts.format = a.slice(9); else if (a.startsWith('--screenshot=')) opts.screenshot = a.slice(13); else if (a.startsWith('--report-dir=')) opts.reportDir = a.slice(13); + else if (a === '--record') opts.record = true; else if (!a.startsWith('--')) positional.push(a); } @@ -381,6 +382,7 @@ async function cmdTest(rawArgs) { if (!tags && config.tags) tags = config.tags; opts.timeout = rawArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout); opts.retry = rawArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry); + opts.record = opts.record || !!config.record; opts.screenshot = opts.screenshot || config.screenshot || 'on-failure'; if (!['on-failure', 'every-step', 'off'].includes(opts.screenshot)) { die(`Invalid --screenshot=${opts.screenshot} (expected on-failure|every-step|off)`); @@ -408,7 +410,7 @@ async function cmdTest(rawArgs) { let hasOnly = false; for (const file of testFiles) { const mod = await import('file:///' + file.replace(/\\/g, '/')); - const t = { + const base = { file: relative(testDir, file).replace(/\\/g, '/'), name: mod.name || basename(file, '.test.mjs'), tags: mod.tags || [], @@ -418,9 +420,18 @@ async function cmdTest(rawArgs) { setup: mod.setup, teardown: mod.teardown, fn: mod.default, + param: undefined, }; - if (t.only) hasOnly = true; - tests.push(t); + if (base.only) hasOnly = true; + if (Array.isArray(mod.params) && mod.params.length) { + for (let i = 0; i < mod.params.length; i++) { + const p = mod.params[i]; + const name = base.name.includes('{') ? interpolate(base.name, p) : `${base.name}[${i}]`; + tests.push({ ...base, name, param: p }); + } + } else { + tests.push(base); + } } // Filter @@ -485,6 +496,12 @@ async function cmdTest(rawArgs) { let stepIdx = 0; const t0 = Date.now(); + let videoFile = null; + if (opts.record) { + videoFile = resolve(reportDir, `${testIdx}-${slugify(t.name)}.mp4`); + try { await browser.startRecording(videoFile, { force: true }); } catch { videoFile = null; } + } + // Wire up per-test log and step ctx.log = (...a) => output.push(a.map(String).join(' ')); ctx.step = async (name, fn) => { @@ -523,7 +540,7 @@ async function cmdTest(rawArgs) { // Run test with timeout await Promise.race([ - t.fn(ctx), + t.fn(ctx, t.param), new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout (${t.timeout}ms)`)), t.timeout)), ]); @@ -534,8 +551,11 @@ async function cmdTest(rawArgs) { // Built-in state reset await resetState(ctx); + if (videoFile) { + 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 }; + 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 }; lastError = null; break; @@ -557,9 +577,12 @@ async function cmdTest(rawArgs) { } catch {} } + if (videoFile) { + try { await browser.stopRecording(); } catch {} + } 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 }; + 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 }; } } @@ -631,11 +654,10 @@ function writeAllure(results, reportDir) { stop: tr.stop, labels: (tr.tags || []).map(t => ({ name: 'tag', value: t })), steps: (tr.steps || []).map(allureStep), - attachments: tr.screenshot ? [{ - name: 'Screenshot on failure', - source: basename(tr.screenshot), - type: 'image/png', - }] : [], + attachments: [ + ...(tr.screenshot ? [{ name: 'Screenshot on failure', source: basename(tr.screenshot), type: 'image/png' }] : []), + ...(tr.video ? [{ name: 'Video', source: basename(tr.video), type: 'video/mp4' }] : []), + ], }; if (tr.status === 'failed' && tr.error) { out.statusDetails = { message: tr.error.message || '', trace: tr.output || '' }; @@ -738,6 +760,11 @@ function elapsed2(start, stop) { return Math.round(((stop || Date.now()) - start) / 100) / 10; } +function interpolate(template, params) { + return String(template).replace(/\{(\w+)\}/g, (_, key) => + params[key] !== undefined ? String(params[key]) : `{${key}}`); +} + function slugify(s) { return String(s).trim() .replace(/[\s/\\:*?"<>|]+/g, '-') @@ -890,5 +917,6 @@ Options for test: --report=path Write JSON report to file --report-dir=path Directory for screenshots and other artifacts --screenshot=mode on-failure (default) | every-step | off - --format=fmt json (default) | allure | junit`); + --format=fmt json (default) | allure | junit + --record Record video for each test (mp4 in report-dir)`); }