diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 3bdae67c..65146710 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -1,4 +1,4 @@ -// web-test browser v1.11 — Playwright browser management for 1C web client +// web-test browser v1.12 — Playwright browser management for 1C web client // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Playwright browser management for 1C web client. @@ -42,6 +42,10 @@ let highlightMode = false; // 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; @@ -189,6 +193,7 @@ export async function disconnect() { } contexts.clear(); activeContextName = null; + activeMode = null; } // Single-session path (connect): auto-stop recording if active @@ -315,7 +320,7 @@ function _attachSessionListeners(pg, slot, name) { * Use this from run.mjs cmdTest only — exec/run/start use connect() and stay on the * legacy persistent-context path. */ -export async function createContext(name, url, { extensionPath } = {}) { +export async function createContext(name, url, { extensionPath, isolation = 'tab' } = {}) { if (contexts.has(name)) { await setActiveContext(name); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT }); @@ -325,35 +330,65 @@ export async function createContext(name, url, { extensionPath } = {}) { return await getPageState(); } - // First context: launch browser. Subsequent: reuse existing browser. - if (!browser) { + if (!['tab', 'window'].includes(isolation)) { + throw new Error(`createContext: invalid isolation "${isolation}", expected 'tab' or 'window'`); + } + if (activeMode && activeMode !== isolation) { + throw new Error(`createContext: cannot mix isolation modes — first context used "${activeMode}", "${name}" requested "${isolation}". Use the same mode for all contexts in one run.`); + } + + // First context: launch browser. Subsequent: reuse existing. + let isFirstContext = !browser; + if (isFirstContext) { const extPath = findExtension(extensionPath); const launchArgs = ['--start-maximized']; if (extPath) { launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath); } - browser = await chromium.launch({ headless: false, args: launchArgs }); - } else if (typeof browser.newContext !== 'function') { - throw new Error('createContext: existing browser was created via connect()/launchPersistentContext and cannot host additional isolated contexts. Call disconnect() first.'); + if (isolation === 'tab') { + // Persistent context: extension loads reliably, one window with tabs per context + persistentUserDataDir = pathJoin(tmpdir(), 'pw-1c-test-' + Date.now()); + mkdirSync(persistentUserDataDir, { recursive: true }); + browser = await chromium.launchPersistentContext(persistentUserDataDir, { + headless: false, + args: launchArgs, + viewport: null, + permissions: ['clipboard-read', 'clipboard-write'], + }); + } else { + // Window mode: separate BrowserContext per slot, full cookie isolation + browser = await chromium.launch({ headless: false, args: launchArgs }); + } + activeMode = isolation; } // Save current active before switching _saveActiveSlot(); - const newCtx = await browser.newContext({ - viewport: null, - permissions: ['clipboard-read', 'clipboard-write'], - }); - const newPage = await newCtx.newPage(); + // Create slot — page differs by mode + let newCtx, newPage; + if (activeMode === 'tab') { + // Reuse the persistent context for all slots; each slot gets its own page (tab) + newCtx = browser; + if (isFirstContext) { + newPage = browser.pages()[0] || await browser.newPage(); + } else { + newPage = await browser.newPage(); + } + } else { + // Window mode: each slot owns its BrowserContext + page + newCtx = await browser.newContext({ + viewport: null, + permissions: ['clipboard-read', 'clipboard-write'], + }); + newPage = await newCtx.newPage(); + } const slot = { context: newCtx, page: newPage, sessionPrefix: null, seanceId: null, - recorder: null, - lastCaptions: [], - lastRecordingDuration: null, highlightMode: false, }; contexts.set(name, slot); diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index ef88cf67..96ab0148 100644 --- a/.claude/skills/web-test/scripts/run.mjs +++ b/.claude/skills/web-test/scripts/run.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// web-test run v1.8 — CLI runner for 1C web client automation +// web-test run v1.9 — CLI runner for 1C web client automation // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * CLI runner for 1C web client automation. @@ -396,11 +396,12 @@ async function cmdTest(rawArgs) { } // Build context registry: name → url. Supports config.contexts or single config.url / CLI url. // CLI url overrides default context's url. - const contextSpecs = {}; // name → { url } + const contextSpecs = {}; // name → { url, isolation } let defaultContextName = 'default'; + const defaultIsolation = config.isolation || 'tab'; if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) { for (const [n, spec] of Object.entries(config.contexts)) { - contextSpecs[n] = { url: spec.url }; + contextSpecs[n] = { url: spec.url, isolation: spec.isolation }; } defaultContextName = config.defaultContext || Object.keys(config.contexts)[0]; if (url) contextSpecs[defaultContextName] = { url }; // CLI override of default @@ -504,7 +505,7 @@ async function cmdTest(rawArgs) { if (browser.hasContext(name)) return; const spec = contextSpecs[name]; if (!spec) throw new Error(`Unknown context "${name}". Defined: [${Object.keys(contextSpecs).join(', ')}]`); - await browser.createContext(name, spec.url); + await browser.createContext(name, spec.url, { isolation: spec.isolation || defaultIsolation }); } try { diff --git a/tests/web-test/webtest.config.mjs b/tests/web-test/webtest.config.mjs index 3906f7b6..03811a95 100644 --- a/tests/web-test/webtest.config.mjs +++ b/tests/web-test/webtest.config.mjs @@ -7,5 +7,10 @@ export default { b: { url: 'http://localhost:8081/webtest/ru_RU' }, }, defaultContext: 'a', + // isolation: 'tab' (default) — persistent context, tabs in one window, 1С extension loads. + // Cookies are shared between tabs but scope by URL path, so different vrd-publications + // give independent auth without extra isolation. + // isolation: 'window' — separate BrowserContext per slot, full cookie isolation, + // extension may not load (Playwright limitation). Use only when really needed. timeout: 60000, };