mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-18 00:29:42 +03:00
fix(web-test): единая модель колонок грида — colindex первым, геометрия запасной
Резолверов колонок было пять, все независимые, и каждый ломался по-своему:
readTable (геометрия X + Y-подряды), clickElement (геометрия X, без Y),
поиск строки {кол: знач} (геометрия X, без Y и fixed-гарда), filterList
(порядковый индекс шапки), fillTableRow (colindex — единственный целый).
Механика поломки (снята живьём на списке задач ERP): шапка «Исполнитель»
широкая (x 1085..1515) и накрывает «Срок» (1085..1251) и «Выполнена»
(1251..1515). Ячейка «Исполнитель» имеет центр 1300 → приписывается к группе
«Выполнена» → та получает лишний под-ряд → срабатывает эвристика «объединённая
шапка» → фантомные «Выполнена 1/2», а значения соседей склеиваются через ' / '.
Теперь COLUMN_MODEL_FN (dom/_shared.mjs) — единственный источник правды:
buildColumnModel / columnForCell / cellForColumn / resolveColumnByName. Идентичность
колонки — colindex (собственный id колонки в 1С, есть и на шапке, и на ячейке);
геометрия работает только там, где своей шапки у ячейки нет — под-ряды
объединённой шапки («Субконто Дт» над тремя ячейками). Путь записи пришёл к этому
решению раньше (grid-edit.mjs: «reliable across merged headers») — остальные
выровнены по нему.
Следствие: имя колонки из readTable теперь годится для клика/заполнения/фильтра —
раньше readTable отдавал «Субконто Дт 2», а клик про такое имя не знал.
Попутно закрыт второй дефект: безымянная picture-колонка определялась по ПЕРВОЙ
строке, а picField над Boolean не рисует картинку при Ложь → колонка пропадала из
columns целиком. Модель сэмплит до 10 строк и ищет ячейку по colindex.
Проверено:
- 24-multirow-header (стенд, оба паттерна ERP) — зелёный; до правки красный;
- клик проверяется по факту (DOM select+focus), а не по эху clicked.column:
до правки клик по «Срок» молча жал «Исполнитель 2» и рапортовал успех;
- живьём на ERP: список задач — 9 честных колонок вместо фантомов, значения на
местах; форма операции (шапка 2 этажа, строка 3 под-ряда) — «Субконто Дт/Кт 1..3»
сохранены, ничего не поехало;
- полный регресс 28/28.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,8 @@ const form = await getFormState();
|
||||
#### `readTable({ maxRows?, offset?, table? })` → `{ columns, rows, total, shown, offset, hasMore }`
|
||||
Read actual grid data with pagination. Each row is `{ columnName: value }`.
|
||||
|
||||
**Names from `columns` work everywhere** — `clickElement({ row, column })`, `fillTableRow`, `filterList` and `{ column: value }` row filters all resolve a column the same way `readTable` names it. Multi-row headers included: a header stacked over several sub-rows is reported (and targeted) as `'Имя 1'`, `'Имя 2'`, … — e.g. the accounting entries grid gives `'Субконто Дт 1'..'Субконто Дт 3'`.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `maxRows` | 20 | Max rows to return per call |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom v1.19 — facade re-exporting injectable DOM scripts from dom/
|
||||
// web-test dom v1.20 — 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom shared v1.3 — embedded JS function constants
|
||||
// web-test dom shared v1.4 — embedded JS function constants
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Shared function strings embedded into page.evaluate() generators.
|
||||
@@ -105,6 +105,209 @@ export const HEADERLESS_GRID_FN = `function synthHeaderlessColumns(grid) {
|
||||
return cols;
|
||||
}`;
|
||||
|
||||
/**
|
||||
* Single source of truth for columns of a grid WITH a header — the headed twin of
|
||||
* synthHeaderlessColumns above, and for the same reason: a column name must map to the same
|
||||
* physical cell for readers (readTable) and resolvers (click, row search, filter, fill).
|
||||
*
|
||||
* Column identity is `colindex` — 1С's own column id, present on both header boxes and body
|
||||
* cells. Geometry is the FALLBACK, used only for cells that have no header of their own
|
||||
* (sub-rows of a merged header, e.g. «Субконто Дт» over three stacked cells).
|
||||
*
|
||||
* Why colindex first: a wide header (ERP task list, «Исполнитель» spanning x 1085…1515) covers
|
||||
* the narrow headers below it («Срок» 1085…1251, «Выполнена» 1251…1515). Matching a cell by its
|
||||
* center-x alone puts the «Исполнитель» cell (center 1300) into the «Выполнена» group — which
|
||||
* both fakes a merged header (phantom «Выполнена 1/2») and glues foreign values together.
|
||||
* The write path (grid-edit.mjs) already resolves cells by colindex for exactly this reason.
|
||||
*
|
||||
* Column: { name, text, title, ci, x, right, y, h, fixed, kind?, subIdx? }
|
||||
* - ci — anchor; null for expanded sub-columns (their cells carry a different colindex).
|
||||
* - subIdx — set on «Имя 1/2/3» columns expanded from ONE header over several sub-rows;
|
||||
* such a cell is found by its Y order inside the header's x-range.
|
||||
*/
|
||||
export const COLUMN_MODEL_FN = HEADERLESS_GRID_FN + `
|
||||
function picInfoShared(cell) {
|
||||
if (!cell) return null;
|
||||
if (cell.querySelector('.gridListH, .gridListV, [tree="true"], .gridBoxTree')) return null;
|
||||
const dib = cell.querySelector('.gridBoxImg .dIB');
|
||||
if (!dib) return null;
|
||||
const bg = dib.style.backgroundImage || '';
|
||||
if (!bg.includes('pictureCollection/picture/')) return null;
|
||||
const m = bg.match(/[?&]gx=(\\d+)/);
|
||||
return { gx: m ? m[1] : '0' };
|
||||
}
|
||||
|
||||
function buildColumnModel(grid) {
|
||||
const head = grid.querySelector('.gridHead');
|
||||
const body = grid.querySelector('.gridBody');
|
||||
const empty = { columns: [], byCi: {}, groups: new Map(), subRows: {}, multiRow: {}, headless: !head };
|
||||
if (!body) return empty;
|
||||
|
||||
if (!head) {
|
||||
const cols = synthHeaderlessColumns(grid).map(c => ({
|
||||
name: c.name, text: c.name, title: '', ci: c.colindex, subTarget: c.subTarget,
|
||||
kind: c.kind, x: 0, right: 0, y: 0, h: 0, fixed: false,
|
||||
}));
|
||||
const byCi = {};
|
||||
cols.forEach(c => { if (c.ci != null && byCi[c.ci] === undefined) byCi[c.ci] = c; });
|
||||
return { columns: cols, byCi, groups: new Map(), subRows: {}, multiRow: {}, headless: true };
|
||||
}
|
||||
|
||||
const headLine = head.querySelector('.gridLine') || head;
|
||||
const lines = [...body.querySelectorAll('.gridLine')];
|
||||
const cellByCi = (line, ci) => [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === ci);
|
||||
const columns = [];
|
||||
|
||||
[...headLine.children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const ci = box.getAttribute('colindex');
|
||||
const textEl = box.querySelector('.gridBoxText');
|
||||
const text = ((textEl || box).innerText || '').trim().replace(/\\n/g, ' ');
|
||||
const title = (box.getAttribute('title') || '').trim();
|
||||
const r = box.getBoundingClientRect();
|
||||
const base = { ci, x: r.x, right: r.x + r.width, y: r.y, h: r.height,
|
||||
fixed: box.classList.contains('gridBoxFix') };
|
||||
if (text) { columns.push(Object.assign(base, { name: text, text: text, title: title })); return; }
|
||||
|
||||
// Unnamed header — a column only if its cells hold a checkbox or a picture. 1С doesn't
|
||||
// expose the technical name, so it is named by the header tooltip.
|
||||
// Sample SEVERAL rows: a picture bound to a Boolean draws nothing for false, so an empty
|
||||
// first row is not evidence that the column has no pictures at all.
|
||||
let kind = null;
|
||||
for (const line of lines.slice(0, 10)) {
|
||||
const cell = ci != null ? cellByCi(line, ci) : null;
|
||||
if (!cell) continue;
|
||||
if (cell.querySelector('.checkbox')) { kind = 'checkbox'; break; }
|
||||
if (picInfoShared(cell)) { kind = 'picture'; break; }
|
||||
}
|
||||
if (!kind && picInfoShared(box)) kind = 'picture';
|
||||
if (!kind) return;
|
||||
let name = kind === 'checkbox' ? '(checkbox)' : (title || '(picture)');
|
||||
if (columns.some(c => c.name === name)) {
|
||||
let n = 2;
|
||||
while (columns.some(c => c.name === name + ' ' + n)) n++;
|
||||
name = name + ' ' + n;
|
||||
}
|
||||
columns.push(Object.assign(base, { name: name, text: '', title: title, kind: kind }));
|
||||
});
|
||||
|
||||
const keyOf = c => Math.round(c.x) + ':' + Math.round(c.right);
|
||||
const groups = new Map();
|
||||
columns.forEach(c => { const k = keyOf(c); if (!groups.has(k)) groups.set(k, []); groups.get(k).push(c); });
|
||||
for (const hdrs of groups.values()) hdrs.sort((a, b) => a.y - b.y);
|
||||
const byCi = {};
|
||||
columns.forEach(c => { if (c.ci != null && byCi[c.ci] === undefined) byCi[c.ci] = c; });
|
||||
|
||||
// Sub-rows per x-group, measured on the first data line. A cell belongs to the group of its
|
||||
// OWN header whenever colindex says so; only header-less cells are placed geometrically.
|
||||
const subRows = {};
|
||||
if (lines[0]) {
|
||||
[...lines[0].children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const ci = box.getAttribute('colindex');
|
||||
const own = ci != null ? byCi[ci] : null;
|
||||
let key = null;
|
||||
const r = box.getBoundingClientRect();
|
||||
if (own) key = keyOf(own);
|
||||
else {
|
||||
const cx = r.x + r.width / 2;
|
||||
for (const [k, hdrs] of groups) {
|
||||
if (cx >= hdrs[0].x && cx < hdrs[0].right) { key = k; break; }
|
||||
}
|
||||
}
|
||||
if (key == null) return;
|
||||
(subRows[key] = subRows[key] || []).push({ y: r.y });
|
||||
});
|
||||
Object.keys(subRows).forEach(k => subRows[k].sort((a, b) => a.y - b.y));
|
||||
}
|
||||
|
||||
// Stacked headers (2+ over several sub-rows) → match by Y order.
|
||||
// ONE header over several sub-rows → merged header: expand into «Имя 1..N».
|
||||
const multiRow = {};
|
||||
for (const [k, hdrs] of groups) {
|
||||
const subs = subRows[k];
|
||||
if (!subs || subs.length <= 1) continue;
|
||||
if (hdrs.length >= 2) { multiRow[k] = hdrs; continue; }
|
||||
const base = hdrs[0];
|
||||
const at = columns.indexOf(base);
|
||||
columns.splice(at, 1);
|
||||
if (base.ci != null && byCi[base.ci] === base) delete byCi[base.ci];
|
||||
const expanded = [];
|
||||
for (let si = 0; si < subs.length; si++) {
|
||||
const col = Object.assign({}, base, {
|
||||
name: base.name + ' ' + (si + 1), ci: null,
|
||||
y: base.y + si, h: base.h / subs.length, subIdx: si,
|
||||
});
|
||||
columns.splice(at + si, 0, col);
|
||||
expanded.push(col);
|
||||
}
|
||||
groups.set(k, expanded);
|
||||
multiRow[k] = expanded;
|
||||
}
|
||||
|
||||
return { columns: columns, byCi: byCi, groups: groups, subRows: subRows, multiRow: multiRow, headless: false };
|
||||
}
|
||||
|
||||
/** Cell → column. colindex first; geometry only for cells without a header of their own. */
|
||||
function columnForCell(model, box) {
|
||||
const ci = box.getAttribute('colindex');
|
||||
if (ci != null && model.byCi[ci]) return model.byCi[ci];
|
||||
const r = box.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
const fixed = box.classList.contains('gridBoxFix');
|
||||
for (const k of Object.keys(model.multiRow)) {
|
||||
const hdrs = model.multiRow[k];
|
||||
if (cx < hdrs[0].x || cx >= hdrs[0].right) continue;
|
||||
const subs = model.subRows[k];
|
||||
if (subs) {
|
||||
const si = subs.findIndex(s => Math.abs(s.y - r.y) < 5);
|
||||
if (si >= 0 && si < hdrs.length) return hdrs[si];
|
||||
}
|
||||
let best = hdrs[0], bd = Infinity;
|
||||
for (const h of hdrs) { const d = Math.abs(r.y - h.y); if (d < bd) { bd = d; best = h; } }
|
||||
return best;
|
||||
}
|
||||
return model.columns.find(c => cx >= c.x && cx < c.right && c.fixed === fixed) || null;
|
||||
}
|
||||
|
||||
/** Column → cell inside a given line. Mirror of columnForCell, same precedence. */
|
||||
function cellForColumn(model, line, col) {
|
||||
const boxes = [...line.children].filter(b => b.offsetWidth > 0);
|
||||
if (col.subIdx != null) {
|
||||
const inGroup = boxes
|
||||
.filter(b => {
|
||||
const r = b.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
return cx >= col.x && cx < col.right && b.classList.contains('gridBoxFix') === col.fixed;
|
||||
})
|
||||
.sort((a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y);
|
||||
return inGroup[col.subIdx] || null;
|
||||
}
|
||||
if (col.ci != null) {
|
||||
const hit = boxes.find(b => b.getAttribute('colindex') === col.ci);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return boxes
|
||||
.filter(b => b.classList.contains('gridBoxFix') === col.fixed)
|
||||
.find(b => {
|
||||
const r = b.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
return cx >= col.x && cx < col.right;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
/** Column by user-supplied name: exact → «Группа / Имя» suffix → substring. */
|
||||
function resolveColumnByName(model, name) {
|
||||
const lo = s => (s || '').toLowerCase().replace(/ё/g, 'е').trim();
|
||||
const cand = c => [c.name, c.text, c.title].filter(Boolean);
|
||||
const n = lo(name);
|
||||
const suffix = lo(' / ' + name);
|
||||
return model.columns.find(c => cand(c).some(t => lo(t) === n))
|
||||
|| model.columns.find(c => cand(c).some(t => lo(t).endsWith(suffix)))
|
||||
|| model.columns.find(c => cand(c).some(t => lo(t).includes(n)))
|
||||
|| null;
|
||||
}`;
|
||||
|
||||
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
||||
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
||||
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// web-test dom/filter v1.0 — DOM scripts for filterList / unfilterList
|
||||
// web-test dom/filter v1.1 — DOM scripts for filterList / unfilterList
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { COLUMN_MODEL_FN } from './_shared.mjs';
|
||||
|
||||
/**
|
||||
* Find the first grid cell on the form and return its center coords.
|
||||
@@ -39,32 +40,26 @@ export function findColumnFirstCellCoordsScript(formNum, field) {
|
||||
const grid = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
||||
.find(g => g.offsetWidth > 0);
|
||||
if (!grid) return { error: 'no_grid' };
|
||||
${COLUMN_MODEL_FN}
|
||||
const targetField = ${JSON.stringify(field)};
|
||||
const headers = [...grid.querySelectorAll('.gridHead .gridBox')];
|
||||
let colIndex = -1;
|
||||
let startsWithIdx = -1;
|
||||
let includesIdx = -1;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const t = headers[i].innerText?.trim().replace(/\\u00a0/g, ' ');
|
||||
if (!t) continue;
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const tl = ny(t.toLowerCase()), fl = ny(targetField.toLowerCase());
|
||||
if (tl === fl) { colIndex = i; break; }
|
||||
if (startsWithIdx < 0 && tl.startsWith(fl)) { startsWithIdx = i; }
|
||||
else if (includesIdx < 0 && tl.includes(fl)) { includesIdx = i; }
|
||||
}
|
||||
if (colIndex < 0) colIndex = startsWithIdx >= 0 ? startsWithIdx : includesIdx;
|
||||
const rows = [...grid.querySelectorAll('.gridBody .gridLine')];
|
||||
if (!rows.length) return { error: 'no_rows' };
|
||||
if (colIndex < 0) {
|
||||
|
||||
// Column resolution — shared model (dom/_shared.mjs), same as readTable/click. The old
|
||||
// positional index (Nth header box → Nth cell box) breaks on multi-row headers, where
|
||||
// header and cell counts and order differ.
|
||||
const model = buildColumnModel(grid);
|
||||
const col = resolveColumnByName(model, targetField);
|
||||
if (!col) {
|
||||
// Unknown column → click the first cell and let the caller fall back to DLB.
|
||||
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||
if (!cells.length) return { error: 'no_cells' };
|
||||
const r = cells[0].getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), needDlb: true };
|
||||
const r0 = cells[0].getBoundingClientRect();
|
||||
return { x: Math.round(r0.x + r0.width / 2), y: Math.round(r0.y + r0.height / 2), needDlb: true };
|
||||
}
|
||||
const cells = [...rows[0].querySelectorAll('.gridBox')];
|
||||
if (colIndex >= cells.length) return { error: 'cell_not_found' };
|
||||
const r = cells[colIndex].getBoundingClientRect();
|
||||
const cell = cellForColumn(model, rows[0], col);
|
||||
if (!cell) return { error: 'cell_not_found' };
|
||||
const r = cell.getBoundingClientRect();
|
||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||
})()`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// web-test dom/grid-edit v1.2 — DOM scripts for row-fill (grid edit-time operations)
|
||||
// web-test dom/grid-edit v1.3 — DOM scripts for row-fill (grid edit-time operations)
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
import { HEADERLESS_GRID_FN } from './_shared.mjs';
|
||||
import { HEADERLESS_GRID_FN, COLUMN_MODEL_FN } from './_shared.mjs';
|
||||
//
|
||||
// All helpers below accept an optional `gridSelector`. When passed, they target
|
||||
// that exact grid; when null/undefined they pick the LAST visible `.grid` on
|
||||
@@ -67,45 +67,28 @@ export function sortFieldKeysByColindexScript(gridSelector, fieldKeys) {
|
||||
*/
|
||||
export function findCellCoordsByFieldsScript(gridSelector, row, fieldKeys) {
|
||||
return `(() => {
|
||||
${HEADERLESS_GRID_FN}
|
||||
${COLUMN_MODEL_FN}
|
||||
const grid = ${gridResolver(gridSelector)};
|
||||
if (!grid) return { error: 'no_grid' };
|
||||
const head = grid.querySelector('.gridHead');
|
||||
const body = grid.querySelector('.gridBody');
|
||||
if (!body) return { error: 'no_grid_body' };
|
||||
|
||||
// Read columns to find target colindex (+ subTarget for headerless split mark-boxes)
|
||||
const cols = [];
|
||||
if (head) {
|
||||
const headLine = head.querySelector('.gridLine') || head;
|
||||
[...headLine.children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const t = box.querySelector('.gridBoxText');
|
||||
const ci = box.getAttribute('colindex');
|
||||
cols.push({ colindex: ci, text: ((t || box).innerText?.trim() || '').toLowerCase(), subTarget: null });
|
||||
});
|
||||
} else {
|
||||
synthHeaderlessColumns(grid).forEach(c => cols.push({ colindex: c.colindex, text: c.name.toLowerCase(), subTarget: c.subTarget }));
|
||||
}
|
||||
|
||||
// Column resolution — shared model (dom/_shared.mjs): same names and same physical cell
|
||||
// as readTable/click report, including «Имя 1/2/3» of an expanded merged header.
|
||||
const model = buildColumnModel(grid);
|
||||
const keys = ${JSON.stringify(fieldKeys)};
|
||||
let targetColindex = null, targetSub = null;
|
||||
let targetCol = null;
|
||||
for (const key of keys) {
|
||||
const exact = cols.find(c => c.text === key);
|
||||
if (exact) { targetColindex = exact.colindex; targetSub = exact.subTarget; break; }
|
||||
const inc = cols.find(c => c.text.includes(key) || key.includes(c.text));
|
||||
if (inc) { targetColindex = inc.colindex; targetSub = inc.subTarget; break; }
|
||||
targetCol = resolveColumnByName(model, key);
|
||||
if (targetCol) break;
|
||||
}
|
||||
const targetSub = targetCol ? (targetCol.subTarget || null) : null;
|
||||
|
||||
const rows = [...body.querySelectorAll('.gridLine')];
|
||||
if (${row} >= rows.length) return { error: 'row_out_of_range', total: rows.length };
|
||||
const line = rows[${row}];
|
||||
|
||||
// Find body cell by colindex (reliable across merged headers)
|
||||
let box = null;
|
||||
if (targetColindex != null) {
|
||||
box = [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === targetColindex);
|
||||
}
|
||||
let box = targetCol ? cellForColumn(model, line, targetCol) : null;
|
||||
// Fallback: second visible box (skip checkbox/N column)
|
||||
if (!box) {
|
||||
const boxes = [...line.children].filter(b => b.offsetWidth > 0 && !b.classList.contains('gridBoxComp'));
|
||||
@@ -134,39 +117,30 @@ export function findCellCoordsByFieldsScript(gridSelector, row, fieldKeys) {
|
||||
*/
|
||||
export function findNextCellCoordsByKeyScript(gridSelector, row, key) {
|
||||
return `(() => {
|
||||
${HEADERLESS_GRID_FN}
|
||||
${COLUMN_MODEL_FN}
|
||||
const grid = ${gridResolver(gridSelector)};
|
||||
if (!grid) return null;
|
||||
const head = grid.querySelector('.gridHead');
|
||||
const body = grid.querySelector('.gridBody');
|
||||
if (!body) return null;
|
||||
const cols = [];
|
||||
if (head) {
|
||||
const headLine = head.querySelector('.gridLine') || head;
|
||||
[...headLine.children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const t = box.querySelector('.gridBoxText');
|
||||
const ci = box.getAttribute('colindex');
|
||||
cols.push({ colindex: ci, text: ((t || box).innerText?.trim() || '').toLowerCase(), subTarget: null });
|
||||
});
|
||||
} else {
|
||||
synthHeaderlessColumns(grid).forEach(c => cols.push({ colindex: c.colindex, text: c.name.toLowerCase(), subTarget: c.subTarget }));
|
||||
}
|
||||
const model = buildColumnModel(grid);
|
||||
const kl = ${JSON.stringify(key.toLowerCase())};
|
||||
const klNoSpace = kl.replace(/[\\s\\-]+/g, '');
|
||||
let targetColindex = null, targetSub = null;
|
||||
const exact = cols.find(c => c.text === kl);
|
||||
if (exact) { targetColindex = exact.colindex; targetSub = exact.subTarget; }
|
||||
else {
|
||||
const inc = cols.find(c => c.text.includes(kl) || kl.includes(c.text)
|
||||
|| c.text.includes(klNoSpace) || klNoSpace.includes(c.text));
|
||||
if (inc) { targetColindex = inc.colindex; targetSub = inc.subTarget; }
|
||||
// Extra "no-space/no-dash" fuzz on top of the shared resolver: header «Группа Контрагентов»
|
||||
// must match the key «ГруппаКонтрагентов».
|
||||
let targetCol = resolveColumnByName(model, kl);
|
||||
if (!targetCol) {
|
||||
const squash = s => (s || '').toLowerCase().replace(/ё/g, 'е').replace(/[\\s\\-]+/g, '');
|
||||
const klNoSpace = squash(kl);
|
||||
targetCol = model.columns.find(c => {
|
||||
const t = squash(c.name);
|
||||
return t && (t.includes(klNoSpace) || klNoSpace.includes(t));
|
||||
}) || null;
|
||||
}
|
||||
if (targetColindex == null) return null;
|
||||
if (!targetCol) return null;
|
||||
const targetSub = targetCol.subTarget || null;
|
||||
const rows = [...body.querySelectorAll('.gridLine')];
|
||||
if (${row} >= rows.length) return null;
|
||||
const line = rows[${row}];
|
||||
const box = [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === targetColindex);
|
||||
const box = cellForColumn(model, line, targetCol);
|
||||
if (!box) return null;
|
||||
box.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
let cell;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// web-test dom/grid v1.13 — grid resolution + table reading + edit-time helpers
|
||||
// web-test dom/grid v1.14 — grid resolution + table reading + edit-time helpers
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { ROW_CLICK_POINT_FN, HEADERLESS_GRID_FN } from './_shared.mjs';
|
||||
import { ROW_CLICK_POINT_FN, HEADERLESS_GRID_FN, COLUMN_MODEL_FN } from './_shared.mjs';
|
||||
import { ROW_STATE_FN } from './row-state.mjs';
|
||||
|
||||
/**
|
||||
@@ -95,7 +95,7 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
const m = bg.match(/[?&]gx=(\\d+)/);
|
||||
return { gx: m ? m[1] : '0' };
|
||||
}
|
||||
${HEADERLESS_GRID_FN}
|
||||
${COLUMN_MODEL_FN}
|
||||
${ROW_STATE_FN}
|
||||
|
||||
// Write the leading row-state sprite onto a row: _rowPic (raw, always) + decoded booleans
|
||||
@@ -192,116 +192,10 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract column headers with X-coordinates for alignment
|
||||
const columns = [];
|
||||
const headLine = head.querySelector('.gridLine') || head;
|
||||
[...headLine.children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const textEl = box.querySelector('.gridBoxText');
|
||||
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||
if (!text) {
|
||||
// Unnamed column — check if data cells contain checkboxes or pictures.
|
||||
// Picture columns have no header text (only an icon + a title tooltip); 1С
|
||||
// doesn't expose the technical column name in the DOM, so we name them by
|
||||
// the header's title attribute, falling back to '(picture)'.
|
||||
const firstLine = body?.querySelector('.gridLine');
|
||||
const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0);
|
||||
const idx = visibleHeaders.indexOf(box);
|
||||
const cells = firstLine ? [...firstLine.children].filter(c => c.offsetWidth > 0) : [];
|
||||
const r = box.getBoundingClientRect();
|
||||
if (cells[idx]?.querySelector('.checkbox')) {
|
||||
columns.push({ text: '(checkbox)', x: r.x, w: r.width, right: r.x + r.width, y: r.y, h: r.height });
|
||||
} else if (picInfo(box) || picInfo(cells[idx])) {
|
||||
let title = (box.getAttribute('title') || '').trim() || '(picture)';
|
||||
// Disambiguate duplicate picture-column names with a numeric suffix.
|
||||
if (columns.some(c => c.text === title)) {
|
||||
let n = 2;
|
||||
while (columns.some(c => c.text === title + ' ' + n)) n++;
|
||||
title = title + ' ' + n;
|
||||
}
|
||||
columns.push({ text: title, x: r.x, w: r.width, right: r.x + r.width, y: r.y, h: r.height });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const r = box.getBoundingClientRect();
|
||||
columns.push({ text, x: r.x, w: r.width, right: r.x + r.width, y: r.y, h: r.height });
|
||||
});
|
||||
|
||||
// Multi-row grid support: detect stacked/merged headers.
|
||||
// Group headers by X-range. For each group, count data sub-rows from first line.
|
||||
// - Stacked headers (2+ headers at same X) with multiple data rows → match by Y-order
|
||||
// - Single merged header with multiple data rows → expand to numbered columns (e.g. "Субконто Дт 1")
|
||||
const xGroups = new Map();
|
||||
columns.forEach(c => {
|
||||
const key = Math.round(c.x) + ':' + Math.round(c.right);
|
||||
if (!xGroups.has(key)) xGroups.set(key, []);
|
||||
xGroups.get(key).push(c);
|
||||
});
|
||||
for (const [, hdrs] of xGroups) hdrs.sort((a, b) => a.y - b.y);
|
||||
|
||||
const firstDataLine = body?.querySelector('.gridLine');
|
||||
const subRowMap = new Map();
|
||||
if (firstDataLine) {
|
||||
[...firstDataLine.children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const r = box.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
for (const [key, hdrs] of xGroups) {
|
||||
const h0 = hdrs[0];
|
||||
if (cx >= h0.x && cx < h0.right) {
|
||||
if (!subRowMap.has(key)) subRowMap.set(key, []);
|
||||
subRowMap.get(key).push({ y: r.y });
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const [, subs] of subRowMap) subs.sort((a, b) => a.y - b.y);
|
||||
}
|
||||
|
||||
const multiRowGroups = new Map();
|
||||
for (const [key, hdrs] of xGroups) {
|
||||
const subs = subRowMap.get(key);
|
||||
if (!subs || subs.length <= 1) continue;
|
||||
if (hdrs.length >= 2) {
|
||||
multiRowGroups.set(key, hdrs);
|
||||
} else if (hdrs.length === 1 && subs.length > 1) {
|
||||
const base = hdrs[0];
|
||||
const baseIdx = columns.indexOf(base);
|
||||
columns.splice(baseIdx, 1);
|
||||
const expanded = [];
|
||||
for (let si = 0; si < subs.length; si++) {
|
||||
const numbered = {
|
||||
text: base.text + ' ' + (si + 1),
|
||||
x: base.x, w: base.w, right: base.right,
|
||||
y: base.y + si, h: base.h / subs.length, _subIdx: si
|
||||
};
|
||||
columns.splice(baseIdx + si, 0, numbered);
|
||||
expanded.push(numbered);
|
||||
}
|
||||
multiRowGroups.set(key, expanded);
|
||||
}
|
||||
}
|
||||
|
||||
function matchColumn(cellX, cellW, cellY) {
|
||||
const cx = cellX + cellW / 2;
|
||||
for (const [key, hdrs] of multiRowGroups) {
|
||||
const h0 = hdrs[0];
|
||||
if (cx >= h0.x && cx < h0.right) {
|
||||
const subs = subRowMap.get(key);
|
||||
if (subs) {
|
||||
const subIdx = subs.findIndex(s => Math.abs(s.y - cellY) < 5);
|
||||
if (subIdx >= 0 && subIdx < hdrs.length) return hdrs[subIdx];
|
||||
}
|
||||
let best = hdrs[0], bestDist = Infinity;
|
||||
for (const h of hdrs) {
|
||||
const dist = Math.abs(cellY - h.y);
|
||||
if (dist < bestDist) { bestDist = dist; best = h; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
return columns.find(c => cx >= c.x && cx < c.right);
|
||||
}
|
||||
// Columns + cell↔column mapping — single source of truth in dom/_shared.mjs.
|
||||
// colindex first, geometry only for cells without a header of their own.
|
||||
const model = buildColumnModel(grid);
|
||||
const columns = model.columns;
|
||||
|
||||
// Extract data rows from gridBody
|
||||
const allLines = body.querySelectorAll('.gridLine');
|
||||
@@ -312,7 +206,7 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
const line = allLines[i];
|
||||
if (!line) break;
|
||||
const row = {};
|
||||
columns.forEach(c => { row[c.text] = ''; });
|
||||
columns.forEach(c => { row[c.name] = ''; });
|
||||
[...line.children].forEach(box => {
|
||||
if (box.offsetWidth === 0) return;
|
||||
const textEl = box.querySelector('.gridBoxText');
|
||||
@@ -330,11 +224,9 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
else return;
|
||||
}
|
||||
}
|
||||
// Match cell to column by X+Y overlap (multi-row aware)
|
||||
const r = box.getBoundingClientRect();
|
||||
const col = matchColumn(r.x, r.width, r.y);
|
||||
const col = columnForCell(model, box);
|
||||
if (col) {
|
||||
row[col.text] = row[col.text] ? row[col.text] + ' / ' + val : val;
|
||||
row[col.name] = row[col.name] ? row[col.name] + ' / ' + val : val;
|
||||
}
|
||||
});
|
||||
// Detect row kind: group (gridListH), parent/up (gridListV), or element
|
||||
@@ -388,7 +280,7 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
hasMore = { below: body.scrollHeight > body.clientHeight };
|
||||
}
|
||||
}
|
||||
const result = { name, columns: columns.map(c => c.text), rows, total, offset: ${offset}, shown: rows.length, hasMore };
|
||||
const result = { name, columns: columns.map(c => c.name), rows, total, offset: ${offset}, shown: rows.length, hasMore };
|
||||
if (isTree) result.viewMode = 'tree';
|
||||
if (hasGroups) result.hierarchical = true;
|
||||
return result;
|
||||
@@ -523,7 +415,7 @@ export function getSelectedOrLastRowIndexScript(gridSelector) {
|
||||
export function scanGridRowsScript(formNum, search) {
|
||||
return `(() => {
|
||||
${ROW_CLICK_POINT_FN}
|
||||
${HEADERLESS_GRID_FN}
|
||||
${COLUMN_MODEL_FN}
|
||||
const p = 'form${formNum}_';
|
||||
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
||||
if (!grid) return null;
|
||||
@@ -547,41 +439,14 @@ export function scanGridRowsScript(formNum, search) {
|
||||
if (!search || (isObj && !Object.keys(search).length)) {
|
||||
sel = lines[0]; matchKind = 'first';
|
||||
} else if (isObj) {
|
||||
// Resolve each key to a header column (fuzzy, normalised) — mirror resolveCol.
|
||||
const headLine = grid.querySelector('.gridHead .gridLine') || grid.querySelector('.gridHead');
|
||||
let headers;
|
||||
if (headLine) {
|
||||
headers = [...headLine.children]
|
||||
.filter(c => c.offsetWidth > 0)
|
||||
.map(c => {
|
||||
const t = (c.querySelector('.gridBoxText') || c).innerText || '';
|
||||
const title = c.getAttribute('title') || '';
|
||||
const r = c.getBoundingClientRect();
|
||||
return { name: disp(t) || disp(title), text: t, title, x: r.x, right: r.x + r.width };
|
||||
})
|
||||
.filter(h => h.name);
|
||||
} else {
|
||||
// Headerless: synthesized columns anchored by colindex.
|
||||
headers = synthHeaderlessColumns(grid).map(c => ({ name: c.name, text: c.name, title: '', x: 0, right: 0, colindex: c.colindex }));
|
||||
}
|
||||
const resolveCol = name => {
|
||||
const n = norm(name);
|
||||
const cand = h => [h.text, h.title].filter(Boolean);
|
||||
return headers.find(h => cand(h).some(t => norm(t) === n))
|
||||
|| headers.find(h => cand(h).some(t => norm(t).includes(n)));
|
||||
};
|
||||
const cellAtCol = (line, col) => {
|
||||
if (col.colindex != null) return visCells(line).find(b => b.getAttribute('colindex') === col.colindex);
|
||||
return visCells(line).find(b => {
|
||||
const r = b.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
return cx >= col.x && cx < col.right;
|
||||
});
|
||||
};
|
||||
// Column resolution — shared model (dom/_shared.mjs): colindex first, geometry only
|
||||
// for cells without a header of their own. Same names as readTable reports.
|
||||
const model = buildColumnModel(grid);
|
||||
const cellAtCol = (line, col) => cellForColumn(model, line, col);
|
||||
const keys = Object.keys(search);
|
||||
const cols = {};
|
||||
for (const k of keys) {
|
||||
const c = resolveCol(k);
|
||||
const c = resolveColumnByName(model, k);
|
||||
if (!c) return { rowCount: lines.length, error: 'filter_column_not_found', column: k, visibleSample };
|
||||
cols[k] = c;
|
||||
}
|
||||
@@ -664,66 +529,22 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
||||
const head = grid.querySelector('.gridHead');
|
||||
const body = grid.querySelector('.gridBody');
|
||||
if (!body) return { error: 'no_grid_structure' };
|
||||
${HEADERLESS_GRID_FN}
|
||||
${COLUMN_MODEL_FN}
|
||||
const isHeadless = !head;
|
||||
|
||||
// Header X-ranges (mirror of readTableScript logic, simplified). We also
|
||||
// remember whether each header is frozen (gridBoxFix) — frozen and scrollable
|
||||
// columns can share X coordinates after horizontal scroll, so cell matching
|
||||
// must respect the frozen/scrollable partition.
|
||||
let headers;
|
||||
if (head) {
|
||||
const headLine = head.querySelector('.gridLine') || head;
|
||||
headers = [...headLine.children]
|
||||
.filter(c => c.offsetWidth > 0)
|
||||
.map(c => {
|
||||
const textEl = c.querySelector('.gridBoxText');
|
||||
const text = (textEl || c).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||
// Picture/icon columns have no header text — fall back to the title tooltip
|
||||
// (mirrors readTable naming) so they can still be targeted for clicking.
|
||||
const title = (c.getAttribute('title') || '').trim();
|
||||
const r = c.getBoundingClientRect();
|
||||
return { text, title, name: text || title, x: r.x, right: r.x + r.width, fixed: c.classList.contains('gridBoxFix') };
|
||||
})
|
||||
.filter(h => h.name);
|
||||
} else {
|
||||
// Headerless: synthesized columns anchored by colindex (cellAtColX matches by colindex).
|
||||
headers = synthHeaderlessColumns(grid).map(c => ({ text: c.name, title: '', name: c.name, x: 0, right: 0, fixed: false, colindex: c.colindex, subTarget: c.subTarget }));
|
||||
}
|
||||
|
||||
const resolveCol = (name) => {
|
||||
const suffix = ' / ' + name;
|
||||
const cand = h => [h.text, h.title].filter(Boolean);
|
||||
return headers.find(h => cand(h).some(t => lo(t) === lo(name)))
|
||||
|| headers.find(h => cand(h).some(t => t.endsWith(suffix)))
|
||||
|| headers.find(h => cand(h).some(t => lo(t).includes(lo(name))));
|
||||
};
|
||||
// Columns + name→column + column→cell — single source of truth in dom/_shared.mjs,
|
||||
// shared with readTable, so a name it reports (incl. expanded «Имя 1/2/3») is clickable.
|
||||
const model = buildColumnModel(grid);
|
||||
const headers = model.columns;
|
||||
|
||||
const targetCol = ${JSON.stringify(column)};
|
||||
const col = resolveCol(targetCol);
|
||||
const col = resolveColumnByName(model, targetCol);
|
||||
if (!col) return { error: 'column_not_found', column: targetCol, available: headers.map(h => h.name) };
|
||||
|
||||
const lines = [...body.querySelectorAll('.gridLine')];
|
||||
if (lines.length === 0) return { error: 'empty_grid' };
|
||||
|
||||
// Match cell to column by X overlap, but only among cells with the same
|
||||
// fixed/scrollable kind as the header. After horizontal scroll a scrollable
|
||||
// cell may have the same x as a frozen one — without this guard cellAtColX
|
||||
// would silently return the frozen cell for a scrollable header.
|
||||
const cellAtColX = (line, c) => {
|
||||
// Headerless columns carry colindex → match the body cell directly (robust,
|
||||
// and returns the same box for both logical columns of a combined mark-box).
|
||||
if (c.colindex != null) {
|
||||
return [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === c.colindex);
|
||||
}
|
||||
return [...line.children]
|
||||
.filter(b => b.offsetWidth > 0 && b.classList.contains('gridBoxFix') === c.fixed)
|
||||
.find(b => {
|
||||
const r = b.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
return cx >= c.x && cx < c.right;
|
||||
});
|
||||
};
|
||||
const cellAtColX = (line, c) => cellForColumn(model, line, c);
|
||||
const cellText = (b) => norm(b?.querySelector('.gridBoxText')?.innerText || b?.innerText || '');
|
||||
|
||||
const target = ${JSON.stringify(row)};
|
||||
@@ -738,7 +559,7 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
||||
const entries = Object.entries(target);
|
||||
const colsByKey = {};
|
||||
for (const [k] of entries) {
|
||||
const c = resolveCol(k);
|
||||
const c = resolveColumnByName(model, k);
|
||||
if (!c) return { error: 'filter_column_not_found', column: k, available: headers.map(h => h.name) };
|
||||
colsByKey[k] = c;
|
||||
}
|
||||
|
||||
@@ -252,7 +252,11 @@ console.log('Расшифровка:', JSON.stringify(drilldown.rows));
|
||||
|
||||
#### readTable — подробнее
|
||||
|
||||
Каждая строка — объект `{ columnName: value }`. Специальные поля для иерархии и дерева:
|
||||
Каждая строка — объект `{ columnName: value }`.
|
||||
|
||||
**Имена из `columns` годятся везде** — `clickElement({ row, column })`, `fillTableRow`, `filterList` и фильтры строки `{ колонка: значение }` разрешают колонку так же, как её назвал `readTable`. В том числе на многострочных шапках: шапка над несколькими под-рядами выводится (и адресуется) как `'Имя 1'`, `'Имя 2'`, … — например, грид проводок даёт `'Субконто Дт 1'..'Субконто Дт 3'`.
|
||||
|
||||
Специальные поля для иерархии и дерева:
|
||||
|
||||
- `_kind: 'group'` — группа в иерархическом списке
|
||||
- `_tree: 'expanded'|'collapsed'` — состояние узла дерева
|
||||
|
||||
@@ -38,6 +38,15 @@ export default async function({ navigateSection, openCommand, clickElement, clos
|
||||
// и потеряет состояние. Гард ниже — через DOM: колонка безымянная и в columns не
|
||||
// попадает (её наличие readTable определяет по первой строке, а там Posted=false →
|
||||
// картинки нет), поэтому проверить её через публичный API нельзя.
|
||||
// Колонка безымянная (titleLocation:none) → readTable называет её '(picture)'. Её наличие
|
||||
// определяется сэмплированием НЕСКОЛЬКИХ строк: picField над Posted не рисует картинку при
|
||||
// Ложь, а первая строка списка — непроведённый документ. По одной первой строке колонка
|
||||
// выпадала из columns целиком.
|
||||
const tt = await readTable({ maxRows: 50 });
|
||||
assert.includes(tt.columns, '(picture)', 'безымянная picture-колонка есть в columns');
|
||||
const postedRow = tt.rows.find(r => r['Комментарий'] === 'StatePosted');
|
||||
assert.equal(postedRow['(picture)'], 'pic:0', 'у проведённого документа картинка нарисована');
|
||||
|
||||
const page = await getPage();
|
||||
const icons = await page.evaluate(() => {
|
||||
const grid = [...document.querySelectorAll('.grid')].find(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
||||
|
||||
@@ -13,7 +13,20 @@ export const timeout = 120000;
|
||||
// шапок у них НЕТ. Здесь разворот в «Субконто 1/2/3» — правильное поведение, его
|
||||
// обязана сохранить любая правка.
|
||||
|
||||
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, fillTableRow, assert, step, log }) {
|
||||
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, fillTableRow, getPage, assert, step, log }) {
|
||||
|
||||
// Куда клик попал НА САМОМ ДЕЛЕ. Результат clickElement возвращает запрошенное имя колонки
|
||||
// (эхо), а не разрешённую ячейку, — по нему промах неотличим от попадания. Нажатая ячейка
|
||||
// помечается в DOM классами select+focus.
|
||||
const focusedCell = async () => {
|
||||
const page = await getPage();
|
||||
return page.evaluate(() => {
|
||||
const grid = [...document.querySelectorAll('.grid')].find(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
||||
const box = [...grid.querySelectorAll('.gridBody [colindex]')]
|
||||
.find(b => b.classList.contains('select') && b.classList.contains('focus'));
|
||||
return box ? { ci: box.getAttribute('colindex'), text: (box.innerText || '').trim() } : null;
|
||||
});
|
||||
};
|
||||
|
||||
await step('setup: открыть обработку', async () => {
|
||||
await navigateSection('Склад');
|
||||
@@ -53,24 +66,39 @@ export default async function({ navigateSection, openCommand, clickElement, clos
|
||||
assert.equal(r['Субконто 3'], 'Субконто3 1', 'Субконто 3');
|
||||
});
|
||||
|
||||
await step('click: clickElement({row, column}) попадает в нужную колонку', async () => {
|
||||
await step('click: clickElement({row, column}) попадает в нужную ячейку', async () => {
|
||||
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Срок' });
|
||||
log(`clicked: ${JSON.stringify(res.clicked)}`);
|
||||
assert.equal(res.clicked?.kind, 'gridCell', 'kind=gridCell');
|
||||
assert.equal(res.clicked?.column, 'Срок', 'column=Срок');
|
||||
// Проверяем факт, а не эхо: до правки клик молча уходил в «Исполнитель 2» и рапортовал успех.
|
||||
const cell = await focusedCell();
|
||||
log(`focused: ${JSON.stringify(cell)}`);
|
||||
assert.ok(cell, 'нажатая ячейка помечена в DOM');
|
||||
assert.equal(cell.text, 'Срок 2', `клик попал в ячейку «Срок» строки К2, а не в соседнюю (got «${cell.text}»)`);
|
||||
});
|
||||
|
||||
await step('click: имя колонки из readTable пригодно для клика (единство именования)', async () => {
|
||||
const t = await readTable();
|
||||
assert.includes(t.columns, 'Субконто 2', 'имя развёрнутой колонки из readTable');
|
||||
const res = await clickElement({ row: { 'Код': 'К2' }, column: 'Субконто 2' });
|
||||
log(`clicked: ${JSON.stringify(res.clicked)}`);
|
||||
assert.equal(res.clicked?.kind, 'gridCell', 'по имени из readTable клик находит ячейку');
|
||||
const cell = await focusedCell();
|
||||
log(`focused: ${JSON.stringify(cell)}`);
|
||||
assert.equal(cell?.text, 'Субконто2 2', `клик попал во второй под-ряд «Субконто» (got «${cell?.text}»)`);
|
||||
});
|
||||
|
||||
await step('fill: fillTableRow пишет значение в нужную колонку', async () => {
|
||||
// Путь записи ищет ячейку по colindex (grid-edit.mjs), поэтому ожидается зелёным
|
||||
// и до правки резолверов чтения/клика — это замер, а не регресс.
|
||||
await step('row-filter: строка ищется по «украденной» колонке', async () => {
|
||||
// Фильтр строки идёт через свой резолвер (findRowInGridScript) — тоже геометрия по x,
|
||||
// причём без Y-логики. Ищем строку по «Срок», чью ячейку ворует широкий «Исполнитель».
|
||||
const res = await clickElement({ row: { 'Срок': 'Срок 2' }, column: 'Код' });
|
||||
assert.equal(res.clicked?.kind, 'gridCell', 'строка найдена по значению узкой колонки');
|
||||
const cell = await focusedCell();
|
||||
log(`focused: ${JSON.stringify(cell)}`);
|
||||
assert.equal(cell?.text, 'К2', `найдена именно строка К2 (got «${cell?.text}»)`);
|
||||
});
|
||||
|
||||
await step('fill: fillTableRow пишет в колонку со своей шапкой', async () => {
|
||||
// Путь записи ищет ячейку по colindex (grid-edit.mjs) — ожидается зелёным и до правки
|
||||
// резолверов чтения/клика. Это замер, а не регресс.
|
||||
await fillTableRow({ 'Срок': 'Срок изменён' }, { row: { 'Код': 'К3' } });
|
||||
const t = await readTable();
|
||||
const row = t.rows.find(r => r['Код'] === 'К3');
|
||||
@@ -80,6 +108,19 @@ export default async function({ navigateSection, openCommand, clickElement, clos
|
||||
assert.equal(row['Выполнена'], 'Выполнена 3', 'соседняя узкая колонка не задета');
|
||||
});
|
||||
|
||||
await step('fill: fillTableRow пишет в РАЗВЁРНУТУЮ колонку (шапки у ячейки нет)', async () => {
|
||||
// «Субконто 2» — под-ряд объединённой шапки: своей шапки у ячейки нет, у группы ci=6,
|
||||
// а ячейки идут ci=7/8/9. Проверяем, что запись попадает во второй под-ряд, а не в
|
||||
// первый и не в соседа.
|
||||
await fillTableRow({ 'Субконто 2': 'СК2 изменён' }, { row: { 'Код': 'К1' } });
|
||||
const t = await readTable();
|
||||
const row = t.rows.find(r => r['Код'] === 'К1');
|
||||
log(`after fill: ${JSON.stringify(row)}`);
|
||||
assert.equal(row['Субконто 2'], 'СК2 изменён', 'значение попало в «Субконто 2»');
|
||||
assert.equal(row['Субконто 1'], 'Субконто1 1', '«Субконто 1» не задета');
|
||||
assert.equal(row['Субконто 3'], 'Субконто3 1', '«Субконто 3» не задета');
|
||||
});
|
||||
|
||||
await step('cleanup: закрыть форму', async () => {
|
||||
await closeForm();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user