chore: prepare library types for rpc (#2706)

This commit is contained in:
Pavel Feldman 2020-06-25 08:30:56 -07:00 committed by GitHub
parent 1865c5685a
commit bc3050776e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 270 additions and 195 deletions

View File

@ -14,7 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserContext, BrowserContextOptions, BrowserContextBase, PersistentContextOptions } from './browserContext'; import * as types from './types';
import { BrowserContext, BrowserContextBase } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
@ -22,17 +23,20 @@ import type { BrowserServer } from './server/browserServer';
import { Events } from './events'; import { Events } from './events';
import { Loggers } from './logger'; import { Loggers } from './logger';
import { ProxySettings } from './types'; import { ProxySettings } from './types';
import { LoggerSink } from './loggerSink';
export type BrowserOptions = { export type BrowserOptions = {
loggers: Loggers, loggers: Loggers,
downloadsPath?: string, downloadsPath?: string,
headful?: boolean, headful?: boolean,
persistent?: PersistentContextOptions, // Undefined means no persistent context. persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
slowMo?: number, slowMo?: number,
ownedServer?: BrowserServer, ownedServer?: BrowserServer,
proxy?: ProxySettings, proxy?: ProxySettings,
}; };
export type BrowserContextOptions = types.BrowserContextOptions & { logger?: LoggerSink };
export interface Browser extends EventEmitter { export interface Browser extends EventEmitter {
newContext(options?: BrowserContextOptions): Promise<BrowserContext>; newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
contexts(): BrowserContext[]; contexts(): BrowserContext[];

View File

@ -31,43 +31,18 @@ import { ProgressController } from './progress';
import { DebugController } from './debug/debugController'; import { DebugController } from './debug/debugController';
import { LoggerSink } from './loggerSink'; import { LoggerSink } from './loggerSink';
type CommonContextOptions = {
viewport?: types.Size | null,
ignoreHTTPSErrors?: boolean,
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
locale?: string,
timezoneId?: string,
geolocation?: types.Geolocation,
permissions?: string[],
extraHTTPHeaders?: network.Headers,
offline?: boolean,
httpCredentials?: types.Credentials,
deviceScaleFactor?: number,
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: types.ColorScheme,
acceptDownloads?: boolean,
};
export type PersistentContextOptions = CommonContextOptions;
export type BrowserContextOptions = CommonContextOptions & {
logger?: LoggerSink,
};
export interface BrowserContext { export interface BrowserContext {
setDefaultNavigationTimeout(timeout: number): void; setDefaultNavigationTimeout(timeout: number): void;
setDefaultTimeout(timeout: number): void; setDefaultTimeout(timeout: number): void;
pages(): Page[]; pages(): Page[];
newPage(): Promise<Page>; newPage(): Promise<Page>;
cookies(urls?: string | string[]): Promise<network.NetworkCookie[]>; cookies(urls?: string | string[]): Promise<types.NetworkCookie[]>;
addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>; addCookies(cookies: types.SetNetworkCookieParam[]): Promise<void>;
clearCookies(): Promise<void>; clearCookies(): Promise<void>;
grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void>; grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void>;
clearPermissions(): Promise<void>; clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>; setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
setExtraHTTPHeaders(headers: network.Headers): Promise<void>; setExtraHTTPHeaders(headers: types.Headers): Promise<void>;
setOffline(offline: boolean): Promise<void>; setOffline(offline: boolean): Promise<void>;
setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void>; setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void>;
addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void>; addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void>;
@ -79,6 +54,8 @@ export interface BrowserContext {
close(): Promise<void>; close(): Promise<void>;
} }
type BrowserContextOptions = types.BrowserContextOptions & { logger?: LoggerSink };
export abstract class BrowserContextBase extends EventEmitter implements BrowserContext { export abstract class BrowserContextBase extends EventEmitter implements BrowserContext {
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>(); readonly _pageBindings = new Map<string, PageBinding>();
@ -141,21 +118,27 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
// BrowserContext methods. // BrowserContext methods.
abstract pages(): Page[]; abstract pages(): Page[];
abstract newPage(): Promise<Page>; abstract newPage(): Promise<Page>;
abstract cookies(...urls: string[]): Promise<network.NetworkCookie[]>; abstract _doCookies(urls: string[]): Promise<types.NetworkCookie[]>;
abstract addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>; abstract addCookies(cookies: types.SetNetworkCookieParam[]): Promise<void>;
abstract clearCookies(): Promise<void>; abstract clearCookies(): Promise<void>;
abstract _doGrantPermissions(origin: string, permissions: string[]): Promise<void>; abstract _doGrantPermissions(origin: string, permissions: string[]): Promise<void>;
abstract _doClearPermissions(): Promise<void>; abstract _doClearPermissions(): Promise<void>;
abstract setGeolocation(geolocation: types.Geolocation | null): Promise<void>; abstract setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
abstract setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void>; abstract setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void>;
abstract setExtraHTTPHeaders(headers: network.Headers): Promise<void>; abstract setExtraHTTPHeaders(headers: types.Headers): Promise<void>;
abstract setOffline(offline: boolean): Promise<void>; abstract setOffline(offline: boolean): Promise<void>;
abstract addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, arg?: any): Promise<void>; abstract _doAddInitScript(expression: string): Promise<void>;
abstract _doExposeBinding(binding: PageBinding): Promise<void>; abstract _doExposeBinding(binding: PageBinding): Promise<void>;
abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise<void>; abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise<void>;
abstract unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void>; abstract unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void>;
abstract close(): Promise<void>; abstract close(): Promise<void>;
async cookies(urls: string | string[] | undefined = []): Promise<types.NetworkCookie[]> {
if (urls && !Array.isArray(urls))
urls = [ urls ];
return await this._doCookies(urls as string[]);
}
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> { async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args)); await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args));
} }
@ -172,6 +155,11 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
this._doExposeBinding(binding); this._doExposeBinding(binding);
} }
async addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, arg?: any): Promise<void> {
const source = await helper.evaluationScript(script, arg);
await this._doAddInitScript(source);
}
async grantPermissions(permissions: string[], options?: { origin?: string }) { async grantPermissions(permissions: string[], options?: { origin?: string }) {
let origin = '*'; let origin = '*';
if (options && options.origin) { if (options && options.origin) {

View File

@ -15,10 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserBase, BrowserOptions } from '../browser'; import { BrowserBase, BrowserOptions, BrowserContextOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events as CommonEvents } from '../events'; import { Events as CommonEvents } from '../events';
import { assert, helper } from '../helper'; import { assert } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, Worker } from '../page'; import { Page, PageBinding, Worker } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
@ -280,7 +280,7 @@ export class CRBrowserContext extends BrowserContextBase {
readonly _browserContextId: string | null; readonly _browserContextId: string | null;
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) { constructor(browser: CRBrowser, browserContextId: string | null, options: types.BrowserContextOptions) {
super(browser, options); super(browser, options);
this._browser = browser; this._browser = browser;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
@ -325,18 +325,18 @@ export class CRBrowserContext extends BrowserContextBase {
throw result; throw result;
} }
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> { async _doCookies(urls: string[]): Promise<types.NetworkCookie[]> {
const { cookies } = await this._browser._session.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined }); const { cookies } = await this._browser._session.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined });
return network.filterCookies(cookies.map(c => { return network.filterCookies(cookies.map(c => {
const copy: any = { sameSite: 'None', ...c }; const copy: any = { sameSite: 'None', ...c };
delete copy.size; delete copy.size;
delete copy.priority; delete copy.priority;
delete copy.session; delete copy.session;
return copy as network.NetworkCookie; return copy as types.NetworkCookie;
}), urls); }), urls);
} }
async addCookies(cookies: network.SetNetworkCookieParam[]) { async addCookies(cookies: types.SetNetworkCookieParam[]) {
await this._browser._session.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined }); await this._browser._session.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined });
} }
@ -384,7 +384,7 @@ export class CRBrowserContext extends BrowserContextBase {
await (page._delegate as CRPage).updateGeolocation(); await (page._delegate as CRPage).updateGeolocation();
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers); this._options.extraHTTPHeaders = network.verifyHeaders(headers);
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as CRPage).updateExtraHTTPHeaders(); await (page._delegate as CRPage).updateExtraHTTPHeaders();
@ -402,8 +402,7 @@ export class CRBrowserContext extends BrowserContextBase {
await (page._delegate as CRPage).updateHttpCredentials(); await (page._delegate as CRPage).updateHttpCredentials();
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { async _doAddInitScript(source: string) {
const source = await helper.evaluationScript(script, arg);
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as CRPage).evaluateOnNewDocument(source); await (page._delegate as CRPage).evaluateOnNewDocument(source);

View File

@ -21,7 +21,7 @@ import { assert, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as network from '../network'; import * as network from '../network';
import * as frames from '../frames'; import * as frames from '../frames';
import { Credentials } from '../types'; import * as types from '../types';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
export class CRNetworkManager { export class CRNetworkManager {
@ -63,7 +63,7 @@ export class CRNetworkManager {
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
} }
async authenticate(credentials: Credentials | null) { async authenticate(credentials: types.Credentials | null) {
this._credentials = credentials; this._credentials = credentials;
await this._updateProtocolRequestInterception(); await this._updateProtocolRequestInterception();
} }
@ -350,7 +350,7 @@ class InterceptableRequest implements network.RouteDelegate {
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postData, headersObject(headers)); this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postData, headersObject(headers));
} }
async continue(overrides: { method?: string; headers?: network.Headers; postData?: string } = {}) { async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
await this._client._sendMayFail('Fetch.continueRequest', { await this._client._sendMayFail('Fetch.continueRequest', {
@ -361,7 +361,7 @@ class InterceptableRequest implements network.RouteDelegate {
}); });
} }
async fulfill(response: network.FulfillResponse) { async fulfill(response: types.FulfillResponse) {
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null); const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null);
const responseHeaders: { [s: string]: string; } = {}; const responseHeaders: { [s: string]: string; } = {};
@ -423,8 +423,8 @@ function headersArray(headers: { [s: string]: string; }): { name: string; value:
return result; return result;
} }
function headersObject(headers: Protocol.Network.Headers): network.Headers { function headersObject(headers: Protocol.Network.Headers): types.Headers {
const result: network.Headers = {}; const result: types.Headers = {};
for (const key of Object.keys(headers)) for (const key of Object.keys(headers))
result[key.toLowerCase()] = headers[key]; result[key.toLowerCase()] = headers[key];
return result; return result;

View File

@ -20,10 +20,10 @@
import * as program from 'commander'; import * as program from 'commander';
import { Playwright } from '../server/playwright'; import { Playwright } from '../server/playwright';
import { BrowserType, LaunchOptions } from '../server/browserType'; import { BrowserType } from '../server/browserType';
import { DeviceDescriptors } from '../deviceDescriptors'; import { DeviceDescriptors } from '../deviceDescriptors';
import { BrowserContextOptions } from '../browserContext';
import { helper } from '../helper'; import { helper } from '../helper';
import { LaunchOptions, BrowserContextOptions } from '../types';
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']); const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);

View File

@ -19,7 +19,7 @@ import * as mime from 'mime';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import * as frames from './frames'; import * as frames from './frames';
import { assert, helper } from './helper'; import { assert, helper, assertMaxArguments } from './helper';
import InjectedScript from './injected/injectedScript'; import InjectedScript from './injected/injectedScript';
import * as injectedScriptSource from './generated/injectedScriptSource'; import * as injectedScriptSource from './generated/injectedScriptSource';
import * as debugScriptSource from './generated/debugScriptSource'; import * as debugScriptSource from './generated/debugScriptSource';
@ -55,6 +55,12 @@ export class FrameExecutionContext extends js.ExecutionContext {
}); });
} }
async evaluateExpressionInternal(expression: string, isFunction: boolean, ...args: any[]): Promise<any> {
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
return js.evaluateExpression(this, true /* returnByValue */, expression, isFunction, ...args);
});
}
async evaluateHandleInternal<R>(pageFunction: js.Func0<R>): Promise<js.SmartHandle<R>>; async evaluateHandleInternal<R>(pageFunction: js.Func0<R>): Promise<js.SmartHandle<R>>;
async evaluateHandleInternal<Arg, R>(pageFunction: js.Func1<Arg, R>, arg: Arg): Promise<js.SmartHandle<R>>; async evaluateHandleInternal<Arg, R>(pageFunction: js.Func1<Arg, R>, arg: Arg): Promise<js.SmartHandle<R>>;
async evaluateHandleInternal(pageFunction: never, ...args: never[]): Promise<any> { async evaluateHandleInternal(pageFunction: never, ...args: never[]): Promise<any> {
@ -63,6 +69,12 @@ export class FrameExecutionContext extends js.ExecutionContext {
}); });
} }
async evaluateExpressionHandleInternal(expression: string, isFunction: boolean, ...args: any[]): Promise<any> {
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
return js.evaluateExpression(this, false /* returnByValue */, expression, isFunction, ...args);
});
}
createHandle(remoteObject: js.RemoteObject): js.JSHandle { createHandle(remoteObject: js.RemoteObject): js.JSHandle {
if (this.frame._page._delegate.isElementHandle(remoteObject)) if (this.frame._page._delegate.isElementHandle(remoteObject))
return new ElementHandle(this, remoteObject.objectId!); return new ElementHandle(this, remoteObject.objectId!);
@ -618,10 +630,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>; async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
async $eval<R>(selector: string, pageFunction: js.FuncOn<Element, void, R>, arg?: any): Promise<R>; async $eval<R>(selector: string, pageFunction: js.FuncOn<Element, void, R>, arg?: any): Promise<R>;
async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> { async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3);
return this._$evalExpression(selector, String(pageFunction), typeof pageFunction === 'function', arg);
}
async _$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
const handle = await selectors._query(this._context.frame, selector, this); const handle = await selectors._query(this._context.frame, selector, this);
if (!handle) if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`); throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluate(pageFunction, arg); const result = await handle._evaluateExpression(expression, isFunction, true, arg);
handle.dispose(); handle.dispose();
return result; return result;
} }
@ -629,8 +646,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async $$eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>; async $$eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
async $$eval<R>(selector: string, pageFunction: js.FuncOn<Element[], void, R>, arg?: any): Promise<R>; async $$eval<R>(selector: string, pageFunction: js.FuncOn<Element[], void, R>, arg?: any): Promise<R>;
async $$eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> { async $$eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3);
return this._$$evalExpression(selector, String(pageFunction), typeof pageFunction === 'function', arg);
}
async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
const arrayHandle = await selectors._queryArray(this._context.frame, selector, this); const arrayHandle = await selectors._queryArray(this._context.frame, selector, this);
const result = await arrayHandle.evaluate(pageFunction, arg); const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg);
arrayHandle.dispose(); arrayHandle.dispose();
return result; return result;
} }

View File

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserBase, BrowserOptions } from '../browser'; import { BrowserBase, BrowserOptions, BrowserContextOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events'; import { Events } from '../events';
import { assert, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network'; import * as network from '../network';
@ -146,7 +146,7 @@ export class FFBrowserContext extends BrowserContextBase {
readonly _browser: FFBrowser; readonly _browser: FFBrowser;
readonly _browserContextId: string | null; readonly _browserContextId: string | null;
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) { constructor(browser: FFBrowser, browserContextId: string | null, options: types.BrowserContextOptions) {
super(browser, options); super(browser, options);
this._browser = browser; this._browser = browser;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
@ -237,17 +237,17 @@ export class FFBrowserContext extends BrowserContextBase {
throw pageOrError; throw pageOrError;
} }
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> { async _doCookies(urls: string[]): Promise<types.NetworkCookie[]> {
const { cookies } = await this._browser._connection.send('Browser.getCookies', { browserContextId: this._browserContextId || undefined }); const { cookies } = await this._browser._connection.send('Browser.getCookies', { browserContextId: this._browserContextId || undefined });
return network.filterCookies(cookies.map(c => { return network.filterCookies(cookies.map(c => {
const copy: any = { ... c }; const copy: any = { ... c };
delete copy.size; delete copy.size;
delete copy.session; delete copy.session;
return copy as network.NetworkCookie; return copy as types.NetworkCookie;
}), urls); }), urls);
} }
async addCookies(cookies: network.SetNetworkCookieParam[]) { async addCookies(cookies: types.SetNetworkCookieParam[]) {
await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId || undefined, cookies: network.rewriteCookies(cookies) }); await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId || undefined, cookies: network.rewriteCookies(cookies) });
} }
@ -282,7 +282,7 @@ export class FFBrowserContext extends BrowserContextBase {
await this._browser._connection.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId || undefined, geolocation }); await this._browser._connection.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId || undefined, geolocation });
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers); this._options.extraHTTPHeaders = network.verifyHeaders(headers);
const allHeaders = { ...this._options.extraHTTPHeaders }; const allHeaders = { ...this._options.extraHTTPHeaders };
if (this._options.locale) if (this._options.locale)
@ -300,8 +300,7 @@ export class FFBrowserContext extends BrowserContextBase {
await this._browser._connection.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId || undefined, credentials: httpCredentials }); await this._browser._connection.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId || undefined, credentials: httpCredentials });
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { async _doAddInitScript(source: string) {
const source = await helper.evaluationScript(script, arg);
await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source }); await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source });
} }

View File

@ -20,6 +20,7 @@ import { FFSession } from './ffConnection';
import { Page } from '../page'; import { Page } from '../page';
import * as network from '../network'; import * as network from '../network';
import * as frames from '../frames'; import * as frames from '../frames';
import * as types from '../types';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
export class FFNetworkManager { export class FFNetworkManager {
@ -74,7 +75,7 @@ export class FFNetworkManager {
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`);
return Buffer.from(response.base64body, 'base64'); return Buffer.from(response.base64body, 'base64');
}; };
const headers: network.Headers = {}; const headers: types.Headers = {};
for (const {name, value} of event.headers) for (const {name, value} of event.headers)
headers[name.toLowerCase()] = value; headers[name.toLowerCase()] = value;
const response = new network.Response(request.request, event.status, event.statusText, headers, getResponseBody); const response = new network.Response(request.request, event.status, event.statusText, headers, getResponseBody);
@ -149,7 +150,7 @@ class InterceptableRequest implements network.RouteDelegate {
this._id = payload.requestId; this._id = payload.requestId;
this._session = session; this._session = session;
const headers: network.Headers = {}; const headers: types.Headers = {};
for (const {name, value} of payload.headers) for (const {name, value} of payload.headers)
headers[name.toLowerCase()] = value; headers[name.toLowerCase()] = value;
@ -157,7 +158,7 @@ class InterceptableRequest implements network.RouteDelegate {
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, payload.postData || null, headers); payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, payload.postData || null, headers);
} }
async continue(overrides: { method?: string; headers?: network.Headers; postData?: string }) { async continue(overrides: { method?: string; headers?: types.Headers; postData?: string }) {
const { const {
method, method,
headers, headers,
@ -171,7 +172,7 @@ class InterceptableRequest implements network.RouteDelegate {
}); });
} }
async fulfill(response: network.FulfillResponse) { async fulfill(response: types.FulfillResponse) {
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null); const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null);
const responseHeaders: { [s: string]: string; } = {}; const responseHeaders: { [s: string]: string; } = {};
@ -201,7 +202,7 @@ class InterceptableRequest implements network.RouteDelegate {
} }
} }
export function headersArray(headers: network.Headers): Protocol.Network.HTTPHeader[] { export function headersArray(headers: types.Headers): Protocol.Network.HTTPHeader[] {
const result: Protocol.Network.HTTPHeader[] = []; const result: Protocol.Network.HTTPHeader[] = [];
for (const name in headers) { for (const name in headers) {
if (!Object.is(headers[name], undefined)) if (!Object.is(headers[name], undefined))

View File

@ -36,9 +36,6 @@ type ContextData = {
rerunnableTasks: Set<RerunnableTask<any>>; rerunnableTasks: Set<RerunnableTask<any>>;
}; };
export type GotoOptions = types.NavigateOptions & {
referer?: string,
};
export type GotoResult = { export type GotoResult = {
newDocumentId?: string, newDocumentId?: string,
}; };
@ -347,7 +344,7 @@ export class Frame {
return `${subject}.${method}`; return `${subject}.${method}`;
} }
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> { async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('goto')); const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('goto'));
abortProgressOnFrameDetach(progressController, this); abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => { return progressController.run(async progress => {
@ -439,6 +436,11 @@ export class Frame {
return context.evaluateHandleInternal(pageFunction, arg); return context.evaluateHandleInternal(pageFunction, arg);
} }
async _evaluateExpressionHandle(expression: string, isFunction: boolean, arg: any): Promise<any> {
const context = await this._mainContext();
return context.evaluateExpressionHandleInternal(expression, isFunction, arg);
}
async evaluate<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg): Promise<R>; async evaluate<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg): Promise<R>;
async evaluate<R>(pageFunction: js.Func1<void, R>, arg?: any): Promise<R>; async evaluate<R>(pageFunction: js.Func1<void, R>, arg?: any): Promise<R>;
async evaluate<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg): Promise<R> { async evaluate<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg): Promise<R> {
@ -447,6 +449,11 @@ export class Frame {
return context.evaluateInternal(pageFunction, arg); return context.evaluateInternal(pageFunction, arg);
} }
async _evaluateExpression(expression: string, isFunction: boolean, arg: any): Promise<any> {
const context = await this._mainContext();
return context.evaluateExpressionHandleInternal(expression, isFunction, arg);
}
async $(selector: string): Promise<dom.ElementHandle<Element> | null> { async $(selector: string): Promise<dom.ElementHandle<Element> | null> {
return selectors._query(this, selector); return selectors._query(this, selector);
} }
@ -493,10 +500,14 @@ export class Frame {
async $eval<R>(selector: string, pageFunction: js.FuncOn<Element, void, R>, arg?: any): Promise<R>; async $eval<R>(selector: string, pageFunction: js.FuncOn<Element, void, R>, arg?: any): Promise<R>;
async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> { async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3); assertMaxArguments(arguments.length, 3);
return this._$evalExpression(selector, String(pageFunction), typeof pageFunction === 'function', arg);
}
async _$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
const handle = await this.$(selector); const handle = await this.$(selector);
if (!handle) if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`); throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluate(pageFunction, arg); const result = await handle._evaluateExpression(expression, isFunction, true, arg);
handle.dispose(); handle.dispose();
return result; return result;
} }
@ -505,8 +516,12 @@ export class Frame {
async $$eval<R>(selector: string, pageFunction: js.FuncOn<Element[], void, R>, arg?: any): Promise<R>; async $$eval<R>(selector: string, pageFunction: js.FuncOn<Element[], void, R>, arg?: any): Promise<R>;
async $$eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> { async $$eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3); assertMaxArguments(arguments.length, 3);
return this._$$evalExpression(selector, String(pageFunction), typeof pageFunction === 'function', arg);
}
async _$$evalExpression(selector: string, expression: string, isFunction: boolean, arg: any): Promise<any> {
const arrayHandle = await selectors._queryArray(this, selector); const arrayHandle = await selectors._queryArray(this, selector);
const result = await arrayHandle.evaluate(pageFunction, arg); const result = await arrayHandle._evaluateExpression(expression, isFunction, true, arg);
arrayHandle.dispose(); arrayHandle.dispose();
return result; return result;
} }
@ -804,6 +819,11 @@ export class Frame {
async waitForFunction<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<js.SmartHandle<R>>; async waitForFunction<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<js.SmartHandle<R>>;
async waitForFunction<R>(pageFunction: js.Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<js.SmartHandle<R>>; async waitForFunction<R>(pageFunction: js.Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<js.SmartHandle<R>>;
async waitForFunction<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> { async waitForFunction<R, Arg>(pageFunction: js.Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
return this._waitForFunctionExpression(String(pageFunction), typeof pageFunction === 'function', arg, options);
}
async _waitForFunctionExpression<R>(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise<js.SmartHandle<R>> {
const { polling = 'raf' } = options; const { polling = 'raf' } = options;
if (helper.isString(polling)) if (helper.isString(polling))
assert(polling === 'raf', 'Unknown polling option: ' + polling); assert(polling === 'raf', 'Unknown polling option: ' + polling);
@ -811,7 +831,7 @@ export class Frame {
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
else else
throw new Error('Unknown polling option: ' + polling); throw new Error('Unknown polling option: ' + polling);
const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(arg)'; const predicateBody = isFunction ? 'return (' + expression + ')(arg)' : 'return (' + expression + ')';
const task = async (context: dom.FrameExecutionContext) => { const task = async (context: dom.FrameExecutionContext) => {
const injectedScript = await context.injectedScript(); const injectedScript = await context.injectedScript();
return context.evaluateHandleInternal(({ injectedScript, predicateBody, polling, arg }) => { return context.evaluateHandleInternal(({ injectedScript, predicateBody, polling, arg }) => {

View File

@ -42,8 +42,11 @@ class Helper {
assert(args.length === 0 || (args.length === 1 && args[0] === undefined), 'Cannot evaluate a string with arguments'); assert(args.length === 0 || (args.length === 1 && args[0] === undefined), 'Cannot evaluate a string with arguments');
return fun; return fun;
} }
return `(${fun})(${args.map(serializeArgument).join(',')})`; return Helper.evaluationStringForFunctionBody(String(fun), ...args);
}
static evaluationStringForFunctionBody(functionBody: string, ...args: any[]): string {
return `(${functionBody})(${args.map(serializeArgument).join(',')})`;
function serializeArgument(arg: any): string { function serializeArgument(arg: any): string {
if (Object.is(arg, undefined)) if (Object.is(arg, undefined))
return 'undefined'; return 'undefined';

View File

@ -18,7 +18,6 @@ import * as dom from './dom';
import * as utilityScriptSource from './generated/utilityScriptSource'; import * as utilityScriptSource from './generated/utilityScriptSource';
import * as sourceMap from './utils/sourceMap'; import * as sourceMap from './utils/sourceMap';
import { serializeAsCallArgument } from './common/utilityScriptSerializers'; import { serializeAsCallArgument } from './common/utilityScriptSerializers';
import { helper } from './helper';
import UtilityScript from './injected/utilityScript'; import UtilityScript from './injected/utilityScript';
type ObjectId = string; type ObjectId = string;
@ -106,6 +105,10 @@ export class JSHandle<T = any> {
return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg); return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg);
} }
_evaluateExpression(expression: string, isFunction: boolean, returnByValue: boolean, arg: any) {
return evaluateExpression(this._context, returnByValue, expression, isFunction, this, arg);
}
async getProperty(propertyName: string): Promise<JSHandle> { async getProperty(propertyName: string): Promise<JSHandle> {
const objectHandle = await this.evaluateHandle((object: any, propertyName) => { const objectHandle = await this.evaluateHandle((object: any, propertyName) => {
const result: any = {__proto__: null}; const result: any = {__proto__: null};
@ -147,16 +150,17 @@ export class JSHandle<T = any> {
} }
export async function evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { export async function evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
const utilityScript = await context.utilityScript(); return evaluateExpression(context, returnByValue, String(pageFunction), typeof pageFunction === 'function', ...args);
if (helper.isString(pageFunction)) { }
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)` + sourceMap.generateSourceUrl();
return context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, [returnByValue, sourceMap.ensureSourceUrl(pageFunction)], []);
}
if (typeof pageFunction !== 'function')
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
const originalText = pageFunction.toString(); export async function evaluateExpression(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean, ...args: any[]): Promise<any> {
let functionText = originalText; const utilityScript = await context.utilityScript();
if (!isFunction) {
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)` + sourceMap.generateSourceUrl();
return context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, [returnByValue, sourceMap.ensureSourceUrl(expression)], []);
}
let functionText = expression;
try { try {
new Function('(' + functionText + ')'); new Function('(' + functionText + ')');
} catch (e1) { } catch (e1) {
@ -203,7 +207,7 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean
utilityScriptObjectIds.push(handle._objectId!); utilityScriptObjectIds.push(handle._objectId!);
} }
functionText += await sourceMap.generateSourceMapUrl(originalText, functionText); functionText += await sourceMap.generateSourceMapUrl(expression, functionText);
// See UtilityScript for arguments. // See UtilityScript for arguments.
const utilityScriptValues = [returnByValue, functionText, args.length, ...args]; const utilityScriptValues = [returnByValue, functionText, args.length, ...args];

View File

@ -18,35 +18,11 @@ import * as fs from 'fs';
import * as mime from 'mime'; import * as mime from 'mime';
import * as util from 'util'; import * as util from 'util';
import * as frames from './frames'; import * as frames from './frames';
import * as types from './types';
import { assert, helper } from './helper'; import { assert, helper } from './helper';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
export type NetworkCookie = { export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] {
name: string,
value: string,
domain: string,
path: string,
expires: number,
httpOnly: boolean,
secure: boolean,
sameSite: 'Strict' | 'Lax' | 'None'
};
export type SetNetworkCookieParam = {
name: string,
value: string,
url?: string,
domain?: string,
path?: string,
expires?: number,
httpOnly?: boolean,
secure?: boolean,
sameSite?: 'Strict' | 'Lax' | 'None'
};
export function filterCookies(cookies: NetworkCookie[], urls: string | string[] = []): NetworkCookie[] {
if (!Array.isArray(urls))
urls = [ urls ];
const parsedURLs = urls.map(s => new URL(s)); const parsedURLs = urls.map(s => new URL(s));
// Chromiums's cookies are missing sameSite when it is 'None' // Chromiums's cookies are missing sameSite when it is 'None'
return cookies.filter(c => { return cookies.filter(c => {
@ -65,7 +41,7 @@ export function filterCookies(cookies: NetworkCookie[], urls: string | string[]
}); });
} }
export function rewriteCookies(cookies: SetNetworkCookieParam[]): SetNetworkCookieParam[] { export function rewriteCookies(cookies: types.SetNetworkCookieParam[]): types.SetNetworkCookieParam[] {
return cookies.map(c => { return cookies.map(c => {
assert(c.name, 'Cookie should have a name'); assert(c.name, 'Cookie should have a name');
assert(c.value, 'Cookie should have a value'); assert(c.value, 'Cookie should have a value');
@ -93,8 +69,6 @@ function stripFragmentFromUrl(url: string): string {
return parsed.href; return parsed.href;
} }
export type Headers = { [key: string]: string };
export class Request { export class Request {
readonly _routeDelegate: RouteDelegate | null; readonly _routeDelegate: RouteDelegate | null;
private _response: Response | null = null; private _response: Response | null = null;
@ -107,13 +81,13 @@ export class Request {
private _resourceType: string; private _resourceType: string;
private _method: string; private _method: string;
private _postData: string | null; private _postData: string | null;
private _headers: Headers; private _headers: types.Headers;
private _frame: frames.Frame; private _frame: frames.Frame;
private _waitForResponsePromise: Promise<Response | null>; private _waitForResponsePromise: Promise<Response | null>;
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {}; private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
url: string, resourceType: string, method: string, postData: string | null, headers: Headers) { url: string, resourceType: string, method: string, postData: string | null, headers: types.Headers) {
assert(!url.startsWith('data:'), 'Data urls should not fire requests'); assert(!url.startsWith('data:'), 'Data urls should not fire requests');
assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects'); assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects');
this._routeDelegate = routeDelegate; this._routeDelegate = routeDelegate;
@ -243,7 +217,7 @@ export class Route {
await this._delegate.abort(errorCode); await this._delegate.abort(errorCode);
} }
async fulfill(response: FulfillResponse & { path?: string }) { async fulfill(response: types.FulfillResponse & { path?: string }) {
assert(!this._handled, 'Route is already handled!'); assert(!this._handled, 'Route is already handled!');
this._handled = true; this._handled = true;
if (response.path) { if (response.path) {
@ -257,7 +231,7 @@ export class Route {
await this._delegate.fulfill(response); await this._delegate.fulfill(response);
} }
async continue(overrides: { method?: string; headers?: Headers; postData?: string } = {}) { async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) {
assert(!this._handled, 'Route is already handled!'); assert(!this._handled, 'Route is already handled!');
await this._delegate.continue(overrides); await this._delegate.continue(overrides);
} }
@ -275,10 +249,10 @@ export class Response {
private _status: number; private _status: number;
private _statusText: string; private _statusText: string;
private _url: string; private _url: string;
private _headers: Headers; private _headers: types.Headers;
private _getResponseBodyCallback: GetResponseBodyCallback; private _getResponseBodyCallback: GetResponseBodyCallback;
constructor(request: Request, status: number, statusText: string, headers: Headers, getResponseBodyCallback: GetResponseBodyCallback) { constructor(request: Request, status: number, statusText: string, headers: types.Headers, getResponseBodyCallback: GetResponseBodyCallback) {
this._request = request; this._request = request;
this._status = status; this._status = status;
this._statusText = statusText; this._statusText = statusText;
@ -349,17 +323,10 @@ export class Response {
} }
} }
export type FulfillResponse = {
status?: number,
headers?: Headers,
contentType?: string,
body?: string | Buffer,
};
export interface RouteDelegate { export interface RouteDelegate {
abort(errorCode: string): Promise<void>; abort(errorCode: string): Promise<void>;
fulfill(response: FulfillResponse): Promise<void>; fulfill(response: types.FulfillResponse): Promise<void>;
continue(overrides: { method?: string; headers?: Headers; postData?: string; }): Promise<void>; continue(overrides: { method?: string; headers?: types.Headers; postData?: string; }): Promise<void>;
} }
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
@ -429,8 +396,8 @@ export const STATUS_TEXTS: { [status: string]: string } = {
'511': 'Network Authentication Required', '511': 'Network Authentication Required',
}; };
export function verifyHeaders(headers: Headers): Headers { export function verifyHeaders(headers: types.Headers): types.Headers {
const result: Headers = {}; const result: types.Headers = {};
for (const key of Object.keys(headers)) { for (const key of Object.keys(headers)) {
const value = headers[key]; const value = headers[key];
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`); assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
@ -439,7 +406,7 @@ export function verifyHeaders(headers: Headers): Headers {
return result; return result;
} }
export function mergeHeaders(headers: (Headers | undefined | null)[]): Headers { export function mergeHeaders(headers: (types.Headers | undefined | null)[]): types.Headers {
const lowerCaseToValue = new Map<string, string>(); const lowerCaseToValue = new Map<string, string>();
const lowerCaseToOriginalCase = new Map<string, string>(); const lowerCaseToOriginalCase = new Map<string, string>();
for (const h of headers) { for (const h of headers) {
@ -451,7 +418,7 @@ export function mergeHeaders(headers: (Headers | undefined | null)[]): Headers {
lowerCaseToValue.set(lower, h[key]); lowerCaseToValue.set(lower, h[key]);
} }
} }
const result: Headers = {}; const result: types.Headers = {};
for (const [lower, value] of lowerCaseToValue) for (const [lower, value] of lowerCaseToValue)
result[lowerCaseToOriginalCase.get(lower)!] = value; result[lowerCaseToOriginalCase.get(lower)!] = value;
return result; return result;

View File

@ -84,7 +84,7 @@ type PageState = {
viewportSize: types.Size | null; viewportSize: types.Size | null;
mediaType: types.MediaType | null; mediaType: types.MediaType | null;
colorScheme: types.ColorScheme | null; colorScheme: types.ColorScheme | null;
extraHTTPHeaders: network.Headers | null; extraHTTPHeaders: types.Headers | null;
}; };
export class Page extends EventEmitter { export class Page extends EventEmitter {
@ -265,7 +265,7 @@ export class Page extends EventEmitter {
await this._delegate.exposeBinding(binding); await this._delegate.exposeBinding(binding);
} }
setExtraHTTPHeaders(headers: network.Headers) { setExtraHTTPHeaders(headers: types.Headers) {
this._state.extraHTTPHeaders = network.verifyHeaders(headers); this._state.extraHTTPHeaders = network.verifyHeaders(headers);
return this._delegate.updateExtraHTTPHeaders(); return this._delegate.updateExtraHTTPHeaders();
} }
@ -295,7 +295,7 @@ export class Page extends EventEmitter {
return this._attributeToPage(() => this.mainFrame().setContent(html, options)); return this._attributeToPage(() => this.mainFrame().setContent(html, options));
} }
async goto(url: string, options?: frames.GotoOptions): Promise<network.Response | null> { async goto(url: string, options?: types.GotoOptions): Promise<network.Response | null> {
return this._attributeToPage(() => this.mainFrame().goto(url, options)); return this._attributeToPage(() => this.mainFrame().goto(url, options));
} }
@ -386,6 +386,10 @@ export class Page extends EventEmitter {
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
const source = await helper.evaluationScript(script, arg); const source = await helper.evaluationScript(script, arg);
await this._addInitScriptExpression(source);
}
async _addInitScriptExpression(source: string) {
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
await this._delegate.evaluateOnNewDocument(source); await this._delegate.evaluateOnNewDocument(source);
} }
@ -445,11 +449,11 @@ export class Page extends EventEmitter {
return this._attributeToPage(() => this.mainFrame().title()); return this._attributeToPage(() => this.mainFrame().title());
} }
async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) { async close(options?: { runBeforeUnload?: boolean }) {
if (this._closed) if (this._closed)
return; return;
assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.'); assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.');
const runBeforeUnload = !!options.runBeforeUnload; const runBeforeUnload = !!options && !!options.runBeforeUnload;
await this._delegate.closePage(runBeforeUnload); await this._delegate.closePage(runBeforeUnload);
if (!runBeforeUnload) if (!runBeforeUnload)
await this._closedPromise; await this._closedPromise;

View File

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import { BrowserContext, PersistentContextOptions, verifyProxySettings, validateBrowserContextOptions } from '../browserContext'; import { BrowserContext, verifyProxySettings, validateBrowserContextOptions } from '../browserContext';
import { BrowserServer } from './browserServer'; import { BrowserServer } from './browserServer';
import * as browserPaths from '../install/browserPaths'; import * as browserPaths from '../install/browserPaths';
import { Loggers, Logger } from '../logger'; import { Loggers, Logger } from '../logger';
@ -34,41 +34,21 @@ import { TimeoutSettings } from '../timeoutSettings';
import { WebSocketServer } from './webSocketServer'; import { WebSocketServer } from './webSocketServer';
import { LoggerSink } from '../loggerSink'; import { LoggerSink } from '../loggerSink';
export type FirefoxUserPrefsOptions = { type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
firefoxUserPrefs?: { [key: string]: string | number | boolean }, type LaunchOptions = types.LaunchOptions & { logger?: LoggerSink };
}; type ConnectOptions = types.ConnectOptions & { logger?: LoggerSink };
export type LaunchOptionsBase = {
executablePath?: string,
args?: string[],
ignoreDefaultArgs?: boolean | string[],
handleSIGINT?: boolean,
handleSIGTERM?: boolean,
handleSIGHUP?: boolean,
timeout?: number,
logger?: LoggerSink,
env?: Env,
headless?: boolean,
devtools?: boolean,
proxy?: types.ProxySettings,
downloadsPath?: string,
};
type ConnectOptions = { export type LaunchNonPersistentOptions = LaunchOptions & FirefoxPrefsOptions;
wsEndpoint: string, type LaunchPersistentOptions = LaunchOptions & types.BrowserContextOptions;
slowMo?: number, type LaunchServerOptions = types.LaunchServerOptions & { logger?: LoggerSink } & FirefoxPrefsOptions;
logger?: LoggerSink,
timeout?: number,
};
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
type LaunchServerOptions = LaunchOptionsBase & { port?: number };
export interface BrowserType { export interface BrowserType {
executablePath(): string; executablePath(): string;
name(): string; name(): string;
launch(options?: LaunchOptions & FirefoxUserPrefsOptions): Promise<Browser>; launch(options?: LaunchNonPersistentOptions): Promise<Browser>;
launchServer(options?: LaunchServerOptions & FirefoxUserPrefsOptions): Promise<BrowserServer>; launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
launchPersistentContext(userDataDir: string, options?: LaunchOptions & PersistentContextOptions): Promise<BrowserContext>; launchPersistentContext(userDataDir: string, options?: LaunchPersistentOptions): Promise<BrowserContext>;
connect(options: ConnectOptions): Promise<Browser>; connect(options: ConnectOptions): Promise<Browser>;
} }
@ -102,7 +82,7 @@ export abstract class BrowserTypeBase implements BrowserType {
return this._name; return this._name;
} }
async launch(options: LaunchOptions = {}): Promise<Browser> { async launch(options: LaunchNonPersistentOptions = {}): Promise<Browser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = validateLaunchOptions(options); options = validateLaunchOptions(options);
@ -111,7 +91,7 @@ export abstract class BrowserTypeBase implements BrowserType {
return browser; return browser;
} }
async launchPersistentContext(userDataDir: string, options: LaunchOptions & PersistentContextOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchPersistentOptions = {}): Promise<BrowserContext> {
assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = validateLaunchOptions(options); options = validateLaunchOptions(options);
const persistent = validateBrowserContextOptions(options); const persistent = validateBrowserContextOptions(options);
@ -120,7 +100,7 @@ export abstract class BrowserTypeBase implements BrowserType {
return browser._defaultContext!; return browser._defaultContext!;
} }
async _innerLaunch(progress: Progress, options: LaunchOptions, logger: Loggers, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> { async _innerLaunch(progress: Progress, options: LaunchOptions, logger: Loggers, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined; options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined;
const { browserServer, downloadsPath, transport } = await this._launchServer(progress, options, !!persistent, logger, userDataDir); const { browserServer, downloadsPath, transport } = await this._launchServer(progress, options, !!persistent, logger, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
@ -246,7 +226,7 @@ export abstract class BrowserTypeBase implements BrowserType {
return { browserServer, downloadsPath, transport }; return { browserServer, downloadsPath, transport };
} }
abstract _defaultArgs(options: LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[]; abstract _defaultArgs(options: types.LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[];
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>; abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
abstract _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer; abstract _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer;
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env; abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
@ -260,7 +240,7 @@ function copyTestHooks(from: object, to: object) {
} }
} }
function validateLaunchOptions<Options extends LaunchOptionsBase>(options: Options): Options { function validateLaunchOptions<Options extends types.LaunchOptionsBase>(options: Options): Options {
const { devtools = false, headless = !helper.isDebugMode() && !devtools } = options; const { devtools = false, headless = !helper.isDebugMode() && !devtools } = options;
return { ...options, devtools, headless }; return { ...options, devtools, headless };
} }

View File

@ -21,13 +21,14 @@ import { CRBrowser } from '../chromium/crBrowser';
import * as ws from 'ws'; import * as ws from 'ws';
import { Env } from './processLauncher'; import { Env } from './processLauncher';
import { kBrowserCloseMessageId } from '../chromium/crConnection'; import { kBrowserCloseMessageId } from '../chromium/crConnection';
import { LaunchOptionsBase, BrowserTypeBase } from './browserType'; import { BrowserTypeBase } from './browserType';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { CRDevTools } from '../chromium/crDevTools'; import { CRDevTools } from '../chromium/crDevTools';
import { BrowserOptions } from '../browser'; import { BrowserOptions } from '../browser';
import { WebSocketServer } from './webSocketServer'; import { WebSocketServer } from './webSocketServer';
import { LaunchOptionsBase } from '../types';
export class Chromium extends BrowserTypeBase { export class Chromium extends BrowserTypeBase {
private _devtools: CRDevTools | undefined; private _devtools: CRDevTools | undefined;

View File

@ -21,7 +21,7 @@ import * as path from 'path';
import * as ws from 'ws'; import * as ws from 'ws';
import { FFBrowser } from '../firefox/ffBrowser'; import { FFBrowser } from '../firefox/ffBrowser';
import { kBrowserCloseMessageId } from '../firefox/ffConnection'; import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import { LaunchOptionsBase, BrowserTypeBase, FirefoxUserPrefsOptions } from './browserType'; import { BrowserTypeBase, LaunchNonPersistentOptions } from './browserType';
import { Env } from './processLauncher'; import { Env } from './processLauncher';
import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport'; import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport';
import { Logger } from '../logger'; import { Logger } from '../logger';
@ -56,7 +56,7 @@ export class Firefox extends BrowserTypeBase {
return startWebSocketServer(transport, logger, port); return startWebSocketServer(transport, logger, port);
} }
_defaultArgs(options: LaunchOptionsBase & FirefoxUserPrefsOptions, isPersistent: boolean, userDataDir: string): string[] { _defaultArgs(options: LaunchNonPersistentOptions, isPersistent: boolean, userDataDir: string): string[] {
const { args = [], proxy, devtools, headless } = options; const { args = [], proxy, devtools, headless } = options;
if (devtools) if (devtools)
console.warn('devtools parameter is not supported as a launch argument in Firefox. You can launch the devtools window manually.'); console.warn('devtools parameter is not supported as a launch argument in Firefox. You can launch the devtools window manually.');

View File

@ -19,7 +19,7 @@ import { WKBrowser } from '../webkit/wkBrowser';
import { Env } from './processLauncher'; import { Env } from './processLauncher';
import * as path from 'path'; import * as path from 'path';
import { kBrowserCloseMessageId } from '../webkit/wkConnection'; import { kBrowserCloseMessageId } from '../webkit/wkConnection';
import { LaunchOptionsBase, BrowserTypeBase } from './browserType'; import { BrowserTypeBase } from './browserType';
import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport'; import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport';
import * as ws from 'ws'; import * as ws from 'ws';
import { Logger } from '../logger'; import { Logger } from '../logger';
@ -27,6 +27,7 @@ import { BrowserOptions } from '../browser';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { WebSocketServer } from './webSocketServer'; import { WebSocketServer } from './webSocketServer';
import { assert } from '../helper'; import { assert } from '../helper';
import { LaunchOptionsBase } from '../types';
export class WebKit extends BrowserTypeBase { export class WebKit extends BrowserTypeBase {
constructor(packagePath: string, browser: BrowserDescriptor) { constructor(packagePath: string, browser: BrowserDescriptor) {

View File

@ -180,3 +180,85 @@ export type MouseMultiClickOptions = PointerActionOptions & {
}; };
export type World = 'main' | 'utility'; export type World = 'main' | 'utility';
export type Headers = { [key: string]: string };
export type GotoOptions = NavigateOptions & {
referer?: string,
};
export type FulfillResponse = {
status?: number,
headers?: Headers,
contentType?: string,
body?: string | Buffer,
};
export type NetworkCookie = {
name: string,
value: string,
domain: string,
path: string,
expires: number,
httpOnly: boolean,
secure: boolean,
sameSite: 'Strict' | 'Lax' | 'None'
};
export type SetNetworkCookieParam = {
name: string,
value: string,
url?: string,
domain?: string,
path?: string,
expires?: number,
httpOnly?: boolean,
secure?: boolean,
sameSite?: 'Strict' | 'Lax' | 'None'
};
export type BrowserContextOptions = {
viewport?: Size | null,
ignoreHTTPSErrors?: boolean,
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
userAgent?: string,
locale?: string,
timezoneId?: string,
geolocation?: Geolocation,
permissions?: string[],
extraHTTPHeaders?: Headers,
offline?: boolean,
httpCredentials?: Credentials,
deviceScaleFactor?: number,
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: ColorScheme,
acceptDownloads?: boolean,
};
export type Env = {[key: string]: string | number | boolean | undefined};
export type LaunchOptionsBase = {
executablePath?: string,
args?: string[],
ignoreDefaultArgs?: boolean | string[],
handleSIGINT?: boolean,
handleSIGTERM?: boolean,
handleSIGHUP?: boolean,
timeout?: number,
env?: Env,
headless?: boolean,
devtools?: boolean,
proxy?: ProxySettings,
downloadsPath?: string,
};
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
export type LaunchServerOptions = LaunchOptionsBase & { port?: number };
export type ConnectOptions = {
wsEndpoint: string,
slowMo?: number,
timeout?: number,
};

View File

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { BrowserBase, BrowserOptions } from '../browser'; import { BrowserBase, BrowserOptions, BrowserContextOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events'; import { Events } from '../events';
import { helper, RegisteredListener, assert } from '../helper'; import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network'; import * as network from '../network';
@ -202,7 +202,7 @@ export class WKBrowserContext extends BrowserContextBase {
readonly _browserContextId: string | undefined; readonly _browserContextId: string | undefined;
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) { constructor(browser: WKBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) {
super(browser, options); super(browser, options);
this._browser = browser; this._browser = browser;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
@ -257,17 +257,17 @@ export class WKBrowserContext extends BrowserContextBase {
throw result; throw result;
} }
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> { async _doCookies(urls: string[]): Promise<types.NetworkCookie[]> {
const { cookies } = await this._browser._browserSession.send('Playwright.getAllCookies', { browserContextId: this._browserContextId }); const { cookies } = await this._browser._browserSession.send('Playwright.getAllCookies', { browserContextId: this._browserContextId });
return network.filterCookies(cookies.map((c: network.NetworkCookie) => { return network.filterCookies(cookies.map((c: types.NetworkCookie) => {
const copy: any = { ... c }; const copy: any = { ... c };
copy.expires = c.expires === -1 ? -1 : c.expires / 1000; copy.expires = c.expires === -1 ? -1 : c.expires / 1000;
delete copy.session; delete copy.session;
return copy as network.NetworkCookie; return copy as types.NetworkCookie;
}), urls); }), urls);
} }
async addCookies(cookies: network.SetNetworkCookieParam[]) { async addCookies(cookies: types.SetNetworkCookieParam[]) {
const cc = network.rewriteCookies(cookies).map(c => ({ const cc = network.rewriteCookies(cookies).map(c => ({
...c, ...c,
session: c.expires === -1 || c.expires === undefined, session: c.expires === -1 || c.expires === undefined,
@ -296,7 +296,7 @@ export class WKBrowserContext extends BrowserContextBase {
await this._browser._browserSession.send('Playwright.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload }); await this._browser._browserSession.send('Playwright.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload });
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers); this._options.extraHTTPHeaders = network.verifyHeaders(headers);
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as WKPage).updateExtraHTTPHeaders(); await (page._delegate as WKPage).updateExtraHTTPHeaders();
@ -314,8 +314,7 @@ export class WKBrowserContext extends BrowserContextBase {
await (page._delegate as WKPage).updateHttpCredentials(); await (page._delegate as WKPage).updateHttpCredentials();
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { async _doAddInitScript(source: string) {
const source = await helper.evaluationScript(script, arg);
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript(); await (page._delegate as WKPage)._updateBootstrapScript();

View File

@ -18,6 +18,7 @@
import * as frames from '../frames'; import * as frames from '../frames';
import { assert, helper } from '../helper'; import { assert, helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import * as types from '../types';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
@ -65,7 +66,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
await this._session.sendMayFail('Network.interceptRequestWithError', { requestId: this._requestId, errorType }); await this._session.sendMayFail('Network.interceptRequestWithError', { requestId: this._requestId, errorType });
} }
async fulfill(response: network.FulfillResponse) { async fulfill(response: types.FulfillResponse) {
await this._interceptedPromise; await this._interceptedPromise;
const base64Encoded = !!response.body && !helper.isString(response.body); const base64Encoded = !!response.body && !helper.isString(response.body);
@ -101,7 +102,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
}); });
} }
async continue(overrides: { method?: string; headers?: network.Headers; postData?: string }) { async continue(overrides: { method?: string; headers?: types.Headers; postData?: string }) {
await this._interceptedPromise; await this._interceptedPromise;
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
@ -122,8 +123,8 @@ export class WKInterceptableRequest implements network.RouteDelegate {
} }
} }
function headersObject(headers: Protocol.Network.Headers): network.Headers { function headersObject(headers: Protocol.Network.Headers): types.Headers {
const result: network.Headers = {}; const result: types.Headers = {};
for (const key of Object.keys(headers)) for (const key of Object.keys(headers))
result[key.toLowerCase()] = headers[key]; result[key.toLowerCase()] = headers[key];
return result; return result;

View File

@ -552,7 +552,7 @@ export class WKPage implements PageDelegate {
await this._updateState('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }); await this._updateState('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() });
} }
_calculateExtraHTTPHeaders(): network.Headers { _calculateExtraHTTPHeaders(): types.Headers {
const headers = network.mergeHeaders([ const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders, this._browserContext._options.extraHTTPHeaders,
this._page._state.extraHTTPHeaders this._page._state.extraHTTPHeaders