mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	fix(webkit): improve target swap handling (#175)
- Fix "page closed twice" race. - Do not fire 'disconnected' on swapped out sessions. - Use a different error for commands sent to swapped out targets. This allows callers to detect this situation and retry/throw/catch. - Restore more state on swap: extra http headers, user agent, emulated media.
This commit is contained in:
		
							parent
							
								
									3fe20ba516
								
							
						
					
					
						commit
						0d0f6b7d03
					
				| @ -184,17 +184,8 @@ export class Browser extends EventEmitter { | |||||||
| 
 | 
 | ||||||
|   async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) { |   async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) { | ||||||
|     const oldTarget = this._targets.get(oldTargetId); |     const oldTarget = this._targets.get(oldTargetId); | ||||||
|     if (!oldTarget._pagePromise) |  | ||||||
|       return; |  | ||||||
|     const page = await oldTarget._pagePromise; |  | ||||||
|     const newTarget = this._targets.get(newTargetId); |     const newTarget = this._targets.get(newTargetId); | ||||||
|     const newSession = this._connection.session(newTargetId); |     newTarget._swappedIn(oldTarget, this._connection.session(newTargetId)); | ||||||
|     page._swapSessionOnNavigation(newSession); |  | ||||||
|     newTarget._pagePromise = oldTarget._pagePromise; |  | ||||||
|     newTarget._adoptPage(page); |  | ||||||
|     // Old target should not be accessed by anyone. Reset page promise so that
 |  | ||||||
|     // old target does not close the page on connection reset.
 |  | ||||||
|     oldTarget._pagePromise = null; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   disconnect() { |   disconnect() { | ||||||
|  | |||||||
| @ -104,7 +104,6 @@ export class Connection extends EventEmitter { | |||||||
|     } else if (object.method === 'Target.targetDestroyed') { |     } else if (object.method === 'Target.targetDestroyed') { | ||||||
|       const session = this._sessions.get(object.params.targetId); |       const session = this._sessions.get(object.params.targetId); | ||||||
|       if (session) { |       if (session) { | ||||||
|         // FIXME: this is a workaround for cross-origin navigation in WebKit.
 |  | ||||||
|         session._onClosed(); |         session._onClosed(); | ||||||
|         this._sessions.delete(object.params.targetId); |         this._sessions.delete(object.params.targetId); | ||||||
|       } |       } | ||||||
| @ -121,6 +120,7 @@ export class Connection extends EventEmitter { | |||||||
|       const oldSession = this._sessions.get(oldTargetId); |       const oldSession = this._sessions.get(oldTargetId); | ||||||
|       if (!oldSession) |       if (!oldSession) | ||||||
|         throw new Error('Unknown old target: ' + oldTargetId); |         throw new Error('Unknown old target: ' + oldTargetId); | ||||||
|  |       oldSession._swappedOut = true; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -158,6 +158,7 @@ export class TargetSession extends EventEmitter { | |||||||
|   private _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>(); |   private _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>(); | ||||||
|   private _targetType: string; |   private _targetType: string; | ||||||
|   private _sessionId: string; |   private _sessionId: string; | ||||||
|  |   _swappedOut = false; | ||||||
|   on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; |   on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; | ||||||
|   addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; |   addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; | ||||||
|   off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; |   off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; | ||||||
| @ -194,7 +195,7 @@ export class TargetSession extends EventEmitter { | |||||||
|     }).catch(e => { |     }).catch(e => { | ||||||
|       // There is a possible race of the connection closure. We may have received
 |       // There is a possible race of the connection closure. We may have received
 | ||||||
|       // targetDestroyed notification before response for the command, in that
 |       // targetDestroyed notification before response for the command, in that
 | ||||||
|       // case it's safe to swallow the exception.g
 |       // case it's safe to swallow the exception.
 | ||||||
|       const callback = this._callbacks.get(innerId); |       const callback = this._callbacks.get(innerId); | ||||||
|       assert(!callback, 'Callback was not rejected when target was destroyed.'); |       assert(!callback, 'Callback was not rejected when target was destroyed.'); | ||||||
|     }); |     }); | ||||||
| @ -218,10 +219,16 @@ export class TargetSession extends EventEmitter { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _onClosed() { |   _onClosed() { | ||||||
|     for (const callback of this._callbacks.values()) |     for (const callback of this._callbacks.values()) { | ||||||
|  |       // TODO: make some calls like screenshot catch swapped out error and retry.
 | ||||||
|  |       if (this._swappedOut) | ||||||
|  |         callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target was swapped out.`)); | ||||||
|  |       else | ||||||
|         callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); |         callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); | ||||||
|  |     } | ||||||
|     this._callbacks.clear(); |     this._callbacks.clear(); | ||||||
|     this._connection = null; |     this._connection = null; | ||||||
|  |     if (!this._swappedOut) | ||||||
|       this.emit(TargetSessionEvents.Disconnected); |       this.emit(TargetSessionEvents.Disconnected); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -237,3 +244,7 @@ function rewriteError(error: Error, message: string): Error { | |||||||
|   error.message = message; |   error.message = message; | ||||||
|   return error; |   return error; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function isSwappedOutError(e: Error) { | ||||||
|  |   return e.message.includes('Target was swapped out.'); | ||||||
|  | } | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import { TargetSession } from './Connection'; | import { TargetSession, isSwappedOutError } from './Connection'; | ||||||
| import { helper } from '../helper'; | import { helper } from '../helper'; | ||||||
| import { valueFromRemoteObject, releaseObject } from './protocolHelper'; | import { valueFromRemoteObject, releaseObject } from './protocolHelper'; | ||||||
| import { Protocol } from './protocol'; | import { Protocol } from './protocol'; | ||||||
| @ -25,7 +25,7 @@ export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; | |||||||
| const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; | const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; | ||||||
| 
 | 
 | ||||||
| export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | ||||||
|   private _globalObjectId?: string; |   private _globalObjectId?: Promise<string>; | ||||||
|   _session: TargetSession; |   _session: TargetSession; | ||||||
|   _contextId: number; |   _contextId: number; | ||||||
|   private _contextDestroyedCallback: () => void; |   private _contextDestroyedCallback: () => void; | ||||||
| @ -45,8 +45,6 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { |   async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { | ||||||
|     const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; |  | ||||||
| 
 |  | ||||||
|     if (helper.isString(pageFunction)) { |     if (helper.isString(pageFunction)) { | ||||||
|       const contextId = this._contextId; |       const contextId = this._contextId; | ||||||
|       const expression: string = pageFunction as string; |       const expression: string = pageFunction as string; | ||||||
| @ -58,18 +56,9 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|         emulateUserGesture: true |         emulateUserGesture: true | ||||||
|       }).then(response => { |       }).then(response => { | ||||||
|         if (response.result.type === 'object' && response.result.className === 'Promise') { |         if (response.result.type === 'object' && response.result.className === 'Promise') { | ||||||
|           const contextDiscarded = this._executionContextDestroyedPromise.then(() => ({ |  | ||||||
|             wasThrown: true, |  | ||||||
|             result: { |  | ||||||
|               description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.' |  | ||||||
|             } as Protocol.Runtime.RemoteObject |  | ||||||
|           })); |  | ||||||
|           return Promise.race([ |           return Promise.race([ | ||||||
|             contextDiscarded, |             this._executionContextDestroyedPromise.then(() => contextDestroyedResult), | ||||||
|             this._session.send('Runtime.awaitPromise', { |             this._awaitPromise(response.result.objectId), | ||||||
|               promiseObjectId: response.result.objectId, |  | ||||||
|               returnByValue: false |  | ||||||
|             }) |  | ||||||
|           ]); |           ]); | ||||||
|         } |         } | ||||||
|         return response; |         return response; | ||||||
| @ -78,32 +67,8 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|           throw new Error('Evaluation failed: ' + response.result.description); |           throw new Error('Evaluation failed: ' + response.result.description); | ||||||
|         if (!returnByValue) |         if (!returnByValue) | ||||||
|           return context._createHandle(response.result); |           return context._createHandle(response.result); | ||||||
|         if (response.result.objectId) { |         if (response.result.objectId) | ||||||
|           const serializeFunction = function() { |           return this._returnObjectByValue(response.result.objectId); | ||||||
|             try { |  | ||||||
|               return JSON.stringify(this); |  | ||||||
|             } catch (e) { |  | ||||||
|               if (e instanceof TypeError) |  | ||||||
|                 return void 0; |  | ||||||
|               throw e; |  | ||||||
|             } |  | ||||||
|           }; |  | ||||||
|           return this._session.send('Runtime.callFunctionOn', { |  | ||||||
|             // Serialize object using standard JSON implementation to correctly pass 'undefined'.
 |  | ||||||
|             functionDeclaration: serializeFunction + '\n' + suffix + '\n', |  | ||||||
|             objectId: response.result.objectId, |  | ||||||
|             returnByValue |  | ||||||
|           }).then(serializeResponse => { |  | ||||||
|             if (serializeResponse.wasThrown) |  | ||||||
|               throw new Error('Serialization failed: ' + serializeResponse.result.description); |  | ||||||
|             // This is the case of too long property chain, not serializable to json string.
 |  | ||||||
|             if (serializeResponse.result.type === 'undefined') |  | ||||||
|               return undefined; |  | ||||||
|             if (serializeResponse.result.type !== 'string') |  | ||||||
|               throw new Error('Unexpected result of JSON.stringify: ' + JSON.stringify(serializeResponse, null, 2)); |  | ||||||
|             return JSON.parse(serializeResponse.result.value); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|         return valueFromRemoteObject(response.result); |         return valueFromRemoteObject(response.result); | ||||||
|       }).catch(rewriteError); |       }).catch(rewriteError); | ||||||
|     } |     } | ||||||
| @ -164,18 +129,9 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|     } |     } | ||||||
|     return callFunctionOnPromise.then(response => { |     return callFunctionOnPromise.then(response => { | ||||||
|       if (response.result.type === 'object' && response.result.className === 'Promise') { |       if (response.result.type === 'object' && response.result.className === 'Promise') { | ||||||
|         const contextDiscarded = this._executionContextDestroyedPromise.then(() => ({ |  | ||||||
|           wasThrown: true, |  | ||||||
|           result: { |  | ||||||
|             description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.' |  | ||||||
|           } as Protocol.Runtime.RemoteObject |  | ||||||
|         })); |  | ||||||
|         return Promise.race([ |         return Promise.race([ | ||||||
|           contextDiscarded, |           this._executionContextDestroyedPromise.then(() => contextDestroyedResult), | ||||||
|           this._session.send('Runtime.awaitPromise', { |           this._awaitPromise(response.result.objectId), | ||||||
|             promiseObjectId: response.result.objectId, |  | ||||||
|             returnByValue: false |  | ||||||
|           }) |  | ||||||
|         ]); |         ]); | ||||||
|       } |       } | ||||||
|       return response; |       return response; | ||||||
| @ -184,32 +140,8 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|         throw new Error('Evaluation failed: ' + response.result.description); |         throw new Error('Evaluation failed: ' + response.result.description); | ||||||
|       if (!returnByValue) |       if (!returnByValue) | ||||||
|         return context._createHandle(response.result); |         return context._createHandle(response.result); | ||||||
|       if (response.result.objectId) { |       if (response.result.objectId) | ||||||
|         const serializeFunction = function() { |         return this._returnObjectByValue(response.result.objectId); | ||||||
|           try { |  | ||||||
|             return JSON.stringify(this); |  | ||||||
|           } catch (e) { |  | ||||||
|             if (e instanceof TypeError) |  | ||||||
|               return void 0; |  | ||||||
|             throw e; |  | ||||||
|           } |  | ||||||
|         }; |  | ||||||
|         return this._session.send('Runtime.callFunctionOn', { |  | ||||||
|           // Serialize object using standard JSON implementation to correctly pass 'undefined'.
 |  | ||||||
|           functionDeclaration: serializeFunction + '\n' + suffix + '\n', |  | ||||||
|           objectId: response.result.objectId, |  | ||||||
|           returnByValue |  | ||||||
|         }).then(serializeResponse => { |  | ||||||
|           if (serializeResponse.wasThrown) |  | ||||||
|             throw new Error('Serialization failed: ' + serializeResponse.result.description); |  | ||||||
|           // This is the case of too long property chain, not serializable to json string.
 |  | ||||||
|           if (serializeResponse.result.type === 'undefined') |  | ||||||
|             return undefined; |  | ||||||
|           if (serializeResponse.result.type !== 'string') |  | ||||||
|             throw new Error('Unexpected result of JSON.stringify: ' + JSON.stringify(serializeResponse, null, 2)); |  | ||||||
|           return JSON.parse(serializeResponse.result.value); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|       return valueFromRemoteObject(response.result); |       return valueFromRemoteObject(response.result); | ||||||
|     }).catch(rewriteError); |     }).catch(rewriteError); | ||||||
| 
 | 
 | ||||||
| @ -263,14 +195,64 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async _contextGlobalObjectId() { |   private _contextGlobalObjectId() { | ||||||
|     if (!this._globalObjectId) { |     if (!this._globalObjectId) { | ||||||
|       const globalObject = await this._session.send('Runtime.evaluate', { expression: 'this', contextId: this._contextId }); |       this._globalObjectId = this._session.send('Runtime.evaluate', { | ||||||
|       this._globalObjectId = globalObject.result.objectId; |         expression: 'this', | ||||||
|  |         contextId: this._contextId | ||||||
|  |       }).catch(e => { | ||||||
|  |         if (isSwappedOutError(e)) | ||||||
|  |           throw new Error('Execution context was destroyed, most likely because of a navigation.'); | ||||||
|  |         throw e; | ||||||
|  |       }).then(response => { | ||||||
|  |         return response.result.objectId; | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|     return this._globalObjectId; |     return this._globalObjectId; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private _awaitPromise(objectId: Protocol.Runtime.RemoteObjectId) { | ||||||
|  |     return this._session.send('Runtime.awaitPromise', { | ||||||
|  |       promiseObjectId: objectId, | ||||||
|  |       returnByValue: false | ||||||
|  |     }).catch(e => { | ||||||
|  |       if (isSwappedOutError(e)) | ||||||
|  |         return contextDestroyedResult; | ||||||
|  |       throw e; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private _returnObjectByValue(objectId: Protocol.Runtime.RemoteObjectId) { | ||||||
|  |     const serializeFunction = function() { | ||||||
|  |       try { | ||||||
|  |         return JSON.stringify(this); | ||||||
|  |       } catch (e) { | ||||||
|  |         if (e instanceof TypeError) | ||||||
|  |           return void 0; | ||||||
|  |         throw e; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |     return this._session.send('Runtime.callFunctionOn', { | ||||||
|  |       // Serialize object using standard JSON implementation to correctly pass 'undefined'.
 | ||||||
|  |       functionDeclaration: serializeFunction + '\n' + suffix + '\n', | ||||||
|  |       objectId: objectId, | ||||||
|  |       returnByValue: true | ||||||
|  |     }).catch(e => { | ||||||
|  |       if (isSwappedOutError(e)) | ||||||
|  |         return contextDestroyedResult; | ||||||
|  |       throw e; | ||||||
|  |     }).then(serializeResponse => { | ||||||
|  |       if (serializeResponse.wasThrown) | ||||||
|  |         throw new Error('Serialization failed: ' + serializeResponse.result.description); | ||||||
|  |       // This is the case of too long property chain, not serializable to json string.
 | ||||||
|  |       if (serializeResponse.result.type === 'undefined') | ||||||
|  |         return undefined; | ||||||
|  |       if (serializeResponse.result.type !== 'string') | ||||||
|  |         throw new Error('Unexpected result of JSON.stringify: ' + JSON.stringify(serializeResponse, null, 2)); | ||||||
|  |       return JSON.parse(serializeResponse.result.value); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> { |   async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> { | ||||||
|     const response = await this._session.send('Runtime.getProperties', { |     const response = await this._session.send('Runtime.getProperties', { | ||||||
|       objectId: toRemoteObject(handle).objectId, |       objectId: toRemoteObject(handle).objectId, | ||||||
| @ -328,9 +310,16 @@ export class ExecutionContextDelegate implements js.ExecutionContextDelegate { | |||||||
|     } |     } | ||||||
|     return { value: arg }; |     return { value: arg }; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; | ||||||
|  | const contextDestroyedResult = { | ||||||
|  |   wasThrown: true, | ||||||
|  |   result: { | ||||||
|  |     description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.' | ||||||
|  |   } as Protocol.Runtime.RemoteObject | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject { | function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject { | ||||||
|   return handle._remoteObject as Protocol.Runtime.RemoteObject; |   return handle._remoteObject as Protocol.Runtime.RemoteObject; | ||||||
| } | } | ||||||
|  | |||||||
| @ -91,7 +91,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { | |||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async _swapSessionOnNavigation(newSession) { |   _swapSessionOnNavigation(newSession) { | ||||||
|     helper.removeEventListeners(this._sessionListeners); |     helper.removeEventListeners(this._sessionListeners); | ||||||
|     this.disconnectFromTarget(); |     this.disconnectFromTarget(); | ||||||
|     this._session = newSession; |     this._session = newSession; | ||||||
|  | |||||||
| @ -63,6 +63,7 @@ export class NetworkManager extends EventEmitter { | |||||||
| 
 | 
 | ||||||
|   async initialize() { |   async initialize() { | ||||||
|     await this._sesssion.send('Network.enable'); |     await this._sesssion.send('Network.enable'); | ||||||
|  |     await this._sesssion.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async setExtraHTTPHeaders(extraHTTPHeaders: { [s: string]: string; }) { |   async setExtraHTTPHeaders(extraHTTPHeaders: { [s: string]: string; }) { | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ import * as console from '../console'; | |||||||
| import * as dialog from '../dialog'; | import * as dialog from '../dialog'; | ||||||
| import * as dom from '../dom'; | import * as dom from '../dom'; | ||||||
| import * as frames from '../frames'; | import * as frames from '../frames'; | ||||||
| import { assert, debugError, helper, RegisteredListener } from '../helper'; | import { assert, helper, RegisteredListener } from '../helper'; | ||||||
| import * as input from '../input'; | import * as input from '../input'; | ||||||
| import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input'; | import { ClickOptions, mediaColorSchemes, mediaTypes, MultiClickOptions } from '../input'; | ||||||
| import * as js from '../javascript'; | import * as js from '../javascript'; | ||||||
| @ -49,6 +49,7 @@ export class Page extends EventEmitter { | |||||||
|   private _frameManager: FrameManager; |   private _frameManager: FrameManager; | ||||||
|   private _bootstrapScripts: string[] = []; |   private _bootstrapScripts: string[] = []; | ||||||
|   _javascriptEnabled = true; |   _javascriptEnabled = true; | ||||||
|  |   private _userAgent: string | null = null; | ||||||
|   private _viewport: types.Viewport | null = null; |   private _viewport: types.Viewport | null = null; | ||||||
|   _screenshotter: Screenshotter; |   _screenshotter: Screenshotter; | ||||||
|   private _workers = new Map<string, Worker>(); |   private _workers = new Map<string, Worker>(); | ||||||
| @ -57,14 +58,6 @@ export class Page extends EventEmitter { | |||||||
|   private _emulatedMediaType: string | undefined; |   private _emulatedMediaType: string | undefined; | ||||||
|   private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); |   private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); | ||||||
| 
 | 
 | ||||||
|   static async create(session: TargetSession, browserContext: BrowserContext, defaultViewport: types.Viewport | null): Promise<Page> { |  | ||||||
|     const page = new Page(session, browserContext); |  | ||||||
|     await page._initialize(); |  | ||||||
|     if (defaultViewport) |  | ||||||
|       await page.setViewport(defaultViewport); |  | ||||||
|     return page; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   constructor(session: TargetSession, browserContext: BrowserContext) { |   constructor(session: TargetSession, browserContext: BrowserContext) { | ||||||
|     super(); |     super(); | ||||||
|     this._closedPromise = new Promise(f => this._closedCallback = f); |     this._closedPromise = new Promise(f => this._closedCallback = f); | ||||||
| @ -97,12 +90,16 @@ export class Page extends EventEmitter { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async _initialize() { |   async _initialize() { | ||||||
|     return Promise.all([ |     await Promise.all([ | ||||||
|       this._frameManager.initialize(), |       this._frameManager.initialize(), | ||||||
|       this._session.send('Console.enable'), |       this._session.send('Console.enable'), | ||||||
|       this._session.send('Dialog.enable'), |       this._session.send('Dialog.enable'), | ||||||
|       this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }), |       this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }), | ||||||
|     ]); |     ]); | ||||||
|  |     if (this._userAgent !== null) | ||||||
|  |       await this._session.send('Page.overrideUserAgent', { value: this._userAgent }); | ||||||
|  |     if (this._emulatedMediaType !== undefined) | ||||||
|  |       await this._session.send('Page.setEmulatedMedia', { media: this._emulatedMediaType || '' }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _setSession(newSession: TargetSession) { |   _setSession(newSession: TargetSession) { | ||||||
| @ -130,8 +127,8 @@ export class Page extends EventEmitter { | |||||||
| 
 | 
 | ||||||
|   async _swapSessionOnNavigation(newSession: TargetSession) { |   async _swapSessionOnNavigation(newSession: TargetSession) { | ||||||
|     this._setSession(newSession); |     this._setSession(newSession); | ||||||
|     await this._frameManager._swapSessionOnNavigation(newSession); |     this._frameManager._swapSessionOnNavigation(newSession); | ||||||
|     await this._initialize().catch(e => debugError('failed to enable agents after swap: ' + e)); |     await this._initialize(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   browser(): Browser { |   browser(): Browser { | ||||||
| @ -230,6 +227,7 @@ export class Page extends EventEmitter { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async setUserAgent(userAgent: string) { |   async setUserAgent(userAgent: string) { | ||||||
|  |     this._userAgent = userAgent; | ||||||
|     await this._session.send('Page.overrideUserAgent', { value: userAgent }); |     await this._session.send('Page.overrideUserAgent', { value: userAgent }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -326,9 +324,8 @@ export class Page extends EventEmitter { | |||||||
|     assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); |     assert(!options.type || mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); | ||||||
|     assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); |     assert(!options.colorScheme || mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); | ||||||
|     assert(!options.colorScheme, 'Media feature emulation is not supported'); |     assert(!options.colorScheme, 'Media feature emulation is not supported'); | ||||||
|     const media = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type; |     this._emulatedMediaType = typeof options.type === 'undefined' ? this._emulatedMediaType : options.type; | ||||||
|     await this._session.send('Page.setEmulatedMedia', { media: media || '' }); |     await this._session.send('Page.setEmulatedMedia', { media: this._emulatedMediaType || '' }); | ||||||
|     this._emulatedMediaType = options.type; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async setViewport(viewport: types.Viewport) { |   async setViewport(viewport: types.Viewport) { | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ import { RegisteredListener } from '../helper'; | |||||||
| import { Browser, BrowserContext } from './Browser'; | import { Browser, BrowserContext } from './Browser'; | ||||||
| import { Page } from './Page'; | import { Page } from './Page'; | ||||||
| import { Protocol } from './protocol'; | import { Protocol } from './protocol'; | ||||||
|  | import { isSwappedOutError, TargetSession } from './Connection'; | ||||||
| 
 | 
 | ||||||
| const targetSymbol = Symbol('target'); | const targetSymbol = Symbol('target'); | ||||||
| 
 | 
 | ||||||
| @ -26,7 +27,7 @@ export class Target { | |||||||
|   private _browserContext: BrowserContext; |   private _browserContext: BrowserContext; | ||||||
|   _targetId: string; |   _targetId: string; | ||||||
|   private _type: 'page' | 'service-worker' | 'worker'; |   private _type: 'page' | 'service-worker' | 'worker'; | ||||||
|   _pagePromise: Promise<Page> | null = null; |   private _pagePromise: Promise<Page> | null = null; | ||||||
|   private _url: string; |   private _url: string; | ||||||
|   _initializedPromise: Promise<boolean>; |   _initializedPromise: Promise<boolean>; | ||||||
|   _initializedCallback: (value?: unknown) => void; |   _initializedCallback: (value?: unknown) => void; | ||||||
| @ -52,16 +53,28 @@ export class Target { | |||||||
|       this._pagePromise.then(page => page._didClose()); |       this._pagePromise.then(page => page._didClose()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _adoptPage(page: Page) { |   async _swappedIn(oldTarget: Target, session: TargetSession) { | ||||||
|  |     this._pagePromise = oldTarget._pagePromise; | ||||||
|  |     // Swapped out target should not be accessed by anyone. Reset page promise so that
 | ||||||
|  |     // old target does not close the page on connection reset.
 | ||||||
|  |     oldTarget._pagePromise = null; | ||||||
|  |     if (!this._pagePromise) | ||||||
|  |       return; | ||||||
|  |     const page = await this._pagePromise; | ||||||
|     (page as any)[targetSymbol] = this; |     (page as any)[targetSymbol] = this; | ||||||
|  |     page._swapSessionOnNavigation(session).catch(rethrowIfNotSwapped); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async page(): Promise<Page | null> { |   async page(): Promise<Page | null> { | ||||||
|     if (this._type === 'page' && !this._pagePromise) { |     if (this._type === 'page' && !this._pagePromise) { | ||||||
|       const session = this.browser()._connection.session(this._targetId); |       const session = this.browser()._connection.session(this._targetId); | ||||||
|       this._pagePromise = Page.create(session, this._browserContext, this.browser()._defaultViewport).then(page => { |       this._pagePromise = new Promise(async f => { | ||||||
|         this._adoptPage(page); |         const page = new Page(session, this._browserContext); | ||||||
|         return page; |         await page._initialize().catch(rethrowIfNotSwapped); | ||||||
|  |         if (this.browser()._defaultViewport) | ||||||
|  |           await page.setViewport(this.browser()._defaultViewport); | ||||||
|  |         (page as any)[targetSymbol] = this; | ||||||
|  |         f(page); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     return this._pagePromise; |     return this._pagePromise; | ||||||
| @ -83,3 +96,8 @@ export class Target { | |||||||
|     return this._browserContext; |     return this._browserContext; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | function rethrowIfNotSwapped(e: Error) { | ||||||
|  |   if (!isSwappedOutError(e)) | ||||||
|  |     throw e; | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dmitry Gozman
						Dmitry Gozman