mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-11 16:34:57 +03:00
refactor(web-test): этап E.13 — финализация (v1.17 + чистый facade + чистка)
1. Версия v1.16 → v1.17 во всех заголовках движка.
2. browser.mjs стал чистым facade — только re-exports, 0 функций определено.
Было: 249 LOC с 4 настоящими функциями (saveClipboard, restoreClipboard,
pasteText, getFormState) — теперь 57 LOC чистых re-export'ов.
3. engine/core/clipboard.mjs — новый модуль:
pasteText + saveClipboard + restoreClipboard (~85 LOC, был в browser.mjs).
4. engine/core/form-state.mjs — новый модуль:
getFormState — центральный читатель состояния формы (~30 LOC).
5. Убрано 12 циклических импортов из engine/* → ../../browser.mjs:
- Все читатели pasteText теперь импортят из engine/core/clipboard.mjs
- Все читатели getFormState — из engine/core/form-state.mjs
- session.mjs → nav/navigation.mjs (getPageState напрямую)
- filter.mjs → core/click.mjs (clickElement напрямую)
Граф зависимостей стал деревом (без обратных рёбер).
6. Убраны _-префиксы у 9 функций, которые стали приватными внутри своих
модулей (раньше _ означало "приватная для browser.mjs"):
_detectPlatformDialogs → detectPlatformDialogs
_closePlatformDialogs → closePlatformDialogs
_parseErrorStack → parseErrorStack
_fetchStackViaReport → fetchStackViaReport
_fetchStackViaHamburger → fetchStackViaHamburger
_logoutSlot → logoutSlot
_saveActiveSlot → saveActiveSlot
_activateSlot → activateSlot
_attachSessionListeners → attachSessionListeners
Публичный API: 56 экспортов, идентичный исходному.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,238 +1,47 @@
|
||||
// web-test browser v1.16 — Playwright browser management for 1C web client
|
||||
// web-test browser v1.17 — engine facade: re-exports the public API from engine/*
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Playwright browser management for 1C web client.
|
||||
*
|
||||
* Maintains a single browser instance across MCP tool calls.
|
||||
* Handles connection, navigation, waiting, screenshots.
|
||||
* Public API of the web-test engine. Pure re-export facade — no logic here.
|
||||
* Implementation lives in `./engine/*`. External callers (run.mjs, exec scripts,
|
||||
* tests) import from this file; engine internals import each other directly.
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn, execFileSync } from 'child_process';
|
||||
import { statSync, mkdirSync, existsSync as fsExistsSync, writeFileSync, readFileSync, rmSync, readdirSync } from 'fs';
|
||||
import { dirname, resolve as pathResolve, join as pathJoin, basename, extname } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import {
|
||||
readSectionsScript, readTabsScript, readCommandsScript,
|
||||
readFormScript, navigateSectionScript, openCommandScript,
|
||||
findClickTargetScript, findFieldButtonScript, readSubmenuScript,
|
||||
resolveFieldsScript, getFormStateScript,
|
||||
detectFormScript, readTableScript, checkErrorsScript,
|
||||
switchTabScript, resolveGridScript
|
||||
} from './dom.mjs';
|
||||
|
||||
// Module-level state, constants, normYo and resolveProjectPath live in core/state.mjs.
|
||||
// Imported as live bindings — reads stay current; writes go through setters.
|
||||
import {
|
||||
browser, page, sessionPrefix, seanceId, recorder,
|
||||
lastCaptions, lastRecordingDuration, highlightMode,
|
||||
persistentUserDataDir, preserveClipboard, clipboardWarnLogged,
|
||||
contexts, activeContextName, activeMode,
|
||||
setBrowser, setPage, setSessionPrefix, setSeanceId, setRecorder,
|
||||
setLastCaptions, setLastRecordingDuration, setHighlightMode,
|
||||
setPersistentUserDataDir, setActiveContextName, setActiveMode,
|
||||
setClipboardWarnLogged,
|
||||
LOAD_TIMEOUT, INIT_TIMEOUT, ACTION_WAIT, MAX_WAIT, POLL_INTERVAL, STABLE_CYCLES,
|
||||
EXT_ID, projectRoot, resolveProjectPath, normYo,
|
||||
isConnected, ensureConnected, getPage, setPreserveClipboard,
|
||||
// ── core ──────────────────────────────────────────────────────────────────
|
||||
export {
|
||||
isConnected, getPage, ensureConnected, setPreserveClipboard,
|
||||
} from './engine/core/state.mjs';
|
||||
export {
|
||||
pasteText, saveClipboard, restoreClipboard,
|
||||
} from './engine/core/clipboard.mjs';
|
||||
export { getFormState } from './engine/core/form-state.mjs';
|
||||
export { fetchErrorStack } from './engine/core/errors.mjs';
|
||||
export { clickElement } from './engine/core/click.mjs';
|
||||
|
||||
export { isConnected, getPage, setPreserveClipboard, ensureConnected };
|
||||
export async function saveClipboard() {
|
||||
if (!page) return;
|
||||
try {
|
||||
await page.evaluate(async () => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read();
|
||||
const saved = [];
|
||||
for (const item of items) {
|
||||
const types = {};
|
||||
for (const t of item.types) types[t] = await item.getType(t);
|
||||
saved.push(types);
|
||||
}
|
||||
window.__webTestSavedClipboard = saved;
|
||||
delete window.__webTestClipboardError;
|
||||
} catch (e) {
|
||||
window.__webTestSavedClipboard = null;
|
||||
window.__webTestClipboardError = e?.name || String(e);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// page.evaluate itself failed (closed page, navigation in flight) — skip.
|
||||
}
|
||||
}
|
||||
export async function restoreClipboard() {
|
||||
if (!page) return;
|
||||
let err = null;
|
||||
try {
|
||||
err = await page.evaluate(async () => {
|
||||
const saved = window.__webTestSavedClipboard;
|
||||
const captured = window.__webTestClipboardError || null;
|
||||
delete window.__webTestSavedClipboard;
|
||||
delete window.__webTestClipboardError;
|
||||
try {
|
||||
if (!saved || saved.length === 0) {
|
||||
// Save failed (e.g. CF_HDROP from Explorer not readable via Clipboard API)
|
||||
// or buffer was empty. Either way, the test's writeText already destroyed
|
||||
// any prior native formats in the OS clipboard, so explicitly clear here
|
||||
// to avoid leaking the test value into the user's clipboard.
|
||||
await navigator.clipboard.writeText('');
|
||||
return captured;
|
||||
}
|
||||
const items = saved.map(types => new ClipboardItem(types));
|
||||
await navigator.clipboard.write(items);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e?.name || String(e);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (err && !clipboardWarnLogged) {
|
||||
setClipboardWarnLogged(true);
|
||||
console.error(`[web-test] clipboard preserve skipped: ${err} (logged once per session)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste `text` via OS clipboard (the only trusted-paste path that 1C respects
|
||||
* for autocomplete and Cyrillic). Wraps the writeText+confirm-key pair in a
|
||||
* narrow save/restore so a user's clipboard survives the test run — the window
|
||||
* between save and restore is microseconds.
|
||||
*
|
||||
* - `confirm` — key (string) or sequence (array) to press after writeText.
|
||||
* Defaults to 'Control+V'. Use ['Control+a', 'Control+v'] for select-all-then-paste,
|
||||
* or 'Shift+F11' for the goto-link dialog.
|
||||
* - `postDelay` — ms to wait between confirm-press and restore, for dialogs
|
||||
* that read clipboard asynchronously (e.g. Shift+F11). Default 0.
|
||||
*/
|
||||
export async function pasteText(text, { confirm = 'Control+V', postDelay = 0 } = {}) {
|
||||
if (!page) return;
|
||||
if (preserveClipboard) await saveClipboard();
|
||||
try {
|
||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
||||
if (Array.isArray(confirm)) {
|
||||
for (const key of confirm) await page.keyboard.press(key);
|
||||
} else if (confirm) {
|
||||
await page.keyboard.press(confirm);
|
||||
}
|
||||
if (postDelay) await page.waitForTimeout(postDelay);
|
||||
} finally {
|
||||
if (preserveClipboard) await restoreClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Session lifecycle + multi-context — extracted to core/session.mjs
|
||||
// ============================================================
|
||||
// ── session ───────────────────────────────────────────────────────────────
|
||||
export {
|
||||
connect, disconnect, attach, detach, getSession,
|
||||
createContext, setActiveContext, listContexts, getActiveContext,
|
||||
hasContext, closeContext,
|
||||
} from './engine/core/session.mjs';
|
||||
|
||||
// ============================================================
|
||||
// Wait + error/modal handling — extracted to core/{wait,errors}.mjs
|
||||
// ============================================================
|
||||
import {
|
||||
waitForStable, waitForCondition, startNetworkMonitor,
|
||||
} from './engine/core/wait.mjs';
|
||||
import {
|
||||
closeModals, checkForErrors, dismissPendingErrors, fetchErrorStack,
|
||||
_detectPlatformDialogs, _closePlatformDialogs,
|
||||
} from './engine/core/errors.mjs';
|
||||
import {
|
||||
safeClick, findFieldInputId, readEdd, returnFormState,
|
||||
detectNewForm as helperDetectNewForm,
|
||||
} from './engine/core/helpers.mjs';
|
||||
import { getGridToggleIcon, shouldClickToggle } from './engine/table/grid-toggle.mjs';
|
||||
// Re-export only what was publicly exported before the refactor.
|
||||
// waitForStable/waitForCondition/startNetworkMonitor/closeModals/checkForErrors/
|
||||
// dismissPendingErrors are internal helpers — imported above for local use only.
|
||||
export { fetchErrorStack } from './engine/core/errors.mjs';
|
||||
|
||||
/* getPage moved to core/state.mjs */
|
||||
|
||||
// ============================================================
|
||||
// Navigation — extracted to nav/navigation.mjs
|
||||
// ============================================================
|
||||
// ── navigation ────────────────────────────────────────────────────────────
|
||||
export {
|
||||
getPageState, getSections, navigateSection, getCommands,
|
||||
openCommand, switchTab, openFile, navigateLink,
|
||||
} from './engine/nav/navigation.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;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Table reading + SpreadsheetDocument — extracted to table/spreadsheet.mjs
|
||||
// ============================================================
|
||||
export { readTable } from './engine/table/grid.mjs';
|
||||
export { readSpreadsheet } from './engine/table/spreadsheet.mjs';
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Value selection (DLB/CB) — extracted to forms/select-value.mjs
|
||||
// ============================================================
|
||||
// ── forms ─────────────────────────────────────────────────────────────────
|
||||
export { selectValue } from './engine/forms/select-value.mjs';
|
||||
import {
|
||||
selectValue, pickFromSelectionForm, isTypeDialog, pickFromTypeDialog,
|
||||
fillReferenceField,
|
||||
} from './engine/forms/select-value.mjs';
|
||||
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Fill fields — extracted to forms/fill.mjs
|
||||
// ============================================================
|
||||
export { fillFields, fillField } from './engine/forms/fill.mjs';
|
||||
|
||||
|
||||
// ============================================================
|
||||
// clickElement dispatcher — extracted to core/click.mjs
|
||||
// ============================================================
|
||||
export { clickElement } from './engine/core/click.mjs';
|
||||
import { clickElement } from './engine/core/click.mjs';
|
||||
|
||||
// ============================================================
|
||||
// Close form — extracted to forms/close.mjs
|
||||
// ============================================================
|
||||
export { closeForm } from './engine/forms/close.mjs';
|
||||
|
||||
|
||||
|
||||
// ============================================================
|
||||
// fillTableRow / deleteTableRow — extracted to table/{row-fill,grid}.mjs
|
||||
// ============================================================
|
||||
// ── tables ────────────────────────────────────────────────────────────────
|
||||
export { readTable, deleteTableRow } from './engine/table/grid.mjs';
|
||||
export { readSpreadsheet } from './engine/table/spreadsheet.mjs';
|
||||
export { fillTableRow } from './engine/table/row-fill.mjs';
|
||||
export { deleteTableRow } from './engine/table/grid.mjs';
|
||||
|
||||
// ============================================================
|
||||
// List filters — extracted to table/filter.mjs
|
||||
// ============================================================
|
||||
export { filterList, unfilterList } from './engine/table/filter.mjs';
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Recording, captions, narration, highlight — extracted to recording/*
|
||||
// ============================================================
|
||||
// ── recording / overlays ──────────────────────────────────────────────────
|
||||
export {
|
||||
screenshot, wait, isRecording, startRecording, stopRecording,
|
||||
} from './engine/recording/capture.mjs';
|
||||
@@ -245,5 +54,3 @@ export {
|
||||
highlight, unhighlight, setHighlight, isHighlightMode,
|
||||
} from './engine/recording/highlight.mjs';
|
||||
export { addNarration } from './engine/recording/narration.mjs';
|
||||
|
||||
/* ensureConnected moved to core/state.mjs */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test core/click v1.16 — clickElement dispatcher: spreadsheet cells, submenus, grid groups/trees, buttons/links, tabs.
|
||||
// web-test core/click v1.17 — clickElement dispatcher: spreadsheet cells, submenus, grid groups/trees, buttons/links, tabs.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
clickSpreadsheetCell, findSpreadsheetCellByText,
|
||||
} from '../table/spreadsheet.mjs';
|
||||
// getFormState still in browser.mjs.
|
||||
import { getFormState } from '../../browser.mjs';
|
||||
import { getFormState } from './form-state.mjs';
|
||||
|
||||
/** Click a button/hyperlink/tab on the current form. Use {dblclick: true} to double-click (open items from lists).
|
||||
* First argument can also be an object { row, column } to click a SpreadsheetDocument cell. */
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// web-test engine/core/clipboard v1.17 — OS-clipboard preservation around trusted paste.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// pasteText() — the only path 1C respects for autocomplete and Cyrillic input.
|
||||
// saveClipboard/restoreClipboard preserve full clipboard contents (all MIME
|
||||
// types) around the writeText+Ctrl+V pair so a user's concurrent Ctrl+C isn't
|
||||
// clobbered. Blobs are stashed on `window` to avoid CDP serialization.
|
||||
|
||||
import {
|
||||
page, preserveClipboard, clipboardWarnLogged, setClipboardWarnLogged,
|
||||
} from './state.mjs';
|
||||
|
||||
export async function saveClipboard() {
|
||||
if (!page) return;
|
||||
try {
|
||||
await page.evaluate(async () => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read();
|
||||
const saved = [];
|
||||
for (const item of items) {
|
||||
const types = {};
|
||||
for (const t of item.types) types[t] = await item.getType(t);
|
||||
saved.push(types);
|
||||
}
|
||||
window.__webTestSavedClipboard = saved;
|
||||
delete window.__webTestClipboardError;
|
||||
} catch (e) {
|
||||
window.__webTestSavedClipboard = null;
|
||||
window.__webTestClipboardError = e?.name || String(e);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// page.evaluate itself failed (closed page, navigation in flight) — skip.
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreClipboard() {
|
||||
if (!page) return;
|
||||
let err = null;
|
||||
try {
|
||||
err = await page.evaluate(async () => {
|
||||
const saved = window.__webTestSavedClipboard;
|
||||
const captured = window.__webTestClipboardError || null;
|
||||
delete window.__webTestSavedClipboard;
|
||||
delete window.__webTestClipboardError;
|
||||
try {
|
||||
if (!saved || saved.length === 0) {
|
||||
// Save failed (e.g. CF_HDROP from Explorer not readable via Clipboard API)
|
||||
// or buffer was empty. Either way, the test's writeText already destroyed
|
||||
// any prior native formats in the OS clipboard, so explicitly clear here
|
||||
// to avoid leaking the test value into the user's clipboard.
|
||||
await navigator.clipboard.writeText('');
|
||||
return captured;
|
||||
}
|
||||
const items = saved.map(types => new ClipboardItem(types));
|
||||
await navigator.clipboard.write(items);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e?.name || String(e);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (err && !clipboardWarnLogged) {
|
||||
setClipboardWarnLogged(true);
|
||||
console.error(`[web-test] clipboard preserve skipped: ${err} (logged once per session)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste `text` via OS clipboard (the only trusted-paste path that 1C respects
|
||||
* for autocomplete and Cyrillic). Wraps the writeText+confirm-key pair in a
|
||||
* narrow save/restore so a user's clipboard survives the test run — the window
|
||||
* between save and restore is microseconds.
|
||||
*
|
||||
* - `confirm` — key (string) or sequence (array) to press after writeText.
|
||||
* Defaults to 'Control+V'. Use ['Control+a', 'Control+v'] for select-all-then-paste,
|
||||
* or 'Shift+F11' for the goto-link dialog.
|
||||
* - `postDelay` — ms to wait between confirm-press and restore, for dialogs
|
||||
* that read clipboard asynchronously (e.g. Shift+F11). Default 0.
|
||||
*/
|
||||
export async function pasteText(text, { confirm = 'Control+V', postDelay = 0 } = {}) {
|
||||
if (!page) return;
|
||||
if (preserveClipboard) await saveClipboard();
|
||||
try {
|
||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
||||
if (Array.isArray(confirm)) {
|
||||
for (const key of confirm) await page.keyboard.press(key);
|
||||
} else if (confirm) {
|
||||
await page.keyboard.press(confirm);
|
||||
}
|
||||
if (postDelay) await page.waitForTimeout(postDelay);
|
||||
} finally {
|
||||
if (preserveClipboard) await restoreClipboard();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test core/errors v1.16 — Error/modal/platform-dialog handling: dismiss, detect, fetch stack from 1C UI.
|
||||
// web-test core/errors v1.17 — Error/modal/platform-dialog handling: dismiss, detect, fetch stack from 1C UI.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page } from './state.mjs';
|
||||
@@ -62,8 +62,8 @@ export async function dismissPendingErrors() {
|
||||
// Close leftover platform dialogs first (About, Support Info, Error Report)
|
||||
// These block all interaction via modalSurface and are invisible to 1C form detection
|
||||
try {
|
||||
const pd = await _detectPlatformDialogs();
|
||||
if (pd.length) await _closePlatformDialogs();
|
||||
const pd = await detectPlatformDialogs();
|
||||
if (pd.length) await closePlatformDialogs();
|
||||
} catch { /* OK */ }
|
||||
const err = await checkForErrors();
|
||||
if (!err?.modal) return null;
|
||||
@@ -84,7 +84,7 @@ export async function dismissPendingErrors() {
|
||||
* Detect open platform-level dialogs (About, Support Info, Error Report).
|
||||
* Returns array of { type, title? } for each detected dialog, or empty array.
|
||||
*/
|
||||
export async function _detectPlatformDialogs() {
|
||||
export async function detectPlatformDialogs() {
|
||||
return await page.evaluate(() => {
|
||||
const result = [];
|
||||
// "О программе" dialog
|
||||
@@ -114,7 +114,7 @@ export async function _detectPlatformDialogs() {
|
||||
* These are NOT 1C forms — they are platform UI overlays invisible to getFormState().
|
||||
* Each close is wrapped in try/catch to avoid cascading failures.
|
||||
*/
|
||||
export async function _closePlatformDialogs() {
|
||||
export async function closePlatformDialogs() {
|
||||
await page.evaluate(() => {
|
||||
// "Подробный текст ошибки" OK button (inside error report detail view)
|
||||
// It's a cloud window with its own OK button — look for visible pressDefault in small ps*win
|
||||
@@ -142,7 +142,7 @@ export async function _closePlatformDialogs() {
|
||||
* Input: raw text from errJournalInput (first block) or "Подробный текст ошибки" textarea.
|
||||
* Returns { raw, timestamp?, entries: [{location, code}] }
|
||||
*/
|
||||
function _parseErrorStack(raw) {
|
||||
function parseErrorStack(raw) {
|
||||
if (!raw) return null;
|
||||
const result = { raw, entries: [] };
|
||||
// Extract timestamp if present (format: DD.MM.YYYY HH:MM:SS)
|
||||
@@ -180,13 +180,13 @@ export async function fetchErrorStack(formNum, hasReport) {
|
||||
return !!(el && el.offsetWidth > 2 && el.textContent.trim());
|
||||
}, formNum);
|
||||
}
|
||||
if (hasReport) return await _fetchStackViaReport(formNum);
|
||||
return await _fetchStackViaHamburger(formNum);
|
||||
if (hasReport) return await fetchStackViaReport(formNum);
|
||||
return await fetchStackViaHamburger(formNum);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
// Ensure all platform dialogs are closed
|
||||
try { await _closePlatformDialogs(); } catch {}
|
||||
try { await closePlatformDialogs(); } catch {}
|
||||
// Ensure the error modal itself is closed
|
||||
try {
|
||||
const sel = formNum != null
|
||||
@@ -203,7 +203,7 @@ export async function fetchErrorStack(formNum, hasReport) {
|
||||
* Path 1: Fetch stack via OpenReport link (for platform exceptions).
|
||||
* The error modal must still be open with a visible "Сформировать отчет об ошибке" link.
|
||||
*/
|
||||
async function _fetchStackViaReport(formNum) {
|
||||
async function fetchStackViaReport(formNum) {
|
||||
// 1. Get coordinates of the OpenReport link and click via mouse (modalSurface blocks JS clicks)
|
||||
const coords = await page.evaluate((fn) => {
|
||||
const el = document.getElementById('form' + fn + '_OpenReport#text');
|
||||
@@ -274,7 +274,7 @@ async function _fetchStackViaReport(formNum) {
|
||||
await page.waitForTimeout(300);
|
||||
} catch {}
|
||||
|
||||
return _parseErrorStack(raw);
|
||||
return parseErrorStack(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,7 +282,7 @@ async function _fetchStackViaReport(formNum) {
|
||||
* Works for all error types including simple ВызватьИсключение.
|
||||
* The error modal is closed first to allow access to the hamburger menu.
|
||||
*/
|
||||
async function _fetchStackViaHamburger(formNum) {
|
||||
async function fetchStackViaHamburger(formNum) {
|
||||
// 1. Close the error modal first
|
||||
try {
|
||||
const sel = formNum != null
|
||||
@@ -336,6 +336,6 @@ async function _fetchStackViaHamburger(formNum) {
|
||||
}
|
||||
const firstBlock = firstBlockLines.join('\n').trim();
|
||||
|
||||
// 7. Close support info and about dialogs (done in finally via _closePlatformDialogs)
|
||||
return _parseErrorStack(firstBlock || errorText);
|
||||
// 7. Close support info and about dialogs (done in finally via closePlatformDialogs)
|
||||
return parseErrorStack(firstBlock || errorText);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// web-test engine/core/form-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 './state.mjs';
|
||||
import { getFormStateScript } from '../../dom.mjs';
|
||||
import { checkForErrors, detectPlatformDialogs } from './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;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// web-test core/helpers v1.16 — private, cross-cutting helpers used by the
|
||||
// web-test core/helpers v1.17 — private, cross-cutting helpers used by the
|
||||
// public action functions (clickElement/fillFields/selectValue/etc).
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page } from './state.mjs';
|
||||
import { dismissPendingErrors, checkForErrors } from './errors.mjs';
|
||||
import { getFormState } from '../../browser.mjs';
|
||||
import { getFormState } from './form-state.mjs';
|
||||
|
||||
/**
|
||||
* page.click with the standard "intercepts pointer events" retry ladder:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test core/session v1.16 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
|
||||
// web-test core/session v1.17 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
@@ -14,10 +14,7 @@ import {
|
||||
} from './state.mjs';
|
||||
import { closeModals } from './errors.mjs';
|
||||
import { stopRecording } from '../recording/capture.mjs';
|
||||
// getPageState lives in browser.mjs (moves to nav/navigation.mjs in a later stage).
|
||||
// Static import is a deliberate ESM cycle — fine because the binding is used at
|
||||
// call time (inside async connect/createContext), not at module evaluation time.
|
||||
import { getPageState } from '../../browser.mjs';
|
||||
import { getPageState } from '../nav/navigation.mjs';
|
||||
|
||||
/**
|
||||
* Find the 1C browser extension in Chrome/Edge user profiles.
|
||||
@@ -125,7 +122,7 @@ export async function connect(url, { extensionPath } = {}) {
|
||||
* @param {object} slot { page, sessionPrefix, seanceId } from contexts Map
|
||||
* @param {number} [waitMs=500] pause after logout fetch (gives 1C time to process)
|
||||
*/
|
||||
async function _logoutSlot(slot, waitMs = 500) {
|
||||
async function logoutSlot(slot, waitMs = 500) {
|
||||
if (!slot?.page || slot.page.isClosed() || !slot.seanceId || !slot.sessionPrefix) return;
|
||||
try {
|
||||
const logoutUrl = `${slot.sessionPrefix}/e1cib/logout?seanceId=${slot.seanceId}`;
|
||||
@@ -143,13 +140,13 @@ async function _logoutSlot(slot, waitMs = 500) {
|
||||
export async function disconnect() {
|
||||
// Multi-context path: stop recording + logout each slot before closing browser
|
||||
if (contexts.size > 0) {
|
||||
_saveActiveSlot();
|
||||
saveActiveSlot();
|
||||
// Recorder is global — one stop covers all contexts
|
||||
if (recorder) {
|
||||
try { await stopRecording(); } catch {}
|
||||
}
|
||||
for (const [, slot] of contexts.entries()) {
|
||||
await _logoutSlot(slot);
|
||||
await logoutSlot(slot);
|
||||
}
|
||||
contexts.clear();
|
||||
setActiveContextName(null);
|
||||
@@ -163,7 +160,7 @@ export async function disconnect() {
|
||||
|
||||
if (browser) {
|
||||
// Graceful logout — release the 1C license (single-session connect path)
|
||||
await _logoutSlot({ page, sessionPrefix, seanceId }, 1000);
|
||||
await logoutSlot({ page, sessionPrefix, seanceId }, 1000);
|
||||
await browser.close().catch(() => {});
|
||||
setBrowser(null);
|
||||
setPage(null);
|
||||
@@ -217,7 +214,7 @@ export function getSession() {
|
||||
* Save current module-level state into the active slot before switching.
|
||||
* No-op if no active slot.
|
||||
*/
|
||||
function _saveActiveSlot() {
|
||||
function saveActiveSlot() {
|
||||
if (!activeContextName) return;
|
||||
const slot = contexts.get(activeContextName);
|
||||
if (!slot) return;
|
||||
@@ -231,7 +228,7 @@ function _saveActiveSlot() {
|
||||
}
|
||||
|
||||
/** Load a slot's state into module-level vars and mark it active. */
|
||||
function _activateSlot(name) {
|
||||
function activateSlot(name) {
|
||||
const slot = contexts.get(name);
|
||||
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
|
||||
setPage(slot.page);
|
||||
@@ -242,7 +239,7 @@ function _activateSlot(name) {
|
||||
}
|
||||
|
||||
/** Attach 1C session listeners to a page, writing into the given slot. */
|
||||
function _attachSessionListeners(pg, slot, name) {
|
||||
function attachSessionListeners(pg, slot, name) {
|
||||
pg.on('dialog', dialog => dialog.accept().catch(() => {}));
|
||||
pg.on('request', req => {
|
||||
if (slot.seanceId) return;
|
||||
@@ -311,7 +308,7 @@ export async function createContext(name, url, { extensionPath, isolation = 'tab
|
||||
}
|
||||
|
||||
// Save current active before switching
|
||||
_saveActiveSlot();
|
||||
saveActiveSlot();
|
||||
|
||||
// Create slot — page differs by mode
|
||||
let newCtx, newPage;
|
||||
@@ -341,8 +338,8 @@ export async function createContext(name, url, { extensionPath, isolation = 'tab
|
||||
};
|
||||
contexts.set(name, slot);
|
||||
|
||||
_attachSessionListeners(newPage, slot, name);
|
||||
_activateSlot(name);
|
||||
attachSessionListeners(newPage, slot, name);
|
||||
activateSlot(name);
|
||||
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
||||
@@ -359,8 +356,8 @@ export async function setActiveContext(name) {
|
||||
// If a recording is active, flush the outgoing page's last frame so the gap is filled
|
||||
// up to the moment of the switch (avoids a "jump" in video time).
|
||||
if (recorder && recorder._flushFrames) recorder._flushFrames();
|
||||
_saveActiveSlot();
|
||||
_activateSlot(name);
|
||||
saveActiveSlot();
|
||||
activateSlot(name);
|
||||
// If the recording is still alive (it lives across slots — we keep the same ffmpeg/output),
|
||||
// re-attach its screencast to the newly active page.
|
||||
if (recorder && recorder._attachPage) {
|
||||
@@ -397,7 +394,7 @@ export async function closeContext(name) {
|
||||
throw new Error(`closeContext: cannot close the active context "${name}". setActiveContext to another context first.`);
|
||||
}
|
||||
const slot = contexts.get(name);
|
||||
await _logoutSlot(slot);
|
||||
await logoutSlot(slot);
|
||||
if (activeMode === 'tab') {
|
||||
try { await slot.page.close(); } catch {}
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test core/state v1.16 — module-level state for the web-test engine.
|
||||
// web-test core/state v1.17 — module-level state for the web-test engine.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// Holds the single browser/page/recorder slot plus the multi-context registry,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test core/wait v1.16 — Smart wait helpers: DOM stability polling, JS-expression polling, CDP network monitor.
|
||||
// web-test core/wait v1.17 — Smart wait helpers: DOM stability polling, JS-expression polling, CDP network monitor.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, MAX_WAIT, POLL_INTERVAL, STABLE_CYCLES } from './state.mjs';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// web-test forms/close v1.16 — Close current form via Escape, handle save-changes confirmation.
|
||||
// web-test forms/close v1.17 — 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 { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
|
||||
import { waitForStable } from '../core/wait.mjs';
|
||||
import { getFormState } from '../../browser.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
/**
|
||||
* Close the current form/dialog via Escape.
|
||||
@@ -19,9 +19,9 @@ export async function closeForm({ save } = {}) {
|
||||
ensureConnected();
|
||||
await dismissPendingErrors();
|
||||
// If platform dialogs are open, close them instead of pressing Escape
|
||||
const pd = await _detectPlatformDialogs();
|
||||
const pd = await detectPlatformDialogs();
|
||||
if (pd.length) {
|
||||
await _closePlatformDialogs();
|
||||
await closePlatformDialogs();
|
||||
await page.waitForTimeout(300);
|
||||
const state = await getFormState();
|
||||
state.closed = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/fill v1.16 — Fill form fields by name (text/checkbox/date/dropdown/reference). Delegates references to selectValue / fillReferenceField.
|
||||
// web-test forms/fill v1.17 — Fill form fields by name (text/checkbox/date/dropdown/reference). Delegates references to selectValue / fillReferenceField.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
isTypeDialog, pickFromTypeDialog,
|
||||
} from './select-value.mjs';
|
||||
// pasteText + getFormState live in browser.mjs.
|
||||
import { pasteText, getFormState } from '../../browser.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
/** Fill fields on the current form via Playwright page.fill(). Returns fill results + updated form. */
|
||||
export async function fillFields(fields) {
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
detectNewForm as helperDetectNewForm,
|
||||
} from '../core/helpers.mjs';
|
||||
// pasteText + getFormState live in browser.mjs.
|
||||
import { pasteText, getFormState } from '../../browser.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
/**
|
||||
* Scan visible grid rows for a text match (exact → startsWith → includes).
|
||||
|
||||
@@ -15,7 +15,8 @@ import { highlight, unhighlight } from '../recording/highlight.mjs';
|
||||
import { returnFormState } from '../core/helpers.mjs';
|
||||
// pasteText + getFormState live in browser.mjs (move to forms/ in a later stage).
|
||||
// Static import — ESM cycle that resolves at call time.
|
||||
import { pasteText, getFormState } from '../../browser.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
/**
|
||||
* Get current page state: active section, tabs.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test recording/captions v1.16 — Overlay primitives: captions, title slides, image overlays.
|
||||
// web-test recording/captions v1.17 — Overlay primitives: captions, title slides, image overlays.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { existsSync as fsExistsSync, readFileSync } from 'fs';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test recording/capture v1.16 — Recording lifecycle (CDP screencast + ffmpeg pipe), screenshot, wait helpers.
|
||||
// web-test recording/capture v1.17 — Recording lifecycle (CDP screencast + ffmpeg pipe), screenshot, wait helpers.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
@@ -45,7 +45,7 @@ export async function wait(seconds) {
|
||||
await page.waitForTimeout(ms);
|
||||
}
|
||||
}
|
||||
const { getFormState } = await import('../../browser.mjs');
|
||||
const { getFormState } = await import('../core/form-state.mjs');
|
||||
return await getFormState();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test recording/highlight v1.16 — Visual highlight overlay (single + auto-mode for clickElement/fillFields/selectValue).
|
||||
// web-test recording/highlight v1.17 — Visual highlight overlay (single + auto-mode for clickElement/fillFields/selectValue).
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test recording/narration v1.16 — Post-process: generate TTS audio for captions and merge with recorded video.
|
||||
// web-test recording/narration v1.17 — Post-process: generate TTS audio for captions and merge with recorded video.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test recording/tts v1.16 — TTS providers (edge/openai/elevenlabs) and ffmpeg/ffprobe helpers.
|
||||
// web-test recording/tts v1.17 — TTS providers (edge/openai/elevenlabs) and ffmpeg/ffprobe helpers.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { execFileSync, spawn } from 'child_process';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test table/filter v1.16 — filterList / unfilterList — simple search + advanced-column filter badges.
|
||||
// web-test table/filter v1.17 — filterList / unfilterList — simple search + advanced-column filter badges.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, ensureConnected, normYo, highlightMode, ACTION_WAIT } from '../core/state.mjs';
|
||||
@@ -8,8 +8,9 @@ import { waitForStable, waitForCondition } from '../core/wait.mjs';
|
||||
import { highlight, unhighlight } from '../recording/highlight.mjs';
|
||||
import { safeClick } from '../core/helpers.mjs';
|
||||
import { selectValue, fillReferenceField } from '../forms/select-value.mjs';
|
||||
// pasteText + getFormState + clickElement still in browser.mjs.
|
||||
import { pasteText, getFormState, clickElement } from '../../browser.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
import { clickElement } from '../core/click.mjs';
|
||||
|
||||
/**
|
||||
* Filter the current list by field value, or search via search bar.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test table/grid-toggle v1.16 — shared icon-detection for grid expand/
|
||||
// web-test table/grid-toggle v1.17 — shared icon-detection for grid expand/
|
||||
// collapse toggles. Used by clickElement's gridGroup/gridParent and
|
||||
// gridTreeNode branches; the actual mouse click stays in the caller because
|
||||
// it depends on the caller-local modifier-key handling.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test table/grid v1.16 — Form-grid operations: read table rows, fill rows, delete rows.
|
||||
// web-test table/grid v1.17 — Form-grid operations: read table rows, fill rows, delete rows.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// "Grid" в терминах 1С — таблица на форме (.gridLine/.gridBody/.grid в DOM):
|
||||
@@ -11,7 +11,7 @@ import { dismissPendingErrors } from '../core/errors.mjs';
|
||||
import { waitForStable } from '../core/wait.mjs';
|
||||
import { clickElement } from '../core/click.mjs';
|
||||
// getFormState lives in browser.mjs.
|
||||
import { getFormState } from '../../browser.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
/** Read structured table data with pagination. Returns columns, rows, total count. */
|
||||
export async function readTable({ maxRows = 20, offset = 0, table } = {}) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test table/row-fill v1.16 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
|
||||
// web-test table/row-fill v1.17 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
fillReferenceField, selectValue,
|
||||
} from '../forms/select-value.mjs';
|
||||
// pasteText + getFormState still in browser.mjs (cycle).
|
||||
import { pasteText, getFormState } from '../../browser.mjs';
|
||||
import { pasteText } from '../core/clipboard.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
/**
|
||||
* Fill cells in the current table row via Tab navigation.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// web-test table/spreadsheet v1.16 — readTable, readSpreadsheet, scanSpreadsheetCells, scroll/click helpers for SpreadsheetDocument.
|
||||
// web-test table/spreadsheet v1.17 — readTable, readSpreadsheet, scanSpreadsheetCells, scroll/click helpers for SpreadsheetDocument.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, ensureConnected, normYo } from '../core/state.mjs';
|
||||
import { detectFormScript, readTableScript, resolveGridScript } from '../../dom.mjs';
|
||||
import { waitForStable } from '../core/wait.mjs';
|
||||
// getFormState still in browser.mjs (cycle resolves at call time).
|
||||
import { getFormState } from '../../browser.mjs';
|
||||
import { getFormState } from '../core/form-state.mjs';
|
||||
|
||||
// readTable moved to table/grid.mjs (form-grid операции отделены от SpreadsheetDocument).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user