mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-06 11:18:56 +03:00
Auto-build: agents (powershell) from 78b5b73
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// web-test forms/click-form v1.1 — click handler for form-element targets: button, tab, submenu, link, field-focus.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// Called by core/click.mjs dispatcher after target is found.
|
||||
// Owns the CDP network-monitor lifecycle for button clicks (server roundtrip waits),
|
||||
// post-click submenu detection (split buttons like "Создать на основании"),
|
||||
// and confirmation hint propagation in the final state.
|
||||
|
||||
import { page, ACTION_WAIT } from '../core/state.mjs';
|
||||
import {
|
||||
detectFormScript, readSubmenuScript,
|
||||
} from '../../dom.mjs';
|
||||
import { checkForErrors } from '../core/errors.mjs';
|
||||
import { waitForStable, startNetworkMonitor } from '../core/wait.mjs';
|
||||
import { safeClick, returnFormState, isInputFocused } from '../core/helpers.mjs';
|
||||
|
||||
/**
|
||||
* Click a form target (button, tab, submenu, link) using its resolved {kind, id, x, y, name}.
|
||||
* Handles three special concerns:
|
||||
* 1. **netMonitor** for `kind: 'button'` — captures CDP requests started by the click
|
||||
* so we can wait for them (when the form doesn't change) before stabilising.
|
||||
* 2. **Submenu detection** — both pre-click (`kind: 'submenu'` already known) and
|
||||
* post-click (split buttons like "Создать на основании" which open a popup).
|
||||
* Returns `submenu[]` items as a hint for the caller.
|
||||
* 3. **Confirmation propagation** — if a confirmation dialog opens as a result of the
|
||||
* click, surface `confirmation` and `hint` fields on the returned state so the
|
||||
* caller can react with Да/Нет/Отмена on the next call.
|
||||
*/
|
||||
export async function clickFormTarget(target, ctx) {
|
||||
const { formNum, timeout } = ctx;
|
||||
let netMonitor = null;
|
||||
|
||||
try {
|
||||
// CDP network monitor BEFORE the click for buttons — captures all server requests
|
||||
// triggered by the click so we can wait for them after.
|
||||
if (target.kind === 'button') {
|
||||
try { netMonitor = await startNetworkMonitor(); } catch {}
|
||||
}
|
||||
|
||||
// Tabs without ID — use coordinate click to avoid global [data-content] ambiguity
|
||||
if (target.kind === 'tab' && !target.id && target.x && target.y) {
|
||||
await page.mouse.click(target.x, target.y);
|
||||
} else {
|
||||
const selector = `[id="${target.id}"]`;
|
||||
// Use Playwright click for proper mousedown/mouseup events
|
||||
await safeClick(selector, { timeout: 5000 });
|
||||
}
|
||||
|
||||
// Pre-known submenu button — read popup items and return them as hints
|
||||
if (target.kind === 'submenu') {
|
||||
await page.waitForTimeout(ACTION_WAIT);
|
||||
const submenuItems = await page.evaluate(readSubmenuScript());
|
||||
const extras = { clicked: { kind: 'submenu', name: target.name } };
|
||||
if (Array.isArray(submenuItems)) {
|
||||
extras.submenu = submenuItems.map(i => i.name);
|
||||
extras.hint = 'Call web_click again with a submenu item name to select it';
|
||||
}
|
||||
return returnFormState(extras);
|
||||
}
|
||||
|
||||
await waitForStable(formNum);
|
||||
|
||||
// Check if the click opened a popup/submenu (split buttons like "Создать на основании")
|
||||
const openedPopup = await page.evaluate(readSubmenuScript());
|
||||
if (Array.isArray(openedPopup) && openedPopup.length > 0) {
|
||||
return returnFormState({
|
||||
clicked: { kind: 'submenu', name: target.name },
|
||||
submenu: openedPopup.map(i => i.name),
|
||||
hint: 'Call web_click again with a submenu item name to select it',
|
||||
});
|
||||
}
|
||||
|
||||
// For buttons that trigger server-side operations (post, write, etc.),
|
||||
// the DOM may stabilise BEFORE the server response arrives.
|
||||
// The CDP monitor (started before click) lets us wait for all in-flight requests
|
||||
// to complete (300ms debounce) or for a modal/balloon/confirm to appear.
|
||||
// Skip for grid edit mode (e.g. "Добавить" row) — no server round-trip expected.
|
||||
if (target.kind === 'button') {
|
||||
const postForm = await page.evaluate(detectFormScript());
|
||||
if (postForm === formNum) {
|
||||
const inGridEdit = await page.evaluate(`(() => {
|
||||
const f = document.activeElement;
|
||||
if (!f || (f.tagName !== 'INPUT' && f.tagName !== 'TEXTAREA')) return false;
|
||||
let n = f; while (n) { if (n.classList?.contains('grid')) return true; n = n.parentElement; }
|
||||
return false;
|
||||
})()`);
|
||||
if (!inGridEdit && netMonitor) {
|
||||
await netMonitor.waitDone(timeout);
|
||||
await waitForStable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build final state with confirmation propagation
|
||||
// (the one custom branch deliberately skipped by Phase 2 — surfaces confirmation
|
||||
// + hint when a save/delete dialog opened as a result of the click).
|
||||
const extras = { clicked: { kind: target.kind, name: target.name } };
|
||||
const err = await checkForErrors();
|
||||
if (err?.confirmation) {
|
||||
extras.confirmation = err.confirmation;
|
||||
extras.hint = 'Call web_click with a button name (e.g. "Да", "Нет", "Отмена") to respond';
|
||||
}
|
||||
return returnFormState(extras);
|
||||
} finally {
|
||||
if (netMonitor) try { await netMonitor.cleanup(); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a form input field (last-resort target kind: 'field') by clicking the input itself —
|
||||
* does NOT change its value. Lets the caller then drive focus-dependent shortcuts
|
||||
* (F4 selection form, Shift+F4 clear, etc.) via getPage().keyboard.
|
||||
* Returns flat form state with `focused: { field, id, ok }`; `ok` reflects whether the
|
||||
* input actually received focus (false for disabled/readonly fields). Never throws on ok=false.
|
||||
*/
|
||||
export async function focusFormField(target, ctx) {
|
||||
const selector = `[id="${target.id}"]`;
|
||||
await safeClick(selector, { timeout: 5000 });
|
||||
await waitForStable(ctx.formNum);
|
||||
const ok = await isInputFocused({ allowTextarea: true });
|
||||
return returnFormState({ focused: { field: target.name, id: target.id, ok } });
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// web-test forms/click-popup v1.0 — click handlers for in-form popups: confirmation dialogs and open submenus.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// Both handlers run BEFORE clickElement's regular target-finding flow:
|
||||
// - clickConfirmationButton intercepts when a pending confirmation dialog is open
|
||||
// - tryClickPopupItem intercepts when a submenu/popup is open from a previous click
|
||||
|
||||
import { page, ACTION_WAIT, normYo } from '../core/state.mjs';
|
||||
import { readSubmenuScript } from '../../dom.mjs';
|
||||
import { waitForStable } from '../core/wait.mjs';
|
||||
import { returnFormState } from '../core/helpers.mjs';
|
||||
|
||||
/**
|
||||
* Click a button in the currently-open confirmation dialog (Да/Нет/Отмена, etc).
|
||||
* Caller is responsible for verifying that a confirmation is actually pending
|
||||
* (via checkForErrors().confirmation) before invoking this handler.
|
||||
*
|
||||
* Throws if no button matching `text` is found in the dialog.
|
||||
*/
|
||||
export async function clickConfirmationButton(text) {
|
||||
const btnResult = await page.evaluate(`(() => {
|
||||
const norm = s => s?.trim().replace(/\\u00a0/g, ' ') || '';
|
||||
const ny = s => s.replace(/ё/gi, 'е').replace(/\\u00a0/g, ' ');
|
||||
const target = ny(${JSON.stringify(text.toLowerCase())});
|
||||
const btns = [...document.querySelectorAll('a.press.pressButton')].filter(el => el.offsetWidth > 0);
|
||||
let best = btns.find(el => ny(norm(el.innerText).toLowerCase()) === target);
|
||||
if (!best) best = btns.find(el => ny(norm(el.innerText).toLowerCase()).includes(target));
|
||||
if (best) {
|
||||
const r = best.getBoundingClientRect();
|
||||
return { name: norm(best.innerText), x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2) };
|
||||
}
|
||||
return { error: 'not_found', available: btns.map(el => norm(el.innerText)).filter(Boolean) };
|
||||
})()`);
|
||||
if (btnResult?.error) {
|
||||
throw new Error(`clickElement: "${text}" not found among confirmation buttons. Available: ${btnResult.available?.join(', ') || 'none'}`);
|
||||
}
|
||||
await page.mouse.click(btnResult.x, btnResult.y);
|
||||
await waitForStable();
|
||||
return returnFormState({ clicked: { kind: 'confirmation', name: btnResult.name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to click an item inside an already-open submenu/popup.
|
||||
*
|
||||
* Returns a form-state result on match (kind: 'popupItem' or 'submenuArrow'),
|
||||
* or `null` if the requested text doesn't match any visible popup item — in
|
||||
* which case the caller should fall through to regular form-element finding.
|
||||
*
|
||||
* @param {string} text — fuzzy-matched against item labels (NBSP/ё-normalised)
|
||||
* @param {Array} popupItems — items already read via readSubmenuScript()
|
||||
*/
|
||||
export async function tryClickPopupItem(text, popupItems) {
|
||||
const target = normYo(text.toLowerCase());
|
||||
let found = popupItems.find(i => normYo(i.name.toLowerCase()) === target);
|
||||
if (!found) found = popupItems.find(i => normYo(i.name.toLowerCase()).includes(target));
|
||||
if (!found) return null;
|
||||
|
||||
// submenuArrow items (group headers like "Создать", "Печать") — hover to expand nested submenu
|
||||
if (found.kind === 'submenuArrow') {
|
||||
// page.hover(selector) is more reliable than page.mouse.move(x,y) —
|
||||
// some submenu groups don't expand with plain mouse.move
|
||||
if (found.id) {
|
||||
await page.hover(`[id="${found.id}"]`);
|
||||
} else {
|
||||
await page.mouse.move(found.x, found.y);
|
||||
}
|
||||
await page.waitForTimeout(ACTION_WAIT);
|
||||
const nestedItems = await page.evaluate(readSubmenuScript());
|
||||
const extras = { clicked: { kind: 'submenuArrow', name: found.name } };
|
||||
if (Array.isArray(nestedItems)) {
|
||||
extras.submenu = nestedItems.map(i => i.name);
|
||||
extras.hint = 'Call web_click again with a submenu item name to select it';
|
||||
}
|
||||
return returnFormState(extras);
|
||||
}
|
||||
|
||||
// Regular submenu/dropdown items — trusted events required.
|
||||
// Use mouse.click(x,y) when in viewport; use :visible selector for clipped items
|
||||
// (same ID can exist hidden in parent cloud AND visible in nested cloud).
|
||||
const vpHeight = await page.evaluate('window.innerHeight');
|
||||
if (found.x && found.y && found.y > 0 && found.y < vpHeight) {
|
||||
await page.mouse.click(found.x, found.y);
|
||||
} else if (found.id) {
|
||||
await page.click(`[id="${found.id}"]:visible`);
|
||||
} else if (found.x && found.y) {
|
||||
await page.mouse.click(found.x, found.y);
|
||||
}
|
||||
await waitForStable();
|
||||
return returnFormState({ clicked: { kind: 'popupItem', name: found.name } });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// web-test forms/close v1.18 — Close current form via Escape, handle save-changes confirmation.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, recorder, ensureConnected } from '../core/state.mjs';
|
||||
import { detectFormScript } from '../../dom.mjs';
|
||||
import { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
|
||||
import { waitForStable } from '../core/wait.mjs';
|
||||
import { returnFormState } from '../core/helpers.mjs';
|
||||
import { getFormState } from './state.mjs';
|
||||
|
||||
/**
|
||||
* Close the current form/dialog via Escape.
|
||||
* @param {Object} [opts]
|
||||
* @param {boolean} [opts.save] - Handle "Save changes?" confirmation automatically:
|
||||
* true → click "Да" (save and close)
|
||||
* false → click "Нет" (discard and close)
|
||||
* undefined → return confirmation as hint for caller to decide
|
||||
*/
|
||||
export async function closeForm({ save } = {}) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
// If platform dialogs are open, close them instead of pressing Escape
|
||||
const pd = await detectPlatformDialogs();
|
||||
if (pd.length) {
|
||||
await closePlatformDialogs();
|
||||
await page.waitForTimeout(300);
|
||||
return returnFormState({ closed: true, closedPlatformDialogs: pd });
|
||||
}
|
||||
const beforeForm = await page.evaluate(detectFormScript());
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForStable(beforeForm);
|
||||
const state = await getFormState();
|
||||
const err = await checkForErrors();
|
||||
if (err?.confirmation) {
|
||||
if (save === true || save === false) {
|
||||
const label = save ? 'Да' : 'Нет';
|
||||
const btnSel = `#form${err.confirmation.formNum}_container a.press.pressButton`;
|
||||
const btns = await page.$$(btnSel);
|
||||
for (const b of btns) {
|
||||
const txt = (await b.textContent()).trim();
|
||||
if (txt === label) {
|
||||
if (recorder) await page.waitForTimeout(500); // show confirmation to viewer during recording
|
||||
await b.click({ force: true });
|
||||
await waitForStable(beforeForm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const afterForm = await page.evaluate(detectFormScript());
|
||||
return returnFormState({ closed: afterForm !== beforeForm });
|
||||
}
|
||||
state.confirmation = err.confirmation;
|
||||
state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel';
|
||||
return state;
|
||||
}
|
||||
return returnFormState({ closed: state.form !== beforeForm });
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// web-test forms/fill v1.20 — Fill form fields by name (text/checkbox/date/number/dropdown/reference; array → multi-select via selectValue). Delegates references to selectValue / fillReferenceField.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
page, ensureConnected, ACTION_WAIT, highlightMode, normYo,
|
||||
} from '../core/state.mjs';
|
||||
import {
|
||||
detectFormScript, resolveFieldsScript,
|
||||
} from '../../dom.mjs';
|
||||
import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs';
|
||||
import { waitForStable, startNetworkMonitor } from '../core/wait.mjs';
|
||||
import { highlight, unhighlight } from '../recording/highlight.mjs';
|
||||
import {
|
||||
fillReferenceField, selectValue, pickFromSelectionForm,
|
||||
isTypeDialog, pickFromTypeDialog,
|
||||
} from './select-value.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { returnFormState } from '../core/helpers.mjs';
|
||||
|
||||
/** Fill fields on the current form via Playwright page.fill(). Returns fill results + updated form. */
|
||||
export async function fillFields(fields) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
const formNum = await page.evaluate(detectFormScript());
|
||||
if (formNum === null) throw new Error('fillFields: no form found');
|
||||
|
||||
// Resolve field names to element IDs
|
||||
const resolved = await page.evaluate(resolveFieldsScript(formNum, fields));
|
||||
const results = [];
|
||||
|
||||
for (const r of resolved) {
|
||||
if (r.error) {
|
||||
results.push(r);
|
||||
continue;
|
||||
}
|
||||
// Array value → multi-select. Delegate to selectValue's array branch (auto-detects
|
||||
// the surface) so fillFields({field:[...]}) works the same as selectValue(field,[...]).
|
||||
if (Array.isArray(fields[r.field])) {
|
||||
const sv = await selectValue(r.field, fields[r.field]);
|
||||
const sel = sv.selected || {};
|
||||
if (sel.error) {
|
||||
results.push({ field: r.field, error: sel.error });
|
||||
} else {
|
||||
const res = { field: r.field, ok: true, values: sel.values || [], method: 'multi-select' };
|
||||
if (sel.notSelected?.length) res.notSelected = sel.notSelected;
|
||||
results.push(res);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Auto-highlight the field input before filling
|
||||
if (highlightMode && r.inputId) {
|
||||
try {
|
||||
await page.evaluate(({ id }) => {
|
||||
const target = document.getElementById(id);
|
||||
if (!target) return;
|
||||
let div = document.getElementById('__web_test_highlight');
|
||||
if (!div) { div = document.createElement('div'); div.id = '__web_test_highlight'; document.body.appendChild(div); }
|
||||
const r = target.getBoundingClientRect();
|
||||
div.style.cssText = 'position:fixed;pointer-events:none;z-index:999998;top:' + (r.y-4) + 'px;left:' + (r.x-4) + 'px;width:' + (r.width+8) + 'px;height:' + (r.height+8) + 'px;outline:3px solid #e74c3c;border-radius:4px;box-shadow:0 0 16px #e74c3c80';
|
||||
}, { id: r.inputId });
|
||||
await page.waitForTimeout(500);
|
||||
await unhighlight();
|
||||
} catch {}
|
||||
}
|
||||
try {
|
||||
// Auto-enable DCS checkbox if resolved via label
|
||||
if (r.dcsCheckbox && !r.dcsCheckbox.checked) {
|
||||
await page.click(`[id="${r.dcsCheckbox.inputId}"]`);
|
||||
await waitForStable();
|
||||
}
|
||||
const selector = `[id="${r.inputId}"]`;
|
||||
// Clear field via Shift+F4 if value is empty (not applicable to checkbox/radio)
|
||||
const rawValue = fields[r.field];
|
||||
const isEmpty = rawValue === '' || rawValue === null || rawValue === undefined;
|
||||
if (isEmpty && !r.isCheckbox && !r.isRadio) {
|
||||
await page.click(selector);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Shift+F4');
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('Tab');
|
||||
await waitForStable();
|
||||
results.push({ field: r.field, ok: true, value: '', method: 'clear' });
|
||||
continue;
|
||||
}
|
||||
if (r.isCheckbox) {
|
||||
// Checkbox: compare desired with current, toggle if mismatch
|
||||
const desired = String(fields[r.field]).toLowerCase();
|
||||
const wantChecked = ['true', '1', 'да', 'yes', 'on'].includes(desired);
|
||||
if (wantChecked !== r.checked) {
|
||||
await page.click(selector);
|
||||
await waitForStable();
|
||||
}
|
||||
results.push({ field: r.field, ok: true, value: String(wantChecked), method: 'toggle' });
|
||||
} else if (r.isRadio) {
|
||||
// Radio button: find option by label (fuzzy match) and click it
|
||||
const desired = normYo(String(fields[r.field]).toLowerCase());
|
||||
const opt = r.options.find(o => normYo(o.label.toLowerCase()) === desired)
|
||||
|| r.options.find(o => normYo(o.label.toLowerCase()).includes(desired));
|
||||
if (opt) {
|
||||
// Option 0 = base element (no suffix), options 1+ = #N#radio
|
||||
const radioId = opt.index === 0 ? r.inputId : `${r.inputId}#${opt.index}#radio`;
|
||||
await page.click(`[id="${radioId}"]`);
|
||||
await waitForStable();
|
||||
results.push({ field: r.field, ok: true, value: opt.label, method: 'radio' });
|
||||
} else {
|
||||
results.push({ field: r.field, error: 'option_not_found', available: r.options.map(o => o.label) });
|
||||
}
|
||||
} else if (r.hasSelect) {
|
||||
// Combobox/reference with DLB: DLB-first, then paste fallback
|
||||
const refResult = await fillReferenceField(selector, r.field, fields[r.field], formNum);
|
||||
results.push(refResult);
|
||||
} else if (r.hasPick && (r.isDate || r.isCalc)) {
|
||||
// Date/time (calendar CB) or numeric (calculator CB) field — use paste:
|
||||
// the pick button is a calendar/calculator widget, not a selection form.
|
||||
await page.click(selector);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(fields[r.field]);
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('Tab');
|
||||
await waitForStable();
|
||||
results.push({ field: r.field, ok: true, value: String(fields[r.field]), method: 'paste' });
|
||||
} else if (r.hasPick) {
|
||||
// Reference field with CB (non-editable or editable ref): delegate to selectValue (F4 → selection form)
|
||||
const svResult = await selectValue(r.field, String(fields[r.field]));
|
||||
if (svResult?.error) {
|
||||
results.push({ field: r.field, error: svResult.error, message: svResult.message });
|
||||
} else {
|
||||
results.push({ field: r.field, ok: true, value: svResult.value || String(fields[r.field]), method: svResult.method || 'form' });
|
||||
}
|
||||
} else {
|
||||
// Plain field: clipboard paste + Tab to commit
|
||||
// page.fill() sets DOM value but doesn't trigger 1C input events;
|
||||
// clipboard paste (Ctrl+V) is a trusted event that 1C processes correctly.
|
||||
await page.click(selector);
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(fields[r.field]);
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('Tab');
|
||||
await waitForStable();
|
||||
results.push({ field: r.field, ok: true, value: String(fields[r.field]), method: 'paste' });
|
||||
}
|
||||
} catch (e) {
|
||||
results.push({ field: r.field, error: e.message });
|
||||
}
|
||||
if (highlightMode) try { await unhighlight(); } catch {}
|
||||
}
|
||||
|
||||
const failed = results.filter(r => r.error);
|
||||
if (failed.length > 0) {
|
||||
const details = failed.map(f => ` ${f.field}: ${f.message || f.error}${f.available ? ' (available: ' + f.available.join(', ') + ')' : ''}`).join('\n');
|
||||
throw new Error(`fillFields: ${failed.length} of ${results.length} field(s) failed:\n${details}`);
|
||||
}
|
||||
return returnFormState({ filled: results });
|
||||
}
|
||||
|
||||
/** Convenience alias: fill a single field. Same as fillFields({ name: value }). */
|
||||
export async function fillField(name, value) {
|
||||
return fillFields({ [name]: value });
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
// web-test engine/forms/state v1.17 — central form-state reader.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// getFormState — the canonical "what's on the screen right now" call. Combines:
|
||||
// 1. DOM script (getFormStateScript) → form structure (fields, buttons, tables, openForms, ...)
|
||||
// 2. checkForErrors → state.errors + state.confirmation hint
|
||||
// 3. detectPlatformDialogs → state.platformDialogs (About / Support Info / Error Report)
|
||||
//
|
||||
// Returned by virtually every action-function as the "after" snapshot.
|
||||
|
||||
import { page, ensureConnected } from '../core/state.mjs';
|
||||
import { getFormStateScript } from '../../dom.mjs';
|
||||
import { checkForErrors, detectPlatformDialogs } from '../core/errors.mjs';
|
||||
|
||||
/** Read current form state. Single evaluate call via combined script. */
|
||||
export async function getFormState() {
|
||||
ensureConnected();
|
||||
const state = await page.evaluate(getFormStateScript());
|
||||
const err = await checkForErrors();
|
||||
if (err) {
|
||||
state.errors = err;
|
||||
if (err.confirmation) {
|
||||
state.confirmation = err.confirmation;
|
||||
state.hint = 'Call web_click with a button name (e.g. "Да", "Нет", "Отмена") to respond';
|
||||
}
|
||||
}
|
||||
// Detect platform-level dialogs (About, Support Info, Error Report)
|
||||
// These are NOT 1C forms — invisible to detectForms() and not closeable via Escape.
|
||||
const pd = await detectPlatformDialogs();
|
||||
if (pd.length) state.platformDialogs = pd;
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user