feat(web-test): T4.6 — гибридные режимы изоляции контекстов (tab default, window opt-in)

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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-10 18:34:44 +03:00
co-authored by Claude Opus 4.7
parent eef4f4bcea
commit 6c19846051
3 changed files with 60 additions and 19 deletions
+50 -15
View File
@@ -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);