From baefeaa05bd5f2d3937b4ad2568bd5c5a18b9c99 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 26 Mar 2026 18:27:40 +0300 Subject: [PATCH] =?UTF-8?q?fix(web-test):=20clickElement=20expand:false=20?= =?UTF-8?q?=E2=80=94=20idempotent=20expand/collapse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit expand:false was silently ignored because `if (expand || toggle)` evaluates to false when expand=false. Now uses `expand != null` to enter the branch, checks current state (gridListH/gridListV for groups, backgroundImage gx=0 for tree nodes), and only clicks when the state needs to change. - expand:true on collapsed → expand (click) - expand:true on expanded → noop (idempotent) - expand:false on expanded → collapse (click) - expand:false on collapsed → noop (idempotent) - toggle → always click (unchanged) Returns `toggled: true/false` in result to indicate whether click happened. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/scripts/browser.mjs | 57 +++++++++++++-------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 5748b259..60a96182 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -1956,9 +1956,10 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = { // Grid row targets — use coordinate click (single or double) if (target.kind === 'gridGroup' || target.kind === 'gridParent') { - if (expand || toggle) { + if (expand != null || toggle) { // Expand/collapse group in hierarchy mode — click the triangle icon (.gridListH/.gridListV) - const levelIconCoords = await page.evaluate(`(() => { + // expand=true: only expand (skip if already expanded), expand=false: only collapse, toggle: always click + const levelIconInfo = await page.evaluate(`(() => { const p = ${JSON.stringify(`form${formNum}_`)}; const gridSel = ${JSON.stringify(target.gridId ? '#' + target.gridId : null)}; const grid = gridSel ? document.querySelector(gridSel) : document.querySelector('[id^="' + p + '"].grid'); @@ -1972,22 +1973,28 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = { const icon = line.querySelector('.gridListH, .gridListV'); if (icon) { const r = icon.getBoundingClientRect(); - return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; + const isExpanded = !!icon.classList.contains('gridListH'); + return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), isExpanded }; } } } return null; })()`); - if (levelIconCoords) { - await page.mouse.click(levelIconCoords.x, levelIconCoords.y); - } else { - // Fallback: dblclick (standard hierarchy navigation) - await page.mouse.dblclick(target.x, target.y); + const shouldClick = toggle || !levelIconInfo + || (expand === true && !levelIconInfo.isExpanded) + || (expand === false && levelIconInfo.isExpanded); + if (shouldClick) { + if (levelIconInfo) { + await page.mouse.click(levelIconInfo.x, levelIconInfo.y); + } else { + // Fallback: dblclick (standard hierarchy navigation) + await page.mouse.dblclick(target.x, target.y); + } } await waitForStable(formNum); const state = await getFormState(); - state.clicked = { kind: target.kind, name: target.name, toggled: true }; - state.hint = 'Group toggled. Use readTable to see updated list.'; + state.clicked = { kind: target.kind, name: target.name, toggled: shouldClick }; + state.hint = shouldClick ? 'Group toggled. Use readTable to see updated list.' : 'Group already in desired state.'; return state; } // Default: dblclick to enter group / go up to parent @@ -1998,9 +2005,10 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = { return state; } if (target.kind === 'gridTreeNode') { - if (expand || toggle) { - // Toggle: click the tree expand/collapse icon [tree="true"] - const treeIconCoords = await page.evaluate(`(() => { + if (expand != null || toggle) { + // Expand/collapse tree node — click the tree icon [tree="true"] + // expand=true: only expand (skip if already expanded), expand=false: only collapse, toggle: always click + const treeIconInfo = await page.evaluate(`(() => { const p = ${JSON.stringify(`form${formNum}_`)}; const gridSel = ${JSON.stringify(target.gridId ? '#' + target.gridId : null)}; const grid = gridSel ? document.querySelector(gridSel) : document.querySelector('[id^="' + p + '"].grid'); @@ -2014,22 +2022,29 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = { const treeIcon = line.querySelector('.gridBoxImg [tree="true"]'); if (treeIcon) { const r = treeIcon.getBoundingClientRect(); - return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; + const bg = treeIcon.style.backgroundImage || ''; + const isExpanded = bg.includes('gx=0'); + return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), isExpanded }; } } } return null; })()`); - if (treeIconCoords) { - await page.mouse.click(treeIconCoords.x, treeIconCoords.y); - } else { - // Fallback: dblclick on row (works for trees without clickable +/- icons) - await page.mouse.dblclick(target.x, target.y); + const shouldClick = toggle || !treeIconInfo + || (expand === true && !treeIconInfo.isExpanded) + || (expand === false && treeIconInfo.isExpanded); + if (shouldClick) { + if (treeIconInfo) { + await page.mouse.click(treeIconInfo.x, treeIconInfo.y); + } else { + // Fallback: dblclick on row (works for trees without clickable +/- icons) + await page.mouse.dblclick(target.x, target.y); + } } await waitForStable(formNum); const state = await getFormState(); - state.clicked = { kind: 'gridTreeNode', name: target.name, toggled: true }; - state.hint = 'Tree node toggled. Use readTable to see updated tree.'; + state.clicked = { kind: 'gridTreeNode', name: target.name, toggled: shouldClick }; + state.hint = shouldClick ? 'Tree node toggled. Use readTable to see updated tree.' : 'Tree node already in desired state.'; return state; } // Default: select row (click text, no expand/collapse)