chore: get rid of <Browser> templating (#209)

This commit is contained in:
Dmitry Gozman 2019-12-11 07:18:43 -08:00 committed by Pavel Feldman
parent 57acdfd860
commit 58336d3eb9
11 changed files with 102 additions and 89 deletions

View File

@ -18,28 +18,41 @@
import { assert } from './helper';
import { Page } from './page';
import * as network from './network';
import * as childProcess from 'child_process';
export interface BrowserDelegate<Browser> {
contextPages(): Promise<Page<Browser>[]>;
createPageInContext(): Promise<Page<Browser>>;
export interface BrowserInterface {
browserContexts(): BrowserContext[];
close(): Promise<void>;
createIncognitoBrowserContext(): Promise<BrowserContext>;
defaultBrowserContext(): BrowserContext;
newPage(): Promise<Page>;
pages(): Promise<Page[]>;
process(): childProcess.ChildProcess | null;
version(): Promise<string>;
userAgent(): Promise<string>;
}
export interface BrowserDelegate {
contextPages(): Promise<Page[]>;
createPageInContext(): Promise<Page>;
closeContext(): Promise<void>;
getContextCookies(): Promise<network.NetworkCookie[]>;
clearContextCookies(): Promise<void>;
setContextCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
}
export class BrowserContext<Browser> {
private readonly _delegate: BrowserDelegate<Browser>;
private readonly _browser: Browser;
export class BrowserContext {
private readonly _delegate: BrowserDelegate;
private readonly _browser: BrowserInterface;
private readonly _isIncognito: boolean;
constructor(delegate: BrowserDelegate<Browser>, browser: Browser, isIncognito: boolean) {
constructor(delegate: BrowserDelegate, browser: BrowserInterface, isIncognito: boolean) {
this._delegate = delegate;
this._browser = browser;
this._isIncognito = isIncognito;
}
async pages(): Promise<Page<Browser>[]> {
async pages(): Promise<Page[]> {
return this._delegate.contextPages();
}
@ -47,11 +60,11 @@ export class BrowserContext<Browser> {
return this._isIncognito;
}
async newPage(): Promise<Page<Browser>> {
async newPage(): Promise<Page> {
return this._delegate.createPageInContext();
}
browser(): Browser {
browser(): BrowserInterface {
return this._browser;
}

View File

@ -19,7 +19,7 @@ import * as childProcess from 'child_process';
import { EventEmitter } from 'events';
import { Events } from './events';
import { assert, helper } from '../helper';
import { BrowserContext } from '../browserContext';
import { BrowserContext, BrowserInterface } from '../browserContext';
import { Connection, ConnectionEvents, CDPSession } from './Connection';
import { Page } from '../page';
import { Target } from './Target';
@ -30,15 +30,15 @@ import { FrameManager } from './FrameManager';
import * as network from '../network';
import { Permissions } from './features/permissions';
export class Browser extends EventEmitter {
export class Browser extends EventEmitter implements BrowserInterface {
private _ignoreHTTPSErrors: boolean;
private _defaultViewport: types.Viewport;
private _process: childProcess.ChildProcess;
_connection: Connection;
_client: CDPSession;
private _closeCallback: () => Promise<void>;
private _defaultContext: BrowserContext<Browser>;
private _contexts = new Map<string, BrowserContext<Browser>>();
private _defaultContext: BrowserContext;
private _contexts = new Map<string, BrowserContext>();
_targets = new Map<string, Target>();
readonly chromium: Chromium;
@ -80,16 +80,16 @@ export class Browser extends EventEmitter {
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
}
_createBrowserContext(contextId: string | null): BrowserContext<Browser> {
_createBrowserContext(contextId: string | null): BrowserContext {
const isIncognito = !!contextId;
const context = new BrowserContext({
contextPages: async (): Promise<Page<Browser>[]> => {
contextPages: async (): Promise<Page[]> => {
const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
},
createPageInContext: async (): Promise<Page<Browser>> => {
createPageInContext: async (): Promise<Page> => {
const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined });
const target = this._targets.get(targetId);
assert(await target._initializedPromise, 'Failed to create target for page');
@ -127,18 +127,18 @@ export class Browser extends EventEmitter {
return this._process;
}
async createIncognitoBrowserContext(): Promise<BrowserContext<Browser>> {
async createIncognitoBrowserContext(): Promise<BrowserContext> {
const {browserContextId} = await this._client.send('Target.createBrowserContext');
const context = this._createBrowserContext(browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): BrowserContext<Browser>[] {
browserContexts(): BrowserContext[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext(): BrowserContext<Browser> {
defaultBrowserContext(): BrowserContext {
return this._defaultContext;
}
@ -174,11 +174,11 @@ export class Browser extends EventEmitter {
this.chromium.emit(Events.Chromium.TargetChanged, target);
}
async newPage(): Promise<Page<Browser>> {
async newPage(): Promise<Page> {
return this._defaultContext.newPage();
}
async _closePage(page: Page<Browser>) {
async _closePage(page: Page) {
await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId });
}
@ -186,7 +186,7 @@ export class Browser extends EventEmitter {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
}
async _activatePage(page: Page<Browser>) {
async _activatePage(page: Page) {
await (page._delegate as FrameManager)._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId});
}
@ -216,7 +216,7 @@ export class Browser extends EventEmitter {
}
}
async pages(): Promise<Page<Browser>[]> {
async pages(): Promise<Page[]> {
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);

View File

@ -62,7 +62,7 @@ type FrameData = {
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
_client: CDPSession;
private _page: Page<Browser>;
private _page: Page;
private _networkManager: NetworkManager;
private _frames = new Map<string, frames.Frame>();
private _contextIdToContext = new Map<number, js.ExecutionContext>();
@ -72,7 +72,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
rawKeyboard: RawKeyboardImpl;
screenshotterDelegate: CRScreenshotDelegate;
constructor(client: CDPSession, browserContext: BrowserContext<Browser>, ignoreHTTPSErrors: boolean) {
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
super();
this._client = client;
this.rawKeyboard = new RawKeyboardImpl(client);
@ -251,7 +251,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._handleFrameTree(child);
}
page(): Page<Browser> {
page(): Page {
return this._page;
}
@ -540,7 +540,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
if (runBeforeUnload)
await this._client.send('Page.close');
else
await this._page.browser()._closePage(this._page);
await (this._page.browser() as Browser)._closePage(this._page);
}
}

View File

@ -30,25 +30,25 @@ const targetSymbol = Symbol('target');
export class Target {
private _targetInfo: Protocol.Target.TargetInfo;
private _browserContext: BrowserContext<Browser>;
private _browserContext: BrowserContext;
_targetId: string;
private _sessionFactory: () => Promise<CDPSession>;
private _ignoreHTTPSErrors: boolean;
private _defaultViewport: types.Viewport;
private _pagePromise: Promise<Page<Browser>> | null = null;
private _page: Page<Browser> | null = null;
private _pagePromise: Promise<Page> | null = null;
private _page: Page | null = null;
private _workerPromise: Promise<Worker> | null = null;
_initializedPromise: Promise<boolean>;
_initializedCallback: (value?: unknown) => void;
_isInitialized: boolean;
static fromPage(page: Page<Browser>): Target {
static fromPage(page: Page): Target {
return (page as any)[targetSymbol];
}
constructor(
targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext<Browser>,
browserContext: BrowserContext,
sessionFactory: () => Promise<CDPSession>,
ignoreHTTPSErrors: boolean,
defaultViewport: types.Viewport | null) {
@ -81,7 +81,7 @@ export class Target {
this._page._didClose();
}
async page(): Promise<Page<Browser> | null> {
async page(): Promise<Page | null> {
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
this._pagePromise = this._sessionFactory().then(async client => {
const frameManager = new FrameManager(client, this._browserContext, this._ignoreHTTPSErrors);
@ -128,10 +128,10 @@ export class Target {
}
browser(): Browser {
return this._browserContext.browser();
return this._browserContext.browser() as Browser;
}
browserContext(): BrowserContext<Browser> {
browserContext(): BrowserContext {
return this._browserContext;
}

View File

@ -48,7 +48,7 @@ export class Chromium extends EventEmitter {
return target._worker();
}
async startTracing(page: Page<Browser> | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
assert(!this._recording, 'Cannot start recording trace while already recording trace.');
this._tracingClient = page ? (page._delegate as FrameManager)._client : this._client;
@ -87,12 +87,12 @@ export class Chromium extends EventEmitter {
return contentPromise;
}
targets(context?: BrowserContext<Browser>): Target[] {
targets(context?: BrowserContext): Target[] {
const targets = this._browser._allTargets();
return context ? targets.filter(t => t.browserContext() === context) : targets;
}
pageTarget(page: Page<Browser>): Target {
pageTarget(page: Page): Target {
return Target.fromPage(page);
}

View File

@ -25,16 +25,16 @@ import { Page } from '../page';
import * as types from '../types';
import { FrameManager } from './FrameManager';
import * as network from '../network';
import { BrowserContext } from '../browserContext';
import { BrowserContext, BrowserInterface } from '../browserContext';
export class Browser extends EventEmitter {
export class Browser extends EventEmitter implements BrowserInterface {
private _connection: Connection;
_defaultViewport: types.Viewport;
private _process: import('child_process').ChildProcess;
private _closeCallback: () => void;
_targets: Map<string, Target>;
private _defaultContext: BrowserContext<Browser>;
private _contexts: Map<string, BrowserContext<Browser>>;
private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
private _eventListeners: RegisteredListener[];
static async create(connection: Connection, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) {
@ -75,14 +75,14 @@ export class Browser extends EventEmitter {
return !this._connection._closed;
}
async createIncognitoBrowserContext(): Promise<BrowserContext<Browser>> {
async createIncognitoBrowserContext(): Promise<BrowserContext> {
const {browserContextId} = await this._connection.send('Target.createBrowserContext');
const context = this._createBrowserContext(browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): Array<BrowserContext<Browser>> {
browserContexts(): Array<BrowserContext> {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
@ -128,7 +128,7 @@ export class Browser extends EventEmitter {
}
}
newPage(): Promise<Page<Browser>> {
newPage(): Promise<Page> {
return this._defaultContext.newPage();
}
@ -170,16 +170,16 @@ export class Browser extends EventEmitter {
this._closeCallback();
}
_createBrowserContext(browserContextId: string | null): BrowserContext<Browser> {
_createBrowserContext(browserContextId: string | null): BrowserContext {
const isIncognito = !!browserContextId;
const context = new BrowserContext({
contextPages: async (): Promise<Page<Browser>[]> => {
contextPages: async (): Promise<Page[]> => {
const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
},
createPageInContext: async (): Promise<Page<Browser>> => {
createPageInContext: async (): Promise<Page> => {
const {targetId} = await this._connection.send('Target.newPage', {
browserContextId: browserContextId || undefined
});
@ -215,17 +215,17 @@ export class Browser extends EventEmitter {
}
export class Target {
_pagePromise?: Promise<Page<Browser>>;
private _page: Page<Browser> | null = null;
_pagePromise?: Promise<Page>;
private _page: Page | null = null;
private _browser: Browser;
_context: BrowserContext<Browser>;
_context: BrowserContext;
private _connection: Connection;
private _targetId: string;
private _type: 'page' | 'browser';
_url: string;
private _openerId: string;
constructor(connection: any, browser: Browser, context: BrowserContext<Browser>, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) {
constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) {
this._browser = browser;
this._context = context;
this._connection = connection;
@ -252,11 +252,11 @@ export class Target {
return this._url;
}
browserContext(): BrowserContext<Browser> {
browserContext(): BrowserContext {
return this._context;
}
page(): Promise<Page<Browser>> {
page(): Promise<Page> {
if (this._type === 'page' && !this._pagePromise) {
this._pagePromise = new Promise(async f => {
const session = await this._connection.createSession(this._targetId);

View File

@ -56,14 +56,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
readonly rawKeyboard: RawKeyboardImpl;
readonly screenshotterDelegate: FFScreenshotDelegate;
readonly _session: JugglerSession;
readonly _page: Page<Browser>;
readonly _page: Page;
private readonly _networkManager: NetworkManager;
private _mainFrame: frames.Frame;
private readonly _frames: Map<string, frames.Frame>;
private readonly _contextIdToContext: Map<string, js.ExecutionContext>;
private _eventListeners: RegisteredListener[];
constructor(session: JugglerSession, browserContext: BrowserContext<Browser>) {
constructor(session: JugglerSession, browserContext: BrowserContext) {
super();
this._session = session;
this.rawKeyboard = new RawKeyboardImpl(session);

View File

@ -26,7 +26,7 @@ import { Screenshotter, ScreenshotterDelegate } from './screenshotter';
import { TimeoutSettings } from './TimeoutSettings';
import * as types from './types';
import { Events } from './events';
import { BrowserContext } from './browserContext';
import { BrowserContext, BrowserInterface } from './browserContext';
import { ConsoleMessage, ConsoleMessageLocation } from './console';
export interface PageDelegate {
@ -69,14 +69,14 @@ export type FileChooser = {
multiple: boolean
};
export class Page<Browser> extends EventEmitter {
export class Page extends EventEmitter {
private _closed = false;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
private _disconnected = false;
private _disconnectedCallback: (e: Error) => void;
readonly _disconnectedPromise: Promise<Error>;
private _browserContext: BrowserContext<Browser>;
private _browserContext: BrowserContext;
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
readonly _timeoutSettings: TimeoutSettings;
@ -87,7 +87,7 @@ export class Page<Browser> extends EventEmitter {
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
readonly _lifecycleWatchers = new Set<frames.LifecycleWatcher>();
constructor(delegate: PageDelegate, browserContext: BrowserContext<Browser>) {
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
super();
this._delegate = delegate;
this._closedPromise = new Promise(f => this._closedCallback = f);
@ -150,11 +150,11 @@ export class Page<Browser> extends EventEmitter {
});
}
browser(): Browser {
browser(): BrowserInterface {
return this._browserContext.browser();
}
browserContext(): BrowserContext<Browser> {
browserContext(): BrowserContext {
return this._browserContext;
}

View File

@ -25,15 +25,15 @@ import { Target } from './Target';
import { Protocol } from './protocol';
import * as types from '../types';
import { Events } from '../events';
import { BrowserContext } from '../browserContext';
import { BrowserContext, BrowserInterface } from '../browserContext';
export class Browser extends EventEmitter {
export class Browser extends EventEmitter implements BrowserInterface {
readonly _defaultViewport: types.Viewport;
private readonly _process: childProcess.ChildProcess;
readonly _connection: Connection;
private _closeCallback: () => Promise<void>;
private readonly _defaultContext: BrowserContext<Browser>;
private _contexts = new Map<string, BrowserContext<Browser>>();
private readonly _defaultContext: BrowserContext;
private _contexts = new Map<string, BrowserContext>();
_targets = new Map<string, Target>();
private _eventListeners: RegisteredListener[];
private _privateEvents = new EventEmitter();
@ -86,25 +86,25 @@ export class Browser extends EventEmitter {
return this._process;
}
async createIncognitoBrowserContext(): Promise<BrowserContext<Browser>> {
async createIncognitoBrowserContext(): Promise<BrowserContext> {
const {browserContextId} = await this._connection.send('Browser.createContext');
const context = this._createBrowserContext(browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): BrowserContext<Browser>[] {
browserContexts(): BrowserContext[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext(): BrowserContext<Browser> {
defaultBrowserContext(): BrowserContext {
return this._defaultContext;
}
async _disposeContext(browserContextId: string | null) {
}
async newPage(): Promise<Page<Browser>> {
async newPage(): Promise<Page> {
return this._defaultContext.newPage();
}
@ -136,7 +136,7 @@ export class Browser extends EventEmitter {
}
}
async pages(): Promise<Page<Browser>[]> {
async pages(): Promise<Page[]> {
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);
@ -181,13 +181,13 @@ export class Browser extends EventEmitter {
target._didClose();
}
_closePage(page: Page<Browser>) {
_closePage(page: Page) {
this._connection.send('Target.close', {
targetId: Target.fromPage(page)._targetId
}).catch(debugError);
}
async _activatePage(page: Page<Browser>): Promise<void> {
async _activatePage(page: Page): Promise<void> {
await this._connection.send('Target.activate', { targetId: Target.fromPage(page)._targetId });
}
@ -210,16 +210,16 @@ export class Browser extends EventEmitter {
await this._closeCallback.call(null);
}
_createBrowserContext(browserContextId: string | undefined): BrowserContext<Browser> {
_createBrowserContext(browserContextId: string | undefined): BrowserContext {
const isIncognito = !!browserContextId;
const context = new BrowserContext({
contextPages: async (): Promise<Page<Browser>[]> => {
contextPages: async (): Promise<Page[]> => {
const targets = this.targets().filter(target => target._browserContext === context && target._type === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
},
createPageInContext: async (): Promise<Page<Browser>> => {
createPageInContext: async (): Promise<Page> => {
const { targetId } = await this._connection.send('Browser.createPage', { browserContextId });
const target = this._targets.get(targetId);
return await target.page();

View File

@ -59,7 +59,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
readonly rawKeyboard: RawKeyboardImpl;
readonly screenshotterDelegate: WKScreenshotDelegate;
_session: TargetSession;
readonly _page: Page<Browser>;
readonly _page: Page;
private readonly _networkManager: NetworkManager;
private readonly _frames: Map<string, frames.Frame>;
private readonly _contextIdToContext: Map<number, js.ExecutionContext>;
@ -68,7 +68,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
private _mainFrame: frames.Frame;
private readonly _bootstrapScripts: string[] = [];
constructor(browserContext: BrowserContext<Browser>) {
constructor(browserContext: BrowserContext) {
super();
this.rawKeyboard = new RawKeyboardImpl();
this.rawMouse = new RawMouseImpl();
@ -197,7 +197,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._handleFrameTree(child);
}
page(): Page<Browser> {
page(): Page {
return this._page;
}
@ -524,6 +524,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
async closePage(runBeforeUnload: boolean): Promise<void> {
if (runBeforeUnload)
throw new Error('Not implemented');
this._page.browser()._closePage(this._page);
(this._page.browser() as Browser)._closePage(this._page);
}
}

View File

@ -25,18 +25,18 @@ import { FrameManager } from './FrameManager';
const targetSymbol = Symbol('target');
export class Target {
readonly _browserContext: BrowserContext<Browser>;
readonly _browserContext: BrowserContext;
readonly _targetId: string;
readonly _type: 'page' | 'service-worker' | 'worker';
private readonly _session: TargetSession;
private _pagePromise: Promise<Page<Browser>> | null = null;
_page: Page<Browser> | null = null;
private _pagePromise: Promise<Page> | null = null;
_page: Page | null = null;
static fromPage(page: Page<Browser>): Target {
static fromPage(page: Page): Target {
return (page as any)[targetSymbol];
}
constructor(session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext<Browser>) {
constructor(session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) {
const {targetId, type} = targetInfo;
this._session = session;
this._browserContext = browserContext;
@ -84,9 +84,9 @@ export class Target {
(this._page._delegate as FrameManager).setSession(this._session);
}
async page(): Promise<Page<Browser>> {
async page(): Promise<Page> {
if (this._type === 'page' && !this._pagePromise) {
const browser = this._browserContext.browser();
const browser = this._browserContext.browser() as Browser;
// Reference local page variable as _page may be
// cleared on swap.
const frameManager = new FrameManager(this._browserContext);