feat(web-test): add visual label support for multi-grid tables

Extract group title text from #title_div DOM elements so tables can be
referenced by their visible on-screen names (e.g. "Входящие") in addition
to technical attribute names. Labels appear in getFormState().tables[] and
resolveGridScript cascade matching (exact name → exact label → contains).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-14 12:05:23 +03:00
parent f2bd42c54c
commit 91b5204ab2
2 changed files with 19 additions and 9 deletions
+4 -4
View File
@@ -123,7 +123,7 @@ Returns current form structure. This is the primary way to understand what's on
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields)
**tables** — array of all visible grids: `[{ name, columns, rowCount }]`. Use `readTable()` for actual data.
**tables** — array of all visible grids: `[{ name, columns, rowCount, label? }]`. `label` is the visual group title shown on screen (e.g. "Входящие"), absent when grid has no visible title. Use `readTable()` for actual data.
**table** — backward-compatible alias for the first grid: `{ present, columns, rowCount }`.
@@ -387,8 +387,8 @@ Some forms have multiple grids (e.g. "Входящие" and "Исходящие"
const form = await getFormState();
// form.tables = [
// { name: "ДеревоБизнесПроцессов", columns: ["Полный код", "Бизнес-процесс"], rowCount: 21 },
// { name: "Входящие", columns: ["Объект", "Бизнес-процесс источник", ...], rowCount: 1 },
// { name: "Исходящие", columns: ["Объект", "Бизнес-процесс приемник", ...], rowCount: 1 }
// { name: "Входящие", label: "Входящие", columns: ["Объект", "Бизнес-процесс источник", ...], rowCount: 1 },
// { name: "Исходящие", label: "Исходящие", columns: ["Объект", "Бизнес-процесс приемник", ...], rowCount: 1 }
// ]
```
@@ -407,7 +407,7 @@ await clickElement('Добавить', { table: 'Входящие' });
await deleteTableRow(0, { table: 'Исходящие' });
```
Table name matching is fuzzy: `'Исходящие'` matches grid id `form1_Исходящие`. If the grid id is technical (e.g. `ТаблицаТоваров`), use that name — it's from `tables[].name`, not the visual label.
Table matching accepts both technical name (`tables[].name`) and visual label (`tables[].label`). Label is the group title shown on screen — useful when working from screenshots. Name match takes priority over label match.
### Keyboard shortcuts (via `page.keyboard.press`)
+15 -5
View File
@@ -204,7 +204,10 @@ const READ_FORM_FN = `function readForm(p) {
});
}
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
return { name, columns, rowCount };
// Visual label from group title (e.g. "Входящие:" for grid "Входящие")
const titleEl = document.getElementById(p + name + '#title_div');
const label = titleEl ? (titleEl.innerText?.trim().replace(/:\\s*$/, '').replace(/\\u00a0/g, ' ') || null) : null;
return { name, columns, rowCount, ...(label ? { label } : {}) };
});
result.tables = tables;
// Backward compat: table = first grid summary
@@ -391,13 +394,20 @@ export function resolveGridScript(formNum, tableName) {
if (text) columns.push(text);
});
}
return { idx, gridId, gridName, columns, el: g };
// Visual label from group title element
const titleEl = document.getElementById(p + gridName + '#title_div');
const label = titleEl ? (titleEl.innerText?.trim().replace(/:\s*$/, '').replace(/\u00a0/g, ' ') || '') : '';
return { idx, gridId, gridName, label, columns, el: g };
});
// 1. Exact gridName match (case-insensitive)
let found = infos.find(i => norm(i.gridName).toLowerCase() === target);
// 2. gridName contains target
// 2. Exact label match
if (!found) found = infos.find(i => i.label && norm(i.label).toLowerCase() === target);
// 3. gridName contains target
if (!found) found = infos.find(i => norm(i.gridName).toLowerCase().includes(target));
// 3. Any column contains target
// 4. Label contains target
if (!found) found = infos.find(i => i.label && norm(i.label).toLowerCase().includes(target));
// 5. Any column contains target
if (!found) found = infos.find(i => i.columns.some(c => norm(c).toLowerCase().includes(target)));
if (found) {
return {
@@ -411,7 +421,7 @@ export function resolveGridScript(formNum, tableName) {
return {
error: 'not_found',
message: 'Table "' + ${JSON.stringify(tableName)} + '" not found',
available: infos.map(i => ({ name: i.gridName, columns: i.columns }))
available: infos.map(i => ({ name: i.gridName, ...(i.label ? { label: i.label } : {}), columns: i.columns }))
};
})()`;
}