test(web-test): стенд с двухэтажной шапкой + красный тест резолвинга колонок

Обработка МногострочнаяШапка воспроизводит два паттерна, снятых живьём с ERP:

1. паттерн «Задачи» — широкая колонка «Исполнитель» (x 705..1206) над парой узких
   «Срок» (705..956) и «Выполнена» (956..1206). У каждой ячейки есть шапка со своим
   colindex → верный ответ однозначен, но матчинг по центру x его не находит.
2. паттерн «Операция» — шапка только у группы «Субконто» (showInHeader:false у детей,
   как «Субконто Дт» в ERP), ячеек три без своих шапок → разворот в «Субконто 1/2/3»
   правилен и должен пережить правку.

Тест 24-multirow-header покрывает чтение, клик и заполнение. Сейчас КРАСНЫЙ — фиксирует
дефект до правки:

  columns: [... "Исполнитель 1","Исполнитель 2","Исполнитель 3","Срок","Выполнена" ...]
  row0:    "Исполнитель 2": "Срок 1 / Выполнена 1"   ← значения склеены в чужую колонку
           "Срок": ""   "Выполнена": ""              ← свои колонки пусты

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-17 10:35:03 +03:00
parent ad8a2e5cab
commit 96b38d9f91
2 changed files with 184 additions and 0 deletions
@@ -542,6 +542,22 @@ export const steps = [
validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/БезшапочнаяТаблица' },
},
// Обработка МногострочнаяШапка — грид с ДВУХЭТАЖНОЙ шапкой, для регресса резолвинга колонок
// (24-multirow-header). Воспроизводит два паттерна, снятые с ERP:
// 1) «Задачи» — широкая колонка сверху над парой узких: центр широкой ячейки попадает в
// x-диапазон ПРАВОЙ узкой шапки, и матчинг по геометрии ворует ячейку;
// 2) «Операция» — объединённая шапка над несколькими под-рядами: законный разворот в
// «Имя 1/2/3», который ломать нельзя.
{
name: 'meta-compile: Обработка МногострочнаяШапка',
script: 'meta-compile/scripts/meta-compile',
input: {
type: 'DataProcessor', name: 'МногострочнаяШапка',
},
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/МногострочнаяШапка' },
},
// Обработка ПроверкаДоступности — форма с парами «доступный / недоступный»
// элементов каждого типа (кнопка командной панели, обычная кнопка, поле ввода,
// флажок, переключатель). Недоступность задаётся декларативно (disabled:true →
@@ -1584,6 +1600,86 @@ export const steps = [
`,
},
// Обработка МногострочнаяШапка — форма с двухэтажной шапкой (columnGroup vertical).
{
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: 'Многострочная шапка',
events: { OnCreateAtServer: 'ПриСозданииНаСервере' },
attributes: [
{ name: 'Объект', type: 'DataProcessorObject.МногострочнаяШапка', main: true },
{ name: 'Таблица', type: 'ValueTable', columns: [
{ name: 'Код', type: 'String(10)', title: 'Код' },
{ name: 'Задача', type: 'String(50)', title: 'Задача' },
{ name: 'Автор', type: 'String(50)', title: 'Автор' },
{ name: 'Исполнитель', type: 'String(50)', title: 'Исполнитель' },
{ name: 'Срок', type: 'String(50)', title: 'Срок' },
{ name: 'Выполнена', type: 'String(50)', title: 'Выполнена' },
{ name: 'Субконто1', type: 'String(50)', title: 'Субконто1' },
{ name: 'Субконто2', type: 'String(50)', title: 'Субконто2' },
{ name: 'Субконто3', type: 'String(50)', title: 'Субконто3' },
]},
],
elements: [
{ table: 'Таблица', path: 'Таблица', changeRowSet: true, columns: [
{ input: 'Код', path: 'Таблица.Код', title: 'Код', width: 8 },
// Простая вертикальная пара: у каждой колонки своя шапка (как «Задача»/«Автор» в ERP).
{ columnGroup: 'vertical', name: 'ГруппаЗадача', showInHeader: false, children: [
{ input: 'Задача', path: 'Таблица.Задача', title: 'Задача', width: 30 },
{ input: 'Автор', path: 'Таблица.Автор', title: 'Автор', width: 30 },
]},
// ПАТТЕРН «ЗАДАЧИ»: широкая колонка сверху над горизонтальной парой узких.
// Ширина «Исполнитель» = сумма пары → его центр падает в x-диапазон «Выполнена».
{ columnGroup: 'vertical', name: 'ГруппаИсполнитель', showInHeader: false, children: [
{ input: 'Исполнитель', path: 'Таблица.Исполнитель', title: 'Исполнитель', width: 40 },
{ columnGroup: 'horizontal', name: 'ГруппаСрокВыполнена', showInHeader: false, children: [
{ input: 'Срок', path: 'Таблица.Срок', title: 'Срок', width: 20 },
{ input: 'Выполнена', path: 'Таблица.Выполнена', title: 'Выполнена', width: 20 },
]},
]},
// ПАТТЕРН «ОПЕРАЦИЯ»: одна шапка группы над тремя под-рядами БЕЗ своих боксов в шапке
// (showInHeader:false у детей — как «Субконто Дт» в ERP: шапка только у группы,
// ячеек три) → readTable разворачивает в «Субконто 1/2/3». Регресс этого разворота.
{ columnGroup: 'vertical', name: 'ГруппаСубконто', title: 'Субконто', showInHeader: true, children: [
{ input: 'Субконто1', path: 'Таблица.Субконто1', showInHeader: false, width: 25 },
{ input: 'Субконто2', path: 'Таблица.Субконто2', showInHeader: false, width: 25 },
{ input: 'Субконто3', path: 'Таблица.Субконто3', showInHeader: false, width: 25 },
]},
]},
],
},
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Для Сч = 1 По 3 Цикл
\t\tС = Формат(Сч, "ЧН=; ЧГ=");
\t\tСтрока = Таблица.Добавить();
\t\tСтрока.Код = "К" + С;
\t\tСтрока.Задача = "Задача " + С;
\t\tСтрока.Автор = "Автор " + С;
\t\tСтрока.Исполнитель = "Исполнитель " + С;
\t\tСтрока.Срок = "Срок " + С;
\t\tСтрока.Выполнена = "Выполнена " + С;
\t\tСтрока.Субконто1 = "Субконто1 " + С;
\t\tСтрока.Субконто2 = "Субконто2 " + С;
\t\tСтрока.Субконто3 = "Субконто3 " + С;
\tКонецЦикла;
КонецПроцедуры
`,
},
// ── 4. DCS for report ──
// Сначала добавляем макет ОсновнаяСхемаКомпоновкиДанных к отчёту (регистрируется
// в Reports/ОстаткиТоваров.xml + автоматически выставляется MainDataCompositionSchema),
@@ -1647,6 +1743,7 @@ export const steps = [
'Report.ОстаткиТоваров',
'DataProcessor.МножественныйВыбор',
'DataProcessor.БезшапочнаяТаблица',
'DataProcessor.МногострочнаяШапка',
],
},
args: { '-DefinitionFile': '{inputFile}', '-OutputDir': '{workDir}' },
@@ -1687,6 +1784,7 @@ export const steps = [
'DataProcessor.ДеревоНоменклатуры: Use View',
'DataProcessor.МножественныйВыбор: Use View',
'DataProcessor.БезшапочнаяТаблица: Use View',
'DataProcessor.МногострочнаяШапка: Use View',
'DataProcessor.ПроверкаДоступности: Use View',
],
},
@@ -0,0 +1,86 @@
export const name = 'multirow-header: резолвинг колонок на двухэтажной шапке (чтение/клик/заполнение)';
export const tags = ['table', 'columns'];
export const timeout = 120000;
// Стенд «Многострочная шапка» воспроизводит два паттерна, снятых живьём с ERP:
//
// 1. паттерн «Задачи» — широкая колонка «Исполнитель» (x 705…1206) над парой узких
// «Срок» (705…956) и «Выполнена» (956…1206). У КАЖДОЙ ячейки есть шапка со своим
// colindex, поэтому верный ответ однозначен. Матчинг по центру x его не находит:
// центр широкой ячейки падает в диапазон одной из узких шапок.
//
// 2. паттерн «Операция» — шапка только у группы «Субконто», а ячеек под ней три и своих
// шапок у них НЕТ. Здесь разворот в «Субконто 1/2/3» — правильное поведение, его
// обязана сохранить любая правка.
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, fillTableRow, assert, step, log }) {
await step('setup: открыть обработку', async () => {
await navigateSection('Склад');
await openCommand('Многострочная шапка');
});
await step('read: значения лежат в своих колонках, фантомных «Исполнитель N» нет', async () => {
const t = await readTable();
log(`columns=${JSON.stringify(t.columns)}`);
log(`row0=${JSON.stringify(t.rows[0])}`);
// Своя шапка у каждой ячейки → колонка называется ровно как в шапке, без нумерации.
assert.includes(t.columns, 'Исполнитель', 'колонка «Исполнитель» есть');
assert.includes(t.columns, 'Срок', 'колонка «Срок» есть');
assert.includes(t.columns, 'Выполнена', 'колонка «Выполнена» есть');
assert.ok(!t.columns.includes('Исполнитель 1'),
`фантомной «Исполнитель 1» быть не должно — шапка не объединённая (columns=${JSON.stringify(t.columns)})`);
const r = t.rows[0];
assert.equal(r['Код'], 'К1', 'Код');
assert.equal(r['Задача'], 'Задача 1', 'Задача');
assert.equal(r['Автор'], 'Автор 1', 'Автор');
assert.equal(r['Исполнитель'], 'Исполнитель 1', 'Исполнитель — своё значение, не склейка');
assert.equal(r['Срок'], 'Срок 1', 'Срок — своё значение, а не пусто');
assert.equal(r['Выполнена'], 'Выполнена 1', 'Выполнена — своё значение, а не пусто');
});
await step('read: разворот объединённой шапки сохранён (паттерн «Операция»)', async () => {
const t = await readTable();
// У «Субконто» шапка одна, ячеек три, своих шапок у них нет → нумерация обязана остаться.
assert.includes(t.columns, 'Субконто 1', 'колонка «Субконто 1»');
assert.includes(t.columns, 'Субконто 2', 'колонка «Субконто 2»');
assert.includes(t.columns, 'Субконто 3', 'колонка «Субконто 3»');
const r = t.rows[0];
assert.equal(r['Субконто 1'], 'Субконто1 1', 'Субконто 1');
assert.equal(r['Субконто 2'], 'Субконто2 1', 'Субконто 2');
assert.equal(r['Субконто 3'], 'Субконто3 1', 'Субконто 3');
});
await step('click: clickElement({row, column}) попадает в нужную колонку', async () => {
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Срок' });
log(`clicked: ${JSON.stringify(res.clicked)}`);
assert.equal(res.clicked?.kind, 'gridCell', 'kind=gridCell');
assert.equal(res.clicked?.column, 'Срок', 'column=Срок');
});
await step('click: имя колонки из readTable пригодно для клика (единство именования)', async () => {
const t = await readTable();
assert.includes(t.columns, 'Субконто 2', 'имя развёрнутой колонки из readTable');
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Субконто 2' });
log(`clicked: ${JSON.stringify(res.clicked)}`);
assert.equal(res.clicked?.kind, 'gridCell', 'по имени из readTable клик находит ячейку');
});
await step('fill: fillTableRow пишет значение в нужную колонку', async () => {
// Путь записи ищет ячейку по colindex (grid-edit.mjs), поэтому ожидается зелёным
// и до правки резолверов чтения/клика — это замер, а не регресс.
await fillTableRow({ 'Срок': 'Срок изменён' }, { row: { 'Код': 'К3' } });
const t = await readTable();
const row = t.rows.find(r => r['Код'] === 'К3');
log(`after fill: ${JSON.stringify(row)}`);
assert.equal(row['Срок'], 'Срок изменён', 'значение попало в «Срок»');
assert.equal(row['Исполнитель'], 'Исполнитель 3', 'соседняя широкая колонка не задета');
assert.equal(row['Выполнена'], 'Выполнена 3', 'соседняя узкая колонка не задета');
});
await step('cleanup: закрыть форму', async () => {
await closeForm();
});
}