chore: pull common functionality into the BrowserTypeBase (#2312)

This commit is contained in:
Pavel Feldman 2020-05-20 16:30:04 -07:00 committed by GitHub
parent aa0d844c76
commit f9b437a49e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 118 additions and 184 deletions

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { BrowserContext, BrowserContextOptions } from './browserContext';
import { BrowserContext, BrowserContextOptions, BrowserContextBase } from './browserContext';
import { Page } from './page';
import { EventEmitter } from 'events';
import { Download } from './download';
@ -44,6 +44,7 @@ export interface Browser extends EventEmitter {
export abstract class BrowserBase extends EventEmitter implements Browser, InnerLogger {
readonly _options: BrowserOptions;
private _downloads = new Map<string, Download>();
_defaultContext: BrowserContextBase | null = null;
constructor(options: BrowserOptions) {
super();
@ -102,4 +103,3 @@ export abstract class BrowserBase extends EventEmitter implements Browser, Inner
}
}
export type LaunchType = 'local' | 'server' | 'persistent';

View file

@ -36,7 +36,6 @@ export class CRBrowser extends BrowserBase {
readonly _connection: CRConnection;
_session: CRSession;
private _clientRootSessionPromise: Promise<CRSession> | null = null;
readonly _defaultContext: CRBrowserContext | null = null;
readonly _contexts = new Map<string, CRBrowserContext>();
_crPages = new Map<string, CRPage>();
_backgroundPages = new Map<string, CRPage>();
@ -123,7 +122,7 @@ export class CRBrowser extends BrowserBase {
if (!context) {
// TODO: auto attach only to pages from our contexts.
// assert(this._defaultContext);
context = this._defaultContext;
context = this._defaultContext as CRBrowserContext;
}
if (targetInfo.type === 'other' && targetInfo.url.startsWith('devtools://devtools') && this._devtools) {

View file

@ -31,7 +31,6 @@ import { Protocol } from './protocol';
export class FFBrowser extends BrowserBase {
_connection: FFConnection;
readonly _ffPages: Map<string, FFPage>;
readonly _defaultContext: FFBrowserContext | null = null;
readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[];
@ -91,7 +90,7 @@ export class FFBrowser extends BrowserBase {
_onAttachedToTarget(payload: Protocol.Browser.attachedToTargetPayload) {
const {targetId, browserContextId, openerId, type} = payload.targetInfo;
assert(type === 'page');
const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext;
const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext as FFBrowserContext;
assert(context, `Unknown context id:${browserContextId}, _defaultContext: ${this._defaultContext}`);
const session = this._connection.createSession(payload.sessionId, type);
const opener = openerId ? this._ffPages.get(openerId)! : null;

View file

@ -19,7 +19,7 @@ import { EventEmitter } from 'events';
import { helper } from '../helper';
import { RootLogger } from '../logger';
import { TimeoutSettings } from '../timeoutSettings';
import { LaunchOptionsBase } from './browserType';
import { LaunchOptions } from './browserType';
export class WebSocketWrapper {
readonly wsEndpoint: string;
@ -54,11 +54,11 @@ export class BrowserServer extends EventEmitter {
private _process: ChildProcess | undefined;
private _gracefullyClose: (() => Promise<void>) | undefined;
private _webSocketWrapper: WebSocketWrapper | null = null;
private _launchOptions: LaunchOptionsBase;
readonly _launchOptions: LaunchOptions;
readonly _logger: RootLogger;
readonly _launchDeadline: number;
constructor(options: LaunchOptionsBase) {
constructor(options: LaunchOptions) {
super();
this._launchOptions = options;
this._logger = new RootLogger(options.logger);

View file

@ -17,7 +17,10 @@
import { BrowserContext } from '../browserContext';
import { BrowserServer } from './browserServer';
import * as browserPaths from '../install/browserPaths';
import { Logger } from '../logger';
import { Logger, RootLogger } from '../logger';
import { ConnectionTransport, WebSocketTransport } from '../transport';
import { BrowserBase, BrowserOptions, Browser } from '../browser';
import { assert } from '../helper';
export type BrowserArgOptions = {
headless?: boolean,
@ -25,7 +28,7 @@ export type BrowserArgOptions = {
devtools?: boolean,
};
export type LaunchOptionsBase = BrowserArgOptions & {
type LaunchOptionsBase = BrowserArgOptions & {
executablePath?: string,
ignoreDefaultArgs?: boolean | string[],
handleSIGINT?: boolean,
@ -46,9 +49,11 @@ export type ConnectOptions = {
slowMo?: number,
logger?: Logger,
};
export type LaunchType = 'local' | 'server' | 'persistent';
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
export type LaunchServerOptions = LaunchOptionsBase & { port?: number };
export interface BrowserType<Browser> {
export interface BrowserType {
executablePath(): string;
name(): string;
launch(options?: LaunchOptions): Promise<Browser>;
@ -57,7 +62,7 @@ export interface BrowserType<Browser> {
connect(options: ConnectOptions): Promise<Browser>;
}
export abstract class AbstractBrowserType<Browser> implements BrowserType<Browser> {
export abstract class BrowserTypeBase implements BrowserType {
private _name: string;
private _executablePath: string | undefined;
readonly _browserPath: string;
@ -79,8 +84,44 @@ export abstract class AbstractBrowserType<Browser> implements BrowserType<Browse
return this._name;
}
abstract launch(options?: LaunchOptions): Promise<Browser>;
abstract launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
abstract launchPersistentContext(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext>;
abstract connect(options: ConnectOptions): Promise<Browser>;
async launch(options: LaunchOptions = {}): Promise<Browser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const browserServer = new BrowserServer(options);
const { transport, downloadsPath } = await this._launchServer(options, 'local', browserServer);
return await browserServer._initializeOrClose(async () => {
return this._connectToServer(browserServer, false, transport!, downloadsPath);
});
}
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const browserServer = new BrowserServer(options);
const { transport, downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
return await browserServer._initializeOrClose(async () => {
const browser = await this._connectToServer(browserServer, true, transport!, downloadsPath);
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
}
async connect(options: ConnectOptions): Promise<Browser> {
const logger = new RootLogger(options.logger);
return await WebSocketTransport.connect(options.wsEndpoint, async transport => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
return this._connectToTransport(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
}, logger);
}
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string }>;
abstract _connectToServer(browserServer: BrowserServer, persistent: boolean, transport: ConnectionTransport, downloadsPath: string): Promise<BrowserBase>;
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
}

View file

@ -25,17 +25,16 @@ import * as ws from 'ws';
import { launchProcess } from './processLauncher';
import { kBrowserCloseMessageId } from '../chromium/crConnection';
import { PipeTransport } from './pipeTransport';
import { LaunchOptions, BrowserArgOptions, ConnectOptions, LaunchServerOptions, AbstractBrowserType, processBrowserArgOptions } from './browserType';
import { LaunchType } from '../browser';
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
import { BrowserServer, WebSocketWrapper } from './browserServer';
import { Events } from '../events';
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
import { BrowserContext } from '../browserContext';
import { InnerLogger, logError, RootLogger } from '../logger';
import { ConnectionTransport, ProtocolRequest } from '../transport';
import { InnerLogger, logError } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { CRDevTools } from '../chromium/crDevTools';
import { BrowserBase, BrowserOptions } from '../browser';
export class Chromium extends AbstractBrowserType<CRBrowser> {
export class Chromium extends BrowserTypeBase {
private _devtools: CRDevTools | undefined;
constructor(packagePath: string, browser: BrowserDescriptor) {
@ -48,52 +47,28 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
return new CRDevTools(path.join(this._browserPath, 'devtools-preferences.json'));
}
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const browserServer = new BrowserServer(options);
const { transport, downloadsPath } = await this._launchServer(options, 'local', browserServer);
return await browserServer._initializeOrClose(async () => {
let devtools = this._devtools;
if ((options as any).__testHookForDevTools) {
devtools = this._createDevTools();
await (options as any).__testHookForDevTools(devtools);
}
return await CRBrowser.connect(transport!, {
slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless,
logger: browserServer._logger,
downloadsPath,
ownedServer: browserServer,
}, devtools);
});
async _connectToServer(browserServer: BrowserServer, persistent: boolean, transport: ConnectionTransport, downloadsPath: string): Promise<BrowserBase> {
const options = browserServer._launchOptions;
let devtools = this._devtools;
if ((options as any).__testHookForDevTools) {
devtools = this._createDevTools();
await (options as any).__testHookForDevTools(devtools);
}
return await CRBrowser.connect(transport, {
slowMo: options.slowMo,
persistent,
headful: !processBrowserArgOptions(options).headless,
logger: browserServer._logger,
downloadsPath,
ownedServer: browserServer,
}, devtools);
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<CRBrowser> {
return CRBrowser.connect(transport, options);
}
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const browserServer = new BrowserServer(options);
const { transport, downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
return await browserServer._initializeOrClose(async () => {
const browser = await CRBrowser.connect(transport!, {
slowMo: options.slowMo,
persistent: true,
logger: browserServer._logger,
downloadsPath,
headful: !processBrowserArgOptions(options).headless,
ownedServer: browserServer
}, this._devtools);
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
}
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string }> {
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string }> {
const {
ignoreDefaultArgs = false,
args = [],
@ -163,15 +138,6 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
return { transport, downloadsPath };
}
async connect(options: ConnectOptions): Promise<CRBrowser> {
const logger = new RootLogger(options.logger);
return await WebSocketTransport.connect(options.wsEndpoint, async transport => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
return CRBrowser.connect(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
}, logger);
}
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string): string[] {
const { devtools, headless } = processBrowserArgOptions(options);
const { args = [] } = options;

View file

@ -70,7 +70,7 @@ export class ElectronApplication extends ExtendedEventEmitter {
constructor(logger: InnerLogger, browser: CRBrowser, nodeConnection: CRConnection) {
super();
this._logger = logger;
this._browserContext = browser._defaultContext!;
this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(Events.BrowserContext.Close, () => this.emit(ElectronEvents.ElectronApplication.Close));
this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event));
this._nodeConnection = nodeConnection;

View file

@ -20,73 +20,46 @@ import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import * as ws from 'ws';
import { LaunchType } from '../browser';
import { BrowserContext } from '../browserContext';
import { TimeoutError } from '../errors';
import { Events } from '../events';
import { FFBrowser } from '../firefox/ffBrowser';
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import { helper, assert, debugAssert } from '../helper';
import { BrowserServer, WebSocketWrapper } from './browserServer';
import { BrowserArgOptions, LaunchOptions, LaunchServerOptions, ConnectOptions, AbstractBrowserType, processBrowserArgOptions } from './browserType';
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
import { launchProcess, waitForLine } from './processLauncher';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
import { RootLogger, InnerLogger, logError } from '../logger';
import { InnerLogger, logError } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { BrowserBase, BrowserOptions } from '../browser';
const mkdtempAsync = util.promisify(fs.mkdtemp);
export class Firefox extends AbstractBrowserType<FFBrowser> {
export class Firefox extends BrowserTypeBase {
constructor(packagePath: string, browser: BrowserDescriptor) {
super(packagePath, browser);
}
async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const browserServer = new BrowserServer(options);
const { downloadsPath } = await this._launchServer(options, 'local', browserServer);
return await browserServer._initializeOrClose(async () => {
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, {
slowMo: options.slowMo,
logger: browserServer._logger,
downloadsPath,
headful: !processBrowserArgOptions(options).headless,
ownedServer: browserServer,
});
}, browserServer._logger);
return browser;
});
_connectToServer(browserServer: BrowserServer, persistent: boolean, transport: ConnectionTransport, downloadsPath: string): Promise<BrowserBase> {
const options = browserServer._launchOptions;
// TODO: connect to the underlying socket.
return WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, {
slowMo: options.slowMo,
logger: browserServer._logger,
persistent,
downloadsPath,
headful: !processBrowserArgOptions(options).headless,
ownedServer: browserServer,
});
}, browserServer._logger);
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
return FFBrowser.connect(transport, options);
}
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const browserServer = new BrowserServer(options);
const { downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
return await browserServer._initializeOrClose(async () => {
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, {
slowMo: options.slowMo,
logger: browserServer._logger,
persistent: true,
downloadsPath,
ownedServer: browserServer,
headful: !processBrowserArgOptions(options).headless,
});
}, browserServer._logger);
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
}
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ downloadsPath: string }> {
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ downloadsPath: string }> {
const {
ignoreDefaultArgs = false,
args = [],
@ -143,7 +116,7 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
// We try to gracefully close to prevent crash reporting and core dumps.
const transport = await WebSocketTransport.connect(browserWSEndpoint!, async transport => transport);
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
await transport.send(message);
transport.send(message);
},
onkill: (exitCode, signal) => {
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
@ -162,15 +135,6 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
return { downloadsPath };
}
async connect(options: ConnectOptions): Promise<FFBrowser> {
const logger = new RootLogger(options.logger);
return await WebSocketTransport.connect(options.wsEndpoint, async transport => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
return FFBrowser.connect(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
}, logger);
}
private _defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
const { devtools, headless } = processBrowserArgOptions(options);
const { args = [] } = options;

View file

@ -24,62 +24,37 @@ import * as os from 'os';
import * as util from 'util';
import { helper, assert } from '../helper';
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
import { LaunchOptions, BrowserArgOptions, LaunchServerOptions, ConnectOptions, AbstractBrowserType, processBrowserArgOptions } from './browserType';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
import { BrowserArgOptions, LaunchServerOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
import * as ws from 'ws';
import { LaunchType } from '../browser';
import { BrowserServer, WebSocketWrapper } from './browserServer';
import { Events } from '../events';
import { BrowserContext } from '../browserContext';
import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { BrowserBase, BrowserOptions } from '../browser';
export class WebKit extends AbstractBrowserType<WKBrowser> {
export class WebKit extends BrowserTypeBase {
constructor(packagePath: string, browser: BrowserDescriptor) {
super(packagePath, browser);
}
async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const browserServer = new BrowserServer(options);
const { transport, downloadsPath } = await this._launchServer(options, 'local', browserServer);
return await browserServer._initializeOrClose(async () => {
return await WKBrowser.connect(transport!, {
slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless,
logger: browserServer._logger,
downloadsPath,
ownedServer: browserServer
});
_connectToServer(browserServer: BrowserServer, persistent: boolean, transport: ConnectionTransport, downloadsPath: string): Promise<BrowserBase> {
const options = browserServer._launchOptions;
return WKBrowser.connect(transport, {
slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless,
logger: browserServer._logger,
persistent,
downloadsPath,
ownedServer: browserServer
});
}
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
return WKBrowser.connect(transport, options);
}
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const browserServer = new BrowserServer(options);
const { transport, downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
return await browserServer._initializeOrClose(async () => {
const browser = await WKBrowser.connect(transport!, {
slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless,
logger: browserServer._logger,
persistent: true,
downloadsPath,
ownedServer: browserServer
});
const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext();
return context;
});
}
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string, logger: RootLogger }> {
async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string, logger: RootLogger }> {
const {
ignoreDefaultArgs = false,
args = [],
@ -144,15 +119,6 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
return { transport, downloadsPath, logger };
}
async connect(options: ConnectOptions): Promise<WKBrowser> {
const logger = new RootLogger(options.logger);
return await WebSocketTransport.connect(options.wsEndpoint, async transport => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
return WKBrowser.connect(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
}, logger);
}
_defaultArgs(options: BrowserArgOptions = {}, launchType: LaunchType, userDataDir: string, port: number): string[] {
const { devtools, headless } = processBrowserArgOptions(options);
const { args = [] } = options;

View file

@ -32,7 +32,6 @@ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) Appl
export class WKBrowser extends BrowserBase {
private readonly _connection: WKConnection;
readonly _browserSession: WKSession;
readonly _defaultContext: WKBrowserContext | null = null;
readonly _contexts = new Map<string, WKBrowserContext>();
readonly _wkPages = new Map<string, WKPage>();
private readonly _eventListeners: RegisteredListener[];
@ -132,7 +131,7 @@ export class WKBrowser extends BrowserBase {
context = this._contexts.get(pageProxyInfo.browserContextId) || null;
}
if (!context)
context = this._defaultContext;
context = this._defaultContext as WKBrowserContext;
if (!context)
return;
const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => {