magnitude-core
Version:
Magnitude e2e testing agent
154 lines (153 loc) • 6.63 kB
JavaScript
import { chromium } from "playwright";
import objectHash from 'object-hash';
import crypto from 'node:crypto';
import logger from "@/logger";
const DEFAULT_BROWSER_OPTIONS = {
headless: false,
args: ["--disable-gpu", "--disable-blink-features=AutomationControlled"],
};
const DEFAULT_BROWSER_CONTEXT_OPTIONS = {
viewport: { width: 1024, height: 768 },
};
export class BrowserProvider {
activeBrowsers = {};
logger;
constructor() {
this.logger = logger.child({ name: 'browser_provider' });
}
static getInstance() {
if (!globalThis.__magnitude__) {
globalThis.__magnitude__ = {};
}
if (!globalThis.__magnitude__.browserProvider) {
globalThis.__magnitude__.browserProvider = new BrowserProvider();
}
return globalThis.__magnitude__.browserProvider;
}
async _launchOrReuseBrowser(options) {
// hash options
const hash = objectHash({
...options,
logger: options.logger ? crypto.randomUUID() : '' // replace unserializable logger - use UUID to force re-instance in case different loggers provided
});
let activeBrowser;
if (!(hash in this.activeBrowsers)) {
this.logger.trace("Launching new browser");
// Launch new browser, get the PROMISE
const launchPromise = chromium.launch({ ...DEFAULT_BROWSER_OPTIONS, ...options });
activeBrowser = {
browserPromise: launchPromise,
activeContextsCount: 0
};
// add immediately in case others need to await the same one as well
this.activeBrowsers[hash] = activeBrowser;
// Wait for browser to fully start
const browser = await launchPromise;
browser.on('disconnected', () => {
delete this.activeBrowsers[hash];
});
return activeBrowser;
}
else {
this.logger.trace("Browser with same launch options exists, reusing");
return this.activeBrowsers[hash];
}
}
async _createAndTrackContext(options) {
const activeBrowserEntry = await this._launchOrReuseBrowser('launchOptions' in options ? options.launchOptions : {});
const browser = await activeBrowserEntry.browserPromise;
const contextOptions = 'contextOptions' in options ? options.contextOptions : undefined;
const context = await browser.newContext(contextOptions);
// Get viewport dimensions from context options or use defaults
const viewport = contextOptions?.viewport || { width: 1024, height: 768 };
const deviceScaleFactor = contextOptions?.deviceScaleFactor || 1;
// Apply emulation settings to any new pages created
context.on('page', async (page) => {
const cdpSession = await page.context().newCDPSession(page);
await this._applyEmulationSettings(cdpSession, viewport.width, viewport.height, deviceScaleFactor);
});
activeBrowserEntry.activeContextsCount++;
context.on('close', async () => {
activeBrowserEntry.activeContextsCount--;
if (activeBrowserEntry.activeContextsCount <= 0 && browser.isConnected()) {
await browser.close();
}
});
return context;
}
async newContext(options) {
if (options && 'context' in options) {
// Context directly provided, we don't need to manage it
return options.context;
}
const dpr = process.env.DEVICE_PIXEL_RATIO ?
parseInt(process.env.DEVICE_PIXEL_RATIO) :
process.platform === 'darwin' ? 2 : 1;
const contextOptions = {
...DEFAULT_BROWSER_CONTEXT_OPTIONS,
deviceScaleFactor: dpr,
...(options && 'contextOptions' in options && options.contextOptions ? options.contextOptions : {}) //options.browser?.contextOptions
};
options = { ...options, contextOptions };
if (process.env.MAGNTIUDE_PLAYGROUND) {
// this.logger.trace("MAGNITUDE_PLAYGROUND environment detected, connecting to browser via CDP");
// Playground environment - force use CDP on 9222
//const browser = await chromium.connectOverCDP('http://localhost:9222');
//return browser.newContext(options?.contextOptions);
this.logger.trace("MAGNITUDE_PLAYGROUND environment detected, applying playground launch options");
const playgroundLaunchOptions = {
args: [
'--remote-debugging-port=9222',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu'
]
};
// Overwrite any launch options, instance, or cdp configuration with playground launch options
// Ignore context options (?)
options = {
launchOptions: playgroundLaunchOptions
};
}
if ('cdp' in options) {
const browser = await chromium.connectOverCDP(options.cdp);
if (browser.contexts().length > 0) {
return browser.contexts()[0];
}
else {
return browser.newContext(options.contextOptions);
}
}
else if ('instance' in options) {
const browser = options.instance;
if (browser.contexts().length > 0) {
return browser.contexts()[0];
}
else {
return browser.newContext(options.contextOptions);
}
}
else if ('launchOptions' in options) {
this.logger.trace('Creating context with custom launch options');
return await this._createAndTrackContext(options);
}
else {
// contextOptions might be passed but no instance | cdp | launchOptions
this.logger.trace('Creating context for default browser options');
return await this._createAndTrackContext(options);
}
}
async _applyEmulationSettings(cdpSession, width, height, deviceScaleFactor) {
await cdpSession.send('Emulation.setDeviceMetricsOverride', {
width: width,
height: height,
deviceScaleFactor: deviceScaleFactor,
mobile: false,
screenWidth: width,
screenHeight: height,
positionX: 0,
positionY: 0,
screenOrientation: { angle: 0, type: 'portraitPrimary' }
});
}
}