From 91ab32a20cee4bec6cbb517fbfbfaf61f6c33d93 Mon Sep 17 00:00:00 2001 From: Szymon Marczak <36894700+szmarczak@users.noreply.github.com> Date: Mon, 30 Aug 2021 17:28:59 +0200 Subject: [PATCH] fix: incognito mode (#43) --- package.json | 2 +- src/playwright/browser.ts | 64 ++++++++++--------------- src/playwright/playwright-controller.ts | 8 +--- src/playwright/playwright-plugin.ts | 32 +++++-------- src/puppeteer/puppeteer-controller.ts | 25 +--------- src/puppeteer/puppeteer-plugin.ts | 38 +++++++++++++-- test/browser-plugins/plugins.test.ts | 12 +++-- 7 files changed, 82 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index 05e2a18..1a76d65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browser-pool", - "version": "2.0.0", + "version": "2.0.1", "description": "Rotate multiple browsers using popular automation libraries such as Playwright or Puppeteer.", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/playwright/browser.ts b/src/playwright/browser.ts index b2b0d28..bbdb798 100644 --- a/src/playwright/browser.ts +++ b/src/playwright/browser.ts @@ -1,20 +1,16 @@ -import { BrowserContext } from 'playwright'; -import { TypedEmitter } from 'tiny-typed-emitter'; +import { EventEmitter } from 'events'; +import { BrowserContext, Browser as PlaywrightBrowser } from 'playwright'; export interface BrowserOptions { browserContext: BrowserContext; version: string; } -export interface BrowserEvents { - disconnected: () => void; -} - /** * Browser wrapper created to have consistent API with persistent and non-persistent contexts. */ -export class Browser extends TypedEmitter { - browserContext: BrowserContext; +export class Browser extends EventEmitter implements PlaywrightBrowser { + private _browserContext: BrowserContext; private _version: string; @@ -24,59 +20,49 @@ export class Browser extends TypedEmitter { super(); const { browserContext, version } = options; - this.browserContext = browserContext; + this._browserContext = browserContext; this._version = version; - this.browserContext.on('close', () => { + this._browserContext.once('close', () => { this._isConnected = false; this.emit('disconnected'); }); } - /** - * Closes browser and all pages/contexts assigned to it. - */ async close(): Promise { - await this.browserContext.close(); + await this._browserContext.close(); } - /** - * Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts. - */ contexts(): BrowserContext[] { - return [this.browserContext]; + return [this._browserContext]; } - /** - * Indicates that the browser is connected. - * @returns {boolean} - */ isConnected(): boolean { return this._isConnected; } - /** - * Method added for API consistency. - * Should not be used. - * Throws an error if called. - */ - async newContext(): Promise { - throw new Error('Could not call `newContext()` on browser, when `useIncognitoPages` is set to `false`'); + version(): string { + return this._version; } - /** - * Creates a new page in a new browser context. Closing this page will close the context as well. - * @param args - New Page options. See https://playwright.dev/docs/next/api/class-browser#browsernewpageoptions. - */ async newPage(...args: Parameters): ReturnType { - return this.browserContext.newPage(...args); + return this._browserContext.newPage(...args); } - /** - * Returns the browser version. - */ - version(): string { - return this._version; + async newContext(): Promise { + throw new Error('Function `newContext()` is not available in incognito mode'); + } + + async newBrowserCDPSession(): Promise { + throw new Error('Function `newBrowserCDPSession()` is not available in incognito mode'); + } + + async startTracing(): Promise { + throw new Error('Function `startTracing()` is not available in incognito mode'); + } + + async stopTracing(): Promise { + throw new Error('Function `stopTracing()` is not available in incognito mode'); } } diff --git a/src/playwright/playwright-controller.ts b/src/playwright/playwright-controller.ts index ec000d3..750aaa6 100644 --- a/src/playwright/playwright-controller.ts +++ b/src/playwright/playwright-controller.ts @@ -1,17 +1,13 @@ import { Browser, BrowserType, Page } from 'playwright'; import { BrowserController, Cookie } from '../abstract-classes/browser-controller'; -import { PlaywrightPluginBrowsers } from './playwright-plugin'; -/** - * Playwright - */ -export class PlaywrightController extends BrowserController[0], PlaywrightPluginBrowsers> { +export class PlaywrightController extends BrowserController[0], Browser> { override supportsPageOptions = true; protected async _newPage(pageOptions?: Parameters[0]): Promise { const page = await this.browser.newPage(pageOptions); - page.once('close', async () => { + page.once('close', () => { this.activePages--; }); diff --git a/src/playwright/playwright-plugin.ts b/src/playwright/playwright-plugin.ts index bb3fa20..71bceb3 100644 --- a/src/playwright/playwright-plugin.ts +++ b/src/playwright/playwright-plugin.ts @@ -1,31 +1,22 @@ import { Browser as PlaywrightBrowser, BrowserType } from 'playwright'; +import { Browser as PlaywrightBrowserWithPersistentContext } from './browser'; +import { PlaywrightController } from './playwright-controller'; import { BrowserController } from '../abstract-classes/browser-controller'; import { BrowserPlugin } from '../abstract-classes/browser-plugin'; import { LaunchContext } from '../launch-context'; -import { noop } from '../utils'; -import { Browser as ApifyBrowser } from './browser'; -import { PlaywrightController } from './playwright-controller'; - -export type PlaywrightPluginBrowsers = ApifyBrowser | PlaywrightBrowser; +import { log } from '../logger'; -/** - * Playwright - */ -export class PlaywrightPlugin extends BrowserPlugin[0], PlaywrightPluginBrowsers> { - private _browserVersion!: string; +export class PlaywrightPlugin extends BrowserPlugin[0], PlaywrightBrowser> { + private _browserVersion?: string; - protected _launch(launchContext: LaunchContext & { useIncognitoPages: true }): Promise; - - protected _launch(launchContext: LaunchContext & { useIncognitoPages: false }): Promise; - - protected async _launch(launchContext: LaunchContext): Promise { + protected async _launch(launchContext: LaunchContext): Promise { const { launchOptions, anonymizedProxyUrl, useIncognitoPages, userDataDir, } = launchContext; - let browser: ApifyBrowser | PlaywrightBrowser; + let browser: PlaywrightBrowser; if (useIncognitoPages) { browser = await this.library.launch(launchOptions); @@ -34,14 +25,15 @@ export class PlaywrightPlugin extends BrowserPlugin { + log.exception(error, 'Failed to close browser.'); + }); } - browser = new ApifyBrowser({ browserContext, version: this._browserVersion }); + browser = new PlaywrightBrowserWithPersistentContext({ browserContext, version: this._browserVersion }); } if (anonymizedProxyUrl) { @@ -53,7 +45,7 @@ export class PlaywrightPlugin extends BrowserPlugin[0], PlaywrightPluginBrowsers> { + protected _createController(): BrowserController[0], PlaywrightBrowser> { return new PlaywrightController(this); } diff --git a/src/puppeteer/puppeteer-controller.ts b/src/puppeteer/puppeteer-controller.ts index 37e5c8f..1984d54 100644 --- a/src/puppeteer/puppeteer-controller.ts +++ b/src/puppeteer/puppeteer-controller.ts @@ -1,37 +1,14 @@ import * as Puppeteer from 'puppeteer'; import { BrowserController, Cookie } from '../abstract-classes/browser-controller'; -import { log } from '../logger'; -import { noop } from '../utils'; const PROCESS_KILL_TIMEOUT_MILLIS = 5000; -/** - * Puppeteer - */ export class PuppeteerController extends BrowserController { protected async _newPage(): Promise { - const { useIncognitoPages } = this.launchContext; - let page: Puppeteer.Page; - let context: Puppeteer.BrowserContext; - - if (useIncognitoPages) { - context = await this.browser.createIncognitoBrowserContext(); - page = await context.newPage(); - } else { - page = await this.browser.newPage(); - } + const page = await this.browser.newPage(); page.once('close', () => { this.activePages--; - - if (useIncognitoPages) { - context.close().catch(noop); - } - }); - - page.once('error', (error) => { - log.exception(error, 'Page crashed.'); - page.close().catch(noop); }); return page; diff --git a/src/puppeteer/puppeteer-plugin.ts b/src/puppeteer/puppeteer-plugin.ts index 7056c27..2dc4529 100644 --- a/src/puppeteer/puppeteer-plugin.ts +++ b/src/puppeteer/puppeteer-plugin.ts @@ -2,19 +2,19 @@ import * as Puppeteer from 'puppeteer'; import { BrowserController } from '../abstract-classes/browser-controller'; import { BrowserPlugin } from '../abstract-classes/browser-plugin'; import { LaunchContext } from '../launch-context'; +import { log } from '../logger'; +import { noop } from '../utils'; import { PuppeteerController } from './puppeteer-controller'; const PROXY_SERVER_ARG = '--proxy-server='; -/** - * Puppeteer - */ export class PuppeteerPlugin extends BrowserPlugin { protected async _launch(launchContext: LaunchContext): Promise { const { launchOptions, anonymizedProxyUrl, userDataDir, + useIncognitoPages, } = launchContext; const finalLaunchOptions = { @@ -22,7 +22,37 @@ export class PuppeteerPlugin extends BrowserPlugin { userDataDir: launchOptions?.userDataDir ?? userDataDir, }; - const browser = await this.library.launch(finalLaunchOptions); + let browser = await this.library.launch(finalLaunchOptions); + + browser.on('targetcreated', async (target: Puppeteer.Target) => { + try { + const page = await target.page(); + + if (page) { + page.on('error', (error) => { + log.exception(error, 'Page crashed.'); + page.close().catch(noop); + }); + } + } catch (error: any) { + log.exception(error, 'Failed to retrieve page from target.'); + } + }); + + if (useIncognitoPages) { + browser = new Proxy(browser, { + get: (target, property: keyof typeof browser) => { + if (property === 'newPage') { + return (async (...args) => { + const incognitoContext = await browser.createIncognitoBrowserContext(); + return incognitoContext.newPage(...args); + }) as typeof browser.newPage; + } + + return target[property]; + }, + }); + } if (anonymizedProxyUrl) { browser.once('disconnected', () => { diff --git a/test/browser-plugins/plugins.test.ts b/test/browser-plugins/plugins.test.ts index e67eb93..686ba83 100644 --- a/test/browser-plugins/plugins.test.ts +++ b/test/browser-plugins/plugins.test.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import { PuppeteerPlugin } from '../../src/puppeteer/puppeteer-plugin'; import { PuppeteerController } from '../../src/puppeteer/puppeteer-controller'; -import { PlaywrightPlugin, PlaywrightPluginBrowsers } from '../../src/playwright/playwright-plugin'; +import { PlaywrightPlugin } from '../../src/playwright/playwright-plugin'; import { PlaywrightController } from '../../src/playwright/playwright-controller'; import { Browser } from '../../src/playwright/browser'; @@ -23,7 +23,7 @@ const runPluginTest = < let plugin = new Plugin(library as never); describe(`${plugin.constructor.name} - ${'name' in library ? library.name!() : ''} general`, () => { - let browser: PlaywrightPluginBrowsers | UnwrapPromise> | undefined; + let browser: playwright.Browser | UnwrapPromise> | undefined; beforeEach(() => { plugin = new Plugin(library as never); @@ -220,7 +220,7 @@ describe('Plugins', () => { runPluginTest(PuppeteerPlugin, PuppeteerController, puppeteer); describe('Playwright specifics', () => { - let browser: PlaywrightPluginBrowsers; + let browser: playwright.Browser; afterEach(async () => { await browser.close(); @@ -364,7 +364,9 @@ describe('Plugins', () => { browser = await plugin.launch(launchContext); const contexts = browser.contexts(); expect(contexts).toHaveLength(1); - expect(contexts[0]).toEqual((browser as Browser).browserContext); + // Cast to any to access private property + // eslint-disable-next-line no-underscore-dangle + expect(contexts[0]).toEqual((browser as any)._browserContext); }); test('should return correct connected status', async () => { @@ -386,7 +388,7 @@ describe('Plugins', () => { expect(browser.newContext()) .rejects - .toThrow('Could not call `newContext()` on browser, when `useIncognitoPages` is set to `false`'); + .toThrow('Function `newContext()` is not available in incognito mode'); }); test('should have same public interface as playwright browserType', async () => {