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:
parent
3677818202
commit
a43b4095e1
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue