mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-14 09:54:56 +03:00
feat(web-test): распознавание колонок-картинок в readTable
readTable теперь отдаёт картиночные ячейки как 'pic:<N>' (есть иконка) или '' (нет): N — индекс кадра спрайта, кодирующий состояние. Присутствие читается как truthy, разные иконки различаются по индексу. Безымянные картиночные колонки (напр. индикатор присоединённых файлов) больше не выпадают — именуются по title-тултипу, fallback '(picture)'. - dom/grid.mjs: helper picInfo (парсинг gx из pictureCollection url, исключение декоративных иконок дерева/групп); ветка пустого заголовка добавляет картиночные колонки; resolveCol резолвит колонку по тексту И по title (клик по картиночной колонке). - click-cell.mjs: fail-fast при попытке отбора строки по 'pic:N' — понятная ошибка вместо row_not_found (картинки read/assert-only). - SKILL.md: компактный раздел про картиночные колонки. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,12 @@ Read actual grid data with pagination. Each row is `{ columnName: value }`.
|
||||
| `offset` | 0 | Skip first N rows |
|
||||
| `table` | — | Grid name from `tables[]` (for multi-grid forms) |
|
||||
|
||||
**Picture columns.** Cells that render an icon (status/stage marks, the "ЭДО" mark, the attached-files paperclip) read as `'pic:<N>'` (`N` = icon frame/state) when shown, `''` when absent — so presence is truthy and icons differ by index. Icon-only columns (no header text) still appear, named by their tooltip or `'(picture)'`. These values are read-only — filter/select rows by a text column, not by `'pic:N'`.
|
||||
```js
|
||||
if (t.rows[0]['Присоединенные файлы']) { /* has an attached file */ }
|
||||
t.rows[0]['ЭДО'] === 'pic:1'; // connected to 1С-ЭДО ('pic:0' = not)
|
||||
```
|
||||
|
||||
Special row fields:
|
||||
- `_kind: 'group'` — hierarchical group row
|
||||
- `_kind: 'parent'` — parent row in hierarchy
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom/grid v1.7 — grid resolution + table reading + edit-time helpers
|
||||
// web-test dom/grid v1.8 — grid resolution + table reading + edit-time helpers
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
/**
|
||||
@@ -79,6 +79,21 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
if (!grid) return { error: 'no_table', message: 'No table found on form ${formNum}' };
|
||||
const name = grid.id ? grid.id.replace(p, '') : '';
|
||||
|
||||
// Detect a "picture value" cell: a sprite from a picture collection
|
||||
// (.gridBoxImg .dIB with background-image .../pictureCollection/picture/<id>?...&gx=<N>).
|
||||
// Excludes decorative tree/group markers (gridListH/gridListV/[tree]/gridBoxTree).
|
||||
// Returns { gx } — the sprite frame index that encodes the cell state, or null.
|
||||
function picInfo(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' };
|
||||
}
|
||||
|
||||
// DOM-based parsing: gridHead → columns, gridBody → gridLine rows → gridBox cells
|
||||
const head = grid.querySelector('.gridHead');
|
||||
const body = grid.querySelector('.gridBody');
|
||||
@@ -98,16 +113,26 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
const textEl = box.querySelector('.gridBoxText');
|
||||
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||
if (!text) {
|
||||
// Unnamed column — check if data cells contain checkboxes
|
||||
// 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');
|
||||
if (firstLine) {
|
||||
const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0);
|
||||
const idx = visibleHeaders.indexOf(box);
|
||||
const cells = [...firstLine.children].filter(c => c.offsetWidth > 0);
|
||||
if (cells[idx]?.querySelector('.checkbox')) {
|
||||
const r = box.getBoundingClientRect();
|
||||
columns.push({ text: '(checkbox)', x: r.x, w: r.width, right: r.x + r.width, y: r.y, h: r.height });
|
||||
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;
|
||||
}
|
||||
@@ -210,7 +235,13 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
val = chk.classList.contains('select') ? 'true' : 'false';
|
||||
} else {
|
||||
val = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||
if (!val) return;
|
||||
if (!val) {
|
||||
// Empty text → maybe a picture cell. 'pic:<gx>' encodes the sprite frame
|
||||
// (state). Absent picture stays '' (truthy check distinguishes presence).
|
||||
const pic = picInfo(box);
|
||||
if (pic) val = 'pic:' + pic.gx;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
// Match cell to column by X+Y overlap (multi-row aware)
|
||||
const r = box.getBoundingClientRect();
|
||||
@@ -448,21 +479,25 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
||||
.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, x: r.x, right: r.x + r.width, fixed: c.classList.contains('gridBoxFix') };
|
||||
return { text, title, name: text || title, x: r.x, right: r.x + r.width, fixed: c.classList.contains('gridBoxFix') };
|
||||
})
|
||||
.filter(h => h.text);
|
||||
.filter(h => h.name);
|
||||
|
||||
const resolveCol = (name) => {
|
||||
const suffix = ' / ' + name;
|
||||
return headers.find(h => lo(h.text) === lo(name))
|
||||
|| headers.find(h => h.text.endsWith(suffix))
|
||||
|| headers.find(h => lo(h.text).includes(lo(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))));
|
||||
};
|
||||
|
||||
const targetCol = ${JSON.stringify(column)};
|
||||
const col = resolveCol(targetCol);
|
||||
if (!col) return { error: 'column_not_found', column: targetCol, available: headers.map(h => h.text) };
|
||||
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' };
|
||||
@@ -493,7 +528,7 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
||||
const colsByKey = {};
|
||||
for (const [k] of entries) {
|
||||
const c = resolveCol(k);
|
||||
if (!c) return { error: 'filter_column_not_found', column: k, available: headers.map(h => h.text) };
|
||||
if (!c) return { error: 'filter_column_not_found', column: k, available: headers.map(h => h.name) };
|
||||
colsByKey[k] = c;
|
||||
}
|
||||
const matches = (ln) => {
|
||||
@@ -516,7 +551,7 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
||||
}
|
||||
|
||||
const cell = cellAtColX(line, col);
|
||||
if (!cell) return { error: 'cell_not_in_dom', column: col.text, rowIdx };
|
||||
if (!cell) return { error: 'cell_not_in_dom', column: col.name, rowIdx };
|
||||
const r = cell.getBoundingClientRect();
|
||||
const gridBox = grid.getBoundingClientRect();
|
||||
// Frozen columns (.gridBoxFix) stay pinned at the left edge of the grid even
|
||||
@@ -544,7 +579,7 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
||||
cellX: Math.round(r.x), cellRight: Math.round(r.x + r.width),
|
||||
gridX: Math.round(gridBox.x), gridRight: Math.round(gridBox.x + gridBox.width),
|
||||
scrollableLeft: Math.round(scrollableLeft),
|
||||
columnText: col.text, rowIdx, isFixed,
|
||||
columnText: col.name, rowIdx, isFixed,
|
||||
cellText: cellText(cell),
|
||||
visible
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test table/click-cell v1.2 — click a cell in a form grid by (row, column).
|
||||
// web-test table/click-cell v1.3 — click a cell in a form grid by (row, column).
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// Routed from core/click.mjs when the user calls clickElement({row, column}) and
|
||||
@@ -47,6 +47,17 @@ const FOCUS_WAIT_MS = 150;
|
||||
export async function clickGridCell(target, ctx) {
|
||||
const { formNum, gridSelector, gridName, modifier, dblclick, scroll } = ctx;
|
||||
|
||||
// Guard: a 'pic:N' filter value is a readTable picture token, not real cell text.
|
||||
// Picture cells render an icon (no text), so they can't select a row — fail fast
|
||||
// with guidance instead of a confusing 'row_not_found'.
|
||||
if (target?.row && typeof target.row === 'object') {
|
||||
for (const [k, v] of Object.entries(target.row)) {
|
||||
if (typeof v === 'string' && /^pic:\d+$/.test(v.trim())) {
|
||||
throw new Error(`clickElement: "${v}" is a readTable picture value (column "${k}"), not selectable text — it can't be used as a row filter. Filter by a text column (e.g. name/number) instead.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Try to find the cell in current DOM window.
|
||||
let cell = await page.evaluate(findGridCellScript(formNum, gridSelector, target));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user