Files
cc-1c-skills/tests/web-test/24-multirow-header.test.mjs
T
Nick Shirokov 7696a8b3ee fix(web-test): единая модель колонок грида — colindex первым, геометрия запасной
Резолверов колонок было пять, все независимые, и каждый ломался по-своему:
readTable (геометрия X + Y-подряды), clickElement (геометрия X, без Y),
поиск строки {кол: знач} (геометрия X, без Y и fixed-гарда), filterList
(порядковый индекс шапки), fillTableRow (colindex — единственный целый).

Механика поломки (снята живьём на списке задач ERP): шапка «Исполнитель»
широкая (x 1085..1515) и накрывает «Срок» (1085..1251) и «Выполнена»
(1251..1515). Ячейка «Исполнитель» имеет центр 1300 → приписывается к группе
«Выполнена» → та получает лишний под-ряд → срабатывает эвристика «объединённая
шапка» → фантомные «Выполнена 1/2», а значения соседей склеиваются через ' / '.

Теперь COLUMN_MODEL_FN (dom/_shared.mjs) — единственный источник правды:
buildColumnModel / columnForCell / cellForColumn / resolveColumnByName. Идентичность
колонки — colindex (собственный id колонки в 1С, есть и на шапке, и на ячейке);
геометрия работает только там, где своей шапки у ячейки нет — под-ряды
объединённой шапки («Субконто Дт» над тремя ячейками). Путь записи пришёл к этому
решению раньше (grid-edit.mjs: «reliable across merged headers») — остальные
выровнены по нему.

Следствие: имя колонки из readTable теперь годится для клика/заполнения/фильтра —
раньше readTable отдавал «Субконто Дт 2», а клик про такое имя не знал.

Попутно закрыт второй дефект: безымянная picture-колонка определялась по ПЕРВОЙ
строке, а picField над Boolean не рисует картинку при Ложь → колонка пропадала из
columns целиком. Модель сэмплит до 10 строк и ищет ячейку по colindex.

Проверено:
- 24-multirow-header (стенд, оба паттерна ERP) — зелёный; до правки красный;
- клик проверяется по факту (DOM select+focus), а не по эху clicked.column:
  до правки клик по «Срок» молча жал «Исполнитель 2» и рапортовал успех;
- живьём на ERP: список задач — 9 честных колонок вместо фантомов, значения на
  местах; форма операции (шапка 2 этажа, строка 3 под-ряда) — «Субконто Дт/Кт 1..3»
  сохранены, ничего не поехало;
- полный регресс 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:14:27 +03:00

128 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, getPage, assert, step, log }) {
// Куда клик попал НА САМОМ ДЕЛЕ. Результат clickElement возвращает запрошенное имя колонки
// (эхо), а не разрешённую ячейку, — по нему промах неотличим от попадания. Нажатая ячейка
// помечается в DOM классами select+focus.
const focusedCell = async () => {
const page = await getPage();
return page.evaluate(() => {
const grid = [...document.querySelectorAll('.grid')].find(g => g.offsetWidth > 0 && g.offsetHeight > 0);
const box = [...grid.querySelectorAll('.gridBody [colindex]')]
.find(b => b.classList.contains('select') && b.classList.contains('focus'));
return box ? { ci: box.getAttribute('colindex'), text: (box.innerText || '').trim() } : null;
});
};
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: 'Срок' });
assert.equal(res.clicked?.kind, 'gridCell', 'kind=gridCell');
// Проверяем факт, а не эхо: до правки клик молча уходил в «Исполнитель 2» и рапортовал успех.
const cell = await focusedCell();
log(`focused: ${JSON.stringify(cell)}`);
assert.ok(cell, 'нажатая ячейка помечена в DOM');
assert.equal(cell.text, 'Срок 2', `клик попал в ячейку «Срок» строки К2, а не в соседнюю (got «${cell.text}»)`);
});
await step('click: имя колонки из readTable пригодно для клика (единство именования)', async () => {
const t = await readTable();
assert.includes(t.columns, 'Субконто 2', 'имя развёрнутой колонки из readTable');
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Субконто 2' });
assert.equal(res.clicked?.kind, 'gridCell', 'по имени из readTable клик находит ячейку');
const cell = await focusedCell();
log(`focused: ${JSON.stringify(cell)}`);
assert.equal(cell?.text, 'Субконто2 2', `клик попал во второй под-ряд «Субконто» (got «${cell?.text}»)`);
});
await step('row-filter: строка ищется по «украденной» колонке', async () => {
// Фильтр строки идёт через свой резолвер (findRowInGridScript) — тоже геометрия по x,
// причём без Y-логики. Ищем строку по «Срок», чью ячейку ворует широкий «Исполнитель».
const res = await clickElement({ row: { 'Срок': 'Срок 2' }, column: 'Код' });
assert.equal(res.clicked?.kind, 'gridCell', 'строка найдена по значению узкой колонки');
const cell = await focusedCell();
log(`focused: ${JSON.stringify(cell)}`);
assert.equal(cell?.text, 'К2', `найдена именно строка К2 (got «${cell?.text}»)`);
});
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('fill: fillTableRow пишет в РАЗВЁРНУТУЮ колонку (шапки у ячейки нет)', async () => {
// «Субконто 2» — под-ряд объединённой шапки: своей шапки у ячейки нет, у группы ci=6,
// а ячейки идут ci=7/8/9. Проверяем, что запись попадает во второй под-ряд, а не в
// первый и не в соседа.
await fillTableRow({ 'Субконто 2': 'СК2 изменён' }, { row: { 'Код': 'К1' } });
const t = await readTable();
const row = t.rows.find(r => r['Код'] === 'К1');
log(`after fill: ${JSON.stringify(row)}`);
assert.equal(row['Субконто 2'], 'СК2 изменён', 'значение попало в «Субконто 2»');
assert.equal(row['Субконто 1'], 'Субконто1 1', '«Субконто 1» не задета');
assert.equal(row['Субконто 3'], 'Субконто3 1', '«Субконто 3» не задета');
});
await step('cleanup: закрыть форму', async () => {
await closeForm();
});
}