feat(web-test): гард недоступных элементов + метка disabled в getFormState

clickElement/fillFields/selectValue бросали ложный успех при действии над
недоступным (disabled) контролом — в 1С это no-op. Причина: резолвер цели
клика не смотрел признак недоступности, который ридер getFormState уже знал.

- резолвер клика снимает disabled (кнопки/frameButton/флажок/тумблер/поле),
  clickElement бросает `"X" is disabled` вместо тихого no-op;
- fillFields и selectValue тоже бросают на недоступном поле/флажке/ссылке;
- getFormState помечает disabled у frameButton, флажка, переключателя и
  тумблера (раньше был только у a.press-кнопок и полей ввода);
- стенд: обработка ПроверкаДоступности с парами доступный/недоступный по всем
  типам контролов (в подсистеме Администрирование) + тест 23-availability;
- SKILL.md: заметка про disabled у getFormState и throw у clickElement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-16 13:53:33 +03:00
co-authored by Claude Opus 4.8
parent 4a778cb3b1
commit b81a7504ce
9 changed files with 299 additions and 21 deletions
@@ -1,4 +1,4 @@
// web-test dom shared v1.2 — embedded JS function constants
// web-test dom shared v1.3 — embedded JS function constants
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
/**
* Shared function strings embedded into page.evaluate() generators.
@@ -214,6 +214,7 @@ function readForm(p) {
type: 'checkbox'
};
if (label && label !== name) field.label = label;
if (el.classList.contains('checkboxDisabled')) field.disabled = true;
fields.push(field);
});
@@ -249,6 +250,7 @@ function readForm(p) {
options: options.map(o => o.label)
};
if (label && label !== name) field.label = label;
if (document.getElementById(p + name)?.classList.contains('radioDisabled')) field.disabled = true;
fields.push(field);
}
@@ -277,15 +279,21 @@ function readForm(p) {
const text = nbsp(el.innerText?.trim() || '');
const idName = el.id?.replace(p, '') || '';
if (!text && !idName) return;
buttons.push({ name: text || idName, frame: true });
// frameButton disabled uses the same class as a.press buttons (pressDisabled).
const btn = { name: text || idName, frame: true };
if (el.classList.contains('pressDisabled')) btn.disabled = true;
buttons.push(btn);
});
// Tumbler items
// Tumbler items. Disabled state lives on the group element .frameTumbler
// (class tumblerDisabled), not on the individual segments.
document.querySelectorAll('[id^="' + p + '"].tumblerItem').forEach(el => {
if (el.offsetWidth === 0) return;
const text = el.innerText?.trim();
const idName = el.id?.replace(p, '') || '';
buttons.push({ name: text || idName, tumbler: true });
const btn = { name: text || idName, tumbler: true };
if (el.closest('.frameTumbler')?.classList.contains('tumblerDisabled')) btn.disabled = true;
buttons.push(btn);
});
// Tabs — scoped to form by checking ancestor IDs