fix(web-test): скриншоты падений доезжают до Allure-отчёта

Вложение «Screenshot on failure» весило 0 B у всех красных тестов —
две независимых причины, маскирующие друг друга.

1. slugify сохранял кириллицу в именах артефактов. Allure CLI молча не
   находит вложение с не-ASCII именем: пишет "size": 0 без ссылки на файл
   (JAVA_OPTS с file.encoding/sun.jnu.encoding не помогает). Теперь slugify
   транслитерирует кириллицу и схлопывает остальное не-ASCII в дефис.
   Чинит и видео — оно использовало то же имя.

2. Скриншот 1С-ошибки писался в фиксированный <навык>/error-shot.png:
   вне reportDir (репортер аттачит по basename → мёртвая ссылка) и одним
   именем на весь прогон (каждый следующий тест перетирал предыдущий).
   exec-context получил setErrorShotDir + уникальные имена; раннер
   направляет их в reportDir. Дефолт для интерактивных exec/run не менялся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-15 15:36:48 +03:00
parent 3f1168b975
commit 3889c7279f
3 changed files with 64 additions and 12 deletions
@@ -1,10 +1,10 @@
// web-test cli/commands/test v1.4 — regression test runner
// web-test cli/commands/test v1.5 — regression test runner
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { existsSync, writeFileSync, mkdirSync, renameSync, copyFileSync, unlinkSync } from 'fs';
import { resolve, dirname, basename, relative } from 'path';
import * as browser from '../../browser.mjs';
import { out, die, elapsed, slugify, formatDuration, interpolate, printSteps } from '../util.mjs';
import { buildContext, buildScopedContext } from '../exec-context.mjs';
import { buildContext, buildScopedContext, setErrorShotDir } from '../exec-context.mjs';
import { createAssertions } from '../test-runner/assertions.mjs';
import { buildSeverityIndex } from '../test-runner/severity.mjs';
import { writeAllure, buildJUnit, syncAllureExtras } from '../test-runner/reporters.mjs';
@@ -143,6 +143,10 @@ export async function cmdTest(rawArgs) {
: (opts.report && !reportToStdout ? dirname(resolve(opts.report)) : testDir);
if (opts.screenshot !== 'off') {
try { mkdirSync(reportDir, { recursive: true }); } catch {}
// 1C-error screenshots (taken inside the action wrapper) default to a single
// fixed file at the skill root — outside reportDir and shared by every test.
// Point them at reportDir so each failure keeps its own attachable file.
setErrorShotDir(reportDir);
}
// Discover test files
@@ -432,6 +436,21 @@ export async function cmdTest(rawArgs) {
shotFile = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`);
writeFileSync(shotFile, png);
} catch {}
} else if (shotFile && dirname(resolve(shotFile)) !== reportDir) {
// Shot came from a context built before setErrorShotDir (e.g. a server
// session started earlier): reporters attach by basename, so anything
// outside reportDir is a dead link. Move it in under a unique name.
const dest = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`);
try {
renameSync(resolve(shotFile), dest);
shotFile = dest;
} catch {
try {
copyFileSync(resolve(shotFile), dest);
try { unlinkSync(resolve(shotFile)); } catch {}
shotFile = dest;
} catch {}
}
}
if (t.teardown) try { await t.teardown(ctx); } catch {}
@@ -1,13 +1,29 @@
// web-test cli/exec-context v1.0 — buildContext + executeScript для run/exec/test
// web-test cli/exec-context v1.1 — buildContext + executeScript для run/exec/test
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { readFileSync, writeFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import * as browser from '../browser.mjs';
import { elapsed } from './util.mjs';
import { elapsed, slugify } from './util.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ERROR_SHOT_PATH = resolve(__dirname, '..', '..', 'error-shot.png');
const DEFAULT_ERROR_SHOT_PATH = resolve(__dirname, '..', '..', 'error-shot.png');
// Where 1C-error screenshots go. The default is a single fixed file: fine for
// interactive `exec`/`run` (last error wins, easy to find), wrong for a test run
// where every test needs its own file inside reportDir. cmdTest overrides this.
let errorShotDir = null;
let errorShotSeq = 0;
export function setErrorShotDir(dir) {
errorShotDir = dir;
errorShotSeq = 0;
}
function nextErrorShotPath(label) {
if (!errorShotDir) return DEFAULT_ERROR_SHOT_PATH;
return resolve(errorShotDir, `onec-error-${++errorShotSeq}-${slugify(label || 'shot')}.png`);
}
/**
* Build a per-context wrapper: same shape as buildContext output, but every call
@@ -69,7 +85,7 @@ export function buildContext({ noRecord = false } = {}) {
let errorShot;
try {
const png = await ctx.screenshot();
errorShot = ERROR_SHOT_PATH;
errorShot = nextErrorShotPath(name);
writeFileSync(errorShot, png);
} catch {}
// Try to fetch call stack for modal errors before throwing
@@ -127,7 +143,7 @@ export async function executeScript(code, { noRecord } = {}) {
if (!shotFile) {
try {
const png = await browser.screenshot();
shotFile = ERROR_SHOT_PATH;
shotFile = nextErrorShotPath('script');
writeFileSync(shotFile, png);
} catch {}
}
+21 -4
View File
@@ -1,4 +1,4 @@
// web-test cli/util v1.2 — generic helpers for CLI commands
// web-test cli/util v1.3 — generic helpers for CLI commands
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
export function out(obj) {
@@ -35,12 +35,29 @@ export function elapsed2(start, stop) {
return Math.round(((stop || Date.now()) - start) / 100) / 10;
}
const TRANSLIT = {
а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i',
й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't',
у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '',
э: 'e', ю: 'yu', я: 'ya',
};
/**
* ASCII-only slug for artifact file names (screenshots, videos).
* Non-ASCII names are unusable as Allure attachments: the Allure CLI silently
* fails to resolve them and emits `"size": 0` with no link to the file
* (JAVA_OPTS encoding flags do not help). Cyrillic is transliterated so the
* name stays readable; anything else non-ASCII collapses to `-`.
*/
export function slugify(s) {
return String(s).trim()
.replace(/[\s/\\:*?"<>|]+/g, '-')
const ascii = String(s).trim().toLowerCase()
.replace(/[а-яё]/g, ch => TRANSLIT[ch] ?? '-');
return ascii
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 60) || 'step';
.slice(0, 60)
.replace(/^-|-$/g, '') || 'step';
}
export function formatDuration(seconds) {