From 56cd18a6b4230f645acba90c76b3f10202bfdf93 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 3 May 2026 15:54:38 +0300 Subject: [PATCH] feat(web-test): --screenshot=on-failure|every-step|off + --report-dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раннер v1.5. Парсит --screenshot и --report-dir, мерж с config.screenshot. - every-step: после успешного step() пишет {reportDir}/{testIdx}-{stepIdx}-{slug}.png, путь в step.screenshot. - off: ни пошаговых, ни error-shot. - on-failure (default): error-shot уехал из .claude/skills/web-test/ в {reportDir}/error-{testIdx}-{slug}.png. reportDir фоллбэчит: --report-dir → dirname(--report) → testDir. Известная нестыковка: error-shot из buildContext/executeScript остаётся в .claude/skills/web-test/error-shot.png — затронем при T2 (Allure). Live-проверка: 01-navigation с every-step (5 PNG), off (пусто), default on-failure на стуб-failing тесте (error-shot в reportDir). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/run.mjs | 52 +++++++++++++++++++++---- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index 3659ac83..a114d57b 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.4 — CLI runner for 1C web client automation +// web-test run v1.5 — CLI runner for 1C web client automation // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * CLI runner for 1C web client automation. @@ -18,7 +18,7 @@ */ import http from 'http'; import * as browser from './browser.mjs'; -import { readFileSync, writeFileSync, unlinkSync, existsSync, readdirSync } from 'fs'; +import { readFileSync, writeFileSync, unlinkSync, existsSync, readdirSync, mkdirSync } from 'fs'; import { resolve, dirname, basename, relative } from 'path'; import { fileURLToPath } from 'url'; @@ -335,7 +335,7 @@ function cmdStatus() { async function cmdTest(rawArgs) { // Parse flags - const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json' }; + const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null }; let tags = null, grep = null; const positional = []; for (const a of rawArgs) { @@ -346,6 +346,8 @@ async function cmdTest(rawArgs) { else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000; else if (a.startsWith('--report=')) opts.report = a.slice(9); 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.startsWith('--')) positional.push(a); } @@ -378,6 +380,17 @@ 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.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)`); + } + // Resolve report directory: --report-dir, else dirname(--report), else testDir + const reportDir = opts.reportDir + ? resolve(opts.reportDir) + : (opts.report ? dirname(resolve(opts.report)) : testDir); + if (opts.screenshot !== 'off') { + try { mkdirSync(reportDir, { recursive: true }); } catch {} + } // Discover test files const testFiles = discoverTests(testPath); @@ -443,7 +456,9 @@ async function cmdTest(rawArgs) { if (hooks.beforeAll) await hooks.beforeAll(ctx); // Execute tests + let testIdx = 0; for (const t of filtered) { + testIdx++; if (t.skip) { const reason = typeof t.skip === 'string' ? t.skip : ''; W.write(` \u25CB ${t.name}${reason ? ` (skip: ${reason})` : ' (skip)'}\n`); @@ -460,6 +475,7 @@ async function cmdTest(rawArgs) { const output = []; let steps = []; let currentSteps = steps; + let stepIdx = 0; const t0 = Date.now(); // Wire up per-test log and step @@ -469,6 +485,8 @@ async function cmdTest(rawArgs) { currentSteps.push(s); const prev = currentSteps; currentSteps = s.steps; + stepIdx++; + const myIdx = stepIdx; try { await fn(); } catch (e) { @@ -478,6 +496,15 @@ async function cmdTest(rawArgs) { } finally { s.stop = Date.now(); currentSteps = prev; + if (opts.screenshot === 'every-step' && s.status === 'passed') { + try { + const slug = slugify(name); + const file = resolve(reportDir, `${testIdx}-${myIdx}-${slug}.png`); + const png = await browser.screenshot(); + writeFileSync(file, png); + s.screenshot = file; + } catch {} + } } }; @@ -513,12 +540,12 @@ async function cmdTest(rawArgs) { // Built-in state reset await resetState(ctx); - // Screenshot on failure + // Screenshot on failure (skip if strategy is 'off') let shotFile = e.onecError?.screenshot; - if (!shotFile) { + if (!shotFile && opts.screenshot !== 'off') { try { const png = await browser.screenshot(); - shotFile = resolve(__dirname, '..', `error-shot-${t.file.replace(/[/\\]/g, '-')}.png`); + shotFile = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`); writeFileSync(shotFile, png); } catch {} } @@ -623,6 +650,14 @@ function elapsed2(start, stop) { return Math.round(((stop || Date.now()) - start) / 100) / 10; } +function slugify(s) { + return String(s).trim() + .replace(/[\s/\\:*?"<>|]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 60) || 'step'; +} + function formatDuration(seconds) { if (seconds < 60) return `${Math.round(seconds * 10) / 10}s`; const m = Math.floor(seconds / 60); @@ -764,5 +799,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 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`); }