935 lines
36 KiB
JavaScript
935 lines
36 KiB
JavaScript
/*!
|
|
* playwright-extra v4.3.5 by berstend
|
|
* https://github.com/berstend/puppeteer-extra/tree/master/packages/playwright-extra#readme
|
|
* @license MIT
|
|
*/
|
|
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
|
|
|
var Debug = _interopDefault(require('debug'));
|
|
|
|
/** Node.js module loader helper */
|
|
class Loader {
|
|
constructor(moduleName, packageNames) {
|
|
this.moduleName = moduleName;
|
|
this.packageNames = packageNames;
|
|
}
|
|
/**
|
|
* Lazy load a top level export from another module by wrapping it in a JS proxy.
|
|
*
|
|
* This allows us to re-export e.g. `devices` from `playwright` while redirecting direct calls
|
|
* to it to the module version the user has installed, rather than shipping with a hardcoded version.
|
|
*
|
|
* If we don't do this and the user doesn't have the target module installed we'd throw immediately when our code is imported.
|
|
*
|
|
* We use a "super" Proxy defining all traps, so calls like `Object.keys(playwright.devices).length` will return the correct value.
|
|
*/
|
|
lazyloadExportOrDie(exportName) {
|
|
const that = this;
|
|
const trapHandler = Object.fromEntries(Object.getOwnPropertyNames(Reflect).map((name) => [
|
|
name,
|
|
function (target, ...args) {
|
|
const moduleExport = that.loadModuleOrDie()[exportName];
|
|
const customTarget = moduleExport;
|
|
const result = Reflect[name](customTarget || target, ...args);
|
|
return result;
|
|
}
|
|
]));
|
|
return new Proxy({}, trapHandler);
|
|
}
|
|
/** Load the module if possible */
|
|
loadModule() {
|
|
return requirePackages(this.packageNames);
|
|
}
|
|
/** Load the module if possible or throw */
|
|
loadModuleOrDie() {
|
|
const module = requirePackages(this.packageNames);
|
|
if (module) {
|
|
return module;
|
|
}
|
|
throw this.requireError;
|
|
}
|
|
get requireError() {
|
|
const moduleNamePretty = this.moduleName.charAt(0).toUpperCase() + this.moduleName.slice(1);
|
|
return new Error(`
|
|
${moduleNamePretty} is missing. :-)
|
|
|
|
I've tried loading ${this.packageNames
|
|
.map(p => `"${p}"`)
|
|
.join(', ')} - no luck.
|
|
|
|
Make sure you install one of those packages or use the named 'addExtra' export,
|
|
to patch a specific (and maybe non-standard) implementation of ${moduleNamePretty}.
|
|
|
|
To get the latest stable version of ${moduleNamePretty} run:
|
|
'yarn add ${this.moduleName}' or 'npm i ${this.moduleName}'
|
|
`);
|
|
}
|
|
}
|
|
function requirePackages(packageNames) {
|
|
for (const name of packageNames) {
|
|
try {
|
|
return require(name);
|
|
}
|
|
catch (_) {
|
|
continue; // noop
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
/** Playwright specific module loader */
|
|
const playwrightLoader = new Loader('playwright', [
|
|
'playwright-core',
|
|
'playwright'
|
|
]);
|
|
|
|
const debug = Debug('playwright-extra:puppeteer-compat');
|
|
const isPlaywrightPage = (obj) => {
|
|
return 'unroute' in obj;
|
|
};
|
|
const isPlaywrightFrame = (obj) => {
|
|
return ['parentFrame', 'frameLocator'].every(x => x in obj);
|
|
};
|
|
const isPlaywrightBrowser = (obj) => {
|
|
return 'newContext' in obj;
|
|
};
|
|
const isPuppeteerCompat = (obj) => {
|
|
return !!obj && typeof obj === 'object' && !!obj.isCompatShim;
|
|
};
|
|
const cache = {
|
|
objectToShim: new Map(),
|
|
cdpSession: {
|
|
page: new Map(),
|
|
browser: new Map()
|
|
}
|
|
};
|
|
/** Augment a Playwright object with compatibility with certain Puppeteer methods */
|
|
function addPuppeteerCompat(object) {
|
|
if (!object || typeof object !== 'object') {
|
|
return object;
|
|
}
|
|
if (cache.objectToShim.has(object)) {
|
|
return cache.objectToShim.get(object);
|
|
}
|
|
if (isPuppeteerCompat(object)) {
|
|
return object;
|
|
}
|
|
debug('addPuppeteerCompat', cache.objectToShim.size);
|
|
if (isPlaywrightPage(object) || isPlaywrightFrame(object)) {
|
|
const shim = createPageShim(object);
|
|
cache.objectToShim.set(object, shim);
|
|
return shim;
|
|
}
|
|
if (isPlaywrightBrowser(object)) {
|
|
const shim = createBrowserShim(object);
|
|
cache.objectToShim.set(object, shim);
|
|
return shim;
|
|
}
|
|
debug('Received unknown object:', Reflect.ownKeys(object));
|
|
return object;
|
|
}
|
|
// Only chromium browsers support CDP
|
|
const dummyCDPClient = {
|
|
send: async (...args) => {
|
|
debug('dummy CDP client called', 'send', args);
|
|
},
|
|
on: (...args) => {
|
|
debug('dummy CDP client called', 'on', args);
|
|
}
|
|
};
|
|
async function getPageCDPSession(page) {
|
|
let session = cache.cdpSession.page.get(page);
|
|
if (session) {
|
|
debug('getPageCDPSession: use existing');
|
|
return session;
|
|
}
|
|
debug('getPageCDPSession: use new');
|
|
const context = isPlaywrightFrame(page)
|
|
? page.page().context()
|
|
: page.context();
|
|
try {
|
|
session = await context.newCDPSession(page);
|
|
cache.cdpSession.page.set(page, session);
|
|
return session;
|
|
}
|
|
catch (err) {
|
|
debug('getPageCDPSession: error while creating session:', err.message);
|
|
debug('getPageCDPSession: Unable create CDP session (most likely a different browser than chromium) - returning a dummy');
|
|
}
|
|
return dummyCDPClient;
|
|
}
|
|
async function getBrowserCDPSession(browser) {
|
|
let session = cache.cdpSession.browser.get(browser);
|
|
if (session) {
|
|
debug('getBrowserCDPSession: use existing');
|
|
return session;
|
|
}
|
|
debug('getBrowserCDPSession: use new');
|
|
try {
|
|
session = await browser.newBrowserCDPSession();
|
|
cache.cdpSession.browser.set(browser, session);
|
|
return session;
|
|
}
|
|
catch (err) {
|
|
debug('getBrowserCDPSession: error while creating session:', err.message);
|
|
debug('getBrowserCDPSession: Unable create CDP session (most likely a different browser than chromium) - returning a dummy');
|
|
}
|
|
return dummyCDPClient;
|
|
}
|
|
function createPageShim(page) {
|
|
const objId = Math.random().toString(36).substring(2, 7);
|
|
const shim = new Proxy(page, {
|
|
get(target, prop) {
|
|
if (prop === 'isCompatShim' || prop === 'isPlaywright') {
|
|
return true;
|
|
}
|
|
debug('page - get', objId, prop);
|
|
if (prop === '_client') {
|
|
return () => ({
|
|
send: async (method, params) => {
|
|
const session = await getPageCDPSession(page);
|
|
return await session.send(method, params);
|
|
},
|
|
on: (event, listener) => {
|
|
getPageCDPSession(page).then(session => {
|
|
session.on(event, listener);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
if (prop === 'setBypassCSP') {
|
|
return async (enabled) => {
|
|
const session = await getPageCDPSession(page);
|
|
return await session.send('Page.setBypassCSP', {
|
|
enabled
|
|
});
|
|
};
|
|
}
|
|
if (prop === 'setUserAgent') {
|
|
return async (userAgent, userAgentMetadata) => {
|
|
const session = await getPageCDPSession(page);
|
|
return await session.send('Emulation.setUserAgentOverride', {
|
|
userAgent,
|
|
userAgentMetadata
|
|
});
|
|
};
|
|
}
|
|
if (prop === 'browser') {
|
|
if (isPlaywrightPage(page)) {
|
|
return () => {
|
|
let browser = page.context().browser();
|
|
if (!browser) {
|
|
debug('page.browser() - not available, most likely due to launchPersistentContext');
|
|
// Use a page shim as quick drop-in (so browser.userAgent() still works)
|
|
browser = page;
|
|
}
|
|
return addPuppeteerCompat(browser);
|
|
};
|
|
}
|
|
}
|
|
if (prop === 'evaluateOnNewDocument') {
|
|
if (isPlaywrightPage(page)) {
|
|
return async function (pageFunction, ...args) {
|
|
return await page.addInitScript(pageFunction, args[0]);
|
|
};
|
|
}
|
|
}
|
|
// Only relevant when page is being used a pseudo stand-in for the browser object (launchPersistentContext)
|
|
if (prop === 'userAgent') {
|
|
return async (enabled) => {
|
|
const session = await getPageCDPSession(page);
|
|
const data = await session.send('Browser.getVersion');
|
|
return data.userAgent;
|
|
};
|
|
}
|
|
return Reflect.get(target, prop);
|
|
}
|
|
});
|
|
return shim;
|
|
}
|
|
function createBrowserShim(browser) {
|
|
const objId = Math.random().toString(36).substring(2, 7);
|
|
const shim = new Proxy(browser, {
|
|
get(target, prop) {
|
|
if (prop === 'isCompatShim' || prop === 'isPlaywright') {
|
|
return true;
|
|
}
|
|
debug('browser - get', objId, prop);
|
|
if (prop === 'pages') {
|
|
return () => browser
|
|
.contexts()
|
|
.flatMap(c => c.pages().map(page => addPuppeteerCompat(page)));
|
|
}
|
|
if (prop === 'userAgent') {
|
|
return async () => {
|
|
const session = await getBrowserCDPSession(browser);
|
|
const data = await session.send('Browser.getVersion');
|
|
return data.userAgent;
|
|
};
|
|
}
|
|
return Reflect.get(target, prop);
|
|
}
|
|
});
|
|
return shim;
|
|
}
|
|
|
|
const debug$1 = Debug('playwright-extra:plugins');
|
|
class PluginList {
|
|
constructor() {
|
|
this._plugins = [];
|
|
this._dependencyDefaults = new Map();
|
|
this._dependencyResolution = new Map();
|
|
}
|
|
/**
|
|
* Get a list of all registered plugins.
|
|
*/
|
|
get list() {
|
|
return this._plugins;
|
|
}
|
|
/**
|
|
* Get the names of all registered plugins.
|
|
*/
|
|
get names() {
|
|
return this._plugins.map(p => p.name);
|
|
}
|
|
/**
|
|
* Add a new plugin to the list (after checking if it's well-formed).
|
|
*
|
|
* @param plugin
|
|
* @internal
|
|
*/
|
|
add(plugin) {
|
|
var _a;
|
|
if (!this.isValidPluginInstance(plugin)) {
|
|
return false;
|
|
}
|
|
if (!!plugin.onPluginRegistered) {
|
|
plugin.onPluginRegistered({ framework: 'playwright' });
|
|
}
|
|
// PuppeteerExtraPlugin: Populate `_childClassMembers` list containing methods defined by the plugin
|
|
if (!!plugin._registerChildClassMembers) {
|
|
plugin._registerChildClassMembers(Object.getPrototypeOf(plugin));
|
|
}
|
|
if ((_a = plugin.requirements) === null || _a === void 0 ? void 0 : _a.has('dataFromPlugins')) {
|
|
plugin.getDataFromPlugins = this.getData.bind(this);
|
|
}
|
|
this._plugins.push(plugin);
|
|
return true;
|
|
}
|
|
/** Check if the shape of a plugin is correct or warn */
|
|
isValidPluginInstance(plugin) {
|
|
if (!plugin ||
|
|
typeof plugin !== 'object' ||
|
|
!plugin._isPuppeteerExtraPlugin) {
|
|
console.error(`Warning: Plugin is not derived from PuppeteerExtraPlugin, ignoring.`, plugin);
|
|
return false;
|
|
}
|
|
if (!plugin.name) {
|
|
console.error(`Warning: Plugin with no name registering, ignoring.`, plugin);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/** Error callback in case calling a plugin method throws an error. Can be overwritten. */
|
|
onPluginError(plugin, method, err) {
|
|
console.warn(`An error occured while executing "${method}" in plugin "${plugin.name}":`, err);
|
|
}
|
|
/**
|
|
* Define default values for plugins implicitly required through the `dependencies` plugin stanza.
|
|
*
|
|
* @param dependencyPath - The string by which the dependency is listed (not the plugin name)
|
|
*
|
|
* @example
|
|
* chromium.use(stealth)
|
|
* chromium.plugins.setDependencyDefaults('stealth/evasions/webgl.vendor', { vendor: 'Bob', renderer: 'Alice' })
|
|
*/
|
|
setDependencyDefaults(dependencyPath, opts) {
|
|
this._dependencyDefaults.set(dependencyPath, opts);
|
|
return this;
|
|
}
|
|
/**
|
|
* Define custom plugin modules for plugins implicitly required through the `dependencies` plugin stanza.
|
|
*
|
|
* Using this will prevent dynamic imports from being used, which JS bundlers often have issues with.
|
|
*
|
|
* @example
|
|
* chromium.use(stealth)
|
|
* chromium.plugins.setDependencyResolution('stealth/evasions/webgl.vendor', VendorPlugin)
|
|
*/
|
|
setDependencyResolution(dependencyPath, pluginModule) {
|
|
this._dependencyResolution.set(dependencyPath, pluginModule);
|
|
return this;
|
|
}
|
|
/**
|
|
* Prepare plugins to be used (resolve dependencies, ordering)
|
|
* @internal
|
|
*/
|
|
prepare() {
|
|
this.resolveDependencies();
|
|
this.order();
|
|
}
|
|
/** Return all plugins using the supplied method */
|
|
filterByMethod(methodName) {
|
|
return this._plugins.filter(plugin => {
|
|
// PuppeteerExtraPlugin: The base class will already define all methods, hence we need to do a different check
|
|
if (!!plugin._childClassMembers &&
|
|
Array.isArray(plugin._childClassMembers)) {
|
|
return plugin._childClassMembers.includes(methodName);
|
|
}
|
|
return methodName in plugin;
|
|
});
|
|
}
|
|
/** Conditionally add puppeteer compatibility to values provided to the plugins */
|
|
_addPuppeteerCompatIfNeeded(plugin, method, args) {
|
|
const canUseShim = plugin._isPuppeteerExtraPlugin && !plugin.noPuppeteerShim;
|
|
const methodWhitelist = [
|
|
'onBrowser',
|
|
'onPageCreated',
|
|
'onPageClose',
|
|
'afterConnect',
|
|
'afterLaunch'
|
|
];
|
|
const shouldUseShim = methodWhitelist.includes(method);
|
|
if (!canUseShim || !shouldUseShim) {
|
|
return args;
|
|
}
|
|
debug$1('add puppeteer compatibility', plugin.name, method);
|
|
return [...args.map(arg => addPuppeteerCompat(arg))];
|
|
}
|
|
/**
|
|
* Dispatch plugin lifecycle events in a typesafe way.
|
|
* Only Plugins that expose the supplied property will be called.
|
|
*
|
|
* Will not await results to dispatch events as fast as possible to all plugins.
|
|
*
|
|
* @param method - The lifecycle method name
|
|
* @param args - Optional: Any arguments to be supplied to the plugin methods
|
|
* @internal
|
|
*/
|
|
dispatch(method, ...args) {
|
|
var _a, _b;
|
|
const plugins = this.filterByMethod(method);
|
|
debug$1('dispatch', method, {
|
|
all: this._plugins.length,
|
|
filteredByMethod: plugins.length
|
|
});
|
|
for (const plugin of plugins) {
|
|
try {
|
|
args = this._addPuppeteerCompatIfNeeded.bind(this)(plugin, method, args);
|
|
const fnType = (_b = (_a = plugin[method]) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name;
|
|
debug$1('dispatch to plugin', {
|
|
plugin: plugin.name,
|
|
method,
|
|
fnType
|
|
});
|
|
if (fnType === 'AsyncFunction') {
|
|
;
|
|
plugin[method](...args).catch((err) => this.onPluginError(plugin, method, err));
|
|
}
|
|
else {
|
|
;
|
|
plugin[method](...args);
|
|
}
|
|
}
|
|
catch (err) {
|
|
this.onPluginError(plugin, method, err);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Dispatch plugin lifecycle events in a typesafe way.
|
|
* Only Plugins that expose the supplied property will be called.
|
|
*
|
|
* Can also be used to get a definite return value after passing it to plugins:
|
|
* Calls plugins sequentially and passes on a value (waterfall style).
|
|
*
|
|
* The plugins can either modify the value or return an updated one.
|
|
* Will return the latest, updated value which ran through all plugins.
|
|
*
|
|
* By convention only the first argument will be used as the updated value.
|
|
*
|
|
* @param method - The lifecycle method name
|
|
* @param args - Optional: Any arguments to be supplied to the plugin methods
|
|
* @internal
|
|
*/
|
|
async dispatchBlocking(method, ...args) {
|
|
const plugins = this.filterByMethod(method);
|
|
debug$1('dispatchBlocking', method, {
|
|
all: this._plugins.length,
|
|
filteredByMethod: plugins.length
|
|
});
|
|
let retValue = null;
|
|
for (const plugin of plugins) {
|
|
try {
|
|
args = this._addPuppeteerCompatIfNeeded.bind(this)(plugin, method, args);
|
|
retValue = await plugin[method](...args);
|
|
// In case we got a return value use that as new first argument for followup function calls
|
|
if (retValue !== undefined) {
|
|
args[0] = retValue;
|
|
}
|
|
}
|
|
catch (err) {
|
|
this.onPluginError(plugin, method, err);
|
|
return retValue;
|
|
}
|
|
}
|
|
return retValue;
|
|
}
|
|
/**
|
|
* Order plugins that have expressed a special placement requirement.
|
|
*
|
|
* This is useful/necessary for e.g. plugins that depend on the data from other plugins.
|
|
*
|
|
* @private
|
|
*/
|
|
order() {
|
|
debug$1('order:before', this.names);
|
|
const runLast = this._plugins
|
|
.filter(p => { var _a; return (_a = p.requirements) === null || _a === void 0 ? void 0 : _a.has('runLast'); })
|
|
.map(p => p.name);
|
|
for (const name of runLast) {
|
|
const index = this._plugins.findIndex(p => p.name === name);
|
|
this._plugins.push(this._plugins.splice(index, 1)[0]);
|
|
}
|
|
debug$1('order:after', this.names);
|
|
}
|
|
/**
|
|
* Collects the exposed `data` property of all registered plugins.
|
|
* Will be reduced/flattened to a single array.
|
|
*
|
|
* Can be accessed by plugins that listed the `dataFromPlugins` requirement.
|
|
*
|
|
* Implemented mainly for plugins that need data from other plugins (e.g. `user-preferences`).
|
|
*
|
|
* @see [PuppeteerExtraPlugin]/data
|
|
* @param name - Filter data by optional name
|
|
*
|
|
* @private
|
|
*/
|
|
getData(name) {
|
|
const data = this._plugins
|
|
.filter((p) => !!p.data)
|
|
.map((p) => (Array.isArray(p.data) ? p.data : [p.data]))
|
|
.reduce((acc, arr) => [...acc, ...arr], []);
|
|
return name ? data.filter((d) => d.name === name) : data;
|
|
}
|
|
/**
|
|
* Handle `plugins` stanza (already instantiated plugins that don't require dynamic imports)
|
|
*/
|
|
resolvePluginsStanza() {
|
|
debug$1('resolvePluginsStanza');
|
|
const pluginNames = new Set(this.names);
|
|
this._plugins
|
|
.filter(p => !!p.plugins && p.plugins.length)
|
|
.filter(p => !pluginNames.has(p.name)) // TBD: Do we want to filter out existing?
|
|
.forEach(parent => {
|
|
(parent.plugins || []).forEach(p => {
|
|
debug$1(parent.name, 'adding missing plugin', p.name);
|
|
this.add(p);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Handle `dependencies` stanza (which requires dynamic imports)
|
|
*
|
|
* Plugins can define `dependencies` as a Set or Array of dependency paths, or a Map with additional opts
|
|
*
|
|
* @note
|
|
* - The default opts for implicit dependencies can be defined using `setDependencyDefaults()`
|
|
* - Dynamic imports can be avoided by providing plugin modules with `setDependencyResolution()`
|
|
*/
|
|
resolveDependenciesStanza() {
|
|
debug$1('resolveDependenciesStanza');
|
|
/** Attempt to dynamically require a plugin module */
|
|
const requireDependencyOrDie = (parentName, dependencyPath) => {
|
|
// If the user provided the plugin module already we use that
|
|
if (this._dependencyResolution.has(dependencyPath)) {
|
|
return this._dependencyResolution.get(dependencyPath);
|
|
}
|
|
const possiblePrefixes = ['puppeteer-extra-plugin-']; // could be extended later
|
|
const isAlreadyPrefixed = possiblePrefixes.some(prefix => dependencyPath.startsWith(prefix));
|
|
const packagePaths = [];
|
|
// If the dependency is not already prefixed we attempt to require all possible combinations to find one that works
|
|
if (!isAlreadyPrefixed) {
|
|
packagePaths.push(...possiblePrefixes.map(prefix => prefix + dependencyPath));
|
|
}
|
|
// We always attempt to require the path verbatim (as a last resort)
|
|
packagePaths.push(dependencyPath);
|
|
const pluginModule = requirePackages(packagePaths);
|
|
if (pluginModule) {
|
|
return pluginModule;
|
|
}
|
|
const explanation = `
|
|
The plugin '${parentName}' listed '${dependencyPath}' as dependency,
|
|
which could not be found. Please install it:
|
|
|
|
${packagePaths
|
|
.map(packagePath => `yarn add ${packagePath.split('/')[0]}`)
|
|
.join(`\n or:\n`)}
|
|
|
|
Note: You don't need to require the plugin yourself,
|
|
unless you want to modify it's default settings.
|
|
|
|
If your bundler has issues with dynamic imports take a look at '.plugins.setDependencyResolution()'.
|
|
`;
|
|
console.warn(explanation);
|
|
throw new Error('Plugin dependency not found');
|
|
};
|
|
const existingPluginNames = new Set(this.names);
|
|
const recursivelyLoadMissingDependencies = ({ name: parentName, dependencies }) => {
|
|
if (!dependencies) {
|
|
return;
|
|
}
|
|
const processDependency = (dependencyPath, opts) => {
|
|
const pluginModule = requireDependencyOrDie(parentName, dependencyPath);
|
|
opts = opts || this._dependencyDefaults.get(dependencyPath) || {};
|
|
const plugin = pluginModule(opts);
|
|
if (existingPluginNames.has(plugin.name)) {
|
|
debug$1(parentName, '=> dependency already exists:', plugin.name);
|
|
return;
|
|
}
|
|
existingPluginNames.add(plugin.name);
|
|
debug$1(parentName, '=> adding new dependency:', plugin.name, opts);
|
|
this.add(plugin);
|
|
return recursivelyLoadMissingDependencies(plugin);
|
|
};
|
|
if (dependencies instanceof Set || Array.isArray(dependencies)) {
|
|
return [...dependencies].forEach(dependencyPath => processDependency(dependencyPath));
|
|
}
|
|
if (dependencies instanceof Map) {
|
|
// Note: `k,v => v,k` (Map + forEach will reverse the order)
|
|
return dependencies.forEach((v, k) => processDependency(k, v));
|
|
}
|
|
};
|
|
this.list.forEach(recursivelyLoadMissingDependencies);
|
|
}
|
|
/**
|
|
* Lightweight plugin dependency management to require plugins and code mods on demand.
|
|
* @private
|
|
*/
|
|
resolveDependencies() {
|
|
debug$1('resolveDependencies');
|
|
this.resolvePluginsStanza();
|
|
this.resolveDependenciesStanza();
|
|
}
|
|
}
|
|
|
|
const debug$2 = Debug('playwright-extra');
|
|
/**
|
|
* Modular plugin framework to teach `playwright` new tricks.
|
|
*/
|
|
class PlaywrightExtraClass {
|
|
constructor(_launcher) {
|
|
this._launcher = _launcher;
|
|
this.plugins = new PluginList();
|
|
}
|
|
/**
|
|
* The **main interface** to register plugins.
|
|
*
|
|
* Can be called multiple times to enable multiple plugins.
|
|
*
|
|
* Plugins derived from `PuppeteerExtraPlugin` will be used with a compatiblity layer.
|
|
*
|
|
* @example
|
|
* chromium.use(plugin1).use(plugin2)
|
|
* firefox.use(plugin1).use(plugin2)
|
|
*
|
|
* @see [PuppeteerExtraPlugin]
|
|
*
|
|
* @return The same `PlaywrightExtra` instance (for optional chaining)
|
|
*/
|
|
use(plugin) {
|
|
const isValid = plugin && 'name' in plugin;
|
|
if (!isValid) {
|
|
throw new Error('A plugin must be provided to .use()');
|
|
}
|
|
if (this.plugins.add(plugin)) {
|
|
debug$2('Plugin registered', plugin.name);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* In order to support a default export which will require vanilla playwright automatically,
|
|
* as well as `addExtra` to patch a provided launcher, we need to so some gymnastics here.
|
|
*
|
|
* Otherwise this would throw immediately, even when only using the `addExtra` export with an arbitrary compatible launcher.
|
|
*
|
|
* The solution is to make the vanilla launcher optional and only throw once we try to effectively use and can't find it.
|
|
*
|
|
* @internal
|
|
*/
|
|
get launcher() {
|
|
if (!this._launcher) {
|
|
throw playwrightLoader.requireError;
|
|
}
|
|
return this._launcher;
|
|
}
|
|
async launch(...args) {
|
|
if (!this.launcher.launch) {
|
|
throw new Error('Launcher does not support "launch"');
|
|
}
|
|
let [options] = args;
|
|
options = Object.assign({ args: [] }, (options || {})); // Initialize args array
|
|
debug$2('launch', options);
|
|
this.plugins.prepare();
|
|
// Give plugins the chance to modify the options before continuing
|
|
options =
|
|
(await this.plugins.dispatchBlocking('beforeLaunch', options)) || options;
|
|
debug$2('launch with options', options);
|
|
if ('userDataDir' in options) {
|
|
debug$2("A plugin defined userDataDir during .launch, which isn't supported by playwright - ignoring");
|
|
delete options.userDataDir;
|
|
}
|
|
const browser = await this.launcher['launch'](options);
|
|
await this.plugins.dispatchBlocking('onBrowser', browser);
|
|
await this._bindBrowserEvents(browser);
|
|
await this.plugins.dispatchBlocking('afterLaunch', browser);
|
|
return browser;
|
|
}
|
|
async launchPersistentContext(...args) {
|
|
if (!this.launcher.launchPersistentContext) {
|
|
throw new Error('Launcher does not support "launchPersistentContext"');
|
|
}
|
|
let [userDataDir, options] = args;
|
|
options = Object.assign({ args: [] }, (options || {})); // Initialize args array
|
|
debug$2('launchPersistentContext', options);
|
|
this.plugins.prepare();
|
|
// Give plugins the chance to modify the options before continuing
|
|
options =
|
|
(await this.plugins.dispatchBlocking('beforeLaunch', options)) || options;
|
|
const context = await this.launcher['launchPersistentContext'](userDataDir, options);
|
|
await this.plugins.dispatchBlocking('afterLaunch', context);
|
|
this._bindBrowserContextEvents(context);
|
|
return context;
|
|
}
|
|
async connect(wsEndpointOrOptions, wsOptions = {}) {
|
|
if (!this.launcher.connect) {
|
|
throw new Error('Launcher does not support "connect"');
|
|
}
|
|
this.plugins.prepare();
|
|
// Playwright currently supports two function signatures for .connect
|
|
let options = {};
|
|
let wsEndpointAsString = false;
|
|
if (typeof wsEndpointOrOptions === 'object') {
|
|
options = Object.assign(Object.assign({}, wsEndpointOrOptions), wsOptions);
|
|
}
|
|
else {
|
|
wsEndpointAsString = true;
|
|
options = Object.assign({ wsEndpoint: wsEndpointOrOptions }, wsOptions);
|
|
}
|
|
debug$2('connect', options);
|
|
// Give plugins the chance to modify the options before launch/connect
|
|
options =
|
|
(await this.plugins.dispatchBlocking('beforeConnect', options)) || options;
|
|
// Follow call signature of end user
|
|
const args = [];
|
|
const wsEndpoint = options.wsEndpoint;
|
|
if (wsEndpointAsString) {
|
|
delete options.wsEndpoint;
|
|
args.push(wsEndpoint, options);
|
|
}
|
|
else {
|
|
args.push(options);
|
|
}
|
|
const browser = (await this.launcher['connect'](...args));
|
|
await this.plugins.dispatchBlocking('onBrowser', browser);
|
|
await this._bindBrowserEvents(browser);
|
|
await this.plugins.dispatchBlocking('afterConnect', browser);
|
|
return browser;
|
|
}
|
|
async connectOverCDP(wsEndpointOrOptions, wsOptions = {}) {
|
|
if (!this.launcher.connectOverCDP) {
|
|
throw new Error(`Launcher does not implement 'connectOverCDP'`);
|
|
}
|
|
this.plugins.prepare();
|
|
// Playwright currently supports two function signatures for .connectOverCDP
|
|
let options = {};
|
|
let wsEndpointAsString = false;
|
|
if (typeof wsEndpointOrOptions === 'object') {
|
|
options = Object.assign(Object.assign({}, wsEndpointOrOptions), wsOptions);
|
|
}
|
|
else {
|
|
wsEndpointAsString = true;
|
|
options = Object.assign({ endpointURL: wsEndpointOrOptions }, wsOptions);
|
|
}
|
|
debug$2('connectOverCDP'), options;
|
|
// Give plugins the chance to modify the options before launch/connect
|
|
options =
|
|
(await this.plugins.dispatchBlocking('beforeConnect', options)) || options;
|
|
// Follow call signature of end user
|
|
const args = [];
|
|
const endpointURL = options.endpointURL;
|
|
if (wsEndpointAsString) {
|
|
delete options.endpointURL;
|
|
args.push(endpointURL, options);
|
|
}
|
|
else {
|
|
args.push(options);
|
|
}
|
|
const browser = (await this.launcher['connectOverCDP'](...args));
|
|
await this.plugins.dispatchBlocking('onBrowser', browser);
|
|
await this._bindBrowserEvents(browser);
|
|
await this.plugins.dispatchBlocking('afterConnect', browser);
|
|
return browser;
|
|
}
|
|
async _bindBrowserContextEvents(context, contextOptions) {
|
|
debug$2('_bindBrowserContextEvents');
|
|
this.plugins.dispatch('onContextCreated', context, contextOptions);
|
|
// Make sure things like `addInitScript` show an effect on the very first page as well
|
|
context.newPage = ((originalMethod, ctx) => {
|
|
return async () => {
|
|
const page = await originalMethod.call(ctx);
|
|
await page.goto('about:blank');
|
|
return page;
|
|
};
|
|
})(context.newPage, context);
|
|
context.on('close', () => {
|
|
// When using `launchPersistentContext` context closing is the same as browser closing
|
|
if (!context.browser()) {
|
|
this.plugins.dispatch('onDisconnected');
|
|
}
|
|
});
|
|
context.on('page', page => {
|
|
this.plugins.dispatch('onPageCreated', page);
|
|
page.on('close', () => {
|
|
this.plugins.dispatch('onPageClose', page);
|
|
});
|
|
});
|
|
}
|
|
async _bindBrowserEvents(browser) {
|
|
debug$2('_bindPlaywrightBrowserEvents');
|
|
browser.on('disconnected', () => {
|
|
this.plugins.dispatch('onDisconnected', browser);
|
|
});
|
|
// Note: `browser.newPage` will implicitly call `browser.newContext` as well
|
|
browser.newContext = ((originalMethod, ctx) => {
|
|
return async (options = {}) => {
|
|
const contextOptions = (await this.plugins.dispatchBlocking('beforeContext', options, browser)) || options;
|
|
const context = await originalMethod.call(ctx, contextOptions);
|
|
this._bindBrowserContextEvents(context, contextOptions);
|
|
return context;
|
|
};
|
|
})(browser.newContext, browser);
|
|
}
|
|
}
|
|
/**
|
|
* PlaywrightExtra class with additional launcher methods.
|
|
*
|
|
* Augments the class with an instance proxy to pass on methods that are not augmented to the original target.
|
|
*
|
|
*/
|
|
const PlaywrightExtra = new Proxy(PlaywrightExtraClass, {
|
|
construct(classTarget, args) {
|
|
debug$2(`create instance of ${classTarget.name}`);
|
|
const result = Reflect.construct(classTarget, args);
|
|
return new Proxy(result, {
|
|
get(target, prop) {
|
|
if (prop in target) {
|
|
return Reflect.get(target, prop);
|
|
}
|
|
debug$2('proxying property to original launcher: ', prop);
|
|
return Reflect.get(target.launcher, prop);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Augment the provided Playwright browser launcher with plugin functionality.
|
|
*
|
|
* Using `addExtra` will always create a fresh PlaywrightExtra instance.
|
|
*
|
|
* @example
|
|
* import playwright from 'playwright'
|
|
* import { addExtra } from 'playwright-extra'
|
|
*
|
|
* const chromium = addExtra(playwright.chromium)
|
|
* chromium.use(plugin)
|
|
*
|
|
* @param launcher - Playwright (or compatible) browser launcher
|
|
*/
|
|
const addExtra = (launcher) => new PlaywrightExtra(launcher);
|
|
/**
|
|
* This object can be used to launch or connect to Chromium with plugin functionality.
|
|
*
|
|
* This default export will behave exactly the same as the regular playwright
|
|
* (just with extra plugin functionality) and can be used as a drop-in replacement.
|
|
*
|
|
* Behind the scenes it will try to require either the `playwright-core`
|
|
* or `playwright` module from the installed dependencies.
|
|
*
|
|
* @note
|
|
* Due to Node.js import caching this will result in a single
|
|
* PlaywrightExtra instance, even when used in different files. If you need multiple
|
|
* instances with different plugins please use `addExtra`.
|
|
*
|
|
* @example
|
|
* // javascript import
|
|
* const { chromium } = require('playwright-extra')
|
|
*
|
|
* // typescript/es6 module import
|
|
* import { chromium } from 'playwright-extra'
|
|
*
|
|
* // Add plugins
|
|
* chromium.use(...)
|
|
*/
|
|
const chromium = addExtra((playwrightLoader.loadModule() || {}).chromium);
|
|
/**
|
|
* This object can be used to launch or connect to Firefox with plugin functionality
|
|
* @note This export will always return the same instance, if you wish to use multiple instances with different plugins use `addExtra`
|
|
*/
|
|
const firefox = addExtra((playwrightLoader.loadModule() || {}).firefox);
|
|
/**
|
|
* This object can be used to launch or connect to Webkit with plugin functionality
|
|
* @note This export will always return the same instance, if you wish to use multiple instances with different plugins use `addExtra`
|
|
*/
|
|
const webkit = addExtra((playwrightLoader.loadModule() || {}).webkit);
|
|
// Other playwright module exports we simply re-export with lazy loading
|
|
const _android = playwrightLoader.lazyloadExportOrDie('_android');
|
|
const _electron = playwrightLoader.lazyloadExportOrDie('_electron');
|
|
const request = playwrightLoader.lazyloadExportOrDie('request');
|
|
const selectors = playwrightLoader.lazyloadExportOrDie('selectors');
|
|
const devices = playwrightLoader.lazyloadExportOrDie('devices');
|
|
const errors = playwrightLoader.lazyloadExportOrDie('errors');
|
|
/** Playwright with plugin functionality */
|
|
const moduleExports = {
|
|
// custom exports
|
|
PlaywrightExtra,
|
|
PlaywrightExtraClass,
|
|
PluginList,
|
|
addExtra,
|
|
chromium,
|
|
firefox,
|
|
webkit,
|
|
// vanilla exports
|
|
_android,
|
|
_electron,
|
|
request,
|
|
selectors,
|
|
devices,
|
|
errors
|
|
};
|
|
|
|
exports.PlaywrightExtra = PlaywrightExtra;
|
|
exports.PlaywrightExtraClass = PlaywrightExtraClass;
|
|
exports.PluginList = PluginList;
|
|
exports._android = _android;
|
|
exports._electron = _electron;
|
|
exports.addExtra = addExtra;
|
|
exports.chromium = chromium;
|
|
exports.default = moduleExports;
|
|
exports.devices = devices;
|
|
exports.errors = errors;
|
|
exports.firefox = firefox;
|
|
exports.request = request;
|
|
exports.selectors = selectors;
|
|
exports.webkit = webkit;
|
|
|
|
|
|
module.exports = exports.default || {}
|
|
Object.entries(exports).forEach(([key, value]) => { module.exports[key] = value })
|
|
//# sourceMappingURL=index.cjs.js.map
|