mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-28 05:59:41 +03:00
Compare commits
6
Commits
a314ec32fc
...
6c01f3a261
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c01f3a261 | ||
|
|
506f0b84df | ||
|
|
f5c02144cb | ||
|
|
d982c5082a | ||
|
|
cce00a4def | ||
|
|
bc4ee63986 |
@@ -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.
|
||||||
@@ -240,6 +250,7 @@ Fill form fields by label (fuzzy match). Auto-detects field type.
|
|||||||
| `'5000'` | Plain text | Clipboard paste |
|
| `'5000'` | Plain text | Clipboard paste |
|
||||||
| `'true'` / `'да'` | Checkbox | Toggle |
|
| `'true'` / `'да'` | Checkbox | Toggle |
|
||||||
| `'Оплата поставщику'` | Radio | Fuzzy label match |
|
| `'Оплата поставщику'` | Radio | Fuzzy label match |
|
||||||
|
| `''` / `null` | Any (except checkbox/radio) | Clear via Shift+F4 |
|
||||||
|
|
||||||
**DCS report filters**: use human-readable label names. Checkbox is auto-enabled:
|
**DCS report filters**: use human-readable label names. Checkbox is auto-enabled:
|
||||||
```js
|
```js
|
||||||
@@ -250,10 +261,10 @@ await fillFields({
|
|||||||
```
|
```
|
||||||
|
|
||||||
Returns `{ filled: [{ field, ok, value, method }], form: {...} }`.
|
Returns `{ filled: [{ field, ok, value, method }], form: {...} }`.
|
||||||
Method is one of: `'toggle'` | `'radio'` | `'paste'` | `'dropdown'` | `'form'` | `'typeahead'`
|
Method is one of: `'clear'` | `'toggle'` | `'radio'` | `'paste'` | `'dropdown'` | `'form'` | `'typeahead'`
|
||||||
|
|
||||||
#### `selectValue(field, search, opts?)` → form state with `selected`
|
#### `selectValue(field, search, opts?)` → form state with `selected`
|
||||||
Select a value from reference field via dropdown or selection form. More reliable than `fillFields` for reference fields that need exact selection from a catalog.
|
Select a value from reference field via dropdown or selection form. More reliable than `fillFields` for reference fields that need exact selection from a catalog. Pass empty `search` (`''` or `null`) to clear the field (Shift+F4).
|
||||||
|
|
||||||
`search` — string for simple search, or `{ field: value }` object for per-field advanced search:
|
`search` — string for simple search, or `{ field: value }` object for per-field advanced search:
|
||||||
```js
|
```js
|
||||||
@@ -274,7 +285,7 @@ await selectValue('Документ', '0000-000601', { type: 'Реализаци
|
|||||||
Also supports DCS labels — auto-enables the paired checkbox.
|
Also supports DCS labels — auto-enables the paired checkbox.
|
||||||
|
|
||||||
#### `fillTableRow(fields, opts)` → form state
|
#### `fillTableRow(fields, opts)` → form state
|
||||||
Fill table row cells via Tab navigation. Value is a plain string or `{ value, type }` for composite-type cells.
|
Fill table row cells via Tab navigation. Value is a plain string, `{ value, type }` for composite-type cells, or `''`/`null` to clear (Shift+F4).
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
@@ -435,7 +446,7 @@ Table matching accepts both technical name (`tables[].name`) and visual label (`
|
|||||||
| Key | Context | Action |
|
| Key | Context | Action |
|
||||||
|-----|---------|--------|
|
|-----|---------|--------|
|
||||||
| `F8` | Reference field focused | Create new catalog item |
|
| `F8` | Reference field focused | Create new catalog item |
|
||||||
| `Shift+F4` | Reference field focused | Clear field value |
|
| `Shift+F4` | Any input field focused | Clear field value (auto via `''`/`null` in fillFields/selectValue/fillTableRow) |
|
||||||
| `F4` | Reference field focused | Open selection form |
|
| `F4` | Reference field focused | Open selection form |
|
||||||
| `Alt+F` | List/table form | Open advanced search dialog |
|
| `Alt+F` | List/table form | Open advanced search dialog |
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -313,6 +313,62 @@ async function waitForStable(previousFormNum = null) {
|
|||||||
// Fallback: max wait reached
|
// Fallback: max wait reached
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start monitoring network activity via CDP.
|
||||||
|
* Must be called BEFORE the click so it captures all server requests.
|
||||||
|
* Returns a monitor object with waitDone() and cleanup() methods.
|
||||||
|
*/
|
||||||
|
async function startNetworkMonitor() {
|
||||||
|
const client = await page.context().newCDPSession(page);
|
||||||
|
await client.send('Network.enable');
|
||||||
|
|
||||||
|
let pending = 0;
|
||||||
|
let total = 0;
|
||||||
|
let lastZeroTime = null;
|
||||||
|
const DEBOUNCE = 300;
|
||||||
|
|
||||||
|
client.on('Network.requestWillBeSent', () => {
|
||||||
|
pending++;
|
||||||
|
total++;
|
||||||
|
lastZeroTime = null;
|
||||||
|
});
|
||||||
|
client.on('Network.loadingFinished', () => {
|
||||||
|
if (--pending === 0) lastZeroTime = Date.now();
|
||||||
|
});
|
||||||
|
client.on('Network.loadingFailed', () => {
|
||||||
|
if (--pending === 0) lastZeroTime = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Wait until all network requests complete (300ms debounce) or UI element appears. */
|
||||||
|
async waitDone(timeout = 10000) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
|
||||||
|
// Check for UI elements (modal, balloon, confirm)
|
||||||
|
const ui = await page.evaluate(`(() => {
|
||||||
|
const modal = document.querySelector('#modalSurface:not([style*="display: none"])');
|
||||||
|
const balloon = document.querySelector('.balloon');
|
||||||
|
const confirm = document.querySelector('.confirm');
|
||||||
|
return !!(modal || balloon || confirm);
|
||||||
|
})()`);
|
||||||
|
if (ui) return;
|
||||||
|
|
||||||
|
// CDP debounce: pending===0 held for DEBOUNCE ms
|
||||||
|
if (total > 0 && pending === 0 && lastZeroTime !== null) {
|
||||||
|
if (Date.now() - lastZeroTime >= DEBOUNCE) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Detach CDP session. Always call this when done. */
|
||||||
|
async cleanup() {
|
||||||
|
await client.send('Network.disable').catch(() => {});
|
||||||
|
await client.detach().catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll until a JS expression returns truthy, or timeout (ms) expires.
|
* Poll until a JS expression returns truthy, or timeout (ms) expires.
|
||||||
* Resolves early — typically within 100-300ms instead of fixed delays.
|
* Resolves early — typically within 100-300ms instead of fixed delays.
|
||||||
@@ -1778,6 +1834,19 @@ export async function fillFields(fields) {
|
|||||||
await waitForStable();
|
await waitForStable();
|
||||||
}
|
}
|
||||||
const selector = `[id="${r.inputId}"]`;
|
const selector = `[id="${r.inputId}"]`;
|
||||||
|
// Clear field via Shift+F4 if value is empty (not applicable to checkbox/radio)
|
||||||
|
const rawValue = fields[r.field];
|
||||||
|
const isEmpty = rawValue === '' || rawValue === null || rawValue === undefined;
|
||||||
|
if (isEmpty && !r.isCheckbox && !r.isRadio) {
|
||||||
|
await page.click(selector);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await waitForStable();
|
||||||
|
results.push({ field: r.field, ok: true, value: '', method: 'clear' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (r.isCheckbox) {
|
if (r.isCheckbox) {
|
||||||
// Checkbox: compare desired with current, toggle if mismatch
|
// Checkbox: compare desired with current, toggle if mismatch
|
||||||
const desired = String(fields[r.field]).toLowerCase();
|
const desired = String(fields[r.field]).toLowerCase();
|
||||||
@@ -1859,10 +1928,11 @@ 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 } = {}) {
|
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 {}
|
||||||
|
let netMonitor = null;
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// First check if there's a confirmation dialog — click matching button
|
// First check if there's a confirmation dialog — click matching button
|
||||||
@@ -1965,6 +2035,19 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = {
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
@@ -1996,23 +2079,23 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = {
|
|||||||
|| (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') {
|
||||||
@@ -2046,41 +2129,47 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = {
|
|||||||
|| (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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start CDP network monitor BEFORE the click for buttons —
|
||||||
|
// so we capture all server requests triggered by the click.
|
||||||
|
if (target.kind === 'button') {
|
||||||
|
try { netMonitor = await startNetworkMonitor(); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
// Tabs without ID — use coordinate click to avoid global [data-content] ambiguity
|
// Tabs without ID — use coordinate click to avoid global [data-content] ambiguity
|
||||||
if (target.kind === 'tab' && !target.id && target.x && target.y) {
|
if (target.kind === 'tab' && !target.id && target.x && target.y) {
|
||||||
await page.mouse.click(target.x, target.y);
|
await page.mouse.click(target.x, target.y);
|
||||||
@@ -2148,15 +2237,11 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = {
|
|||||||
let n = f; while (n) { if (n.classList?.contains('grid')) return true; n = n.parentElement; }
|
let n = f; while (n) { if (n.classList?.contains('grid')) return true; n = n.parentElement; }
|
||||||
return false;
|
return false;
|
||||||
})()`);
|
})()`);
|
||||||
if (!inGridEdit) {
|
if (!inGridEdit && netMonitor) {
|
||||||
// Form didn't change — server might still be processing.
|
// Form didn't change — server might still be processing.
|
||||||
// waitForSelector uses MutationObserver internally — doesn't block event loop.
|
// CDP monitor was started before click — wait for all requests to complete
|
||||||
try {
|
// (300ms debounce) or for a modal/balloon/confirm to appear.
|
||||||
await page.waitForSelector(
|
await netMonitor.waitDone(timeout);
|
||||||
'#modalSurface:not([style*="display: none"]), .balloon, .confirm',
|
|
||||||
{ state: 'visible', timeout: 10000 }
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
await waitForStable();
|
await waitForStable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2175,7 +2260,10 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = {
|
|||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
|
||||||
} finally { if (highlightMode) try { await unhighlight(); } catch {} }
|
} finally {
|
||||||
|
if (netMonitor) try { await netMonitor.cleanup(); } catch {}
|
||||||
|
if (highlightMode) try { await unhighlight(); } catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2212,6 +2300,7 @@ export async function closeForm({ save } = {}) {
|
|||||||
for (const b of btns) {
|
for (const b of btns) {
|
||||||
const txt = (await b.textContent()).trim();
|
const txt = (await b.textContent()).trim();
|
||||||
if (txt === label) {
|
if (txt === label) {
|
||||||
|
if (recorder) await page.waitForTimeout(500); // show confirmation to viewer during recording
|
||||||
await b.click({ force: true });
|
await b.click({ force: true });
|
||||||
await waitForStable(beforeForm);
|
await waitForStable(beforeForm);
|
||||||
break;
|
break;
|
||||||
@@ -2251,6 +2340,27 @@ export async function selectValue(fieldName, searchText, { type } = {}) {
|
|||||||
if (highlightMode) try { await highlight(fieldName); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
if (highlightMode) try { await highlight(fieldName); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// === CLEAR FIELD if searchText is empty/null ===
|
||||||
|
if (!searchText && searchText !== 0) {
|
||||||
|
const inputId = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${formNum}_';
|
||||||
|
const name = ${JSON.stringify(btn.fieldName)};
|
||||||
|
const el = document.querySelector('[id="' + p + name + '"], [id="' + p + name + '_i0"]');
|
||||||
|
return el ? el.id : null;
|
||||||
|
})()`);
|
||||||
|
if (inputId) {
|
||||||
|
await page.click(`[id="${inputId}"]`);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await waitForStable();
|
||||||
|
}
|
||||||
|
if (highlightMode) try { await unhighlight(); } catch {}
|
||||||
|
const formData = await getFormState();
|
||||||
|
return { ...formData, selected: { field: fieldName, search: null, method: 'clear' } };
|
||||||
|
}
|
||||||
|
|
||||||
// === COMPOSITE TYPE HANDLING ===
|
// === COMPOSITE TYPE HANDLING ===
|
||||||
// When `type` is specified, clear the field first to reset cached type,
|
// When `type` is specified, clear the field first to reset cached type,
|
||||||
// then open type selection dialog, pick the type, then pick the value.
|
// then open type selection dialog, pick the type, then pick the value.
|
||||||
@@ -2718,7 +2828,9 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
|
|
||||||
// Skip if cell already contains the desired value (single-field optimization)
|
// Skip if cell already contains the desired value (single-field optimization)
|
||||||
const firstKey0 = Object.keys(fields)[0];
|
const firstKey0 = Object.keys(fields)[0];
|
||||||
const firstVal0 = typeof fields[firstKey0] === 'object' ? fields[firstKey0].value : String(fields[firstKey0]);
|
const rawFirstVal = fields[firstKey0];
|
||||||
|
const firstVal0 = rawFirstVal === null || rawFirstVal === undefined || rawFirstVal === ''
|
||||||
|
? '' : (typeof rawFirstVal === 'object' ? rawFirstVal.value : String(rawFirstVal));
|
||||||
let firstFieldSkipped = false;
|
let firstFieldSkipped = false;
|
||||||
if (cellCoords.currentText && firstVal0 &&
|
if (cellCoords.currentText && firstVal0 &&
|
||||||
cellCoords.currentText.toLowerCase().includes(firstVal0.toLowerCase())) {
|
cellCoords.currentText.toLowerCase().includes(firstVal0.toLowerCase())) {
|
||||||
@@ -2732,6 +2844,57 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
// Then escalate: dblclick → F4 if needed.
|
// Then escalate: dblclick → F4 if needed.
|
||||||
await page.mouse.click(cellCoords.x, cellCoords.y);
|
await page.mouse.click(cellCoords.x, cellCoords.y);
|
||||||
|
|
||||||
|
// Clear cell via Shift+F4 if value is empty
|
||||||
|
if (firstVal0 === '') {
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
// Check if click opened a selection form — close it first
|
||||||
|
let openedForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (openedForm !== null) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
} else {
|
||||||
|
// No form opened — need to enter edit mode first (dblclick), then close any form that opens
|
||||||
|
await page.mouse.dblclick(cellCoords.x, cellCoords.y);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
openedForm = await page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
|
||||||
|
const m = el.id.match(/^form(\\d+)_/);
|
||||||
|
if (m) forms[m[1]] = true;
|
||||||
|
});
|
||||||
|
const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum});
|
||||||
|
return nums.length > 0 ? Math.max(...nums) : null;
|
||||||
|
})()`);
|
||||||
|
if (openedForm !== null) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const results = [{ field: firstKey0, ok: true, method: 'clear', value: '' }];
|
||||||
|
// If more fields remain, process them on the same row
|
||||||
|
const remaining = { ...fields };
|
||||||
|
delete remaining[firstKey0];
|
||||||
|
if (Object.keys(remaining).length > 0) {
|
||||||
|
const more = await fillTableRow(remaining, { row, table });
|
||||||
|
if (Array.isArray(more)) results.push(...more);
|
||||||
|
else if (more?.filled) results.push(...more.filled);
|
||||||
|
}
|
||||||
|
const formData = await getFormState();
|
||||||
|
return { filled: results, form: formData };
|
||||||
|
}
|
||||||
|
|
||||||
// Check if clicked cell is a checkbox (toggle-on-click, no edit mode)
|
// Check if clicked cell is a checkbox (toggle-on-click, no edit mode)
|
||||||
const checkboxInfo = await page.evaluate(`(() => {
|
const checkboxInfo = await page.evaluate(`(() => {
|
||||||
const el = document.elementFromPoint(${cellCoords.x}, ${cellCoords.y});
|
const el = document.elementFromPoint(${cellCoords.x}, ${cellCoords.y});
|
||||||
@@ -3091,8 +3254,14 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
// 4. Prepare pending fields for fuzzy matching
|
// 4. Prepare pending fields for fuzzy matching
|
||||||
const pending = new Map();
|
const pending = new Map();
|
||||||
for (const [key, val] of Object.entries(fields)) {
|
for (const [key, val] of Object.entries(fields)) {
|
||||||
if (val && typeof val === 'object' && 'value' in val) {
|
if (val === null || val === undefined || val === '') {
|
||||||
pending.set(key, { value: String(val.value), type: val.type || null, filled: false });
|
pending.set(key, { value: '', type: null, filled: false });
|
||||||
|
} else if (val && typeof val === 'object' && 'value' in val) {
|
||||||
|
const innerVal = val.value;
|
||||||
|
pending.set(key, {
|
||||||
|
value: innerVal === null || innerVal === undefined || innerVal === '' ? '' : String(innerVal),
|
||||||
|
type: val.type || null, filled: false
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
pending.set(key, { value: String(val), type: null, filled: false });
|
pending.set(key, { value: String(val), type: null, filled: false });
|
||||||
}
|
}
|
||||||
@@ -3205,6 +3374,18 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
const info = pending.get(matchedKey);
|
const info = pending.get(matchedKey);
|
||||||
const text = info.value;
|
const text = info.value;
|
||||||
|
|
||||||
|
// Clear cell if value is empty (Shift+F4 = native 1C clear)
|
||||||
|
if (text === '') {
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
info.filled = true;
|
||||||
|
results.push({ field: matchedKey, cell: cell.fullName, ok: true, method: 'clear', value: '' });
|
||||||
|
if ([...pending.values()].every(p => p.filled)) break;
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If user specified a type, always clear and use type selection flow
|
// If user specified a type, always clear and use type selection flow
|
||||||
if (info.type) {
|
if (info.type) {
|
||||||
await page.keyboard.press('Shift+F4'); // Clear cell to reset any inherited type
|
await page.keyboard.press('Shift+F4'); // Clear cell to reset any inherited type
|
||||||
|
|||||||
@@ -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,16 +236,17 @@ 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-фильтры) | `{ filled: [{field, ok, method}], form }` |
|
| `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры). Пустое значение (`''`/`null`) = очистка | `{ filled: [{field, ok, method}], form }` |
|
||||||
| `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст или `{поле: значение}`. `{ type }` для составного типа | form state с `selected` |
|
| `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст, `{поле: значение}` или `''`/`null` для очистки. `{ type }` для составного типа | form state с `selected` |
|
||||||
| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка или `{ value, type }` для составного типа | form state |
|
| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state |
|
||||||
| `deleteTableRow(row, {tab?})` | Удалить строку по индексу | form state |
|
| `deleteTableRow(row, {tab?})` | Удалить строку по индексу | form state |
|
||||||
| `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да". Возвращает `closed: true/false` | form state с `closed` |
|
| `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да". Возвращает `closed: true/false` | form state с `closed` |
|
||||||
| `filterList(text, {field?, exact?})` | Фильтр списка. Без field = все колонки, с field = расширенный поиск | form state |
|
| `filterList(text, {field?, exact?})` | Фильтр списка. Без field = все колонки, с field = расширенный поиск | form state |
|
||||||
@@ -260,6 +261,7 @@ await closeForm({ save: false });
|
|||||||
| `'true'` / `'да'` | Чекбокс | toggle |
|
| `'true'` / `'да'` | Чекбокс | toggle |
|
||||||
| `'Оплата поставщику'` | Радио | fuzzy match по меткам |
|
| `'Оплата поставщику'` | Радио | fuzzy match по меткам |
|
||||||
| `'Склад бытовой техники'` (DCS) | Фильтр отчёта | авто-включение чекбокса + заполнение |
|
| `'Склад бытовой техники'` (DCS) | Фильтр отчёта | авто-включение чекбокса + заполнение |
|
||||||
|
| `''` / `null` | Любое (кроме чекбокс/радио) | очистка через Shift+F4 |
|
||||||
|
|
||||||
### Утилиты
|
### Утилиты
|
||||||
|
|
||||||
@@ -291,7 +293,7 @@ await closeForm({ save: false });
|
|||||||
| Клавиша | Контекст | Действие |
|
| Клавиша | Контекст | Действие |
|
||||||
|---------|----------|----------|
|
|---------|----------|----------|
|
||||||
| `F8` | Ссылочное поле | Создать новый элемент |
|
| `F8` | Ссылочное поле | Создать новый элемент |
|
||||||
| `Shift+F4` | Ссылочное поле | Очистить значение |
|
| `Shift+F4` | Любое поле | Очистить значение (автоматизировано: `fillFields({ поле: '' })`) |
|
||||||
| `F4` | Ссылочное поле | Форма выбора |
|
| `F4` | Ссылочное поле | Форма выбора |
|
||||||
| `Alt+F` | Список/таблица | Расширенный поиск |
|
| `Alt+F` | Список/таблица | Расширенный поиск |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user