mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-19 17:19:42 +03:00
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>
172 lines
7.5 KiB
JavaScript
172 lines
7.5 KiB
JavaScript
// web-test table/click-cell v1.1 — click a cell in a form grid by (row, column).
|
||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
//
|
||
// Routed from core/click.mjs when the user calls clickElement({row, column}) and
|
||
// the form has no SpreadsheetDocument (or `table` matches a grid).
|
||
//
|
||
// Key behaviors:
|
||
// - `row` can be a number (index in current DOM window) or `{col: value}` filter.
|
||
// - `scroll: true | number` enables reveal-loop via PageDown when a filter row
|
||
// isn't visible. End detected by snapshot stability between PageDowns.
|
||
// - Horizontal scroll mirrors SpreadsheetDocument: focus a visible cell in the
|
||
// target row, press ArrowRight/Left until the target column is in viewport.
|
||
//
|
||
// 1С virtualization quirks worth knowing:
|
||
// - DOM holds a window of ~N visible rows. PageDown's first press moves the
|
||
// cursor inside the window; subsequent presses swap the window contents.
|
||
// - scrollTop/scrollLeft are always 0; absolute X of cells shifts on horizontal
|
||
// scroll. So scroll progress must be inferred from cell coordinates / snapshot
|
||
// diffs, never from scrollTop/Height.
|
||
// - Frozen columns (.gridBoxFix) stay pinned at the left, overlap with scrolled
|
||
// cells — DOM scripts handle the partition; engine just consumes their results.
|
||
|
||
import { page } from '../core/state.mjs';
|
||
import { waitForStable } from '../core/wait.mjs';
|
||
import { modifierClick, returnFormState } from '../core/helpers.mjs';
|
||
import { scrollHorizontallyByKey } from '../core/scroll-horiz.mjs';
|
||
import {
|
||
findGridCellScript, findFocusCellScript, snapshotGridScript,
|
||
} from '../../dom.mjs';
|
||
|
||
const REVEAL_DEFAULT_LIMIT = 50;
|
||
const PD_WAIT_MS = 300;
|
||
const FOCUS_WAIT_MS = 150;
|
||
|
||
/**
|
||
* Click a cell in a form grid by (row, column). Called from core/click.mjs.
|
||
*
|
||
* @param {object} target - { row: number|{col:value}, column: string }
|
||
* @param {object} ctx
|
||
* @param {number} ctx.formNum
|
||
* @param {string} ctx.gridSelector - CSS selector for the target grid
|
||
* @param {string} [ctx.gridName] - for diagnostics
|
||
* @param {string} [ctx.modifier] - 'ctrl' | 'shift' for multi-select
|
||
* @param {boolean} [ctx.dblclick]
|
||
* @param {boolean|number} [ctx.scroll] - true = up to 50 PageDowns, number = exact limit
|
||
*/
|
||
export async function clickGridCell(target, ctx) {
|
||
const { formNum, gridSelector, gridName, modifier, dblclick, scroll } = ctx;
|
||
|
||
// 1. Try to find the cell in current DOM window.
|
||
let cell = await page.evaluate(findGridCellScript(formNum, gridSelector, target));
|
||
|
||
// 2. Reveal loop: only for filter-based row search with scroll opt-in.
|
||
if (cell?.error === 'row_not_found' && scroll && target.row && typeof target.row === 'object') {
|
||
cell = await revealAndFindCell({ formNum, gridSelector, target, scroll });
|
||
}
|
||
|
||
if (cell?.error) throw cellError(cell, target, gridName, scroll);
|
||
|
||
// 3. Horizontal scroll if cell is off-viewport.
|
||
if (!cell.visible) {
|
||
await scrollGridToCell({ formNum, gridSelector, target, cell });
|
||
cell = await page.evaluate(findGridCellScript(formNum, gridSelector, target));
|
||
if (cell?.error) {
|
||
throw new Error(`clickElement: cell vanished after horizontal scroll: ${cell.error}`);
|
||
}
|
||
if (!cell.visible) {
|
||
// Scroll loop bailed out before reaching the target. Don't silently click
|
||
// at off-screen coordinates — that would report a false success.
|
||
const ctxMsg = gridName ? ` in table "${gridName}"` : '';
|
||
throw new Error(`clickElement: horizontal scroll could not reach column "${cell.columnText}"${ctxMsg} (cell still at x=${cell.cellX}, viewport ends at ${cell.gridRight}).`);
|
||
}
|
||
}
|
||
|
||
// 4. Click.
|
||
await modifierClick(cell.x, cell.y, modifier, { dbl: !!dblclick });
|
||
await waitForStable();
|
||
return returnFormState({
|
||
clicked: {
|
||
kind: 'gridCell',
|
||
row: target.row,
|
||
column: cell.columnText,
|
||
...(dblclick ? { dblclick: true } : {}),
|
||
...(modifier ? { modifier } : {}),
|
||
},
|
||
});
|
||
}
|
||
|
||
function cellError(cell, target, gridName, scroll) {
|
||
const ctxMsg = gridName ? ` in table "${gridName}"` : '';
|
||
if (cell.error === 'row_not_found') {
|
||
const hint = scroll
|
||
? ' (reveal-loop exhausted)'
|
||
: ' — pass { scroll: true } to scan beyond the current DOM window';
|
||
return new Error(`clickElement: row matching ${JSON.stringify(target.row)} not found${ctxMsg}${hint}.`);
|
||
}
|
||
if (cell.error === 'column_not_found' || cell.error === 'filter_column_not_found') {
|
||
return new Error(`clickElement: column "${cell.column}" not found${ctxMsg}. Available: ${(cell.available || []).join(', ')}`);
|
||
}
|
||
if (cell.error === 'row_out_of_range') {
|
||
return new Error(`clickElement: row index ${cell.row} out of range${ctxMsg} (loaded: ${cell.loaded}). Note: row index is into current DOM window, not absolute — long lists are virtualized.`);
|
||
}
|
||
return new Error(`clickElement: cannot resolve cell ${JSON.stringify(target)}${ctxMsg}: ${cell.error}`);
|
||
}
|
||
|
||
/**
|
||
* Press PageDown in a loop, scanning DOM each iteration for the target row.
|
||
* Bail when the row is found, snapshots stop changing (end of list), or limit hit.
|
||
* page.mouse.click on a safe cell first — PageDown needs keyboard focus on gridBody.
|
||
*/
|
||
async function revealAndFindCell({ formNum, gridSelector, target, scroll }) {
|
||
const limit = typeof scroll === 'number' ? scroll : REVEAL_DEFAULT_LIMIT;
|
||
|
||
const focusPt = await page.evaluate(findFocusCellScript(gridSelector));
|
||
if (!focusPt) return { error: 'no_focusable_cell' };
|
||
await page.mouse.click(focusPt.x, focusPt.y);
|
||
await page.waitForTimeout(FOCUS_WAIT_MS);
|
||
|
||
let prevSnap = await page.evaluate(snapshotGridScript(gridSelector));
|
||
for (let i = 0; i < limit; i++) {
|
||
await page.keyboard.press('PageDown');
|
||
await page.waitForTimeout(PD_WAIT_MS);
|
||
|
||
const cell = await page.evaluate(findGridCellScript(formNum, gridSelector, target));
|
||
if (!cell?.error) return cell;
|
||
|
||
const snap = await page.evaluate(snapshotGridScript(gridSelector));
|
||
const stable = snap
|
||
&& snap.firstText === prevSnap?.firstText
|
||
&& snap.lastText === prevSnap?.lastText
|
||
&& snap.selIdx === prevSnap?.selIdx
|
||
&& snap.lineCount === prevSnap?.lineCount;
|
||
if (stable) return { error: 'row_not_found', filter: target.row };
|
||
prevSnap = snap;
|
||
}
|
||
return { error: 'row_not_found', filter: target.row };
|
||
}
|
||
|
||
/**
|
||
* Scroll the grid horizontally so the target cell falls inside the viewport.
|
||
* Focuses an edge cell in the target row (rightmost-visible for ArrowRight,
|
||
* leftmost-visible for ArrowLeft) so the next arrow key immediately scrolls.
|
||
*
|
||
* Frozen columns (gridBoxFix) are excluded from focus candidates — they don't
|
||
* drive the scrollable viewport. The DOM script handles that detail.
|
||
*/
|
||
async function scrollGridToCell({ formNum, gridSelector, target, cell }) {
|
||
const direction = cell.cellX > cell.gridRight ? 'ArrowRight'
|
||
: cell.cellRight < cell.gridX ? 'ArrowLeft'
|
||
: (cell.cellRight > cell.gridRight ? 'ArrowRight' : 'ArrowLeft');
|
||
|
||
const focusPt = await page.evaluate(
|
||
findFocusCellScript(gridSelector, { rowIdx: cell.rowIdx, direction })
|
||
);
|
||
if (!focusPt) throw new Error('clickElement: no visible cell to focus for horizontal scroll');
|
||
await page.mouse.click(focusPt.x, focusPt.y);
|
||
await page.waitForTimeout(FOCUS_WAIT_MS);
|
||
|
||
await scrollHorizontallyByKey({
|
||
page,
|
||
direction,
|
||
isFullyVisible: async () => {
|
||
const c = await page.evaluate(findGridCellScript(formNum, gridSelector, target));
|
||
return !!c && !c.error && c.visible;
|
||
},
|
||
getCenterX: async () => {
|
||
const c = await page.evaluate(findGridCellScript(formNum, gridSelector, target));
|
||
return c && !c.error ? c.x : null;
|
||
},
|
||
});
|
||
}
|