Новый канонический документ docs/web-test-regression-spec.md — техническое описание движка регрессионных тестов: CLI, формат тест-модулей, ctx-контракт, утверждения, три уровня хуков (инфра/тест/контекст), конфиг, контексты Playwright и режимы изоляции, форматы отчётов (JSON/Allure/JUnit), обнаружение тестов, ошибки/таймауты/повторы, анализ результатов, глоссарий. Документ предназначен для CI-интеграторов, ручного редактирования сгенерированных тестов и сопровождения самого движка. Без дорожной карты и внутренних self-тестов — только публичный контракт. regress.md в скилле почищен: добавлены контракт ctx и список утверждений (раньше модели приходилось читать исходники), срезаны дубликаты с SKILL.md (live recon, паттерны catalog/document), переформулированы анти-паттерны под специфику регресс-движка. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
62 KiB
Регрессионное тестирование — спецификация
Техническое описание движка регрессионных тестов: инструмент исполняет описанные кодом пользовательские сценарии в веб-клиенте прикладного решения на платформе 1С и сверяет результат с ожиданиями.
Смежные документы:
- web-test-regression-guide.md — пользовательский гайд с быстрым стартом.
- web-test-guide.md — справочник по browser-API (
clickElement,getFormState,readTable, …), который используется внутри тестов. - web-test-recording-guide.md — видеозапись, озвучка, overlays.
1. Командная строка
node run.mjs test [url] <dir|file> [флаги]
| Флаг | По умолчанию | Описание |
|---|---|---|
--tags=smoke,crud |
(все) | Фильтр тестов по тегам (пересечение) |
--grep=pattern |
(все) | Фильтр тестов по имени (регулярное выражение) |
--bail |
false | Остановиться при первом падении |
--retry=N |
0 | Повторить упавшие тесты N раз |
--timeout=ms |
30000 | Таймаут на тест (мс) |
--report=path |
(нет) | Записать отчёт в файл (JSON или XML для --format=junit) |
--format=fmt |
json | Формат отчёта: json / allure / junit |
--report-dir=path |
dirname(report) / testDir | Каталог для скриншотов, видео, Allure-результатов |
--screenshot=strategy |
on-failure | on-failure / every-step / off |
--record |
false | Записывать видео для каждого теста (mp4 в --report-dir) |
-- <hookArgs…> |
— | Всё после -- пробрасывается в _hooks.mjs как hookArgs (см. §6.1) |
URL необязателен, если в каталоге тестов есть webtest.config.mjs. CLI URL переопределяет URL дефолтного контекста.
Валидация CLI
--screenshot=<v>принимается толькоon-failure | every-step | off; при невалидном значении движок выводит ошибку и завершается с ненулевым кодом до старта прогона.--format=<v>принимается толькоjson | allure | junit; иначе — завершение с ошибкой.--format=junitтребует--report=<path>(иначе некуда писать XML); иначе — завершение с ошибкой.
Режим выполнения
- Загружается
webtest.config.mjs(если есть). - Обнаруживаются файлы
*.test.mjs, читается каждый, извлекаются метаданные. - Применяются фильтры
--tags/--grep/only. Параметризованные тесты разворачиваются. - Запускается браузер и default-контекст (
chromium.launch()либоlaunchPersistentContextв зависимости отisolation). - Тесты выполняются последовательно в алфавитном порядке относительного пути файла (внутри файла — в порядке экспорта).
- Для каждого теста: лениво создаются нужные
BrowserContext-ы (ensureContext), переключается активный, прогоняются хуки и тело, выполняется встроенный сброс состояния. - По завершении: финальная очистка контекстов с
beforeCloseContext-хуками, закрытие браузера,cleanup().
2. Формат тест-модуля
Каждый файл *.test.mjs — ES-модуль.
Экспорты
| Экспорт | Тип | Обязателен | По умолчанию | Описание |
|---|---|---|---|---|
name |
string |
да | — | Читаемое имя теста |
default |
async function(ctx, param?) |
да | — | Тело теста |
tags |
string[] |
нет | [] |
Теги для фильтрации |
timeout |
number |
нет | 30000 | Таймаут теста (мс) |
skip |
boolean | string |
нет | false | Пропустить тест (строка = причина) |
only |
boolean |
нет | false | Запустить только этот тест (отладка) |
context |
string |
нет | defaultContext | Имя контекста из файла конфигурации |
contexts |
string[] |
нет | — | Мульти-пользовательский процессный тест |
severity |
string |
нет | — | blocker / critical / normal / minor / trivial |
params |
object[] |
нет | — | Параметризация (см. §13) |
setup |
async function(ctx) |
нет | — | Подготовка перед тестом |
teardown |
async function(ctx) |
нет | — | Очистка после теста (выполняется всегда) |
Пример: тест с одним контекстом
export const name = 'CRUD справочника Контрагенты';
export const tags = ['smoke', 'crud', 'catalog'];
export const timeout = 45000;
export default async function({ navigateSection, openCommand, clickElement,
fillFields, readTable, closeForm, getFormState, assert, step, log }) {
await step('Открыть список', async () => {
await navigateSection('Склад');
await openCommand('Контрагенты');
});
await step('Создать элемент', async () => {
await clickElement('Создать');
await fillFields({ 'Наименование': 'Тест-' + Date.now() });
await clickElement('Записать и закрыть');
});
await step('Проверить в списке', async () => {
const table = await readTable();
assert.tableHasRow(table, r => r['Наименование']?.startsWith('Тест-'));
log('Элемент найден в списке');
});
}
Пример: мульти-контекстный процессный тест
Рекомендация: латинский ID контекста + кириллический displayName в webtest.config.mjs.contexts.<id>.displayName (см. §7).
export const name = 'Согласование приходной накладной';
export const contexts = ['clerk', 'manager'];
export const tags = ['process'];
export default async function({ clerk, manager, step }) {
await step('Кладовщик создаёт накладную', async () => {
await clerk.navigateSection('Склад');
await clerk.openCommand('Приходные накладные');
await clerk.clickElement('Создать');
await clerk.fillFields({ 'Контрагент': 'ООО Поставщик' });
await clerk.clickElement('Записать');
});
await step('Менеджер утверждает', async () => {
await manager.navigateSection('Согласование');
await manager.openCommand('На утверждении');
await manager.clickElement('ООО Поставщик', { dblclick: true });
await manager.clickElement('Утвердить');
});
await step('Освобождаем контекст clerk', async () => {
await manager.closeContext('clerk'); // освободить лицензию 1С
});
}
3. Объект контекста
Каждая тестовая функция получает объект контекста ctx.
API браузера (все экспорты browser.mjs)
Все функции обёрнуты авто-обнаружением 1С-ошибок (как в executeScript):
- При модальной/всплывающей ошибке 1С: скриншот →
fetchErrorStack→ исключение с заполненнымerr.onecError. - Обёрнутые ACTION_FNS:
clickElement,fillFields,fillField,selectValue,fillTableRow,deleteTableRow,openCommand,navigateSection,navigateLink,openFile,closeForm,filterList,unfilterList.
Полный список доступных функций (по группам, детальное описание — в web-test-guide.md):
Навигация: navigateSection, openCommand, switchTab, navigateLink, openFile
Состояние: getFormState, getPageState, getSections, getCommands
Таблицы: readTable, readSpreadsheet, fillTableRow, deleteTableRow
Поля: fillFields, fillField, selectValue
Действия: clickElement, closeForm, filterList, unfilterList
Ошибки: fetchErrorStack
Контексты: createContext, setActiveContext, closeContext, listContexts, hasContext, getActiveContext
Запись: startRecording, stopRecording, isRecording, addNarration, getCaptions
Презентация: showCaption, hideCaption, showTitleSlide, hideTitleSlide, showImage, hideImage, highlight, unhighlight, setHighlight, isHighlightMode
Утилиты: screenshot, wait, getPage, getSession, readFileSync, writeFileSync
dismissPendingErrors— внутренняя функция (browser.mjs), наctxне публикуется. Тест её не вызывает напрямую: она срабатывает автоматически перед каждым ACTION_FN и внутри встроенного сброса.
Тестовые утилиты
step(name, fn)— обёртка шага (см. §4)assert.*— хелперы утверждений (см. §5)log(...args)— добавить строку в вывод теста. Строки накапливаются в массив, склеиваются и попадают в JSONtests[].output. В Allure-отчётеoutputпишется вstatusDetails.traceтолько для упавших тестов; для успешных теряется (отдельного вложения не создаётся).
Метаданные теста (ctx.testInfo)
Декларативная информация о текущем тесте. Движок выставляет ctx.testInfo перед каждой попыткой (до beforeEach); хук и тело теста могут читать. Изменять не следует — объект используется самим движком при сборке отчёта.
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 — декларация теста; не зависит от текущего значения getActiveContext() (которое может меняться внутри теста).
Результат теста в afterEach (ctx.testResult)
Только в afterEach. До запуска теста — null. После — заполняется движком перед вызовом хука:
ctx.testResult = {
status, // 'passed' | 'failed'
duration, // ms
attempts, // фактически выполнено попыток (1..maxAttempts)
error, // { message, step?, screenshot?, onecError? } | null
steps, // массив step-результатов (структура — см. §4)
}
В итоговый JSON-отчёт (tests[]) добавляются ещё name, file, tags, contexts, severity, start, stop, output, screenshot, video (см. §9). В afterEach они недоступны — движок собирает финальную запись после хука.
Мульти-контекст
При export const contexts = ['a', 'b']:
ctx.aиctx.b— отдельные scoped-объекты, каждый с полным API браузера. Перед каждым вызовом scoped-обёртка переключает активный контекст черезsetActiveContext.ctx.step,ctx.assert,ctx.log,ctx.testInfo,ctx.testResultостаются на верхнем уровне.
При single-context (export const context = 'X' или дефолт) API публикуется плоско на ctx.
4. step(name, fn) — обёртка шага
await step('Имя шага', async () => {
// тело шага
});
Поведение:
- Записывает метку
startпередfn(). - Записывает метку
stopпослеfn()(успех или ошибка). - При ошибке: устанавливает
status: 'failed', прикрепляет сообщение, пробрасывает исключение. - При успехе: устанавливает
status: 'passed'. - Если стратегия скриншотов
every-step— делает скриншот послеfn(). - Вложенные шаги поддерживаются (шаг внутри шага).
- Напрямую маппится на шаги Allure.
Структура данных шага (для отчётов):
{
name: 'Имя шага',
start: 1712345678000, // мс от эпохи
stop: 1712345679200,
status: 'passed' | 'failed',
error: 'сообщение' | undefined,
screenshot: 'путь' | undefined,
steps: [] // вложенные шаги
}
5. Утверждения (assertions)
Простые хелперы без зависимостей. Бросают AssertionError со свойствами .message, .actual, .expected.
Общие
assert.ok(value, msg?) // истинность
assert.equal(actual, expected, msg?) // ===
assert.notEqual(actual, expected, msg?) // !==
assert.deepEqual(actual, expected, msg?) // сравнение через JSON
assert.includes(haystack, needle, msg?) // string/array .includes()
assert.match(string, regex, msg?) // regex.test(string)
await assert.throws(asyncFn, msg?) // ожидает исключение из async fn
Специфичные для 1С
assert.formHasField(state, fieldName, msg?)
// проверяет наличие state.fields[fieldName]; в сообщении об ошибке
// перечисляются доступные поля для быстрой диагностики
assert.formTitle(state, expected, msg?)
// проверяет, что state.title содержит expected
assert.tableHasRow(table, predicate, msg?)
// predicate: объект (частичное совпадение по ===) или функция row => bool
// объект: assert.tableHasRow(table, { 'Наименование': 'Тест' })
// функция: assert.tableHasRow(table, r => r['Сумма'] > 100)
assert.tableRowCount(table, expected, msg?)
// проверяет table.rows.length === expected
assert.noErrors(state, msg?)
// проверяет !state.errors
Расширения assert API нет. Для нестандартных проверок — throw new Error(...) или комбинация существующих хелперов.
6. Хуки
Все хуки определяются в _hooks.mjs в корне каталога тестов.
Три уровня
Инфраструктурный уровень (без браузера):
prepare({ hookArgs, log, config })— до подключения (восстановление БД, публикация, загрузка данных).cleanup({ hookArgs, log, config })— после отключения (удаление публикации, очистка).
Поля параметра:
hookArgs: string[]— всё, что в командной строке передано после разделителя--, без интерпретации со стороны движка. Хук парсит сам (см. §6.1).log: (...args) => void— функция логирования движка (структурированный вывод с префиксом[hooks]). Использовать вместоconsole.log, чтобы не ломать формат отчёта.config: object— разобранныйwebtest.config.mjs(URL контекстов, режим изоляции, правила severity и т.д.).
Тестовый уровень (с контекстом браузера):
beforeAll(ctx)— после подключения, перед первым тестом.afterAll(ctx)— после последнего теста, до отключения.beforeEach(ctx)— перед каждым тестом. На входе уже доступенctx.testInfo(см. §3).afterEach(ctx)— после каждого теста. Дополнительно доступенctx.testResultс результатом завершившегося теста.
Контекстный уровень (на каждый browser-контекст, жизненный цикл = создан → удалён):
afterOpenContext(ctx, name, spec)— сразу после успешногоcreateContext.spec— запись изconfig.contexts[name]со всеми пользовательскими полями (displayName,url,isolation, …). Полезно: вставка постоянного DOM-оверлея/бейджа, предварительная навигация в контексте, регистрация телеметрии.beforeCloseContext(ctx, name, spec)— передcloseContext(контекст ещё активен и работает). Полезно: сохранение остатков буферов, сбор метрик, последний скриншот. Срабатывает и при явномctx.closeContext(name)из теста, и в финальной очистке движка передdisconnect.
closeContext(name) валиден только когда name !== getActiveContext() — иначе бросается исключение. В scoped API (ctx.a.closeContext('b')) это естественно: scoped-обёртка сначала вызывает setActiveContext('a'), потом закрывает 'b' — целевой контекст всегда неактивен.
Подавление ошибок в хуках
Ошибки в afterEach, teardown, afterAll и cleanup ловятся и логируются движком, но не прерывают прогон и не помечают тест/прогон как failed. Логика: пост-хуки очистки должны быть устойчивы к собственным сбоям, чтобы один сломанный teardown не приводил к падению остальных тестов по цепочке. Если в этих хуках произошла фатальная для регресса проблема — бросайте отдельный Error в beforeAll/beforeEach, чтобы он прервал прогон, либо проверяйте состояние в самом тесте.
Порядок выполнения
prepare() // без браузера (восстановление БД, публикация)
browser.launch() // запуск процесса браузера
createContext(default) // первый контекст создан
afterOpenContext(ctx, default) // hook: контекст готов
beforeAll(ctx) // браузер готов, default-контекст создан
[lazy ensureContext(name)] // для multi-context тестов
afterOpenContext(ctx, name)
beforeEach(ctx)
test.setup(ctx) // подготовка теста
test.default(ctx) // тело теста (может вызвать ctx.closeContext)
[при ctx.closeContext(x)]: beforeCloseContext(ctx, x) → close(x)
test.teardown(ctx) // очистка теста (всегда)
afterEach(ctx) // всегда
[встроенный сброс] // всегда (для каждого живого контекста теста)
…следующий тест…
afterAll(ctx)
[для каждого оставшегося контекста]: beforeCloseContext(ctx, name, spec)
browser.close() // финальный disconnect (без явных closeContext —
// контексты умирают вместе с браузером)
cleanup() // без браузера (удаление публикации)
Встроенный сброс состояния
После каждого теста (после afterEach) движок гарантирует чистое состояние:
async function resetState(ctx) {
try { await ctx.dismissPendingErrors(); } catch {} // no-op на ctx (не экспортируется);
// внутренний dismiss всё равно отработает
// через ACTION_FN-обёртки ниже
for (let i = 0; i < 10; i++) {
const state = await ctx.getFormState();
if (state.form == null) break; // важно: == null, не !state.form —
// form может быть 0 (валидный idx фоновой формы)
try { await ctx.closeForm({ save: false }); } catch { break; }
}
}
Гарантирует, что каждый тест стартует с чистого рабочего стола, независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). Реимплементировать это в пользовательском afterEach не нужно.
Пример _hooks.mjs
import { execSync } from 'child_process';
export async function prepare({ hookArgs, log, config }) {
const force = hookArgs.includes('--rebuild-stand');
const dataArg = hookArgs.find(a => a.startsWith('--data='))?.slice('--data='.length);
log('preparing stand, force=', force, 'data=', dataArg);
execSync('powershell.exe -File scripts/restore-db.ps1');
execSync('powershell.exe -File scripts/publish.ps1');
}
export async function cleanup({ log }) {
log('cleaning up stand');
execSync('powershell.exe -File scripts/unpublish.ps1');
}
export async function beforeAll(ctx) {
// По умолчанию 1С после входа уже показывает дефолтную секцию — навигация
// в beforeAll обычно не нужна. Хук удобен для общего setup'а который
// должен случиться один раз для всего прогона.
}
export async function afterEach(ctx) {
// Доступен ctx.testResult — { status, duration, attempts, error, steps }.
// Встроенный сброс состояния выполняется ПОСЛЕ afterEach автоматически.
}
export async function afterOpenContext(ctx, name, spec) {
// Удобно для persistent DOM-overlay'я с displayName (видно в видео,
// какая вкладка к какому пользователю относится).
}
export async function beforeCloseContext(ctx, name, spec) {
// Срабатывает и при ctx.closeContext из теста, и в финальной очистке.
}
6.1. Проброс пользовательских флагов через --
Движок не знает о пользовательских флагах хуков. Чтобы хуки получили разовые параметры без правки webtest.config.mjs или окружения, используется стандартная shell-конвенция -- (как у npm, cargo, pytest): всё, что идёт после -- в CLI движка, передаётся в prepare / cleanup через поле hookArgs: string[] без интерпретации.
node run.mjs test tests/myapp/ --bail -- --rebuild-stand --reload-data
└─ runner ─┘ └────── hookArgs ────────┘
В этом примере движок получает --bail, а hookArgs хуков становится ['--rebuild-stand', '--reload-data']. Парсинг этого массива — ответственность хуков.
Если разделитель -- не указан, hookArgs — пустой массив. Это позволяет движку и хукам развиваться независимо: новый встроенный флаг движка никогда не пересечётся с пользовательским.
7. Файл конфигурации
webtest.config.mjs в корне каталога тестов. Необязателен — если отсутствует, URL должен быть передан через CLI.
export default {
// Контексты: именованные URL для разных пользователей/ролей.
// Рекомендация: латинский ID контекста (`clerk`, `manager`) + кириллический
// `displayName` для UI/слайдов. Любые пользовательские поля пробрасываются как есть
// и доступны хукам через `ctx.testInfo.contexts[name]` (см. §3).
contexts: {
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: 'clerk',
// Значения по умолчанию (переопределяются флагами CLI)
timeout: 30000,
retries: 0,
screenshot: 'on-failure', // 'every-step' | 'off'
record: false,
// Дефолтный тег-фильтр. Применяется только если CLI не передал --tags.
// Удобно для сценариев «прогон по умолчанию = smoke», при этом --tags=full
// (или --tags=) с CLI прозрачно перекрывает.
tags: ['smoke'],
// Дефолтный режим изоляции для контекстов, которые сами его не указали
// (config.contexts.<name>.isolation). См. §8.
isolation: 'tab', // 'tab' | 'window'
// Allure severity policy (опционально). Маппинг наоборот: уровень → [теги].
// Резолв см. §9 «Авто-эмиссия label-ов».
severity: {
critical: ['smoke', 'multi-context'],
minor: ['recording'],
// blocker / trivial — необязательны, можно опустить
},
defaultSeverity: 'normal',
};
Упрощённая форма (один контекст, без именованных):
export default {
url: 'http://localhost/app/ru_RU',
timeout: 30000,
};
Валидация файла конфигурации
severity валидируется при загрузке:
- ключи — только из
blocker | critical | normal | minor | trivial; - значение каждого ключа — массив тегов;
- тег не может одновременно состоять в двух уровнях severity (явная ошибка с указанием конфликта);
defaultSeverity— из стандартного набора.
При нарушении любого правила движок выводит сообщение с указанием конфликта и завершается с ненулевым кодом до запуска тестов.
Кириллица в ID контекстов работает, но смешанный регистр снижает читаемость кода (testInfo.contexts.кладовщик.displayName рядом с testInfo.contexts.clerk.displayName). Рекомендуем разделять технический ID и человекочитаемое имя.
Флаги CLI всегда переопределяют значения из файла конфигурации.
8. Контексты
Механизм: Playwright BrowserContext
Один процесс браузера (chromium.launch()), несколько изолированных контекстов. Каждый контекст — отдельная сессия (куки, авторизация, состояние страницы).
browser (один процесс chromium)
├─ BrowserContext "кладовщик" → page → http://localhost/app-clerk/ru_RU
├─ BrowserContext "менеджер" → page → http://localhost/app-mgr/ru_RU
└─ BrowserContext "админ" → page → http://localhost/app-admin/ru_RU
Преимущества:
- Мгновенное переключение между пользователями (смена активного
page). - Состояние сохраняется — переключились на менеджера и обратно, у кладовщика все формы остались открытыми.
- Нет переподключений — каждая сессия живёт независимо.
- Один процесс — экономия ресурсов по сравнению с несколькими браузерами.
Одиночный контекст (по умолчанию)
Большинство тестов. Один BrowserContext, один пользователь. Тест получает плоский ctx со всем API.
export const context = 'manager'; // необязательно, иначе defaultContext
export default async function({ clickElement, fillFields, … }) { }
Порядок выполнения и переключение контекста
Движок НЕ группирует тесты по контексту. Порядок выполнения — алфавитный по полному относительному пути файла (плюс порядок экспорта внутри файла). Для каждого теста:
- Через
ensureContext(name)создаются BrowserContext-ы, упомянутые вt.context/t.contexts(если ещё не созданы). setActiveContext(primaryContext)— активный контекст = первый объявленный (для single —t.context || defaultContext, для multi —t.contexts[0]).- После теста встроенный сброс пробегает по всем использованным контекстам.
Контексты живут между тестами: переключение через setActiveContext — дешёвое, повторный вход в 1С не требуется. Закрываются явно (closeContext) или финальной очисткой движка перед закрытием браузера.
Мульти-контекст (процессные тесты)
export const contexts = ['clerk', 'manager'];
export default async function({ clerk, manager, step, assert }) { … }
Каждый именованный контекст — полноценный scoped-объект API со своим page. Тест оркестрирует переключение между пользователями. Состояние каждого пользователя сохраняется между переключениями:
await step('Кладовщик создаёт документ', async () => {
await clerk.openCommand('Приходные накладные');
await clerk.clickElement('Создать');
await clerk.fillFields({ 'Контрагент': 'ООО Поставщик' });
await clerk.clickElement('Записать');
// кладовщик стоит на форме документа
});
await step('Менеджер утверждает', async () => {
await manager.navigateSection('Согласование');
await manager.clickElement('Утвердить');
});
await step('Кладовщик проверяет статус', async () => {
// страница кладовщика ТА ЖЕ — форма открыта, навигация не нужна
const state = await clerk.getFormState();
assert.equal(state.fields['Статус']?.value, 'Утверждён');
});
Публичный контекстный API
| Метод | Назначение |
|---|---|
createContext(name, url, { isolation, extensionPath }) |
Создаёт BrowserContext и переходит по URL. |
setActiveContext(name) |
Переключает активный слот; при активной записи дописывает последние кадры старой страницы и переподключает screencast. |
closeContext(name) |
Выход из 1С + закрытие (page для tab, BrowserContext для window), удаляет из реестра. Бросает исключение, если name === active. |
listContexts() / hasContext(name) / getActiveContext() |
Только для чтения. |
Режимы изоляции
Поле isolation задаётся в двух местах:
- На уровне контекста:
config.contexts.<name>.isolation— приоритет 1. - На уровне файла конфигурации:
config.isolation— применяется к контекстам, у которых своего значения нет. По умолчанию'tab'.
| Режим | Реализация | Окна | Cookies | 1С-расширение |
|---|---|---|---|---|
'tab' (default) |
launchPersistentContext + newPage() per context |
1 окно, N вкладок | общие по path | загружается надёжно |
'window' |
chromium.launch() + newContext() per context |
N окон | полная изоляция | может не загружаться |
Смешивать режимы в одном прогоне нельзя — createContext бросает явную ошибку. То есть config.isolation фактически становится режимом всего прогона, если хотя бы один контекст явно не переопределил его на тот же режим.
Закрытие неактивных контекстов
closeContext(name) нельзя вызвать на активном контексте — будет исключение. В scoped API это естественно: вызывать manager.closeContext('clerk') (scoped-обёртка сначала переключает активный на manager, потом закрывает clerk). Если контекст лишний (роль больше не нужна в рамках теста / прогона) — закрывайте его сразу: освобождает лицензию платформы и снимает нагрузку со следующих тестов.
9. Отчёты
JSON (нативный, по умолчанию)
{
"runner": "web-test",
"url": "http://localhost/app/ru_RU",
"startedAt": "2026-04-05T10:00:00.000Z",
"finishedAt": "2026-04-05T10:05:30.000Z",
"duration": 330.0,
"summary": {
"total": 25,
"passed": 23,
"failed": 1,
"skipped": 1
},
"tests": [
{
"name": "CRUD справочника Контрагенты",
"file": "02-catalog-crud.test.mjs",
"tags": ["smoke", "crud"],
"contexts": ["clerk"],
"severity": "critical",
"status": "passed",
"start": 1712345678000,
"stop": 1712345690300,
"duration": 12.3,
"attempts": 1,
"steps": [
{
"name": "Открыть список",
"start": 1712345678000,
"stop": 1712345679200,
"status": "passed",
"steps": []
}
],
"output": "Элемент найден в списке",
"error": null,
"screenshot": null,
"video": null
},
{
"name": "Обязательное поле",
"file": "10-validation.test.mjs",
"tags": ["validation"],
"contexts": ["clerk"],
"status": "failed",
"duration": 8.1,
"attempts": 2,
"steps": [
{
"name": "Сохранить пустую форму",
"start": 1712345700000,
"stop": 1712345708100,
"status": "failed",
"error": "Ожидалось модальное окно ошибки, но форма сохранилась"
}
],
"output": "",
"error": {
"message": "Ожидалось модальное окно ошибки, но форма сохранилась",
"step": "Сохранить пустую форму",
"screenshot": "error-shot-10.png"
},
"screenshot": "error-shot-10.png"
}
]
}
Allure (--format=allure --report-dir=allure-results/)
Отдельные JSON-файлы для каждого теста в каталоге allure-results/:
{
"uuid": "сгенерированный-uuid",
"name": "CRUD справочника",
"fullName": "02-catalog-crud.test.mjs",
"status": "passed",
"stage": "finished",
"start": 1712345678000,
"stop": 1712345690300,
"labels": [
{ "name": "tag", "value": "smoke" },
{ "name": "tag", "value": "crud" },
{ "name": "suite", "value": "root" },
{ "name": "severity", "value": "critical" }
],
"steps": [
{
"name": "Открыть список",
"status": "passed",
"start": 1712345678000,
"stop": 1712345679200,
"steps": []
}
],
"attachments": [
{
"name": "Скриншот при падении",
"source": "uuid-attachment.png",
"type": "image/png"
}
]
}
Скриншоты/видео копируются в allure-results/ с уникальными именами.
Авто-эмиссия меток
Движок всегда заполняет следующие метки (labels):
-
tag— по одному на каждый элементmod.tags[]. Готовая фильтрация в Allure-отчёте без дополнительной разметки. -
suite—dirname(t.filePath). Тесты в корнеtestDirидут под'root', тесты в подкаталогеsales/— под'sales'. Это даёт левую группировку отчёта без ручной разметки. -
severity— резолв в порядке приоритета:export const severity = 'critical'в самом тесте, если значение валидное (одно изblocker | critical | normal | minor | trivial). Если экспорт задан, но значение невалидное — пункт пропускается и идём в (3); резолв через теги (пункт 2) при этом не выполняется (хотел бы автор иначе — он бы не объявлялseverity).- Иначе максимальный ранг среди тегов теста (стандартные имена
blocker | critical | normal | minor | trivialнапрямую, либо черезconfig.severity-маппинг). - Иначе
config.defaultSeverityили'normal'.
Ранги:
blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1). Выбор по максимуму не зависит от порядка тегов вmod.tags.
Пример: tags: ['smoke', 'recording'] + severity: { critical: ['smoke'], minor: ['recording'] } → severity = critical (5 > 2).
Доп. файлы Allure через <testDir>/_allure/
Движок ищет каталог _allure/ рядом с тестами и копирует все его файлы в reportDir перед генерацией отчёта. Конвенция для статичной настройки Allure, для которой нет места внутри JSON-файла теста:
| Файл | Назначение |
|---|---|
categories.json |
Классификация падений по regex (группировка failed-тестов в виджете Categories — «timeout», «license-flake», «1C modal» и т.п.). |
environment.properties |
key=value строки в виджет Environment (URL, версия 1С, ветка git, номер сборки). Часто формируется динамически из prepare(). |
executor.json |
CI/CD-метаданные (Jenkins URL, GitHub run-id и т.п.). |
Подчёркивание в имени — параллель _hooks.mjs (инфраструктура, не тест). Сборщик тестов пропускает каталог _allure/ по общему правилу (startsWith('_')). Если каталога нет — ничего не происходит, отчёт собирается обычным образом.
Пример categories.json (минимальный):
[
{ "name": "Timeout", "messageRegex": "Timeout \\(\\d+ms\\)" },
{ "name": "Assertion", "messageRegex": "(Expected|AssertionError).*" }
]
JUnit XML (--format=junit)
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="web-test" tests="25" failures="1" skipped="1" time="330.0">
<testsuite name="tests/myapp" tests="25" failures="1" skipped="1">
<testcase name="CRUD справочника" classname="02-catalog-crud.test.mjs" time="12.3"/>
<testcase name="Обязательное поле" classname="10-validation.test.mjs" time="8.1">
<failure message="Ожидалось модальное окно ошибки, но форма сохранилась">
Стек вызовов…
</failure>
<system-out>Скриншот: error-shot-10.png</system-out>
</testcase>
</testsuite>
</testsuites>
10. Консольный вывод
web-test — http://localhost/app/ru_RU
Запуск 25 тестов из tests/myapp/
✓ Навигация по разделам (2.1s)
✓ CRUD справочника Контрагенты (12.3s)
├ Открыть список (1.2s)
├ Создать элемент (8.0s)
└ Проверить в списке (3.1s)
✗ Обязательное поле (8.1s)
├ Открыть форму (2.0s)
└ ✗ Сохранить пустую форму (6.1s)
Ожидалось модальное окно ошибки, но форма сохранилась
скриншот: error-shot-10.png
○ Составной тип (skip: не реализовано)
23 passed, 1 failed, 1 skipped (2m 0.5s)
Для passed-тестов выводится одна строка ✓ name (duration). Шаги печатаются только для упавших — после строки ✗, с отступом, плюс сообщение ошибки и путь к скриншоту. Полная картина по шагам — в JSON-отчёте (--report=…).
11. Скриншоты и видео
Стратегия скриншотов
| Стратегия | Поведение |
|---|---|
on-failure (по умолчанию) |
Скриншот при падении теста, прикрепляется к ошибке. |
every-step |
Скриншот в конце каждого step(), плюс при падении. |
off |
Без автоматических скриншотов. |
Скриншоты сохраняются в каталог отчёта по шаблону {индекс-теста}-{имя-шага}.png. В JSON-отчёте — путь относительно каталога отчёта.
Видеозапись
При включённом --record:
startRecording()перед каждым тестом.stopRecording()после каждого теста.- Видео сохраняется как
{индекс-теста}-{имя-теста}.mp4. - Прикрепляется к отчёту (Allure: вложение видео).
Подробности по записи (overlays, captions, narration) — см. web-test-recording-guide.md.
12. Сброс состояния
Встроенный механизм, выполняется после afterEach (и teardown) каждого теста. Псевдокод и условие выхода — в §6 «Встроенный сброс состояния».
Для мульти-контекстных тестов сброс пробегает по всем живым контекстам, использованным тестом.
Гарантирует, что каждый тест стартует с чистого рабочего стола, независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения).
13. Параметризация
export const name = 'Заполнение поля {type}';
export const params = [
{ type: 'String', field: 'Наименование', value: 'Тест' },
{ type: 'Number', field: 'Цена', value: '100.50' },
{ type: 'Date', field: 'ДатаПоступления', value: '01.01.2024' },
{ type: 'Boolean',field: 'Активен', value: true },
];
export default async function({ fillFields, getFormState, assert }, { type, field, value }) {
await fillFields({ [field]: value });
const state = await getFormState();
assert.equal(state.fields[field]?.value, String(value));
}
Параметры разворачиваются в отдельные тесты на этапе discovery:
- Имя теста формируется подстановкой через шаблон
{key}вmod.name; если шаблона нет — суффикс[index]. - Тест получает
paramвторым аргументом (default(ctx, param)). - В отчётах каждый набор — отдельная запись со своим
nameиparamвtestInfo. ctx.testInfo.paramдоступен в теле теста и хуках.
14. Обнаружение тестов
testDir (первый позиционный аргумент после URL) — каталог, в котором живут тесты. Сборщик рекурсивно обходит дерево и собирает файлы по правилам ниже.
tests/myapp/
_hooks.mjs # пропускается (префикс '_')
_allure/ # пропускается (префикс '_')
webtest.config.mjs # пропускается (не *.test.mjs)
sales/
01-order-create.test.mjs
02-order-post.test.mjs
warehouse/
01-receipt.test.mjs
Правила
| Аспект | Поведение |
|---|---|
| Обход | Рекурсивный; файлы и каталоги, имя которых начинается на _ или ., пропускаются |
| Шаблон имени | Только *.test.mjs |
| Порядок | Сортировка по полному относительному пути (sales/01 идёт до warehouse/01) |
file в отчёте |
relative(testDir, file) с разделителем /, например sales/01-order-create.test.mjs |
| Фильтр по пути с CLI | node run.mjs test tests/myapp/sales/ запустит только подкаталог |
| Конкретный файл | node run.mjs test tests/myapp/sales/01-order-create.test.mjs |
Чего НЕТ (сознательное упрощение)
_hooks.mjsна уровне подкаталога. Движок ищет_hooks.mjsтолько в корнеtestDir. Подкаталоги свои хуки не получают.webtest.config.mjsна уровне подкаталога. Тоже только в корне.- Многоуровневой Suite-разметки из дерева каталогов. Allure-метка
suiteстроится только по первому уровню (dirname(filePath)); более глубокую группировку делайте черезtags. - Контекста по умолчанию на уровне подкаталога. Каждый тест объявляет
context/contextsсам; от пути контексты не наследуются.
Конвенции
- Папки — для организации, не для механики. Общая подготовка — в глобальном
_hooks.mjs.beforeAllили вsetup/teardownконкретного теста. - Группировку в отчётах делайте через
tags: ['sales'], не через путь. Это даёт фильтрацию (--tags=sales) и работает в Allure/JUnit без дополнительной разметки. - «Запустить только sales» — двумя путями:
tests/myapp/sales/(по каталогу) или--tags=sales(по тегу). - Сортировка по полному пути означает, что
warehouse/01-xзапустится ПОСЛЕsales/02-y. Для строгого глобального порядка используйте 3-значные префиксы (010-/020-/…) либо явные теги-фазы.
15. Ошибки и трассировка
Авто-обнаружение 1С-ошибок
Все ACTION_FNS (clickElement, fillFields, fillField, selectValue, fillTableRow, deleteTableRow, openCommand, navigateSection, navigateLink, openFile, closeForm, filterList, unfilterList) обёрнуты. После каждого вызова:
- Проверяется
state.errors.modal/balloon. - Если есть — делается скриншот (до того, как
fetchErrorStackзакроет модалку). - Для модальных ошибок вызывается
fetchErrorStack(две стратегии — Path 1 для платформенных исключений с кнопкой «Открыть отчёт», Path 2 дляВызватьИсключениечерез гамбургер-меню → О программе → Информация для тех. поддержки; см. web-test-guide.md). - Бросается исключение со структурированным
err.onecError:err.onecError = { step, // имя действия (например 'clickElement') args, // аргументы, с которыми вызывалось errors, // { modal?, balloon? } formState, // снапшот getFormState stack, // { raw, entries: [{ location, code }], timestamp } | null screenshot, // путь к скриншоту };
В отчёте это превращается в error.onecError.stack для упавшего теста. Разбор причин падения и категории — см. §16.
Платформенные модальные диалоги
getFormState() возвращает platformDialogs — массив платформенных диалогов (About, Support Info, Error Report). closeForm() закрывает их. dismissPendingErrors() чистит ожидающие модалки автоматически (вызывается перед каждым ACTION_FN, плюс в встроенном сбросе после теста).
Модальное окно платформенной ошибки сначала рендерится в переходном состоянии (~1 с), затем перерисовывается в стабильное. fetchErrorStack ждёт 1.5 с и перепроверяет hasReport перед выбором стратегии.
Таймауты
- Глобальный таймаут теста:
mod.timeoutилиconfig.timeoutили CLI--timeout=ms. - Таймаут срабатывает на уровне теста (
testFn()+setup+teardown), не на уровне отдельногоstepили action. - При таймауте: текущий step помечается failed, бросается ошибка с сообщением
Timeout (<N>ms), далее запускаетсяafterEachи встроенный сброс.
Повторы
При --retry=N (или config.retries) упавший тест повторяется до 1 + N раз. Для каждой попытки:
beforeEach/setup/default/teardown/afterEach+ встроенный сброс выполняются заново.ctx.testInfo.attemptинкрементируется.- В отчёте фиксируется
attempts— фактически выполнено попыток. - Считается passed, если последняя попытка зелёная; иначе failed.
beforeAll / afterAll / prepare / cleanup / afterOpenContext / beforeCloseContext не повторяются (это жизненный цикл всего прогона или контекста, не теста).
16. Анализ результатов
Что лежит в записи об упавшем тесте
JSON-отчёт (tests[], полная структура — §9) для каждого падения содержит:
error.message— текст исключения.error.step— имя шага, на котором упало.error.screenshot— путь к скриншоту падения (если стратегия скриншотов неoff).error.onecError(только для 1С-исключений) — структура с полями:step(имя действия, напримерclickElement),args(аргументы вызова),errors(модальное окно или balloon),formState(снимок формы на момент ошибки),stack— платформенный стек вызовов 1С сentries[{location, code}].steps[]— пошаговая разбивка с метками времени, у каждого шага свойstatusиerror.
В Allure-отчёте те же данные лежат в statusDetails (текст ошибки и трассировка), скриншоты и видео — во вложениях, автоматическая группировка по причинам — через categories.json (§9).
Типовые причины падений
Большинство падений на 1С-стенде сводится к трём причинам, и их полезно различать при разборе отчёта:
- Ошибка в тесте — селектор не нашёл элемент, ожидание не сошлось, гонка без точки синхронизации. Признаки: падение стабильно повторяется на одном и том же шаге; после правки теста воспроизводимость исчезает. Действие — изменить тест.
- Ошибка в прикладном решении — реально воспроизведённое некорректное поведение конфигурации. Признаки: упал шаг, имитирующий пользовательскую операцию; в
error.onecError.stackесть платформенный стек вызовов 1С с указанием на код решения. Действие — передать разработчику конфигурации, тест править не нужно. - Сбой стенда — таймаут Apache, форма входа не загрузилась, не хватило веб-лицензий. Признаки: падение на навигации или входе; от прогона к прогону падает «то одно, то другое», без связи с содержанием теста. Действие — править инфраструктуру (
prepare(), очистка сессий, идемпотентность хуков), не тесты.
categories.json Allure (§9) удобно настраивать именно под эти три категории — regex по error.message уже даёт первичную классификацию в виджете Categories.
17. Глоссарий
| Термин | Определение |
|---|---|
| testDir | Каталог тестов, переданный позиционным аргументом движку. Корень для discovery, _hooks.mjs, webtest.config.mjs, _allure/. |
| Context (BrowserContext) | Изолированная сессия Playwright. Куки/состояние/страница независимы. В рамках одного теста используется один или несколько контекстов. |
| Active context | Контекст, на котором сейчас оперируют функции browser-API. Переключается setActiveContext. |
| Primary context | Контекст, активный на входе в тест. Декларация (mod.context или mod.contexts[0]). Зафиксирован в testInfo.primaryContext. |
| Default context | Контекст из config.defaultContext (или единственный URL в упрощённой конфигурации). Используется, если тест не указал context / contexts. |
| Scoped API | Объект на ctx.<name> в мульти-контекстных тестах — обёртки browser-функций, авто-переключающие контекст перед каждым вызовом. |
| Action function (ACTION_FN) | Browser-функция, обёрнутая авто-обнаружением 1С-ошибок. Список — в §3. |
| Step | Логический блок внутри теста, обёрнутый step(name, fn). Маппится на Allure-step, попадает в report.tests[].steps[]. |
| Reset state | Встроенная пост-тестовая очистка: dismissPendingErrors + закрытие всех открытых форм до рабочего стола. Выполняется после afterEach. |
| hookArgs | Массив строк, переданных в prepare / cleanup после CLI-разделителя --. Движком не интерпретируются. |
| Severity | Уровень критичности теста (blocker / critical / normal / minor / trivial) для Allure. Резолвится из mod.severity, тегов, config.severity, config.defaultSeverity. |
См. также
- web-test-guide.md — browser API (
clickElement,getFormState,readTable, …) и интерактивный режим. - web-test-recording-guide.md — видеозапись, captions, narration, overlays.
- web-test-regression-guide.md — пользовательский гайд (на русском, с быстрым стартом).
/web-testskill —.claude/skills/web-test/SKILL.md,regress.md(рабочая шпаргалка для модели).