Files
cc-1c-skills/tests/web-test/24-multirow-header.test.mjs
T
Nick ShirokovandClaude Opus 5 006e64405c fix(web-test): readTable разбирает сгруппированную шапку по «Группа / Колонка»
1С кладёт заголовок группы колонок и её листья в ОДНУ строку шапки, различая их
только высотой и шириной. Колонка заводилась на каждый бокс с текстом, а строка
ключуется по имени — поэтому одноимённые листья соседних групп («План» под «Цена»
и под «Количество») схлопывались в один ключ и склеивались через " / ", а
заголовки групп становились пустыми колонками.

Заголовок группы отличается от настоящей широкой колонки над узкими (паттерн
«Исполнитель» над «Срок»/«Выполнена») одним надёжным признаком: его colindex не
встречается ни в одной ячейке тела. По нему листья переименовываются в
«Группа / Лист» — той же конвенцией, что уже применяет читалка табличного
документа, — а заголовок из колонок убирается. Разворот объединённой шапки
(«Субконто 1/2/3») не задет: под ним боксов-листьев в шапке нет.

Заодно у пути записи заголовок ячейки резолвился x-сканом шапки и под группой
отдавал «Цена» всем трём листьям — fillTableRow по имени из readTable отвечал
notFilled. Теперь имя берётся из общей модели, как в чтении и клике.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 10:12:56 +03:00

201 lines
15 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 = 180000;
// Стенд «Многострочная шапка» воспроизводит два паттерна, снятых живьём с ERP:
//
// 1. паттерн «Задачи» — широкая колонка «Исполнитель» (x 705…1206) над парой узких
// «Срок» (705…956) и «Выполнена» (956…1206). У КАЖДОЙ ячейки есть шапка со своим
// colindex, поэтому верный ответ однозначен. Матчинг по центру x его не находит:
// центр широкой ячейки падает в диапазон одной из узких шапок.
//
// 2. паттерн «Операция» — шапка только у группы «Субконто», а ячеек под ней три и своих
// шапок у них НЕТ. Здесь разворот в «Субконто 1/2/3» — правильное поведение, его
// обязана сохранить любая правка.
//
// Вторая часть теста — стенд «Группы колонок»: горизонтальные ГруппыКолонок («Цена», «Количество»)
// с ОДИНАКОВЫМИ именами листьев («План»/«Факт»). Родитель здесь, в отличие от «Исполнитель»,
// собственных ячеек не имеет — его colindex отсутствует в теле. Листья именуются «Группа / Лист».
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, fillTableRow, getFormState, 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('setup: перейти на стенд «Группы колонок»', async () => {
await closeForm();
await navigateSection('Склад');
await openCommand('Группы колонок');
});
await step('groups: листья именуются «Группа / Лист», значения не склеены', async () => {
const t = await readTable();
log(`columns=${JSON.stringify(t.columns)}`);
log(`row0=${JSON.stringify(t.rows[0])}`);
['Цена / План', 'Цена / Факт', 'Цена / Откл.', 'Количество / План', 'Количество / Факт']
.forEach(c => assert.includes(t.columns, c, `колонка «${c}»`));
// Голых имён быть не должно: они одинаковы в обеих группах, и строка ключуется по имени —
// именно на этом значения «Цена»/«Количество» схлопывались в один ключ через ' / '.
assert.ok(!t.columns.includes('План'), `голой «План» быть не должно (columns=${JSON.stringify(t.columns)})`);
assert.ok(!t.columns.includes('Факт'), `голой «Факт» быть не должно (columns=${JSON.stringify(t.columns)})`);
// Заголовок группы — не колонка данных: своих ячеек у него нет.
assert.ok(!t.columns.includes('Цена'), 'заголовок группы «Цена» не колонка');
assert.ok(!t.columns.includes('Количество'), 'заголовок группы «Количество» не колонка');
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('groups: getFormState().tables[] даёт те же имена, что readTable', async () => {
// У form-state своя ветка вывода колонок — она обязана совпадать с моделью readTable,
// иначе имя из tables[] не годится для клика.
const t = await readTable();
const f = await getFormState();
log(`tables=${JSON.stringify(f.tables)}`);
assert.deepEqual(f.tables[0].columns, t.columns, 'columns из getFormState == columns из readTable');
});
await step('groups: клик по полному имени попадает в нужную группу', async () => {
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Количество / Факт' });
assert.equal(res.clicked?.kind, 'gridCell', 'kind=gridCell');
const cell = await focusedCell();
log(`focused: ${JSON.stringify(cell)}`);
assert.equal(cell?.text, 'К-факт 2', `клик попал в «Количество / Факт» (got «${cell?.text}»)`);
});
await step('groups: короткое имя резолвится через суффикс «Группа / Имя»', async () => {
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Откл.' });
assert.equal(res.clicked?.kind, 'gridCell', 'kind=gridCell');
const cell = await focusedCell();
log(`focused: ${JSON.stringify(cell)}`);
assert.equal(cell?.text, 'Ц-откл 2', `«Откл.» нашлось как «Цена / Откл.» (got «${cell?.text}»)`);
});
await step('groups: fillTableRow пишет по имени из readTable', async () => {
// Путь записи матчит ячейку по её техническому имени и по заголовку. Заголовок листа под
// группой резолвился x-скáном и давал «Цена» для всех трёх листьев — имя из readTable не
// находилось вовсе (notFilled).
const r = await fillTableRow({ 'Цена / Факт': 'изменён' }, { row: { 'Код': 'К3' } });
log(`filled=${JSON.stringify(r.filled)} notFilled=${JSON.stringify(r.notFilled)}`);
assert.ok(!r.notFilled, `все поля найдены (notFilled=${JSON.stringify(r.notFilled)})`);
const row = (await readTable()).rows.find(x => x['Код'] === 'К3');
log(`after fill: ${JSON.stringify(row)}`);
assert.equal(row['Цена / Факт'], 'изменён', 'значение попало в «Цена / Факт»');
assert.equal(row['Цена / План'], 'Ц-план 3', 'сосед по группе не задет');
assert.equal(row['Количество / Факт'], 'К-факт 3', 'одноимённый лист соседней группы не задет');
});
await step('cleanup: закрыть форму', async () => {
await closeForm();
});
}