chore: make BrowserContext an interface, with 3 implementations (#1075)

This is in preparation for moving targets to BrowserContext, so that one can work with targets in default context.
This commit is contained in:
Dmitry Gozman 2020-02-24 08:53:30 -08:00 committed by GitHub
parent 3677818202
commit a43b4095e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 417 additions and 374 deletions

View file

@ -19,26 +19,8 @@ import { Page } from './page';
import * as network from './network';
import * as types from './types';
import { helper } from './helper';
import * as platform from './platform';
import { Events } from './events';
import { TimeoutSettings } from './timeoutSettings';
export interface BrowserContextDelegate {
pages(): Promise<Page[]>;
existingPages(): Page[];
newPage(): Promise<Page>;
close(): Promise<void>;
cookies(): Promise<network.NetworkCookie[]>;
setCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
clearCookies(): Promise<void>;
setPermissions(origin: string, permissions: string[]): Promise<void>;
clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
}
export type BrowserContextOptions = {
viewport?: types.Viewport | null,
ignoreHTTPSErrors?: boolean,
@ -51,106 +33,44 @@ export type BrowserContextOptions = {
permissions?: { [key: string]: string[] };
};
export class BrowserContext extends platform.EventEmitter {
private readonly _delegate: BrowserContextDelegate;
readonly _options: BrowserContextOptions;
export interface BrowserContext {
setDefaultNavigationTimeout(timeout: number): void;
setDefaultTimeout(timeout: number): void;
pages(): Promise<Page[]>;
newPage(): Promise<Page>;
cookies(...urls: string[]): Promise<network.NetworkCookie[]>;
setCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
clearCookies(): Promise<void>;
setPermissions(origin: string, permissions: string[]): Promise<void>;
clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
close(): Promise<void>;
_existingPages(): Page[];
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
readonly _options: BrowserContextOptions;
}
constructor(delegate: BrowserContextDelegate, options: BrowserContextOptions) {
super();
this._delegate = delegate;
this._timeoutSettings = new TimeoutSettings();
this._options = { ...options };
if (!this._options.viewport && this._options.viewport !== null)
this._options.viewport = { width: 1280, height: 720 };
if (this._options.viewport)
this._options.viewport = { ...this._options.viewport };
if (this._options.geolocation)
this._options.geolocation = verifyGeolocation(this._options.geolocation);
}
async _initialize() {
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
return this._delegate.existingPages();
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
return this._delegate.pages();
}
async newPage(): Promise<Page> {
const pages = this._delegate.existingPages();
for (const page of pages) {
if (page._ownedContext)
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
}
return this._delegate.newPage();
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
return network.filterCookies(await this._delegate.cookies(), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._delegate.setCookies(network.rewriteCookies(cookies));
}
async clearCookies() {
await this._delegate.clearCookies();
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
await this._delegate.setPermissions(origin, permissions);
}
async clearPermissions() {
await this._delegate.clearPermissions();
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
await this._delegate.setGeolocation(geolocation);
}
async close() {
if (this._closed)
return;
await this._delegate.close();
this._closed = true;
this.emit(Events.BrowserContext.Close);
}
static validateOptions(options: BrowserContextOptions) {
if (options.geolocation)
verifyGeolocation(options.geolocation);
}
_browserClosed() {
this._closed = true;
for (const page of this._delegate.existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
export function assertBrowserContextIsNotOwned(context: BrowserContext) {
const pages = context._existingPages();
for (const page of pages) {
if (page._ownedContext)
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
}
}
function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
export function validateBrowserContextOptions(options: BrowserContextOptions): BrowserContextOptions {
const result = { ...options };
if (!result.viewport && result.viewport !== null)
result.viewport = { width: 1280, height: 720 };
if (result.viewport)
result.viewport = { ...result.viewport };
if (result.geolocation)
result.geolocation = verifyGeolocation(result.geolocation);
return result;
}
export function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
const result = { ...geolocation };
result.accuracy = result.accuracy || 0;
const { longitude, latitude, accuracy } = result;

View file

@ -18,7 +18,7 @@
import { Events } from './events';
import { Events as CommonEvents } from '../events';
import { assert, helper } from '../helper';
import { BrowserContext, BrowserContextOptions } from '../browserContext';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { CRConnection, ConnectionEvents, CRSession } from './crConnection';
import { Page } from '../page';
import { CRTarget } from './crTarget';
@ -30,12 +30,13 @@ import * as types from '../types';
import * as platform from '../platform';
import { readProtocolStream } from './crProtocolHelper';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
export class CRBrowser extends platform.EventEmitter implements Browser {
_connection: CRConnection;
_client: CRSession;
readonly _defaultContext: BrowserContext;
private _contexts = new Map<string, BrowserContext>();
readonly _contexts = new Map<string, CRBrowserContext>();
_targets = new Map<string, CRTarget>();
private _tracingRecording = false;
@ -54,9 +55,9 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._connection = connection;
this._client = connection.rootSession;
this._defaultContext = this._createBrowserContext(null, {});
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this.contexts())
for (const context of this._contexts.values())
context._browserClosed();
this.emit(CommonEvents.Browser.Disconnected);
});
@ -65,99 +66,10 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
}
_createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext {
const context = new BrowserContext({
pages: async (): Promise<Page[]> => {
const targets = this._allTargets().filter(target => target.context() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page) as Page[];
},
existingPages: (): Page[] => {
const pages: Page[] = [];
for (const target of this._allTargets()) {
if (target.context() === context && target._crPage)
pages.push(target._crPage.page());
}
return pages;
},
newPage: 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');
const page = await target.page();
return page!;
},
close: async (): Promise<void> => {
assert(contextId, 'Non-incognito profiles cannot be closed!');
await this._client.send('Target.disposeBrowserContext', { browserContextId: contextId });
this._contexts.delete(contextId);
},
cookies: 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;
delete copy.priority;
return copy as network.NetworkCookie;
});
},
clearCookies: async (): Promise<void> => {
await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined });
},
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
const webPermissionToProtocol = new Map<string, Protocol.Browser.PermissionType>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardSanitizedWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._client.send('Browser.grantPermissions', { origin, browserContextId: contextId || undefined, permissions: filtered });
},
clearPermissions: async () => {
await this._client.send('Browser.resetPermissions', { browserContextId: contextId || undefined });
},
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
for (const page of await context.pages())
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
}, options);
return context;
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
BrowserContext.validateOptions(options);
options = validateBrowserContextOptions(options);
const { browserContextId } = await this._client.send('Target.createBrowserContext');
const context = this._createBrowserContext(browserContextId, options);
const context = new CRBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
return context;
@ -304,3 +216,133 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._connection._debugProtocol = debugFunction;
}
}
export class CRBrowserContext extends platform.EventEmitter implements BrowserContext {
readonly _browser: CRBrowser;
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
}
async _initialize() {
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
const pages: Page[] = [];
for (const target of this._browser._allTargets()) {
if (target.context() === this && target._crPage)
pages.push(target._crPage.page());
}
return pages;
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page) as Page[];
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
const target = this._browser._targets.get(targetId)!;
assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page!;
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
const { cookies } = await this._browser._client.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined });
return network.filterCookies(cookies.map(c => {
const copy: any = { sameSite: 'None', ...c };
delete copy.size;
delete copy.priority;
return copy as network.NetworkCookie;
}), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._browser._client.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined });
}
async clearCookies() {
await this._browser._client.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined });
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
const webPermissionToProtocol = new Map<string, Protocol.Browser.PermissionType>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardSanitizedWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browser._client.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered });
}
async clearPermissions() {
await this._browser._client.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined });
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
for (const page of this._existingPages())
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
async close() {
if (this._closed)
return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._client.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(CommonEvents.BrowserContext.Close);
}
_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(CommonEvents.BrowserContext.Close);
}
}

View file

@ -16,7 +16,7 @@
*/
import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions } from '../browserContext';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
import { Events } from '../events';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network';
@ -27,12 +27,13 @@ import { FFPage } from './ffPage';
import * as platform from '../platform';
import { Protocol } from './protocol';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection;
_targets: Map<string, Target>;
readonly _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[];
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> {
@ -47,10 +48,10 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
this._connection = connection;
this._targets = new Map();
this._defaultContext = this._createBrowserContext(null, {});
this._defaultContext = new FFBrowserContext(this, null, validateBrowserContextOptions({}));
this._contexts = new Map();
this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this.contexts())
for (const context of this._contexts.values())
context._browserClosed();
this.emit(Events.Browser.Disconnected);
});
@ -67,6 +68,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
options = validateBrowserContextOptions(options);
let viewport;
if (options.viewport) {
viewport = {
@ -92,7 +94,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
// TODO: move ignoreHTTPSErrors to browser context level.
if (options.ignoreHTTPSErrors)
await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true });
const context = this._createBrowserContext(browserContextId, options);
const context = new FFBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
return context;
@ -176,82 +178,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
await disconnected;
}
_createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext {
BrowserContext.validateOptions(options);
const context = new BrowserContext({
pages: async (): Promise<Page[]> => {
const targets = this._allTargets().filter(target => target.context() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
},
existingPages: (): Page[] => {
const pages: Page[] = [];
for (const target of this._allTargets()) {
if (target.context() === context && target._ffPage)
pages.push(target._ffPage._page);
}
return pages;
},
newPage: async (): Promise<Page> => {
const {targetId} = await this._connection.send('Target.newPage', {
browserContextId: browserContextId || undefined
});
const target = this._targets.get(targetId)!;
return target.page();
},
close: async (): Promise<void> => {
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
await this._connection.send('Target.removeBrowserContext', { browserContextId });
this._contexts.delete(browserContextId);
},
cookies: 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;
});
},
clearCookies: async (): Promise<void> => {
await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies });
},
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
const webPermissionToProtocol = new Map<string, 'geo' | 'microphone' | 'camera' | 'desktop-notifications'>([
['geolocation', 'geo'],
['microphone', 'microphone'],
['camera', 'camera'],
['notifications', 'desktop-notifications'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {origin, browserContextId: browserContextId || undefined, permissions: filtered});
},
clearPermissions: async () => {
await this._connection.send('Browser.resetPermissions', { browserContextId: browserContextId || undefined });
},
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
throw new Error('Geolocation emulation is not supported in Firefox');
}
}, options);
return context;
}
_setDebugFunction(debugFunction: (message: string) => void) {
this._connection._debugProtocol = debugFunction;
}
@ -326,3 +252,116 @@ class Target {
return this._browser;
}
}
export class FFBrowserContext extends platform.EventEmitter implements BrowserContext {
readonly _browser: FFBrowser;
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
}
async _initialize() {
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
const pages: Page[] = [];
for (const target of this._browser._allTargets()) {
if (target.context() === this && target._ffPage)
pages.push(target._ffPage._page);
}
return pages;
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const {targetId} = await this._browser._connection.send('Target.newPage', {
browserContextId: this._browserContextId || undefined
});
const target = this._browser._targets.get(targetId)!;
return target.page();
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
const { cookies } = await this._browser._connection.send('Browser.getCookies', { browserContextId: this._browserContextId || undefined });
return network.filterCookies(cookies.map(c => {
const copy: any = { ... c };
delete copy.size;
return copy as network.NetworkCookie;
}), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId || undefined, cookies: network.rewriteCookies(cookies) });
}
async clearCookies() {
await this._browser._connection.send('Browser.clearCookies', { browserContextId: this._browserContextId || undefined });
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
const webPermissionToProtocol = new Map<string, 'geo' | 'microphone' | 'camera' | 'desktop-notifications'>([
['geolocation', 'geo'],
['microphone', 'microphone'],
['camera', 'camera'],
['notifications', 'desktop-notifications'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browser._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions: filtered});
}
async clearPermissions() {
await this._browser._connection.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined });
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
throw new Error('Geolocation emulation is not supported in Firefox');
}
async close() {
if (this._closed)
return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._connection.send('Target.removeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(Events.BrowserContext.Close);
}
_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
}
}

View file

@ -16,7 +16,7 @@
*/
import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions } from '../browserContext';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page } from '../page';
@ -27,15 +27,16 @@ import { Protocol } from './protocol';
import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageReceivedPayload } from './wkConnection';
import { WKPageProxy } from './wkPageProxy';
import * as platform from '../platform';
import { TimeoutSettings } from '../timeoutSettings';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
export class WKBrowser extends platform.EventEmitter implements Browser {
private readonly _connection: WKConnection;
private readonly _browserSession: WKSession;
readonly _browserSession: WKSession;
readonly _defaultContext: BrowserContext;
private readonly _contexts = new Map<string, BrowserContext>();
private readonly _pageProxies = new Map<string, WKPageProxy>();
readonly _contexts = new Map<string, WKBrowserContext>();
readonly _pageProxies = new Map<string, WKPageProxy>();
private readonly _eventListeners: RegisteredListener[];
private _firstPageProxyCallback?: () => void;
@ -51,7 +52,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
this._browserSession = this._connection.browserSession;
this._defaultContext = this._createBrowserContext(undefined, {});
this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
this._eventListeners = [
helper.addEventListener(this._browserSession, 'Browser.pageProxyCreated', this._onPageProxyCreated.bind(this)),
@ -64,7 +65,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
}
_onDisconnect() {
for (const context of this.contexts())
for (const context of this._contexts.values())
context._browserClosed();
for (const pageProxy of this._pageProxies.values())
pageProxy.dispose();
@ -73,13 +74,10 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
options = validateBrowserContextOptions(options);
const { browserContextId } = await this._browserSession.send('Browser.createContext');
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
const context = this._createBrowserContext(browserContextId, options);
if (options.ignoreHTTPSErrors)
await this._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true });
if (options.locale)
await this._browserSession.send('Browser.setLanguages', { browserContextId, languages: [options.locale] });
const context = new WKBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
return context;
@ -166,81 +164,125 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
await disconnected;
}
_createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext {
BrowserContext.validateOptions(options);
const context = new BrowserContext({
pages: async (): Promise<Page[]> => {
const pageProxies = Array.from(this._pageProxies.values()).filter(proxy => proxy._browserContext === context);
return await Promise.all(pageProxies.map(proxy => proxy.page()));
},
existingPages: (): Page[] => {
const pages: Page[] = [];
for (const pageProxy of this._pageProxies.values()) {
if (pageProxy._browserContext !== context)
continue;
const page = pageProxy.existingPage();
if (page)
pages.push(page);
}
return pages;
},
newPage: async (): Promise<Page> => {
const { pageProxyId } = await this._browserSession.send('Browser.createPage', { browserContextId });
const pageProxy = this._pageProxies.get(pageProxyId)!;
return await pageProxy.page();
},
close: async (): Promise<void> => {
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browserSession.send('Browser.deleteContext', { browserContextId: browserContextId });
this._contexts.delete(browserContextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._browserSession.send('Browser.getAllCookies', { browserContextId });
return cookies.map((c: network.NetworkCookie) => ({
...c,
expires: c.expires === 0 ? -1 : c.expires
}));
},
clearCookies: async (): Promise<void> => {
await this._browserSession.send('Browser.deleteAllCookies', { browserContextId });
},
setCookies: 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._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId });
},
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
const webPermissionToProtocol = new Map<string, string>([
['geolocation', 'geolocation'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browserSession.send('Browser.grantPermissions', { origin, browserContextId, permissions: filtered });
},
clearPermissions: async () => {
await this._browserSession.send('Browser.resetPermissions', { browserContextId });
},
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined;
await this._browserSession.send('Browser.setGeolocationOverride', { browserContextId, geolocation: payload });
}
}, options);
return context;
}
_setDebugFunction(debugFunction: (message: string) => void) {
this._connection._debugFunction = debugFunction;
}
}
export class WKBrowserContext extends platform.EventEmitter implements BrowserContext {
readonly _browser: WKBrowser;
readonly _browserContextId: string | undefined;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
super();
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
}
async _initialize() {
if (this._options.ignoreHTTPSErrors)
await this._browser._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true });
if (this._options.locale)
await this._browser._browserSession.send('Browser.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] });
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
const pages: Page[] = [];
for (const pageProxy of this._browser._pageProxies.values()) {
if (pageProxy._browserContext !== this)
continue;
const page = pageProxy.existingPage();
if (page)
pages.push(page);
}
return pages;
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this);
return await Promise.all(pageProxies.map(proxy => proxy.page()));
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
const pageProxy = this._browser._pageProxies.get(pageProxyId)!;
return await pageProxy.page();
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
const { cookies } = await this._browser._browserSession.send('Browser.getAllCookies', { browserContextId: this._browserContextId });
return network.filterCookies(cookies.map((c: network.NetworkCookie) => ({
...c,
expires: c.expires === 0 ? -1 : c.expires
})), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
const cc = network.rewriteCookies(cookies).map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[];
await this._browser._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId: this._browserContextId });
}
async clearCookies() {
await this._browser._browserSession.send('Browser.deleteAllCookies', { browserContextId: this._browserContextId });
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
const webPermissionToProtocol = new Map<string, string>([
['geolocation', 'geolocation'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browser._browserSession.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId, permissions: filtered });
}
async clearPermissions() {
await this._browser._browserSession.send('Browser.resetPermissions', { browserContextId: this._browserContextId });
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined;
await this._browser._browserSession.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload });
}
async close() {
if (this._closed)
return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._browserSession.send('Browser.deleteContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(Events.BrowserContext.Close);
}
_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
}
}