2019-12-19 16:53:24 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2020-01-23 14:40:37 -08:00
|
|
|
import { Browser, createTransport, ConnectOptions } from '../browser';
|
2020-01-07 10:39:01 -08:00
|
|
|
import { BrowserContext, BrowserContextOptions } from '../browserContext';
|
2020-01-07 13:57:37 -08:00
|
|
|
import { assert, helper, RegisteredListener } from '../helper';
|
2019-12-19 16:53:24 -08:00
|
|
|
import * as network from '../network';
|
|
|
|
import { Page } from '../page';
|
2020-01-23 14:40:37 -08:00
|
|
|
import { ConnectionTransport } from '../transport';
|
2020-01-07 10:39:01 -08:00
|
|
|
import * as types from '../types';
|
2020-01-08 13:55:38 -08:00
|
|
|
import { Events } from '../events';
|
2020-01-07 10:39:01 -08:00
|
|
|
import { Protocol } from './protocol';
|
2020-01-09 15:14:35 -08:00
|
|
|
import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageReceivedPayload } from './wkConnection';
|
2020-01-07 10:39:01 -08:00
|
|
|
import { WKPageProxy } from './wkPageProxy';
|
2020-01-08 14:04:33 -08:00
|
|
|
import * as platform from '../platform';
|
2019-12-19 16:53:24 -08:00
|
|
|
|
2020-01-08 14:04:33 -08:00
|
|
|
export class WKBrowser extends platform.EventEmitter implements Browser {
|
2020-01-09 15:14:35 -08:00
|
|
|
private readonly _connection: WKConnection;
|
|
|
|
private readonly _browserSession: WKSession;
|
2020-01-04 10:12:40 -08:00
|
|
|
private readonly _defaultContext: BrowserContext;
|
|
|
|
private readonly _contexts = new Map<string, BrowserContext>();
|
2020-01-07 10:39:01 -08:00
|
|
|
private readonly _pageProxies = new Map<string, WKPageProxy>();
|
2020-01-04 10:12:40 -08:00
|
|
|
private readonly _eventListeners: RegisteredListener[];
|
|
|
|
|
2020-01-07 10:39:01 -08:00
|
|
|
private _firstPageProxyCallback?: () => void;
|
|
|
|
private readonly _firstPageProxyPromise: Promise<void>;
|
2019-12-19 16:53:24 -08:00
|
|
|
|
2020-01-23 14:40:37 -08:00
|
|
|
static async connect(options: ConnectOptions): Promise<WKBrowser> {
|
2020-01-07 16:15:07 -08:00
|
|
|
const transport = await createTransport(options);
|
|
|
|
const browser = new WKBrowser(transport);
|
|
|
|
// TODO: figure out the timeout.
|
|
|
|
await browser._waitForFirstPageTarget(30000);
|
|
|
|
return browser;
|
|
|
|
}
|
|
|
|
|
2019-12-19 16:53:24 -08:00
|
|
|
constructor(transport: ConnectionTransport) {
|
|
|
|
super();
|
2020-01-09 15:14:35 -08:00
|
|
|
this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
|
|
|
|
this._browserSession = this._connection.browserSession;
|
2019-12-19 16:53:24 -08:00
|
|
|
|
|
|
|
this._defaultContext = this._createBrowserContext(undefined, {});
|
|
|
|
|
|
|
|
this._eventListeners = [
|
2020-01-09 15:14:35 -08:00
|
|
|
helper.addEventListener(this._browserSession, 'Browser.pageProxyCreated', this._onPageProxyCreated.bind(this)),
|
|
|
|
helper.addEventListener(this._browserSession, 'Browser.pageProxyDestroyed', this._onPageProxyDestroyed.bind(this)),
|
2020-01-14 11:46:08 -08:00
|
|
|
helper.addEventListener(this._browserSession, 'Browser.provisionalLoadFailed', event => this._onProvisionalLoadFailed(event)),
|
2020-01-09 15:14:35 -08:00
|
|
|
helper.addEventListener(this._browserSession, kPageProxyMessageReceived, this._onPageProxyMessageReceived.bind(this)),
|
2019-12-19 16:53:24 -08:00
|
|
|
];
|
|
|
|
|
2020-01-07 10:39:01 -08:00
|
|
|
this._firstPageProxyPromise = new Promise<void>(resolve => this._firstPageProxyCallback = resolve);
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
2020-01-09 15:14:35 -08:00
|
|
|
_onDisconnect() {
|
|
|
|
for (const pageProxy of this._pageProxies.values())
|
|
|
|
pageProxy.dispose();
|
|
|
|
this._pageProxies.clear();
|
|
|
|
this.emit(Events.Browser.Disconnected);
|
|
|
|
}
|
|
|
|
|
2019-12-19 16:53:24 -08:00
|
|
|
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
|
2020-01-09 15:14:35 -08:00
|
|
|
const { browserContextId } = await this._browserSession.send('Browser.createContext');
|
2019-12-19 16:53:24 -08:00
|
|
|
const context = this._createBrowserContext(browserContextId, options);
|
|
|
|
if (options.ignoreHTTPSErrors)
|
2020-01-09 15:14:35 -08:00
|
|
|
await this._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true });
|
2020-01-13 13:32:44 -08:00
|
|
|
await context._initialize();
|
2019-12-19 16:53:24 -08:00
|
|
|
this._contexts.set(browserContextId, context);
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
|
|
|
browserContexts(): BrowserContext[] {
|
|
|
|
return [this._defaultContext, ...Array.from(this._contexts.values())];
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultContext(): BrowserContext {
|
|
|
|
return this._defaultContext;
|
|
|
|
}
|
|
|
|
|
2020-01-04 10:12:40 -08:00
|
|
|
async _waitForFirstPageTarget(timeout: number): Promise<void> {
|
2020-01-07 10:39:01 -08:00
|
|
|
assert(!this._pageProxies.size);
|
|
|
|
await helper.waitWithTimeout(this._firstPageProxyPromise, 'firstPageProxy', timeout);
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
2020-01-09 15:14:35 -08:00
|
|
|
_onPageProxyCreated(event: Protocol.Browser.pageProxyCreatedPayload) {
|
|
|
|
const { pageProxyInfo } = event;
|
|
|
|
const pageProxyId = pageProxyInfo.pageProxyId;
|
2019-12-19 16:53:24 -08:00
|
|
|
let context = null;
|
2020-01-07 10:39:01 -08:00
|
|
|
if (pageProxyInfo.browserContextId) {
|
2019-12-19 16:53:24 -08:00
|
|
|
// FIXME: we don't know about the default context id, so assume that all targets from
|
|
|
|
// unknown contexts are created in the 'default' context which can in practice be represented
|
|
|
|
// by multiple actual contexts in WebKit. Solving this properly will require adding context
|
|
|
|
// lifecycle events.
|
2020-01-07 10:39:01 -08:00
|
|
|
context = this._contexts.get(pageProxyInfo.browserContextId);
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
if (!context)
|
|
|
|
context = this._defaultContext;
|
2020-01-09 15:14:35 -08:00
|
|
|
const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => {
|
|
|
|
this._connection.rawSend({ ...message, pageProxyId });
|
|
|
|
});
|
|
|
|
const pageProxy = new WKPageProxy(pageProxySession, context);
|
|
|
|
this._pageProxies.set(pageProxyId, pageProxy);
|
2019-12-19 16:53:24 -08:00
|
|
|
|
2020-01-07 10:39:01 -08:00
|
|
|
if (pageProxyInfo.openerId) {
|
|
|
|
const opener = this._pageProxies.get(pageProxyInfo.openerId);
|
|
|
|
if (opener)
|
|
|
|
opener.onPopupCreated(pageProxy);
|
|
|
|
}
|
2019-12-19 16:53:24 -08:00
|
|
|
|
2020-01-07 10:39:01 -08:00
|
|
|
if (this._firstPageProxyCallback) {
|
|
|
|
this._firstPageProxyCallback();
|
2020-01-13 13:33:25 -08:00
|
|
|
this._firstPageProxyCallback = undefined;
|
2020-01-07 10:39:01 -08:00
|
|
|
}
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
2020-01-09 15:14:35 -08:00
|
|
|
_onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) {
|
|
|
|
const pageProxyId = event.pageProxyId;
|
2020-01-13 13:33:25 -08:00
|
|
|
const pageProxy = this._pageProxies.get(pageProxyId)!;
|
2020-01-09 15:14:35 -08:00
|
|
|
pageProxy.didClose();
|
2020-01-07 10:39:01 -08:00
|
|
|
pageProxy.dispose();
|
|
|
|
this._pageProxies.delete(pageProxyId);
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
2020-01-09 15:14:35 -08:00
|
|
|
_onPageProxyMessageReceived(event: PageProxyMessageReceivedPayload) {
|
2020-01-13 13:33:25 -08:00
|
|
|
const pageProxy = this._pageProxies.get(event.pageProxyId)!;
|
2020-01-09 15:14:35 -08:00
|
|
|
pageProxy.dispatchMessageToSession(event.message);
|
|
|
|
}
|
|
|
|
|
2020-01-14 11:46:08 -08:00
|
|
|
_onProvisionalLoadFailed(event: Protocol.Browser.provisionalLoadFailedPayload) {
|
|
|
|
const pageProxy = this._pageProxies.get(event.pageProxyId)!;
|
|
|
|
pageProxy.handleProvisionalLoadFailed(event);
|
|
|
|
}
|
|
|
|
|
2020-01-22 17:42:10 -08:00
|
|
|
async disconnect() {
|
|
|
|
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
|
|
|
|
this._connection.close();
|
|
|
|
await disconnected;
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
isConnected(): boolean {
|
2020-01-22 17:42:10 -08:00
|
|
|
return !this._connection.isClosed();
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
async close() {
|
|
|
|
helper.removeEventListeners(this._eventListeners);
|
2020-01-09 15:14:35 -08:00
|
|
|
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
|
|
|
|
await this._browserSession.send('Browser.close');
|
2020-01-08 13:55:38 -08:00
|
|
|
await disconnected;
|
2019-12-19 16:53:24 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
_createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext {
|
2020-01-13 17:16:05 -08:00
|
|
|
BrowserContext.validateOptions(options);
|
2019-12-19 16:53:24 -08:00
|
|
|
const context = new BrowserContext({
|
|
|
|
pages: async (): Promise<Page[]> => {
|
2020-01-07 10:39:01 -08:00
|
|
|
const pageProxies = Array.from(this._pageProxies.values()).filter(proxy => proxy._browserContext === context);
|
|
|
|
return await Promise.all(pageProxies.map(proxy => proxy.page()));
|
2019-12-19 16:53:24 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
newPage: async (): Promise<Page> => {
|
2020-01-09 15:14:35 -08:00
|
|
|
const { pageProxyId } = await this._browserSession.send('Browser.createPage', { browserContextId });
|
2020-01-13 13:33:25 -08:00
|
|
|
const pageProxy = this._pageProxies.get(pageProxyId)!;
|
2020-01-07 10:39:01 -08:00
|
|
|
return await pageProxy.page();
|
2019-12-19 16:53:24 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
close: async (): Promise<void> => {
|
|
|
|
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
|
2020-01-13 13:33:25 -08:00
|
|
|
await this._browserSession.send('Browser.deleteContext', { browserContextId: browserContextId! });
|
|
|
|
this._contexts.delete(browserContextId!);
|
2019-12-19 16:53:24 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
cookies: async (): Promise<network.NetworkCookie[]> => {
|
2020-01-09 15:14:35 -08:00
|
|
|
const { cookies } = await this._browserSession.send('Browser.getAllCookies', { browserContextId });
|
2019-12-19 16:53:24 -08:00
|
|
|
return cookies.map((c: network.NetworkCookie) => ({
|
|
|
|
...c,
|
|
|
|
expires: c.expires === 0 ? -1 : c.expires
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
|
|
|
clearCookies: async (): Promise<void> => {
|
2020-01-09 15:14:35 -08:00
|
|
|
await this._browserSession.send('Browser.deleteAllCookies', { browserContextId });
|
2019-12-19 16:53:24 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
|
|
|
|
const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[];
|
2020-01-09 15:14:35 -08:00
|
|
|
await this._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId });
|
2019-12-19 16:53:24 -08:00
|
|
|
},
|
2019-12-20 13:07:14 -08:00
|
|
|
|
|
|
|
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
|
2020-01-03 10:14:50 -08:00
|
|
|
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;
|
|
|
|
});
|
2020-01-09 15:14:35 -08:00
|
|
|
await this._browserSession.send('Browser.grantPermissions', { origin, browserContextId, permissions: filtered });
|
2019-12-20 13:07:14 -08:00
|
|
|
},
|
2019-12-20 15:32:30 -08:00
|
|
|
|
2019-12-20 13:07:14 -08:00
|
|
|
clearPermissions: async () => {
|
2020-01-09 15:14:35 -08:00
|
|
|
await this._browserSession.send('Browser.resetPermissions', { browserContextId });
|
2020-01-03 10:14:50 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
|
|
|
|
const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined;
|
2020-01-09 15:14:35 -08:00
|
|
|
await this._browserSession.send('Browser.setGeolocationOverride', { browserContextId, geolocation: payload });
|
2019-12-20 13:07:14 -08:00
|
|
|
}
|
2019-12-19 16:53:24 -08:00
|
|
|
}, options);
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
}
|