diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 2b438b35..f45bf946 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -1619,31 +1619,32 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = { return state; } - // Build selector: tabs without ID use [data-content], others use [id] - const selector = (target.kind === 'tab' && !target.id) - ? `[data-content="${target.name}"]` - : `[id="${target.id}"]`; - - // Use Playwright click for proper mousedown/mouseup events - try { - await page.click(selector, { timeout: 5000 }); - } catch (clickErr) { - if (clickErr.message.includes('intercepts pointer events')) { - // Surface overlay intercepts — try force click first (no side effects), - // then Escape + retry as fallback (Escape can trigger save dialogs on forms) - try { - await page.click(selector, { force: true, timeout: 5000 }); - } catch (clickErr2) { - if (clickErr2.message.includes('intercepts pointer events')) { - await page.keyboard.press('Escape'); - await page.waitForTimeout(500); - await page.click(selector, { timeout: 5000 }); - } else { - throw clickErr2; + // Tabs without ID — use coordinate click to avoid global [data-content] ambiguity + if (target.kind === 'tab' && !target.id && target.x && target.y) { + await page.mouse.click(target.x, target.y); + } else { + const selector = `[id="${target.id}"]`; + // Use Playwright click for proper mousedown/mouseup events + try { + await page.click(selector, { timeout: 5000 }); + } catch (clickErr) { + if (clickErr.message.includes('intercepts pointer events')) { + // Surface overlay intercepts — try force click first (no side effects), + // then Escape + retry as fallback (Escape can trigger save dialogs on forms) + try { + await page.click(selector, { force: true, timeout: 5000 }); + } catch (clickErr2) { + if (clickErr2.message.includes('intercepts pointer events')) { + await page.keyboard.press('Escape'); + await page.waitForTimeout(500); + await page.click(selector, { timeout: 5000 }); + } else { + throw clickErr2; + } } + } else { + throw clickErr; } - } else { - throw clickErr; } } @@ -2526,10 +2527,29 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { if (!f) return { tag: 'none' }; if (f.tagName === 'INPUT' || f.tagName === 'TEXTAREA') { const inGrid = (() => { let n = f; while (n) { if (n.classList?.contains('grid') || n.classList?.contains('gridContent')) return true; n = n.parentElement; } return false; })(); - if (inGrid) return { - tag: 'INPUT', id: f.id, - fullName: f.id.replace(/^form\\d+_/, '').replace(/_i\\d+$/, '') - }; + if (inGrid) { + let headerText = ''; + let grid = f; while (grid && !grid.classList?.contains('grid')) grid = grid.parentElement; + if (grid) { + const fr = f.getBoundingClientRect(); + const head = grid.querySelector('.gridHead'); + const hl = head?.querySelector('.gridLine') || head; + if (hl) for (const h of hl.children) { + if (h.offsetWidth === 0) continue; + const hr = h.getBoundingClientRect(); + if (fr.x >= hr.x && fr.x < hr.x + hr.width) { + const t = h.querySelector('.gridBoxText'); + headerText = (t || h).innerText?.trim() || ''; + break; + } + } + } + return { + tag: 'INPUT', id: f.id, + fullName: f.id.replace(/^form\\d+_/, '').replace(/_i\\d+$/, ''), + headerText + }; + } } return { tag: f.tagName || 'none' }; })()`); @@ -2578,6 +2598,19 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } } + // Fallback: match by column header text (handles metadata typos in cell id) + if (!matchedKey && cell.headerText) { + const htLower = cell.headerText.toLowerCase(); + for (const [key, info] of pending) { + if (info.filled) continue; + const kl = key.toLowerCase(); + if (htLower === kl || htLower.endsWith(kl) || htLower.includes(kl)) { + matchedKey = key; + break; + } + } + } + if (!matchedKey) { // Skip this cell await page.keyboard.press('Tab'); @@ -2739,7 +2772,9 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { if (eddItems && eddItems.length > 0) { // Reference field with autocomplete — click best match - const realItems = eddItems.filter(i => !i.startsWith('Создать')); + // Filter out reference field "create" actions (Создать элемент, Создать группу, Создать: ...) + // but keep standalone enum values like "Создать" (no space/colon after) + const realItems = eddItems.filter(i => !/^Создать[\s:]/.test(i)); if (realItems.length > 0) { const tgt = normYo(text.toLowerCase()); diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index d352a55e..12bdd0f8 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -686,7 +686,9 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector } } return false; }).forEach(el => { - items.push({ id: el.id, name: el.dataset.content, label: '', kind: 'tab' }); + const r = el.getBoundingClientRect(); + items.push({ id: el.id, name: el.dataset.content, label: '', kind: 'tab', + x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); }); // Navigation panel items (FormNavigationPanel) — in parent page{N} @@ -724,7 +726,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector } if (!cf) cf = containerItems.find(i => i.label && i.label.toLowerCase() === target); if (!cf && target.length >= 4) cf = containerItems.find(i => i.name.toLowerCase().includes(target)); if (!cf && target.length >= 4) cf = containerItems.find(i => i.label && i.label.toLowerCase().includes(target)); - if (cf) return { id: cf.id, kind: cf.kind, name: cf.name }; + if (cf) { const res = { id: cf.id, kind: cf.kind, name: cf.name }; if (cf.x != null) { res.x = cf.x; res.y = cf.y; } return res; } // Fallback: filter by gridName id-prefix (e.g. ИсходящиеКоманднаяПанель_Добавить) const gridName = gridEl.id ? gridEl.id.replace(p, '') : ''; if (gridName) { @@ -732,7 +734,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector } let pf = prefixItems.find(i => i.name.toLowerCase() === target); if (!pf && target.length >= 4) pf = prefixItems.find(i => i.label && i.label.toLowerCase().includes(target)); if (!pf && target.length >= 4) pf = prefixItems.find(i => i.name.toLowerCase().includes(target)); - if (pf) return { id: pf.id, kind: pf.kind, name: pf.name }; + if (pf) { const res = { id: pf.id, kind: pf.kind, name: pf.name }; if (pf.x != null) { res.x = pf.x; res.y = pf.y; } return res; } } } // Fall through to unscoped search @@ -749,7 +751,9 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector } if (!found && target.length >= 4) found = items.find(i => i.label && i.label.toLowerCase().includes(target)); if (found) { - return { id: found.id, kind: found.kind, name: found.name }; + const res = { id: found.id, kind: found.kind, name: found.name }; + if (found.x != null) { res.x = found.x; res.y = found.y; } + return res; } // Grid rows — fallback: search in table rows (for hierarchical/tree navigation)