feat(web-test): чтение и раскрытие сворачиваемых групп формы

getFormState().groups → [{name, title, collapsed}] для сворачиваемых
групп (оба варианта ControlRepresentation: заголовок-гиперссылка и
картинка-каретка #titleBtn). Обычные несворачиваемые группы не
попадают. Состояние — по display первого контент-сиблинга за #title_div
(переживает свободные элементы между группами: при обходе Form.xml дети
группы идут до следующего сиблинга).

clickElement(title, {expand}/{expand:false}/{toggle}) раскрывает/
сворачивает группу — единый словарь с грид-узлами/деревьями, клик по
#titleBtn (вариант «картинка») или заголовку-гиперссылке. Новый
kind:'formGroup' в findClickTargetScript + хендлер click-group.mjs.

Фикстура СтраницаНастроек расширена вариантами A/B + негатив (обычная
группа) + стресс-привязка (свободный элемент между группами); тест
25-decoration-form покрывает чтение и expand/collapse/toggle. Полный
регресс 29 passed.

Popup-группы: содержимое в отдельном слое, состояние пока не читается
надёжно (follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-18 12:56:55 +03:00
co-authored by Claude Opus 4.8
parent 86cddc8ec3
commit bd86ece90e
7 changed files with 167 additions and 5 deletions
@@ -1296,6 +1296,44 @@ export const steps = [
{ label: 'ПояснениеКлассификаторы', title: 'Загрузка курсов валют и классификаторов' },
],
},
// Развёрнутая сворачиваемая (collapsed:false) — сверка сигнала collapsed/expanded.
{ group: 'collapsible', name: 'ГруппаРазвёрнутая', title: 'Развёрнутая группа',
collapsed: false, showTitle: true, children: [
{ label: 'ПояснениеРазвёрнутая', title: 'Содержимое развёрнутой группы' },
],
},
// Вариант B: контрол сворачивания как КАРТИНКА (#titleBtn-каретка) — как в реальном ERP,
// в отличие от дефолтного «заголовок-гиперссылка» (вариант A) на этой платформе.
{ group: 'collapsible', name: 'ГруппаКартинкой', title: 'Свёрнута картинкой',
controlRepresentation: 'Picture', collapsed: true, showTitle: true, children: [
{ label: 'ПояснениеКартинкой', title: 'Содержимое группы с картинкой-контролом' },
],
},
// Всплывающая (popup) группа — тоже прячет содержимое за триггером.
{ group: 'vertical', name: 'ГруппаВсплывающая', title: 'Всплывающая группа', behavior: 'popup',
collapsed: true, showTitle: true, children: [
{ label: 'ПояснениеВсплывающая', title: 'Содержимое всплывающей группы' },
],
},
// Обычная НЕсворачиваемая группа — негатив: НЕ должна попадать в groups[] (не шуметь).
{ group: 'vertical', name: 'ГруппаОбычная', title: 'Обычная группа', showTitle: true, children: [
{ label: 'ПояснениеОбычная', title: 'Содержимое обычной группы' },
],
},
// СТРЕСС привязки контента: свёрнутая группа → свободный элемент верхнего уровня →
// развёрнутая группа. Проверяем, отличим ли «свободный сосед после свёрнутой» от
// «видимого дочернего развёрнутой» (порядок DOM их путает; нужен геометрич./иной сигнал).
{ group: 'collapsible', name: 'ГруппаСтрессСвёрнутая', title: 'Стресс: свёрнутая',
collapsed: true, showTitle: true, children: [
{ label: 'ВнутриСтрессСвёрнутой', title: 'Внутри свёрнутой стресс-группы' },
],
},
{ label: 'СвободныйМеждуГруппами', title: 'Свободный элемент между группами' },
{ group: 'collapsible', name: 'ГруппаСтрессРазвёрнутая', title: 'Стресс: развёрнутая',
collapsed: false, showTitle: true, children: [
{ label: 'ВнутриСтрессРазвёрнутой', title: 'Внутри развёрнутой стресс-группы' },
],
},
],
},
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
+38 -1
View File
@@ -8,8 +8,16 @@ export const timeout = 90000;
// Регресс на расширенный селектор детекции формы (dom/_shared.mjs, DETECT_FORM_FN /
// DETECT_FORMS_FN): до фикса detectForm возвращал null → getFormState = «No form detected»
// (form:null, formCount:0), навык такую форму НЕ видел.
//
// Плюс: сворачиваемые группы формы в getFormState().groups (состояние collapsed) и их
// раскрытие/сворачивание через clickElement {expand}/{toggle}. Форма содержит оба варианта
// рендера контрола (заголовок-гиперссылка и картинка-каретка ControlRepresentation), негатив
// (обычная несворачиваемая группа — НЕ в groups[]) и стресс-привязку (свободный элемент между
// группами не путает определение состояния).
export default async function({ navigateSection, openCommand, navigateLink, getFormState, closeForm, assert, step, log }) {
export default async function({ navigateSection, openCommand, navigateLink, getFormState, clickElement, closeForm, assert, step, log }) {
const collapsedOf = (s, name) => (s.groups || []).find(x => x.name === name)?.collapsed;
await step('раздел: открыть «Страница настроек» командой из «Администрирование»', async () => {
await navigateSection('Администрирование');
@@ -33,4 +41,33 @@ export default async function({ navigateSection, openCommand, navigateLink, getF
assert.includes(hy, 'Техническая информация', 'гиперссылка видна и в getFormState');
await closeForm();
});
await step('groups: getFormState показывает сворачиваемые группы + состояние', async () => {
const s = await navigateLink('Обработка.СтраницаНастроек');
log(`groups=${JSON.stringify((s.groups || []).map(g => [g.name, g.collapsed]))}`);
assert.ok(s.groups?.length, 'groups[] присутствует');
assert.equal(collapsedOf(s, 'ГруппаКлассификаторы'), true, 'вариант A (гиперссылка) — свёрнута');
assert.equal(collapsedOf(s, 'ГруппаРазвёрнутая'), false, 'развёрнутая — collapsed:false');
assert.equal(collapsedOf(s, 'ГруппаКартинкой'), true, 'вариант B (картинка) — свёрнута');
assert.ok(!s.groups.some(g => g.name === 'ГруппаОбычная'),
'обычная несворачиваемая группа НЕ попадает в groups[] (негатив — не шумит)');
// Стресс-привязка: свободный элемент между группами не путает определение состояния.
assert.equal(collapsedOf(s, 'ГруппаСтрессСвёрнутая'), true, 'стресс: свёрнутая определена верно');
assert.equal(collapsedOf(s, 'ГруппаСтрессРазвёрнутая'), false, 'стресс: развёрнутая определена верно');
});
await step('group toggle: clickElement {expand}/{toggle} по вариантам A и B', async () => {
// Вариант A (заголовок-гиперссылка): раскрыть → идемпотентный повтор → свернуть.
let r = await clickElement('Классификаторы и курсы валют', { expand: true });
assert.equal(r.clicked.toggled, true, 'A expand:true — кликнул');
assert.equal(collapsedOf(r, 'ГруппаКлассификаторы'), false, 'A раскрыта');
r = await clickElement('Классификаторы и курсы валют', { expand: true });
assert.equal(r.clicked.toggled, false, 'A expand:true повторно — идемпотентно (no-op)');
r = await clickElement('Классификаторы и курсы валют', { expand: false });
assert.equal(collapsedOf(r, 'ГруппаКлассификаторы'), true, 'A свёрнута обратно');
// Вариант B (картинка-каретка #titleBtn): toggle.
r = await clickElement('Свёрнута картинкой', { toggle: true });
assert.equal(collapsedOf(r, 'ГруппаКартинкой'), false, 'B раскрыта тоглом');
await closeForm();
});
}