diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 84ca517d..20895767 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -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 */ diff --git a/.claude/skills/web-test/scripts/engine/core/click.mjs b/.claude/skills/web-test/scripts/engine/core/click.mjs index e33cc575..c8e46141 100644 --- a/.claude/skills/web-test/scripts/engine/core/click.mjs +++ b/.claude/skills/web-test/scripts/engine/core/click.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. */ diff --git a/.claude/skills/web-test/scripts/engine/core/clipboard.mjs b/.claude/skills/web-test/scripts/engine/core/clipboard.mjs new file mode 100644 index 00000000..2e51c96e --- /dev/null +++ b/.claude/skills/web-test/scripts/engine/core/clipboard.mjs @@ -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(); + } +} diff --git a/.claude/skills/web-test/scripts/engine/core/errors.mjs b/.claude/skills/web-test/scripts/engine/core/errors.mjs index 1d37a048..d3945da9 100644 --- a/.claude/skills/web-test/scripts/engine/core/errors.mjs +++ b/.claude/skills/web-test/scripts/engine/core/errors.mjs @@ -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); } diff --git a/.claude/skills/web-test/scripts/engine/core/form-state.mjs b/.claude/skills/web-test/scripts/engine/core/form-state.mjs new file mode 100644 index 00000000..12391936 --- /dev/null +++ b/.claude/skills/web-test/scripts/engine/core/form-state.mjs @@ -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; +} diff --git a/.claude/skills/web-test/scripts/engine/core/helpers.mjs b/.claude/skills/web-test/scripts/engine/core/helpers.mjs index c6d1af26..595f1e9f 100644 --- a/.claude/skills/web-test/scripts/engine/core/helpers.mjs +++ b/.claude/skills/web-test/scripts/engine/core/helpers.mjs @@ -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: diff --git a/.claude/skills/web-test/scripts/engine/core/session.mjs b/.claude/skills/web-test/scripts/engine/core/session.mjs index d2b7dfbf..c33be755 100644 --- a/.claude/skills/web-test/scripts/engine/core/session.mjs +++ b/.claude/skills/web-test/scripts/engine/core/session.mjs @@ -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 { diff --git a/.claude/skills/web-test/scripts/engine/core/state.mjs b/.claude/skills/web-test/scripts/engine/core/state.mjs index 6c193769..985c065b 100644 --- a/.claude/skills/web-test/scripts/engine/core/state.mjs +++ b/.claude/skills/web-test/scripts/engine/core/state.mjs @@ -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, diff --git a/.claude/skills/web-test/scripts/engine/core/wait.mjs b/.claude/skills/web-test/scripts/engine/core/wait.mjs index 20c84ed2..c584bc52 100644 --- a/.claude/skills/web-test/scripts/engine/core/wait.mjs +++ b/.claude/skills/web-test/scripts/engine/core/wait.mjs @@ -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'; diff --git a/.claude/skills/web-test/scripts/engine/forms/close.mjs b/.claude/skills/web-test/scripts/engine/forms/close.mjs index c4faae14..6c5be6dd 100644 --- a/.claude/skills/web-test/scripts/engine/forms/close.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/close.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; diff --git a/.claude/skills/web-test/scripts/engine/forms/fill.mjs b/.claude/skills/web-test/scripts/engine/forms/fill.mjs index dcd669e7..8783fa9a 100644 --- a/.claude/skills/web-test/scripts/engine/forms/fill.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/fill.mjs @@ -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) { diff --git a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs index bfe0892d..dc6bbdf6 100644 --- a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs @@ -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). diff --git a/.claude/skills/web-test/scripts/engine/nav/navigation.mjs b/.claude/skills/web-test/scripts/engine/nav/navigation.mjs index 060396e6..0c002aab 100644 --- a/.claude/skills/web-test/scripts/engine/nav/navigation.mjs +++ b/.claude/skills/web-test/scripts/engine/nav/navigation.mjs @@ -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. diff --git a/.claude/skills/web-test/scripts/engine/recording/captions.mjs b/.claude/skills/web-test/scripts/engine/recording/captions.mjs index c70cd987..1043bc5b 100644 --- a/.claude/skills/web-test/scripts/engine/recording/captions.mjs +++ b/.claude/skills/web-test/scripts/engine/recording/captions.mjs @@ -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'; diff --git a/.claude/skills/web-test/scripts/engine/recording/capture.mjs b/.claude/skills/web-test/scripts/engine/recording/capture.mjs index d89f5ab1..dcbe1c02 100644 --- a/.claude/skills/web-test/scripts/engine/recording/capture.mjs +++ b/.claude/skills/web-test/scripts/engine/recording/capture.mjs @@ -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(); } diff --git a/.claude/skills/web-test/scripts/engine/recording/highlight.mjs b/.claude/skills/web-test/scripts/engine/recording/highlight.mjs index a8fa3e6c..ea61de2c 100644 --- a/.claude/skills/web-test/scripts/engine/recording/highlight.mjs +++ b/.claude/skills/web-test/scripts/engine/recording/highlight.mjs @@ -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 { diff --git a/.claude/skills/web-test/scripts/engine/recording/narration.mjs b/.claude/skills/web-test/scripts/engine/recording/narration.mjs index dff34e0a..6891fddf 100644 --- a/.claude/skills/web-test/scripts/engine/recording/narration.mjs +++ b/.claude/skills/web-test/scripts/engine/recording/narration.mjs @@ -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'; diff --git a/.claude/skills/web-test/scripts/engine/recording/tts.mjs b/.claude/skills/web-test/scripts/engine/recording/tts.mjs index 0a965fb0..fd218540 100644 --- a/.claude/skills/web-test/scripts/engine/recording/tts.mjs +++ b/.claude/skills/web-test/scripts/engine/recording/tts.mjs @@ -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'; diff --git a/.claude/skills/web-test/scripts/engine/table/filter.mjs b/.claude/skills/web-test/scripts/engine/table/filter.mjs index db3ba8d1..03df38c1 100644 --- a/.claude/skills/web-test/scripts/engine/table/filter.mjs +++ b/.claude/skills/web-test/scripts/engine/table/filter.mjs @@ -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. diff --git a/.claude/skills/web-test/scripts/engine/table/grid-toggle.mjs b/.claude/skills/web-test/scripts/engine/table/grid-toggle.mjs index cf5e7a2d..5fe96d3f 100644 --- a/.claude/skills/web-test/scripts/engine/table/grid-toggle.mjs +++ b/.claude/skills/web-test/scripts/engine/table/grid-toggle.mjs @@ -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. diff --git a/.claude/skills/web-test/scripts/engine/table/grid.mjs b/.claude/skills/web-test/scripts/engine/table/grid.mjs index b97b4d6b..9e0be5a9 100644 --- a/.claude/skills/web-test/scripts/engine/table/grid.mjs +++ b/.claude/skills/web-test/scripts/engine/table/grid.mjs @@ -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 } = {}) { diff --git a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs index 0f7ec083..d8ad78c8 100644 --- a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs +++ b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs @@ -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. diff --git a/.claude/skills/web-test/scripts/engine/table/spreadsheet.mjs b/.claude/skills/web-test/scripts/engine/table/spreadsheet.mjs index 6f5c7522..99bad349 100644 --- a/.claude/skills/web-test/scripts/engine/table/spreadsheet.mjs +++ b/.claude/skills/web-test/scripts/engine/table/spreadsheet.mjs @@ -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).