From b992cd11c580a039eda9435453f8e08f50e711f3 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 13 May 2026 18:53:09 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20=5Fallure/=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=B2=D0=B5=D0=BD=D1=86=D0=B8=D1=8F=20+=20categories.jso?= =?UTF-8?q?n=20=D0=B4=D0=BB=D1=8F=20=D1=82=D1=80=D0=B8=D0=B0=D0=B6=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run.mjs: - syncAllureExtras(testDir, reportDir) копирует все файлы из /_allure/ в reportDir перед генерацией отчёта. Underscore в имени параллелен _hooks.mjs (инфра, не тест) — discovery его пропускает. - Вызов после writeAllure при --format=allure. tests/web-test/_allure/categories.json — 7 правил классификации падений по нашему 1С-домену: 1. License pool exhausted (1C) — известный multi-context flake. 2. 1C application error (modal) — exception modal через fetchErrorStack. 3. Section panel icon-only — деградация состояния стенда. 4. Navigation lookup miss — navigateSection/openCommand/navigateLink/switchTab. 5. Element not found — clickElement/fillField/selectValue/closeForm/fillTableRow/deleteTableRow. 6. Test timeout — Timeout (Nms) от раннера. 7. Assertion failure — наши createAssertions + 1С-specific (formHasField/tableHasRow/noErrors). spec §9: раздел «Доп. файлы Allure через /_allure/» с таблицей поддерживаемых типов (categories.json / environment.properties / executor.json) и минимальным примером. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/run.mjs | 25 ++++++++++++++++- docs/web-test-runner-spec.md | 26 +++++++++++++++++ tests/web-test/_allure/categories.json | 37 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/web-test/_allure/categories.json diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index 340bfd12..90622990 100644 --- a/.claude/skills/web-test/scripts/run.mjs +++ b/.claude/skills/web-test/scripts/run.mjs @@ -18,7 +18,7 @@ */ import http from 'http'; import * as browser from './browser.mjs'; -import { readFileSync, writeFileSync, unlinkSync, existsSync, readdirSync, mkdirSync } from 'fs'; +import { readFileSync, writeFileSync, unlinkSync, existsSync, readdirSync, mkdirSync, copyFileSync, statSync } from 'fs'; import { resolve, dirname, basename, relative } from 'path'; import { fileURLToPath } from 'url'; import { randomUUID } from 'crypto'; @@ -821,6 +821,7 @@ async function cmdTest(rawArgs) { if (opts.format === 'allure') { writeAllure(results, reportDir, severityIndex); + syncAllureExtras(testDir, reportDir); } else if (opts.format === 'junit') { writeFileSync(resolve(opts.report), buildJUnit(report, testDir)); } else if (opts.report) { @@ -830,6 +831,28 @@ async function cmdTest(rawArgs) { if (failCount > 0) process.exit(1); } +/** + * Copy any files from `/_allure/` into `reportDir`. Convention for + * Allure customization that doesn't fit inside per-test JSON: + * - `categories.json` — failure classification (regex → bucket) + * - `environment.properties` — values shown in the Environment widget + * - `executor.json` — CI/CD metadata + * Underscored folder mirrors `_hooks.mjs` convention (infra, not a test). + * Silent if folder absent. + */ +function syncAllureExtras(testDir, reportDir) { + const extrasDir = resolve(testDir, '_allure'); + if (!existsSync(extrasDir)) return; + try { + if (!statSync(extrasDir).isDirectory()) return; + } catch { return; } + for (const entry of readdirSync(extrasDir, { withFileTypes: true })) { + if (!entry.isFile()) continue; + try { copyFileSync(resolve(extrasDir, entry.name), resolve(reportDir, entry.name)); } + catch { /* best-effort */ } + } +} + function writeAllure(results, reportDir, severityIndex) { for (const tr of results) { if (tr.status === 'skipped') continue; // Allure ignores skipped without start/stop diff --git a/docs/web-test-runner-spec.md b/docs/web-test-runner-spec.md index 696c2c2f..82288dad 100644 --- a/docs/web-test-runner-spec.md +++ b/docs/web-test-runner-spec.md @@ -752,6 +752,32 @@ await step('Кладовщик проверяет статус', async () => { Пример: `tags: ['smoke', 'recording']` + `severity: { critical: ['smoke'], minor: ['recording'] }` → severity = `critical` (5 > 2). +#### Доп. файлы Allure через `/_allure/` + +Раннер ищет каталог `_allure/` рядом с тестами и копирует все его файлы в +`reportDir` перед генерацией отчёта. Конвенция для статичной настройки +Allure, которой нет места внутри per-test JSON: + +| Файл | Назначение | +|------|-----------| +| `categories.json` | Классификация падений по regex (группировка failed-тестов в виджете Categories — «timeout», «license-flake», «1C modal» etc.) | +| `environment.properties` | `key=value` строки в виджет Environment (URL, версия 1С, ветка git, build-номер) | +| `executor.json` | CI/CD-метаданные (Jenkins URL, GitHub run-id и т.п.) | + +Underscore в имени — параллель `_hooks.mjs` (инфраструктура, не тест). +Discovery каталог `_allure/` пропускает по общему правилу (`startsWith('_')`). +Если каталога нет — no-op. + +Пример `categories.json` (минимальный): +```json +[ + { "name": "Timeout", "messageRegex": "Timeout \\(\\d+ms\\)" }, + { "name": "Assertion", "messageRegex": "(Expected|AssertionError).*" } +] +``` + +Полный пример с 1С-специфичными паттернами — см. `tests/web-test/_allure/categories.json`. + ### JUnit XML (`--format=junit`) ```xml diff --git a/tests/web-test/_allure/categories.json b/tests/web-test/_allure/categories.json new file mode 100644 index 00000000..2cb13af2 --- /dev/null +++ b/tests/web-test/_allure/categories.json @@ -0,0 +1,37 @@ +[ + { + "name": "License pool exhausted (1C)", + "matchedStatuses": ["failed", "broken"], + "messageRegex": ".*Не обнаружено свободной лицензии.*" + }, + { + "name": "1C application error (modal)", + "matchedStatuses": ["failed"], + "messageRegex": ".*(ВызватьИсключение|В поле введены некорректные данные|Произошла ошибка|Ошибка при вызове).*" + }, + { + "name": "Section panel icon-only (stand state)", + "matchedStatuses": ["failed"], + "messageRegex": ".*icon-only mode.*" + }, + { + "name": "Navigation lookup miss", + "matchedStatuses": ["failed"], + "messageRegex": ".*(navigateSection|openCommand|navigateLink|switchTab).*not found.*" + }, + { + "name": "Element not found", + "matchedStatuses": ["failed"], + "messageRegex": ".*(clickElement|fillField|fillFields|selectValue|closeForm|fillTableRow|deleteTableRow).*not found.*" + }, + { + "name": "Test timeout", + "matchedStatuses": ["failed", "broken"], + "messageRegex": "Timeout \\(\\d+ms\\)" + }, + { + "name": "Assertion failure", + "matchedStatuses": ["failed"], + "messageRegex": "(Expected|AssertionError|Field \".*\" not found in form|Form title .*does not contain|No row matching predicate|Form has errors).*" + } +]