From 96b38d9f913d303e9f344cab98b711d00c0ade71 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 17 Jul 2026 10:35:03 +0300 Subject: [PATCH] =?UTF-8?q?test(web-test):=20=D1=81=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=20=D1=81=20=D0=B4=D0=B2=D1=83=D1=85=D1=8D=D1=82=D0=B0?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D0=B9=20=D1=88=D0=B0=D0=BF=D0=BA=D0=BE=D0=B9?= =?UTF-8?q?=20+=20=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D1=8B=D0=B9=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB=D0=B2=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=D0=B0=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обработка МногострочнаяШапка воспроизводит два паттерна, снятых живьём с 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) --- .../integration/build-webtest-config.test.mjs | 98 +++++++++++++++++++ tests/web-test/24-multirow-header.test.mjs | 86 ++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 tests/web-test/24-multirow-header.test.mjs diff --git a/tests/skills/integration/build-webtest-config.test.mjs b/tests/skills/integration/build-webtest-config.test.mjs index 0a3d6233..c86fb6a2 100644 --- a/tests/skills/integration/build-webtest-config.test.mjs +++ b/tests/skills/integration/build-webtest-config.test.mjs @@ -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', ], }, diff --git a/tests/web-test/24-multirow-header.test.mjs b/tests/web-test/24-multirow-header.test.mjs new file mode 100644 index 00000000..52ab1945 --- /dev/null +++ b/tests/web-test/24-multirow-header.test.mjs @@ -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(); + }); +}