mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-20 09:30:59 +03:00
refactor(web-test): этап A.1 — выделить module-level state в core/state.mjs
Состояние движка (browser, page, sessionPrefix, seanceId, recorder, контексты, константы, normYo, isConnected/ensureConnected/getPage) переехало в core/state.mjs. Импортируется как live-binding; присваивания в browser.mjs конвертированы в setX(...) — ESM imports read-only. Публичный API не меняется (56 экспортов). Регресс 19/19 зелёный. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d3be9c8dea
commit
cecf4dd9a2
@@ -21,53 +21,23 @@ import {
|
|||||||
switchTabScript, resolveGridScript
|
switchTabScript, resolveGridScript
|
||||||
} from './dom.mjs';
|
} from './dom.mjs';
|
||||||
|
|
||||||
// Project root: 4 levels up from .claude/skills/web-test/scripts/browser.mjs
|
// Module-level state, constants, normYo and resolveProjectPath live in core/state.mjs.
|
||||||
const __fn_browser = fileURLToPath(import.meta.url);
|
// Imported as live bindings — reads stay current; writes go through setters.
|
||||||
const projectRoot = pathResolve(dirname(__fn_browser), '..', '..', '..', '..');
|
import {
|
||||||
|
browser, page, sessionPrefix, seanceId, recorder,
|
||||||
|
lastCaptions, lastRecordingDuration, highlightMode,
|
||||||
|
persistentUserDataDir, preserveClipboard,
|
||||||
|
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,
|
||||||
|
} from './core/state.mjs';
|
||||||
|
|
||||||
/** Resolve a user-provided path relative to the project root (not cwd). */
|
export { isConnected, getPage, setPreserveClipboard, ensureConnected };
|
||||||
const resolveProjectPath = (p) => pathResolve(projectRoot, p);
|
|
||||||
|
|
||||||
let browser = null;
|
|
||||||
let page = null;
|
|
||||||
let sessionPrefix = null; // e.g. "http://localhost:8081/bpdemo/ru_RU"
|
|
||||||
let seanceId = null;
|
|
||||||
let recorder = null; // { cdp, ffmpeg, startTime, outputPath, ffmpegError, captions }
|
|
||||||
let lastCaptions = []; // captions from the last completed recording (for addNarration)
|
|
||||||
let lastRecordingDuration = null; // wall-clock duration of the last recording (seconds)
|
|
||||||
let highlightMode = false;
|
|
||||||
|
|
||||||
// Multi-context registry: name → { context, page, sessionPrefix, seanceId, recorder, lastCaptions, lastRecordingDuration, highlightMode }
|
|
||||||
// Populated by createContext(); module-level vars above mirror the active slot.
|
|
||||||
// connect() does NOT use this Map — it preserves legacy single-session behavior for exec/run/start.
|
|
||||||
const contexts = new Map();
|
|
||||||
let activeContextName = null;
|
|
||||||
// Isolation mode for the current cmdTest session — set by the first createContext call.
|
|
||||||
// 'tab': all contexts share one persistent context (one window, multiple tabs, extension loads reliably).
|
|
||||||
// 'window': each context gets its own BrowserContext (separate window per context, full cookie isolation, extension may not load).
|
|
||||||
let activeMode = null;
|
|
||||||
|
|
||||||
const LOAD_TIMEOUT = 60000;
|
|
||||||
const INIT_TIMEOUT = 60000;
|
|
||||||
const ACTION_WAIT = 2000; // fallback minimum wait
|
|
||||||
|
|
||||||
/** Normalize ё→е and \u00a0→space for fuzzy matching. */
|
|
||||||
const normYo = s => s.replace(/ё/gi, 'е').replace(/\u00a0/g, ' ');
|
|
||||||
const MAX_WAIT = 10000; // max wait for stability
|
|
||||||
const POLL_INTERVAL = 200; // polling interval
|
|
||||||
const STABLE_CYCLES = 3; // consecutive stable cycles needed
|
|
||||||
|
|
||||||
// 1C browser extension ID (stable across versions, defined by key in manifest.json)
|
|
||||||
const EXT_ID = 'pbhelknnhilelbnhfpcjlcabhmfangik';
|
|
||||||
let persistentUserDataDir = null; // temp dir for launchPersistentContext, cleaned on disconnect
|
|
||||||
|
|
||||||
// Clipboard preservation: save full clipboard contents (all MIME types) right before
|
|
||||||
// each writeText+Ctrl+V pair, restore right after — narrow window so a user's
|
|
||||||
// concurrent Ctrl+C isn't clobbered. Blobs are stashed on `window` (no CDP
|
|
||||||
// serialization). Toggled via setPreserveClipboard() from run.mjs.
|
|
||||||
let preserveClipboard = true;
|
|
||||||
let clipboardWarnLogged = false;
|
|
||||||
export function setPreserveClipboard(v) { preserveClipboard = !!v; }
|
|
||||||
export async function saveClipboard() {
|
export async function saveClipboard() {
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
try {
|
try {
|
||||||
@@ -120,7 +90,7 @@ export async function restoreClipboard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err && !clipboardWarnLogged) {
|
if (err && !clipboardWarnLogged) {
|
||||||
clipboardWarnLogged = true;
|
setClipboardWarnLogged(true);
|
||||||
console.error(`[web-test] clipboard preserve skipped: ${err} (logged once per session)`);
|
console.error(`[web-test] clipboard preserve skipped: ${err} (logged once per session)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,14 +157,7 @@ function findExtension(overridePath) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if browser is connected and page is usable. */
|
/* isConnected moved to core/state.mjs */
|
||||||
export function isConnected() {
|
|
||||||
if (!browser || !page || page.isClosed()) return false;
|
|
||||||
// launchPersistentContext returns BrowserContext (no isConnected), launch returns Browser
|
|
||||||
if (typeof browser.isConnected === 'function') return browser.isConnected();
|
|
||||||
// For persistent context, check via context's browser()
|
|
||||||
return browser.browser()?.isConnected() ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open browser and navigate to 1C web client URL.
|
* Open browser and navigate to 1C web client URL.
|
||||||
@@ -207,7 +170,7 @@ export async function connect(url, { extensionPath } = {}) {
|
|||||||
const extPath = findExtension(extensionPath);
|
const extPath = findExtension(extensionPath);
|
||||||
if (extPath) {
|
if (extPath) {
|
||||||
// Launch with 1C browser extension via persistent context
|
// Launch with 1C browser extension via persistent context
|
||||||
persistentUserDataDir = pathJoin(tmpdir(), 'pw-1c-ext-' + Date.now());
|
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-ext-' + Date.now()));
|
||||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
mkdirSync(persistentUserDataDir, { recursive: true });
|
||||||
const context = await chromium.launchPersistentContext(persistentUserDataDir, {
|
const context = await chromium.launchPersistentContext(persistentUserDataDir, {
|
||||||
headless: false,
|
headless: false,
|
||||||
@@ -219,28 +182,28 @@ export async function connect(url, { extensionPath } = {}) {
|
|||||||
viewport: null,
|
viewport: null,
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
permissions: ['clipboard-read', 'clipboard-write'],
|
||||||
});
|
});
|
||||||
browser = context; // persistent context IS the browser
|
setBrowser(context); // persistent context IS the browser
|
||||||
page = context.pages()[0] || await context.newPage();
|
setPage(context.pages()[0] || await context.newPage());
|
||||||
} else {
|
} else {
|
||||||
// Fallback: launch without extension
|
// Fallback: launch without extension
|
||||||
browser = await chromium.launch({ headless: false, args: ['--start-maximized'] });
|
setBrowser(await chromium.launch({ headless: false, args: ['--start-maximized'] }));
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
viewport: null,
|
viewport: null,
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
permissions: ['clipboard-read', 'clipboard-write'],
|
||||||
});
|
});
|
||||||
page = await context.newPage();
|
setPage(await context.newPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-accept native browser dialogs (confirm/alert from 1C scripts like vis.js)
|
// Auto-accept native browser dialogs (confirm/alert from 1C scripts like vis.js)
|
||||||
page.on('dialog', dialog => dialog.accept().catch(() => {}));
|
page.on('dialog', dialog => dialog.accept().catch(() => {}));
|
||||||
|
|
||||||
// Capture seanceId from network requests for graceful logout
|
// Capture seanceId from network requests for graceful logout
|
||||||
sessionPrefix = null;
|
setSessionPrefix(null);
|
||||||
seanceId = null;
|
setSeanceId(null);
|
||||||
page.on('request', req => {
|
page.on('request', req => {
|
||||||
if (seanceId) return;
|
if (seanceId) return;
|
||||||
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
||||||
if (m) { sessionPrefix = m[1]; seanceId = m[2]; }
|
if (m) { setSessionPrefix(m[1]); setSeanceId(m[2]); }
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
||||||
@@ -293,8 +256,8 @@ export async function disconnect() {
|
|||||||
await _logoutSlot(slot);
|
await _logoutSlot(slot);
|
||||||
}
|
}
|
||||||
contexts.clear();
|
contexts.clear();
|
||||||
activeContextName = null;
|
setActiveContextName(null);
|
||||||
activeMode = null;
|
setActiveMode(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single-session path (connect): auto-stop recording if active
|
// Single-session path (connect): auto-stop recording if active
|
||||||
@@ -306,14 +269,14 @@ export async function disconnect() {
|
|||||||
// Graceful logout — release the 1C license (single-session connect path)
|
// 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(() => {});
|
await browser.close().catch(() => {});
|
||||||
browser = null;
|
setBrowser(null);
|
||||||
page = null;
|
setPage(null);
|
||||||
sessionPrefix = null;
|
setSessionPrefix(null);
|
||||||
seanceId = null;
|
setSeanceId(null);
|
||||||
// Clean up persistent user data dir
|
// Clean up persistent user data dir
|
||||||
if (persistentUserDataDir) {
|
if (persistentUserDataDir) {
|
||||||
try { rmSync(persistentUserDataDir, { recursive: true, force: true }); } catch {}
|
try { rmSync(persistentUserDataDir, { recursive: true, force: true }); } catch {}
|
||||||
persistentUserDataDir = null;
|
setPersistentUserDataDir(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,12 +287,12 @@ export async function disconnect() {
|
|||||||
*/
|
*/
|
||||||
export async function attach(wsEndpoint, session = {}) {
|
export async function attach(wsEndpoint, session = {}) {
|
||||||
if (isConnected()) return;
|
if (isConnected()) return;
|
||||||
browser = await chromium.connect(wsEndpoint);
|
setBrowser(await chromium.connect(wsEndpoint));
|
||||||
const ctx = browser.contexts()[0];
|
const ctx = browser.contexts()[0];
|
||||||
page = ctx?.pages()[0];
|
setPage(ctx?.pages()[0]);
|
||||||
if (!page) throw new Error('No page found in browser');
|
if (!page) throw new Error('No page found in browser');
|
||||||
sessionPrefix = session.sessionPrefix || null;
|
setSessionPrefix(session.sessionPrefix || null);
|
||||||
seanceId = session.seanceId || null;
|
setSeanceId(session.seanceId || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -338,10 +301,10 @@ export async function attach(wsEndpoint, session = {}) {
|
|||||||
*/
|
*/
|
||||||
export function detach() {
|
export function detach() {
|
||||||
const session = { sessionPrefix, seanceId };
|
const session = { sessionPrefix, seanceId };
|
||||||
browser = null;
|
setBrowser(null);
|
||||||
page = null;
|
setPage(null);
|
||||||
sessionPrefix = null;
|
setSessionPrefix(null);
|
||||||
seanceId = null;
|
setSeanceId(null);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,11 +338,11 @@ function _saveActiveSlot() {
|
|||||||
function _activateSlot(name) {
|
function _activateSlot(name) {
|
||||||
const slot = contexts.get(name);
|
const slot = contexts.get(name);
|
||||||
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
|
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
|
||||||
page = slot.page;
|
setPage(slot.page);
|
||||||
sessionPrefix = slot.sessionPrefix;
|
setSessionPrefix(slot.sessionPrefix);
|
||||||
seanceId = slot.seanceId;
|
setSeanceId(slot.seanceId);
|
||||||
highlightMode = slot.highlightMode || false;
|
setHighlightMode(slot.highlightMode || false);
|
||||||
activeContextName = name;
|
setActiveContextName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attach 1C session listeners to a page, writing into the given slot. */
|
/** Attach 1C session listeners to a page, writing into the given slot. */
|
||||||
@@ -392,8 +355,8 @@ function _attachSessionListeners(pg, slot, name) {
|
|||||||
slot.sessionPrefix = m[1];
|
slot.sessionPrefix = m[1];
|
||||||
slot.seanceId = m[2];
|
slot.seanceId = m[2];
|
||||||
if (activeContextName === name) {
|
if (activeContextName === name) {
|
||||||
sessionPrefix = m[1];
|
setSessionPrefix(m[1]);
|
||||||
seanceId = m[2];
|
setSeanceId(m[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -436,19 +399,19 @@ export async function createContext(name, url, { extensionPath, isolation = 'tab
|
|||||||
}
|
}
|
||||||
if (isolation === 'tab') {
|
if (isolation === 'tab') {
|
||||||
// Persistent context: extension loads reliably, one window with tabs per context
|
// Persistent context: extension loads reliably, one window with tabs per context
|
||||||
persistentUserDataDir = pathJoin(tmpdir(), 'pw-1c-test-' + Date.now());
|
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-test-' + Date.now()));
|
||||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
mkdirSync(persistentUserDataDir, { recursive: true });
|
||||||
browser = await chromium.launchPersistentContext(persistentUserDataDir, {
|
setBrowser(await chromium.launchPersistentContext(persistentUserDataDir, {
|
||||||
headless: false,
|
headless: false,
|
||||||
args: launchArgs,
|
args: launchArgs,
|
||||||
viewport: null,
|
viewport: null,
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
permissions: ['clipboard-read', 'clipboard-write'],
|
||||||
});
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Window mode: separate BrowserContext per slot, full cookie isolation
|
// Window mode: separate BrowserContext per slot, full cookie isolation
|
||||||
browser = await chromium.launch({ headless: false, args: launchArgs });
|
setBrowser(await chromium.launch({ headless: false, args: launchArgs }));
|
||||||
}
|
}
|
||||||
activeMode = isolation;
|
setActiveMode(isolation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current active before switching
|
// Save current active before switching
|
||||||
@@ -1000,11 +963,7 @@ async function _fetchStackViaHamburger(formNum) {
|
|||||||
return _parseErrorStack(firstBlock || errorText);
|
return _parseErrorStack(firstBlock || errorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the raw Playwright page object (for advanced scripting in skill mode). */
|
/* getPage moved to core/state.mjs */
|
||||||
export function getPage() {
|
|
||||||
ensureConnected();
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current page state: active section, tabs.
|
* Get current page state: active section, tabs.
|
||||||
@@ -5152,8 +5111,8 @@ export async function startRecording(outputPath, opts = {}) {
|
|||||||
throw new Error('Already recording. Call stopRecording() first, or use { force: true }.');
|
throw new Error('Already recording. Call stopRecording() first, or use { force: true }.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastCaptions = [];
|
setLastCaptions([]);
|
||||||
lastRecordingDuration = null;
|
setLastRecordingDuration(null);
|
||||||
|
|
||||||
const fps = opts.fps || 25;
|
const fps = opts.fps || 25;
|
||||||
const quality = opts.quality || 80;
|
const quality = opts.quality || 80;
|
||||||
@@ -5240,7 +5199,7 @@ export async function startRecording(outputPath, opts = {}) {
|
|||||||
recorder.activePage = targetPage;
|
recorder.activePage = targetPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
recorder = {
|
setRecorder({
|
||||||
cdp: null,
|
cdp: null,
|
||||||
activePage: null,
|
activePage: null,
|
||||||
ffmpeg,
|
ffmpeg,
|
||||||
@@ -5255,7 +5214,7 @@ export async function startRecording(outputPath, opts = {}) {
|
|||||||
_flushFrames,
|
_flushFrames,
|
||||||
_attachPage,
|
_attachPage,
|
||||||
speechRate,
|
speechRate,
|
||||||
};
|
});
|
||||||
ffmpeg.stderr.on('data', d => { recorder.ffmpegError += d.toString(); });
|
ffmpeg.stderr.on('data', d => { recorder.ffmpegError += d.toString(); });
|
||||||
|
|
||||||
await _attachPage(page);
|
await _attachPage(page);
|
||||||
@@ -5301,15 +5260,15 @@ export async function stopRecording() {
|
|||||||
const stats = statSync(outputPath);
|
const stats = statSync(outputPath);
|
||||||
|
|
||||||
// Preserve captions for addNarration()
|
// Preserve captions for addNarration()
|
||||||
lastCaptions = recorder.captions || [];
|
setLastCaptions(recorder.captions || []);
|
||||||
lastRecordingDuration = duration;
|
setLastRecordingDuration(duration);
|
||||||
if (lastCaptions.length) {
|
if (lastCaptions.length) {
|
||||||
const captionsPath = outputPath.replace(/\.[^.]+$/, '.captions.json');
|
const captionsPath = outputPath.replace(/\.[^.]+$/, '.captions.json');
|
||||||
const captionsData = { recordingDuration: duration, videoTimestamps: true, captions: lastCaptions };
|
const captionsData = { recordingDuration: duration, videoTimestamps: true, captions: lastCaptions };
|
||||||
writeFileSync(captionsPath, JSON.stringify(captionsData, null, 2), 'utf-8');
|
writeFileSync(captionsPath, JSON.stringify(captionsData, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder = null;
|
setRecorder(null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
file: outputPath,
|
file: outputPath,
|
||||||
@@ -6107,7 +6066,7 @@ export async function unhighlight() {
|
|||||||
* @param {boolean} on true to enable, false to disable
|
* @param {boolean} on true to enable, false to disable
|
||||||
*/
|
*/
|
||||||
export function setHighlight(on) {
|
export function setHighlight(on) {
|
||||||
highlightMode = !!on;
|
setHighlightMode(!!on);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {boolean} Whether auto-highlight mode is active. */
|
/** @returns {boolean} Whether auto-highlight mode is active. */
|
||||||
@@ -6286,8 +6245,4 @@ function generateSilence(outputPath, seconds, ffmpegPath) {
|
|||||||
], { stdio: 'pipe', timeout: 10000 });
|
], { stdio: 'pipe', timeout: 10000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureConnected() {
|
/* ensureConnected moved to core/state.mjs */
|
||||||
if (!isConnected()) {
|
|
||||||
throw new Error('Browser not connected. Call web_connect first.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// web-test core/state v1.16 — 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,
|
||||||
|
// constants, and small state-only utilities (ensureConnected, getPage,
|
||||||
|
// resolveProjectPath, normYo). Mutable values are exported as `let` bindings
|
||||||
|
// for live read access from consumer modules; writes go through setters so
|
||||||
|
// imported bindings stay read-only at the import site.
|
||||||
|
|
||||||
|
import { dirname, resolve as pathResolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
// Project root: 4 levels up from .claude/skills/web-test/scripts/core/state.mjs
|
||||||
|
const __fn_state = fileURLToPath(import.meta.url);
|
||||||
|
export const projectRoot = pathResolve(dirname(__fn_state), '..', '..', '..', '..', '..');
|
||||||
|
|
||||||
|
/** Resolve a user-provided path relative to the project root (not cwd). */
|
||||||
|
export const resolveProjectPath = (p) => pathResolve(projectRoot, p);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Mutable single-session state. Importers read via the live binding; writes
|
||||||
|
// must go through the corresponding setter (ESM imports are read-only).
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export let browser = null;
|
||||||
|
export let page = null;
|
||||||
|
export let sessionPrefix = null; // e.g. "http://localhost:8081/bpdemo/ru_RU"
|
||||||
|
export let seanceId = null;
|
||||||
|
export let recorder = null; // { cdp, ffmpeg, startTime, outputPath, ffmpegError, captions }
|
||||||
|
export let lastCaptions = []; // captions from the last completed recording (for addNarration)
|
||||||
|
export let lastRecordingDuration = null; // wall-clock duration of the last recording (seconds)
|
||||||
|
export let highlightMode = false;
|
||||||
|
export let persistentUserDataDir = null; // temp dir for launchPersistentContext, cleaned on disconnect
|
||||||
|
|
||||||
|
// Clipboard preservation: save full clipboard contents (all MIME types) right
|
||||||
|
// before each writeText+Ctrl+V pair, restore right after. Toggled via
|
||||||
|
// setPreserveClipboard() from run.mjs.
|
||||||
|
export let preserveClipboard = true;
|
||||||
|
export let clipboardWarnLogged = false;
|
||||||
|
|
||||||
|
export const setBrowser = (v) => { browser = v; };
|
||||||
|
export const setPage = (v) => { page = v; };
|
||||||
|
export const setSessionPrefix = (v) => { sessionPrefix = v; };
|
||||||
|
export const setSeanceId = (v) => { seanceId = v; };
|
||||||
|
export const setRecorder = (v) => { recorder = v; };
|
||||||
|
export const setLastCaptions = (v) => { lastCaptions = v; };
|
||||||
|
export const setLastRecordingDuration = (v) => { lastRecordingDuration = v; };
|
||||||
|
export const setHighlightMode = (v) => { highlightMode = !!v; };
|
||||||
|
export const setPersistentUserDataDir = (v) => { persistentUserDataDir = v; };
|
||||||
|
export const setPreserveClipboard = (v) => { preserveClipboard = !!v; };
|
||||||
|
export const setClipboardWarnLogged = (v) => { clipboardWarnLogged = !!v; };
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Multi-context registry: name → { context, page, sessionPrefix, seanceId,
|
||||||
|
// recorder, lastCaptions, lastRecordingDuration, highlightMode }.
|
||||||
|
// Populated by createContext(); module-level vars above mirror the active
|
||||||
|
// slot. connect() does NOT use this Map — it preserves legacy single-session
|
||||||
|
// behavior for exec/run/start.
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const contexts = new Map();
|
||||||
|
export let activeContextName = null;
|
||||||
|
// Isolation mode for the current cmdTest session — set by the first
|
||||||
|
// createContext call. 'tab': all contexts share one persistent context
|
||||||
|
// (one window, multiple tabs, extension loads reliably). 'window': each
|
||||||
|
// context gets its own BrowserContext (separate window per context, full
|
||||||
|
// cookie isolation, extension may not load).
|
||||||
|
export let activeMode = null;
|
||||||
|
|
||||||
|
export const setActiveContextName = (v) => { activeContextName = v; };
|
||||||
|
export const setActiveMode = (v) => { activeMode = v; };
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Constants.
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const LOAD_TIMEOUT = 60000;
|
||||||
|
export const INIT_TIMEOUT = 60000;
|
||||||
|
export const ACTION_WAIT = 2000; // fallback minimum wait
|
||||||
|
export const MAX_WAIT = 10000; // max wait for stability
|
||||||
|
export const POLL_INTERVAL = 200; // polling interval
|
||||||
|
export const STABLE_CYCLES = 3; // consecutive stable cycles needed
|
||||||
|
|
||||||
|
// 1C browser extension ID (stable across versions, defined by key in manifest.json)
|
||||||
|
export const EXT_ID = 'pbhelknnhilelbnhfpcjlcabhmfangik';
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
// Utilities that only depend on state.
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Normalize ё→е and →space for fuzzy matching. */
|
||||||
|
export const normYo = (s) => s.replace(/ё/gi, 'е').replace(/ /g, ' ');
|
||||||
|
|
||||||
|
/** Check if browser is connected and page is usable. */
|
||||||
|
export function isConnected() {
|
||||||
|
if (!browser || !page || page.isClosed()) return false;
|
||||||
|
// launchPersistentContext returns BrowserContext (no isConnected), launch returns Browser
|
||||||
|
if (typeof browser.isConnected === 'function') return browser.isConnected();
|
||||||
|
// For persistent context, check via context's browser()
|
||||||
|
return browser.browser()?.isConnected() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureConnected() {
|
||||||
|
if (!isConnected()) {
|
||||||
|
throw new Error('Browser not connected. Call web_connect first.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the raw Playwright page object (for advanced scripting in skill mode). */
|
||||||
|
export function getPage() {
|
||||||
|
ensureConnected();
|
||||||
|
return page;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user