mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
	
	
		
			390 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			390 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|   | /* This Source Code Form is subject to the terms of the Mozilla Public | ||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||
|  | 
 | ||
|  | "use strict"; | ||
|  | 
 | ||
|  | const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); | ||
|  | const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); | ||
|  | const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js'); | ||
|  | const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js'); | ||
|  | 
 | ||
|  | const Cc = Components.classes; | ||
|  | const Ci = Components.interfaces; | ||
|  | const Cu = Components.utils; | ||
|  | const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; | ||
|  | const helper = new Helper(); | ||
|  | 
 | ||
|  | function hashConsoleMessage(params) { | ||
|  |   return params.location.lineNumber + ':' + params.location.columnNumber + ':' + params.location.url; | ||
|  | } | ||
|  | 
 | ||
|  | class WorkerHandler { | ||
|  |   constructor(session, contentChannel, workerId) { | ||
|  |     this._session = session; | ||
|  |     this._contentWorker = contentChannel.connect(workerId); | ||
|  |     this._workerConsoleMessages = new Set(); | ||
|  |     this._workerId = workerId; | ||
|  | 
 | ||
|  |     const emitWrappedProtocolEvent = eventName => { | ||
|  |       return params => { | ||
|  |         this._session.emitEvent('Page.dispatchMessageFromWorker', { | ||
|  |           workerId, | ||
|  |           message: JSON.stringify({method: eventName, params}), | ||
|  |         }); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this._eventListeners = [ | ||
|  |       contentChannel.register(workerId, { | ||
|  |         runtimeConsole: (params) => { | ||
|  |           this._workerConsoleMessages.add(hashConsoleMessage(params)); | ||
|  |           emitWrappedProtocolEvent('Runtime.console')(params); | ||
|  |         }, | ||
|  |         runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'), | ||
|  |         runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'), | ||
|  |       }), | ||
|  |     ]; | ||
|  |   } | ||
|  | 
 | ||
|  |   async sendMessage(message) { | ||
|  |     const [domain, method] = message.method.split('.'); | ||
|  |     if (domain !== 'Runtime') | ||
|  |       throw new Error('ERROR: can only dispatch to Runtime domain inside worker'); | ||
|  |     const result = await this._contentWorker.send(method, message.params); | ||
|  |     this._session.emitEvent('Page.dispatchMessageFromWorker', { | ||
|  |       workerId: this._workerId, | ||
|  |       message: JSON.stringify({result, id: message.id}), | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   dispose() { | ||
|  |     this._contentWorker.dispose(); | ||
|  |     helper.removeListeners(this._eventListeners); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | class PageHandler { | ||
|  |   constructor(target, session, contentChannel) { | ||
|  |     this._session = session; | ||
|  |     this._contentChannel = contentChannel; | ||
|  |     this._contentPage = contentChannel.connect('page'); | ||
|  |     this._workers = new Map(); | ||
|  | 
 | ||
|  |     this._pageTarget = target; | ||
|  |     this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); | ||
|  | 
 | ||
|  |     const emitProtocolEvent = eventName => { | ||
|  |       return (...args) => this._session.emitEvent(eventName, ...args); | ||
|  |     } | ||
|  | 
 | ||
|  |     this._reportedFrameIds = new Set(); | ||
|  |     this._networkEventsForUnreportedFrameIds = new Map(); | ||
|  | 
 | ||
|  |     // `Page.ready` protocol event is emitted whenever page has completed initialization, e.g.
 | ||
|  |     // finished all the transient navigations to the `about:blank`.
 | ||
|  |     //
 | ||
|  |     // We'd like to avoid reporting meaningful events before the `Page.ready` since they are likely
 | ||
|  |     // to be ignored by the protocol clients.
 | ||
|  |     this._isPageReady = false; | ||
|  | 
 | ||
|  |     if (this._pageTarget.screencastInfo()) | ||
|  |       this._onScreencastStarted(); | ||
|  | 
 | ||
|  |     this._eventListeners = [ | ||
|  |       helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)), | ||
|  |       helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)), | ||
|  |       helper.on(this._pageTarget, PageTarget.Events.Crashed, () => { | ||
|  |         this._session.emitEvent('Page.crashed', {}); | ||
|  |       }), | ||
|  |       helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onScreencastStarted.bind(this)), | ||
|  |       helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')), | ||
|  |       helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')), | ||
|  |       helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')), | ||
|  |       helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')), | ||
|  |       contentChannel.register('page', { | ||
|  |         pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), | ||
|  |         pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), | ||
|  |         pageEventFired: emitProtocolEvent('Page.eventFired'), | ||
|  |         pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'), | ||
|  |         pageFrameAttached: this._onFrameAttached.bind(this), | ||
|  |         pageFrameDetached: emitProtocolEvent('Page.frameDetached'), | ||
|  |         pageLinkClicked: emitProtocolEvent('Page.linkClicked'), | ||
|  |         pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'), | ||
|  |         pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'), | ||
|  |         pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'), | ||
|  |         pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'), | ||
|  |         pageReady: this._onPageReady.bind(this), | ||
|  |         pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'), | ||
|  |         pageUncaughtError: emitProtocolEvent('Page.uncaughtError'), | ||
|  |         pageWorkerCreated: this._onWorkerCreated.bind(this), | ||
|  |         pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), | ||
|  |         runtimeConsole: params => { | ||
|  |           const consoleMessageHash = hashConsoleMessage(params); | ||
|  |           for (const worker of this._workers) { | ||
|  |             if (worker._workerConsoleMessages.has(consoleMessageHash)) { | ||
|  |               worker._workerConsoleMessages.delete(consoleMessageHash); | ||
|  |               return; | ||
|  |             } | ||
|  |           } | ||
|  |           emitProtocolEvent('Runtime.console')(params); | ||
|  |         }, | ||
|  |         runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), | ||
|  |         runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), | ||
|  | 
 | ||
|  |         webSocketCreated: emitProtocolEvent('Page.webSocketCreated'), | ||
|  |         webSocketOpened: emitProtocolEvent('Page.webSocketOpened'), | ||
|  |         webSocketClosed: emitProtocolEvent('Page.webSocketClosed'), | ||
|  |         webSocketFrameReceived: emitProtocolEvent('Page.webSocketFrameReceived'), | ||
|  |         webSocketFrameSent: emitProtocolEvent('Page.webSocketFrameSent'), | ||
|  |       }), | ||
|  |     ]; | ||
|  |   } | ||
|  | 
 | ||
|  |   async dispose() { | ||
|  |     this._contentPage.dispose(); | ||
|  |     helper.removeListeners(this._eventListeners); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onScreencastStarted() { | ||
|  |     const info = this._pageTarget.screencastInfo(); | ||
|  |     this._session.emitEvent('Page.screencastStarted', { screencastId: info.videoSessionId, file: info.file }); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onPageReady(event) { | ||
|  |     this._isPageReady = true; | ||
|  |     this._session.emitEvent('Page.ready'); | ||
|  |     for (const dialog of this._pageTarget.dialogs()) | ||
|  |       this._onDialogOpened(dialog); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onDialogOpened(dialog) { | ||
|  |     if (!this._isPageReady) | ||
|  |       return; | ||
|  |     this._session.emitEvent('Page.dialogOpened', { | ||
|  |       dialogId: dialog.id(), | ||
|  |       type: dialog.type(), | ||
|  |       message: dialog.message(), | ||
|  |       defaultValue: dialog.defaultValue(), | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onDialogClosed(dialog) { | ||
|  |     if (!this._isPageReady) | ||
|  |       return; | ||
|  |     this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), }); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onWorkerCreated({workerId, frameId, url}) { | ||
|  |     const worker = new WorkerHandler(this._session, this._contentChannel, workerId); | ||
|  |     this._workers.set(workerId, worker); | ||
|  |     this._session.emitEvent('Page.workerCreated', {workerId, frameId, url}); | ||
|  |   } | ||
|  | 
 | ||
|  |   _onWorkerDestroyed({workerId}) { | ||
|  |     const worker = this._workers.get(workerId); | ||
|  |     if (!worker) | ||
|  |       return; | ||
|  |     this._workers.delete(workerId); | ||
|  |     worker.dispose(); | ||
|  |     this._session.emitEvent('Page.workerDestroyed', {workerId}); | ||
|  |   } | ||
|  | 
 | ||
|  |   _handleNetworkEvent(protocolEventName, eventDetails, frameId) { | ||
|  |     if (!this._reportedFrameIds.has(frameId)) { | ||
|  |       let events = this._networkEventsForUnreportedFrameIds.get(frameId); | ||
|  |       if (!events) { | ||
|  |         events = []; | ||
|  |         this._networkEventsForUnreportedFrameIds.set(frameId, events); | ||
|  |       } | ||
|  |       events.push({eventName: protocolEventName, eventDetails}); | ||
|  |     } else { | ||
|  |       this._session.emitEvent(protocolEventName, eventDetails); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   _onFrameAttached({frameId, parentFrameId}) { | ||
|  |     this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId}); | ||
|  |     this._reportedFrameIds.add(frameId); | ||
|  |     const events = this._networkEventsForUnreportedFrameIds.get(frameId) || []; | ||
|  |     this._networkEventsForUnreportedFrameIds.delete(frameId); | ||
|  |     for (const {eventName, eventDetails} of events) | ||
|  |       this._session.emitEvent(eventName, eventDetails); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.close']({runBeforeUnload}) { | ||
|  |     // Postpone target close to deliver response in session.
 | ||
|  |     Services.tm.dispatchToMainThread(() => { | ||
|  |       this._pageTarget.close(runBeforeUnload); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.setViewportSize']({viewportSize}) { | ||
|  |     await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Runtime.evaluate'](options) { | ||
|  |     return await this._contentPage.send('evaluate', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Runtime.callFunction'](options) { | ||
|  |     return await this._contentPage.send('callFunction', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Runtime.getObjectProperties'](options) { | ||
|  |     return await this._contentPage.send('getObjectProperties', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Runtime.disposeObject'](options) { | ||
|  |     return await this._contentPage.send('disposeObject', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Network.getResponseBody']({requestId}) { | ||
|  |     return this._pageNetwork.getResponseBody(requestId); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Network.setExtraHTTPHeaders']({headers}) { | ||
|  |     this._pageNetwork.setExtraHTTPHeaders(headers); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Network.setRequestInterception']({enabled}) { | ||
|  |     if (enabled) | ||
|  |       this._pageNetwork.enableRequestInterception(); | ||
|  |     else | ||
|  |       this._pageNetwork.disableRequestInterception(); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Network.resumeInterceptedRequest']({requestId, url, method, headers, postData}) { | ||
|  |     this._pageNetwork.resumeInterceptedRequest(requestId, url, method, headers, postData); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Network.abortInterceptedRequest']({requestId, errorCode}) { | ||
|  |     this._pageNetwork.abortInterceptedRequest(requestId, errorCode); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) { | ||
|  |     this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Accessibility.getFullAXTree'](params) { | ||
|  |     return await this._contentPage.send('getFullAXTree', params); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.setFileInputFiles'](options) { | ||
|  |     return await this._contentPage.send('setFileInputFiles', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.setEmulatedMedia']({colorScheme, type}) { | ||
|  |     this._pageTarget.setColorScheme(colorScheme || null); | ||
|  |     this._pageTarget.setEmulatedMedia(type); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.bringToFront'](options) { | ||
|  |     this._pageTarget._window.focus(); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.setCacheDisabled'](options) { | ||
|  |     return await this._contentPage.send('setCacheDisabled', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.addBinding'](options) { | ||
|  |     return await this._contentPage.send('addBinding', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.adoptNode'](options) { | ||
|  |     return await this._contentPage.send('adoptNode', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.screenshot'](options) { | ||
|  |     return await this._contentPage.send('screenshot', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.getContentQuads'](options) { | ||
|  |     return await this._contentPage.send('getContentQuads', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.navigate'](options) { | ||
|  |     return await this._contentPage.send('navigate', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.goBack'](options) { | ||
|  |     return await this._contentPage.send('goBack', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.goForward'](options) { | ||
|  |     return await this._contentPage.send('goForward', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.reload'](options) { | ||
|  |     return await this._contentPage.send('reload', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.describeNode'](options) { | ||
|  |     return await this._contentPage.send('describeNode', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.scrollIntoViewIfNeeded'](options) { | ||
|  |     return await this._contentPage.send('scrollIntoViewIfNeeded', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.addScriptToEvaluateOnNewDocument'](options) { | ||
|  |     return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.removeScriptToEvaluateOnNewDocument'](options) { | ||
|  |     return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.dispatchKeyEvent'](options) { | ||
|  |     return await this._contentPage.send('dispatchKeyEvent', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.dispatchTouchEvent'](options) { | ||
|  |     return await this._contentPage.send('dispatchTouchEvent', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.dispatchTapEvent'](options) { | ||
|  |     return await this._contentPage.send('dispatchTapEvent', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.dispatchMouseEvent'](options) { | ||
|  |     return await this._contentPage.send('dispatchMouseEvent', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.insertText'](options) { | ||
|  |     return await this._contentPage.send('insertText', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.crash'](options) { | ||
|  |     return await this._contentPage.send('crash', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.handleDialog']({dialogId, accept, promptText}) { | ||
|  |     const dialog = this._pageTarget.dialog(dialogId); | ||
|  |     if (!dialog) | ||
|  |       throw new Error('Failed to find dialog with id = ' + dialogId); | ||
|  |     if (accept) | ||
|  |       dialog.accept(promptText); | ||
|  |     else | ||
|  |       dialog.dismiss(); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.setInterceptFileChooserDialog'](options) { | ||
|  |     return await this._contentPage.send('setInterceptFileChooserDialog', options); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.sendMessageToWorker']({workerId, message}) { | ||
|  |     const worker = this._workers.get(workerId); | ||
|  |     if (!worker) | ||
|  |       throw new Error('ERROR: cannot find worker with id ' + workerId); | ||
|  |     return await worker.sendMessage(JSON.parse(message)); | ||
|  |   } | ||
|  | 
 | ||
|  |   async ['Page.stopVideoRecording']() { | ||
|  |     await this._pageTarget.stopVideoRecording(); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | var EXPORTED_SYMBOLS = ['PageHandler']; | ||
|  | this.PageHandler = PageHandler; |