chore: reuse BrowserContext across browsers (#201)

This commit is contained in:
Dmitry Gozman 2019-12-10 15:13:56 -08:00 committed by GitHub
parent 0af3b9dfc8
commit 5ffb710d7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 292 additions and 324 deletions

View File

@ -359,8 +359,8 @@ export const macEditingCommands: {[key: string]: string|string[]} = {
'Alt+Tab': 'insertTabIgnoringFieldEditor:',
'Alt+Enter': 'insertNewlineIgnoringFieldEditor:',
'Alt+Escape': 'complete:',
"Alt+ArrowUp": ['moveBackward:', 'moveToBeginningOfParagraph:'],
"Alt+ArrowDown": ['moveForward:', 'moveToEndOfParagraph:'],
'Alt+ArrowUp': ['moveBackward:', 'moveToBeginningOfParagraph:'],
'Alt+ArrowDown': ['moveForward:', 'moveToEndOfParagraph:'],
'Alt+ArrowLeft': 'moveWordLeft:',
'Alt+ArrowRight': 'moveWordRight:',
'Alt+Delete': 'deleteWordForward:',

74
src/browserContext.ts Normal file
View File

@ -0,0 +1,74 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from './helper';
import { Page } from './page';
import * as network from './network';
export interface BrowserDelegate<Browser> {
contextPages(): Promise<Page<Browser>[]>;
createPageInContext(): Promise<Page<Browser>>;
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;
private readonly _isIncognito: boolean;
constructor(delegate: BrowserDelegate<Browser>, browser: Browser, isIncognito: boolean) {
this._delegate = delegate;
this._browser = browser;
this._isIncognito = isIncognito;
}
async pages(): Promise<Page<Browser>[]> {
return this._delegate.contextPages();
}
isIncognito(): boolean {
return this._isIncognito;
}
async newPage(): Promise<Page<Browser>> {
return this._delegate.createPageInContext();
}
browser(): Browser {
return this._browser;
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
return network.filterCookies(await this._delegate.getContextCookies(), urls);
}
async clearCookies() {
await this._delegate.clearContextCookies();
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._delegate.setContextCookies(network.rewriteCookies(cookies));
}
async close() {
assert(this._isIncognito, 'Non-incognito profiles cannot be closed!');
await this._delegate.closeContext();
}
}

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 } from '../browserContext';
import { Connection, ConnectionEvents, CDPSession } from './Connection';
import { Page } from '../page';
import { Target } from './Target';
@ -27,6 +27,8 @@ import { Protocol } from './protocol';
import { Chromium } from './features/chromium';
import * as types from '../types';
import { FrameManager } from './FrameManager';
import * as network from '../network';
import { Permissions } from './features/permissions';
export class Browser extends EventEmitter {
private _ignoreHTTPSErrors: boolean;
@ -35,8 +37,8 @@ export class Browser extends EventEmitter {
_connection: Connection;
_client: CDPSession;
private _closeCallback: () => Promise<void>;
private _defaultContext: BrowserContext;
private _contexts = new Map<string, BrowserContext>();
private _defaultContext: BrowserContext<Browser>;
private _contexts = new Map<string, BrowserContext<Browser>>();
_targets = new Map<string, Target>();
readonly chromium: Chromium;
@ -68,9 +70,9 @@ export class Browser extends EventEmitter {
this._closeCallback = closeCallback || (() => Promise.resolve());
this.chromium = new Chromium(this);
this._defaultContext = new BrowserContext(this._client, this, null);
this._defaultContext = this._createBrowserContext(null);
for (const contextId of contextIds)
this._contexts.set(contextId, new BrowserContext(this._client, this, contextId));
this._contexts.set(contextId, this._createBrowserContext(contextId));
this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected));
this._client.on('Target.targetCreated', this._targetCreated.bind(this));
@ -78,30 +80,68 @@ export class Browser extends EventEmitter {
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
}
_createBrowserContext(contextId: string | null): BrowserContext<Browser> {
const isIncognito = !!contextId;
const context = new BrowserContext({
contextPages: async (): Promise<Page<Browser>[]> => {
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>> => {
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');
const page = await target.page();
return page;
},
closeContext: async (): Promise<void> => {
await this._client.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
this._contexts.delete(contextId);
},
getContextCookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._client.send('Storage.getCookies', { browserContextId: contextId || undefined });
return cookies.map(c => {
const copy: any = { sameSite: 'None', ...c };
delete copy.size;
return copy as network.NetworkCookie;
});
},
clearContextCookies: async (): Promise<void> => {
await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined });
},
setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined });
},
}, this, isIncognito);
(context as any).permissions = new Permissions(this._client, contextId);
return context;
}
process(): childProcess.ChildProcess | null {
return this._process;
}
async createIncognitoBrowserContext(): Promise<BrowserContext> {
async createIncognitoBrowserContext(): Promise<BrowserContext<Browser>> {
const {browserContextId} = await this._client.send('Target.createBrowserContext');
const context = new BrowserContext(this._client, this, browserContextId);
const context = this._createBrowserContext(browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): BrowserContext[] {
browserContexts(): BrowserContext<Browser>[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext(): BrowserContext {
defaultBrowserContext(): BrowserContext<Browser> {
return this._defaultContext;
}
async _disposeContext(contextId: string | null) {
await this._client.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
this._contexts.delete(contextId);
}
async _targetCreated(event: Protocol.Target.targetCreatedPayload) {
const targetInfo = event.targetInfo;
const {browserContextId} = targetInfo;
@ -134,19 +174,11 @@ export class Browser extends EventEmitter {
this.chromium.emit(Events.Chromium.TargetChanged, target);
}
async newPage(): Promise<Page<Browser, BrowserContext>> {
async newPage(): Promise<Page<Browser>> {
return this._defaultContext.newPage();
}
async _createPageInContext(contextId: string | null): Promise<Page<Browser, BrowserContext>> {
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');
const page = await target.page();
return page;
}
async _closePage(page: Page<Browser, BrowserContext>) {
async _closePage(page: Page<Browser>) {
await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId });
}
@ -154,13 +186,7 @@ export class Browser extends EventEmitter {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
}
async _pages(context: BrowserContext): Promise<Page<Browser, BrowserContext>[]> {
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);
}
async _activatePage(page: Page<Browser, BrowserContext>) {
async _activatePage(page: Page<Browser>) {
await (page._delegate as FrameManager)._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId});
}
@ -190,7 +216,7 @@ export class Browser extends EventEmitter {
}
}
async pages(): Promise<Page<Browser, BrowserContext>[]> {
async pages(): Promise<Page<Browser>[]> {
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);

View File

@ -1,75 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from '../helper';
import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network';
import { Browser } from './Browser';
import { CDPSession } from './Connection';
import { Permissions } from './features/permissions';
import { Page } from '../page';
export class BrowserContext {
readonly permissions: Permissions;
private _browser: Browser;
private _id: string;
constructor(client: CDPSession, browser: Browser, contextId: string | null) {
this._browser = browser;
this._id = contextId;
this.permissions = new Permissions(client, contextId);
}
pages(): Promise<Page<Browser, BrowserContext>[]> {
return this._browser._pages(this);
}
isIncognito(): boolean {
return !!this._id;
}
newPage(): Promise<Page<Browser, BrowserContext>> {
return this._browser._createPageInContext(this._id);
}
browser(): Browser {
return this._browser;
}
async cookies(...urls: string[]): Promise<NetworkCookie[]> {
const { cookies } = await this._browser._client.send('Storage.getCookies', { browserContextId: this._id || undefined });
return filterCookies(cookies.map(c => {
const copy: any = { sameSite: 'None', ...c };
delete copy.size;
return copy as NetworkCookie;
}), urls);
}
async clearCookies() {
await this._browser._client.send('Storage.clearCookies', { browserContextId: this._id || undefined });
}
async setCookies(cookies: SetNetworkCookieParam[]) {
cookies = rewriteCookies(cookies);
await this._browser._client.send('Storage.setCookies', { cookies, browserContextId: this._id || undefined });
}
async close() {
assert(this._id, 'Non-incognito profiles cannot be closed!');
await this._browser._disposeContext(this._id);
}
}

View File

@ -41,7 +41,7 @@ import { Workers } from './features/workers';
import { Overrides } from './features/overrides';
import { Interception } from './features/interception';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import { BrowserContext } from '../browserContext';
import * as types from '../types';
import * as input from '../input';
import { ConsoleMessage } from '../console';
@ -64,7 +64,7 @@ type FrameData = {
export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate {
_client: CDPSession;
private _page: Page<Browser, BrowserContext>;
private _page: Page<Browser>;
private _networkManager: NetworkManager;
private _frames = new Map<string, frames.Frame>();
private _contextIdToContext = new Map<number, js.ExecutionContext>();
@ -74,7 +74,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
rawKeyboard: RawKeyboardImpl;
screenshotterDelegate: CRScreenshotDelegate;
constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) {
constructor(client: CDPSession, browserContext: BrowserContext<Browser>, ignoreHTTPSErrors: boolean) {
super();
this._client = client;
this.rawKeyboard = new RawKeyboardImpl(client);
@ -254,7 +254,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._handleFrameTree(child);
}
page(): Page<Browser, BrowserContext> {
page(): Page<Browser> {
return this._page;
}

View File

@ -17,7 +17,7 @@
import * as types from '../types';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import { BrowserContext } from '../browserContext';
import { CDPSession, CDPSessionEvents } from './Connection';
import { Events } from '../events';
import { Worker } from './features/workers';
@ -30,25 +30,25 @@ const targetSymbol = Symbol('target');
export class Target {
private _targetInfo: Protocol.Target.TargetInfo;
private _browserContext: BrowserContext;
private _browserContext: BrowserContext<Browser>;
_targetId: string;
private _sessionFactory: () => Promise<CDPSession>;
private _ignoreHTTPSErrors: boolean;
private _defaultViewport: types.Viewport;
private _pagePromise: Promise<Page<Browser, BrowserContext>> | null = null;
private _page: Page<Browser, BrowserContext> | null = null;
private _pagePromise: Promise<Page<Browser>> | null = null;
private _page: Page<Browser> | null = null;
private _workerPromise: Promise<Worker> | null = null;
_initializedPromise: Promise<boolean>;
_initializedCallback: (value?: unknown) => void;
_isInitialized: boolean;
static fromPage(page: Page<Browser, BrowserContext>): Target {
static fromPage(page: Page<Browser>): Target {
return (page as any)[targetSymbol];
}
constructor(
targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext,
browserContext: BrowserContext<Browser>,
sessionFactory: () => Promise<CDPSession>,
ignoreHTTPSErrors: boolean,
defaultViewport: types.Viewport | null) {
@ -81,7 +81,7 @@ export class Target {
this._page._didClose();
}
async page(): Promise<Page<Browser, BrowserContext> | null> {
async page(): Promise<Page<Browser> | 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);
@ -131,7 +131,7 @@ export class Target {
return this._browserContext.browser();
}
browserContext(): BrowserContext {
browserContext(): BrowserContext<Browser> {
return this._browserContext;
}

View File

@ -10,7 +10,7 @@ export { Keyboard, Mouse } from '../input';
export { ExecutionContext, JSHandle } from '../javascript';
export { Request, Response } from '../network';
export { Browser } from './Browser';
export { BrowserContext } from './BrowserContext';
export { BrowserContext } from '../browserContext';
export { BrowserFetcher } from '../browserFetcher';
export { CDPSession } from './Connection';
export { Accessibility } from './features/accessibility';

View File

@ -17,7 +17,7 @@
import { EventEmitter } from 'events';
import { assert } from '../../helper';
import { Browser } from '../Browser';
import { BrowserContext } from '../BrowserContext';
import { BrowserContext } from '../../browserContext';
import { CDPSession, Connection } from '../Connection';
import { Page } from '../../page';
import { readProtocolStream } from '../protocolHelper';
@ -48,7 +48,7 @@ export class Chromium extends EventEmitter {
return target._worker();
}
async startTracing(page: Page<Browser, BrowserContext> | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
async startTracing(page: Page<Browser> | 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): Target[] {
targets(context?: BrowserContext<Browser>): Target[] {
const targets = this._browser._allTargets();
return context ? targets.filter(t => t.browserContext() === context) : targets;
}
pageTarget(page: Page<Browser, BrowserContext>): Target {
pageTarget(page: Page<Browser>): Target {
return Target.fromPage(page);
}

View File

@ -16,8 +16,7 @@
*/
import { EventEmitter } from 'events';
import { assert, helper, RegisteredListener } from '../helper';
import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } from '../network';
import { helper, RegisteredListener } from '../helper';
import { Connection, ConnectionEvents, JugglerSessionEvents } from './Connection';
import { Events } from './events';
import { Events as CommonEvents } from '../events';
@ -25,6 +24,8 @@ import { Permissions } from './features/permissions';
import { Page } from '../page';
import * as types from '../types';
import { FrameManager } from './FrameManager';
import * as network from '../network';
import { BrowserContext } from '../browserContext';
export class Browser extends EventEmitter {
private _connection: Connection;
@ -32,8 +33,8 @@ export class Browser extends EventEmitter {
private _process: import('child_process').ChildProcess;
private _closeCallback: () => void;
_targets: Map<string, Target>;
private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
private _defaultContext: BrowserContext<Browser>;
private _contexts: Map<string, BrowserContext<Browser>>;
private _eventListeners: RegisteredListener[];
static async create(connection: Connection, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => void) {
@ -52,10 +53,10 @@ export class Browser extends EventEmitter {
this._targets = new Map();
this._defaultContext = new BrowserContext(this._connection, this, null);
this._defaultContext = this._createBrowserContext(null);
this._contexts = new Map();
for (const browserContextId of browserContextIds)
this._contexts.set(browserContextId, new BrowserContext(this._connection, this, browserContextId));
this._contexts.set(browserContextId, this._createBrowserContext(browserContextId));
this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected));
@ -74,14 +75,14 @@ export class Browser extends EventEmitter {
return !this._connection._closed;
}
async createIncognitoBrowserContext(): Promise<BrowserContext> {
async createIncognitoBrowserContext(): Promise<BrowserContext<Browser>> {
const {browserContextId} = await this._connection.send('Target.createBrowserContext');
const context = new BrowserContext(this._connection, this, browserContextId);
const context = this._createBrowserContext(browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): Array<BrowserContext> {
browserContexts(): Array<BrowserContext<Browser>> {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
@ -89,11 +90,6 @@ export class Browser extends EventEmitter {
return this._defaultContext;
}
async _disposeContext(browserContextId) {
await this._connection.send('Target.removeBrowserContext', {browserContextId});
this._contexts.delete(browserContextId);
}
async userAgent(): Promise<string> {
const info = await this._connection.send('Browser.getInfo');
return info.userAgent;
@ -132,16 +128,8 @@ export class Browser extends EventEmitter {
}
}
newPage(): Promise<Page<Browser, BrowserContext>> {
return this._createPageInContext(this._defaultContext._browserContextId);
}
async _createPageInContext(browserContextId: string | null): Promise<Page<Browser, BrowserContext>> {
const {targetId} = await this._connection.send('Target.newPage', {
browserContextId: browserContextId || undefined
});
const target = this._targets.get(targetId);
return await target.page();
newPage(): Promise<Page<Browser>> {
return this._defaultContext.newPage();
}
async pages() {
@ -153,12 +141,6 @@ export class Browser extends EventEmitter {
return Array.from(this._targets.values());
}
async _pages(context: BrowserContext): Promise<Page<Browser, BrowserContext>[]> {
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);
}
async _onTargetCreated({targetId, url, browserContextId, openerId, type}) {
const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext;
const target = new Target(this._connection, this, context, targetId, type, url, openerId);
@ -187,20 +169,63 @@ export class Browser extends EventEmitter {
helper.removeEventListeners(this._eventListeners);
this._closeCallback();
}
_createBrowserContext(browserContextId: string | null): BrowserContext<Browser> {
const isIncognito = !!browserContextId;
const context = new BrowserContext({
contextPages: async (): Promise<Page<Browser>[]> => {
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>> => {
const {targetId} = await this._connection.send('Target.newPage', {
browserContextId: browserContextId || undefined
});
const target = this._targets.get(targetId);
return await target.page();
},
closeContext: async (): Promise<void> => {
await this._connection.send('Target.removeBrowserContext', { browserContextId });
this._contexts.delete(browserContextId);
},
getContextCookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._connection.send('Browser.getCookies', { browserContextId: browserContextId || undefined });
return cookies.map(c => {
const copy: any = { ... c };
delete copy.size;
return copy as network.NetworkCookie;
});
},
clearContextCookies: async (): Promise<void> => {
await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined });
},
setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies });
},
}, this, isIncognito);
(context as any).permissions = new Permissions(this._connection, browserContextId);
return context;
}
}
export class Target {
_pagePromise?: Promise<Page<Browser, BrowserContext>>;
private _page: Page<Browser, BrowserContext> | null = null;
_pagePromise?: Promise<Page<Browser>>;
private _page: Page<Browser> | null = null;
private _browser: Browser;
_context: BrowserContext;
_context: BrowserContext<Browser>;
private _connection: Connection;
private _targetId: string;
private _type: 'page' | 'browser';
_url: string;
private _openerId: string;
constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) {
constructor(connection: any, browser: Browser, context: BrowserContext<Browser>, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) {
this._browser = browser;
this._context = context;
this._connection = connection;
@ -227,11 +252,11 @@ export class Target {
return this._url;
}
browserContext(): BrowserContext {
browserContext(): BrowserContext<Browser> {
return this._context;
}
page(): Promise<Page<Browser, BrowserContext>> {
page(): Promise<Page<Browser>> {
if (this._type === 'page' && !this._pagePromise) {
this._pagePromise = new Promise(async f => {
const session = await this._connection.createSession(this._targetId);
@ -252,64 +277,3 @@ export class Target {
return this._browser;
}
}
export class BrowserContext {
_connection: Connection;
_browser: Browser;
_browserContextId: string;
readonly permissions: Permissions;
constructor(connection: Connection, browser: Browser, browserContextId: string | null) {
this._connection = connection;
this._browser = browser;
this._browserContextId = browserContextId;
this.permissions = new Permissions(connection, browserContextId);
}
pages(): Promise<Page<Browser, BrowserContext>[]> {
return this._browser._pages(this);
}
isIncognito(): boolean {
return !!this._browserContextId;
}
newPage() {
return this._browser._createPageInContext(this._browserContextId);
}
browser(): Browser {
return this._browser;
}
async cookies(...urls: string[]): Promise<NetworkCookie[]> {
const { cookies } = await this._connection.send('Browser.getCookies', {
browserContextId: this._browserContextId || undefined
});
return filterCookies(cookies, urls).map(c => {
const copy: any = { ... c };
delete copy.size;
return copy as NetworkCookie;
});
}
async clearCookies() {
await this._connection.send('Browser.clearCookies', {
browserContextId: this._browserContextId || undefined,
});
}
async setCookies(cookies: SetNetworkCookieParam[]) {
cookies = rewriteCookies(cookies);
await this._connection.send('Browser.setCookies', {
browserContextId: this._browserContextId || undefined,
cookies
});
}
async close() {
assert(this._browserContextId, 'Non-incognito contexts cannot be closed!');
await this._browser._disposeContext(this._browserContextId);
}
}

View File

@ -33,7 +33,8 @@ import { Protocol } from './protocol';
import * as input from '../input';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { FFScreenshotDelegate } from './Screenshotter';
import { Browser, BrowserContext } from './Browser';
import { Browser } from './Browser';
import { BrowserContext } from '../browserContext';
import { Interception } from './features/interception';
import { Accessibility } from './features/accessibility';
import * as network from '../network';
@ -58,14 +59,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
readonly rawKeyboard: RawKeyboardImpl;
readonly screenshotterDelegate: FFScreenshotDelegate;
readonly _session: JugglerSession;
readonly _page: Page<Browser, BrowserContext>;
readonly _page: Page<Browser>;
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) {
constructor(session: JugglerSession, browserContext: BrowserContext<Browser>) {
super();
this._session = session;
this.rawKeyboard = new RawKeyboardImpl(session);

View File

@ -3,7 +3,8 @@
export { TimeoutError } from '../Errors';
export { Keyboard, Mouse } from '../input';
export { Browser, BrowserContext } from './Browser';
export { Browser } from './Browser';
export { BrowserContext } from '../browserContext';
export { BrowserFetcher } from '../browserFetcher';
export { Dialog } from '../dialog';
export { ExecutionContext, JSHandle } from '../javascript';

View File

@ -27,6 +27,7 @@ import { Screenshotter, ScreenshotterDelegate } from './screenshotter';
import { TimeoutSettings } from './TimeoutSettings';
import * as types from './types';
import { Events } from './events';
import { BrowserContext } from './browserContext';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@ -52,10 +53,6 @@ export interface PageDelegate {
setCacheEnabled(enabled: boolean): Promise<void>;
}
interface BrowserContextInterface<Browser> {
browser(): Browser;
}
type PageState = {
viewport: types.Viewport | null;
userAgent: string | null;
@ -72,14 +69,14 @@ export type FileChooser = {
multiple: boolean
};
export class Page<Browser, BrowserContext extends BrowserContextInterface<Browser>> extends EventEmitter {
export class Page<Browser> 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;
private _browserContext: BrowserContext<Browser>;
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
readonly _timeoutSettings: TimeoutSettings;
@ -89,7 +86,7 @@ export class Page<Browser, BrowserContext extends BrowserContextInterface<Browse
readonly _screenshotter: Screenshotter;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
constructor(delegate: PageDelegate, browserContext: BrowserContext<Browser>) {
super();
this._delegate = delegate;
this._closedPromise = new Promise(f => this._closedCallback = f);
@ -156,7 +153,7 @@ export class Page<Browser, BrowserContext extends BrowserContextInterface<Browse
return this._browserContext.browser();
}
browserContext(): BrowserContext {
browserContext(): BrowserContext<Browser> {
return this._browserContext;
}

View File

@ -17,22 +17,23 @@
import * as childProcess from 'child_process';
import { EventEmitter } from 'events';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import { filterCookies, NetworkCookie, rewriteCookies, SetNetworkCookieParam } from '../network';
import { helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network';
import { Connection, ConnectionEvents, TargetSession } from './Connection';
import { Page } from '../page';
import { Target } from './Target';
import { Protocol } from './protocol';
import * as types from '../types';
import { Events } from '../events';
import { BrowserContext } from '../browserContext';
export class Browser extends EventEmitter {
readonly _defaultViewport: types.Viewport;
private readonly _process: childProcess.ChildProcess;
readonly _connection: Connection;
private _closeCallback: () => Promise<void>;
private readonly _defaultContext: BrowserContext;
private _contexts = new Map<string, BrowserContext>();
private readonly _defaultContext: BrowserContext<Browser>;
private _contexts = new Map<string, BrowserContext<Browser>>();
_targets = new Map<string, Target>();
private _eventListeners: RegisteredListener[];
private _privateEvents = new EventEmitter();
@ -51,7 +52,7 @@ export class Browser extends EventEmitter {
/** @type {!Map<string, !Target>} */
this._targets = new Map();
this._defaultContext = new BrowserContext(this);
this._defaultContext = this._createBrowserContext(undefined);
/** @type {!Map<string, !BrowserContext>} */
this._contexts = new Map();
@ -85,34 +86,26 @@ export class Browser extends EventEmitter {
return this._process;
}
async createIncognitoBrowserContext(): Promise<BrowserContext> {
async createIncognitoBrowserContext(): Promise<BrowserContext<Browser>> {
const {browserContextId} = await this._connection.send('Browser.createContext');
const context = new BrowserContext(this, browserContextId);
const context = this._createBrowserContext(browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
browserContexts(): BrowserContext[] {
browserContexts(): BrowserContext<Browser>[] {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultBrowserContext(): BrowserContext {
defaultBrowserContext(): BrowserContext<Browser> {
return this._defaultContext;
}
async _disposeContext(browserContextId: string | null) {
await this._connection.send('Browser.deleteContext', {browserContextId});
this._contexts.delete(browserContextId);
}
async newPage(): Promise<Page<Browser, BrowserContext>> {
return this._createPageInContext(this._defaultContext._id);
}
async _createPageInContext(browserContextId?: string): Promise<Page<Browser, BrowserContext>> {
const { targetId } = await this._connection.send('Browser.createPage', { browserContextId });
const target = this._targets.get(targetId);
return await target.page();
async newPage(): Promise<Page<Browser>> {
return this._defaultContext.newPage();
}
targets(): Target[] {
@ -143,7 +136,7 @@ export class Browser extends EventEmitter {
}
}
async pages(): Promise<Page<Browser, BrowserContext>[]> {
async pages(): Promise<Page<Browser>[]> {
const contextPages = await Promise.all(this.browserContexts().map(context => context.pages()));
// Flatten array.
return contextPages.reduce((acc, x) => acc.concat(x), []);
@ -188,19 +181,13 @@ export class Browser extends EventEmitter {
target._didClose();
}
_closePage(page: Page<Browser, BrowserContext>) {
_closePage(page: Page<Browser>) {
this._connection.send('Target.close', {
targetId: Target.fromPage(page)._targetId
}).catch(debugError);
}
async _pages(context: BrowserContext): Promise<Page<Browser, BrowserContext>[]> {
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);
}
async _activatePage(page: Page<Browser, BrowserContext>): Promise<void> {
async _activatePage(page: Page<Browser>): Promise<void> {
await this._connection.send('Target.activate', { targetId: Target.fromPage(page)._targetId });
}
@ -222,54 +209,45 @@ export class Browser extends EventEmitter {
helper.removeEventListeners(this._eventListeners);
await this._closeCallback.call(null);
}
}
export class BrowserContext {
private _browser: Browser;
_id: string;
_createBrowserContext(browserContextId: string | undefined): BrowserContext<Browser> {
const isIncognito = !!browserContextId;
const context = new BrowserContext({
contextPages: async (): Promise<Page<Browser>[]> => {
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);
},
constructor(browser: Browser, contextId?: string) {
this._browser = browser;
this._id = contextId;
}
createPageInContext: async (): Promise<Page<Browser>> => {
const { targetId } = await this._connection.send('Browser.createPage', { browserContextId });
const target = this._targets.get(targetId);
return await target.page();
},
pages(): Promise<Page<Browser, BrowserContext>[]> {
return this._browser._pages(this);
}
closeContext: async (): Promise<void> => {
await this._connection.send('Browser.deleteContext', { browserContextId });
this._contexts.delete(browserContextId);
},
isIncognito(): boolean {
return !!this._id;
}
getContextCookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._connection.send('Browser.getAllCookies', { browserContextId });
return cookies.map((c: network.NetworkCookie) => ({
...c,
expires: c.expires === 0 ? -1 : c.expires
}));
},
newPage(): Promise<Page<Browser, BrowserContext>> {
return this._browser._createPageInContext(this._id);
}
clearContextCookies: async (): Promise<void> => {
await this._connection.send('Browser.deleteAllCookies', { browserContextId });
},
browser(): Browser {
return this._browser;
}
async close() {
assert(this._id, 'Non-incognito profiles cannot be closed!');
await this._browser._disposeContext(this._id);
}
async cookies(...urls: string[]): Promise<NetworkCookie[]> {
const { cookies } = await this._browser._connection.send('Browser.getAllCookies', { browserContextId: this._id });
return filterCookies(cookies.map((c: NetworkCookie) => ({
...c,
expires: c.expires === 0 ? -1 : c.expires
})), urls);
}
async setCookies(cookies: SetNetworkCookieParam[]) {
cookies = rewriteCookies(cookies);
const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[];
await this._browser._connection.send('Browser.setCookies', { cookies: cc, browserContextId: this._id });
}
async clearCookies() {
await this._browser._connection.send('Browser.deleteAllCookies', { browserContextId: this._id });
setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[];
await this._connection.send('Browser.setCookies', { cookies: cc, browserContextId });
},
}, this, isIncognito);
return context;
}
}

View File

@ -20,7 +20,6 @@ import * as debug from 'debug';
import {EventEmitter} from 'events';
import { ConnectionTransport } from '../types';
import { Protocol } from './protocol';
import { throws } from 'assert';
const debugProtocol = debug('playwright:protocol');
const debugWrappedMessage = require('debug')('wrapped');
@ -96,7 +95,7 @@ export class Connection extends EventEmitter {
const delay = this._delay || 0;
this._dispatchTimerId = setTimeout(() => {
this._dispatchTimerId = undefined;
this._dispatchOneMessageFromQueue()
this._dispatchOneMessageFromQueue();
}, delay);
}

View File

@ -18,11 +18,11 @@
import * as EventEmitter from 'events';
import { TimeoutError } from '../Errors';
import * as frames from '../frames';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { assert, helper, RegisteredListener } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import * as network from '../network';
import { TargetSession, TargetSessionEvents } from './Connection';
import { TargetSession } from './Connection';
import { Events } from '../events';
import { ExecutionContextDelegate, EVALUATION_SCRIPT_URL } from './ExecutionContext';
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
@ -30,7 +30,8 @@ import { Page, PageDelegate } from '../page';
import { Protocol } from './protocol';
import { DOMWorldDelegate } from './JSHandle';
import * as dialog from '../dialog';
import { Browser, BrowserContext } from './Browser';
import { Browser } from './Browser';
import { BrowserContext } from '../browserContext';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { WKScreenshotDelegate } from './Screenshotter';
import * as input from '../input';
@ -57,7 +58,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
readonly rawKeyboard: RawKeyboardImpl;
readonly screenshotterDelegate: WKScreenshotDelegate;
_session: TargetSession;
readonly _page: Page<Browser, BrowserContext>;
readonly _page: Page<Browser>;
private readonly _networkManager: NetworkManager;
private readonly _frames: Map<string, frames.Frame>;
private readonly _contextIdToContext: Map<number, js.ExecutionContext>;
@ -66,7 +67,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
private _mainFrame: frames.Frame;
private readonly _bootstrapScripts: string[] = [];
constructor(browserContext: BrowserContext) {
constructor(browserContext: BrowserContext<Browser>) {
super();
this.rawKeyboard = new RawKeyboardImpl();
this.rawMouse = new RawMouseImpl();
@ -95,7 +96,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
}
// This method is called for provisional targets as well. The session passed as the parameter
// may be different from the current session and may be destroyed without becoming current.
// may be different from the current session and may be destroyed without becoming current.
async _initializeSession(session: TargetSession) {
const promises : Promise<any>[] = [
// Page agent must be enabled before Runtime.
@ -109,7 +110,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
];
if (!session.isProvisional()) {
// FIXME: move dialog agent to web process.
// Dialog agent resides in the UI process and should not be re-enabled on navigation.
// Dialog agent resides in the UI process and should not be re-enabled on navigation.
promises.push(session.send('Dialog.enable'));
}
if (this._page._state.userAgent !== null)
@ -193,7 +194,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate,
this._handleFrameTree(child);
}
page(): Page<Browser, BrowserContext> {
page(): Page<Browser> {
return this._page;
}

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { BrowserContext, Browser } from './Browser';
import { Browser } from './Browser';
import { BrowserContext } from '../browserContext';
import { Page } from '../page';
import { Protocol } from './protocol';
import { isSwappedOutError, TargetSession, TargetSessionEvents } from './Connection';
@ -24,18 +25,18 @@ import { FrameManager } from './FrameManager';
const targetSymbol = Symbol('target');
export class Target {
readonly _browserContext: BrowserContext;
readonly _browserContext: BrowserContext<Browser>;
readonly _targetId: string;
readonly _type: 'page' | 'service-worker' | 'worker';
private readonly _session: TargetSession;
private _pagePromise: Promise<Page<Browser, BrowserContext>> | null = null;
_page: Page<Browser, BrowserContext> | null = null;
private _pagePromise: Promise<Page<Browser>> | null = null;
_page: Page<Browser> | null = null;
static fromPage(page: Page<Browser, BrowserContext>): Target {
static fromPage(page: Page<Browser>): Target {
return (page as any)[targetSymbol];
}
constructor(session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) {
constructor(session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext<Browser>) {
const {targetId, type} = targetInfo;
this._session = session;
this._browserContext = browserContext;
@ -83,7 +84,7 @@ export class Target {
(this._page._delegate as FrameManager).setSession(this._session);
}
async page(): Promise<Page<Browser, BrowserContext>> {
async page(): Promise<Page<Browser>> {
if (this._type === 'page' && !this._pagePromise) {
const browser = this._browserContext.browser();
// Reference local page variable as _page may be

View File

@ -2,7 +2,8 @@
// Licensed under the MIT license.
export { TimeoutError } from '../Errors';
export { Browser, BrowserContext } from './Browser';
export { Browser } from './Browser';
export { BrowserContext } from '../browserContext';
export { BrowserFetcher } from '../browserFetcher';
export { ExecutionContext, JSHandle } from '../javascript';
export { ElementHandle } from '../dom';

View File

@ -22,7 +22,7 @@ module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
describe('JSCoverage', function() {
it('should work', async function({page, server}) {
await page.coverage.startJSCoverage();
await page.goto(server.PREFIX + '/jscoverage/simple.html', {waitUntil: 'networkidle0'});
await page.goto(server.PREFIX + '/jscoverage/simple.html', { waitUntil: 'load' });
const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(1);
expect(coverage[0].url).toContain('/jscoverage/simple.html');