fix(web-test): клик в видимую ячейку строки формы выбора + пер-колоночный/exact матчинг

scanGridRowsScript целился в центр всей .gridLine; на широкой форме выбора (колонок
больше, чем влезает в окно) центр-X уезжает за вьюпорт в оверлей → клик мимо строки →
Enter не выбирает → форма не закрывается → ложное not_selectable «group/folder row».

- dom/grid v1.10: матч по ячейкам (строка: exact→startsWith→includes; объект {col:val}:
  пер-колоночно, AND, предпочтение exact-all), точка клика — первая видимая текстовая
  ячейка строки (не центр); возврат visibleSample/isGroup. scrollIntoView не нужен
  (динсписок держит в DOM только видимые строки).
- forms/select-value v1.25: структурированный search в скан (без склейки значений
  объекта), различение still_open vs реальная группа (isGroup), actionable not_found
  со списком видимых кандидатов.
- tests: широкая (14 колонок, choiceMode) форма выбора Контрагентов + контрагент «Север»
  рядом с «ООО Север» → детерминированный регресс центр-X + exact-preference;
  selectValue (04-selectvalue) и fillTableRow составной ячейки (05-table).

Проверено: живой E2E на реальной форме выбора составного типа, red-green на синтетике,
полный регресс web-test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-24 12:30:21 +03:00
parent 38d89af47d
commit a633520c66
5 changed files with 255 additions and 56 deletions
@@ -31,6 +31,16 @@ export const steps = [
{ name: 'Телефон', type: 'String', length: 20 },
{ name: 'Адрес', type: 'String', length: 200 },
{ name: 'КодКПП', type: 'String', length: 9 },
// Доп. строковые реквизиты — выводятся в широкую ФОРМУ ВЫБОРА (ниже),
// чтобы строка формы выбора стала шире окна выбора. Регресс бага
// «центр широкой строки уезжает за вьюпорт → клик мимо» (04-selectvalue).
{ name: 'Регион', type: 'String', length: 50 },
{ name: 'Город', type: 'String', length: 50 },
{ name: 'Улица', type: 'String', length: 100 },
{ name: 'БИК', type: 'String', length: 9 },
{ name: 'ОГРН', type: 'String', length: 13 },
{ name: 'ОКПО', type: 'String', length: 10 },
{ name: 'ВидДеятельности', type: 'String', length: 100 },
],
},
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
@@ -311,6 +321,11 @@ export const steps = [
\tСписок.Добавить(Новый Структура("Имя,ИНН", "ООО Юг", "7700000002"));
\tСписок.Добавить(Новый Структура("Имя,ИНН", "ООО Восток", "7700000003"));
\tСписок.Добавить(Новый Структура("Имя,ИНН", "АО Запад", "7700000004"));
\t// Контрагент с именем ровно «Север» рядом с «ООО Север» — для детерминированного
\t// регресса широкой формы выбора (04-selectvalue): поиск «Север» даёт 2 вхождения,
\t// «ООО Север» сортируется раньше. Багованный клик-по-центру/эскалация выберут
\t// «ООО Север»; фикс через exact-preference обязан выбрать точное «Север».
\tСписок.Добавить(Новый Структура("Имя,ИНН", "Север", "7700000005"));
\tДля Каждого Запись Из Список Цикл
\t\tЭлемент = Справочники.Контрагенты.СоздатьЭлемент();
\t\tЭлемент.Наименование = Запись.Имя;
@@ -572,6 +587,50 @@ export const steps = [
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'Catalogs/Контрагенты/Forms/ФормаСписка/Ext/Form.xml' },
},
// Форма ВЫБОРА Контрагенты — НАМЕРЕННО ШИРОКАЯ (14 колонок), чтобы строка была
// шире окна выбора. Регресс бага «центр широкой строки уезжает за вьюпорт →
// клик в оверлей → not_selectable» (04-selectvalue/direct-form, выбор «Север»).
// form-add с Purpose=Choice авто-назначает её DefaultChoiceForm → именно она
// открывается при выборе ссылки на Контрагента.
{
name: 'form-add: Форма выбора Контрагенты',
script: 'form-add/scripts/form-add',
args: { '-ObjectPath': '{workDir}/Catalogs/Контрагенты.xml', '-FormName': 'ФормаВыбора', '-Purpose': 'Choice' },
},
{
name: 'form-compile: Форма выбора Контрагенты (широкая)',
script: 'form-compile/scripts/form-compile',
input: {
title: 'Выбор контрагента',
attributes: [
{ name: 'Список', type: 'DynamicList', main: true,
settings: { mainTable: 'Catalog.Контрагенты', dynamicDataRead: true } },
],
elements: [
// choiceMode: true → <ChoiceMode>true</ChoiceMode> на таблице: Enter/двойной
// клик ПОДТВЕРЖДАЮТ выбор (а не открывают элемент). Без него форма ведёт
// себя как обычный список (Enter открывает элемент).
{ table: 'Список', path: 'Список', choiceMode: true, columns: [
{ input: 'Code', path: 'Список.Code', title: 'Код' },
{ input: 'Description', path: 'Список.Description', title: 'Наименование' },
{ input: 'ИНН', path: 'Список.ИНН', title: 'ИНН' },
{ input: 'Телефон', path: 'Список.Телефон', title: 'Телефон' },
{ input: 'Адрес', path: 'Список.Адрес', title: 'Адрес' },
{ input: 'КодКПП', path: 'Список.КодКПП', title: 'КПП' },
{ input: 'Регион', path: 'Список.Регион', title: 'Регион' },
{ input: 'Город', path: 'Список.Город', title: 'Город' },
{ input: 'Улица', path: 'Список.Улица', title: 'Улица' },
{ input: 'БИК', path: 'Список.БИК', title: 'БИК' },
{ input: 'ОГРН', path: 'Список.ОГРН', title: 'ОГРН' },
{ input: 'ОКПО', path: 'Список.ОКПО', title: 'ОКПО' },
{ input: 'ВидДеятельности', path: 'Список.ВидДеятельности', title: 'Вид деятельности' },
]},
],
},
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/Catalogs/Контрагенты/Forms/ФормаВыбора/Ext/Form.xml' },
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'Catalogs/Контрагенты/Forms/ФормаВыбора/Ext/Form.xml' },
},
// Форма элемента Номенклатура — 2 вкладки, все типы полей
{
name: 'form-add: Форма элемента Номенклатура',
+11 -3
View File
@@ -22,18 +22,26 @@ export default async function({ navigateSection, openCommand, clickElement, sele
await closeForm({ save: false });
});
await step('direct-form: Контрагент → CatalogRef.Контрагенты (quickChoice=false)', async () => {
await step('direct-form: Контрагент → форма выбора (ШИРОКАЯ — регресс центр-X + exact-preference)', async () => {
// Форма выбора Контрагентов намеренно широкая (14 колонок) — строка шире окна.
// Старый scanGridRows целился в ЦЕНТР строки → клик в оверлей за вьюпортом →
// не та строка / not_selectable. Новый — в первую видимую ячейку.
// В справочнике есть и «ООО Север», и ровно «Север»; поиск «Север» даёт 2
// вхождения, «ООО Север» сортируется раньше. Багованный путь выбрал бы «ООО
// Север»; фикс (exact-preference + клик в видимую ячейку) обязан выбрать «Север».
await navigateSection('Склад');
await openCommand('Приходная накладная');
await clickElement('Создать');
const result = await selectValue('Контрагент', 'Север');
log(`method=${result.selected?.method}, search=${result.selected?.search}`);
log(`method=${result.selected?.method}, search=${result.selected?.search}, err=${result.selected?.error || ''}`);
assert.equal(result.selected?.method, 'form', 'Должен быть метод form (через форму выбора)');
assert.ok(!result.selected?.error, `выбор без ошибки (было not_selectable): ${result.selected?.message || ''}`);
const field = findField(result, 'Контрагент');
log(`Контрагент value='${field?.value}'`);
assert.includes(field?.value || '', 'Север', 'Контрагент должен показать выбранное значение');
assert.equal((field?.value || '').trim(), 'Север',
'exact-preference + клик в видимую ячейку: выбран точный «Север», не «ООО Север»');
await closeForm({ save: false });
});
+26
View File
@@ -120,4 +120,30 @@ export default async function({ navigateSection, openCommand, clickElement, fill
assert.equal(t.rows[0]['Номенклатура'], 'Товар 03', 'Удалена первая (Товар 02), осталась Товар 03');
await closeForm({ save: false });
});
await step('composite-wide-form: ИсточникТЧ {value,type} через ШИРОКУЮ форму выбора', async () => {
// Прямой регресс исходного симптома: fillTableRow в составную ячейку → диалог
// типа → широкая форма выбора Контрагентов (14 колонок, строка шире окна).
// Старый scanGridRows целился в центр строки → клик в оверлей → not_selectable.
// Детерминированность: «ООО Север» сортируется раньше точного «Север»; багованный
// путь выбрал бы «ООО Север», фикс (exact-preference + клик в видимую ячейку) — «Север».
await navigateSection('Склад');
await openCommand('Приходная накладная');
await clickElement('Создать');
const r = await fillTableRow(
{ 'Источник': { value: 'Север', type: 'Контрагент' } },
{ table: 'Товары', add: true }
);
log(`composite fill: ${JSON.stringify(r.filled)}`);
const item = (r.filled || []).find(f => /сточник/.test(f.field || ''));
assert.ok(item?.ok, `ячейка Источник заполнена без ошибки: ${item?.error || ''} ${item?.message || ''}`);
const t = await readTable({ table: 'Товары' });
log(`Источник cell='${t.rows[0]?.['Источник']}'`);
assert.equal(t.rows[0]?.['Источник'], 'Север',
'exact-preference + клик в видимую ячейку: выбран точный «Север», не «ООО Север»');
await closeForm({ save: false });
});
}