fix(web-test): не отдавать управление, пока список ещё ищет

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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-20 18:24:01 +03:00
co-authored by Claude Opus 4.8
parent 6256c48c05
commit 9b65dccd8a
3 changed files with 31 additions and 7 deletions
+1 -1
View File
@@ -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
@@ -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';
@@ -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;