Skip to content

Commit

Permalink
fix: incognito mode (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak authored Aug 30, 2021
1 parent 8baab35 commit 91ab32a
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 99 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
64 changes: 25 additions & 39 deletions src/playwright/browser.ts
Original file line number Diff line number Diff line change
@@ -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<BrowserEvents> {
browserContext: BrowserContext;
export class Browser extends EventEmitter implements PlaywrightBrowser {
private _browserContext: BrowserContext;

private _version: string;

Expand All @@ -24,59 +20,49 @@ export class Browser extends TypedEmitter<BrowserEvents> {
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<void> {
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<never> {
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<BrowserContext['newPage']>): ReturnType<BrowserContext['newPage']> {
return this.browserContext.newPage(...args);
return this._browserContext.newPage(...args);
}

/**
* Returns the browser version.
*/
version(): string {
return this._version;
async newContext(): Promise<never> {
throw new Error('Function `newContext()` is not available in incognito mode');
}

async newBrowserCDPSession(): Promise<never> {
throw new Error('Function `newBrowserCDPSession()` is not available in incognito mode');
}

async startTracing(): Promise<never> {
throw new Error('Function `startTracing()` is not available in incognito mode');
}

async stopTracing(): Promise<never> {
throw new Error('Function `stopTracing()` is not available in incognito mode');
}
}
8 changes: 2 additions & 6 deletions src/playwright/playwright-controller.ts
Original file line number Diff line number Diff line change
@@ -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<BrowserType, Parameters<BrowserType['launch']>[0], PlaywrightPluginBrowsers> {
export class PlaywrightController extends BrowserController<BrowserType, Parameters<BrowserType['launch']>[0], Browser> {
override supportsPageOptions = true;

protected async _newPage(pageOptions?: Parameters<Browser['newPage']>[0]): Promise<Page> {
const page = await this.browser.newPage(pageOptions);

page.once('close', async () => {
page.once('close', () => {
this.activePages--;
});

Expand Down
32 changes: 12 additions & 20 deletions src/playwright/playwright-plugin.ts
Original file line number Diff line number Diff line change
@@ -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<BrowserType, Parameters<BrowserType['launch']>[0], PlaywrightPluginBrowsers> {
private _browserVersion!: string;
export class PlaywrightPlugin extends BrowserPlugin<BrowserType, Parameters<BrowserType['launch']>[0], PlaywrightBrowser> {
private _browserVersion?: string;

protected _launch(launchContext: LaunchContext<BrowserType> & { useIncognitoPages: true }): Promise<PlaywrightBrowser>;

protected _launch(launchContext: LaunchContext<BrowserType> & { useIncognitoPages: false }): Promise<ApifyBrowser>;

protected async _launch(launchContext: LaunchContext<BrowserType>): Promise<ApifyBrowser | PlaywrightBrowser> {
protected async _launch(launchContext: LaunchContext<BrowserType>): Promise<PlaywrightBrowser> {
const {
launchOptions,
anonymizedProxyUrl,
useIncognitoPages,
userDataDir,
} = launchContext;
let browser: ApifyBrowser | PlaywrightBrowser;
let browser: PlaywrightBrowser;

if (useIncognitoPages) {
browser = await this.library.launch(launchOptions);
Expand All @@ -34,14 +25,15 @@ export class PlaywrightPlugin extends BrowserPlugin<BrowserType, Parameters<Brow

if (!this._browserVersion) {
// Launches unused browser just to get the browser version.

const inactiveBrowser = await this.library.launch(launchOptions);
this._browserVersion = inactiveBrowser.version();

inactiveBrowser.close().catch(noop);
inactiveBrowser.close().catch((error) => {
log.exception(error, 'Failed to close browser.');
});
}

browser = new ApifyBrowser({ browserContext, version: this._browserVersion });
browser = new PlaywrightBrowserWithPersistentContext({ browserContext, version: this._browserVersion });
}

if (anonymizedProxyUrl) {
Expand All @@ -53,7 +45,7 @@ export class PlaywrightPlugin extends BrowserPlugin<BrowserType, Parameters<Brow
return browser;
}

protected _createController(): BrowserController<BrowserType, Parameters<BrowserType['launch']>[0], PlaywrightPluginBrowsers> {
protected _createController(): BrowserController<BrowserType, Parameters<BrowserType['launch']>[0], PlaywrightBrowser> {
return new PlaywrightController(this);
}

Expand Down
25 changes: 1 addition & 24 deletions src/puppeteer/puppeteer-controller.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Puppeteer> {
protected async _newPage(): Promise<Puppeteer.Page> {
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;
Expand Down
38 changes: 34 additions & 4 deletions src/puppeteer/puppeteer-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,57 @@ 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<typeof Puppeteer> {
protected async _launch(launchContext: LaunchContext<typeof Puppeteer>): Promise<Puppeteer.Browser> {
const {
launchOptions,
anonymizedProxyUrl,
userDataDir,
useIncognitoPages,
} = launchContext;

const finalLaunchOptions = {
...launchOptions,
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', () => {
Expand Down
12 changes: 7 additions & 5 deletions test/browser-plugins/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<ReturnType<typeof puppeteer['launch']>> | undefined;
let browser: playwright.Browser | UnwrapPromise<ReturnType<typeof puppeteer['launch']>> | undefined;

beforeEach(() => {
plugin = new Plugin(library as never);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down

0 comments on commit 91ab32a

Please sign in to comment.