diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index df37a757..84d8563a 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -171,6 +171,7 @@ Special row fields: - `_kind: 'parent'` — parent row in hierarchy - `_tree: 'expanded'|'collapsed'` — tree node state - `_level: N` — nesting depth in tree view +- `_selected: true` — row is selected (highlighted). Use with `clickElement({ modifier: 'ctrl'|'shift' })` to verify multi-selection - `hierarchical: true` — list has groups (on result object) - `viewMode: 'tree'` — tree view active (on result object) @@ -208,7 +209,7 @@ Sections + all open tabs. ### Actions -#### `clickElement(text, { dblclick?, table?, expand? })` → form state +#### `clickElement(text, { dblclick?, table?, expand?, modifier? })` → form state Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match). - `table` — scope button search to a specific grid's command panel (by name from `tables[]`): @@ -230,6 +231,15 @@ Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match). await clickElement('ИСУ ФХД'); // select row await clickElement('ИСУ ФХД', { expand: true }); // expand/collapse ``` +- **Multi-select rows** with `modifier: 'ctrl'` (add to selection) or `modifier: 'shift'` (select range): + ```js + await clickElement('Номенклатура 1'); // select first row + await clickElement('Номенклатура 2', { modifier: 'ctrl' }); // add to selection + await clickElement('Номенклатура 5', { modifier: 'shift' }); // select range 2..5 + // Verify selection: + const t = await readTable(); + t.rows.filter(r => r._selected); // rows with _selected: true + ``` #### `fillFields({ name: value })` → `{ filled, form }` Fill form fields by label (fuzzy match). Auto-detects field type. diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index ca6c07e5..a06724df 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -1,4 +1,4 @@ -// web-test browser v1.5 — Playwright browser management for 1C web client +// web-test browser v1.6 — Playwright browser management for 1C web client // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Playwright browser management for 1C web client. @@ -1928,7 +1928,7 @@ export async function fillField(name, value) { } /** Click a button/hyperlink/tab on the current form. Use {dblclick: true} to double-click (open items from lists). */ -export async function clickElement(text, { dblclick, table, toggle, expand, timeout } = {}) { +export async function clickElement(text, { dblclick, table, toggle, expand, modifier, timeout } = {}) { ensureConnected(); await dismissPendingErrors(); if (highlightMode) try { await highlight(text, { table }); await page.waitForTimeout(500); await unhighlight(); } catch {} @@ -2035,6 +2035,19 @@ export async function clickElement(text, { dblclick, table, toggle, expand, time } if (target?.error) throw new Error(`clickElement: "${text}" not found. Available: ${target.available?.join(', ') || 'none'}`); + // Helper: click with optional modifier key (Ctrl/Shift for multi-select) + const modKey = modifier === 'ctrl' ? 'Control' : modifier === 'shift' ? 'Shift' : null; + async function modClick(x, y) { + if (modKey) await page.keyboard.down(modKey); + await page.mouse.click(x, y); + if (modKey) await page.keyboard.up(modKey); + } + async function modDblClick(x, y) { + if (modKey) await page.keyboard.down(modKey); + await page.mouse.dblclick(x, y); + if (modKey) await page.keyboard.up(modKey); + } + // Grid row targets — use coordinate click (single or double) if (target.kind === 'gridGroup' || target.kind === 'gridParent') { if (expand != null || toggle) { @@ -2066,23 +2079,23 @@ export async function clickElement(text, { dblclick, table, toggle, expand, time || (expand === false && levelIconInfo.isExpanded); if (shouldClick) { if (levelIconInfo) { - await page.mouse.click(levelIconInfo.x, levelIconInfo.y); + await modClick(levelIconInfo.x, levelIconInfo.y); } else { // Fallback: dblclick (standard hierarchy navigation) - await page.mouse.dblclick(target.x, target.y); + await modDblClick(target.x, target.y); } } await waitForStable(formNum); const state = await getFormState(); - state.clicked = { kind: target.kind, name: target.name, toggled: shouldClick }; + state.clicked = { kind: target.kind, name: target.name, toggled: shouldClick, ...(modifier ? { modifier } : {}) }; state.hint = shouldClick ? 'Group toggled. Use readTable to see updated list.' : 'Group already in desired state.'; return state; } // Default: dblclick to enter group / go up to parent - await page.mouse.dblclick(target.x, target.y); + await modDblClick(target.x, target.y); await waitForStable(formNum); const state = await getFormState(); - state.clicked = { kind: target.kind, name: target.name }; + state.clicked = { kind: target.kind, name: target.name, ...(modifier ? { modifier } : {}) }; return state; } if (target.kind === 'gridTreeNode') { @@ -2116,38 +2129,38 @@ export async function clickElement(text, { dblclick, table, toggle, expand, time || (expand === false && treeIconInfo.isExpanded); if (shouldClick) { if (treeIconInfo) { - await page.mouse.click(treeIconInfo.x, treeIconInfo.y); + await modClick(treeIconInfo.x, treeIconInfo.y); } else { // Fallback: dblclick on row (works for trees without clickable +/- icons) - await page.mouse.dblclick(target.x, target.y); + await modDblClick(target.x, target.y); } } await waitForStable(formNum); const state = await getFormState(); - state.clicked = { kind: 'gridTreeNode', name: target.name, toggled: shouldClick }; + state.clicked = { kind: 'gridTreeNode', name: target.name, toggled: shouldClick, ...(modifier ? { modifier } : {}) }; state.hint = shouldClick ? 'Tree node toggled. Use readTable to see updated tree.' : 'Tree node already in desired state.'; return state; } // Default: select row (click text, no expand/collapse) - await page.mouse.click(target.x, target.y); + await modClick(target.x, target.y); await waitForStable(formNum); const state = await getFormState(); - state.clicked = { kind: 'gridTreeNode', name: target.name }; + state.clicked = { kind: 'gridTreeNode', name: target.name, ...(modifier ? { modifier } : {}) }; state.hint = 'Row selected. Use { expand: true } to expand/collapse.'; return state; } if (target.kind === 'gridRow') { if (dblclick) { - await page.mouse.dblclick(target.x, target.y); + await modDblClick(target.x, target.y); await waitForStable(); const state = await getFormState(); - state.clicked = { kind: 'gridRow', name: target.name, dblclick: true }; + state.clicked = { kind: 'gridRow', name: target.name, dblclick: true, ...(modifier ? { modifier } : {}) }; return state; } - await page.mouse.click(target.x, target.y); + await modClick(target.x, target.y); await waitForStable(); const state = await getFormState(); - state.clicked = { kind: 'gridRow', name: target.name }; + state.clicked = { kind: 'gridRow', name: target.name, ...(modifier ? { modifier } : {}) }; return state; } diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index 9d5ab904..ef8c3ebc 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -1,4 +1,4 @@ -// web-test dom v1.2 — DOM selectors and semantic mapping for 1C web client +// web-test dom v1.3 — DOM selectors and semantic mapping for 1C web client // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * DOM selectors and semantic mapping for 1C:Enterprise web client. @@ -583,6 +583,8 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto } row._level = imgBox ? imgBox.querySelectorAll('.dIB').length - 1 : 0; } + // Selection state: selRow = selected row in grid + if (line.classList.contains('selRow') || line.classList.contains('select')) row._selected = true; rows.push(row); } const isTree = !!body.querySelector('.gridBoxTree'); diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index 54134a90..a0ba1e8e 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -236,13 +236,14 @@ await closeForm({ save: false }); - `_kind: 'group'` — группа в иерархическом списке - `_tree: 'expanded'|'collapsed'` — состояние узла дерева - `_level: N` — уровень вложенности +- `_selected: true` — строка выделена (подсвечена). Используйте с `clickElement({ modifier: 'ctrl'|'shift' })` для проверки мультиселекции - На объекте результата: `hierarchical: true`, `viewMode: 'tree'` ### Действия | Функция | Описание | Возвращает | |---------|----------|------------| -| `clickElement(text, {dblclick?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия из списка | form state или `{ submenu }` | +| `clickElement(text, {dblclick?, modifier?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия, `{modifier: 'ctrl'\|'shift'}` для мультиселекции | form state или `{ submenu }` | | `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры). Пустое значение (`''`/`null`) = очистка | `{ filled: [{field, ok, method}], form }` | | `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст, `{поле: значение}` или `''`/`null` для очистки. `{ type }` для составного типа | form state с `selected` | | `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state |