From f4748d76af367888088ad85a1bc58886a8610c70 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 13 May 2026 20:25:44 +0300 Subject: [PATCH] =?UTF-8?q?docs(web-test):=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B3=D0=B0=D0=B9=D0=B4=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B0=20+=20skill-=D0=B8=D0=BD=D1=81?= =?UTF-8?q?=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8=D1=8F=20regress.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/web-test-regression-guide.md — пользовательские сценарии работы с моделью для покрытия прикладного решения регрессом (русский, по аналогии с web-test-recording-guide.md): структура tests//, диалоги с моделью, пример организации покрытия, отчёты Allure + categories.json. - .claude/skills/web-test/regress.md — инструкция модели по написанию регрессионного набора: разведка (метаданные + живой проход через exec), layout по фичам, готовые шаблоны (CRUD/document/DCS/multi-user/repro), severity, anti-patterns, failure triage, _allure/ конвенция. - SKILL.md — указатель на regress.md в конце файла (рядом с recording). - docs/web-test-runner-spec.md → upload/ (был внутренним планом разработки, не пользовательской документацией). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/SKILL.md | 4 + .claude/skills/web-test/regress.md | 433 ++++++++++++ docs/web-test-regression-guide.md | 391 +++++++++++ docs/web-test-runner-spec.md | 1051 ---------------------------- 4 files changed, 828 insertions(+), 1051 deletions(-) create mode 100644 .claude/skills/web-test/regress.md create mode 100644 docs/web-test-regression-guide.md delete mode 100644 docs/web-test-runner-spec.md diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 4924db26..6a1c52b2 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -529,3 +529,7 @@ On error (auto-screenshot taken): - **Cyrillic in bash** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid escaping issues - **Non-breaking spaces** — 1C uses `\u00a0` instead of regular spaces. All matching is normalized internally - **Section panel display** — `navigateSection()` works with any panel position (side, top) but requires "Picture and text" or "Text" display mode. Icon-only mode is not supported — API cannot read section names from icons alone + +## Regression suites + +When the user asks to cover a 1C solution with automated regression — multi-file test suites with assertions, hooks, tags, retries, Allure/JUnit reports, multi-user process tests — switch to the `test` mode. See [regress.md](regress.md) for authoring discipline, recon flow (metadata + live walkthrough via `exec`), per-application folder layout, ready-to-paste templates, and failure triage. Default to ad-hoc `run`/`exec` for single-script automation — `test` is the specialised mode for project-wide coverage. diff --git a/.claude/skills/web-test/regress.md b/.claude/skills/web-test/regress.md new file mode 100644 index 00000000..086316df --- /dev/null +++ b/.claude/skills/web-test/regress.md @@ -0,0 +1,433 @@ +# Regression suite authoring + +Use this when the user asks to cover a 1C solution with automated regression tests, build out a test suite, or run an existing suite and analyse failures. For ad-hoc single-script automation, stay with the `run`/`exec` modes from SKILL.md instead. + +The runner is the same `run.mjs`. The mode is `test`: + +```bash +node $RUN test [url] [flags] +``` + +Tests live next to the project they cover (not inside the skill). Convention: `tests/` at the project root, with `_hooks.mjs` and `webtest.config.mjs` at the suite root. Tests are ES modules with `*.test.mjs` suffix. + +## When to choose `test` over `exec` + +| Goal | Mode | +|------|------| +| Explore a form, prototype a single step, debug one selector | `exec` (interactive session) | +| **Walk through a scenario live before committing it as a test** | `exec` first, then `test` | +| Reproduce a bug as a failing test before fixing it | `test` | +| Cover a feature so future changes are checked automatically | `test` | +| Run the project's regression on a new build | `test` | +| Generate a screencast walkthrough | `exec` with `startRecording` | + +Don't write a `.test.mjs` for a one-shot user request. Don't drive a regression suite through chained `exec` calls. + +## Before writing tests — recon + +Two layers, in order. Don't skip either. + +### 1. Static recon — metadata + +Never invent identifiers. For every metadata object the user mentions (or that you decide to cover), run the matching info skill first: + +| Object type | Skill | +|-------------|-------| +| Catalog/document/register attributes, tabular sections | `/meta-info` | +| Form layout — fields, buttons, tabs, tables | `/form-info` | +| DCS report — fields, parameters, filters | `/skd-info` | +| Spreadsheet template areas/parameters | `/mxl-info` | +| Role rights / restrictions | `/role-info` | +| Subsystem composition / command interface | `/subsystem-info` | + +This gives the real Russian field labels, command names, column headers, table-section names. Without it, fuzzy matching will silently land on the wrong element, or fail with no useful diagnostic. + +If the user names objects you cannot find: stop and ask. Do not guess. + +### 2. Live recon — interactive walkthrough + +For any non-trivial scenario, walk the path live in `exec` mode before writing it down. Metadata tells you what exists; the live walkthrough tells you what actually happens — which button posts the document, which dialog 1C raises, how the form looks after `clickElement('Создать')`, what fields are required, where `wait()` is genuinely needed. + +```bash +# Start a session (background). +node $RUN start http://localhost:9191/myapp/ru_RU + +# Step the scenario interactively. After each step, inspect. +cat <<'EOF' | node $RUN exec - +await navigateSection('Склад'); +const cmds = await getCommands(); +console.log(cmds); +EOF + +cat <<'EOF' | node $RUN exec - +await openCommand('Приходная накладная'); +await clickElement('Создать'); +const s = await getFormState(); +console.log(JSON.stringify(s.fields.map(f => ({ name: f.name, label: f.label, required: f.required })), null, 2)); +console.log('buttons:', s.buttons.map(b => b.name)); +console.log('tables:', s.tables.map(t => ({ name: t.name, label: t.label, columns: t.columns }))); +EOF + +# Try the actions you plan to encode. If a step fails, fix and re-try +# before transcribing it. +cat <<'EOF' | node $RUN exec - +await fillFields({ 'Контрагент': 'ООО Север' }); +await fillTableRow({ 'Номенклатура': 'Товар 01', 'Количество': '5' }, + { table: 'Товары', add: true }); +await clickElement('Провести и закрыть'); +console.log(JSON.stringify(await getFormState())); +EOF + +# When done, stop the session (or leave it for the next test you write). +node $RUN stop +``` + +What to record from the walkthrough into the test: +- Exact button names (`'Провести и закрыть'`, not `'Сохранить'`). +- Field labels as 1C renders them (with possible non-breaking spaces — `fillFields` normalises, but be exact). +- Table section names from `getFormState().tables[].name`/`label` for multi-grid forms. +- Required `wait()` durations — only where a real async event happens (report generation, server-side calculation). Default actions await internally. +- The shape of `getFormState()` after each action — gives you the right `assert.equal(...)` paths. + +After this, transcribe the working sequence into `*.test.mjs`, wrap each chunk in `step('...', async () => { ... })`, add assertions for the invariants you saw. Run the file once with `node $RUN test path/to/file.test.mjs` to confirm. + +When live recon is overkill: trivial reads (`navigateSection` + `readTable` + assert non-empty), or scenarios you've already proven once in this session. When it's essential: anything with confirmation dialogs, posting/cancellation flows, reports with custom filters, multi-grid forms, or user-customised forms you've never seen. + +## Suite layout + +**Each application gets its own subfolder under `tests/`.** A single repo may host several independent suites side by side — they must not share `_hooks.mjs` or `webtest.config.mjs`, because each suite restores a different DB, publishes to a different URL, and ships its own test data. + +``` +tests/ + web-test/ # engine self-tests (reserved if our repo layout) + / # application regression — one per solution + _hooks.mjs + webtest.config.mjs + 01-login/ + 02-counterparties/ + ... + / # second solution, fully isolated + _hooks.mjs + ... +``` + +`` is the project/extension slug (`acc-payroll`, `erp-customisation`, etc.). Pick something stable and pass it on the CLI: + +```bash +node $RUN test tests// +``` + +Inside the application subfolder, organize by **feature**, not by metadata kind. Numeric prefixes on both folder and file enforce run order (discovery is alphabetic by full path). + +``` +tests// + _hooks.mjs # stand prep + cross-cutting hooks (optional) + webtest.config.mjs # url, contexts, defaults (optional) + 01-login/ + 01-open-base.test.mjs + 02-section-navigation.test.mjs + 02-counterparties/ + 01-create.test.mjs + 02-edit-phone.test.mjs + 03-goods-receipt/ + 01-fill.test.mjs + 02-post.test.mjs + 03-unpost.test.mjs + 04-balance-report/ + 01-generate.test.mjs + 02-warehouse-filter.test.mjs + 05-approval-process/ + 01-end-to-end.test.mjs # multi-user +``` + +Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported. Only the application-root copies are loaded. + +## Test file anatomy + +```js +export const name = 'Создание контрагента'; // required +export const tags = ['catalog', 'create']; // optional, used for filtering + Allure +export const timeout = 60000; // optional, default 30000 +// export const skip = 'pending fix #123'; // optional: true | string +// export const only = true; // debug-only — never commit +// export const context = 'manager'; // optional, single non-default context +// export const contexts = ['clerk', 'manager']; // optional, multi-user test +// export const severity = 'critical'; // optional, overrides config severity + +export async function setup(ctx) { + // per-test prep — runs before default. Skip if not needed. +} + +export async function teardown(ctx) { + // per-test cleanup — runs after default, always (even on failure). +} + +export default async function(ctx) { + const { navigateSection, openCommand, clickElement, fillFields, + readTable, closeForm, getFormState, + assert, step, log } = ctx; + + await step('Открыть список контрагентов', async () => { + await navigateSection('Продажи'); + await openCommand('Контрагенты'); + }); + + await step('Создать нового контрагента', async () => { + await clickElement('Создать'); + await fillFields({ 'Наименование': 'Тест ' + Date.now() }); + await clickElement('Записать и закрыть'); + }); + + await step('Убедиться, что элемент появился в списке', async () => { + const t = await readTable(); + assert.tableHasRow(t, r => r['Наименование']?.startsWith('Тест ')); + }); +} +``` + +The runner injects every `browser.mjs` export into `ctx` plus `assert`, `step`, `log`, `testInfo`, `testResult` (afterEach only). For multi-context tests, each context name is its own scoped namespace (`ctx.clerk.clickElement(...)` etc.) — `step`/`assert` stay top-level. + +**Step names — in Russian, descriptive.** Step labels surface in the console output, in JSON/JUnit, and as Allure step nodes. Russian-speaking QA reads them. Use a full action phrase (`'Создать нового контрагента'`, `'Проверить наличие документа в списке'`), not a tag (`'create'`, `'verify'`) and not a transliteration. Same applies to `export const name` and `displayName` in `webtest.config.mjs`. + +## webtest.config.mjs + +```js +export default { + // Single-context: just url. + url: 'http://localhost:9191/myapp/ru_RU', + + // OR multi-context: named contexts. Each test picks via `context`/`contexts` exports. + // contexts: { + // clerk: { url: 'http://localhost:9191/myapp-clerk/ru_RU', displayName: 'Кладовщик' }, + // manager: { url: 'http://localhost:9191/myapp-manager/ru_RU', displayName: 'Менеджер' }, + // }, + // defaultContext: 'clerk', + + timeout: 30000, + retries: 0, + screenshot: 'on-failure', + record: false, + + // Severity → tags mapping for Allure. Each tag at most one bucket. + severity: { + critical: ['smoke', 'crud'], + minor: ['recording'], + }, + defaultSeverity: 'normal', +}; +``` + +CLI flags override config. Recommend latin context IDs + Russian `displayName` for video badges. + +## _hooks.mjs + +Two layers. Infra hooks run without a browser; testlevel hooks receive `ctx`. + +```js +import { execSync } from 'child_process'; + +// Infra — runs once around the whole suite. +export async function prepare({ hookArgs, log, config }) { + // Restore DB, publish to Apache, build EPF, etc. + // hookArgs = everything after `--` on the CLI. Parse yourself. + if (hookArgs.includes('--rebuild-stand')) { /* full rebuild */ } + // Use idempotent hash-locks to skip work on warm starts. +} + +export async function cleanup({ log, config }) { + // Tear down or leave the stand running. Choose per project. +} + +// Testlevel — runs with browser ctx. +export async function beforeAll(ctx) { /* once after first context opens */ } +export async function afterAll(ctx) { /* once before final teardown */ } +export async function beforeEach(ctx) { /* ctx.testInfo is set */ } +export async function afterEach(ctx) { /* ctx.testResult is set */ } + +// Per-context — runs whenever a context is created/closed. +export async function afterOpenContext(ctx, name, spec) { /* spec = config.contexts[name] */ } +export async function beforeCloseContext(ctx, name, spec) { } +``` + +Built-in state reset (`dismissPendingErrors` + close all forms) runs after `afterEach` automatically. Don't reimplement it. + +**Where to put data setup:** +- DB restore, publication, EPF build → `prepare()`. Make it idempotent (hash-locks on inputs — config sources, EPF spec, DB dump) so warm starts skip everything but a liveness probe. +- Test-specific seed data (the document this test will edit, the counterparty it expects) → per-test `setup`. +- Shared session-wide warmup → `beforeAll`. + +## Ready-to-paste patterns + +### Catalog full cycle + +```js +await step('Создать контрагента', async () => { + await navigateSection('Продажи'); + await openCommand('Контрагенты'); + await clickElement('Создать'); + await fillFields({ 'Наименование': 'ТД Тест', 'ИНН': '7707083893' }); + await clickElement('Записать и закрыть'); +}); +await step('Проверить наличие в списке', async () => { + const t = await readTable({ maxRows: 50 }); + assert.tableHasRow(t, { 'Наименование': 'ТД Тест' }); +}); +await step('Удалить контрагента и подтвердить удаление', async () => { + await clickElement('ТД Тест'); + const page = await getPage(); + await page.keyboard.press('Delete'); + await clickElement('Да'); +}); +``` + +### Document create + post + +```js +const marker = 'Тест-' + Date.now(); +await openCommand('Приходная накладная'); +await clickElement('Создать'); +await fillFields({ 'Контрагент': 'ООО Север', 'Комментарий': marker }); +await fillTableRow( + { 'Номенклатура': 'Товар 01', 'Количество': '5', 'Цена': '100' }, + { table: 'Товары', add: true } +); +await clickElement('Провести и закрыть'); +// Verify: re-open list, filter or scan, assert by `marker`. +``` + +Use a unique marker (`Date.now()` or random suffix) so re-runs don't collide. Identify your own row by it, not by position or natural keys that may already exist in the DB. + +### DCS report + +```js +await openCommand('Остатки товаров'); +// Reset user settings — 1C persists them between sessions. +await clickElement('Ещё'); +await clickElement('Установить стандартные настройки'); + +await selectValue('Номенклатура', 'Товар 02'); // auto-enables the filter checkbox +await clickElement('Сформировать'); +await wait(3); +const r = await readSpreadsheet(); +assert.deepEqual(r.headers, ['Номенклатура', 'Количество', 'Сумма']); +assert.ok(r.data.length >= 1); +assert.ok(r.totals?.['Сумма']); +``` + +### Multi-user process + +```js +export const contexts = ['clerk', 'manager']; + +export default async function({ clerk, manager, step, assert }) { + 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('Кладовщик видит новый статус', async () => { + const s = await clerk.getFormState(); + assert.equal(s.fields.find(f => f.name === 'Статус')?.value, 'Утверждён'); + }); + await step('Освободить сессию кладовщика', async () => { + await manager.closeContext('clerk'); // free a 1C license for the next test + }); +} +``` + +License caveat: stock 1C allows ~2 web sessions concurrently. Close contexts you no longer need before the next multi-user test starts. + +### Failing-test repro + +```js +export const name = 'Bug #123: накладная без контрагента не должна проводиться'; +export const tags = ['bug', 'validation']; + +export default async function({ openCommand, clickElement, getFormState, assert, step }) { + await openCommand('Приходные накладные'); + await clickElement('Создать'); + await clickElement('Провести'); + const s = await getFormState(); + assert.ok(s.errorModal || s.fields.find(f => f.name === 'Контрагент')?.required, + 'Должна быть ошибка валидации или поле помечено обязательным'); +} +``` + +Write it red first, hand it to the user, fix the underlying issue, re-run green. + +## Running + +```bash +node $RUN test tests// # full app suite +node $RUN test tests//03-goods-receipt/ # one feature folder +node $RUN test tests//02-counterparties/01-create.test.mjs # one file +node $RUN test tests// --tags=smoke # by tag (intersection) +node $RUN test tests// --grep='накладн' # by name regex +node $RUN test tests// --bail --retry=1 # stop on first fail, allow 1 retry +node $RUN test tests// --report=allure-results --format=allure --report-dir=allure-results +node $RUN test tests// -- --rebuild-stand # everything after `--` goes to hooks +``` + +Default report is JSON when `--report=…` is given. Allure needs `--format=allure` + a directory. JUnit similarly with `--format=junit`. + +### Allure static config — `_allure/` directory + +The runner copies `/_allure/` into the report directory before generating Allure output. Standard Allure convention applies — three files are typically used: + +- **`categories.json`** — failure classification. Always emit this when setting up a suite, with 1C-specific patterns: license pool exhaustion (`Не обнаружено свободной лицензии`), 1C application errors (`ВызватьИсключение|Произошла ошибка|…`), navigation/element lookup misses, runner timeouts, assertion failures. +- **`environment.properties`** — `key=value` lines for the Environment widget. Useful when the suite runs across builds/branches (URL, 1C platform version, git branch, configuration version). Often emitted dynamically by `prepare()` rather than committed as a static file. +- **`executor.json`** — CI metadata (Jenkins URL, GitHub run ID, etc.). Only relevant when the suite runs on a CI server; for local runs, skip it. + +Discovery skips the underscored directory, so it never collides with tests. + +## Severity guidance + +When the user doesn't dictate, default to: + +| Test kind | Severity | +|-----------|----------| +| Login + section navigation, basic CRUD on covered entities | `critical` (also tag `smoke`) | +| Documents posting, report generation, end-to-end processes | `critical` | +| Field-level edge cases, formatting, optional flows | `normal` | +| Cosmetic / recording / non-functional | `minor` | +| Reserved for show-stopper protections | `blocker` (use sparingly) | + +Don't promote everything to `critical` — it loses signal in the Allure dashboard. + +## Anti-patterns + +- **Sleeps as a substitute for assertions.** `wait(5)` after `openCommand` is fine; `wait(30)` because something flakes is a bug — find what state you can wait on with `getFormState` instead. +- **Retry as a substitute for understanding.** "Not found" twice means the data isn't there or the label is wrong. Don't loop. +- **Raw DOM via `getPage().$$(...)`.** Use `getFormState`, `readTable`, `readSpreadsheet`. Raw selectors break across 1C platform versions. +- **`clickElement('×')` or `clickElement('Закрыть')`** to dismiss a form. Use `closeForm({ save: true|false })` — handles confirmation correctly. +- **Position-based row identification** (`rows[0]`) when the DB has shared seed data. Filter by unique marker or label instead. +- **Skipping recon** because "I know what this catalog looks like." You don't — the project's customisation almost certainly differs from a stock config. +- **`tags: ['smoke']` on a 90-second test.** Smoke means fast. +- **Hand-writing reset code** in `afterEach`. The runner already closes forms and dismisses errors. +- **Cross-test state assumptions.** Each test must start from desktop and seed its own data. Order-of-execution coupling is a regression-suite trap. + +## After a run — failure triage + +1. Scan the JSON or Allure summary for `failed`. +2. For each failure, read `error.message` + `error.step` + screenshot (saved next to the report). +3. If `error.onecError.stack` is present — it's a 1C exception, look at the platform trace. +4. Classify: + - **Test bug** — selector wrong, expectation wrong, race with no anchor → fix the test. + - **Application bug** — actual misbehaviour reproduced → report to the user with the failing step name and the platform stack. + - **Stand flake** — Apache timeout, login form not loading, license shortage → fix the hook idempotency or session-cleanup logic, not the test. +5. After fixes, re-run only the affected files (`node $RUN test tests/03-goods-receipt/`) before the full suite. + +Report back to the user with the classification, not raw failure dumps. + +## Reference + +- Browser API: [SKILL.md](SKILL.md) +- Video and narration: [recording.md](recording.md) diff --git a/docs/web-test-regression-guide.md b/docs/web-test-regression-guide.md new file mode 100644 index 00000000..046d7edf --- /dev/null +++ b/docs/web-test-regression-guide.md @@ -0,0 +1,391 @@ +# Регрессионное тестирование прикладного решения + +Навык `/web-test` умеет не только разово выполнить сценарий в браузере, но и сопровождать прикладное решение полноценным набором автотестов: каждый тест — отдельный файл, с шагами, проверками, тегами, отчётом и видеозаписью падений. После каждой правки конфигурации модель прогоняет весь набор и показывает, что ожидаемо ведёт себя как раньше, а что сломалось. + +``` +правка конфигурации → загрузка → обновление → публикация → прогон тестов → отчёт +``` + +Это про прикладное решение в целом, не про разовую проверку одной формы. Для разовых сценариев («открой накладную, проверь сумму») по-прежнему удобнее интерактивный режим из [web-test-guide.md](web-test-guide.md). + +## Предусловия + +- База опубликована через Apache (`/web-publish`). +- Установлен Node.js 18+, зависимости подняты: `cd .claude/skills/web-test/scripts && npm install`. +- ffmpeg — нужен только если хотите видеозапись прогона как доказательство падения. Без него падения фиксируются скриншотами. Установка описана в [web-test-recording-guide.md](web-test-recording-guide.md). + +## Как это устроено + +Набор тестов живёт в каталоге `tests/` вашего проекта. Каждое прикладное решение — отдельная подпапка. Внутри подпапки: + +- `_hooks.mjs` — подготовка стенда (восстановление базы, публикация) и общая очистка после прогона. Необязателен. +- `webtest.config.mjs` — адрес базы и набор пользователей (например, кладовщик и менеджер для процессов согласования). Необязателен — если в проекте один пользователь и один URL, можно обойтись без него. +- Сами тесты — файлы `*.test.mjs`, сгруппированные по функциональным папкам. + +``` +tests/ + моя-конфигурация/ + _hooks.mjs + webtest.config.mjs + 01-вход/ + 01-открытие-базы.test.mjs + 02-контрагенты/ + 01-создание.test.mjs + 02-правка-телефона.test.mjs + 03-поступление-товаров/ + 01-оформление.test.mjs + 02-проведение.test.mjs + 04-отчёт-остатки/ + 01-формирование.test.mjs + 05-согласование/ + 01-полный-цикл.test.mjs +``` + +Порядок выполнения — по алфавиту, поэтому удобно префиксовать папки и файлы номерами. Это даёт предсказуемый сценарий: сначала вход, потом справочники, потом документы, потом отчёты, в конце — процессы с несколькими пользователями. + +## Быстрый старт + +Самый короткий путь от нуля до зелёного теста — попросить модель пройти ваш сценарий руками и зафиксировать его как тест: + +``` +> Покрой регрессом справочник Контрагенты в моей конфигурации. +> Нужны проверки: создание, правка телефона, удаление. +``` + +Что сделает модель: + +1. Соберёт информацию о справочнике через `/meta-info` и `/form-info` — посмотрит реквизиты и форму элемента, чтобы знать правильные имена полей. +2. Подключится к опубликованной базе в интерактивном режиме и **руками пройдёт** каждый сценарий — создание, правка, удаление. Это нужно, чтобы зафиксировать настоящие имена кнопок, увидеть, какие диалоги показывает 1С, понять, требуется ли подтверждение сохранения. +3. Зафиксирует пройденный сценарий как файл `tests/<ваша-конфигурация>/02-контрагенты/01-создание.test.mjs`. +4. Запустит его и покажет результат. + +При следующих прогонах ничего этого делать не нужно — модель просто запустит готовый набор. + +## Сценарии работы с моделью + +### Покрытие регрессом доработанного объекта + +``` +> Я добавил в справочник Номенклатура реквизит "Цена" и "Активен". +> Покрой это регрессом — создание, редактирование, фильтрация по активности +``` + +Модель: +- посмотрит структуру справочника и формы (через `/meta-info`, `/form-info`); +- интерактивно проверит, как ведут себя новые поля в браузере; +- сгенерирует 2-3 тестовых файла под папкой `02-номенклатура/`; +- прогонит — покажет, что зелёное, что красное. + +### Тест процесса с несколькими пользователями + +``` +> Сделай тест для процесса согласования приходных накладных. +> Кладовщик создаёт накладную, менеджер утверждает, +> кладовщик видит обновлённый статус +``` + +Модель настроит в `webtest.config.mjs` двух пользователей (с разными URL базы — например, `app-clerk` и `app-manager`), напишет тест, который оркестрирует переключение между ними, и положит его в `05-согласование/`. + +```js +export const contexts = ['кладовщик', 'менеджер']; + +export default async function({ кладовщик, менеджер, step, assert }) { + await step('Кладовщик создаёт накладную', async () => { + await кладовщик.navigateSection('Склад'); + await кладовщик.openCommand('Приходные накладные'); + await кладовщик.clickElement('Создать'); + // ... + }); + await step('Менеджер утверждает', async () => { + await менеджер.navigateSection('Согласование'); + // ... + }); + // ... +} +``` + +Учтите ограничение по лицензиям 1С: каждый одновременно открытый пользователь — это занятая клиентская лицензия. Если в наборе много многопользовательских тестов, а на стенде лицензий впритык, прогоны начнут спотыкаться на «свободных лицензий не осталось». Модель освобождает сессии между тестами автоматически (закрывает контексты после процессного теста), но если стенд ограничен — закладывайте это в планирование набора: один-два многопользовательских сценария вместо десяти. + +### Воспроизведение ошибки тестом + +``` +> При проведении накладной без заполненного контрагента у нас не появляется +> ошибка валидации, документ просто проводится с пустым контрагентом — это баг. +> Зафиксируй это падающим тестом +``` + +Модель воспроизведёт сценарий, напишет тест с проверкой «должна быть ошибка», получит красный — потом, когда вы поправите конфигурацию и попросите перепрогнать, тест станет зелёным. Это документирует ожидаемое поведение в виде кода. + +### Прогон регресса после изменений + +``` +> Я обновил расширение, накатил в базу. Прогони регресс +``` + +Модель запустит весь набор, дождётся завершения и расскажет: +- сколько тестов прошло, сколько упало, сколько пропущено; +- по каждому упавшему — что именно сломалось (название шага, сообщение об ошибке, ссылка на скриншот); +- классифицирует падения: это ошибка в самом тесте (нужно поправить тест), ошибка в приложении (баг, который вы внесли изменением), или нестабильность стенда (Apache не ответил вовремя, лицензия не освободилась). + +``` +> Прогони только тесты по контрагентам с подробным отчётом +``` + +Запустит подмножество — фильтр по тегу или папке, с записью JSON-отчёта. + +### Подготовка автономного стенда + +Если вы хотите, чтобы регресс можно было запустить «с нуля» — даже на чистой машине без подготовленной базы, — модель настроит автоматическую подготовку стенда: + +``` +> Сделай, чтобы перед прогоном тестов база восстанавливалась из эталона, +> а после прогона публикация снималась +``` + +Это пишется один раз в файле `_hooks.mjs`: при запуске тестов запускается подготовка (через навыки `/db-create`, `/db-load-xml`, `/web-publish`), а после — очистка. Внутри предусмотрено кэширование: если ничего не менялось со прошлого прогона, повторная подготовка занимает доли секунды. + +## Пример организации покрытия + +Допустим, у нас условное прикладное решение «Учёт поступлений товаров» — справочники контрагентов и номенклатуры, документ приходной накладной, отчёт остатков, процесс согласования с двумя пользователями. Логично организовать набор так: + +``` +tests/учёт-поступлений/ + _hooks.mjs # подготовка: восстановление базы + публикация + webtest.config.mjs # URL базы, контексты кладовщика и менеджера + 01-вход/ + 01-открытие-базы.test.mjs # базовая работоспособность: вход проходит, разделы видны + 02-навигация-по-разделам.test.mjs # обход всех разделов конфигурации + 02-контрагенты/ + 01-создание.test.mjs # создание, проверка появления в списке + 02-редактирование.test.mjs # правка реквизита, проверка сохранения + 03-удаление.test.mjs # удаление с подтверждением + 03-номенклатура/ + 01-создание.test.mjs + 02-фильтр-по-активности.test.mjs # быстрая фильтрация списка + 04-поступление-товаров/ + 01-оформление.test.mjs # заполнение шапки и табличной части + 02-проведение.test.mjs # проведение документа, проверка движений + 03-отмена-проведения.test.mjs + 04-валидация-обязательных.test.mjs # негативный тест: пустой контрагент → ошибка + 05-отчёт-остатки/ + 01-формирование.test.mjs + 02-отбор-по-складу.test.mjs + 03-расшифровка.test.mjs # переход из ячейки отчёта в исходный документ + 06-согласование/ + 01-полный-цикл.test.mjs # многопользовательский тест +``` + +Принципы: + +- **Папки — по бизнес-функции**, не по типу метаданных. Лучше `04-поступление-товаров/` (что делает пользователь), чем `документы/` (что лежит в дереве конфигурации). +- **Цифровые префиксы** — на папке и на файле. Гарантируют, что сначала отработают базовые проверки (вход, справочники), потом сложные (документы, отчёты, процессы). При падении базы остальное и так не пройдёт — нет смысла занимать стенд получасом. +- **Один файл — одна логически связанная история.** Не «всё про контрагентов в одном файле», а «отдельно создание, отдельно правка, отдельно удаление». Когда падает — сразу видно, какой именно сценарий сломан. +- **Негативные тесты тоже есть.** «Документ без контрагента не проводится» — такой же важный регресс, как и позитивный сценарий, особенно после правок в обработчиках проверки заполнения. +- **Процессные тесты — в конце.** Они самые хрупкие (зависят от двух сессий, лицензий, синхронизации) и самые длинные. Если упадут — у вас уже есть данные от предыдущих тестов. + +## Анатомия одного теста + +Пользователь, как правило, тест не пишет — генерирует модель. Но прочитать и поправить полезно уметь. Стандартный файл выглядит так: + +```js +export const name = 'Создание контрагента'; +export const tags = ['контрагенты', 'базовая-проверка']; +export const timeout = 60000; + +export default async function({ + navigateSection, openCommand, clickElement, fillFields, + readTable, closeForm, assert, step +}) { + await step('Открыть список контрагентов', async () => { + await navigateSection('Продажи'); + await openCommand('Контрагенты'); + }); + + await step('Создать нового контрагента', async () => { + await clickElement('Создать'); + await fillFields({ 'Наименование': 'ТД Тест', 'ИНН': '7707083893' }); + await clickElement('Записать и закрыть'); + }); + + await step('Убедиться, что элемент появился в списке', async () => { + const t = await readTable(); + assert.tableHasRow(t, r => r['Наименование'] === 'ТД Тест'); + }); +} +``` + +Что здесь есть: + +- **`name`** — человекочитаемое имя теста. Появится в отчёте. +- **`tags`** — теги для фильтрации. Можно прогонять не весь набор, а только нужные: `--tags=контрагенты`. +- **`timeout`** — сколько максимум тест может идти. По умолчанию 30 секунд, для длинных сценариев увеличиваем. +- **Тело теста** — функция, которая получает API браузера (см. [SKILL.md](../.claude/skills/web-test/SKILL.md)) плюс `assert` и `step`. +- **`step('имя', async () => {...})`** — обёртка шага. Имена шагов попадают в отчёт, при падении видно, какой именно шаг сломался. +- **`assert.*`** — проверки. `assert.tableHasRow`, `assert.equal`, `assert.ok` и т.д. Если проверка не выполнилась — тест считается упавшим. + +Имена шагов и теста — по-русски, описательные. Они показываются и в консоли, и в отчётах. + +## Запуск и отчёты + +### Простой прогон + +``` +> Прогони регресс +``` + +Модель запустит весь набор, дождётся, покажет сводку: + +``` +✓ Открытие базы (2.1s) +✓ Создание контрагента (8.4s) +✗ Проведение приходной накладной (12.7s) + └ Заполнить табличную часть (5.2s) + Не найден столбец "Цена" в табличной части "Товары" + скриншот: tests/учёт-поступлений/error-shot.png + +23 пройдено, 1 упал, 0 пропущено (3 мин 42 с) +``` + +### Подробный отчёт + +``` +> Прогони регресс и сохрани подробный отчёт +``` + +Модель добавит флаг записи отчёта (JSON или Allure) — потом по нему можно листать историю прогонов, видеть длительности шагов, открывать прикреплённые скриншоты. + +Allure — стандартный визуальный отчёт с категориями падений, графиками, таймлайном. Чтобы посмотреть отчёт после прогона: + +```bash +# Allure CLI устанавливается отдельно (npm install -g allure-commandline) +allure serve allure-results +``` + +### Категории падений в Allure + +Без дополнительной настройки Allure складывает все упавшие тесты в один общий список «Defects». Если в прогоне упало 15 тестов, не сразу понятно, что из этого — пятнадцать разных проблем или одна и та же ошибка (например, нехватка лицензии на стенде), которая зацепила пятнадцать тестов подряд. + +Чтобы Allure группировал падения по причинам, рядом с тестами кладётся каталог `_allure/` с файлом `categories.json`. Подчёркивание в имени каталога — чтобы он не воспринимался как папка с тестами; раннер копирует его содержимое в отчёт. + +``` +tests/моя-конфигурация/ + _allure/ + categories.json # классификация падений + environment.properties # необязательно: URL, версия 1С, ветка git + executor.json # необязательно: метаданные сборки CI + _hooks.mjs + 01-вход/ + ... +``` + +`categories.json` — это список регулярных выражений, по которым ошибка теста относится к той или иной группе: + +```json +[ + { "name": "Нехватка лицензий 1С", + "matchedStatuses": ["failed", "broken"], + "messageRegex": ".*Не обнаружено свободной лицензии.*" }, + { "name": "Ошибка приложения 1С", + "matchedStatuses": ["failed"], + "messageRegex": ".*(ВызватьИсключение|В поле введены некорректные данные|Произошла ошибка).*" }, + { "name": "Элемент не найден", + "matchedStatuses": ["failed"], + "messageRegex": ".*(clickElement|fillFields|selectValue).*not found.*" }, + { "name": "Превышен лимит времени теста", + "matchedStatuses": ["failed", "broken"], + "messageRegex": "Timeout \\(\\d+ms\\)" }, + { "name": "Несовпадение ожидания и факта", + "matchedStatuses": ["failed"], + "messageRegex": "(Expected|AssertionError).*" } +] +``` + +Когда вы попросите модель в первый раз настроить регресс, она положит шаблонный `categories.json` со стандартными классами. По мере того как вы будете находить новые типичные причины падений (например, специфичные для вашего расширения тексты ошибок), категории дополняются. + +В виджете «Categories» итогового отчёта вы увидите примерно так: + +``` +Нехватка лицензий 1С — 12 падений +Ошибка приложения 1С — 2 падения +Несовпадение ожидания и факта — 1 падение +``` + +— и сразу понятно, что 12 падений — это один стенд-баг, а двумя «ошибками приложения» нужно разобраться по существу. + +Помимо `categories.json` в каталог `_allure/` можно положить ещё два стандартных файла: + +- **`environment.properties`** — список `ключ=значение` (URL базы, версия платформы 1С, имя ветки git, номер сборки). Покажется в отчёте в виджете «Environment». Полезно, когда регресс гоняется на нескольких стендах или после каждого билда — видно, на чём именно был получен результат. Этот файл удобно генерировать прямо в подготовке стенда (`_hooks.mjs`), а не держать статичной копией. +- **`executor.json`** — метаданные системы сборки: ссылка на Jenkins-задачу, идентификатор запуска GitHub Actions и т.д. Нужен только если регресс запускается на сервере сборки. При локальном прогоне ничего класть не надо. + +### Прогон части набора + +``` +> Прогони только тесты по поступлениям товаров +> Прогони только базовые проверки +> Прогони только упавший вчера тест с проведением накладной +``` + +Модель выберет нужное подмножество — по папке, по тегу или по имени теста. + +### Принудительная пересборка стенда + +Если хотите, чтобы перед прогоном база восстановилась с нуля: + +``` +> Прогони регресс с полной пересборкой стенда +``` + +Это передаст в подготовку флаг типа `--rebuild-stand` — `_hooks.mjs` пересоздаст базу из эталона. Полезно после крупных правок или если подозреваете, что предыдущие прогоны загрязнили данные. + +## Что делать, когда тест упал + +Модель проанализирует падение и отнесёт его к одной из трёх категорий: + +1. **Ошибка в самом тесте.** Например, переименовали реквизит — тест ищет старое имя поля. Решение: модель обновит тест. +2. **Ошибка в приложении.** Это и есть то, ради чего регресс существует: что-то поменялось в конфигурации, и сценарий, который раньше работал, теперь не отрабатывает. Модель опишет, что именно произошло, со скриншотом и трассировкой стека 1С, если ошибка была серверной. +3. **Нестабильность стенда.** Apache не ответил, не освободилась лицензия, база отвалилась. Это лечится не правкой теста, а починкой подготовки стенда в `_hooks.mjs` или, реже, повторным прогоном с одним повтором. + +Просите модель не «исправь упавший тест», а «разберись с падением» — иначе она может молча подкрутить ожидание под текущее поведение, замаскировав настоящий баг. + +## Полезные подробности + +### Тестовые данные + +В прикладном решении обычно нужны какие-то стартовые данные: пара контрагентов, номенклатура, заведённые организации. Их кладём не в каждый тест, а один раз в подготовку стенда (`_hooks.mjs`) — после восстановления базы загружаются эталонные данные, на которых работают все тесты. + +Если конкретному тесту нужны свои данные (например, документ, который мы будем редактировать), он создаёт их сам в начале и убирает в конце. + +### Имена документов и уникальность + +Тесты прогоняются многократно. Если тест создаёт документ «Накладная-Тест», следующий прогон может натолкнуться на старую запись. Решение — добавлять к имени метку времени: + +```js +const метка = 'Тест-' + Date.now(); +await fillFields({ 'Комментарий': метка }); +// ... +const t = await readTable(); +assert.tableHasRow(t, r => r['Комментарий'] === метка); +``` + +Модель это делает автоматически, но если правите тест руками — держите в голове. + +### Видео при падении + +Можно включить запись видео всех тестов — тогда при падении прикладывается не только скриншот, но и MP4 со всей сессией: + +``` +> Прогони регресс с записью видео +``` + +Размер прогона при этом растёт (на 2-3 минутах теста выходит 5-10 МБ), но при отладке сложного падения видео экономит кучу времени. + +### Многоязычные конфигурации + +Если у вас есть конфигурация с командами и реквизитами на нескольких языках, тесты пишутся под один язык (как правило, тот, в котором ведётся работа в проде). При смене языка интерфейса в браузере тесты не пройдут — модель видит другие подписи кнопок. + +## Где смотреть дальше + +- API браузера, которое вызывают тесты — [SKILL.md](../.claude/skills/web-test/SKILL.md). +- Подробная инструкция для модели по написанию тестов (на английском, технический документ) — [.claude/skills/web-test/regress.md](../.claude/skills/web-test/regress.md). +- Интерактивный режим без тестов — [web-test-guide.md](web-test-guide.md). +- Запись видеоинструкций — [web-test-recording-guide.md](web-test-recording-guide.md). diff --git a/docs/web-test-runner-spec.md b/docs/web-test-runner-spec.md deleted file mode 100644 index 82288dad..00000000 --- a/docs/web-test-runner-spec.md +++ /dev/null @@ -1,1051 +0,0 @@ -# web-test runner: спецификация - -Версия: 0.2 -Дата: 2026-05-13 (последний sync) - -## Обзор - -Единый механизм регрессионного тестирования веб-клиента 1С. -Два сценария использования, один инструмент: - -1. **Внутренний регресс** -- тестирование API browser.mjs для безопасного рефакторинга -2. **Пользовательский регресс** -- тестирование 1С-приложений (доработанных типовых или разработанных с нуля) - -Принцип: если удобно для пользовательского регресса, подходит и для внутреннего. - -Паттерны следуют конвенциям Playwright Test (обёртки шагов, хуки, утверждения). - ---- - -## 1. Командная строка - -``` -node run.mjs test [url] [флаги] -``` - -| Флаг | По умолчанию | Описание | -|------|-------------|----------| -| `--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`) | -| `-- ` | -- | Всё после `--` пробрасывается в `_hooks.mjs` как `hookArgs` (см. §6.1) | - -URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. CLI URL переопределяет URL дефолтного контекста. - -### Режим выполнения - -In-process (не через HTTP). Раннер: -1. Загружает конфиг (если есть). -2. Обнаруживает файлы `*.test.mjs`, читает каждый, извлекает метаданные. -3. Фильтрует по `--tags`/`--grep`/`only`. Параметризованные тесты разворачиваются. -4. Запускает браузер и default-контекст (`chromium.launch()` или `launchPersistentContext` - в зависимости от `isolation`). -5. Тесты выполняются последовательно **в алфавитном порядке имён файлов** - (внутри файла — в порядке экспорта). -6. Для каждого теста: лениво создаёт нужные BrowserContext-ы (`ensureContext`), - переключает активный, прогоняет хуки и тело, делает встроенный reset. -7. По завершении: финальный teardown контекстов с `beforeCloseContext`-хуками, - `disconnect()`, `cleanup()`. - ---- - -## 2. Формат тест-модуля - -Каждый файл `*.test.mjs` -- ES-модуль. - -### Экспорты - -| Экспорт | Тип | Обязателен | По умолчанию | Описание | -|---------|-----|-----------|-------------|----------| -| `name` | `string` | да | -- | Читаемое имя теста | -| `default` | `async function(ctx)` | да | -- | Тело теста | -| `tags` | `string[]` | нет | `[]` | Теги для фильтрации | -| `timeout` | `number` | нет | 30000 | Таймаут теста (мс) | -| `skip` | `boolean \| string` | нет | false | Пропустить тест (строка = причина) | -| `only` | `boolean` | нет | false | Запустить только этот тест (отладка) | -| `context` | `string` | нет | defaultContext | Имя контекста из конфига | -| `contexts` | `string[]` | нет | -- | Мульти-пользовательский процессный тест | -| `params` | `object[]` | нет | -- | Параметризация (будущее) | -| `setup` | `async function(ctx)` | нет | -- | Подготовка перед тестом | -| `teardown` | `async function(ctx)` | нет | -- | Очистка после теста (выполняется всегда) | - -### Пример: тест с одним контекстом - -```js -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..displayName` (см. §7). - -```js -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) - -Все функции обёрнуты авто-обнаружением ошибок (как в `executeScript`): -- При модальной/всплывающей ошибке 1С: скриншот + `fetchErrorStack` + throw -- Обёрнутые ACTION_FNS: `clickElement`, `fillFields`, `fillField`, `selectValue`, - `fillTableRow`, `deleteTableRow`, `openCommand`, `navigateSection`, - `navigateLink`, `openFile`, `closeForm`, `filterList`, `unfilterList` - -Полный список доступных функций: - -**Навигация:** `navigateSection`, `openCommand`, `switchTab`, `navigateLink`, `openFile` -**Состояние:** `getFormState`, `getPageState`, `getSections`, `getCommands` -**Таблицы:** `readTable`, `readSpreadsheet`, `fillTableRow`, `deleteTableRow` -**Поля:** `fillFields`, `fillField`, `selectValue` -**Действия:** `clickElement`, `closeForm`, `filterList`, `unfilterList` -**Ошибки:** `dismissPendingErrors`, `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` - -### Тестовые утилиты - -- `step(name, fn)` -- обёртка шага (см. раздел 4) -- `assert.*` -- хелперы утверждений (см. раздел 5) -- `log(...args)` -- добавить в вывод теста - -### Метаданные теста (`ctx.testInfo`) - -Декларативная информация о текущем тесте. Раннер выставляет `ctx.testInfo` -перед каждой попыткой (до `beforeEach`), хук и тело теста могут читать. -Не предназначено для мутации. - -```js -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` — декларация теста, не зависит от runtime-состояния -`getActiveContext()` (которое может меняться внутри теста). - -### Результат теста в afterEach (`ctx.testResult`) - -Только в `afterEach`. До запуска теста — `null`. После — заполняется -раннером перед вызовом хука: - -```js -ctx.testResult = { - status, // 'passed' | 'failed' - duration, // ms - attempts, // фактически выполнено попыток (1..maxAttempts) - error, // { message, step?, screenshot? } | null - steps, // массив step-результатов -} -``` - -### Мульти-контекст - -При `export const contexts = ['a', 'b']`: -- `ctx.a` и `ctx.b` -- отдельные объекты контекста, каждый с полным API браузера -- `ctx.step` и `ctx.assert` остаются на верхнем уровне - ---- - -## 4. step(name, fn) -- обёртка шага - -```js -await step('Имя шага', async () => { - // тело шага -}); -``` - -Поведение: -- Записывает метку `start` перед `fn()` -- Записывает метку `stop` после `fn()` (успех или ошибка) -- При ошибке: устанавливает `status: 'failed'`, прикрепляет сообщение, пробрасывает исключение -- При успехе: устанавливает `status: 'passed'` -- Если стратегия скриншотов `every-step`: делает скриншот после `fn()` -- Вложенные шаги поддерживаются (шаг внутри шага) -- Напрямую маппится на шаги Allure - -Структура данных шага (для отчётов): - -```js -{ - name: 'Имя шага', - start: 1712345678000, // мс от эпохи - stop: 1712345679200, - status: 'passed' | 'failed', - error: 'сообщение' | undefined, - screenshot: 'путь' | undefined, - steps: [] // вложенные шаги -} -``` - -Реализация (~15 строк): - -```js -async function step(name, fn) { - const s = { name, start: Date.now(), status: 'passed', steps: [] }; - const parent = currentSteps; - parent.push(s); - const prev = currentSteps; - currentSteps = s.steps; - try { - await fn(); - } catch (e) { - s.status = 'failed'; - s.error = e.message; - throw e; - } finally { - s.stop = Date.now(); - currentSteps = prev; - } -} -``` - ---- - -## 5. Утверждения (assertions) - -Простые хелперы утверждений. Без зависимостей. Бросают `AssertionError` со -свойствами `.actual`, `.expected`, `.message`. - -### Общие - -```js -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) // проверка регулярным выражением -assert.throws(asyncFn, msg) // ожидает исключение -``` - -### Специфичные для 1С - -```js -assert.formHasField(state, fieldName, msg) -// проверяет наличие state.fields[fieldName] - -assert.formTitle(state, expected, msg) -// проверяет state.title === expected (или includes) - -assert.tableHasRow(table, predicate, msg) -// predicate: объект (частичное совпадение) или функция -// объект: assert.tableHasRow(table, { 'Наименование': 'Тест' }) -// функция: assert.tableHasRow(table, r => r['Сумма'] > 100) - -assert.tableRowCount(table, expected, msg) -// проверяет table.rows.length === expected - -assert.noErrors(state, msg) -// проверяет !state.errors -``` - ---- - -## 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 контекстов, isolation, etc.). - -**Тестовый уровень** (с контекстом браузера): -- `beforeAll(ctx)` -- после подключения, перед первым тестом -- `afterAll(ctx)` -- после последнего теста, до отключения -- `beforeEach(ctx)` -- перед каждым тестом. На входе уже доступен `ctx.testInfo` (см. §3). -- `afterEach(ctx)` -- после каждого теста. Дополнительно доступен `ctx.testResult` - с результатом завершившегося теста (status/duration/error/...). - -**Контекстный уровень** (на каждый browser-контекст, lifecycle = создан → удалён): -- `afterOpenContext(ctx, name, spec)` -- сразу после успешного `createContext`. - `spec` -- запись из `config.contexts[name]` со всеми custom-полями (`displayName`, - `url`, `isolation`, ...). Полезно: инжект persistent overlay/badge, - preload-навигация для контекста, регистрация телеметрии. -- `beforeCloseContext(ctx, name, spec)` -- перед `closeContext` (контекст ещё - активен и работает). Полезно: финальный flush, сбор метрик, последний скриншот. - Срабатывает как при явном `ctx.closeContext(name)` из теста, так и в - финальном teardown раннера перед `disconnect`. - -`closeContext(name)` валиден только когда `name !== getActiveContext()` -- иначе -бросает. В scoped API (`ctx.a.closeContext('b')`) это естественно: scoped-обёртка -сначала `setActiveContext('a')`, потом close `'b'` -- target всегда не активен. - -### Порядок выполнения - -``` -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 → closeContext - browser.close() // финальный disconnect -cleanup() // без браузера (удаление публикации) -``` - -### Встроенный сброс состояния - -После каждого теста (после `afterEach`) раннер гарантирует чистое состояние: - -```js -await dismissPendingErrors(); -while (есть открытые формы) { - await closeForm({ save: false }); -} -``` - -Это гарантирует, что каждый тест стартует с чистого рабочего стола, -независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). - -### Пример _hooks.mjs - -```js -import { execSync } from 'child_process'; - -export async function prepare({ hookArgs, log, config }) { - // Простой парсер ad-hoc флагов: hookArgs приходит как есть, без интерпретации - // раннером (см. §6.1 ниже). - const force = hookArgs.includes('--rebuild-stand'); - log('preparing stand, force=', force); - 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) { - // Контекст name создан. spec — config.contexts[name]. Удобно для - // persistent DOM-overlay'я с displayName (видно в видео какая вкладка к - // какому пользователю относится). -} - -export async function beforeCloseContext(ctx, name, spec) { - // Контекст name вот-вот закроется. Срабатывает и при ctx.closeContext - // из теста, и в финальном teardown раннера. -} -``` - -### 6.1. Проброс пользовательских флагов через `--` - -Раннер не знает о пользовательских флагах хуков. Чтобы хуки получили ad-hoc -параметры без правки `webtest.config.mjs` или окружения, используется стандартная -shell-конвенция `--` (как у `npm`, `cargo`, `pytest`): всё что идёт после `--` -в CLI раннера передаётся в `prepare`/`cleanup` через поле `hookArgs: string[]` -без интерпретации. - -``` -node run.mjs test tests/web-test/ --bail -- --rebuild-stand --reload-data - └─ runner ─┘ └──── hookArgs ────────────┘ -``` - -В этом примере раннер получает `--bail`, а `hookArgs` хуков становится -`['--rebuild-stand', '--reload-data']`. Парсинг этого массива — ответственность -хуков. - -Если разделитель `--` не указан, `hookArgs` — пустой массив. Это позволяет -раннеру и хукам эволюционировать независимо: новый builtin-флаг раннера -никогда не пересечётся с пользовательским. - ---- - -## 7. Файл конфигурации - -`webtest.config.mjs` в корне каталога тестов. Необязателен -- если отсутствует, -URL должен быть передан через CLI. - -```js -export default { - // Контексты: именованные URL для разных пользователей/ролей. - // Рекомендация: латинский ID контекста (`clerk`, `manager`) + кириллический - // `displayName` для UI/слайдов. Любые custom-поля пробрасываются как есть - // и доступны хукам через `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, - - // Allure severity policy (опционально). Inverted map: уровень → [теги]. - // Резолв см. §9 «Severity». - severity: { - critical: ['smoke', 'multi-context'], - minor: ['recording'], - // blocker / trivial — необязательны, можно опустить - }, - defaultSeverity: 'normal', // если ничего не подошло -}; -``` - -`severity` валидируется при загрузке конфига: -- ключи — только из `blocker|critical|normal|minor|trivial`; -- значение каждого ключа — массив тегов; -- тег не может быть в двух bucket'ах одновременно (явная ошибка с указанием конфликта); -- `defaultSeverity` — из стандартного набора. - -При нарушении любого правила раннер `die`-ает с понятным сообщением до запуска тестов. - -Кириллица в ID контекстов работает, но смешанный регистр затрудняет ergonomics -(`testInfo.contexts.кладовщик.displayName` vs `testInfo.contexts.clerk.displayName`). -Рекомендуем разделять технический ID и человекочитаемое имя. - -**Упрощённая форма** (один контекст, без именованных): - -```js -export default { - url: 'http://localhost/app/ru_RU', - timeout: 30000, -}; -``` - -Флаги 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`) -- **Состояние сохраняется** -- переключились на менеджера и обратно, у кладовщика - все формы остались открытыми, ничего не потеряно -- **Нет переподключений** -- каждая сессия живёт независимо -- **Один процесс** -- экономия ресурсов по сравнению с несколькими браузерами -- **Стандартный паттерн** Playwright для мульти-пользовательских сценариев - -### Одиночный контекст (по умолчанию) - -Большинство тестов. Один BrowserContext, один пользователь. -Тест получает плоский контекст со всем API. - -```js -export const context = 'кладовщик'; // необязательно, используется defaultContext -export default async function({ clickElement, fillFields, ... }) { } -``` - -### Порядок выполнения и переключение контекста - -Раннер НЕ группирует тесты по контексту. Порядок выполнения — алфавитный -по именам файлов (плюс порядок экспорта внутри файла). Для каждого теста: -1. Через `ensureContext(name)` создаются BrowserContext-ы, упомянутые в - `t.context` / `t.contexts` (если ещё не созданы). -2. `setActiveContext(testContextNames[0])` — активный контекст = первый - объявленный (для single — `t.context || defaultContext`, для multi — - `t.contexts[0]`). -3. После теста встроенный сброс пробегает по всем использованным контекстам. - -Контексты живут между тестами: переключение через `setActiveContext` — -дешёвое, новый login не требуется. Закрываются явно (`closeContext`) или -финальным teardown'ом перед `disconnect()`. - -### Мульти-контекст (процессные тесты) - -```js -export const contexts = ['кладовщик', 'менеджер']; -export default async function({ кладовщик, менеджер, step, assert }) { } -``` - -Каждый именованный контекст -- полноценный объект API со своим `page`. -Тест оркестрирует переключение между пользователями. -Состояние каждого пользователя сохраняется между переключениями: - -```js -await step('Кладовщик создаёт документ', async () => { - await кладовщик.openCommand('Приходные накладные'); - await кладовщик.clickElement('Создать'); - await кладовщик.fillFields({ 'Контрагент': 'ООО Поставщик' }); - await кладовщик.clickElement('Записать'); - // кладовщик стоит на форме документа -}); - -await step('Менеджер утверждает', async () => { - await менеджер.navigateSection('Согласование'); - await менеджер.clickElement('Утвердить'); -}); - -await step('Кладовщик проверяет статус', async () => { - // страница кладовщика ТА ЖЕ -- форма открыта, навигация не нужна - const state = await кладовщик.getFormState(); - assert.equal(state.fields['Статус']?.value, 'Утверждён'); -}); -``` - -### Реализация в browser.mjs - -`browser.mjs` хранит активный слот в module-level `page`/`browser`/`sessionPrefix`/`seanceId`, -зеркалит его из Map `contexts: Map`. Переключение между слотами: -`_saveActiveSlot()` сохраняет module-level → slot, `_activateSlot(name)` -загружает slot → module-level. Это держит API-функции (`clickElement`, -`fillFields` и т.д.) plain — они работают с текущим активным `page`, -не зная про множественность контекстов. - -Публичный контекстный API: -- `createContext(name, url, { isolation, extensionPath })` — создаёт BrowserContext - и navigate'ит на URL. -- `setActiveContext(name)` — переключает активный слот, при активной записи - flush'ит хвост старой страницы и переподключает screencast к новой. -- `closeContext(name)` — logout + close (page для `tab`, BrowserContext для - `window`), удаляет из реестра. Throw если `name === active`. -- `listContexts()` / `hasContext(name)` / `getActiveContext()` — read-only. - -### Режимы изоляции - -`isolation` (per-context или config-level): - -| Режим | Реализация | Окна | Cookies | 1С-расширение | -|-------|-----------|------|---------|---------------| -| `'tab'` (default) | `launchPersistentContext` + `newPage()` per context | 1 окно, N вкладок | shared by path | загружается надёжно | -| `'window'` | `chromium.launch()` + `newContext()` per context | N окон | полная изоляция | может не загружаться | - -Смешивать режимы в одном прогоне нельзя — `createContext` бросает явную ошибку. - ---- - -## 9. Отчёты - -### JSON (нативный, по умолчанию) - -```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"], - "status": "passed", - "duration": 12.3, - "attempts": 1, - "steps": [ - { - "name": "Открыть список", - "start": 1712345678000, - "stop": 1712345679200, - "status": "passed", - "steps": [] - } - ], - "output": "Элемент найден в списке", - "error": null, - "screenshot": 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/`: - -```json -{ - "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" } - ], - "steps": [ - { - "name": "Открыть список", - "status": "passed", - "start": 1712345678000, - "stop": 1712345679200, - "steps": [] - } - ], - "attachments": [ - { - "name": "Скриншот при падении", - "source": "uuid-attachment.png", - "type": "image/png" - } - ] -} -``` - -Скриншоты/видео копируются в `allure-results/` с уникальными именами. - -#### Авто-эмиссия label-ов - -Раннер всегда заполняет следующие labels: - -- **`tag`** — по одному label-у на каждый элемент `mod.tags[]`. Бесплатная фильтрация в Allure-дашборде. -- **`suite`** — `dirname(t.file)`. Тесты в корне `testDir` идут под `'root'`, тесты в подкаталоге `sales/` — под `'sales'`. Это даёт левую группировку отчёта без ручной разметки. -- **`severity`** — резолв в порядке приоритета: - 1. `export const severity = 'critical'` в самом тесте (если задано и значение валидное); - 2. иначе **max-rank** среди тегов теста (стандартные имена `blocker|critical|normal|minor|trivial` напрямую, либо через `config.severity`-маппинг); - 3. иначе `config.defaultSeverity` или `'normal'`. - - Rank: `blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1)`. Max-wins инвариантен к порядку тегов в `mod.tags`. - -Пример: `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 - - - - - - - Стек вызовов... - - Скриншот: error-shot-10.png - - - -``` - ---- - -## 10. Консольный вывод - -``` -web-test -- http://localhost/app/ru_RU -Запуск 25 тестов из tests/web-test/ - - ✓ Навигация по разделам (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`. - -### Видеозапись - -При включённом `--record`: -- `startRecording()` перед каждым тестом -- `stopRecording()` после каждого теста -- Видео сохраняется как `{индекс-теста}-{имя-теста}.mp4` -- Прикрепляется к отчёту (Allure: вложение видео) - ---- - -## 12. Сброс состояния - -Встроенный механизм, выполняется после `afterEach` (и `teardown`) каждого теста: - -```js -async function resetState(ctx) { - // 1. Убрать все ожидающие диалоги ошибок/всплывающие уведомления - try { await ctx.dismissPendingErrors(); } catch {} - - // 2. Закрыть все открытые формы до рабочего стола - for (let i = 0; i < 10; i++) { - const state = await ctx.getFormState(); - if (!state.form) break; - try { await ctx.closeForm({ save: false }); } catch { break; } - } -} -``` - -Гарантирует, что каждый тест стартует с чистого рабочего стола, -независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). - ---- - -## 13. Параметризация - -```js -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. buildContext() - -Общая фабрика контекста, используется и `executeScript()` (для `exec`/`run`/`start`), -и `cmdTest()` (для `test`). - -**Что делает:** -- Собирает все экспорты `browser.*` в плоский объект. -- Оборачивает ACTION_FNS авто-обнаружением 1С-ошибок: после каждого вызова - проверяет `state.errors.modal`/`balloon`, делает скриншот ДО того, как - `fetchErrorStack` закроет модалку, вызывает `fetchErrorStack` для modal-ошибок, - бросает исключение со структурированным `err.onecError = { step, args, errors, formState, stack, screenshot }`. -- Подмешивает заглушки `noRecord` (для функций записи/озвучки в exec-режиме). - -**Сигнатура:** `function buildContext({ noRecord = false } = {}) -> object` - -**Scoped-вариант** (`buildScopedContext(name)`): тот же `buildContext()`, -но каждый вызов функции префиксится `await browser.setActiveContext(name)`. -Используется для мульти-контекстных тестов (`ctx.a`/`ctx.b`). - ---- - -## 15. Синтетическая тестовая конфигурация - -### Текущие объекты base-config - -| Объект | Поля | Форма | -|--------|------|-------| -| Справочник Контрагенты | ИНН (String 12), Телефон (String 20) | ФормаЭлемента: 3 поля ввода | -| Справочник Номенклатура | Артикул (String 25), ЕдиницаИзмерения (String 10) | -- | -| Перечисление ВидыНоменклатуры | Товар, Услуга, Работа | -- | -| Документ ПриходнаяНакладная | Контрагент (String); ТЧ Товары (4 колонки) | ФормаДокумента | -| РН ОстаткиТоваров | Изм: Номенклатура; Рес: Количество, Сумма | -- | -| РС КурсыВалют | Изм: Валюта; Рес: Курс, Кратность | -- | -| Константа ОсновнаяВалюта | String 10 | -- | -| Отчёт ОстаткиТоваров | Схема СКД | -- | -| Подсистема Склад | все объекты | -- | -| Роль Кладовщик | права Read/View | -- | - -### Что нужно добавить - -| Изменение | Зачем (какой API тестируем) | -|-----------|---------------------------| -| Номенклатура: +Цена (Number 15.2) | fillFields -- число | -| Номенклатура: +Активен (Boolean) | fillFields -- флажок | -| Номенклатура: +ВидНоменклатуры (EnumRef) | fillFields -- ссылка на перечисление | -| Номенклатура: +ДатаПоступления (Date) | fillFields -- дата | -| Номенклатура: +Комментарий (String неограниченная) | fillFields -- многострочный текст | -| Номенклатура: FillChecking на Наименование | Тест ошибки валидации | -| Номенклатура: hierarchical=true | clickElement expand/collapse | -| Номенклатура: Форма с 2 вкладками (Основное / Дополнительно) | switchTab | -| ПриходнаяНакладная.Контрагент -> CatalogRef.Контрагенты | selectValue (ссылочное поле) | -| +Подсистема Администрирование (КурсыВалют, ОсновнаяВалюта) | navigateSection между разделами | -| Роль: полные права (не только Read/View) | CRUD без ограничений | - -### Способ сборки - -Интеграционный тест `build-webtest-config.test.mjs` собирает конфигурацию через -пайплайн навыков (cf-init -> meta-compile -> form-compile -> ...). -Результат кэшируется в `.cache/webtest-config/`. -Первый запуск требует: загрузку в 1С (`db-load-xml`) + веб-публикацию (`web-publish`). - ---- - -## 16. Каталог тест-кейсов - -Расположение: `tests/web-test/`. По состоянию на 2026-05-13: 19 файлов. - -| # | Файл | Теги | Покрытие | -|---|------|------|----------| -| 00 | hooks.test.mjs | hooks, smoke | индикатор порядка beforeAll/beforeEach/afterEach + testInfo + afterOpenContext | -| 01 | navigation.test.mjs | nav, smoke | navigateSection, getPageState, navigateLink, switchTab, errors | -| 02 | crud.test.mjs | crud, smoke | openCommand, fillFields, clickElement, closeForm, save-confirm flow | -| 03 | fillfields.test.mjs | fields | text/checkbox/date/dropdown/reference/radio/clear + composite + direct-edit-form | -| 04 | selectvalue.test.mjs | fields, select | dropdown / форма выбора / auto-history / clear | -| 05 | table.test.mjs | table, smoke | fillTableRow/deleteTableRow/tab-loop/checkbox/clear | -| 06 | document.test.mjs | doc, smoke | создание+проведение документа | -| 07 | tabs.test.mjs | tabs | switchTab + errors | -| 08 | hierarchy.test.mjs | hierarchy | groups expand + tree-grid view-mode switch | -| 09 | filter.test.mjs | filter | simple-search/advanced-column/exact/date/reference/unfilter-all/unfilter-specific | -| 10 | validation.test.mjs | validation | сообщения + exception modal (fetchErrorStack Path 1) | -| 11 | report.test.mjs | report | DCS form + быстрый фильтр + readSpreadsheet + drill-down | -| 12 | formstate.test.mjs | state | fields/buttons/tables/openForms/subordinate-nav/platformDialogs | -| 13 | misc.test.mjs | misc | openFile EPF + security confirm | -| 14 | errors-stack.test.mjs | errors | fetchErrorStack Path 1 + dismiss-modal + dismiss-platform | -| 14 | multi-context-routing.test.mjs | multi-context | single test → non-default context | -| 15 | multi-context-handover.test.mjs | multi-context | ctx.a creates → ctx.b sees → closeContext(b) + edge throw | -| 15 | recording.test.mjs | record | startRecording/stopRecording/captions/narration/overlays | -| 16 | tree-form.test.mjs | tree, table | FormDataTree edit (ДеревоНоменклатуры) | - -Полный регресс — **19/19** (~9 минут на warm-стенде). - -### 16.1. Вложенные каталоги - -Discovery (`run.mjs:900`) обходит дерево `testDir` рекурсивно, поэтому -тесты можно раскладывать по подкаталогам без правок раннера: - -``` -tests/web-test/ - sales/ - 01-order-create.test.mjs - 02-order-post.test.mjs - warehouse/ - 01-receipt.test.mjs -``` - -**Что работает:** - -| Аспект | Поведение | -|--------|-----------| -| Обнаружение | Рекурсивный walk; файлы/каталоги на `_`/`.` пропускаются | -| Порядок | `files.sort()` по полному относительному пути (`sales/01` идёт до `warehouse/01`) | -| `file` в отчёте | `relative(testDir, file)` с `/`, например `sales/01-order-create.test.mjs` | -| CLI-фильтр по пути | `node run.mjs test tests/web-test/sales/` запустит только подкаталог | -| Конкретный файл | `node run.mjs test tests/web-test/sales/01-order-create.test.mjs` | - -**Что НЕ поддержано** (сознательно, чтобы держать модель простой): - -- **Per-folder `_hooks.mjs`.** Раннер ищет `_hooks.mjs` только в корне `testDir`. Подкаталоги свои хуки не получают. -- **Per-folder `webtest.config.mjs`.** Тоже только в корне. -- **Suite-концепция в отчётах.** Allure suite labels из дерева каталогов не строятся; группируйте через `tags`. -- **Per-folder context default.** Каждый тест объявляет `context`/`contexts` сам; от пути контексты не наследуются. - -**Конвенции:** - -1. **Папки — для организации**, не для механики. Если хочется shared setup для «процесса» — клади в глобальный `_hooks.mjs.beforeAll` или в per-test `setup`/`teardown`. -2. **Группировку в отчётах** делай через `tags: ['sales']`, не через путь. Это даёт фильтрацию (`--tags=sales`) и работает в Allure/JUnit без дополнительной разметки. -3. **«Запустить только sales»** — двумя путями: `tests/web-test/sales/` (по каталогу) или `--tags=sales` (по тегу). Оба работают, выбирайте удобный. -4. **Сортировка по полному пути** означает, что `warehouse/01-x` запустится ПОСЛЕ `sales/02-y`. Для строгого глобального порядка используйте 3-значные префиксы (`010-`/`020-`/...) либо явные теги-фазы. - ---- - -## 17. Дорожная карта реализации - -| # | Задача | Результат | Статус | -|---|--------|-----------|--------| -| 1 | Архитектурная спецификация | `docs/web-test-runner-spec.md` (этот файл) | done 2026-04-05 | -| 2 | Рефакторинг buildContext() | run.mjs: извлечение из executeScript | done 2026-04-05 | -| 3 | Ядро cmdTest() | run.mjs: обнаружение, импорт, выполнение, JSON-отчёт | done 2026-04-05 | -| 4 | Утверждения + обёртка step() | run.mjs: assert.*, step(name, fn) | done 2026-04-05 | -| 5 | Хуки (prepare/cleanup + before/after) | run.mjs: поддержка `_hooks.mjs` | done 2026-04-05 | -| 6 | Файл конфигурации + BrowserContext-ы | webtest.config.mjs, мульти-контекст | done 2026-05-10 (T4 + T4.5/4.6) | -| 7 | Форматы отчётов (Allure, JUnit) | --format=allure/junit | done 2026-05-03 (T2/T3) | -| 8 | Синтетическая конфигурация | `build-webtest-config.test.mjs` | done 2026-04-05 + M1 расширения 2026-05-01 | -| 9 | Smoke-тесты P0 (~18 кейсов) | `tests/web-test/01-12*.test.mjs` | done 2026-05-04 (M2) | -| 10 | Регресс P1 (~15 кейсов) | расширение 02/03/04/05/09/12 | done 2026-05-10 (M3) | -| 11 | M4: расширенный регресс P2 | validation/errors/recording/hierarchy/openFile | done 2026-05-11 | -| 12 | M5-pre: расширение синтетики | tree-form, composite, textEdit, history, unfilter | done 2026-05-12 | -| 13 | M6: автономный стенд через `_hooks.mjs` | prepare(): config-rebuild/data-reload/EPF + smart Apache | done 2026-05-12 (MVP) | -| 14 | M7.1/M7.2: ctx.testInfo + custom-поля контекстов | спека §3 + run.mjs | done 2026-05-13 | -| 15 | M7.3: Headless-режим | `--headless` CLI + config | deferred (1С-specific блокеры в headless) | -| 16 | M7.4: 4 testlevel-хука + индикатор | `_hooks.mjs` v0.3 + 00-hooks.test.mjs | done 2026-05-13 | -| 17 | M7.5: title slide bonus | `beforeEach` под isRecording() | done 2026-05-13 | -| 18 | M8: per-context lifecycle | closeContext + afterOpenContext/beforeCloseContext | done 2026-05-13 |