mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-16 10:43:18 +03:00
refactor(web-test): этап C.8 — выделить forms/select-value.mjs
Перенос selectValue + helpers из browser.mjs (~960 LOC):
- scanGridRows, dblclickAndVerify, advancedSearchInline
- pickFromSelectionForm, isTypeDialog, pickFromTypeDialog (экспортируются —
вызываются из fillFields/fillTableRow в browser.mjs)
- fillReferenceField (экспортируется — вызывается из fillFields)
- selectValue
Двумя слайсами вокруг fillFields/fillField/clickElement/closeForm, которые
остаются в browser.mjs до этапов C.9/C.10.
browser.mjs: 4095 → 2933 LOC. 56 публичных экспортов.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -757,634 +757,15 @@ export async function readSpreadsheet() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ============================================================
|
||||||
* Scan visible grid rows for a text match (exact → startsWith → includes).
|
// Value selection (DLB/CB) — extracted to forms/select-value.mjs
|
||||||
* Returns center coords of the matched row, or null if not found.
|
// ============================================================
|
||||||
* When searchLower is empty, returns coords of the first row (fallback).
|
export { selectValue } from './forms/select-value.mjs';
|
||||||
*/
|
import {
|
||||||
async function scanGridRows(formNum, searchLower) {
|
selectValue, pickFromSelectionForm, isTypeDialog, pickFromTypeDialog,
|
||||||
return page.evaluate(`(() => {
|
fillReferenceField,
|
||||||
const p = 'form${formNum}_';
|
} from './forms/select-value.mjs';
|
||||||
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
|
||||||
if (!grid) return null;
|
|
||||||
const body = grid.querySelector('.gridBody');
|
|
||||||
if (!body) return null;
|
|
||||||
const lines = [...body.querySelectorAll('.gridLine')];
|
|
||||||
if (!lines.length) return { rowCount: 0 };
|
|
||||||
const searchLower = ${JSON.stringify(searchLower || '')};
|
|
||||||
let sel = null;
|
|
||||||
if (searchLower) {
|
|
||||||
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim().toLowerCase().replace(/ё/gi, 'е');
|
|
||||||
const rowData = lines.map(l => ({ el: l, text: norm(l.innerText) }));
|
|
||||||
sel = rowData.find(r => r.text === searchLower)?.el
|
|
||||||
|| rowData.find(r => r.text.startsWith(searchLower))?.el
|
|
||||||
|| rowData.find(r => r.text.includes(searchLower))?.el;
|
|
||||||
} else {
|
|
||||||
sel = lines[0]; // empty search → first row
|
|
||||||
}
|
|
||||||
if (!sel) return null;
|
|
||||||
const imgBox = sel.querySelector('.gridBoxImg');
|
|
||||||
const isGroup = imgBox ? !!imgBox.querySelector('.gridListH') : false;
|
|
||||||
const r = sel.getBoundingClientRect();
|
|
||||||
return { rowCount: lines.length, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), isGroup };
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a row in a selection form via click + Enter, verify it closed.
|
|
||||||
* Uses click + Enter instead of dblclick because dblclick toggles
|
|
||||||
* expand/collapse in tree-style selection forms.
|
|
||||||
* Returns { field, ok: true, method: 'form' } on success,
|
|
||||||
* or { field, ok: false, reason: 'still_open' } if the item couldn't be selected (e.g. group row).
|
|
||||||
*/
|
|
||||||
async function dblclickAndVerify(coords, selFormNum, fieldName) {
|
|
||||||
// Click to highlight the row, then Enter to confirm selection.
|
|
||||||
// This works for both flat grids and tree forms (dblclick would
|
|
||||||
// toggle expand/collapse on tree group rows).
|
|
||||||
await page.mouse.click(coords.x, coords.y);
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await waitForStable(selFormNum);
|
|
||||||
|
|
||||||
// Verify selection form closed
|
|
||||||
const stillOpen = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${selFormNum}_';
|
|
||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
|
||||||
})()`);
|
|
||||||
if (stillOpen) {
|
|
||||||
// Enter didn't select — item is likely a non-selectable group.
|
|
||||||
// Don't Escape here — let the caller decide (may want to try another row).
|
|
||||||
return { field: fieldName, ok: false, reason: 'still_open' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for 1C error modals after selection
|
|
||||||
const err = await page.evaluate(checkErrorsScript());
|
|
||||||
if (err?.modal) {
|
|
||||||
try {
|
|
||||||
const btn = await page.$('a.press.pressDefault');
|
|
||||||
if (btn) { await btn.click(); await page.waitForTimeout(500); }
|
|
||||||
} catch { /* OK */ }
|
|
||||||
}
|
|
||||||
return { field: fieldName, ok: true, method: 'form' };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inline advanced search on a selection form via Alt+F.
|
|
||||||
* Does NOT click any column — FieldSelector auto-populates with main representation.
|
|
||||||
* Switches to "по части строки" (CompareType#1) to avoid composite type issues.
|
|
||||||
* Does not throw — returns silently on failure.
|
|
||||||
*/
|
|
||||||
async function advancedSearchInline(formNum, text) {
|
|
||||||
try {
|
|
||||||
// 1. Open advanced search via Alt+F
|
|
||||||
await page.keyboard.press('Alt+f');
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
const dialogForm = await page.evaluate(detectFormScript());
|
|
||||||
if (dialogForm === formNum || dialogForm === null) return; // Alt+F didn't open dialog
|
|
||||||
|
|
||||||
// 2. Switch to "по части строки" (CompareType#1)
|
|
||||||
const radioClicked = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${dialogForm}_';
|
|
||||||
const el = document.getElementById(p + 'CompareType#1#radio');
|
|
||||||
if (!el || el.offsetWidth === 0) return false;
|
|
||||||
if (el.classList.contains('select')) return true; // already selected
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
||||||
})()`);
|
|
||||||
if (radioClicked && typeof radioClicked === 'object') {
|
|
||||||
await page.mouse.click(radioClicked.x, radioClicked.y);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fill Pattern field via clipboard paste
|
|
||||||
const patternId = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${dialogForm}_';
|
|
||||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
|
||||||
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
|
||||||
return el ? el.id : null;
|
|
||||||
})()`);
|
|
||||||
if (!patternId) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await page.click(`[id="${patternId}"]`);
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await page.keyboard.press('Control+A');
|
|
||||||
await pasteText(text);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// 4. Click "Найти"
|
|
||||||
const findBtn = await page.evaluate(`(() => {
|
|
||||||
const btns = [...document.querySelectorAll('a.press')].filter(el => el.offsetWidth > 0);
|
|
||||||
const btn = btns.find(el => el.innerText?.trim() === 'Найти');
|
|
||||||
if (!btn) return null;
|
|
||||||
const r = btn.getBoundingClientRect();
|
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
||||||
})()`);
|
|
||||||
if (findBtn) {
|
|
||||||
await page.mouse.click(findBtn.x, findBtn.y);
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Close advanced search dialog
|
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
|
||||||
const dialogVisible = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${dialogForm}_';
|
|
||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
|
||||||
})()`);
|
|
||||||
if (!dialogVisible) break;
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
await waitForStable(formNum);
|
|
||||||
} catch { /* silently fail — caller will re-scan and handle not_found */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick a value from an opened selection form.
|
|
||||||
*
|
|
||||||
* Strategy (escalating):
|
|
||||||
* 1. Scan visible rows for text match (exact → startsWith → includes)
|
|
||||||
* 2. Advanced search (Alt+F, "по части строки") → re-scan
|
|
||||||
* 3. Fallback: simple search (search input + Enter) → re-scan
|
|
||||||
* 4. Not found → Escape → error
|
|
||||||
*
|
|
||||||
* For object search {field: value}: steps 1, then filterList(val, {field}) per entry, then re-scan.
|
|
||||||
* For empty search: pick first visible row.
|
|
||||||
*
|
|
||||||
* @param {number} selFormNum - selection form number
|
|
||||||
* @param {string} fieldName - field being filled (for error messages)
|
|
||||||
* @param {string|Object} search - string for simple search, or { field: value } for per-field search
|
|
||||||
* @param {number} origFormNum - original form number (to verify we returned)
|
|
||||||
* @returns {{ field, ok, method }} or {{ field, error, message }}
|
|
||||||
*/
|
|
||||||
async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum) {
|
|
||||||
const searchText = typeof search === 'string'
|
|
||||||
? search : (search ? Object.values(search).join(' ') : '');
|
|
||||||
const searchLower = normYo((searchText || '').toLowerCase());
|
|
||||||
|
|
||||||
// Helper: try to select a row; returns result if ok, null if item wasn't selectable (group).
|
|
||||||
let hadUnselectableMatch = false;
|
|
||||||
async function trySelect(row) {
|
|
||||||
const r = await dblclickAndVerify(row, selFormNum, fieldName);
|
|
||||||
if (r.ok) return r;
|
|
||||||
hadUnselectableMatch = true; // found match but couldn't select (possibly group row or overlay)
|
|
||||||
return null; // form still open, try next step
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Scan visible rows (no filtering)
|
|
||||||
if (searchLower) {
|
|
||||||
const row = await scanGridRows(selFormNum, searchLower);
|
|
||||||
if (row?.x) {
|
|
||||||
const r = await trySelect(row);
|
|
||||||
if (r) return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Advanced search (Alt+F — fast, no overlay issues)
|
|
||||||
if (typeof search === 'object' && search) {
|
|
||||||
// Per-field advanced search via filterList(val, {field})
|
|
||||||
for (const [fld, val] of Object.entries(search)) {
|
|
||||||
try { await filterList(String(val), { field: fld }); } catch { /* proceed */ }
|
|
||||||
}
|
|
||||||
} else if (searchLower) {
|
|
||||||
// Inline advanced search (Alt+F, "по части строки")
|
|
||||||
await advancedSearchInline(selFormNum, searchText);
|
|
||||||
}
|
|
||||||
if (searchLower) {
|
|
||||||
const row = await scanGridRows(selFormNum, searchLower);
|
|
||||||
if (row?.x) {
|
|
||||||
const r = await trySelect(row);
|
|
||||||
if (r) return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Fallback — simple search via search input (for forms without Alt+F support)
|
|
||||||
if (typeof search === 'string' && searchLower) {
|
|
||||||
const searchInputId = await page.evaluate(`(() => {
|
|
||||||
const p = 'form${selFormNum}_';
|
|
||||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
|
||||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
|
||||||
return el ? el.id : null;
|
|
||||||
})()`);
|
|
||||||
if (searchInputId) {
|
|
||||||
try {
|
|
||||||
await page.click(`[id="${searchInputId}"]`);
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await page.keyboard.press('Control+A');
|
|
||||||
await pasteText(searchText);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await waitForStable(selFormNum);
|
|
||||||
} catch { /* proceed */ }
|
|
||||||
const row = await scanGridRows(selFormNum, searchLower);
|
|
||||||
if (row?.x) {
|
|
||||||
const r = await trySelect(row);
|
|
||||||
if (r) return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Empty search → pick first row; otherwise not found
|
|
||||||
if (!searchLower) {
|
|
||||||
const row = await scanGridRows(selFormNum, '');
|
|
||||||
if (row?.x) {
|
|
||||||
const r = await trySelect(row);
|
|
||||||
if (r) return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable();
|
|
||||||
const searchDesc = typeof search === 'string' ? '"' + search + '"' : JSON.stringify(search);
|
|
||||||
if (hadUnselectableMatch) {
|
|
||||||
return { field: fieldName, error: 'not_selectable',
|
|
||||||
message: 'Found ' + searchDesc + ' in selection form but it is not selectable (group/folder row)' };
|
|
||||||
}
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'No matches in selection form for ' + searchDesc };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect whether a form is a type selection dialog ("Выбор типа данных").
|
|
||||||
* Type dialogs appear when selecting a value for a composite-type field.
|
|
||||||
*
|
|
||||||
* Detection signals (any one is sufficient):
|
|
||||||
* - form{N}_OK element exists (selection forms use "Выбрать", not "OK")
|
|
||||||
* - form{N}_ValueList grid exists (specific to type/value list dialogs)
|
|
||||||
* - Window title contains "Выбор типа" (title attr on .toplineBoxTitle)
|
|
||||||
*/
|
|
||||||
async function isTypeDialog(formNum) {
|
|
||||||
return page.evaluate(`(() => {
|
|
||||||
const p = 'form' + ${formNum} + '_';
|
|
||||||
const hasOK = !!document.getElementById(p + 'OK');
|
|
||||||
const hasValueList = !!document.getElementById(p + 'ValueList');
|
|
||||||
const hasTitle = [...document.querySelectorAll('.toplineBoxTitle')]
|
|
||||||
.some(el => el.offsetWidth > 0 && /выбор типа/i.test(el.getAttribute('title') || ''));
|
|
||||||
return hasOK || hasValueList || hasTitle;
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a type from the type selection dialog ("Выбор типа данных")
|
|
||||||
* using Ctrl+F search. The dialog has a virtual grid (~5 visible rows),
|
|
||||||
* so Ctrl+F is the only reliable way to find a type.
|
|
||||||
*
|
|
||||||
* Algorithm: Ctrl+F → paste typeName → Enter (search) → Escape (close Find) →
|
|
||||||
* verify selected row matches → Enter (OK)
|
|
||||||
*
|
|
||||||
* @param {number} formNum - type dialog form number
|
|
||||||
* @param {string} typeName - type name to search for (fuzzy, e.g. "Реализация (акт")
|
|
||||||
* @throws {Error} if type not found
|
|
||||||
*/
|
|
||||||
async function pickFromTypeDialog(formNum, typeName) {
|
|
||||||
// The type dialog is a modal ValueList grid.
|
|
||||||
// Strategy: scan visible rows first (fast path), fall back to Ctrl+F for large lists.
|
|
||||||
//
|
|
||||||
// Key constraints discovered during testing:
|
|
||||||
// - Grid focus: use evaluate(() => gridBody.focus()), NOT page.click({force:true})
|
|
||||||
// which punches through the modal overlay to the form underneath
|
|
||||||
// - Ctrl+F only opens "Найти" if the GRID is focused (otherwise closes the type dialog)
|
|
||||||
// - Buttons: use page.click({force:true}), NOT evaluate(() => el.click())
|
|
||||||
// because evaluate click doesn't trigger 1C's event chain properly
|
|
||||||
// - Enter/Escape in "Найти" close the ENTIRE dialog chain, not just "Найти"
|
|
||||||
// - Closing "Найти" via Cancel resets the search — verify grid while "Найти" is open
|
|
||||||
|
|
||||||
const typeNorm = normYo(typeName.toLowerCase());
|
|
||||||
|
|
||||||
// Helper: read visible rows and find matching ones
|
|
||||||
async function readVisibleRows() {
|
|
||||||
return page.evaluate(`(() => {
|
|
||||||
const grid = document.getElementById('form${formNum}_ValueList');
|
|
||||||
if (!grid) return { visible: [], matches: [] };
|
|
||||||
const body = grid.querySelector('.gridBody');
|
|
||||||
if (!body) return { visible: [], matches: [] };
|
|
||||||
const lines = body.querySelectorAll('.gridLine');
|
|
||||||
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim();
|
|
||||||
const typeNorm = ${JSON.stringify(typeNorm)};
|
|
||||||
const visible = [];
|
|
||||||
const matches = [];
|
|
||||||
for (const line of lines) {
|
|
||||||
const text = norm(line.innerText);
|
|
||||||
if (!text) continue;
|
|
||||||
visible.push(text);
|
|
||||||
if (text.toLowerCase().replace(/ё/gi, 'е').includes(typeNorm)) {
|
|
||||||
const r = line.getBoundingClientRect();
|
|
||||||
matches.push({ text, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { visible, matches };
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists)
|
|
||||||
const scan = await readVisibleRows();
|
|
||||||
|
|
||||||
if (scan.matches.length === 1) {
|
|
||||||
// Single match — click to select, then OK
|
|
||||||
await page.mouse.click(scan.matches[0].x, scan.matches[0].y);
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await page.click(`#form${formNum}_OK`, { force: true });
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scan.matches.length > 1) {
|
|
||||||
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
|
||||||
await waitForStable();
|
|
||||||
throw new Error(`selectValue: multiple types match "${typeName}": ${scan.matches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Not found in visible rows — use Ctrl+F (virtual grid may have more items)
|
|
||||||
|
|
||||||
// Focus the grid via evaluate (does NOT punch through modal like page.click)
|
|
||||||
await page.evaluate(`(() => {
|
|
||||||
const grid = document.getElementById('form${formNum}_ValueList');
|
|
||||||
if (!grid) return;
|
|
||||||
const body = grid.querySelector('.gridBody');
|
|
||||||
if (body) body.focus(); else grid.focus();
|
|
||||||
})()`);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Ctrl+F to open "Найти" dialog
|
|
||||||
await page.keyboard.press('Control+f');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Paste search text (focus is on "Что искать" field)
|
|
||||||
await page.keyboard.press('Control+a');
|
|
||||||
await pasteText(typeName);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Find the "Найти" dialog form number (it's > formNum)
|
|
||||||
const findFormNum = await page.evaluate(`(() => {
|
|
||||||
for (let n = ${formNum} + 1; n < ${formNum} + 20; n++) {
|
|
||||||
const btn = document.getElementById('form' + n + '_Find');
|
|
||||||
if (btn && btn.offsetWidth > 0) return n;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})()`);
|
|
||||||
|
|
||||||
if (findFormNum === null) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable();
|
|
||||||
throw new Error('selectValue: Ctrl+F did not open "Найти" dialog in type selection');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click "Найти" — search is client-side (no server round-trip), 500ms is enough
|
|
||||||
await page.click(`#form${findFormNum}_Find`, { force: true });
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Re-read visible rows after search scrolled to match
|
|
||||||
const afterSearch = await readVisibleRows();
|
|
||||||
|
|
||||||
if (afterSearch.matches.length === 0) {
|
|
||||||
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
|
||||||
await waitForStable();
|
|
||||||
throw new Error(`selectValue: type "${typeName}" not found in type selection dialog` +
|
|
||||||
`. Visible: ${(scan.visible || []).join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (afterSearch.matches.length > 1) {
|
|
||||||
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
|
||||||
await waitForStable();
|
|
||||||
throw new Error(`selectValue: multiple types match "${typeName}": ${afterSearch.matches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click OK on type dialog via page.click({force:true}) — bypasses "Найти" modal
|
|
||||||
await page.click(`#form${formNum}_OK`, { force: true });
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fill a reference field via clipboard paste + 1C autocomplete.
|
|
||||||
*
|
|
||||||
* Strategy:
|
|
||||||
* 1. Clear field if it has a value (Shift+F4 — native 1C mechanism, no JS errors)
|
|
||||||
* 2. Clipboard paste text (Ctrl+V = trusted event, triggers real 1C autocomplete)
|
|
||||||
* 3. Check editDropDown for autocomplete results → click match or Tab to resolve
|
|
||||||
* 4. Verify result: resolved → ok, not found → clear + error
|
|
||||||
*
|
|
||||||
* Clipboard paste was chosen because:
|
|
||||||
* - Ctrl+V produces trusted browser events that 1C respects for autocomplete
|
|
||||||
* - page.fill() + synthetic keydown/keyup only triggers hints, not real search
|
|
||||||
* - keyboard.type() garbles Cyrillic on some fields
|
|
||||||
*
|
|
||||||
* @returns {{ field, ok?, method?, error?, value?, message?, available? }}
|
|
||||||
*/
|
|
||||||
async function fillReferenceField(selector, fieldName, value, formNum) {
|
|
||||||
const text = String(value);
|
|
||||||
const escapedSel = selector.replace(/'/g, "\\'");
|
|
||||||
|
|
||||||
// Helper: detect new forms opened above the current one (strict — interactive
|
|
||||||
// elements only; fillReferenceField-specific)
|
|
||||||
const detectNewForm = () => helperDetectNewForm(formNum, { strict: true });
|
|
||||||
|
|
||||||
// Helper: clear the field using Shift+F4 (native 1C mechanism)
|
|
||||||
async function clearField() {
|
|
||||||
try {
|
|
||||||
await page.click(selector, { timeout: 3000 });
|
|
||||||
await page.keyboard.press('Shift+F4');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
} catch { /* OK */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: check for "not in list" cloud popup (1C shows positioned div with "нет в списке")
|
|
||||||
async function checkNotInListCloud() {
|
|
||||||
return page.evaluate(`(() => {
|
|
||||||
const divs = document.querySelectorAll('div');
|
|
||||||
for (const el of divs) {
|
|
||||||
if (el.offsetWidth === 0 || el.offsetHeight === 0) continue;
|
|
||||||
const style = getComputedStyle(el);
|
|
||||||
if (style.position !== 'absolute' && style.position !== 'fixed') continue;
|
|
||||||
const z = parseInt(style.zIndex) || 0;
|
|
||||||
if (z < 100) continue;
|
|
||||||
if ((el.innerText || '').includes('нет в списке')) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0. Dismiss any leftover error modal from a previous operation
|
|
||||||
await dismissPendingErrors();
|
|
||||||
|
|
||||||
// 0a. Try DLB (DropListButton) first — works cleanly for combobox/enum fields
|
|
||||||
// and also for reference fields that show a dropdown.
|
|
||||||
const inputId = selector.match(/\[id="(.+)"\]/)?.[1];
|
|
||||||
// DLB button ID uses field name without _iN suffix (e.g. form1_Field_DLB, not form1_Field_i0_DLB)
|
|
||||||
const dlbId = inputId.replace(/_i\d+$/, '') + '_DLB';
|
|
||||||
const dlbSelector = `[id="${dlbId}"]`;
|
|
||||||
try {
|
|
||||||
const dlbVisible = await page.evaluate(`document.querySelector('${dlbSelector.replace(/'/g, "\\'")}')?.offsetWidth > 0`);
|
|
||||||
if (dlbVisible) {
|
|
||||||
await page.click(dlbSelector);
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
const eddState = await readEdd();
|
|
||||||
if (eddState.visible && eddState.items?.length > 0) {
|
|
||||||
const target = normYo(text.toLowerCase());
|
|
||||||
const candidates = eddState.items.filter(i => !i.name.startsWith('Создать'));
|
|
||||||
let match = candidates.find(i => normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase()) === target);
|
|
||||||
if (!match) match = candidates.find(i => normYo(i.name.toLowerCase()).includes(target));
|
|
||||||
if (!match) match = candidates.find(i => {
|
|
||||||
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
|
||||||
return name.includes(target) || target.includes(name);
|
|
||||||
});
|
|
||||||
if (match) {
|
|
||||||
await page.mouse.click(match.x, match.y);
|
|
||||||
await waitForStable();
|
|
||||||
await dismissPendingErrors();
|
|
||||||
return { field: fieldName, ok: true, method: 'dropdown',
|
|
||||||
value: match.name.replace(/\s*\([^)]*\)\s*$/, '') };
|
|
||||||
}
|
|
||||||
// No match in DLB dropdown — close and fall through to paste approach
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
} else if (eddState.visible) {
|
|
||||||
// DLB opened a hint popup (no .eddText items) — close it before proceeding
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* DLB approach failed — fall through to paste */ }
|
|
||||||
|
|
||||||
// 1. Focus (handle surface/modal overlay from previous interaction)
|
|
||||||
await safeClick(selector, { dismissErrors: true });
|
|
||||||
|
|
||||||
// 2. If field already has a value, clear using Shift+F4 (native 1C mechanism).
|
|
||||||
// This is needed for reference fields — Shift+F4 properly clears the ref link.
|
|
||||||
const currentVal = await page.evaluate(`document.querySelector('${escapedSel}')?.value || ''`);
|
|
||||||
if (currentVal) {
|
|
||||||
await page.keyboard.press('Shift+F4');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
// Refocus
|
|
||||||
await page.click(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Paste text via clipboard (trusted event → triggers real 1C autocomplete)
|
|
||||||
await pasteText(text);
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
// 4. Check editDropDown for autocomplete suggestions
|
|
||||||
const eddState = await readEdd();
|
|
||||||
|
|
||||||
if (eddState.visible && eddState.items?.length > 0) {
|
|
||||||
const target = normYo(text.toLowerCase());
|
|
||||||
// Separate real matches from "Создать:" items
|
|
||||||
const candidates = eddState.items.filter(i => !i.name.startsWith('Создать'));
|
|
||||||
|
|
||||||
if (candidates.length > 0) {
|
|
||||||
// Find best match (items have format "Name (Code)" — match against name part)
|
|
||||||
let match = candidates.find(i => {
|
|
||||||
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
|
||||||
return name === target;
|
|
||||||
});
|
|
||||||
if (!match) match = candidates.find(i => normYo(i.name.toLowerCase()).includes(target));
|
|
||||||
if (!match) match = candidates.find(i => {
|
|
||||||
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
|
||||||
return name.includes(target) || target.includes(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
await page.mouse.click(match.x, match.y);
|
|
||||||
await waitForStable();
|
|
||||||
await dismissPendingErrors(); // business logic errors (e.g. СПАРК) may appear async
|
|
||||||
return { field: fieldName, ok: true, method: 'dropdown',
|
|
||||||
value: match.name.replace(/\s*\([^)]*\)\s*$/, '') };
|
|
||||||
}
|
|
||||||
// Candidates exist but none match — report them
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await clearField();
|
|
||||||
return { field: fieldName, error: 'not_matched',
|
|
||||||
available: candidates.map(i => i.name.replace(/\s*\([^)]*\)\s*$/, '')) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only "Создать:" items — no existing matches
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await clearField();
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'No existing values match "' + text + '"' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4b. No edd — check for "not in list" cloud that may have appeared during paste
|
|
||||||
if (await checkNotInListCloud()) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await clearField();
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'Value "' + text + '" not found (not in list)' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. No edd at all — press Tab to trigger direct resolve
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await waitForStable();
|
|
||||||
await dismissPendingErrors();
|
|
||||||
|
|
||||||
// 5x. Check for "not in list" cloud popup after Tab
|
|
||||||
if (await checkNotInListCloud()) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await clearField();
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'Value "' + text + '" not found (not in list)' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5a. New form opened? (creation form = value not found)
|
|
||||||
const newForm = await detectNewForm();
|
|
||||||
if (newForm !== null) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable();
|
|
||||||
await clearField();
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'Value "' + text + '" not found' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5b. Dropdown after Tab?
|
|
||||||
const popup = await page.evaluate(readSubmenuScript());
|
|
||||||
if (Array.isArray(popup) && popup.length > 0) {
|
|
||||||
const realItems = popup.filter(i => !i.name.startsWith('Создать'));
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await clearField();
|
|
||||||
if (realItems.length > 0) {
|
|
||||||
return { field: fieldName, error: 'ambiguous',
|
|
||||||
message: 'Multiple matches for "' + text + '"',
|
|
||||||
available: realItems.map(i => i.name.replace(/\s*\([^)]*\)\s*$/, '')) };
|
|
||||||
}
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'Value "' + text + '" not found' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5c. Check final value
|
|
||||||
const finalVal = await page.evaluate(`document.querySelector('${escapedSel}')?.value || ''`);
|
|
||||||
if (!finalVal) {
|
|
||||||
// 6. Last resort: try F4 to open selection form and pick from there
|
|
||||||
try {
|
|
||||||
await page.click(selector);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
} catch { /* OK — field may be unfocused */ }
|
|
||||||
await page.keyboard.press('F4');
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
|
|
||||||
const selFormNum = await detectNewForm();
|
|
||||||
if (selFormNum !== null) {
|
|
||||||
const pickResult = await pickFromSelectionForm(selFormNum, fieldName, text, formNum);
|
|
||||||
if (pickResult.ok) return pickResult;
|
|
||||||
// pickFromSelectionForm already closed the form on error
|
|
||||||
}
|
|
||||||
|
|
||||||
return { field: fieldName, error: 'not_found',
|
|
||||||
message: 'Value "' + text + '" not found (field is empty)' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { field: fieldName, ok: true, method: 'typeahead', value: finalVal };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Fill fields on the current form via Playwright page.fill(). Returns fill results + updated form. */
|
/** Fill fields on the current form via Playwright page.fill(). Returns fill results + updated form. */
|
||||||
@@ -1871,316 +1252,6 @@ export async function closeForm({ save } = {}) {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a value from a reference field (compound operation).
|
|
||||||
* Handles three patterns:
|
|
||||||
* A) DLB opens an inline dropdown popup — click matching item
|
|
||||||
* B) DLB opens dropdown with history — click "Показать все" or F4 to open selection form
|
|
||||||
* C) DLB opens a separate selection form directly — search + dblclick in grid
|
|
||||||
*/
|
|
||||||
export async function selectValue(fieldName, searchText, { type } = {}) {
|
|
||||||
ensureConnected();
|
|
||||||
await dismissPendingErrors();
|
|
||||||
const formNum = await page.evaluate(detectFormScript());
|
|
||||||
if (formNum === null) throw new Error(`selectValue: no form found`);
|
|
||||||
|
|
||||||
// Detect any new form opened above this one (broad — includes type dialogs).
|
|
||||||
// Hoisted to the top so the composite-type branch can call it before its
|
|
||||||
// original declaration site further below.
|
|
||||||
const detectNewForm = () => helperDetectNewForm(formNum);
|
|
||||||
|
|
||||||
// 1. Find DLB button (fallback to CB — ERP uses Choose Button instead of DLB for some fields)
|
|
||||||
let btn = await page.evaluate(findFieldButtonScript(formNum, fieldName, 'DLB'));
|
|
||||||
if (btn?.error === 'button_not_found') {
|
|
||||||
btn = await page.evaluate(findFieldButtonScript(formNum, fieldName, 'CB'));
|
|
||||||
}
|
|
||||||
if (btn?.error) return btn;
|
|
||||||
if (highlightMode) try { await highlight(fieldName); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
|
||||||
try {
|
|
||||||
|
|
||||||
// === CLEAR FIELD if searchText is empty/null ===
|
|
||||||
if (!searchText && searchText !== 0) {
|
|
||||||
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
|
||||||
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 ===
|
|
||||||
// When `type` is specified, clear the field first to reset cached type,
|
|
||||||
// then open type selection dialog, pick the type, then pick the value.
|
|
||||||
if (type) {
|
|
||||||
// Find and focus the field input
|
|
||||||
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
|
||||||
if (!inputId) throw new Error(`selectValue: field "${btn.fieldName}" input not found`);
|
|
||||||
|
|
||||||
// Clear cached type + value with Shift+F4
|
|
||||||
await page.click(`[id="${inputId}"]`);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await page.keyboard.press('Shift+F4');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Re-focus and press F4 to open type selection dialog
|
|
||||||
await page.click(`[id="${inputId}"]`);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
await page.keyboard.press('F4');
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
await waitForStable(formNum);
|
|
||||||
|
|
||||||
const newFormNum = await detectNewForm();
|
|
||||||
if (newFormNum === null) {
|
|
||||||
throw new Error(`selectValue: F4 for composite field "${btn.fieldName}" did not open type selection dialog`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await isTypeDialog(newFormNum)) {
|
|
||||||
// Pick type from the dialog
|
|
||||||
await pickFromTypeDialog(newFormNum, type);
|
|
||||||
await waitForStable(newFormNum);
|
|
||||||
|
|
||||||
// After type selection, the actual selection form should open
|
|
||||||
const selFormNum = await detectSelectionForm();
|
|
||||||
if (selFormNum === null) {
|
|
||||||
throw new Error(`selectValue: after selecting type "${type}", no selection form opened for "${btn.fieldName}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
|
||||||
const state = await getFormState();
|
|
||||||
state.selected = { field: btn.fieldName, search: searchText || null, type, method: 'form' };
|
|
||||||
if (pickResult.error) state.selected.error = pickResult.error;
|
|
||||||
if (pickResult.message) state.selected.message = pickResult.message;
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err) state.errors = err;
|
|
||||||
return state;
|
|
||||||
} else {
|
|
||||||
// Not a type dialog — field is not composite type, proceed with normal selection
|
|
||||||
const pickResult = await pickFromSelectionForm(newFormNum, btn.fieldName, searchText || '', formNum);
|
|
||||||
const state = await getFormState();
|
|
||||||
state.selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
|
||||||
if (pickResult.error) state.selected.error = pickResult.error;
|
|
||||||
if (pickResult.message) state.selected.message = pickResult.message;
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err) state.errors = err;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// === END COMPOSITE TYPE HANDLING ===
|
|
||||||
|
|
||||||
// Auto-enable DCS checkbox if resolved via label
|
|
||||||
if (btn.dcsCheckbox) {
|
|
||||||
const cbSel = `[id="${btn.dcsCheckbox.inputId}"]`;
|
|
||||||
const isChecked = await page.$eval(cbSel, el =>
|
|
||||||
el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select'));
|
|
||||||
if (!isChecked) { await page.click(cbSel); await waitForStable(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: detect selection form (form number > formNum)
|
|
||||||
async function detectSelectionForm() {
|
|
||||||
return page.evaluate(`(() => {
|
|
||||||
const forms = {};
|
|
||||||
document.querySelectorAll('input.editInput[id], a.press[id]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 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;
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectNewForm is hoisted at the top of selectValue (see above).
|
|
||||||
|
|
||||||
// Helper: open selection form and pick value
|
|
||||||
async function openFormAndPick() {
|
|
||||||
await waitForStable(formNum);
|
|
||||||
const selFormNum = await detectSelectionForm();
|
|
||||||
if (selFormNum !== null) {
|
|
||||||
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
|
||||||
const state = await getFormState();
|
|
||||||
state.selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
|
||||||
if (pickResult.error) state.selected.error = pickResult.error;
|
|
||||||
if (pickResult.message) state.selected.message = pickResult.message;
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err) state.errors = err;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: click EDD item via evaluate (bypasses div.surface overlay from DLB)
|
|
||||||
// page.mouse.click() doesn't work here — surface intercepts pointer events.
|
|
||||||
// Dispatching mousedown directly on the element avoids this.
|
|
||||||
async function clickEddItem(itemName) {
|
|
||||||
return page.evaluate(`(() => {
|
|
||||||
const edd = document.getElementById('editDropDown');
|
|
||||||
if (!edd || edd.offsetWidth === 0) return null;
|
|
||||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
|
||||||
const target = ny(${JSON.stringify(itemName.toLowerCase())});
|
|
||||||
const items = [...edd.querySelectorAll('.eddText')].filter(el => el.offsetWidth > 0);
|
|
||||||
function clickEl(el) {
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 };
|
|
||||||
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
||||||
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
||||||
el.dispatchEvent(new MouseEvent('click', opts));
|
|
||||||
return el.innerText.trim();
|
|
||||||
}
|
|
||||||
// Pass 1: exact match (prefer over partial)
|
|
||||||
for (const el of items) {
|
|
||||||
const t = ny((el.innerText?.trim() || '').toLowerCase());
|
|
||||||
if (t === target) return clickEl(el);
|
|
||||||
const stripped = t.replace(/\\s*\\([^)]*\\)\\s*$/, '');
|
|
||||||
if (stripped === target) return clickEl(el);
|
|
||||||
}
|
|
||||||
// Pass 2: partial match
|
|
||||||
for (const el of items) {
|
|
||||||
const t = ny((el.innerText?.trim() || '').toLowerCase());
|
|
||||||
if (t.includes(target) || target.includes(t.replace(/\\s*\\([^)]*\\)\\s*$/, ''))) return clickEl(el);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: click "Показать все" in EDD footer via evaluate
|
|
||||||
async function clickShowAll() {
|
|
||||||
return page.evaluate(`(() => {
|
|
||||||
const edd = document.getElementById('editDropDown');
|
|
||||||
if (!edd || edd.offsetWidth === 0) return false;
|
|
||||||
let el = edd.querySelector('.eddBottom .hyperlink');
|
|
||||||
if (!el || el.offsetWidth === 0) {
|
|
||||||
const candidates = [...edd.querySelectorAll('span, div, a')]
|
|
||||||
.filter(e => e.offsetWidth > 0 && e.children.length === 0);
|
|
||||||
el = candidates.find(e => {
|
|
||||||
const t = (e.innerText?.trim() || '').toLowerCase();
|
|
||||||
return t === 'показать все' || t === 'show all';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!el) return false;
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 };
|
|
||||||
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
||||||
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
||||||
el.dispatchEvent(new MouseEvent('click', opts));
|
|
||||||
return true;
|
|
||||||
})()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Click DLB (handle funcPanel / surface overlay intercept)
|
|
||||||
const dlbSel = `[id="${btn.buttonId}"]`;
|
|
||||||
await safeClick(dlbSel, { timeout: 5000 });
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
|
|
||||||
// 3A. Check if a dropdown popup appeared (inline quick selection)
|
|
||||||
const popupItems = await page.evaluate(readSubmenuScript());
|
|
||||||
if (Array.isArray(popupItems) && popupItems.length > 0) {
|
|
||||||
const regularItems = popupItems.filter(i => i.kind !== 'showAll');
|
|
||||||
const showAllItem = popupItems.find(i => i.kind === 'showAll');
|
|
||||||
|
|
||||||
if (searchText) {
|
|
||||||
const target = normYo(searchText.toLowerCase());
|
|
||||||
// Try to find match among regular dropdown items
|
|
||||||
let match = regularItems.find(i => normYo(i.name.toLowerCase()) === target);
|
|
||||||
if (!match) match = regularItems.find(i => normYo(i.name.toLowerCase()).includes(target));
|
|
||||||
if (!match) match = regularItems.find(i => {
|
|
||||||
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
|
||||||
return name === target || name.includes(target) || target.includes(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
// Click via evaluate to bypass div.surface overlay
|
|
||||||
await clickEddItem(match.name);
|
|
||||||
await waitForStable();
|
|
||||||
const state = await getFormState();
|
|
||||||
state.selected = { field: btn.fieldName, search: searchText, method: 'dropdown' };
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err) state.errors = err;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No match in dropdown — try "Показать все" to open selection form
|
|
||||||
if (showAllItem) {
|
|
||||||
await clickShowAll();
|
|
||||||
const formResult = await openFormAndPick();
|
|
||||||
if (formResult) return formResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No "Показать все" — close dropdown, try F4
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Focus the field input and press F4 to open selection form
|
|
||||||
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
|
||||||
if (inputId) {
|
|
||||||
await page.click(`[id="${inputId}"]`);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
await page.keyboard.press('F4');
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
|
|
||||||
const formResult = await openFormAndPick();
|
|
||||||
if (formResult) return formResult;
|
|
||||||
|
|
||||||
// Still nothing — report available items from original dropdown
|
|
||||||
throw new Error(`selectValue: "${searchText}" not found for field "${btn.fieldName}". Available: ${regularItems.map(i => i.name).join(', ') || 'none'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No search text — click first regular item
|
|
||||||
if (regularItems.length > 0) {
|
|
||||||
await clickEddItem(regularItems[0].name);
|
|
||||||
await waitForStable();
|
|
||||||
const state = await getFormState();
|
|
||||||
state.selected = { field: btn.fieldName, search: null, picked: regularItems[0].name, method: 'dropdown' };
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err) state.errors = err;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3B. Check if a new selection form opened directly (use broad detection to also catch type dialogs)
|
|
||||||
const selFormNum = await detectNewForm();
|
|
||||||
if (selFormNum !== null) {
|
|
||||||
// Auto-detect type selection dialog when `type` was not specified
|
|
||||||
if (await isTypeDialog(selFormNum)) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable();
|
|
||||||
throw new Error(`selectValue: field "${btn.fieldName}" opened a type selection dialog — this is a composite-type field. Specify the type: selectValue('${btn.fieldName}', '${searchText || ''}', { type: 'ИмяТипа' })`);
|
|
||||||
}
|
|
||||||
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
|
||||||
const state = await getFormState();
|
|
||||||
state.selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
|
||||||
if (pickResult.error) state.selected.error = pickResult.error;
|
|
||||||
if (pickResult.message) state.selected.message = pickResult.message;
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err) state.errors = err;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3C. Neither popup nor form — try F4 as last resort
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
|
||||||
if (inputId) {
|
|
||||||
await page.click(`[id="${inputId}"]`);
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
await page.keyboard.press('F4');
|
|
||||||
await page.waitForTimeout(ACTION_WAIT);
|
|
||||||
|
|
||||||
const formResult = await openFormAndPick();
|
|
||||||
if (formResult) return formResult;
|
|
||||||
|
|
||||||
throw new Error(`selectValue: DLB click for "${btn.fieldName}" did not open a popup or selection form`);
|
|
||||||
|
|
||||||
} finally { if (highlightMode) try { await unhighlight(); } catch {} }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fill cells in the current table row via Tab navigation.
|
* Fill cells in the current table row via Tab navigation.
|
||||||
|
|||||||
@@ -0,0 +1,959 @@
|
|||||||
|
// web-test forms/select-value v1.16 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
||||||
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import {
|
||||||
|
page, ensureConnected, normYo, highlightMode, ACTION_WAIT,
|
||||||
|
} from '../core/state.mjs';
|
||||||
|
import {
|
||||||
|
detectFormScript, findFieldButtonScript, resolveFieldsScript,
|
||||||
|
readSubmenuScript, checkErrorsScript,
|
||||||
|
} from '../dom.mjs';
|
||||||
|
import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs';
|
||||||
|
import { waitForStable, waitForCondition } from '../core/wait.mjs';
|
||||||
|
import { highlight, unhighlight } from '../recording/highlight.mjs';
|
||||||
|
import {
|
||||||
|
safeClick, findFieldInputId, readEdd,
|
||||||
|
detectNewForm as helperDetectNewForm,
|
||||||
|
} from '../core/helpers.mjs';
|
||||||
|
// pasteText + getFormState live in browser.mjs.
|
||||||
|
import { pasteText, getFormState } from '../browser.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan visible grid rows for a text match (exact → startsWith → includes).
|
||||||
|
* Returns center coords of the matched row, or null if not found.
|
||||||
|
* When searchLower is empty, returns coords of the first row (fallback).
|
||||||
|
*/
|
||||||
|
async function scanGridRows(formNum, searchLower) {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const p = 'form${formNum}_';
|
||||||
|
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
||||||
|
if (!grid) return null;
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!body) return null;
|
||||||
|
const lines = [...body.querySelectorAll('.gridLine')];
|
||||||
|
if (!lines.length) return { rowCount: 0 };
|
||||||
|
const searchLower = ${JSON.stringify(searchLower || '')};
|
||||||
|
let sel = null;
|
||||||
|
if (searchLower) {
|
||||||
|
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim().toLowerCase().replace(/ё/gi, 'е');
|
||||||
|
const rowData = lines.map(l => ({ el: l, text: norm(l.innerText) }));
|
||||||
|
sel = rowData.find(r => r.text === searchLower)?.el
|
||||||
|
|| rowData.find(r => r.text.startsWith(searchLower))?.el
|
||||||
|
|| rowData.find(r => r.text.includes(searchLower))?.el;
|
||||||
|
} else {
|
||||||
|
sel = lines[0]; // empty search → first row
|
||||||
|
}
|
||||||
|
if (!sel) return null;
|
||||||
|
const imgBox = sel.querySelector('.gridBoxImg');
|
||||||
|
const isGroup = imgBox ? !!imgBox.querySelector('.gridListH') : false;
|
||||||
|
const r = sel.getBoundingClientRect();
|
||||||
|
return { rowCount: lines.length, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), isGroup };
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a row in a selection form via click + Enter, verify it closed.
|
||||||
|
* Uses click + Enter instead of dblclick because dblclick toggles
|
||||||
|
* expand/collapse in tree-style selection forms.
|
||||||
|
* Returns { field, ok: true, method: 'form' } on success,
|
||||||
|
* or { field, ok: false, reason: 'still_open' } if the item couldn't be selected (e.g. group row).
|
||||||
|
*/
|
||||||
|
async function dblclickAndVerify(coords, selFormNum, fieldName) {
|
||||||
|
// Click to highlight the row, then Enter to confirm selection.
|
||||||
|
// This works for both flat grids and tree forms (dblclick would
|
||||||
|
// toggle expand/collapse on tree group rows).
|
||||||
|
await page.mouse.click(coords.x, coords.y);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await waitForStable(selFormNum);
|
||||||
|
|
||||||
|
// Verify selection form closed
|
||||||
|
const stillOpen = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${selFormNum}_';
|
||||||
|
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
||||||
|
})()`);
|
||||||
|
if (stillOpen) {
|
||||||
|
// Enter didn't select — item is likely a non-selectable group.
|
||||||
|
// Don't Escape here — let the caller decide (may want to try another row).
|
||||||
|
return { field: fieldName, ok: false, reason: 'still_open' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for 1C error modals after selection
|
||||||
|
const err = await page.evaluate(checkErrorsScript());
|
||||||
|
if (err?.modal) {
|
||||||
|
try {
|
||||||
|
const btn = await page.$('a.press.pressDefault');
|
||||||
|
if (btn) { await btn.click(); await page.waitForTimeout(500); }
|
||||||
|
} catch { /* OK */ }
|
||||||
|
}
|
||||||
|
return { field: fieldName, ok: true, method: 'form' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline advanced search on a selection form via Alt+F.
|
||||||
|
* Does NOT click any column — FieldSelector auto-populates with main representation.
|
||||||
|
* Switches to "по части строки" (CompareType#1) to avoid composite type issues.
|
||||||
|
* Does not throw — returns silently on failure.
|
||||||
|
*/
|
||||||
|
async function advancedSearchInline(formNum, text) {
|
||||||
|
try {
|
||||||
|
// 1. Open advanced search via Alt+F
|
||||||
|
await page.keyboard.press('Alt+f');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const dialogForm = await page.evaluate(detectFormScript());
|
||||||
|
if (dialogForm === formNum || dialogForm === null) return; // Alt+F didn't open dialog
|
||||||
|
|
||||||
|
// 2. Switch to "по части строки" (CompareType#1)
|
||||||
|
const radioClicked = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
const el = document.getElementById(p + 'CompareType#1#radio');
|
||||||
|
if (!el || el.offsetWidth === 0) return false;
|
||||||
|
if (el.classList.contains('select')) return true; // already selected
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
})()`);
|
||||||
|
if (radioClicked && typeof radioClicked === 'object') {
|
||||||
|
await page.mouse.click(radioClicked.x, radioClicked.y);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fill Pattern field via clipboard paste
|
||||||
|
const patternId = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||||
|
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
||||||
|
return el ? el.id : null;
|
||||||
|
})()`);
|
||||||
|
if (!patternId) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await page.click(`[id="${patternId}"]`);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Control+A');
|
||||||
|
await pasteText(text);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// 4. Click "Найти"
|
||||||
|
const findBtn = await page.evaluate(`(() => {
|
||||||
|
const btns = [...document.querySelectorAll('a.press')].filter(el => el.offsetWidth > 0);
|
||||||
|
const btn = btns.find(el => el.innerText?.trim() === 'Найти');
|
||||||
|
if (!btn) return null;
|
||||||
|
const r = btn.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
})()`);
|
||||||
|
if (findBtn) {
|
||||||
|
await page.mouse.click(findBtn.x, findBtn.y);
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Close advanced search dialog
|
||||||
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
|
const dialogVisible = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
||||||
|
})()`);
|
||||||
|
if (!dialogVisible) break;
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
await waitForStable(formNum);
|
||||||
|
} catch { /* silently fail — caller will re-scan and handle not_found */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a value from an opened selection form.
|
||||||
|
*
|
||||||
|
* Strategy (escalating):
|
||||||
|
* 1. Scan visible rows for text match (exact → startsWith → includes)
|
||||||
|
* 2. Advanced search (Alt+F, "по части строки") → re-scan
|
||||||
|
* 3. Fallback: simple search (search input + Enter) → re-scan
|
||||||
|
* 4. Not found → Escape → error
|
||||||
|
*
|
||||||
|
* For object search {field: value}: steps 1, then filterList(val, {field}) per entry, then re-scan.
|
||||||
|
* For empty search: pick first visible row.
|
||||||
|
*
|
||||||
|
* @param {number} selFormNum - selection form number
|
||||||
|
* @param {string} fieldName - field being filled (for error messages)
|
||||||
|
* @param {string|Object} search - string for simple search, or { field: value } for per-field search
|
||||||
|
* @param {number} origFormNum - original form number (to verify we returned)
|
||||||
|
* @returns {{ field, ok, method }} or {{ field, error, message }}
|
||||||
|
*/
|
||||||
|
export async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum) {
|
||||||
|
const searchText = typeof search === 'string'
|
||||||
|
? search : (search ? Object.values(search).join(' ') : '');
|
||||||
|
const searchLower = normYo((searchText || '').toLowerCase());
|
||||||
|
|
||||||
|
// Helper: try to select a row; returns result if ok, null if item wasn't selectable (group).
|
||||||
|
let hadUnselectableMatch = false;
|
||||||
|
async function trySelect(row) {
|
||||||
|
const r = await dblclickAndVerify(row, selFormNum, fieldName);
|
||||||
|
if (r.ok) return r;
|
||||||
|
hadUnselectableMatch = true; // found match but couldn't select (possibly group row or overlay)
|
||||||
|
return null; // form still open, try next step
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Scan visible rows (no filtering)
|
||||||
|
if (searchLower) {
|
||||||
|
const row = await scanGridRows(selFormNum, searchLower);
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Advanced search (Alt+F — fast, no overlay issues)
|
||||||
|
if (typeof search === 'object' && search) {
|
||||||
|
// Per-field advanced search via filterList(val, {field})
|
||||||
|
for (const [fld, val] of Object.entries(search)) {
|
||||||
|
try { await filterList(String(val), { field: fld }); } catch { /* proceed */ }
|
||||||
|
}
|
||||||
|
} else if (searchLower) {
|
||||||
|
// Inline advanced search (Alt+F, "по части строки")
|
||||||
|
await advancedSearchInline(selFormNum, searchText);
|
||||||
|
}
|
||||||
|
if (searchLower) {
|
||||||
|
const row = await scanGridRows(selFormNum, searchLower);
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Fallback — simple search via search input (for forms without Alt+F support)
|
||||||
|
if (typeof search === 'string' && searchLower) {
|
||||||
|
const searchInputId = await page.evaluate(`(() => {
|
||||||
|
const p = 'form${selFormNum}_';
|
||||||
|
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||||
|
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
||||||
|
return el ? el.id : null;
|
||||||
|
})()`);
|
||||||
|
if (searchInputId) {
|
||||||
|
try {
|
||||||
|
await page.click(`[id="${searchInputId}"]`);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.keyboard.press('Control+A');
|
||||||
|
await pasteText(searchText);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await waitForStable(selFormNum);
|
||||||
|
} catch { /* proceed */ }
|
||||||
|
const row = await scanGridRows(selFormNum, searchLower);
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Empty search → pick first row; otherwise not found
|
||||||
|
if (!searchLower) {
|
||||||
|
const row = await scanGridRows(selFormNum, '');
|
||||||
|
if (row?.x) {
|
||||||
|
const r = await trySelect(row);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await waitForStable();
|
||||||
|
const searchDesc = typeof search === 'string' ? '"' + search + '"' : JSON.stringify(search);
|
||||||
|
if (hadUnselectableMatch) {
|
||||||
|
return { field: fieldName, error: 'not_selectable',
|
||||||
|
message: 'Found ' + searchDesc + ' in selection form but it is not selectable (group/folder row)' };
|
||||||
|
}
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'No matches in selection form for ' + searchDesc };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect whether a form is a type selection dialog ("Выбор типа данных").
|
||||||
|
* Type dialogs appear when selecting a value for a composite-type field.
|
||||||
|
*
|
||||||
|
* Detection signals (any one is sufficient):
|
||||||
|
* - form{N}_OK element exists (selection forms use "Выбрать", not "OK")
|
||||||
|
* - form{N}_ValueList grid exists (specific to type/value list dialogs)
|
||||||
|
* - Window title contains "Выбор типа" (title attr on .toplineBoxTitle)
|
||||||
|
*/
|
||||||
|
export async function isTypeDialog(formNum) {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const p = 'form' + ${formNum} + '_';
|
||||||
|
const hasOK = !!document.getElementById(p + 'OK');
|
||||||
|
const hasValueList = !!document.getElementById(p + 'ValueList');
|
||||||
|
const hasTitle = [...document.querySelectorAll('.toplineBoxTitle')]
|
||||||
|
.some(el => el.offsetWidth > 0 && /выбор типа/i.test(el.getAttribute('title') || ''));
|
||||||
|
return hasOK || hasValueList || hasTitle;
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a type from the type selection dialog ("Выбор типа данных")
|
||||||
|
* using Ctrl+F search. The dialog has a virtual grid (~5 visible rows),
|
||||||
|
* so Ctrl+F is the only reliable way to find a type.
|
||||||
|
*
|
||||||
|
* Algorithm: Ctrl+F → paste typeName → Enter (search) → Escape (close Find) →
|
||||||
|
* verify selected row matches → Enter (OK)
|
||||||
|
*
|
||||||
|
* @param {number} formNum - type dialog form number
|
||||||
|
* @param {string} typeName - type name to search for (fuzzy, e.g. "Реализация (акт")
|
||||||
|
* @throws {Error} if type not found
|
||||||
|
*/
|
||||||
|
export async function pickFromTypeDialog(formNum, typeName) {
|
||||||
|
// The type dialog is a modal ValueList grid.
|
||||||
|
// Strategy: scan visible rows first (fast path), fall back to Ctrl+F for large lists.
|
||||||
|
//
|
||||||
|
// Key constraints discovered during testing:
|
||||||
|
// - Grid focus: use evaluate(() => gridBody.focus()), NOT page.click({force:true})
|
||||||
|
// which punches through the modal overlay to the form underneath
|
||||||
|
// - Ctrl+F only opens "Найти" if the GRID is focused (otherwise closes the type dialog)
|
||||||
|
// - Buttons: use page.click({force:true}), NOT evaluate(() => el.click())
|
||||||
|
// because evaluate click doesn't trigger 1C's event chain properly
|
||||||
|
// - Enter/Escape in "Найти" close the ENTIRE dialog chain, not just "Найти"
|
||||||
|
// - Closing "Найти" via Cancel resets the search — verify grid while "Найти" is open
|
||||||
|
|
||||||
|
const typeNorm = normYo(typeName.toLowerCase());
|
||||||
|
|
||||||
|
// Helper: read visible rows and find matching ones
|
||||||
|
async function readVisibleRows() {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const grid = document.getElementById('form${formNum}_ValueList');
|
||||||
|
if (!grid) return { visible: [], matches: [] };
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!body) return { visible: [], matches: [] };
|
||||||
|
const lines = body.querySelectorAll('.gridLine');
|
||||||
|
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim();
|
||||||
|
const typeNorm = ${JSON.stringify(typeNorm)};
|
||||||
|
const visible = [];
|
||||||
|
const matches = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const text = norm(line.innerText);
|
||||||
|
if (!text) continue;
|
||||||
|
visible.push(text);
|
||||||
|
if (text.toLowerCase().replace(/ё/gi, 'е').includes(typeNorm)) {
|
||||||
|
const r = line.getBoundingClientRect();
|
||||||
|
matches.push({ text, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { visible, matches };
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists)
|
||||||
|
const scan = await readVisibleRows();
|
||||||
|
|
||||||
|
if (scan.matches.length === 1) {
|
||||||
|
// Single match — click to select, then OK
|
||||||
|
await page.mouse.click(scan.matches[0].x, scan.matches[0].y);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.click(`#form${formNum}_OK`, { force: true });
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scan.matches.length > 1) {
|
||||||
|
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
||||||
|
await waitForStable();
|
||||||
|
throw new Error(`selectValue: multiple types match "${typeName}": ${scan.matches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Not found in visible rows — use Ctrl+F (virtual grid may have more items)
|
||||||
|
|
||||||
|
// Focus the grid via evaluate (does NOT punch through modal like page.click)
|
||||||
|
await page.evaluate(`(() => {
|
||||||
|
const grid = document.getElementById('form${formNum}_ValueList');
|
||||||
|
if (!grid) return;
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (body) body.focus(); else grid.focus();
|
||||||
|
})()`);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Ctrl+F to open "Найти" dialog
|
||||||
|
await page.keyboard.press('Control+f');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Paste search text (focus is on "Что искать" field)
|
||||||
|
await page.keyboard.press('Control+a');
|
||||||
|
await pasteText(typeName);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Find the "Найти" dialog form number (it's > formNum)
|
||||||
|
const findFormNum = await page.evaluate(`(() => {
|
||||||
|
for (let n = ${formNum} + 1; n < ${formNum} + 20; n++) {
|
||||||
|
const btn = document.getElementById('form' + n + '_Find');
|
||||||
|
if (btn && btn.offsetWidth > 0) return n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()`);
|
||||||
|
|
||||||
|
if (findFormNum === null) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await waitForStable();
|
||||||
|
throw new Error('selectValue: Ctrl+F did not open "Найти" dialog in type selection');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click "Найти" — search is client-side (no server round-trip), 500ms is enough
|
||||||
|
await page.click(`#form${findFormNum}_Find`, { force: true });
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Re-read visible rows after search scrolled to match
|
||||||
|
const afterSearch = await readVisibleRows();
|
||||||
|
|
||||||
|
if (afterSearch.matches.length === 0) {
|
||||||
|
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
||||||
|
await waitForStable();
|
||||||
|
throw new Error(`selectValue: type "${typeName}" not found in type selection dialog` +
|
||||||
|
`. Visible: ${(scan.visible || []).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterSearch.matches.length > 1) {
|
||||||
|
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
||||||
|
await waitForStable();
|
||||||
|
throw new Error(`selectValue: multiple types match "${typeName}": ${afterSearch.matches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click OK on type dialog via page.click({force:true}) — bypasses "Найти" modal
|
||||||
|
await page.click(`#form${formNum}_OK`, { force: true });
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill a reference field via clipboard paste + 1C autocomplete.
|
||||||
|
*
|
||||||
|
* Strategy:
|
||||||
|
* 1. Clear field if it has a value (Shift+F4 — native 1C mechanism, no JS errors)
|
||||||
|
* 2. Clipboard paste text (Ctrl+V = trusted event, triggers real 1C autocomplete)
|
||||||
|
* 3. Check editDropDown for autocomplete results → click match or Tab to resolve
|
||||||
|
* 4. Verify result: resolved → ok, not found → clear + error
|
||||||
|
*
|
||||||
|
* Clipboard paste was chosen because:
|
||||||
|
* - Ctrl+V produces trusted browser events that 1C respects for autocomplete
|
||||||
|
* - page.fill() + synthetic keydown/keyup only triggers hints, not real search
|
||||||
|
* - keyboard.type() garbles Cyrillic on some fields
|
||||||
|
*
|
||||||
|
* @returns {{ field, ok?, method?, error?, value?, message?, available? }}
|
||||||
|
*/
|
||||||
|
export async function fillReferenceField(selector, fieldName, value, formNum) {
|
||||||
|
const text = String(value);
|
||||||
|
const escapedSel = selector.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
// Helper: detect new forms opened above the current one (strict — interactive
|
||||||
|
// elements only; fillReferenceField-specific)
|
||||||
|
const detectNewForm = () => helperDetectNewForm(formNum, { strict: true });
|
||||||
|
|
||||||
|
// Helper: clear the field using Shift+F4 (native 1C mechanism)
|
||||||
|
async function clearField() {
|
||||||
|
try {
|
||||||
|
await page.click(selector, { timeout: 3000 });
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
} catch { /* OK */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: check for "not in list" cloud popup (1C shows positioned div with "нет в списке")
|
||||||
|
async function checkNotInListCloud() {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const divs = document.querySelectorAll('div');
|
||||||
|
for (const el of divs) {
|
||||||
|
if (el.offsetWidth === 0 || el.offsetHeight === 0) continue;
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
if (style.position !== 'absolute' && style.position !== 'fixed') continue;
|
||||||
|
const z = parseInt(style.zIndex) || 0;
|
||||||
|
if (z < 100) continue;
|
||||||
|
if ((el.innerText || '').includes('нет в списке')) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0. Dismiss any leftover error modal from a previous operation
|
||||||
|
await dismissPendingErrors();
|
||||||
|
|
||||||
|
// 0a. Try DLB (DropListButton) first — works cleanly for combobox/enum fields
|
||||||
|
// and also for reference fields that show a dropdown.
|
||||||
|
const inputId = selector.match(/\[id="(.+)"\]/)?.[1];
|
||||||
|
// DLB button ID uses field name without _iN suffix (e.g. form1_Field_DLB, not form1_Field_i0_DLB)
|
||||||
|
const dlbId = inputId.replace(/_i\d+$/, '') + '_DLB';
|
||||||
|
const dlbSelector = `[id="${dlbId}"]`;
|
||||||
|
try {
|
||||||
|
const dlbVisible = await page.evaluate(`document.querySelector('${dlbSelector.replace(/'/g, "\\'")}')?.offsetWidth > 0`);
|
||||||
|
if (dlbVisible) {
|
||||||
|
await page.click(dlbSelector);
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const eddState = await readEdd();
|
||||||
|
if (eddState.visible && eddState.items?.length > 0) {
|
||||||
|
const target = normYo(text.toLowerCase());
|
||||||
|
const candidates = eddState.items.filter(i => !i.name.startsWith('Создать'));
|
||||||
|
let match = candidates.find(i => normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase()) === target);
|
||||||
|
if (!match) match = candidates.find(i => normYo(i.name.toLowerCase()).includes(target));
|
||||||
|
if (!match) match = candidates.find(i => {
|
||||||
|
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
||||||
|
return name.includes(target) || target.includes(name);
|
||||||
|
});
|
||||||
|
if (match) {
|
||||||
|
await page.mouse.click(match.x, match.y);
|
||||||
|
await waitForStable();
|
||||||
|
await dismissPendingErrors();
|
||||||
|
return { field: fieldName, ok: true, method: 'dropdown',
|
||||||
|
value: match.name.replace(/\s*\([^)]*\)\s*$/, '') };
|
||||||
|
}
|
||||||
|
// No match in DLB dropdown — close and fall through to paste approach
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
} else if (eddState.visible) {
|
||||||
|
// DLB opened a hint popup (no .eddText items) — close it before proceeding
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* DLB approach failed — fall through to paste */ }
|
||||||
|
|
||||||
|
// 1. Focus (handle surface/modal overlay from previous interaction)
|
||||||
|
await safeClick(selector, { dismissErrors: true });
|
||||||
|
|
||||||
|
// 2. If field already has a value, clear using Shift+F4 (native 1C mechanism).
|
||||||
|
// This is needed for reference fields — Shift+F4 properly clears the ref link.
|
||||||
|
const currentVal = await page.evaluate(`document.querySelector('${escapedSel}')?.value || ''`);
|
||||||
|
if (currentVal) {
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
// Refocus
|
||||||
|
await page.click(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Paste text via clipboard (trusted event → triggers real 1C autocomplete)
|
||||||
|
await pasteText(text);
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// 4. Check editDropDown for autocomplete suggestions
|
||||||
|
const eddState = await readEdd();
|
||||||
|
|
||||||
|
if (eddState.visible && eddState.items?.length > 0) {
|
||||||
|
const target = normYo(text.toLowerCase());
|
||||||
|
// Separate real matches from "Создать:" items
|
||||||
|
const candidates = eddState.items.filter(i => !i.name.startsWith('Создать'));
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
// Find best match (items have format "Name (Code)" — match against name part)
|
||||||
|
let match = candidates.find(i => {
|
||||||
|
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
||||||
|
return name === target;
|
||||||
|
});
|
||||||
|
if (!match) match = candidates.find(i => normYo(i.name.toLowerCase()).includes(target));
|
||||||
|
if (!match) match = candidates.find(i => {
|
||||||
|
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
||||||
|
return name.includes(target) || target.includes(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
await page.mouse.click(match.x, match.y);
|
||||||
|
await waitForStable();
|
||||||
|
await dismissPendingErrors(); // business logic errors (e.g. СПАРК) may appear async
|
||||||
|
return { field: fieldName, ok: true, method: 'dropdown',
|
||||||
|
value: match.name.replace(/\s*\([^)]*\)\s*$/, '') };
|
||||||
|
}
|
||||||
|
// Candidates exist but none match — report them
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await clearField();
|
||||||
|
return { field: fieldName, error: 'not_matched',
|
||||||
|
available: candidates.map(i => i.name.replace(/\s*\([^)]*\)\s*$/, '')) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only "Создать:" items — no existing matches
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await clearField();
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'No existing values match "' + text + '"' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4b. No edd — check for "not in list" cloud that may have appeared during paste
|
||||||
|
if (await checkNotInListCloud()) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await clearField();
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'Value "' + text + '" not found (not in list)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. No edd at all — press Tab to trigger direct resolve
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await waitForStable();
|
||||||
|
await dismissPendingErrors();
|
||||||
|
|
||||||
|
// 5x. Check for "not in list" cloud popup after Tab
|
||||||
|
if (await checkNotInListCloud()) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await clearField();
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'Value "' + text + '" not found (not in list)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5a. New form opened? (creation form = value not found)
|
||||||
|
const newForm = await detectNewForm();
|
||||||
|
if (newForm !== null) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await waitForStable();
|
||||||
|
await clearField();
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'Value "' + text + '" not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5b. Dropdown after Tab?
|
||||||
|
const popup = await page.evaluate(readSubmenuScript());
|
||||||
|
if (Array.isArray(popup) && popup.length > 0) {
|
||||||
|
const realItems = popup.filter(i => !i.name.startsWith('Создать'));
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await clearField();
|
||||||
|
if (realItems.length > 0) {
|
||||||
|
return { field: fieldName, error: 'ambiguous',
|
||||||
|
message: 'Multiple matches for "' + text + '"',
|
||||||
|
available: realItems.map(i => i.name.replace(/\s*\([^)]*\)\s*$/, '')) };
|
||||||
|
}
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'Value "' + text + '" not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5c. Check final value
|
||||||
|
const finalVal = await page.evaluate(`document.querySelector('${escapedSel}')?.value || ''`);
|
||||||
|
if (!finalVal) {
|
||||||
|
// 6. Last resort: try F4 to open selection form and pick from there
|
||||||
|
try {
|
||||||
|
await page.click(selector);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
} catch { /* OK — field may be unfocused */ }
|
||||||
|
await page.keyboard.press('F4');
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
|
||||||
|
const selFormNum = await detectNewForm();
|
||||||
|
if (selFormNum !== null) {
|
||||||
|
const pickResult = await pickFromSelectionForm(selFormNum, fieldName, text, formNum);
|
||||||
|
if (pickResult.ok) return pickResult;
|
||||||
|
// pickFromSelectionForm already closed the form on error
|
||||||
|
}
|
||||||
|
|
||||||
|
return { field: fieldName, error: 'not_found',
|
||||||
|
message: 'Value "' + text + '" not found (field is empty)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { field: fieldName, ok: true, method: 'typeahead', value: finalVal };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a value from a reference field (compound operation).
|
||||||
|
* Handles three patterns:
|
||||||
|
* A) DLB opens an inline dropdown popup — click matching item
|
||||||
|
* B) DLB opens dropdown with history — click "Показать все" or F4 to open selection form
|
||||||
|
* C) DLB opens a separate selection form directly — search + dblclick in grid
|
||||||
|
*/
|
||||||
|
export async function selectValue(fieldName, searchText, { type } = {}) {
|
||||||
|
ensureConnected();
|
||||||
|
await dismissPendingErrors();
|
||||||
|
const formNum = await page.evaluate(detectFormScript());
|
||||||
|
if (formNum === null) throw new Error(`selectValue: no form found`);
|
||||||
|
|
||||||
|
// Detect any new form opened above this one (broad — includes type dialogs).
|
||||||
|
// Hoisted to the top so the composite-type branch can call it before its
|
||||||
|
// original declaration site further below.
|
||||||
|
const detectNewForm = () => helperDetectNewForm(formNum);
|
||||||
|
|
||||||
|
// 1. Find DLB button (fallback to CB — ERP uses Choose Button instead of DLB for some fields)
|
||||||
|
let btn = await page.evaluate(findFieldButtonScript(formNum, fieldName, 'DLB'));
|
||||||
|
if (btn?.error === 'button_not_found') {
|
||||||
|
btn = await page.evaluate(findFieldButtonScript(formNum, fieldName, 'CB'));
|
||||||
|
}
|
||||||
|
if (btn?.error) return btn;
|
||||||
|
if (highlightMode) try { await highlight(fieldName); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
||||||
|
try {
|
||||||
|
|
||||||
|
// === CLEAR FIELD if searchText is empty/null ===
|
||||||
|
if (!searchText && searchText !== 0) {
|
||||||
|
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
||||||
|
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 ===
|
||||||
|
// When `type` is specified, clear the field first to reset cached type,
|
||||||
|
// then open type selection dialog, pick the type, then pick the value.
|
||||||
|
if (type) {
|
||||||
|
// Find and focus the field input
|
||||||
|
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
||||||
|
if (!inputId) throw new Error(`selectValue: field "${btn.fieldName}" input not found`);
|
||||||
|
|
||||||
|
// Clear cached type + value with Shift+F4
|
||||||
|
await page.click(`[id="${inputId}"]`);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Shift+F4');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Re-focus and press F4 to open type selection dialog
|
||||||
|
await page.click(`[id="${inputId}"]`);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('F4');
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
await waitForStable(formNum);
|
||||||
|
|
||||||
|
const newFormNum = await detectNewForm();
|
||||||
|
if (newFormNum === null) {
|
||||||
|
throw new Error(`selectValue: F4 for composite field "${btn.fieldName}" did not open type selection dialog`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isTypeDialog(newFormNum)) {
|
||||||
|
// Pick type from the dialog
|
||||||
|
await pickFromTypeDialog(newFormNum, type);
|
||||||
|
await waitForStable(newFormNum);
|
||||||
|
|
||||||
|
// After type selection, the actual selection form should open
|
||||||
|
const selFormNum = await detectSelectionForm();
|
||||||
|
if (selFormNum === null) {
|
||||||
|
throw new Error(`selectValue: after selecting type "${type}", no selection form opened for "${btn.fieldName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
||||||
|
const state = await getFormState();
|
||||||
|
state.selected = { field: btn.fieldName, search: searchText || null, type, method: 'form' };
|
||||||
|
if (pickResult.error) state.selected.error = pickResult.error;
|
||||||
|
if (pickResult.message) state.selected.message = pickResult.message;
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err) state.errors = err;
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
// Not a type dialog — field is not composite type, proceed with normal selection
|
||||||
|
const pickResult = await pickFromSelectionForm(newFormNum, btn.fieldName, searchText || '', formNum);
|
||||||
|
const state = await getFormState();
|
||||||
|
state.selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
||||||
|
if (pickResult.error) state.selected.error = pickResult.error;
|
||||||
|
if (pickResult.message) state.selected.message = pickResult.message;
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err) state.errors = err;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// === END COMPOSITE TYPE HANDLING ===
|
||||||
|
|
||||||
|
// Auto-enable DCS checkbox if resolved via label
|
||||||
|
if (btn.dcsCheckbox) {
|
||||||
|
const cbSel = `[id="${btn.dcsCheckbox.inputId}"]`;
|
||||||
|
const isChecked = await page.$eval(cbSel, el =>
|
||||||
|
el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select'));
|
||||||
|
if (!isChecked) { await page.click(cbSel); await waitForStable(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: detect selection form (form number > formNum)
|
||||||
|
async function detectSelectionForm() {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const forms = {};
|
||||||
|
document.querySelectorAll('input.editInput[id], a.press[id]').forEach(el => {
|
||||||
|
if (el.offsetWidth === 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;
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectNewForm is hoisted at the top of selectValue (see above).
|
||||||
|
|
||||||
|
// Helper: open selection form and pick value
|
||||||
|
async function openFormAndPick() {
|
||||||
|
await waitForStable(formNum);
|
||||||
|
const selFormNum = await detectSelectionForm();
|
||||||
|
if (selFormNum !== null) {
|
||||||
|
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
||||||
|
const state = await getFormState();
|
||||||
|
state.selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
||||||
|
if (pickResult.error) state.selected.error = pickResult.error;
|
||||||
|
if (pickResult.message) state.selected.message = pickResult.message;
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err) state.errors = err;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: click EDD item via evaluate (bypasses div.surface overlay from DLB)
|
||||||
|
// page.mouse.click() doesn't work here — surface intercepts pointer events.
|
||||||
|
// Dispatching mousedown directly on the element avoids this.
|
||||||
|
async function clickEddItem(itemName) {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const edd = document.getElementById('editDropDown');
|
||||||
|
if (!edd || edd.offsetWidth === 0) return null;
|
||||||
|
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||||
|
const target = ny(${JSON.stringify(itemName.toLowerCase())});
|
||||||
|
const items = [...edd.querySelectorAll('.eddText')].filter(el => el.offsetWidth > 0);
|
||||||
|
function clickEl(el) {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 };
|
||||||
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
||||||
|
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
||||||
|
el.dispatchEvent(new MouseEvent('click', opts));
|
||||||
|
return el.innerText.trim();
|
||||||
|
}
|
||||||
|
// Pass 1: exact match (prefer over partial)
|
||||||
|
for (const el of items) {
|
||||||
|
const t = ny((el.innerText?.trim() || '').toLowerCase());
|
||||||
|
if (t === target) return clickEl(el);
|
||||||
|
const stripped = t.replace(/\\s*\\([^)]*\\)\\s*$/, '');
|
||||||
|
if (stripped === target) return clickEl(el);
|
||||||
|
}
|
||||||
|
// Pass 2: partial match
|
||||||
|
for (const el of items) {
|
||||||
|
const t = ny((el.innerText?.trim() || '').toLowerCase());
|
||||||
|
if (t.includes(target) || target.includes(t.replace(/\\s*\\([^)]*\\)\\s*$/, ''))) return clickEl(el);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: click "Показать все" in EDD footer via evaluate
|
||||||
|
async function clickShowAll() {
|
||||||
|
return page.evaluate(`(() => {
|
||||||
|
const edd = document.getElementById('editDropDown');
|
||||||
|
if (!edd || edd.offsetWidth === 0) return false;
|
||||||
|
let el = edd.querySelector('.eddBottom .hyperlink');
|
||||||
|
if (!el || el.offsetWidth === 0) {
|
||||||
|
const candidates = [...edd.querySelectorAll('span, div, a')]
|
||||||
|
.filter(e => e.offsetWidth > 0 && e.children.length === 0);
|
||||||
|
el = candidates.find(e => {
|
||||||
|
const t = (e.innerText?.trim() || '').toLowerCase();
|
||||||
|
return t === 'показать все' || t === 'show all';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!el) return false;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const opts = { bubbles: true, cancelable: true, clientX: r.x + r.width/2, clientY: r.y + r.height/2 };
|
||||||
|
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
||||||
|
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
||||||
|
el.dispatchEvent(new MouseEvent('click', opts));
|
||||||
|
return true;
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Click DLB (handle funcPanel / surface overlay intercept)
|
||||||
|
const dlbSel = `[id="${btn.buttonId}"]`;
|
||||||
|
await safeClick(dlbSel, { timeout: 5000 });
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
|
||||||
|
// 3A. Check if a dropdown popup appeared (inline quick selection)
|
||||||
|
const popupItems = await page.evaluate(readSubmenuScript());
|
||||||
|
if (Array.isArray(popupItems) && popupItems.length > 0) {
|
||||||
|
const regularItems = popupItems.filter(i => i.kind !== 'showAll');
|
||||||
|
const showAllItem = popupItems.find(i => i.kind === 'showAll');
|
||||||
|
|
||||||
|
if (searchText) {
|
||||||
|
const target = normYo(searchText.toLowerCase());
|
||||||
|
// Try to find match among regular dropdown items
|
||||||
|
let match = regularItems.find(i => normYo(i.name.toLowerCase()) === target);
|
||||||
|
if (!match) match = regularItems.find(i => normYo(i.name.toLowerCase()).includes(target));
|
||||||
|
if (!match) match = regularItems.find(i => {
|
||||||
|
const name = normYo(i.name.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase());
|
||||||
|
return name === target || name.includes(target) || target.includes(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// Click via evaluate to bypass div.surface overlay
|
||||||
|
await clickEddItem(match.name);
|
||||||
|
await waitForStable();
|
||||||
|
const state = await getFormState();
|
||||||
|
state.selected = { field: btn.fieldName, search: searchText, method: 'dropdown' };
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err) state.errors = err;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match in dropdown — try "Показать все" to open selection form
|
||||||
|
if (showAllItem) {
|
||||||
|
await clickShowAll();
|
||||||
|
const formResult = await openFormAndPick();
|
||||||
|
if (formResult) return formResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No "Показать все" — close dropdown, try F4
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Focus the field input and press F4 to open selection form
|
||||||
|
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
||||||
|
if (inputId) {
|
||||||
|
await page.click(`[id="${inputId}"]`);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
await page.keyboard.press('F4');
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
|
||||||
|
const formResult = await openFormAndPick();
|
||||||
|
if (formResult) return formResult;
|
||||||
|
|
||||||
|
// Still nothing — report available items from original dropdown
|
||||||
|
throw new Error(`selectValue: "${searchText}" not found for field "${btn.fieldName}". Available: ${regularItems.map(i => i.name).join(', ') || 'none'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No search text — click first regular item
|
||||||
|
if (regularItems.length > 0) {
|
||||||
|
await clickEddItem(regularItems[0].name);
|
||||||
|
await waitForStable();
|
||||||
|
const state = await getFormState();
|
||||||
|
state.selected = { field: btn.fieldName, search: null, picked: regularItems[0].name, method: 'dropdown' };
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err) state.errors = err;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3B. Check if a new selection form opened directly (use broad detection to also catch type dialogs)
|
||||||
|
const selFormNum = await detectNewForm();
|
||||||
|
if (selFormNum !== null) {
|
||||||
|
// Auto-detect type selection dialog when `type` was not specified
|
||||||
|
if (await isTypeDialog(selFormNum)) {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await waitForStable();
|
||||||
|
throw new Error(`selectValue: field "${btn.fieldName}" opened a type selection dialog — this is a composite-type field. Specify the type: selectValue('${btn.fieldName}', '${searchText || ''}', { type: 'ИмяТипа' })`);
|
||||||
|
}
|
||||||
|
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
||||||
|
const state = await getFormState();
|
||||||
|
state.selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
||||||
|
if (pickResult.error) state.selected.error = pickResult.error;
|
||||||
|
if (pickResult.message) state.selected.message = pickResult.message;
|
||||||
|
const err = await checkForErrors();
|
||||||
|
if (err) state.errors = err;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3C. Neither popup nor form — try F4 as last resort
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const inputId = await findFieldInputId(formNum, btn.fieldName);
|
||||||
|
if (inputId) {
|
||||||
|
await page.click(`[id="${inputId}"]`);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
await page.keyboard.press('F4');
|
||||||
|
await page.waitForTimeout(ACTION_WAIT);
|
||||||
|
|
||||||
|
const formResult = await openFormAndPick();
|
||||||
|
if (formResult) return formResult;
|
||||||
|
|
||||||
|
throw new Error(`selectValue: DLB click for "${btn.fieldName}" did not open a popup or selection form`);
|
||||||
|
|
||||||
|
} finally { if (highlightMode) try { await unhighlight(); } catch {} }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user