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
parent 4a778cb3b1
commit b81a7504ce
9 changed files with 299 additions and 21 deletions
@@ -500,6 +500,35 @@ export const steps = [
validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/БезшапочнаяТаблица' },
},
// Обработка ПроверкаДоступности — форма с парами «доступный / недоступный»
// элементов каждого типа (кнопка командной панели, обычная кнопка, поле ввода,
// флажок, переключатель). Недоступность задаётся декларативно (disabled:true →
// <Enabled>false</Enabled>). Для регресса: clickElement по недоступной кнопке
// должен бросать ошибку, а getFormState — корректно помечать disabled у всех
// типов. Реквизиты парные (А=доступный, Б=недоступный), чтобы у элементов не
// совпадали companion-имена.
{
name: 'meta-compile: Обработка ПроверкаДоступности',
script: 'meta-compile/scripts/meta-compile',
input: {
type: 'DataProcessor', name: 'ПроверкаДоступности',
attributes: [
{ name: 'СтрокаДоступная', type: 'String', length: 50 },
{ name: 'СтрокаНедоступная', type: 'String', length: 50 },
{ name: 'ФлажокДоступный', type: 'Boolean' },
{ name: 'ФлажокНедоступный', type: 'Boolean' },
{ name: 'ВидДоступный', type: 'EnumRef.ВидыНоменклатуры' },
{ name: 'ВидНедоступный', type: 'EnumRef.ВидыНоменклатуры' },
{ name: 'ТумблерДоступный', type: 'EnumRef.СпособыУчёта' },
{ name: 'ТумблерНедоступный', type: 'EnumRef.СпособыУчёта' },
{ name: 'КонтрагентДоступный', type: 'CatalogRef.Контрагенты' },
{ name: 'КонтрагентНедоступный', type: 'CatalogRef.Контрагенты' },
],
},
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/ПроверкаДоступности' },
},
// Отчёт ОстаткиТоваров
{
name: 'meta-compile: Отчёт ОстаткиТоваров',
@@ -884,6 +913,111 @@ export const steps = [
`,
},
// Форма обработки ПроверкаДоступности — доступные/недоступные элементы каждого типа.
// Командная панель формы (autoCmdBar) и обычные кнопки в группе; поля/флажки/
// переключатели парами. Недоступные помечены disabled:true (→ <Enabled>false</Enabled>),
// 1С рисует их серыми: кнопки получают класс pressDisabled.
{
name: 'form-add: Форма обработки ПроверкаДоступности',
script: 'form-add/scripts/form-add',
args: { '-ObjectPath': '{workDir}/DataProcessors/ПроверкаДоступности.xml', '-FormName': 'ФормаОбработки' },
},
{
name: 'form-compile: Форма обработки ПроверкаДоступности',
script: 'form-compile/scripts/form-compile',
input: {
title: 'Проверка доступности',
attributes: [
{ name: 'Объект', type: 'DataProcessorObject.ПроверкаДоступности', main: true },
],
elements: [
// Командная панель формы: доступная + недоступная кнопка.
{ autoCmdBar: 'ФормаКоманднаяПанель', autofill: false, children: [
{ button: 'КомандаПанелиДоступна', command: 'КомандаПанелиДоступна', title: 'Доступная команда' },
{ button: 'КомандаПанелиНедоступна', command: 'КомандаПанелиНедоступна', title: 'Недоступная команда', disabled: true },
]},
// Обычные кнопки (в группе на теле формы, вне командной панели).
{ group: 'horizontal', name: 'ГруппаОбычныхКнопок', title: 'Обычные кнопки', children: [
{ button: 'КнопкаДоступна', command: 'КомандаКнопкаДоступна', title: 'Доступная кнопка' },
{ button: 'КнопкаНедоступна', command: 'КомандаКнопкаНедоступна', title: 'Недоступная кнопка', disabled: true },
]},
// Поля ввода.
{ input: 'СтрокаДоступная', path: 'Объект.СтрокаДоступная', title: 'Строка доступная' },
{ input: 'СтрокаНедоступная', path: 'Объект.СтрокаНедоступная', title: 'Строка недоступная', disabled: true },
// Флажки.
{ check: 'ФлажокДоступный', path: 'Объект.ФлажокДоступный', title: 'Флажок доступный' },
{ check: 'ФлажокНедоступный', path: 'Объект.ФлажокНедоступный', title: 'Флажок недоступный', disabled: true },
// Переключатели.
{ radio: 'ВидДоступный', path: 'Объект.ВидДоступный',
title: 'Вид доступный', radioButtonType: 'RadioButtons', titleLocation: 'Top',
choiceList: [
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Товар', presentation: 'Товар' },
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Услуга', presentation: 'Услуга' },
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Работа', presentation: 'Работа' },
],
},
{ radio: 'ВидНедоступный', path: 'Объект.ВидНедоступный',
title: 'Вид недоступный', radioButtonType: 'RadioButtons', titleLocation: 'Top', disabled: true,
choiceList: [
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Товар', presentation: 'Товар' },
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Услуга', presentation: 'Услуга' },
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Работа', presentation: 'Работа' },
],
},
// Тумблеры (переключатель с видом Tumbler — рендерится иначе, чем RadioButtons).
{ radio: 'ТумблерДоступный', path: 'Объект.ТумблерДоступный',
title: 'Тумблер доступный', radioButtonType: 'Tumbler', titleLocation: 'Top',
choiceList: [
{ value: 'Enum.СпособыУчёта.EnumValue.ПоСреднему', presentation: 'По среднему' },
{ value: 'Enum.СпособыУчёта.EnumValue.ФИФО', presentation: 'ФИФО' },
],
},
{ radio: 'ТумблерНедоступный', path: 'Объект.ТумблерНедоступный',
title: 'Тумблер недоступный', radioButtonType: 'Tumbler', titleLocation: 'Top', disabled: true,
choiceList: [
{ value: 'Enum.СпособыУчёта.EnumValue.ПоСреднему', presentation: 'По среднему' },
{ value: 'Enum.СпособыУчёта.EnumValue.ФИФО', presentation: 'ФИФО' },
],
},
// Ссылочные поля (для selectValue — выбор через DLB/CB-кнопки).
{ input: 'КонтрагентДоступный', path: 'Объект.КонтрагентДоступный', title: 'Контрагент доступный' },
{ input: 'КонтрагентНедоступный', path: 'Объект.КонтрагентНедоступный', title: 'Контрагент недоступный', disabled: true },
],
commands: [
{ name: 'КомандаПанелиДоступна', action: 'КомандаПанелиДоступна' },
{ name: 'КомандаПанелиНедоступна', action: 'КомандаПанелиНедоступна' },
{ name: 'КомандаКнопкаДоступна', action: 'КомандаКнопкаДоступна' },
{ name: 'КомандаКнопкаНедоступна', action: 'КомандаКнопкаНедоступна' },
],
},
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/ПроверкаДоступности/Forms/ФормаОбработки/Ext/Form.xml' },
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'DataProcessors/ПроверкаДоступности/Forms/ФормаОбработки/Ext/Form.xml' },
},
{
name: 'writeFile: ПроверкаДоступности form Module.bsl',
writeFile: 'DataProcessors/ПроверкаДоступности/Forms/ФормаОбработки/Ext/Form/Module.bsl',
content: `&НаКлиенте
Процедура КомандаПанелиДоступна(Команда)
\tСообщить("Команда панели (доступна)");
КонецПроцедуры
&НаКлиенте
Процедура КомандаПанелиНедоступна(Команда)
\tСообщить("Команда панели (недоступна)");
КонецПроцедуры
&НаКлиенте
Процедура КомандаКнопкаДоступна(Команда)
\tСообщить("Обычная кнопка (доступна)");
КонецПроцедуры
&НаКлиенте
Процедура КомандаКнопкаНедоступна(Команда)
\tСообщить("Обычная кнопка (недоступна)");
КонецПроцедуры
`,
},
// Форма обработки ДеревоНоменклатуры — tree-grid с двумя колонками
{
name: 'form-add: Форма обработки ДеревоНоменклатуры',
@@ -1479,6 +1613,7 @@ export const steps = [
'Constant.ОсновнаяВалюта',
'DataProcessor.ТестовыеОшибки',
'DataProcessor.ДеревоНоменклатуры',
'DataProcessor.ПроверкаДоступности',
],
},
args: { '-DefinitionFile': '{inputFile}', '-OutputDir': '{workDir}' },
@@ -1502,6 +1637,7 @@ export const steps = [
'DataProcessor.ДеревоНоменклатуры: Use View',
'DataProcessor.МножественныйВыбор: Use View',
'DataProcessor.БезшапочнаяТаблица: Use View',
'DataProcessor.ПроверкаДоступности: Use View',
],
},
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
+97
View File
@@ -0,0 +1,97 @@
export const name = 'Доступность элементов: getFormState помечает disabled, clickElement/fillFields/selectValue бросают';
export const tags = ['availability', 'disabled', 'click', 'fill', 'smoke'];
export const timeout = 120000;
const findBtn = (s, name) => s.buttons?.find(b => b.name === name);
const findField = (s, name) => s.fields?.find(f => f.name === name || f.label === name);
export default async function({ navigateLink, clickElement, fillFields, selectValue, getFormState, closeForm, assert, step, log }) {
// Один раз открываем форму обработки; disabled-гарды бросают ДО действия и состояние
// не портят, поэтому переиспользуем её между шагами, закрываем в конце.
await step('getFormState помечает disabled по всем типам контролов', async () => {
const s = await navigateLink('Обработка.ПроверкаДоступности');
log('buttons: ' + JSON.stringify(s.buttons));
// Кнопка командной панели (a.press)
assert.ok(!findBtn(s, 'Доступная команда')?.disabled, 'доступная команда — без disabled');
assert.equal(findBtn(s, 'Недоступная команда')?.disabled, true, 'недоступная команда — disabled');
// Обычная кнопка (frameButton)
assert.ok(!findBtn(s, 'Доступная кнопка')?.disabled, 'доступная кнопка — без disabled');
assert.equal(findBtn(s, 'Недоступная кнопка')?.disabled, true, 'недоступная кнопка (frameButton) — disabled');
// Поле ввода (нативный disabled)
assert.ok(!findField(s, 'СтрокаДоступная')?.disabled, 'доступное поле — без disabled');
assert.equal(findField(s, 'СтрокаНедоступная')?.disabled, true, 'недоступное поле — disabled');
// Флажок (checkboxDisabled)
assert.ok(!findField(s, 'ФлажокДоступный')?.disabled, 'доступный флажок — без disabled');
assert.equal(findField(s, 'ФлажокНедоступный')?.disabled, true, 'недоступный флажок — disabled');
// Переключатель RadioButtons (radioDisabled)
assert.ok(!findField(s, 'ВидДоступный')?.disabled, 'доступный переключатель — без disabled');
assert.equal(findField(s, 'ВидНедоступный')?.disabled, true, 'недоступный переключатель — disabled');
// Ссылочное поле (нативный disabled на input)
assert.ok(!findField(s, 'КонтрагентДоступный')?.disabled, 'доступное ссыл. поле — без disabled');
assert.equal(findField(s, 'КонтрагентНедоступный')?.disabled, true, 'недоступное ссыл. поле — disabled');
// Тумблер (tumblerDisabled на группе .frameTumbler) — сегменты идут в buttons[] с tumbler:true;
// у недоступного тумблера оба сегмента помечены disabled.
const disabledTumblers = (s.buttons || []).filter(b => b.tumbler && b.disabled);
assert.equal(disabledTumblers.length, 2, 'оба сегмента недоступного тумблера помечены disabled');
const enabledTumblers = (s.buttons || []).filter(b => b.tumbler && !b.disabled);
assert.equal(enabledTumblers.length, 2, 'сегменты доступного тумблера — без disabled');
});
await step('clickElement бросает на недоступной кнопке/frameButton/флажке, проходит на доступной', async () => {
let err = null;
try { await clickElement('Недоступная команда'); } catch (e) { err = e; }
assert.ok(err, 'клик по недоступной команде панели должен бросить');
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
err = null;
try { await clickElement('Недоступная кнопка'); } catch (e) { err = e; }
assert.ok(err, 'клик по недоступной обычной кнопке (frameButton) должен бросить');
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
err = null;
try { await clickElement('Флажок недоступный'); } catch (e) { err = e; }
assert.ok(err, 'клик по недоступному флажку должен бросить');
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
// Позитив: доступная кнопка кликается штатно (регресс — гард не ложно-срабатывает).
const r = await clickElement('Доступная команда');
assert.ok(r.clicked, 'доступная команда → clicked без ошибки');
});
await step('fillFields бросает на недоступном поле и флажке, проходит на доступном', async () => {
let err = null;
try { await fillFields({ 'Строка недоступная': 'тест' }); } catch (e) { err = e; }
assert.ok(err, 'заполнение недоступного поля должно бросить');
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
err = null;
try { await fillFields({ 'Флажок недоступный': 'true' }); } catch (e) { err = e; }
assert.ok(err, 'toggle недоступного флажка должен бросить');
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
// Позитив: доступное поле заполняется штатно.
const r = await fillFields({ 'Строка доступная': 'привет' });
assert.ok(r.filled?.some(f => f.ok), 'доступное поле заполнено');
});
await step('selectValue бросает на недоступном ссылочном поле, проходит на доступном', async () => {
let err = null;
try { await selectValue('Контрагент недоступный', 'ООО Север'); } catch (e) { err = e; }
assert.ok(err, 'selectValue по недоступному ссыл. полю должен бросить');
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
// Позитив: доступное ссылочное поле выбирается штатно.
const r = await selectValue('Контрагент доступный', 'ООО Север');
assert.ok(r.selected, 'доступное ссыл. поле — selected');
await closeForm({ save: false });
});
}
+1 -1
View File
@@ -5,7 +5,7 @@ E2E-тесты движка `web-test` (Playwright + изолированная
## Запуск
```bash
# Полный регресс (все 25 тестов; фикстура _hang/ в него не входит — см. ниже)
# Полный регресс (фикстура _hang/ в него не входит — см. ниже)
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/
# Один файл