From 9b65dccd8a79a7b8b545092442e1a822a435f613 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 20 Jul 2026 17:49:26 +0300 Subject: [PATCH] =?UTF-8?q?fix(web-test):=20=D0=BD=D0=B5=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B4=D0=B0=D0=B2=D0=B0=D1=82=D1=8C=20=D1=83=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5,=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=B5=D1=89?= =?UTF-8?q?=D1=91=20=D0=B8=D1=89=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filterList отчитывался успехом, пока динамический список ещё выполнял поиск: readTable возвращал предыдущие строки, а следующий клик по строке попадал в чужой объект. Отличить такой результат от верного нельзя — сценарий проходит зелёным по не тому документу. Дыра оказалась не в filterList, а в общем ожидании. waitForStable следил только за .loadingImage/.waitCurtain/.progressBar и счётчиком полей ввода — ни то, ни другое не меняется при перерисовке строк списка. Замер на реальной базе: за весь поиск старый признак isLoading не сработал НИ РАЗУ. При этом 1С всё это время показывает над списком информбар «Поиск...» — тот же .stateWindowSupportSurface, который движок уже отдаёт в errors.stateText. Читать умели, ждать — нет. waitForStable теперь считает видимый маркер занятости признаком «не готово»: счётчик стабильности сбрасывается, дедлайн продлевается, пока маркер виден, но не дольше BUSY_MAX_WAIT (60 с) — реальный поиск на боевом списке идёт десятки секунд, а зависшая операция всё равно завершает ожидание. Маркеры сопоставляются ПО ТЕКСТУ (Поиск/Ожид/Searching/Please wait), а не по факту наличия информбара: тот же носитель несёт терминальные сообщения отчётов («Отчет не сформирован», «Не установлено значение параметра»), и ожидание их исчезновения вешало бы каждый отчёт до таймаута. Радиус общий, а не точечный в filterList: маркер — индикатор длительной операции вообще, тот же класс гонки достижим из clickElement и openCommand. Частное лечение для кнопок (CDP-монитор в click-form) уже есть; второй костыль сделал бы третий неизбежным. CDP-монитор как гейт не годится отдельно: он считает готовностью паузу 300 мс без запросов, а при фоновой операции с периодическим опросом такие паузы штатны. Проверено на реальной базе: текст информбара — «Поиск...», виден t=6.7..13.2 с, опрос, идентичный гейтовому, увидел занятость дважды (isLoading — ноль раз); полный регресс 29/29 до и после, file/name/status идентичны, суммарное время 879.9 → 859.0 с, отчёты не зависли. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/web-test/SKILL.md | 2 +- .../web-test/scripts/engine/core/state.mjs | 7 ++++- .../web-test/scripts/engine/core/wait.mjs | 29 +++++++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index fd07007f..1a3b1922 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -170,7 +170,7 @@ const form = await getFormState(); **confirmation** — if present, a Yes/No dialog is shown. Call `clickElement('Да')` or `clickElement('Нет')`. -**errors.stateText** — array of SpreadsheetDocument state messages (e.g. `"Не установлено значение параметра \"X\""`, `"Отчет не сформирован..."`, `"Изменились настройки..."`). Present when the report area shows an info bar instead of data. +**errors.stateText** — array of SpreadsheetDocument state messages (e.g. `"Не установлено значение параметра \"X\""`, `"Отчет не сформирован..."`, `"Изменились настройки..."`). Present when the report area shows an info bar instead of data. The same info bar carries `"Поиск..."` while a list is still searching — actions do not return while it is up, so a filtered list never hands you the previous rows. ### Reading data diff --git a/.claude/skills/web-test/scripts/engine/core/state.mjs b/.claude/skills/web-test/scripts/engine/core/state.mjs index 612530f9..b275cfcc 100644 --- a/.claude/skills/web-test/scripts/engine/core/state.mjs +++ b/.claude/skills/web-test/scripts/engine/core/state.mjs @@ -1,4 +1,4 @@ -// web-test core/state v1.17 — module-level state for the web-test engine. +// web-test core/state v1.18 — module-level state for the web-test engine. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // // Holds the single browser/page/recorder slot plus the multi-context registry, @@ -80,6 +80,11 @@ export const ACTION_WAIT = 2000; // fallback minimum wait export const MAX_WAIT = 10000; // max wait for stability export const POLL_INTERVAL = 200; // polling interval export const STABLE_CYCLES = 3; // consecutive stable cycles needed +// Ceiling for the case where 1C is VISIBLY still working (its "Поиск…" state window is up). +// Higher than MAX_WAIT on purpose: a search on a production-sized list legitimately runs for +// tens of seconds, and returning mid-search hands the caller the previous rows — a wrong +// result that looks like a right one. Bounded so a wedged operation still ends the wait. +export const BUSY_MAX_WAIT = 60000; // 1C browser extension ID (stable across versions, defined by key in manifest.json) export const EXT_ID = 'pbhelknnhilelbnhfpcjlcabhmfangik'; diff --git a/.claude/skills/web-test/scripts/engine/core/wait.mjs b/.claude/skills/web-test/scripts/engine/core/wait.mjs index 4d6590a3..851b5850 100644 --- a/.claude/skills/web-test/scripts/engine/core/wait.mjs +++ b/.claude/skills/web-test/scripts/engine/core/wait.mjs @@ -1,30 +1,49 @@ -// web-test core/wait v1.17 — Smart wait helpers: DOM stability polling, JS-expression polling, CDP network monitor. +// web-test core/wait v1.18 — Smart wait helpers: DOM stability polling, JS-expression polling, CDP network monitor. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -import { page, MAX_WAIT, POLL_INTERVAL, STABLE_CYCLES } from './state.mjs'; +import { page, MAX_WAIT, BUSY_MAX_WAIT, POLL_INTERVAL, STABLE_CYCLES } from './state.mjs'; import { detectFormScript } from '../../dom.mjs'; /** * Smart wait: poll until DOM is stable and no loading indicators are visible. - * Checks: form number change, loading indicators, DOM stability. + * Checks: form number change, loading indicators, busy state window, DOM stability. * @param {number|null} previousFormNum — form number before the action (null = don't check) */ export async function waitForStable(previousFormNum = null) { let stableCount = 0; let lastSnapshot = ''; const start = Date.now(); + let deadline = start + MAX_WAIT; - while (Date.now() - start < MAX_WAIT) { + while (Date.now() < deadline) { await page.waitForTimeout(POLL_INTERVAL); // Check for loading indicators const status = await page.evaluate(`(() => { const loading = document.querySelector('.loadingImage, .waitCurtain, .progressBar'); const isLoading = loading && loading.offsetWidth > 0; + // While a dynamic list is still searching, 1C floats a state window over the grid + // ("Поиск…") — and NOTHING else in the DOM says so: the old rows stay put, the element + // counters below don't move, so the page looks perfectly stable with stale data. + // Match busy markers by text, never "state window present": the same carrier also holds + // TERMINAL report messages ("Отчет не сформирован", "Не установлено значение параметра"), + // and waiting for those to disappear would hang until the timeout on every report. + const busy = [...document.querySelectorAll('.stateWindowSupportSurface')].some(el => + el.offsetWidth > 0 && /^\\s*(Поиск|Ожид|Searching|Please wait)/i.test(el.innerText || '')); const formCount = document.querySelectorAll('input.editInput[id], a.press[id]').length; - return { isLoading, formCount }; + return { isLoading, busy, formCount }; })()`); + // A visible busy indicator outranks DOM stability — it is the only evidence we get that the + // server is still working. Push the deadline while it lasts (bounded by BUSY_MAX_WAIT) instead + // of reporting "stable": returning mid-search is what handed a caller the previous rows and + // let the next click open the wrong document. + if (status.busy) { + deadline = Math.min(Date.now() + MAX_WAIT, start + BUSY_MAX_WAIT); + stableCount = 0; + continue; + } + if (status.isLoading) { stableCount = 0; continue;