Резолверов колонок было пять, все независимые, и каждый ломался по-своему:
readTable (геометрия X + Y-подряды), clickElement (геометрия X, без Y),
поиск строки {кол: знач} (геометрия X, без Y и fixed-гарда), filterList
(порядковый индекс шапки), fillTableRow (colindex — единственный целый).
Механика поломки (снята живьём на списке задач ERP): шапка «Исполнитель»
широкая (x 1085..1515) и накрывает «Срок» (1085..1251) и «Выполнена»
(1251..1515). Ячейка «Исполнитель» имеет центр 1300 → приписывается к группе
«Выполнена» → та получает лишний под-ряд → срабатывает эвристика «объединённая
шапка» → фантомные «Выполнена 1/2», а значения соседей склеиваются через ' / '.
Теперь COLUMN_MODEL_FN (dom/_shared.mjs) — единственный источник правды:
buildColumnModel / columnForCell / cellForColumn / resolveColumnByName. Идентичность
колонки — colindex (собственный id колонки в 1С, есть и на шапке, и на ячейке);
геометрия работает только там, где своей шапки у ячейки нет — под-ряды
объединённой шапки («Субконто Дт» над тремя ячейками). Путь записи пришёл к этому
решению раньше (grid-edit.mjs: «reliable across merged headers») — остальные
выровнены по нему.
Следствие: имя колонки из readTable теперь годится для клика/заполнения/фильтра —
раньше readTable отдавал «Субконто Дт 2», а клик про такое имя не знал.
Попутно закрыт второй дефект: безымянная picture-колонка определялась по ПЕРВОЙ
строке, а picField над Boolean не рисует картинку при Ложь → колонка пропадала из
columns целиком. Модель сэмплит до 10 строк и ищет ячейку по colindex.
Проверено:
- 24-multirow-header (стенд, оба паттерна ERP) — зелёный; до правки красный;
- клик проверяется по факту (DOM select+focus), а не по эху clicked.column:
до правки клик по «Срок» молча жал «Исполнитель 2» и рапортовал успех;
- живьём на ERP: список задач — 9 честных колонок вместо фантомов, значения на
местах; форма операции (шапка 2 этажа, строка 3 под-ряда) — «Субконто Дт/Кт 1..3»
сохранены, ничего не поехало;
- полный регресс 28/28.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Регресс-тесты web-test
E2E-тесты движка web-test (Playwright + изолированная синтетическая БД 1С), запускаются через node .claude/skills/web-test/scripts/run.mjs test.
Запуск
# Полный регресс (фикстура _hang/ в него не входит — см. ниже)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/
# Один файл
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/02-crud.test.mjs
# Несколько файлов (позиционные = пути к тестам, можно сколько угодно)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/04-selectvalue.test.mjs tests/web-test/11-report.test.mjs
# Несколько по фильтру тегов
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ --tags=table,smoke
# По regex имени теста
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ --grep=multi
URL не передаём позиционно — берётся из webtest.config.mjs (contexts.a.url = http://localhost:9191/webtest-runner/ru_RU). Переопределить можно флагом --url=<url>.
Exit code: 0 = все прошли, 1 = есть падения, 2 = сработал --global-timeout (отчёт записан, сеансы освобождены), 3 = зависло само сворачивание.
CLI флаги runner'а
| Флаг | Описание |
|---|---|
--url=URL |
Переопределить базовый URL (по умолчанию — из webtest.config.mjs) |
--tags=A,B |
Запустить только тесты с одним из тегов |
--grep=regex |
Фильтр по имени теста |
--bail |
Остановиться на первой ошибке |
--retry=N |
Перепрогон упавших тестов N раз |
--timeout=ms |
Таймаут одного теста (default 30000) |
--global-timeout=ms |
Потолок на весь прогон; по истечении — отчёт, освобождение сеансов, выход с кодом 2 |
--report=path |
Сохранить машинный отчёт в файл |
--report=- |
Машинный отчёт в stdout (прогресс → stderr) |
--format=json|allure|junit |
Формат отчёта |
--report-dir=path |
Корень для Allure/JUnit артефактов |
--screenshot=on-failure|every-step|off |
Когда снимать скриншоты |
--record |
Включить запись MP4 (CDP screencast → ffmpeg) |
Опции стенда (после --)
_hooks.mjs поднимает изолированный стенд (Apache на :9191, своя БД, отдельный набор EPF). По умолчанию работает в smart-режиме: пересборка только когда поменялся config-hash / epf-hash. Принудительно — через флаги после --:
# Принудительно пересобрать XML + БД + EPF
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ -- --rebuild-stand
# Точечно — только пересобрать БД из существующего XML (свежая синтетика)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ -- --reload-data
# Только пересобрать XML (когда хочется новой конфигурации)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ -- --rebuild-config
# Только EPF (внешние обработки для openFile)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ -- --rebuild-epf
| Флаг | Что делает |
|---|---|
--rebuild-stand |
Эквивалент всех трёх ниже |
--rebuild-config |
XML-исходники + БД |
--reload-data |
Только БД (drop+create+load+update) |
--rebuild-epf |
Только EPF-обработки |
Когда пересобирать стенд
Warm-старт (~200 ms): lockfile + probe Apache, БД жива, EPF на диске — ничего не делаем.
Триггеры авто-пересборки (без флагов):
- Изменился
config-hashсинтетической XML — пересобирается конфигурация + БД. - Изменился
epf-hashисходников EPF — пересобираются EPF.
Когда нужен --rebuild-stand вручную:
- БД накопила «мусорных» данных от write-сценариев.
15-multi-context-handoverсоздаёт нового Контрагента каждый прогон с unique-именем — со временем02-crudначнёт падать (КонтрагентООО Северуезжает заmaxRows=20). - Подозрение что Apache держит зависший процесс —
--rebuild-standделаетweb-stop+web-publish.
Конфигурация
tests/web-test/webtest.config.mjs задаёт:
contexts.a/contexts.b/contexts.c— независимые 1C-сеансы (разные cookies) на той же URL.a,b— два «пользователя» мультиконтекст-тестов;cзадействован только14-multi-context-routingи служит топливом для проверки вытеснения пула.defaultContext: 'a'— большинство тестов работают в одном контексте.isolation: 'tab'— вкладки в одном окне (default). Альтернатива'window'— отдельный BrowserContext (полная изоляция cookies).deadlines: {...}— бюджеты служебных операций (сброс состояния, закрытие контекста, скриншот…). Дефолты рассчитаны на лёгкий стенд; здесь не переопределяются. Если в выводе появились строки! resetState(a): timed out after Nms— стенд стал тяжелее бюджета, поднимай ключ, а не терпи: пробой сброса прерывает контекст (следующий тест получит чистый ценой перезапуска).maxContexts: 2/contextPolicy: 'reuse'/pinnedContexts: []— дай-фуд управления пулом лицензий. Лимит 2 одновременных сеанса: на границе14 (c) → 15 (a,b)раннер автоматически вытесняет LRU-контекстc(проверяется первым шагом15-multi-context-handover). Благодаря лимиту 3 контекста никогда не живут одновременно.
Env переменные
| Переменная | Значение |
|---|---|
WEB_TEST_PRESERVE_CLIPBOARD=0 |
Отключить save/restore буфера обмена вокруг pasteText |
WEBTEST_HOOKS_RUNTIME=python |
Использовать py-версии скиллов вместо ps1 (для не-Windows) |
Фикстура зависания — _hang/
Не часть регресса, а ручная проверка прерывания зависшего теста. _-префикс держит её вне обычного
прогона: walk() в discover.mjs пропускает _-записи внутри каталога, но явно переданный корневой путь
не фильтрует — поэтому test tests/web-test/ её не видит, а test tests/web-test/_hang находит.
# стенд должен быть уже опубликован — своих _hooks.mjs у фикстуры нет
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/_hang
Состав: 01-renderer-block вешает JS-поток рендерера (page.evaluate(() => { while(true) {} })) —
до появления abort-механики такой тест вешал прогон навсегда; 02-survivor идёт следом и доказывает,
что прогон поехал дальше, а контекст пересоздан со свежим сеансом (то есть лицензия вернулась).
Ожидаемый результат — 1 passed, 1 failed и exit code 1. Красный 01 здесь означает успех.
Читать глазами два условия:
01упал со строкойverdict: hang (renderer unresponsive, browser alive)иrecovery: context aborted (logout: node, closed: page)— а не завис;02зелёный — прогон продолжился и лицензия освободилась.
Автоматической обвязки (spawn раннера + проверка этих условий → 0/1) пока нет, поэтому в CI фикстуру как обычный набор класть нельзя — код выхода 1 тут штатный.
Когда гонять:
- правки пути очистки в
cli/commands/test.mjs(дедлайны, ветка таймаута) и жизненного цикла вengine/core/session.mjs(abortContext,probeContext,disconnect,createContext); - обновление Playwright — механика стоит на замеренном поведении библиотеки:
page.close({runBeforeUnload:false})возвращается поверх зависшегоevaluate, тот отваливается «Target closed», аcookies()отвечает при мёртвом рендерере (пробник). Новая версия может это поменять; - перед вливанием движковых правок web-test в
dev.
Цена: ~18 с чистого таймаута, намеренное уничтожение контекста, а в tab-режиме — ещё и перезапуск
браузера (закрывается последняя вкладка persistent-контекста).
Слепая зона: каскад logout считает успехом ответ 2xx. Если 1С однажды начнёт держать сеанс на куке,
а не на seanceId в URL, Node-logout продолжит получать 200, фикстура покажет logout: node — и поломка
всплывёт только когда утёкшие сеансы упрутся в лимит лицензий. Фикстура этого не ловит.
Воспроизводит один режим — заблокированный рендерер. Полумёртвый CDP (сокет жив, браузер не отвечает)
ловится дедлайнами, а не abortContext, и здесь не покрыт.
Артефакты
tests/web-test/error-*.png— скриншоты упавших шагов (auto на--screenshot=on-failure)tests/web-test/_allure/— Allure-результаты (на--format=allure)tests/skills/.cache/webtest-stand/— lockfiles стенда (config-hash, epf-hash, data-hash)
Известные нюансы
15-multi-context-handoverсоздаётunique-Контрагента и сохраняет — за серию прогонов накапливаются «лишние» записи. Если02-crudначал падать на «ООО Севердолжен быть в списке» — это симптом, лечится-- --rebuild-stand.04-selectvalueauto-history шаг — в изоляции делает warm-up через двойнойselectValue('Менеджер', 'ООО Юг')чтобы наполнить history, иначе первый вызов идёт черезmethod:form, а тест ожидаетmethod:dropdown. Не зависит от других файлов.- Скриншот ошибки только на последнем падении —
--screenshot=on-failure(default) делает один кадр в момент исключения. Для full-trace используй--screenshot=every-step.