From 91b5204ab2b5a9b96eafa6dde622292e8176fb2e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 14 Mar 2026 12:05:23 +0300 Subject: [PATCH] feat(web-test): add visual label support for multi-grid tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/skills/web-test/SKILL.md | 8 ++++---- .claude/skills/web-test/scripts/dom.mjs | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 402c1c83..7c9b989d 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -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`) diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index 36164300..4d560022 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -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 })) }; })()`; }