mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-19 09:09:41 +03:00
fix(web-test): reliable arrow-key scroll for off-screen spreadsheet cells
Rewrites scrollSpreadsheetToCell with fixes for multiple issues discovered during E2E testing: - Use Playwright boundingBox (page-level coords) instead of frame-internal getBoundingClientRect for visibility checks — frame's clientWidth is wider than the actual visible iframe area clipped by parent elements - Use iframe element's boundingBox to determine visible region — cells behind the section panel (x < iframeBox.x) were incorrectly considered "visible" and focus clicks hit the section panel instead of the spreadsheet - Use div[y]+div[x] attribute selectors instead of div.RxCy CSS classes — the RxCy class numbering differs from y/x attribute values - Accept cellLoc parameter from caller instead of re-searching — avoids selector mismatch and handles cells missing from some rows - Native click through mxlCurrBody overlay (page.mouse.click) for focus — frame.locator().click() bypasses overlay causing header/data desync, page.mouse.click() + frameEl.focus() doesn't transfer keyboard focus - Pick rightmost/leftmost fully-visible cell for focus based on scroll direction — each arrow press immediately triggers platform scroll Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1126,57 +1126,89 @@ function buildSpreadsheetMapping(allCells) {
|
|||||||
/**
|
/**
|
||||||
* Scroll SpreadsheetDocument to make a cell visible using arrow keys.
|
* Scroll SpreadsheetDocument to make a cell visible using arrow keys.
|
||||||
* Uses native platform scroll — keeps headers, data, and scrollbar synchronized.
|
* Uses native platform scroll — keeps headers, data, and scrollbar synchronized.
|
||||||
* Clicks a visible cell for focus, then ArrowRight/ArrowLeft to bring target into view.
|
*
|
||||||
|
* How it works:
|
||||||
|
* 1. Check target cell visibility via Playwright boundingBox (page-level coords).
|
||||||
|
* 2. Click a fully-visible cell via page.mouse.click through the mxlCurrBody overlay.
|
||||||
|
* This is the same native click that clickSpreadsheetCell uses — it gives keyboard
|
||||||
|
* focus to the spreadsheet and keeps headers/data/scrollbar in sync.
|
||||||
|
* (frame.locator().click() bypasses overlay → desyncs frozen headers;
|
||||||
|
* page.mouse.click() + frameEl.focus() doesn't transfer keyboard focus.)
|
||||||
|
* 3. Press ArrowRight/ArrowLeft until the target cell is fully within the viewport.
|
||||||
|
*
|
||||||
|
* @param {Frame} frame - Playwright Frame containing the spreadsheet cells
|
||||||
|
* @param {number} physRow - physical row (y attribute) in the frame
|
||||||
|
* @param {number} physCol - physical column (x attribute) in the frame
|
||||||
|
* @param {Locator} cellLoc - Playwright locator for the target cell (from caller)
|
||||||
*/
|
*/
|
||||||
async function scrollSpreadsheetToCell(frame, physRow, physCol) {
|
async function scrollSpreadsheetToCell(frame, physRow, physCol, cellLoc) {
|
||||||
const getRect = async () => frame.evaluate(`(() => {
|
const pageVw = await page.evaluate('window.innerWidth');
|
||||||
const el = document.querySelector('div.R${physRow}C${physCol}');
|
// Get iframe bounds — the actual visible region on page.
|
||||||
if (!el) return null;
|
// The iframe may extend behind the section panel on the left, so cells with
|
||||||
const r = el.getBoundingClientRect();
|
// x >= 0 but x < iframeBox.x are behind the panel. Clicking them hits the panel.
|
||||||
const vw = document.documentElement.clientWidth || document.body.clientWidth;
|
const frameElm = await frame.frameElement();
|
||||||
return { left: r.left, right: r.right, vw };
|
const frameBox = await frameElm.boundingBox();
|
||||||
})()`);
|
const visLeft = frameBox ? frameBox.x : 0;
|
||||||
|
const visRight = frameBox ? Math.min(frameBox.x + frameBox.width, pageVw) : pageVw;
|
||||||
|
|
||||||
let rect = await getRect();
|
const getBox = async () => {
|
||||||
if (!rect) return;
|
try { return await cellLoc.boundingBox({ timeout: 500 }); }
|
||||||
const isFullyVisible = (r) => r.left >= 0 && r.right <= r.vw;
|
catch { return null; }
|
||||||
if (isFullyVisible(rect)) return;
|
};
|
||||||
|
const isFullyVisible = (box) => box && box.x >= visLeft && (box.x + box.width) <= visRight;
|
||||||
|
|
||||||
// Click a visible cell to establish focus.
|
let box = await getBox();
|
||||||
// For ArrowRight: click leftmost visible cell (maximum room to scroll right).
|
if (!box) return; // cell not in DOM
|
||||||
// For ArrowLeft: click leftmost visible cell too (NOT rightmost — re-clicking breaks scroll context).
|
if (isFullyVisible(box)) return;
|
||||||
const direction = rect.right > rect.vw ? 'ArrowRight' : 'ArrowLeft';
|
|
||||||
const vpWidth = await page.evaluate('window.innerWidth');
|
const direction = (box.x + box.width) > pageVw ? 'ArrowRight' : 'ArrowLeft';
|
||||||
const allCellLocs = frame.locator('div[x]');
|
|
||||||
const cellCount = await allCellLocs.count();
|
// Find a fully-visible cell to click for focus.
|
||||||
const candidates = [];
|
// Prefer cells in the target row (scrollable area), fall back to any row.
|
||||||
for (let ci = 0; ci < cellCount; ci++) {
|
const targetRowSel = `div[y="${physRow}"] div[x]`;
|
||||||
const box = await allCellLocs.nth(ci).boundingBox();
|
const anyRowSel = 'div[x]';
|
||||||
if (box && box.x >= 0 && (box.x + box.width) <= vpWidth && box.width > 5) {
|
let focusClicked = false;
|
||||||
candidates.push({ ci, box });
|
for (const sel of [targetRowSel, anyRowSel]) {
|
||||||
|
const locs = frame.locator(sel);
|
||||||
|
const count = await locs.count();
|
||||||
|
const candidates = [];
|
||||||
|
for (let ci = 0; ci < count; ci++) {
|
||||||
|
const b = await locs.nth(ci).boundingBox();
|
||||||
|
if (b && b.width > 5 && b.x >= visLeft && (b.x + b.width) <= visRight) {
|
||||||
|
candidates.push({ ci, box: b });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (candidates.length === 0) continue;
|
||||||
if (candidates.length > 0) {
|
|
||||||
candidates.sort((a, b) => a.box.x - b.box.x);
|
candidates.sort((a, b) => a.box.x - b.box.x);
|
||||||
const pick = candidates[0]; // always leftmost — safest for focus
|
// ArrowRight → rightmost fully-visible (each press scrolls right immediately)
|
||||||
|
// ArrowLeft → leftmost fully-visible (each press scrolls left immediately)
|
||||||
|
const pick = direction === 'ArrowRight'
|
||||||
|
? candidates[candidates.length - 1]
|
||||||
|
: candidates[0];
|
||||||
|
// Native click through overlay — gives keyboard focus + no header desync.
|
||||||
await page.mouse.click(pick.box.x + pick.box.width / 2, pick.box.y + pick.box.height / 2);
|
await page.mouse.click(pick.box.x + pick.box.width / 2, pick.box.y + pick.box.height / 2);
|
||||||
await page.waitForTimeout(100);
|
await page.waitForTimeout(100);
|
||||||
|
focusClicked = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
if (!focusClicked) return; // no visible cells — can't scroll
|
||||||
|
|
||||||
// Arrow keys until cell is fully visible or we're stuck at document edge.
|
// Arrow keys until cell is fully visible or we detect no progress.
|
||||||
let prevCx = (rect.left + rect.right) / 2;
|
const MAX_STALE = 5; // bail out if arrows aren't scrolling (lost focus?)
|
||||||
let scrollStarted = false;
|
let prevCx = box.x + box.width / 2;
|
||||||
|
let staleCount = 0;
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
await page.keyboard.press(direction);
|
await page.keyboard.press(direction);
|
||||||
await page.waitForTimeout(50);
|
await page.waitForTimeout(50);
|
||||||
rect = await getRect();
|
box = await getBox();
|
||||||
if (!rect) break;
|
if (!box) break;
|
||||||
if (isFullyVisible(rect)) break;
|
if (isFullyVisible(box)) break;
|
||||||
const cx = (rect.left + rect.right) / 2;
|
const cx = box.x + box.width / 2;
|
||||||
if (Math.abs(cx - prevCx) >= 1) {
|
if (Math.abs(cx - prevCx) >= 1) {
|
||||||
scrollStarted = true;
|
staleCount = 0;
|
||||||
} else if (scrollStarted) {
|
} else {
|
||||||
break; // scroll was moving, now stopped — reached document edge
|
staleCount++;
|
||||||
|
if (staleCount >= MAX_STALE) break;
|
||||||
}
|
}
|
||||||
prevCx = cx;
|
prevCx = cx;
|
||||||
}
|
}
|
||||||
@@ -1245,13 +1277,13 @@ async function clickSpreadsheetCell(target, { dblclick: dbl, modifier } = {}) {
|
|||||||
|
|
||||||
// Get bounding box and click via page.mouse (bypasses mxlCurrBody overlay)
|
// Get bounding box and click via page.mouse (bypasses mxlCurrBody overlay)
|
||||||
const frame = page.frames()[frameIndex];
|
const frame = page.frames()[frameIndex];
|
||||||
const cellDiv = frame.locator(`div.R${physRow}C${physCol}`).first();
|
// Use [y]+[x] attributes — CSS class RxCy uses different numbering than y/x attrs.
|
||||||
|
const cellDiv = frame.locator(`div[y="${physRow}"] div[x="${physCol}"]`).first();
|
||||||
// Scroll cell into view using arrow keys — the only reliable way to scroll
|
// Scroll cell into view using arrow keys — the only reliable way to scroll
|
||||||
// 1C SpreadsheetDocument without desynchronizing headers, data, and scrollbar.
|
// 1C SpreadsheetDocument without desynchronizing headers, data, and scrollbar.
|
||||||
// First click a visible cell to focus the spreadsheet, then arrow-key to target.
|
await scrollSpreadsheetToCell(frame, physRow, physCol, cellDiv);
|
||||||
await scrollSpreadsheetToCell(frame, physRow, physCol);
|
|
||||||
const box = await cellDiv.boundingBox();
|
const box = await cellDiv.boundingBox();
|
||||||
if (!box) throw new Error(`clickElement: cell R${physRow}C${physCol} not visible (no bounding box).`);
|
if (!box) throw new Error(`clickElement: cell y=${physRow} x=${physCol} not visible (no bounding box).`);
|
||||||
|
|
||||||
const x = box.x + box.width / 2;
|
const x = box.x + box.width / 2;
|
||||||
const y = box.y + box.height / 2;
|
const y = box.y + box.height / 2;
|
||||||
@@ -1298,8 +1330,8 @@ async function findSpreadsheetCellByText(formNum, searchText) {
|
|||||||
|
|
||||||
const frame = page.frames()[frameIndex];
|
const frame = page.frames()[frameIndex];
|
||||||
// Scroll cell into view using native arrow-key mechanism
|
// Scroll cell into view using native arrow-key mechanism
|
||||||
await scrollSpreadsheetToCell(frame, found.cell.r, found.cell.c);
|
const cellDiv = frame.locator(`div[y="${found.cell.r}"] div[x="${found.cell.c}"]`).first();
|
||||||
const cellDiv = frame.locator(`div.R${found.cell.r}C${found.cell.c}`).first();
|
await scrollSpreadsheetToCell(frame, found.cell.r, found.cell.c, cellDiv);
|
||||||
const box = await cellDiv.boundingBox();
|
const box = await cellDiv.boundingBox();
|
||||||
if (!box) return null;
|
if (!box) return null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user