// web-test table/click-cell v1.3 — 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, isInputFocusedInGrid } 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; // Guard: a 'pic:N' filter value is a readTable picture token, not real cell text. // Picture cells render an icon (no text), so they can't select a row — fail fast // with guidance instead of a confusing 'row_not_found'. if (target?.row && typeof target.row === 'object') { for (const [k, v] of Object.entries(target.row)) { if (typeof v === 'string' && /^pic:\d+$/.test(v.trim())) { throw new Error(`clickElement: "${v}" is a readTable picture value (column "${k}"), not selectable text — it can't be used as a row filter. Filter by a text column (e.g. name/number) instead.`); } } } // 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); // Click on a Number/Date cell auto-enters edit mode in 1С; PageDown there // is a no-op. Exit edit mode before driving the reveal loop. if (await isInputFocusedInGrid({ gridSelector })) { await page.keyboard.press('Escape'); await page.waitForTimeout(150); } 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); // Click on a Number/Date cell auto-enters edit mode in 1С; arrow keys there // navigate text inside the input rather than scrolling the viewport. Exit first. if (await isInputFocusedInGrid({ gridSelector })) { await page.keyboard.press('Escape'); await page.waitForTimeout(150); } 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; }, }); }