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],
|
||||
]);
|
||||
|
||||
class DebugLoggerSink {
|
||||
export class DebugLoggerSink {
|
||||
private _debuggers = new Map<string, debug.IDebugger>();
|
||||
|
||||
isEnabled(name: string, severity: LoggerSeverity): boolean {
|
||||
|
|
|
@ -54,14 +54,16 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
|||
async newContext(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<BrowserContext> {
|
||||
const logger = options.logger;
|
||||
options = { ...options, logger: undefined };
|
||||
const contextOptions: BrowserContextOptions = {
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
};
|
||||
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
|
||||
this._contexts.add(context);
|
||||
context._logger = logger || this._logger;
|
||||
return context;
|
||||
return this._wrapApiCall('browser.newContext', async () => {
|
||||
const contextOptions: BrowserContextOptions = {
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
};
|
||||
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
|
||||
this._contexts.add(context);
|
||||
context._logger = logger || this._logger;
|
||||
return context;
|
||||
});
|
||||
}
|
||||
|
||||
contexts(): BrowserContext[] {
|
||||
|
@ -81,10 +83,12 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
|||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (!this._isClosedOrClosing) {
|
||||
this._isClosedOrClosing = true;
|
||||
await this._channel.close();
|
||||
}
|
||||
await this._closedPromise;
|
||||
return this._wrapApiCall('browser.close', async () => {
|
||||
if (!this._isClosedOrClosing) {
|
||||
this._isClosedOrClosing = true;
|
||||
await this._channel.close();
|
||||
}
|
||||
await this._closedPromise;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,9 +98,11 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||
}
|
||||
|
||||
async newPage(): Promise<Page> {
|
||||
if (this._ownerPage)
|
||||
throw new Error('Please use browser.newContext()');
|
||||
return Page.from((await this._channel.newPage()).page);
|
||||
return this._wrapApiCall('browserContext.newPage', async () => {
|
||||
if (this._ownerPage)
|
||||
throw new Error('Please use browser.newContext()');
|
||||
return Page.from((await this._channel.newPage()).page);
|
||||
});
|
||||
}
|
||||
|
||||
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
|
||||
|
@ -108,55 +110,77 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||
urls = [];
|
||||
if (urls && typeof urls === 'string')
|
||||
urls = [ urls ];
|
||||
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
|
||||
return this._wrapApiCall('browserContext.cookies', async () => {
|
||||
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
|
||||
});
|
||||
}
|
||||
|
||||
async addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void> {
|
||||
await this._channel.addCookies({ cookies });
|
||||
return this._wrapApiCall('browserContext.addCookies', async () => {
|
||||
await this._channel.addCookies({ cookies });
|
||||
});
|
||||
}
|
||||
|
||||
async clearCookies(): Promise<void> {
|
||||
await this._channel.clearCookies();
|
||||
return this._wrapApiCall('browserContext.clearCookies', async () => {
|
||||
await this._channel.clearCookies();
|
||||
});
|
||||
}
|
||||
|
||||
async grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void> {
|
||||
await this._channel.grantPermissions({ permissions, ...options });
|
||||
return this._wrapApiCall('browserContext.grantPermissions', async () => {
|
||||
await this._channel.grantPermissions({ permissions, ...options });
|
||||
});
|
||||
}
|
||||
|
||||
async clearPermissions(): Promise<void> {
|
||||
await this._channel.clearPermissions();
|
||||
return this._wrapApiCall('browserContext.clearPermissions', async () => {
|
||||
await this._channel.clearPermissions();
|
||||
});
|
||||
}
|
||||
|
||||
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
|
||||
await this._channel.setGeolocation({ geolocation });
|
||||
return this._wrapApiCall('browserContext.setGeolocation', async () => {
|
||||
await this._channel.setGeolocation({ geolocation });
|
||||
});
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
|
||||
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
||||
return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async () => {
|
||||
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
||||
});
|
||||
}
|
||||
|
||||
async setOffline(offline: boolean): Promise<void> {
|
||||
await this._channel.setOffline({ offline });
|
||||
return this._wrapApiCall('browserContext.setOffline', async () => {
|
||||
await this._channel.setOffline({ offline });
|
||||
});
|
||||
}
|
||||
|
||||
async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void> {
|
||||
await this._channel.setHTTPCredentials({ httpCredentials });
|
||||
return this._wrapApiCall('browserContext.setHTTPCredentials', async () => {
|
||||
await this._channel.setHTTPCredentials({ httpCredentials });
|
||||
});
|
||||
}
|
||||
|
||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
||||
const source = await helper.evaluationScript(script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
return this._wrapApiCall('browserContext.addInitScript', async () => {
|
||||
const source = await helper.evaluationScript(script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
});
|
||||
}
|
||||
|
||||
async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise<void> {
|
||||
for (const page of this.pages()) {
|
||||
if (page._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered in one of the pages`);
|
||||
}
|
||||
if (this._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
this._bindings.set(name, binding);
|
||||
await this._channel.exposeBinding({ name });
|
||||
return this._wrapApiCall('browserContext.exposeBinding', async () => {
|
||||
for (const page of this.pages()) {
|
||||
if (page._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered in one of the pages`);
|
||||
}
|
||||
if (this._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
this._bindings.set(name, binding);
|
||||
await this._channel.exposeBinding({ name });
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
this._routes.push({ url, handler });
|
||||
if (this._routes.length === 1)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
||||
return this._wrapApiCall('browserContext.route', async () => {
|
||||
this._routes.push({ url, handler });
|
||||
if (this._routes.length === 1)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
||||
});
|
||||
}
|
||||
|
||||
async unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void> {
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
if (this._routes.length === 0)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||
return this._wrapApiCall('browserContext.unroute', async () => {
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
if (this._routes.length === 0)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||
|
@ -196,10 +224,12 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (!this._isClosedOrClosing) {
|
||||
this._isClosedOrClosing = true;
|
||||
await this._channel.close();
|
||||
}
|
||||
await this._closedPromise;
|
||||
return this._wrapApiCall('browserContext.close', async () => {
|
||||
if (!this._isClosedOrClosing) {
|
||||
this._isClosedOrClosing = true;
|
||||
await this._channel.close();
|
||||
}
|
||||
await this._closedPromise;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,11 +38,15 @@ export class BrowserServer extends ChannelOwner<BrowserServerChannel, BrowserSer
|
|||
}
|
||||
|
||||
async kill(): Promise<void> {
|
||||
await this._channel.kill();
|
||||
return this._wrapApiCall('browserServer.kill', async () => {
|
||||
await this._channel.kill();
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this._channel.close();
|
||||
return this._wrapApiCall('browserServer.close', async () => {
|
||||
await this._channel.close();
|
||||
});
|
||||
}
|
||||
|
||||
_checkLeaks() {}
|
||||
|
|
|
@ -46,11 +46,15 @@ export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitia
|
|||
method: T,
|
||||
params?: Protocol.CommandParameters[T]
|
||||
): Promise<Protocol.CommandReturnValues[T]> {
|
||||
const result = await this._channel.send({ method, params });
|
||||
return result.result as Protocol.CommandReturnValues[T];
|
||||
return this._wrapApiCall('cdpSession.send', async () => {
|
||||
const result = await this._channel.send({ method, params });
|
||||
return result.result as Protocol.CommandReturnValues[T];
|
||||
});
|
||||
}
|
||||
|
||||
async detach() {
|
||||
return this._channel.detach();
|
||||
return this._wrapApiCall('cdpSession.detach', async () => {
|
||||
return this._channel.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Channel } from '../channels';
|
|||
import { Connection } from './connection';
|
||||
import { assert } from '../../helper';
|
||||
import { LoggerSink } from '../../loggerSink';
|
||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||
import { DebugLoggerSink } from '../../logger';
|
||||
|
||||
export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}> extends EventEmitter {
|
||||
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> {
|
||||
const stackObject: any = {};
|
||||
Error.captureStackTrace(stackObject);
|
||||
const stack = stackObject.stack.startsWith('Error') ? stackObject.stack.substring(5) : stackObject.stack;
|
||||
logger = logger || this._logger;
|
||||
try {
|
||||
if (logger && logger.isEnabled('api', 'info'))
|
||||
logger.log('api', 'info', `=> ${apiName} started`, [], { color: 'cyan' });
|
||||
logApiCall(logger, `=> ${apiName} started`);
|
||||
const result = await func();
|
||||
if (logger && logger.isEnabled('api', 'info'))
|
||||
logger.log('api', 'info', `=> ${apiName} succeeded`, [], { color: 'cyan' });
|
||||
logApiCall(logger, `<= ${apiName} succeeded`);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (logger && logger.isEnabled('api', 'info'))
|
||||
logger.log('api', 'info', `=> ${apiName} failed`, [], { color: 'cyan' });
|
||||
rewriteErrorMessage(e, `${apiName}: ` + e.message);
|
||||
logApiCall(logger, `<= ${apiName} failed`);
|
||||
// TODO: we could probably save "e.stack" in some log-heavy mode
|
||||
// because it gives some insights into the server part.
|
||||
e.message = `${apiName}: ` + e.message;
|
||||
e.stack = e.message + stack;
|
||||
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 {
|
||||
async newBrowserCDPSession(): Promise<CDPSession> {
|
||||
return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session);
|
||||
return this._wrapApiCall('chromiumBrowser.newBrowserCDPSession', async () => {
|
||||
return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session);
|
||||
});
|
||||
}
|
||||
|
||||
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
|
||||
await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined });
|
||||
return this._wrapApiCall('chromiumBrowser.startTracing', async () => {
|
||||
await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined });
|
||||
});
|
||||
}
|
||||
|
||||
async stopTracing(): Promise<Buffer> {
|
||||
return Buffer.from((await this._channel.crStopTracing()).binary, 'base64');
|
||||
return this._wrapApiCall('chromiumBrowser.stopTracing', async () => {
|
||||
return Buffer.from((await this._channel.crStopTracing()).binary, 'base64');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,9 @@ export class ChromiumBrowserContext extends BrowserContext {
|
|||
}
|
||||
|
||||
async newCDPSession(page: Page): Promise<CDPSession> {
|
||||
const result = await this._channel.crNewCDPSession({ page: page._channel });
|
||||
return CDPSession.from(result.session);
|
||||
return this._wrapApiCall('chromiumBrowserContext.newCDPSession', async () => {
|
||||
const result = await this._channel.crNewCDPSession({ page: page._channel });
|
||||
return CDPSession.from(result.session);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,10 +39,14 @@ export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {
|
|||
}
|
||||
|
||||
async accept(promptText: string | undefined) {
|
||||
await this._channel.accept({ promptText });
|
||||
return this._wrapApiCall('dialog.accept', async () => {
|
||||
await this._channel.accept({ promptText });
|
||||
});
|
||||
}
|
||||
|
||||
async dismiss() {
|
||||
await this._channel.dismiss();
|
||||
return this._wrapApiCall('dialog.dismiss', async () => {
|
||||
await this._channel.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { TimeoutSettings } from '../../timeoutSettings';
|
|||
import { Waiter } from './waiter';
|
||||
import { TimeoutError } from '../../errors';
|
||||
import { Events } from '../../events';
|
||||
import { LoggerSink } from '../../loggerSink';
|
||||
|
||||
export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer> {
|
||||
static from(electron: ElectronChannel): Electron {
|
||||
|
@ -35,10 +36,12 @@ export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer>
|
|||
super(parent, type, guid, initializer, true);
|
||||
}
|
||||
|
||||
async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise<ElectronApplication> {
|
||||
options = { ...options };
|
||||
delete (options as any).logger;
|
||||
return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication);
|
||||
async launch(executablePath: string, options: ElectronLaunchOptions & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
|
||||
const logger = options.logger;
|
||||
options = { ...options, logger: undefined };
|
||||
return this._wrapApiCall('electron.launch', async () => {
|
||||
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> {
|
||||
return await this._mainFrame.addStyleTag(options);
|
||||
return this._attributeToPage(() => this._mainFrame.addStyleTag(options));
|
||||
}
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function) {
|
||||
|
@ -267,16 +267,20 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||
}
|
||||
|
||||
async exposeBinding(name: string, binding: FunctionWithSource) {
|
||||
if (this._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
if (this._browserContext._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered in the browser context`);
|
||||
this._bindings.set(name, binding);
|
||||
await this._channel.exposeBinding({ name });
|
||||
return this._wrapApiCall('page.exposeBinding', async () => {
|
||||
if (this._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
if (this._browserContext._bindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered in the browser context`);
|
||||
this._bindings.set(name, binding);
|
||||
await this._channel.exposeBinding({ name });
|
||||
});
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers: types.Headers) {
|
||||
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
||||
return this._wrapApiCall('page.setExtraHTTPHeaders', async () => {
|
||||
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
|
||||
});
|
||||
}
|
||||
|
||||
url(): string {
|
||||
|
@ -296,7 +300,9 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||
}
|
||||
|
||||
async reload(options: types.NavigateOptions = {}): Promise<Response | null> {
|
||||
return Response.fromNullable((await this._channel.reload(options)).response);
|
||||
return this._wrapApiCall('page.reload', async () => {
|
||||
return Response.fromNullable((await this._channel.reload(options)).response);
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
return Response.fromNullable((await this._channel.goBack(options)).response);
|
||||
return this._wrapApiCall('page.goBack', async () => {
|
||||
return Response.fromNullable((await this._channel.goBack(options)).response);
|
||||
});
|
||||
}
|
||||
|
||||
async goForward(options: types.NavigateOptions = {}): Promise<Response | null> {
|
||||
return Response.fromNullable((await this._channel.goForward(options)).response);
|
||||
return this._wrapApiCall('page.goForward', async () => {
|
||||
return Response.fromNullable((await this._channel.goForward(options)).response);
|
||||
});
|
||||
}
|
||||
|
||||
async emulateMedia(options: { media?: types.MediaType, colorScheme?: types.ColorScheme }) {
|
||||
await this._channel.emulateMedia(options);
|
||||
return this._wrapApiCall('page.emulateMedia', async () => {
|
||||
await this._channel.emulateMedia(options);
|
||||
});
|
||||
}
|
||||
|
||||
async setViewportSize(viewportSize: types.Size) {
|
||||
this._viewportSize = viewportSize;
|
||||
await this._channel.setViewportSize({ viewportSize });
|
||||
return this._wrapApiCall('page.setViewportSize', async () => {
|
||||
this._viewportSize = viewportSize;
|
||||
await this._channel.setViewportSize({ viewportSize });
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
const source = await helper.evaluationScript(script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
return this._wrapApiCall('page.addInitScript', async () => {
|
||||
const source = await helper.evaluationScript(script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
});
|
||||
}
|
||||
|
||||
async route(url: types.URLMatch, handler: RouteHandler): Promise<void> {
|
||||
this._routes.push({ url, handler });
|
||||
if (this._routes.length === 1)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
||||
return this._wrapApiCall('page.route', async () => {
|
||||
this._routes.push({ url, handler });
|
||||
if (this._routes.length === 1)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
|
||||
});
|
||||
}
|
||||
|
||||
async unroute(url: types.URLMatch, handler?: RouteHandler): Promise<void> {
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
if (this._routes.length === 0)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||
return this._wrapApiCall('page.unroute', async () => {
|
||||
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
|
||||
if (this._routes.length === 0)
|
||||
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
|
||||
});
|
||||
}
|
||||
|
||||
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}) {
|
||||
await this._channel.close(options);
|
||||
if (this._ownedContext)
|
||||
await this._ownedContext.close();
|
||||
return this._wrapApiCall('page.close', async () => {
|
||||
await this._channel.close(options);
|
||||
if (this._ownedContext)
|
||||
await this._ownedContext.close();
|
||||
});
|
||||
}
|
||||
|
||||
isClosed(): boolean {
|
||||
|
|
|
@ -104,11 +104,11 @@ describe('BrowserContext', function() {
|
|||
});
|
||||
it('should not allow deviceScaleFactor with null viewport', async({ browser }) => {
|
||||
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 }) => {
|
||||
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 }) => {
|
||||
const context = await browser.newContext();
|
||||
|
@ -393,13 +393,13 @@ describe('BrowserContext.exposeFunction', () => {
|
|||
await context.exposeFunction('foo', () => {});
|
||||
await context.exposeFunction('bar', () => {});
|
||||
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();
|
||||
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', () => {});
|
||||
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();
|
||||
});
|
||||
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.');
|
||||
});
|
||||
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 error = await theSourceOfTheProblems().catch(error => error);
|
||||
expect(error.stack).toContain('theSourceOfTheProblems');
|
||||
|
|
|
@ -369,7 +369,7 @@ describe('BrowserContext.addCookies', function() {
|
|||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error.message).toEqual(
|
||||
expect(error.message).toContain(
|
||||
`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}) => {
|
||||
let error = null;
|
||||
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}) => {
|
||||
let error = null;
|
||||
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}) => {
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
|
@ -353,7 +353,7 @@ describe('BrowserContext({timezoneId})', function() {
|
|||
let error = null;
|
||||
const context = await browser.newContext({ timezoneId });
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -489,7 +489,7 @@ describe('Page.addInitScript', function() {
|
|||
});
|
||||
it('should throw without path and content', async({page, server}) => {
|
||||
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}) => {
|
||||
const context = await browser.newContext();
|
||||
|
|
|
@ -458,6 +458,6 @@ describe('Page.setExtraHTTPHeaders', function() {
|
|||
} catch (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}) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
req.socket.end();
|
||||
|
|
|
@ -36,7 +36,7 @@ describe.skip(WEBKIT)('Permissions', function() {
|
|||
await page.goto(server.EMPTY_PAGE);
|
||||
let error = {};
|
||||
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}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
|
@ -458,7 +458,7 @@ describe('Frame.waitForSelector', function() {
|
|||
await page.setContent(`<div class='zombo'>anything</div>`);
|
||||
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;
|
||||
await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e);
|
||||
expect(error.stack).toContain('waittask.spec.js');
|
||||
|
|
Loading…
Reference in New Issue