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
	 Dmitry Gozman
						Dmitry Gozman