fix(web-test): clickElement expand:false — idempotent expand/collapse

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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-26 18:27:40 +03:00
parent 2d88cdc864
commit baefeaa05b
+36 -21
View File
@@ -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)