From 89efcad12520ea74d139ccaf687b85d7b24025b2 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 26 May 2026 20:57:58 +0300 Subject: [PATCH] =?UTF-8?q?refactor(web-test):=20=D0=B8=D0=B7=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D1=87=D1=91=D0=BD=20EDD-=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=20=D0=B2=20dom/edd.mjs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новое в dom/edd.mjs: - readEddScript — {visible, items:[{name,x,y}]} - isEddVisibleScript — boolean, лёгкая проверка - clickEddItemViaDispatchScript — клик по name через dispatchEvent (bypass div.surface); fuzzy с NBSP/ё/bracket-strip - clickShowAllInEddScript — "Показать все" в footer Wrappers в helpers.mjs: readEdd, isEddVisible, clickEddItemViaDispatch, clickShowAllInEdd. row-fill clickEddItem унифицирован с select-value вариантом (NBSP/ё normalization теперь работает и для табличных строк). Метрики: select-value 880 → 827 LOC (−53), row-fill 1065 → 1041 LOC (−24); evaluates row-fill 20 → 17, select-value 7 → 5. Полный регресс зелёный. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/dom.mjs | 9 +- .claude/skills/web-test/scripts/dom/edd.mjs | 108 ++++++++++++++++++ .../web-test/scripts/engine/core/helpers.mjs | 43 ++++--- .../scripts/engine/forms/select-value.mjs | 61 +--------- .../scripts/engine/table/row-fill.mjs | 34 +----- 5 files changed, 155 insertions(+), 100 deletions(-) create mode 100644 .claude/skills/web-test/scripts/dom/edd.mjs diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index 1da748c8..ea76975a 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -1,4 +1,4 @@ -// web-test dom v1.11 — facade re-exporting injectable DOM scripts from dom/ +// web-test dom v1.12 — facade re-exporting injectable DOM scripts from dom/ // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Facade: re-exports DOM selector & semantic mapping script generators. @@ -43,6 +43,13 @@ export { findOpenPopupScript, } from './dom/edit-state.mjs'; +export { + readEddScript, + isEddVisibleScript, + clickEddItemViaDispatchScript, + clickShowAllInEddScript, +} from './dom/edd.mjs'; + export { getFormStateScript } from './dom/form-state.mjs'; export { diff --git a/.claude/skills/web-test/scripts/dom/edd.mjs b/.claude/skills/web-test/scripts/dom/edd.mjs new file mode 100644 index 00000000..4d5639fe --- /dev/null +++ b/.claude/skills/web-test/scripts/dom/edd.mjs @@ -0,0 +1,108 @@ +// web-test dom/edd v1.0 — DOM scripts for the #editDropDown autocomplete popup +// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +/** + * Read the `#editDropDown` autocomplete popup. + * + * Returns `{ visible: false }` when EDD is absent/hidden, or + * `{ visible: true, items: [{ name, x, y }] }` with center coords suitable + * for `page.mouse.click(x, y)`. + * + * Note: `page.mouse.click` is often intercepted by `div.surface` overlays + * from DLB — prefer `clickEddItemViaDispatchScript` for those cases. + */ +export function readEddScript() { + return `(() => { + const edd = document.getElementById('editDropDown'); + if (!edd || edd.offsetWidth === 0) return { visible: false }; + const eddTexts = [...edd.querySelectorAll('.eddText')].filter(el => el.offsetWidth > 0); + return { + visible: true, + items: eddTexts.map(el => { + const r = el.getBoundingClientRect(); + return { name: el.innerText?.trim() || '', x: r.x + r.width / 2, y: r.y + r.height / 2 }; + }) + }; + })()`; +} + +/** + * Is the EDD popup currently visible? Returns boolean. + * Lighter than `readEddScript` when only presence matters. + */ +export function isEddVisibleScript() { + return `(() => { + const edd = document.getElementById('editDropDown'); + return !!(edd && edd.offsetWidth > 0); + })()`; +} + +/** + * Click an EDD item by name via `dispatchEvent` — bypasses `div.surface` + * overlays from DLB that intercept `page.mouse.click`. + * + * Matching is fuzzy: exact (with optional `(suffix)` strip) → includes, + * normalizes ё/е and NBSP. + * + * Returns the clicked item's innerText (trimmed), or `null` when no match. + */ +export function clickEddItemViaDispatchScript(itemName) { + return `(() => { + const edd = document.getElementById('editDropDown'); + if (!edd || edd.offsetWidth === 0) return null; + const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' '); + const target = ny(${JSON.stringify(itemName.toLowerCase())}); + const items = [...edd.querySelectorAll('.eddText')].filter(el => el.offsetWidth > 0); + function clickEl(el) { + const r = el.getBoundingClientRect(); + const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; + el.dispatchEvent(new MouseEvent('mousedown', opts)); + el.dispatchEvent(new MouseEvent('mouseup', opts)); + el.dispatchEvent(new MouseEvent('click', opts)); + return el.innerText.trim(); + } + // Pass 1: exact match (prefer over partial) + for (const el of items) { + const t = ny((el.innerText?.trim() || '').toLowerCase()); + if (t === target) return clickEl(el); + const stripped = t.replace(/\\s*\\([^)]*\\)\\s*$/, ''); + if (stripped === target) return clickEl(el); + } + // Pass 2: partial match + for (const el of items) { + const t = ny((el.innerText?.trim() || '').toLowerCase()); + if (t.includes(target) || target.includes(t.replace(/\\s*\\([^)]*\\)\\s*$/, ''))) return clickEl(el); + } + return null; + })()`; +} + +/** + * Click the "Показать все" / "Show all" link in the EDD footer via + * `dispatchEvent`. Tries `.eddBottom .hyperlink` first, then falls back + * to scanning for span/div/a with the literal text. + * + * Returns boolean — whether the link was found and clicked. + */ +export function clickShowAllInEddScript() { + return `(() => { + const edd = document.getElementById('editDropDown'); + if (!edd || edd.offsetWidth === 0) return false; + let el = edd.querySelector('.eddBottom .hyperlink'); + if (!el || el.offsetWidth === 0) { + const candidates = [...edd.querySelectorAll('span, div, a')] + .filter(e => e.offsetWidth > 0 && e.children.length === 0); + el = candidates.find(e => { + const t = (e.innerText?.trim() || '').toLowerCase(); + return t === 'показать все' || t === 'show all'; + }); + } + if (!el) return false; + const r = el.getBoundingClientRect(); + const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; + el.dispatchEvent(new MouseEvent('mousedown', opts)); + el.dispatchEvent(new MouseEvent('mouseup', opts)); + el.dispatchEvent(new MouseEvent('click', opts)); + return true; + })()`; +} diff --git a/.claude/skills/web-test/scripts/engine/core/helpers.mjs b/.claude/skills/web-test/scripts/engine/core/helpers.mjs index 98344511..9f0aa46b 100644 --- a/.claude/skills/web-test/scripts/engine/core/helpers.mjs +++ b/.claude/skills/web-test/scripts/engine/core/helpers.mjs @@ -1,4 +1,4 @@ -// web-test core/helpers v1.18 — private, cross-cutting helpers used by the +// web-test core/helpers v1.19 — private, cross-cutting helpers used by the // public action functions (clickElement/fillFields/selectValue/etc). // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills @@ -10,6 +10,10 @@ import { isInputFocusedScript, isInputFocusedInGridScript, findOpenPopupScript, + readEddScript, + isEddVisibleScript, + clickEddItemViaDispatchScript, + clickShowAllInEddScript, } from '../../dom.mjs'; /** @@ -112,18 +116,31 @@ export async function findOpenPopup() { * @returns {Promise<{visible: boolean, items?: Array<{name:string, x:number, y:number}>}>} */ export async function readEdd() { - return await page.evaluate(`(() => { - const edd = document.getElementById('editDropDown'); - if (!edd || edd.offsetWidth === 0) return { visible: false }; - const eddTexts = [...edd.querySelectorAll('.eddText')].filter(el => el.offsetWidth > 0); - return { - visible: true, - items: eddTexts.map(el => { - const r = el.getBoundingClientRect(); - return { name: el.innerText?.trim() || '', x: r.x + r.width / 2, y: r.y + r.height / 2 }; - }) - }; - })()`); + return page.evaluate(readEddScript()); +} + +/** + * Thin wrapper: is the EDD popup currently visible? + * Lighter than `readEdd` when only presence matters. + */ +export async function isEddVisible() { + return page.evaluate(isEddVisibleScript()); +} + +/** + * Click an EDD item by name via dispatchEvent (bypasses div.surface overlays). + * Returns the clicked item's innerText, or `null` if no match. + */ +export async function clickEddItemViaDispatch(itemName) { + return page.evaluate(clickEddItemViaDispatchScript(itemName)); +} + +/** + * Click the "Показать все" / "Show all" link in the EDD footer. + * Returns boolean. + */ +export async function clickShowAllInEdd() { + return page.evaluate(clickShowAllInEddScript()); } /** diff --git a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs index 37fae823..9dfab2d4 100644 --- a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs @@ -17,6 +17,7 @@ import { highlight, unhighlight } from '../recording/highlight.mjs'; import { safeClick, findFieldInputId, readEdd, detectNewForm as helperDetectNewForm, + clickEddItemViaDispatch, clickShowAllInEdd, } from '../core/helpers.mjs'; import { pasteText } from '../core/clipboard.mjs'; import { getFormState } from './state.mjs'; @@ -711,63 +712,9 @@ export async function selectValue(fieldName, searchText, { type } = {}) { return null; } - // Helper: click EDD item via evaluate (bypasses div.surface overlay from DLB) - // page.mouse.click() doesn't work here — surface intercepts pointer events. - // Dispatching mousedown directly on the element avoids this. - async function clickEddItem(itemName) { - return page.evaluate(`(() => { - const edd = document.getElementById('editDropDown'); - if (!edd || edd.offsetWidth === 0) return null; - const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' '); - const target = ny(${JSON.stringify(itemName.toLowerCase())}); - const items = [...edd.querySelectorAll('.eddText')].filter(el => el.offsetWidth > 0); - function clickEl(el) { - const r = el.getBoundingClientRect(); - const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; - el.dispatchEvent(new MouseEvent('mousedown', opts)); - el.dispatchEvent(new MouseEvent('mouseup', opts)); - el.dispatchEvent(new MouseEvent('click', opts)); - return el.innerText.trim(); - } - // Pass 1: exact match (prefer over partial) - for (const el of items) { - const t = ny((el.innerText?.trim() || '').toLowerCase()); - if (t === target) return clickEl(el); - const stripped = t.replace(/\\s*\\([^)]*\\)\\s*$/, ''); - if (stripped === target) return clickEl(el); - } - // Pass 2: partial match - for (const el of items) { - const t = ny((el.innerText?.trim() || '').toLowerCase()); - if (t.includes(target) || target.includes(t.replace(/\\s*\\([^)]*\\)\\s*$/, ''))) return clickEl(el); - } - return null; - })()`); - } - - // Helper: click "Показать все" in EDD footer via evaluate - async function clickShowAll() { - return page.evaluate(`(() => { - const edd = document.getElementById('editDropDown'); - if (!edd || edd.offsetWidth === 0) return false; - let el = edd.querySelector('.eddBottom .hyperlink'); - if (!el || el.offsetWidth === 0) { - const candidates = [...edd.querySelectorAll('span, div, a')] - .filter(e => e.offsetWidth > 0 && e.children.length === 0); - el = candidates.find(e => { - const t = (e.innerText?.trim() || '').toLowerCase(); - return t === 'показать все' || t === 'show all'; - }); - } - if (!el) return false; - const r = el.getBoundingClientRect(); - const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; - el.dispatchEvent(new MouseEvent('mousedown', opts)); - el.dispatchEvent(new MouseEvent('mouseup', opts)); - el.dispatchEvent(new MouseEvent('click', opts)); - return true; - })()`); - } + // Locals → dom-scripts in helpers.mjs (see clickEddItemViaDispatch / clickShowAllInEdd) + const clickEddItem = clickEddItemViaDispatch; + const clickShowAll = clickShowAllInEdd; // 2. Click DLB (handle funcPanel / surface overlay intercept) const dlbSel = `[id="${btn.buttonId}"]`; diff --git a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs index eda8bf24..f1b8f1df 100644 --- a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs +++ b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs @@ -14,6 +14,7 @@ import { safeClick, findFieldInputId, detectNewForm as helperDetectNewForm, isInputFocused, isInputFocusedInGrid, findOpenPopup, + readEdd, isEddVisible, clickEddItemViaDispatch, } from '../core/helpers.mjs'; import { clickElement } from '../core/click.mjs'; import { @@ -427,11 +428,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { await pasteText(info.value, { confirm: ['Control+a', 'Control+v'] }); await page.waitForTimeout(400); // Dismiss EDD autocomplete if it appeared - const hasEdd = await page.evaluate(`(() => { - const edd = document.getElementById('editDropDown'); - return edd && edd.offsetWidth > 0; - })()`); - if (hasEdd) { + if (await isEddVisible()) { await page.keyboard.press('Escape'); await page.waitForTimeout(200); } @@ -741,13 +738,8 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } // Check for EDD autocomplete (indicates reference field) - const eddItems = await page.evaluate(`(() => { - const edd = document.getElementById('editDropDown'); - if (!edd || edd.offsetWidth === 0) return null; - return [...edd.querySelectorAll('.eddText')] - .filter(el => el.offsetWidth > 0) - .map(el => el.innerText?.trim() || ''); - })()`); + const edd = await readEdd(); + const eddItems = edd.visible ? edd.items.map(i => i.name) : null; if (eddItems && eddItems.length > 0) { // Reference field with autocomplete — click best match @@ -763,23 +755,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { if (!pick) pick = realItems[0]; // Click EDD item via dispatchEvent (bypasses div.surface overlay) - const pickLower = pick.toLowerCase(); - await page.evaluate(`(() => { - const edd = document.getElementById('editDropDown'); - if (!edd) return; - for (const el of edd.querySelectorAll('.eddText')) { - if (el.offsetWidth === 0) continue; - if (el.innerText.trim().toLowerCase().includes(${JSON.stringify(pickLower)})) { - const r = el.getBoundingClientRect(); - const opts = { bubbles:true, cancelable:true, - clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; - el.dispatchEvent(new MouseEvent('mousedown', opts)); - el.dispatchEvent(new MouseEvent('mouseup', opts)); - el.dispatchEvent(new MouseEvent('click', opts)); - return; - } - } - })()`); + await clickEddItemViaDispatch(pick); await waitForStable(); info.filled = true; results.push({ field: matchedKey, cell: cell.fullName, ok: true,