From 6c19846051b9a021b5f7dae6c92f50e0e25e93d0 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 10 May 2026 18:34:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20T4.6=20=E2=80=94=20=D0=B3?= =?UTF-8?q?=D0=B8=D0=B1=D1=80=D0=B8=D0=B4=D0=BD=D1=8B=D0=B5=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D1=8B=20=D0=B8=D0=B7=D0=BE=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20(tab=20default,=20window=20opt-in)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit browser.mjs v1.12 + run.mjs v1.9: createContext принимает isolation параметр. По умолчанию 'tab' — все контексты живут в одном launchPersistentContext, каждый слот получает свою Page (вкладку). Преимущества: 1С extension грузится надёжно (через --load-extension в persistent profile), один процесс Chromium, дешёвая память. Cookies делятся между вкладками, но скоупятся по URL-path — для модели «разные пользователи через разные vrd-публикации» это естественно и достаточно. isolation: 'window' (opt-in) — старый путь chromium.launch() + newContext(): полная изоляция cookies, отдельный BrowserContext (и окно) на каждый слот, но extension может не подняться. Использовать когда нужна изоляция auth внутри одного URL. Смешивать режимы в одном прогоне нельзя — createContext бросает явную ошибку (первый createContext устанавливает activeMode, остальные обязаны совпадать). Конфиг tests/web-test/webtest.config.mjs: добавлен комментарий с описанием обоих режимов. По умолчанию tab — синтетика и наши smoke-тесты идут им. Live: 11/12 в полном прогоне (default tab) + 3/3 sanity-check в window mode (01-navigation + 14 + 15). Видеозапись из T4.5 работает в обоих режимах. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/web-test/scripts/browser.mjs | 65 ++++++++++++++++----- .claude/skills/web-test/scripts/run.mjs | 9 +-- tests/web-test/webtest.config.mjs | 5 ++ 3 files changed, 60 insertions(+), 19 deletions(-) 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, };