mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-28 14:09:41 +03:00
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:
co-authored by
Claude Opus 4.7
parent
eef4f4bcea
commit
6c19846051
@@ -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
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Playwright browser management for 1C web client.
|
* 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.
|
// connect() does NOT use this Map — it preserves legacy single-session behavior for exec/run/start.
|
||||||
const contexts = new Map();
|
const contexts = new Map();
|
||||||
let activeContextName = null;
|
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 LOAD_TIMEOUT = 60000;
|
||||||
const INIT_TIMEOUT = 60000;
|
const INIT_TIMEOUT = 60000;
|
||||||
@@ -189,6 +193,7 @@ export async function disconnect() {
|
|||||||
}
|
}
|
||||||
contexts.clear();
|
contexts.clear();
|
||||||
activeContextName = null;
|
activeContextName = null;
|
||||||
|
activeMode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single-session path (connect): auto-stop recording if active
|
// 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
|
* Use this from run.mjs cmdTest only — exec/run/start use connect() and stay on the
|
||||||
* legacy persistent-context path.
|
* legacy persistent-context path.
|
||||||
*/
|
*/
|
||||||
export async function createContext(name, url, { extensionPath } = {}) {
|
export async function createContext(name, url, { extensionPath, isolation = 'tab' } = {}) {
|
||||||
if (contexts.has(name)) {
|
if (contexts.has(name)) {
|
||||||
await setActiveContext(name);
|
await setActiveContext(name);
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
||||||
@@ -325,35 +330,65 @@ export async function createContext(name, url, { extensionPath } = {}) {
|
|||||||
return await getPageState();
|
return await getPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// First context: launch browser. Subsequent: reuse existing browser.
|
if (!['tab', 'window'].includes(isolation)) {
|
||||||
if (!browser) {
|
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 extPath = findExtension(extensionPath);
|
||||||
const launchArgs = ['--start-maximized'];
|
const launchArgs = ['--start-maximized'];
|
||||||
if (extPath) {
|
if (extPath) {
|
||||||
launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath);
|
launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath);
|
||||||
}
|
}
|
||||||
browser = await chromium.launch({ headless: false, args: launchArgs });
|
if (isolation === 'tab') {
|
||||||
} else if (typeof browser.newContext !== 'function') {
|
// Persistent context: extension loads reliably, one window with tabs per context
|
||||||
throw new Error('createContext: existing browser was created via connect()/launchPersistentContext and cannot host additional isolated contexts. Call disconnect() first.');
|
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
|
// Save current active before switching
|
||||||
_saveActiveSlot();
|
_saveActiveSlot();
|
||||||
|
|
||||||
const newCtx = await browser.newContext({
|
// Create slot — page differs by mode
|
||||||
viewport: null,
|
let newCtx, newPage;
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
if (activeMode === 'tab') {
|
||||||
});
|
// Reuse the persistent context for all slots; each slot gets its own page (tab)
|
||||||
const newPage = await newCtx.newPage();
|
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 = {
|
const slot = {
|
||||||
context: newCtx,
|
context: newCtx,
|
||||||
page: newPage,
|
page: newPage,
|
||||||
sessionPrefix: null,
|
sessionPrefix: null,
|
||||||
seanceId: null,
|
seanceId: null,
|
||||||
recorder: null,
|
|
||||||
lastCaptions: [],
|
|
||||||
lastRecordingDuration: null,
|
|
||||||
highlightMode: false,
|
highlightMode: false,
|
||||||
};
|
};
|
||||||
contexts.set(name, slot);
|
contexts.set(name, slot);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* CLI runner for 1C web client automation.
|
* 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.
|
// Build context registry: name → url. Supports config.contexts or single config.url / CLI url.
|
||||||
// CLI url overrides default context's url.
|
// CLI url overrides default context's url.
|
||||||
const contextSpecs = {}; // name → { url }
|
const contextSpecs = {}; // name → { url, isolation }
|
||||||
let defaultContextName = 'default';
|
let defaultContextName = 'default';
|
||||||
|
const defaultIsolation = config.isolation || 'tab';
|
||||||
if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) {
|
if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) {
|
||||||
for (const [n, spec] of Object.entries(config.contexts)) {
|
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];
|
defaultContextName = config.defaultContext || Object.keys(config.contexts)[0];
|
||||||
if (url) contextSpecs[defaultContextName] = { url }; // CLI override of default
|
if (url) contextSpecs[defaultContextName] = { url }; // CLI override of default
|
||||||
@@ -504,7 +505,7 @@ async function cmdTest(rawArgs) {
|
|||||||
if (browser.hasContext(name)) return;
|
if (browser.hasContext(name)) return;
|
||||||
const spec = contextSpecs[name];
|
const spec = contextSpecs[name];
|
||||||
if (!spec) throw new Error(`Unknown context "${name}". Defined: [${Object.keys(contextSpecs).join(', ')}]`);
|
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 {
|
try {
|
||||||
|
|||||||
@@ -7,5 +7,10 @@ export default {
|
|||||||
b: { url: 'http://localhost:8081/webtest/ru_RU' },
|
b: { url: 'http://localhost:8081/webtest/ru_RU' },
|
||||||
},
|
},
|
||||||
defaultContext: 'a',
|
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,
|
timeout: 60000,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user