mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-28 22:19:41 +03:00
feat(web-test): multi-select rows with modifier + _selected in readTable
Add modifier option ('ctrl'|'shift') to clickElement for Ctrl+click
(add to selection) and Shift+click (select range) in grid rows.
Add _selected: true flag to readTable rows so the model can verify
which rows are currently selected before performing actions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
506f0b84df
commit
6c01f3a261
@@ -171,6 +171,7 @@ Special row fields:
|
|||||||
- `_kind: 'parent'` — parent row in hierarchy
|
- `_kind: 'parent'` — parent row in hierarchy
|
||||||
- `_tree: 'expanded'|'collapsed'` — tree node state
|
- `_tree: 'expanded'|'collapsed'` — tree node state
|
||||||
- `_level: N` — nesting depth in tree view
|
- `_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)
|
- `hierarchical: true` — list has groups (on result object)
|
||||||
- `viewMode: 'tree'` — tree view active (on result object)
|
- `viewMode: 'tree'` — tree view active (on result object)
|
||||||
|
|
||||||
@@ -208,7 +209,7 @@ Sections + all open tabs.
|
|||||||
|
|
||||||
### Actions
|
### 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).
|
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[]`):
|
- `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('ИСУ ФХД'); // select row
|
||||||
await clickElement('ИСУ ФХД', { expand: true }); // expand/collapse
|
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 }`
|
#### `fillFields({ name: value })` → `{ filled, form }`
|
||||||
Fill form fields by label (fuzzy match). Auto-detects field type.
|
Fill form fields by label (fuzzy match). Auto-detects field type.
|
||||||
|
|||||||
@@ -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
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Playwright browser management for 1C web client.
|
* 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). */
|
/** 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();
|
ensureConnected();
|
||||||
await dismissPendingErrors();
|
await dismissPendingErrors();
|
||||||
if (highlightMode) try { await highlight(text, { table }); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
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'}`);
|
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)
|
// Grid row targets — use coordinate click (single or double)
|
||||||
if (target.kind === 'gridGroup' || target.kind === 'gridParent') {
|
if (target.kind === 'gridGroup' || target.kind === 'gridParent') {
|
||||||
if (expand != null || toggle) {
|
if (expand != null || toggle) {
|
||||||
@@ -2066,23 +2079,23 @@ export async function clickElement(text, { dblclick, table, toggle, expand, time
|
|||||||
|| (expand === false && levelIconInfo.isExpanded);
|
|| (expand === false && levelIconInfo.isExpanded);
|
||||||
if (shouldClick) {
|
if (shouldClick) {
|
||||||
if (levelIconInfo) {
|
if (levelIconInfo) {
|
||||||
await page.mouse.click(levelIconInfo.x, levelIconInfo.y);
|
await modClick(levelIconInfo.x, levelIconInfo.y);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: dblclick (standard hierarchy navigation)
|
// Fallback: dblclick (standard hierarchy navigation)
|
||||||
await page.mouse.dblclick(target.x, target.y);
|
await modDblClick(target.x, target.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await waitForStable(formNum);
|
await waitForStable(formNum);
|
||||||
const state = await getFormState();
|
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.';
|
state.hint = shouldClick ? 'Group toggled. Use readTable to see updated list.' : 'Group already in desired state.';
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
// Default: dblclick to enter group / go up to parent
|
// 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);
|
await waitForStable(formNum);
|
||||||
const state = await getFormState();
|
const state = await getFormState();
|
||||||
state.clicked = { kind: target.kind, name: target.name };
|
state.clicked = { kind: target.kind, name: target.name, ...(modifier ? { modifier } : {}) };
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
if (target.kind === 'gridTreeNode') {
|
if (target.kind === 'gridTreeNode') {
|
||||||
@@ -2116,38 +2129,38 @@ export async function clickElement(text, { dblclick, table, toggle, expand, time
|
|||||||
|| (expand === false && treeIconInfo.isExpanded);
|
|| (expand === false && treeIconInfo.isExpanded);
|
||||||
if (shouldClick) {
|
if (shouldClick) {
|
||||||
if (treeIconInfo) {
|
if (treeIconInfo) {
|
||||||
await page.mouse.click(treeIconInfo.x, treeIconInfo.y);
|
await modClick(treeIconInfo.x, treeIconInfo.y);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: dblclick on row (works for trees without clickable +/- icons)
|
// 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);
|
await waitForStable(formNum);
|
||||||
const state = await getFormState();
|
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.';
|
state.hint = shouldClick ? 'Tree node toggled. Use readTable to see updated tree.' : 'Tree node already in desired state.';
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
// Default: select row (click text, no expand/collapse)
|
// 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);
|
await waitForStable(formNum);
|
||||||
const state = await getFormState();
|
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.';
|
state.hint = 'Row selected. Use { expand: true } to expand/collapse.';
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
if (target.kind === 'gridRow') {
|
if (target.kind === 'gridRow') {
|
||||||
if (dblclick) {
|
if (dblclick) {
|
||||||
await page.mouse.dblclick(target.x, target.y);
|
await modDblClick(target.x, target.y);
|
||||||
await waitForStable();
|
await waitForStable();
|
||||||
const state = await getFormState();
|
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;
|
return state;
|
||||||
}
|
}
|
||||||
await page.mouse.click(target.x, target.y);
|
await modClick(target.x, target.y);
|
||||||
await waitForStable();
|
await waitForStable();
|
||||||
const state = await getFormState();
|
const state = await getFormState();
|
||||||
state.clicked = { kind: 'gridRow', name: target.name };
|
state.clicked = { kind: 'gridRow', name: target.name, ...(modifier ? { modifier } : {}) };
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* DOM selectors and semantic mapping for 1C:Enterprise web client.
|
* 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;
|
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);
|
rows.push(row);
|
||||||
}
|
}
|
||||||
const isTree = !!body.querySelector('.gridBoxTree');
|
const isTree = !!body.querySelector('.gridBoxTree');
|
||||||
|
|||||||
@@ -236,13 +236,14 @@ await closeForm({ save: false });
|
|||||||
- `_kind: 'group'` — группа в иерархическом списке
|
- `_kind: 'group'` — группа в иерархическом списке
|
||||||
- `_tree: 'expanded'|'collapsed'` — состояние узла дерева
|
- `_tree: 'expanded'|'collapsed'` — состояние узла дерева
|
||||||
- `_level: N` — уровень вложенности
|
- `_level: N` — уровень вложенности
|
||||||
|
- `_selected: true` — строка выделена (подсвечена). Используйте с `clickElement({ modifier: 'ctrl'|'shift' })` для проверки мультиселекции
|
||||||
- На объекте результата: `hierarchical: true`, `viewMode: 'tree'`
|
- На объекте результата: `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 }` |
|
| `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры). Пустое значение (`''`/`null`) = очистка | `{ filled: [{field, ok, method}], form }` |
|
||||||
| `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст, `{поле: значение}` или `''`/`null` для очистки. `{ type }` для составного типа | form state с `selected` |
|
| `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст, `{поле: значение}` или `''`/`null` для очистки. `{ type }` для составного типа | form state с `selected` |
|
||||||
| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state |
|
| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state |
|
||||||
|
|||||||
Reference in New Issue
Block a user