feat(web-test): поддержка гридов без шапки (headerless grids)

Единый источник деривации колонок для безголовых гридов (без .gridHead):
synthHeaderlessColumns (HEADERLESS_GRID_FN в _shared.mjs) — привязка по
colindex, имена Колонка{N}/(checkbox)/(picture), расщепление комбинированной
марк-ячейки (галка+текст в одной .gridBox) на (checkbox)+Колонка1 через subTarget.

Ветки head ? существующее : synth добавлены в:
- readTableScript, findGridCellScript, scanGridRowsScript,
  findGridHeadCenterCoordsScript (grid.mjs)
- findCellCoordsByFieldsScript, findNextCellCoordsByKeyScript,
  sortFieldKeysByColindexScript, readActiveGridCellScript (grid-edit.mjs)
- сводка таблиц в READ_FORM_FN (_shared.mjs)

Гейт — неизменная истинность head, headed-путь нетронут. Регресс 23/23 зелёный.
Новый tests/web-test/21-headerless.test.mjs. Live на БезшапочнаяТаблица +
МножественныйВыбор: readTable/getFormState/fillTableRow(тоггл (checkbox))/clickElement.

Follow-up: правка data-ячейки на безголовом ValueTable (edit-Tab-loop row-fill).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-28 18:41:55 +03:00
parent 08ea3f0806
commit 7c75424153
4 changed files with 388 additions and 76 deletions
+92
View File
@@ -0,0 +1,92 @@
export const name = 'headerless grids: readTable / getFormState / fillTableRow / clickElement без шапки';
export const tags = ['headerless', 'table', 'smoke'];
export const timeout = 180000;
// Покрывает поддержку гридов без .gridHead (фаза 2 мультивыбора):
// - БезшапочнаяТаблица (ValueTable, отдельная колонка-чекбокс)
// - МножественныйВыбор → Через флажки (марк-список, комбинированная ячейка галка+текст)
// Деривация колонок едина (synthHeaderlessColumns): Колонка{N} + (checkbox), привязка по colindex.
// Правка data-ячейки на безголовом ValueTable — follow-up (edit-Tab-loop row-fill), здесь не тестируется.
export default async function({
navigateLink, clickElement, fillTableRow, readTable, getFormState, wait, assert, step, log, closeForm
}) {
// ── ValueTable (отдельная колонка-чекбокс) ──────────────────────────────────
await step('readTable: ValueTable без шапки → Колонка1..3 + (checkbox)', async () => {
await navigateLink('Обработка.БезшапочнаяТаблица');
await wait(1.2);
const t = await readTable({ maxRows: 10 });
log(`columns: ${JSON.stringify(t.columns)}`);
assert.deepEqual(t.columns, ['Колонка1', 'Колонка2', 'Колонка3', '(checkbox)'],
'синтетические колонки по colindex');
assert.equal(t.total, 3, '3 строки');
assert.equal(t.rows[0]['Колонка1'], 'Позиция 001', 'текст data-колонки');
assert.ok(t.rows[0]['(checkbox)'] === 'true' || t.rows[0]['(checkbox)'] === 'false',
'(checkbox) читается как true/false');
});
await step('getFormState: сводка таблицы показывает synth-колонки + rowCount', async () => {
const fs = await getFormState();
const tbl = (fs.tables || []).find(x => (x.columns || []).includes('(checkbox)'));
log(`tables: ${JSON.stringify(fs.tables)}`);
assert.ok(tbl, 'таблица с (checkbox) в сводке');
assert.deepEqual(tbl.columns, ['Колонка1', 'Колонка2', 'Колонка3', '(checkbox)'], 'колонки сводки совпадают с readTable');
assert.equal(tbl.rowCount, 3, 'rowCount');
});
await step('fillTableRow: тоггл (checkbox) по индексу строки', async () => {
const before = (await readTable({ maxRows: 5 })).rows.map(r => r['(checkbox)']);
const target = before[1] === 'true' ? 'false' : 'true'; // строка 1 — переключаем в противоположное
const r = await fillTableRow({ '(checkbox)': target }, { row: 1 });
log(`filled: ${JSON.stringify(r.filled)}`);
assert.ok(r.filled?.[0]?.ok && r.filled[0].method === 'toggle', 'method=toggle, ok');
const after = (await readTable({ maxRows: 5 })).rows.map(r => r['(checkbox)']);
assert.equal(after[1], target, `строка 1 переключена в ${target}`);
});
await step('fillTableRow: тоггл (checkbox) по фильтру {Колонка1}', async () => {
const r = await fillTableRow({ '(checkbox)': 'true' }, { row: { 'Колонка1': 'Позиция 002' } });
log(`filled: ${JSON.stringify(r.filled)}`);
assert.ok(r.filled?.[0]?.ok, 'ok через row-фильтр {Колонка1}');
const t = await readTable({ maxRows: 5 });
const row = t.rows.find(x => x['Колонка1'] === 'Позиция 002');
assert.equal(row['(checkbox)'], 'true', 'Позиция 002 отмечена');
});
await step('clickElement: клик по ячейке (checkbox) тогглит галку', async () => {
const t0 = await readTable({ maxRows: 5 });
const r0 = t0.rows.find(x => x['Колонка1'] === 'Позиция 003');
const want = r0['(checkbox)'] === 'true' ? 'false' : 'true';
await clickElement({ row: { 'Колонка1': 'Позиция 003' }, column: '(checkbox)' });
await wait(0.4);
const t1 = await readTable({ maxRows: 5 });
const r1 = t1.rows.find(x => x['Колонка1'] === 'Позиция 003');
assert.equal(r1['(checkbox)'], want, `клик переключил галку Позиция 003 в ${want}`);
});
// ── Марк-список (комбинированная ячейка галка+текст в одной .gridBox) ────────
await step('марк-список: readTable → (checkbox) + Колонка1, расщепление одной ячейки', async () => {
await navigateLink('Обработка.МножественныйВыбор');
await clickElement('Через флажки');
await wait(1.2);
const t = await readTable({ maxRows: 10 });
log(`columns: ${JSON.stringify(t.columns)} rows: ${JSON.stringify(t.rows)}`);
assert.deepEqual(t.columns, ['(checkbox)', 'Колонка1'], 'комбинированная ячейка → 2 колонки');
assert.ok(t.rows.some(r => r['Колонка1'] === 'Альфа'), 'Альфа в Колонка1');
assert.ok(t.rows.every(r => r['(checkbox)'] === 'true' || r['(checkbox)'] === 'false'), '(checkbox) булев');
});
await step('марк-список: fillTableRow отмечает по фильтру {Колонка1: Альфа}', async () => {
const r = await fillTableRow({ '(checkbox)': 'true' }, { row: { 'Колонка1': 'Альфа' } });
log(`filled: ${JSON.stringify(r.filled)}`);
assert.ok(r.filled?.[0]?.ok && r.filled[0].method === 'toggle', 'method=toggle, ok');
const t = await readTable({ maxRows: 10 });
const alpha = t.rows.find(x => x['Колонка1'] === 'Альфа');
assert.equal(alpha['(checkbox)'], 'true', 'Альфа отмечена');
});
await step('cleanup: закрыть форму ввода значений', async () => {
await closeForm({ save: false });
});
}