refactor(web-test): извлечены grid read-helpers + cloud-popup в dom/

Новое в 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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-26 21:07:10 +03:00
parent 89efcad125
commit b08ee99521
4 changed files with 123 additions and 83 deletions
+6 -1
View File
@@ -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 {
+34 -1
View File
@@ -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 "нет в списке".
+73 -1
View File
@@ -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;
})()`;
}
@@ -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 });