feat(web-test): clickElement({row,column}) для гридов формы + readTable.hasMore

clickElement({row,column}) теперь работает не только на SpreadsheetDocument,
но и на гридах формы (динамические списки, табчасти). Маршрутизация:
spreadsheet приоритет (backward-compat), без spreadsheet — первый видимый
грид; явный table='Имя' форсит конкретный грид.

Поддержка:
- row: number — индекс в текущем DOM окне (виртуализация — документировано)
- row: { Колонка: значение } — фильтр по нормализованному содержимому
- scroll: true | number — reveal-loop через PageDown пока строка не найдена
  или DOM не перестал меняться (с лимитом)
- Автоматический горизонтальный скролл к колонке за viewport
  (учитывает frozen-колонки .gridBoxFix)
- Post-scroll visibility check — throw вместо ложного success

readTable обогащён полем hasMore: { above?, below } — единственный
надёжный сигнал виртуализации. total/shown остаются как DOM-окно
(backward-compat) с честным описанием в SKILL.md.

Общий хелпер scrollHorizontallyByKey вынесен в engine/core/, переиспользуется
spreadsheet'ом и грид-click'ом. DOM-логика (findGridCellScript,
findFocusCellScript, snapshotGridScript, resolveCellTargetScript) живёт
в dom/grid.mjs — engine только оркестрирует.

Покрытие: новый 18-cell-click.test.mjs (7 шагов: spreadsheet
regression-guard, catalog dblclick, табчасть, hasMore, 2 error-paths,
cleanup). Расширен 05-table.test.mjs проверкой hasMore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-28 19:01:11 +03:00
parent dff3ced847
commit e05c0a4a61
9 changed files with 746 additions and 46 deletions
+8 -1
View File
@@ -20,10 +20,17 @@ export default async function({ navigateSection, openCommand, clickElement, fill
);
const t = await readTable({ table: 'Товары' });
log(`rows after add: ${t.rows?.length}`);
log(`rows after add: ${t.rows?.length}, hasMore: ${JSON.stringify(t.hasMore)}`);
assert.equal(t.rows?.length, 2, 'Должно быть 2 строки');
assert.equal(t.rows[0]['Номенклатура'], 'Товар 01', 'Строка 0 = Товар 01');
assert.equal(t.rows[1]['Номенклатура'], 'Товар 02', 'Строка 1 = Товар 02');
// hasMore: две строки точно помещаются в табчасть — both false
assert.ok(t.hasMore, 'hasMore должен быть в результате readTable');
assert.equal(t.hasMore.below, false, 'hasMore.below должно быть false (всё видно)');
// above либо false (видимый scrollbar), либо undefined (дин-список) — но для табчасти ждём false
if (t.hasMore.above !== undefined) {
assert.equal(t.hasMore.above, false, 'hasMore.above должно быть false (мы на первой странице)');
}
});
await step('edit: изменить количество в строке 0 через fillTableRow row:0', async () => {