mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(rpc): ensure that error stack traces point to the user code (#2961)
This also adds more "_wrapApiCall" calls for correct logs and stack traces.
This commit is contained in:
parent
b890569afc
commit
056f0e290d
@ -140,7 +140,7 @@ const colorMap = new Map<string, number>([
|
|||||||
['reset', 0],
|
['reset', 0],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
class DebugLoggerSink {
|
export class DebugLoggerSink {
|
||||||
private _debuggers = new Map<string, debug.IDebugger>();
|
private _debuggers = new Map<string, debug.IDebugger>();
|
||||||
|
|
||||||
isEnabled(name: string, severity: LoggerSeverity): boolean {
|
isEnabled(name: string, severity: LoggerSeverity): boolean {
|
||||||
|
@ -54,6 +54,7 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
|||||||
async newContext(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<BrowserContext> {
|
async newContext(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<BrowserContext> {
|
||||||
const logger = options.logger;
|
const logger = options.logger;
|
||||||
options = { ...options, logger: undefined };
|
options = { ...options, logger: undefined };
|
||||||
|
return this._wrapApiCall('browser.newContext', async () => {
|
||||||
const contextOptions: BrowserContextOptions = {
|
const contextOptions: BrowserContextOptions = {
|
||||||
...options,
|
...options,
|
||||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||||
@ -62,6 +63,7 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
|||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._logger = logger || this._logger;
|
context._logger = logger || this._logger;
|
||||||
return context;
|
return context;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
contexts(): BrowserContext[] {
|
contexts(): BrowserContext[] {
|
||||||
@ -81,10 +83,12 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
return this._wrapApiCall('browser.close', async () => {
|
||||||
if (!this._isClosedOrClosing) {
|
if (!this._isClosedOrClosing) {
|
||||||
this._isClosedOrClosing = true;
|
this._isClosedOrClosing = true;
|
||||||
await this._channel.close();
|
await this._channel.close();
|
||||||
}
|
}
|
||||||
await this._closedPromise;
|
await this._closedPromise;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,9 +98,11 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
}
|
}
|
||||||
|
|
||||||
async newPage(): Promise<Page> {
|
async newPage(): Promise<Page> {
|
||||||
|
return this._wrapApiCall('browserContext.newPage', async () => {
|
||||||
if (this._ownerPage)
|
if (this._ownerPage)
|
||||||
throw new Error('Please use browser.newContext()');
|
throw new Error('Please use browser.newContext()');
|
||||||
return Page.from((await this._channel.newPage()).page);
|
return Page.from((await this._channel.newPage()).page);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
|
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
|
||||||
@ -108,47 +110,68 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
urls = [];
|
urls = [];
|
||||||
if (urls && typeof urls === 'string')
|
if (urls && typeof urls === 'string')
|
||||||
urls = [ urls ];
|
urls = [ urls ];
|
||||||
|
return this._wrapApiCall('browserContext.cookies', async () => {
|
||||||
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
|
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void> {
|
async addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.addCookies', async () => {
|
||||||
await this._channel.addCookies({ cookies });
|
await this._channel.addCookies({ cookies });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearCookies(): Promise<void> {
|
async clearCookies(): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.clearCookies', async () => {
|
||||||
await this._channel.clearCookies();
|
await this._channel.clearCookies();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void> {
|
async grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.grantPermissions', async () => {
|
||||||
await this._channel.grantPermissions({ permissions, ...options });
|
await this._channel.grantPermissions({ permissions, ...options });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearPermissions(): Promise<void> {
|
async clearPermissions(): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.clearPermissions', async () => {
|
||||||
await this._channel.clearPermissions();
|
await this._channel.clearPermissions();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
|
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.setGeolocation', async () => {
|
||||||
await this._channel.setGeolocation({ geolocation });
|
await this._channel.setGeolocation({ geolocation });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
|
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async () => {
|
||||||
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setOffline(offline: boolean): Promise<void> {
|
async setOffline(offline: boolean): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.setOffline', async () => {
|
||||||
await this._channel.setOffline({ offline });
|
await this._channel.setOffline({ offline });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void> {
|
async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.setHTTPCredentials', async () => {
|
||||||
await this._channel.setHTTPCredentials({ httpCredentials });
|
await this._channel.setHTTPCredentials({ httpCredentials });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.addInitScript', async () => {
|
||||||
const source = await helper.evaluationScript(script, arg);
|
const source = await helper.evaluationScript(script, arg);
|
||||||
await this._channel.addInitScript({ source });
|
await this._channel.addInitScript({ source });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise<void> {
|
async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.exposeBinding', async () => {
|
||||||
for (const page of this.pages()) {
|
for (const page of this.pages()) {
|
||||||
if (page._bindings.has(name))
|
if (page._bindings.has(name))
|
||||||
throw new Error(`Function "${name}" has been already registered in one of the pages`);
|
throw new Error(`Function "${name}" has been already registered in one of the pages`);
|
||||||
@ -157,6 +180,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
throw new Error(`Function "${name}" has been already registered`);
|
throw new Error(`Function "${name}" has been already registered`);
|
||||||
this._bindings.set(name, binding);
|
this._bindings.set(name, binding);
|
||||||
await this._channel.exposeBinding({ name });
|
await this._channel.exposeBinding({ name });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
|
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
|
||||||
@ -164,15 +188,19 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
}
|
}
|
||||||
|
|
||||||
async route(url: types.URLMatch, handler: network.RouteHandler): Promise<void> {
|
async route(url: types.URLMatch, handler: network.RouteHandler): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.route', async () => {
|
||||||
this._routes.push({ url, handler });
|
this._routes.push({ url, handler });
|
||||||
if (this._routes.length === 1)
|
if (this._routes.length === 1)
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void> {
|
async unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.unroute', async () => {
|
||||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||||
if (this._routes.length === 0)
|
if (this._routes.length === 0)
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||||
@ -196,10 +224,12 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserContext.close', async () => {
|
||||||
if (!this._isClosedOrClosing) {
|
if (!this._isClosedOrClosing) {
|
||||||
this._isClosedOrClosing = true;
|
this._isClosedOrClosing = true;
|
||||||
await this._channel.close();
|
await this._channel.close();
|
||||||
}
|
}
|
||||||
await this._closedPromise;
|
await this._closedPromise;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,15 @@ export class BrowserServer extends ChannelOwner<BrowserServerChannel, BrowserSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
async kill(): Promise<void> {
|
async kill(): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserServer.kill', async () => {
|
||||||
await this._channel.kill();
|
await this._channel.kill();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
return this._wrapApiCall('browserServer.close', async () => {
|
||||||
await this._channel.close();
|
await this._channel.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkLeaks() {}
|
_checkLeaks() {}
|
||||||
|
@ -46,11 +46,15 @@ export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitia
|
|||||||
method: T,
|
method: T,
|
||||||
params?: Protocol.CommandParameters[T]
|
params?: Protocol.CommandParameters[T]
|
||||||
): Promise<Protocol.CommandReturnValues[T]> {
|
): Promise<Protocol.CommandReturnValues[T]> {
|
||||||
|
return this._wrapApiCall('cdpSession.send', async () => {
|
||||||
const result = await this._channel.send({ method, params });
|
const result = await this._channel.send({ method, params });
|
||||||
return result.result as Protocol.CommandReturnValues[T];
|
return result.result as Protocol.CommandReturnValues[T];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async detach() {
|
async detach() {
|
||||||
|
return this._wrapApiCall('cdpSession.detach', async () => {
|
||||||
return this._channel.detach();
|
return this._channel.detach();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import { Channel } from '../channels';
|
|||||||
import { Connection } from './connection';
|
import { Connection } from './connection';
|
||||||
import { assert } from '../../helper';
|
import { assert } from '../../helper';
|
||||||
import { LoggerSink } from '../../loggerSink';
|
import { LoggerSink } from '../../loggerSink';
|
||||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
import { DebugLoggerSink } from '../../logger';
|
||||||
|
|
||||||
export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}> extends EventEmitter {
|
export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}> extends EventEmitter {
|
||||||
private _connection: Connection;
|
private _connection: Connection;
|
||||||
@ -99,19 +99,30 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async _wrapApiCall<T>(apiName: string, func: () => Promise<T>, logger?: LoggerSink): Promise<T> {
|
protected async _wrapApiCall<T>(apiName: string, func: () => Promise<T>, logger?: LoggerSink): Promise<T> {
|
||||||
|
const stackObject: any = {};
|
||||||
|
Error.captureStackTrace(stackObject);
|
||||||
|
const stack = stackObject.stack.startsWith('Error') ? stackObject.stack.substring(5) : stackObject.stack;
|
||||||
logger = logger || this._logger;
|
logger = logger || this._logger;
|
||||||
try {
|
try {
|
||||||
if (logger && logger.isEnabled('api', 'info'))
|
logApiCall(logger, `=> ${apiName} started`);
|
||||||
logger.log('api', 'info', `=> ${apiName} started`, [], { color: 'cyan' });
|
|
||||||
const result = await func();
|
const result = await func();
|
||||||
if (logger && logger.isEnabled('api', 'info'))
|
logApiCall(logger, `<= ${apiName} succeeded`);
|
||||||
logger.log('api', 'info', `=> ${apiName} succeeded`, [], { color: 'cyan' });
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (logger && logger.isEnabled('api', 'info'))
|
logApiCall(logger, `<= ${apiName} failed`);
|
||||||
logger.log('api', 'info', `=> ${apiName} failed`, [], { color: 'cyan' });
|
// TODO: we could probably save "e.stack" in some log-heavy mode
|
||||||
rewriteErrorMessage(e, `${apiName}: ` + e.message);
|
// because it gives some insights into the server part.
|
||||||
|
e.message = `${apiName}: ` + e.message;
|
||||||
|
e.stack = e.message + stack;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debugLogger = new DebugLoggerSink();
|
||||||
|
function logApiCall(logger: LoggerSink | undefined, message: string) {
|
||||||
|
if (logger && logger.isEnabled('api', 'info'))
|
||||||
|
logger.log('api', 'info', message, [], { color: 'cyan' });
|
||||||
|
if (debugLogger.isEnabled('api', 'info'))
|
||||||
|
debugLogger.log('api', 'info', message, [], { color: 'cyan' });
|
||||||
|
}
|
||||||
|
@ -20,14 +20,20 @@ import { Browser } from './browser';
|
|||||||
|
|
||||||
export class ChromiumBrowser extends Browser {
|
export class ChromiumBrowser extends Browser {
|
||||||
async newBrowserCDPSession(): Promise<CDPSession> {
|
async newBrowserCDPSession(): Promise<CDPSession> {
|
||||||
|
return this._wrapApiCall('chromiumBrowser.newBrowserCDPSession', async () => {
|
||||||
return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session);
|
return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
|
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
|
||||||
|
return this._wrapApiCall('chromiumBrowser.startTracing', async () => {
|
||||||
await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined });
|
await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopTracing(): Promise<Buffer> {
|
async stopTracing(): Promise<Buffer> {
|
||||||
|
return this._wrapApiCall('chromiumBrowser.stopTracing', async () => {
|
||||||
return Buffer.from((await this._channel.crStopTracing()).binary, 'base64');
|
return Buffer.from((await this._channel.crStopTracing()).binary, 'base64');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,9 @@ export class ChromiumBrowserContext extends BrowserContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async newCDPSession(page: Page): Promise<CDPSession> {
|
async newCDPSession(page: Page): Promise<CDPSession> {
|
||||||
|
return this._wrapApiCall('chromiumBrowserContext.newCDPSession', async () => {
|
||||||
const result = await this._channel.crNewCDPSession({ page: page._channel });
|
const result = await this._channel.crNewCDPSession({ page: page._channel });
|
||||||
return CDPSession.from(result.session);
|
return CDPSession.from(result.session);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,14 @@ export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async accept(promptText: string | undefined) {
|
async accept(promptText: string | undefined) {
|
||||||
|
return this._wrapApiCall('dialog.accept', async () => {
|
||||||
await this._channel.accept({ promptText });
|
await this._channel.accept({ promptText });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async dismiss() {
|
async dismiss() {
|
||||||
|
return this._wrapApiCall('dialog.dismiss', async () => {
|
||||||
await this._channel.dismiss();
|
await this._channel.dismiss();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import { TimeoutSettings } from '../../timeoutSettings';
|
|||||||
import { Waiter } from './waiter';
|
import { Waiter } from './waiter';
|
||||||
import { TimeoutError } from '../../errors';
|
import { TimeoutError } from '../../errors';
|
||||||
import { Events } from '../../events';
|
import { Events } from '../../events';
|
||||||
|
import { LoggerSink } from '../../loggerSink';
|
||||||
|
|
||||||
export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer> {
|
export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer> {
|
||||||
static from(electron: ElectronChannel): Electron {
|
static from(electron: ElectronChannel): Electron {
|
||||||
@ -35,10 +36,12 @@ export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer>
|
|||||||
super(parent, type, guid, initializer, true);
|
super(parent, type, guid, initializer, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise<ElectronApplication> {
|
async launch(executablePath: string, options: ElectronLaunchOptions & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
|
||||||
options = { ...options };
|
const logger = options.logger;
|
||||||
delete (options as any).logger;
|
options = { ...options, logger: undefined };
|
||||||
|
return this._wrapApiCall('electron.launch', async () => {
|
||||||
return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication);
|
return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication);
|
||||||
|
}, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
|
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
|
||||||
return await this._mainFrame.addStyleTag(options);
|
return this._attributeToPage(() => this._mainFrame.addStyleTag(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async exposeFunction(name: string, playwrightFunction: Function) {
|
async exposeFunction(name: string, playwrightFunction: Function) {
|
||||||
@ -267,16 +267,20 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async exposeBinding(name: string, binding: FunctionWithSource) {
|
async exposeBinding(name: string, binding: FunctionWithSource) {
|
||||||
|
return this._wrapApiCall('page.exposeBinding', async () => {
|
||||||
if (this._bindings.has(name))
|
if (this._bindings.has(name))
|
||||||
throw new Error(`Function "${name}" has been already registered`);
|
throw new Error(`Function "${name}" has been already registered`);
|
||||||
if (this._browserContext._bindings.has(name))
|
if (this._browserContext._bindings.has(name))
|
||||||
throw new Error(`Function "${name}" has been already registered in the browser context`);
|
throw new Error(`Function "${name}" has been already registered in the browser context`);
|
||||||
this._bindings.set(name, binding);
|
this._bindings.set(name, binding);
|
||||||
await this._channel.exposeBinding({ name });
|
await this._channel.exposeBinding({ name });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExtraHTTPHeaders(headers: types.Headers) {
|
async setExtraHTTPHeaders(headers: types.Headers) {
|
||||||
|
return this._wrapApiCall('page.setExtraHTTPHeaders', async () => {
|
||||||
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
url(): string {
|
url(): string {
|
||||||
@ -296,7 +300,9 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reload(options: types.NavigateOptions = {}): Promise<Response | null> {
|
async reload(options: types.NavigateOptions = {}): Promise<Response | null> {
|
||||||
|
return this._wrapApiCall('page.reload', async () => {
|
||||||
return Response.fromNullable((await this._channel.reload(options)).response);
|
return Response.fromNullable((await this._channel.reload(options)).response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise<void> {
|
async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise<void> {
|
||||||
@ -340,20 +346,28 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async goBack(options: types.NavigateOptions = {}): Promise<Response | null> {
|
async goBack(options: types.NavigateOptions = {}): Promise<Response | null> {
|
||||||
|
return this._wrapApiCall('page.goBack', async () => {
|
||||||
return Response.fromNullable((await this._channel.goBack(options)).response);
|
return Response.fromNullable((await this._channel.goBack(options)).response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async goForward(options: types.NavigateOptions = {}): Promise<Response | null> {
|
async goForward(options: types.NavigateOptions = {}): Promise<Response | null> {
|
||||||
|
return this._wrapApiCall('page.goForward', async () => {
|
||||||
return Response.fromNullable((await this._channel.goForward(options)).response);
|
return Response.fromNullable((await this._channel.goForward(options)).response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async emulateMedia(options: { media?: types.MediaType, colorScheme?: types.ColorScheme }) {
|
async emulateMedia(options: { media?: types.MediaType, colorScheme?: types.ColorScheme }) {
|
||||||
|
return this._wrapApiCall('page.emulateMedia', async () => {
|
||||||
await this._channel.emulateMedia(options);
|
await this._channel.emulateMedia(options);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewportSize(viewportSize: types.Size) {
|
async setViewportSize(viewportSize: types.Size) {
|
||||||
|
return this._wrapApiCall('page.setViewportSize', async () => {
|
||||||
this._viewportSize = viewportSize;
|
this._viewportSize = viewportSize;
|
||||||
await this._channel.setViewportSize({ viewportSize });
|
await this._channel.setViewportSize({ viewportSize });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportSize(): types.Size | null {
|
viewportSize(): types.Size | null {
|
||||||
@ -368,20 +382,26 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
||||||
|
return this._wrapApiCall('page.addInitScript', async () => {
|
||||||
const source = await helper.evaluationScript(script, arg);
|
const source = await helper.evaluationScript(script, arg);
|
||||||
await this._channel.addInitScript({ source });
|
await this._channel.addInitScript({ source });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async route(url: types.URLMatch, handler: RouteHandler): Promise<void> {
|
async route(url: types.URLMatch, handler: RouteHandler): Promise<void> {
|
||||||
|
return this._wrapApiCall('page.route', async () => {
|
||||||
this._routes.push({ url, handler });
|
this._routes.push({ url, handler });
|
||||||
if (this._routes.length === 1)
|
if (this._routes.length === 1)
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async unroute(url: types.URLMatch, handler?: RouteHandler): Promise<void> {
|
async unroute(url: types.URLMatch, handler?: RouteHandler): Promise<void> {
|
||||||
|
return this._wrapApiCall('page.unroute', async () => {
|
||||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||||
if (this._routes.length === 0)
|
if (this._routes.length === 0)
|
||||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> {
|
async screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> {
|
||||||
@ -395,9 +415,11 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) {
|
async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) {
|
||||||
|
return this._wrapApiCall('page.close', async () => {
|
||||||
await this._channel.close(options);
|
await this._channel.close(options);
|
||||||
if (this._ownedContext)
|
if (this._ownedContext)
|
||||||
await this._ownedContext.close();
|
await this._ownedContext.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isClosed(): boolean {
|
isClosed(): boolean {
|
||||||
|
@ -104,11 +104,11 @@ describe('BrowserContext', function() {
|
|||||||
});
|
});
|
||||||
it('should not allow deviceScaleFactor with null viewport', async({ browser }) => {
|
it('should not allow deviceScaleFactor with null viewport', async({ browser }) => {
|
||||||
const error = await browser.newContext({ viewport: null, deviceScaleFactor: 1 }).catch(e => e);
|
const error = await browser.newContext({ viewport: null, deviceScaleFactor: 1 }).catch(e => e);
|
||||||
expect(error.message).toBe('"deviceScaleFactor" option is not supported with null "viewport"');
|
expect(error.message).toContain('"deviceScaleFactor" option is not supported with null "viewport"');
|
||||||
});
|
});
|
||||||
it('should not allow isMobile with null viewport', async({ browser }) => {
|
it('should not allow isMobile with null viewport', async({ browser }) => {
|
||||||
const error = await browser.newContext({ viewport: null, isMobile: true }).catch(e => e);
|
const error = await browser.newContext({ viewport: null, isMobile: true }).catch(e => e);
|
||||||
expect(error.message).toBe('"isMobile" option is not supported with null "viewport"');
|
expect(error.message).toContain('"isMobile" option is not supported with null "viewport"');
|
||||||
});
|
});
|
||||||
it('close() should work for empty context', async({ browser }) => {
|
it('close() should work for empty context', async({ browser }) => {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
@ -393,13 +393,13 @@ describe('BrowserContext.exposeFunction', () => {
|
|||||||
await context.exposeFunction('foo', () => {});
|
await context.exposeFunction('foo', () => {});
|
||||||
await context.exposeFunction('bar', () => {});
|
await context.exposeFunction('bar', () => {});
|
||||||
let error = await context.exposeFunction('foo', () => {}).catch(e => e);
|
let error = await context.exposeFunction('foo', () => {}).catch(e => e);
|
||||||
expect(error.message).toBe('Function "foo" has been already registered');
|
expect(error.message).toContain('Function "foo" has been already registered');
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
error = await page.exposeFunction('foo', () => {}).catch(e => e);
|
error = await page.exposeFunction('foo', () => {}).catch(e => e);
|
||||||
expect(error.message).toBe('Function "foo" has been already registered in the browser context');
|
expect(error.message).toContain('Function "foo" has been already registered in the browser context');
|
||||||
await page.exposeFunction('baz', () => {});
|
await page.exposeFunction('baz', () => {});
|
||||||
error = await context.exposeFunction('baz', () => {}).catch(e => e);
|
error = await context.exposeFunction('baz', () => {}).catch(e => e);
|
||||||
expect(error.message).toBe('Function "baz" has been already registered in one of the pages');
|
expect(error.message).toContain('Function "baz" has been already registered in one of the pages');
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
it('should be callable from-inside addInitScript', async({browser, server}) => {
|
it('should be callable from-inside addInitScript', async({browser, server}) => {
|
||||||
|
@ -66,7 +66,7 @@ describe('ChromiumBrowserContext.createSession', function() {
|
|||||||
}
|
}
|
||||||
expect(error.message).toContain(CHANNEL ? 'Target browser or context has been closed' : 'Session closed.');
|
expect(error.message).toContain(CHANNEL ? 'Target browser or context has been closed' : 'Session closed.');
|
||||||
});
|
});
|
||||||
it.skip(CHANNEL)('should throw nice errors', async function({page, browser}) {
|
it('should throw nice errors', async function({page, browser}) {
|
||||||
const client = await page.context().newCDPSession(page);
|
const client = await page.context().newCDPSession(page);
|
||||||
const error = await theSourceOfTheProblems().catch(error => error);
|
const error = await theSourceOfTheProblems().catch(error => error);
|
||||||
expect(error.stack).toContain('theSourceOfTheProblems');
|
expect(error.stack).toContain('theSourceOfTheProblems');
|
||||||
|
@ -369,7 +369,7 @@ describe('BrowserContext.addCookies', function() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error.message).toEqual(
|
expect(error.message).toContain(
|
||||||
`Blank page can not have cookie "example-cookie-blank"`
|
`Blank page can not have cookie "example-cookie-blank"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -242,7 +242,7 @@ describe('Page.emulateMedia type', function() {
|
|||||||
it('should throw in case of bad type argument', async({page, server}) => {
|
it('should throw in case of bad type argument', async({page, server}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
await page.emulateMedia({ media: 'bad' }).catch(e => error = e);
|
await page.emulateMedia({ media: 'bad' }).catch(e => error = e);
|
||||||
expect(error.message).toBe('Unsupported media: bad');
|
expect(error.message).toContain('Unsupported media: bad');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -270,7 +270,7 @@ describe('Page.emulateMedia colorScheme', function() {
|
|||||||
it('should throw in case of bad argument', async({page, server}) => {
|
it('should throw in case of bad argument', async({page, server}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
await page.emulateMedia({ colorScheme: 'bad' }).catch(e => error = e);
|
await page.emulateMedia({ colorScheme: 'bad' }).catch(e => error = e);
|
||||||
expect(error.message).toBe('Unsupported color scheme: bad');
|
expect(error.message).toContain('Unsupported color scheme: bad');
|
||||||
});
|
});
|
||||||
it('should work during navigation', async({page, server}) => {
|
it('should work during navigation', async({page, server}) => {
|
||||||
await page.emulateMedia({ colorScheme: 'light' });
|
await page.emulateMedia({ colorScheme: 'light' });
|
||||||
@ -353,7 +353,7 @@ describe('BrowserContext({timezoneId})', function() {
|
|||||||
let error = null;
|
let error = null;
|
||||||
const context = await browser.newContext({ timezoneId });
|
const context = await browser.newContext({ timezoneId });
|
||||||
const page = await context.newPage().catch(e => error = e);
|
const page = await context.newPage().catch(e => error = e);
|
||||||
expect(error.message).toBe(`Invalid timezone ID: ${timezoneId}`);
|
expect(error.message).toContain(`Invalid timezone ID: ${timezoneId}`);
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -489,7 +489,7 @@ describe('Page.addInitScript', function() {
|
|||||||
});
|
});
|
||||||
it('should throw without path and content', async({page, server}) => {
|
it('should throw without path and content', async({page, server}) => {
|
||||||
const error = await page.addInitScript({ foo: 'bar' }).catch(e => e);
|
const error = await page.addInitScript({ foo: 'bar' }).catch(e => e);
|
||||||
expect(error.message).toBe('Either path or content property must be present');
|
expect(error.message).toContain('Either path or content property must be present');
|
||||||
});
|
});
|
||||||
it('should work with browser context scripts', async({browser, server}) => {
|
it('should work with browser context scripts', async({browser, server}) => {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
|
@ -458,6 +458,6 @@ describe('Page.setExtraHTTPHeaders', function() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.');
|
expect(error.message).toContain('Expected value of header "foo" to be String, but "number" is found.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -101,7 +101,7 @@ describe('Page.Events.Load', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip(CHANNEL)('Async stacks', () => {
|
describe('Async stacks', () => {
|
||||||
it('should work', async({page, server}) => {
|
it('should work', async({page, server}) => {
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
server.setRoute('/empty.html', (req, res) => {
|
||||||
req.socket.end();
|
req.socket.end();
|
||||||
|
@ -36,7 +36,7 @@ describe.skip(WEBKIT)('Permissions', function() {
|
|||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
let error = {};
|
let error = {};
|
||||||
await context.grantPermissions(['foo'], { origin: server.EMPTY_PAGE }).catch(e => error = e);
|
await context.grantPermissions(['foo'], { origin: server.EMPTY_PAGE }).catch(e => error = e);
|
||||||
expect(error.message).toBe('Unknown permission: foo');
|
expect(error.message).toContain('Unknown permission: foo');
|
||||||
});
|
});
|
||||||
it('should grant geolocation permission when listed', async({page, server, context}) => {
|
it('should grant geolocation permission when listed', async({page, server, context}) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
@ -458,7 +458,7 @@ describe('Frame.waitForSelector', function() {
|
|||||||
await page.setContent(`<div class='zombo'>anything</div>`);
|
await page.setContent(`<div class='zombo'>anything</div>`);
|
||||||
expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything');
|
expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything');
|
||||||
});
|
});
|
||||||
it.skip(CHANNEL)('should have correct stack trace for timeout', async({page, server}) => {
|
it('should have correct stack trace for timeout', async({page, server}) => {
|
||||||
let error;
|
let error;
|
||||||
await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e);
|
await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e);
|
||||||
expect(error.stack).toContain('waittask.spec.js');
|
expect(error.stack).toContain('waittask.spec.js');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user