From b08ee99521c981541a1680f994273e1806154bad Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 26 May 2026 21:07:10 +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=D0=B5=D0=BD=D1=8B=20grid=20read-helpers=20+=20clou?= =?UTF-8?q?d-popup=20=D0=B2=20dom/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новое в dom/grid.mjs (все принимают опциональный gridSelector): - countGridRowsScript — кол-во .gridLine в body - isTreeGridScript — тип grid'а (есть .gridBoxTree) - findGridHeadCenterCoordsScript — центр .gridHead для commit-клика - getSelectedOrLastRowIndexScript — selected row index, fallback на последний Также: - isInputFocusedInGrid wrapper (S1) применён в add-row "ready" поллинге - isNotInListCloudVisibleScript (S3) применён вместо локального notInList - clickShowAllInNotInListCloudScript — новая в dom/forms.mjs (клик "Показать все" в "нет в списке" cloud popup через dispatchEvent) Метрики row-fill: 1041 → 971 LOC (−70), evaluates 17 → 10. Регресс 05/08/16/10 — зелёный. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/dom.mjs | 7 +- .claude/skills/web-test/scripts/dom/forms.mjs | 35 +++++++- .claude/skills/web-test/scripts/dom/grid.mjs | 74 ++++++++++++++- .../scripts/engine/table/row-fill.mjs | 90 +++---------------- 4 files changed, 123 insertions(+), 83 deletions(-) diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index ea76975a..df2698e4 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.12 — facade re-exporting injectable DOM scripts from dom/ +// web-test dom v1.13 — 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. @@ -23,6 +23,7 @@ export { findPatternInputIdScript, isTypeDialogScript, isNotInListCloudVisibleScript, + clickShowAllInNotInListCloudScript, findChildFormByButtonScript, readTypeDialogVisibleRowsScript, } from './dom/forms.mjs'; @@ -55,6 +56,10 @@ export { getFormStateScript } from './dom/form-state.mjs'; export { resolveGridScript, readTableScript, + countGridRowsScript, + isTreeGridScript, + findGridHeadCenterCoordsScript, + getSelectedOrLastRowIndexScript, } from './dom/grid.mjs'; export { diff --git a/.claude/skills/web-test/scripts/dom/forms.mjs b/.claude/skills/web-test/scripts/dom/forms.mjs index 0f714f74..70c4cfc8 100644 --- a/.claude/skills/web-test/scripts/dom/forms.mjs +++ b/.claude/skills/web-test/scripts/dom/forms.mjs @@ -1,4 +1,4 @@ -// web-test dom/forms v1.3 — form detection, content read, click-target/field-button resolution +// web-test dom/forms v1.4 — form detection, content read, click-target/field-button resolution // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { DETECT_FORM_FN, READ_FORM_FN } from './_shared.mjs'; @@ -517,6 +517,39 @@ export function isTypeDialogScript(formNum) { })()`; } +/** + * Click the "Показать все" / "Show all" link inside the "нет в списке" + * cloud popup via `dispatchEvent`. Returns boolean — whether clicked. + */ +export function clickShowAllInNotInListCloudScript() { + return `(() => { + for (const el of document.querySelectorAll('div')) { + if (el.offsetWidth === 0 || el.offsetHeight === 0) continue; + const s = getComputedStyle(el); + if (s.position !== 'absolute' && s.position !== 'fixed') continue; + if ((parseInt(s.zIndex) || 0) < 100) continue; + if (!(el.innerText || '').includes('нет в списке')) continue; + const links = [...el.querySelectorAll('a, span, div')] + .filter(e => e.offsetWidth > 0 && e.children.length === 0); + const showAll = links.find(e => { + const t = (e.innerText?.trim() || '').toLowerCase(); + return t === 'показать все' || t === 'show all'; + }); + if (showAll) { + const r = showAll.getBoundingClientRect(); + const opts = { bubbles:true, cancelable:true, + clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; + showAll.dispatchEvent(new MouseEvent('mousedown', opts)); + showAll.dispatchEvent(new MouseEvent('mouseup', opts)); + showAll.dispatchEvent(new MouseEvent('click', opts)); + return true; + } + return false; + } + return false; + })()`; +} + /** * Is the "нет в списке" cloud popup visible? 1C shows it as a positioned div * (absolute/fixed, high z-index) whose text contains "нет в списке". diff --git a/.claude/skills/web-test/scripts/dom/grid.mjs b/.claude/skills/web-test/scripts/dom/grid.mjs index 8c205ebf..959897f6 100644 --- a/.claude/skills/web-test/scripts/dom/grid.mjs +++ b/.claude/skills/web-test/scripts/dom/grid.mjs @@ -1,4 +1,4 @@ -// web-test dom/grid v1.0 — grid resolution + table reading +// web-test dom/grid v1.1 — grid resolution + table reading + edit-time helpers // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** @@ -247,3 +247,75 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto return result; })()`; } + +// ─── Edit-time grid helpers (for fillTableRow / row-fill) ──────────────────── +// +// All helpers below accept an optional `gridSelector`. When passed, they target +// that exact grid; when null/undefined they pick the LAST visible `.grid` on +// the page (this matches the implicit "current grid" used by row-fill). + +/** Inline JS fragment that resolves the target grid into `const grid`. */ +function gridResolver(gridSelector) { + return gridSelector + ? `document.querySelector(${JSON.stringify(gridSelector)})` + : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`; +} + +/** + * Count `.gridLine` rows in the body of the target grid. + * Returns the row count, or `0` when grid/body absent. + */ +export function countGridRowsScript(gridSelector) { + return `(() => { + const grid = ${gridResolver(gridSelector)}; + const body = grid?.querySelector('.gridBody'); + return body ? body.querySelectorAll('.gridLine').length : 0; + })()`; +} + +/** + * Is the target grid a tree grid? (presence of `.gridBoxTree`) + * Returns boolean. + */ +export function isTreeGridScript(gridSelector) { + return `(() => { + const grid = ${gridResolver(gridSelector)}; + return grid ? !!grid.querySelector('.gridBoxTree') : false; + })()`; +} + +/** + * Return center coords of the grid's `.gridHead` element. + * Used as a click target to commit a pending cell edit (clicking the header + * defocuses the input without selecting another row). + * + * Returns `{ x, y } | null`. + */ +export function findGridHeadCenterCoordsScript(gridSelector) { + return `(() => { + const grid = ${gridResolver(gridSelector)}; + if (!grid) return null; + const head = grid.querySelector('.gridHead'); + if (!head) return null; + const r = head.getBoundingClientRect(); + return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; + })()`; +} + +/** + * Return the index of the currently selected row in the target grid, or + * fall back to the last row when nothing is selected. + * + * Returns row index, or `-1` when no rows. + */ +export function getSelectedOrLastRowIndexScript(gridSelector) { + return `(() => { + const grid = ${gridResolver(gridSelector)}; + if (!grid) return -1; + const body = grid.querySelector('.gridBody'); + if (!body) return -1; + const lines = [...body.querySelectorAll('.gridLine')]; + const sel = lines.findIndex(l => l.classList.contains('selected')); + return sel >= 0 ? sel : lines.length - 1; + })()`; +} 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 f1b8f1df..9a935ef6 100644 --- a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs +++ b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs @@ -6,6 +6,9 @@ import { } from '../core/state.mjs'; import { detectFormScript, resolveGridScript, readTableScript, + countGridRowsScript, isTreeGridScript, findGridHeadCenterCoordsScript, + getSelectedOrLastRowIndexScript, + isNotInListCloudVisibleScript, clickShowAllInNotInListCloudScript, } from '../../dom.mjs'; import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs'; import { waitForStable, waitForCondition, startNetworkMonitor } from '../core/wait.mjs'; @@ -62,24 +65,12 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { let addedRowIdx = -1; if (add) { // Count rows before add — new row will be appended at this index - addedRowIdx = await page.evaluate(`(() => { - const grid = ${gridSelector - ? `document.querySelector(${JSON.stringify(gridSelector)})` - : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`}; - const body = grid?.querySelector('.gridBody'); - return body ? body.querySelectorAll('.gridLine').length : 0; - })()`); + addedRowIdx = await page.evaluate(countGridRowsScript(gridSelector)); await clickElement('Добавить', { table }); // Poll for edit mode (INPUT inside grid) instead of fixed 1000ms wait for (let aw = 0; aw < 6; aw++) { await page.waitForTimeout(150); - const ready = await page.evaluate(`(() => { - const f = document.activeElement; - if (!f || (f.tagName !== 'INPUT' && f.tagName !== 'TEXTAREA')) return false; - let n = f; while (n) { if (n.classList?.contains('grid')) return true; n = n.parentElement; } - return false; - })()`); - if (ready) break; + if (await isInputFocusedInGrid()) break; } } @@ -294,12 +285,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { // When click entered INPUT mode but no selection form yet — try F4 only for tree grids // (tree grid ref fields need F4 to open selection form; flat grids work via Tab-loop) if (inEdit && directEditForm === null) { - const isTreeGrid = await page.evaluate(`(() => { - const grid = ${gridSelector - ? `document.querySelector(${JSON.stringify(gridSelector)})` - : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`}; - return grid ? !!grid.querySelector('.gridBoxTree') : false; - })()`); + const isTreeGrid = await page.evaluate(isTreeGridScript(gridSelector)); if (isTreeGrid) { await page.keyboard.press('F4'); for (let fw = 0; fw < 8; fw++) { @@ -782,46 +768,11 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { await page.waitForTimeout(1000); // Check for "нет в списке" cloud popup (reference field, value not found) - const notInList = await page.evaluate(`(() => { - for (const el of document.querySelectorAll('div')) { - if (el.offsetWidth === 0 || el.offsetHeight === 0) continue; - const s = getComputedStyle(el); - if (s.position !== 'absolute' && s.position !== 'fixed') continue; - if ((parseInt(s.zIndex) || 0) < 100) continue; - if ((el.innerText || '').includes('нет в списке')) return true; - } - return false; - })()`); + const notInList = await page.evaluate(isNotInListCloudVisibleScript()); if (notInList) { // Cloud has "Показать все" link — try to open selection form via it - const clickedShowAll = await page.evaluate(`(() => { - for (const el of document.querySelectorAll('div')) { - if (el.offsetWidth === 0 || el.offsetHeight === 0) continue; - const s = getComputedStyle(el); - if (s.position !== 'absolute' && s.position !== 'fixed') continue; - if ((parseInt(s.zIndex) || 0) < 100) continue; - if (!(el.innerText || '').includes('нет в списке')) continue; - // Found the cloud — look for "Показать все" hyperlink inside - const links = [...el.querySelectorAll('a, span, div')] - .filter(e => e.offsetWidth > 0 && e.children.length === 0); - const showAll = links.find(e => { - const t = (e.innerText?.trim() || '').toLowerCase(); - return t === 'показать все' || t === 'show all'; - }); - if (showAll) { - const r = showAll.getBoundingClientRect(); - const opts = { bubbles:true, cancelable:true, - clientX: r.x + r.width/2, clientY: r.y + r.height/2 }; - showAll.dispatchEvent(new MouseEvent('mousedown', opts)); - showAll.dispatchEvent(new MouseEvent('mouseup', opts)); - showAll.dispatchEvent(new MouseEvent('click', opts)); - return true; - } - return false; - } - return false; - })()`); + const clickedShowAll = await page.evaluate(clickShowAllInNotInListCloudScript()); if (clickedShowAll) { await waitForStable(formNum); @@ -958,18 +909,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { // Clicking a different data row would re-enter edit mode on that row. // Without this commit click, the row stays in "uncommitted add" state // and a subsequent Escape (e.g. from closeForm) would cancel the entire row. - const commitTarget = await page.evaluate(`(() => { - const grid = ${gridSelector - ? `document.querySelector(${JSON.stringify(gridSelector)})` - : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`}; - if (!grid) return null; - const head = grid.querySelector('.gridHead'); - if (head) { - const r = head.getBoundingClientRect(); - return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; - } - return null; - })()`); + const commitTarget = await page.evaluate(findGridHeadCenterCoordsScript(gridSelector)); if (commitTarget) { await page.mouse.click(commitTarget.x, commitTarget.y); await page.waitForTimeout(500); @@ -1001,17 +941,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } if (Object.keys(checkboxFields).length > 0) { // Use row index: addedRowIdx (from add mode) or fallback to selected row - const currentRow = addedRowIdx >= 0 ? addedRowIdx : (row != null ? row : await page.evaluate(`(() => { - const grid = ${gridSelector - ? `document.querySelector(${JSON.stringify(gridSelector)})` - : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`}; - if (!grid) return -1; - const body = grid.querySelector('.gridBody'); - if (!body) return -1; - const lines = [...body.querySelectorAll('.gridLine')]; - const sel = lines.findIndex(l => l.classList.contains('selected')); - return sel >= 0 ? sel : lines.length - 1; - })()`) + const currentRow = addedRowIdx >= 0 ? addedRowIdx : (row != null ? row : await page.evaluate(getSelectedOrLastRowIndexScript(gridSelector)) ); if (currentRow >= 0) { const more = await fillTableRow(checkboxFields, { row: currentRow, table });