mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1048 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1048 lines
		
	
	
		
			36 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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| const Ci = Components.interfaces;
 | |
| const Cr = Components.results;
 | |
| const Cu = Components.utils;
 | |
| 
 | |
| const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
 | |
| const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
 | |
|   Ci.nsIDragService
 | |
| );
 | |
| const obs = Cc["@mozilla.org/observer-service;1"].getService(
 | |
|   Ci.nsIObserverService
 | |
| );
 | |
| 
 | |
| const helper = new Helper();
 | |
| 
 | |
| class WorkerData {
 | |
|   constructor(pageAgent, browserChannel, worker) {
 | |
|     this._workerRuntime = worker.channel().connect('runtime');
 | |
|     this._browserWorker = browserChannel.connect(worker.id());
 | |
|     this._worker = worker;
 | |
|     const emit = name => {
 | |
|       return (...args) => this._browserWorker.emit(name, ...args);
 | |
|     };
 | |
|     this._eventListeners = [
 | |
|       worker.channel().register('runtime', {
 | |
|         runtimeConsole: emit('runtimeConsole'),
 | |
|         runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'),
 | |
|         runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'),
 | |
|       }),
 | |
|       browserChannel.register(worker.id(), {
 | |
|         evaluate: (options) => this._workerRuntime.send('evaluate', options),
 | |
|         callFunction: (options) => this._workerRuntime.send('callFunction', options),
 | |
|         getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options),
 | |
|         disposeObject: (options) =>this._workerRuntime.send('disposeObject', options),
 | |
|       }),
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   dispose() {
 | |
|     this._workerRuntime.dispose();
 | |
|     this._browserWorker.dispose();
 | |
|     helper.removeListeners(this._eventListeners);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class FrameData {
 | |
|   constructor(agent, runtime, frame) {
 | |
|     this._agent = agent;
 | |
|     this._runtime = runtime;
 | |
|     this._frame = frame;
 | |
|     this._isolatedWorlds = new Map();
 | |
|     this._initialNavigationDone = false;
 | |
|     this.reset();
 | |
|   }
 | |
| 
 | |
|   reset() {
 | |
|     for (const world of this._isolatedWorlds.values())
 | |
|       this._runtime.destroyExecutionContext(world);
 | |
|     this._isolatedWorlds.clear();
 | |
| 
 | |
|     for (const {script, worldName} of this._agent._isolatedWorlds.values()) {
 | |
|       const context = worldName ? this.createIsolatedWorld(worldName) : this._frame.executionContext();
 | |
|       try {
 | |
|         let result = context.evaluateScript(script);
 | |
|         if (result && result.objectId)
 | |
|           context.disposeObject(result.objectId);
 | |
|       } catch (e) {
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   createIsolatedWorld(name) {
 | |
|     const principal = [this._frame.domWindow()]; // extended principal
 | |
|     const sandbox = Cu.Sandbox(principal, {
 | |
|       sandboxPrototype: this._frame.domWindow(),
 | |
|       wantComponents: false,
 | |
|       wantExportHelpers: false,
 | |
|       wantXrays: true,
 | |
|     });
 | |
|     const world = this._runtime.createExecutionContext(this._frame.domWindow(), sandbox, {
 | |
|       frameId: this._frame.id(),
 | |
|       name,
 | |
|     });
 | |
|     this._isolatedWorlds.set(world.id(), world);
 | |
|     return world;
 | |
|   }
 | |
| 
 | |
|   unsafeObject(objectId) {
 | |
|     const contexts = [this._frame.executionContext(), ...this._isolatedWorlds.values()];
 | |
|     for (const context of contexts) {
 | |
|       const result = context.unsafeObject(objectId);
 | |
|       if (result)
 | |
|         return result.object;
 | |
|     }
 | |
|     throw new Error('Cannot find object with id = ' + objectId);
 | |
|   }
 | |
| 
 | |
|   dispose() {
 | |
|     for (const world of this._isolatedWorlds.values())
 | |
|       this._runtime.destroyExecutionContext(world);
 | |
|     this._isolatedWorlds.clear();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PageAgent {
 | |
|   constructor(messageManager, browserChannel, frameTree) {
 | |
|     this._messageManager = messageManager;
 | |
|     this._browserChannel = browserChannel;
 | |
|     this._browserPage = browserChannel.connect('page');
 | |
|     this._frameTree = frameTree;
 | |
|     this._runtime = frameTree.runtime();
 | |
| 
 | |
|     this._frameData = new Map();
 | |
|     this._workerData = new Map();
 | |
|     this._scriptsToEvaluateOnNewDocument = new Map();
 | |
|     this._isolatedWorlds = new Map();
 | |
| 
 | |
|     const docShell = frameTree.mainFrame().docShell();
 | |
|     this._docShell = docShell;
 | |
|     this._initialDPPX = docShell.contentViewer.overrideDPPX;
 | |
|     this._customScrollbars = null;
 | |
|     this._dragging = false;
 | |
| 
 | |
|     // Dispatch frameAttached events for all initial frames
 | |
|     for (const frame of this._frameTree.frames()) {
 | |
|       this._onFrameAttached(frame);
 | |
|       if (frame.url())
 | |
|         this._onNavigationCommitted(frame);
 | |
|       if (frame.pendingNavigationId())
 | |
|         this._onNavigationStarted(frame);
 | |
|     }
 | |
| 
 | |
|     // Report created workers.
 | |
|     for (const worker of this._frameTree.workers())
 | |
|       this._onWorkerCreated(worker);
 | |
| 
 | |
|     // Report execution contexts.
 | |
|     for (const context of this._runtime.executionContexts())
 | |
|       this._onExecutionContextCreated(context);
 | |
| 
 | |
|     if (this._frameTree.isPageReady()) {
 | |
|       this._browserPage.emit('pageReady', {});
 | |
|       const mainFrame = this._frameTree.mainFrame();
 | |
|       const domWindow = mainFrame.domWindow();
 | |
|       const document = domWindow ? domWindow.document : null;
 | |
|       const readyState = document ? document.readyState : null;
 | |
|       // Sometimes we initialize later than the first about:blank page is opened.
 | |
|       // In this case, the page might've been loaded already, and we need to issue
 | |
|       // the `DOMContentLoaded` and `load` events.
 | |
|       if (mainFrame.url() === 'about:blank' && readyState === 'complete')
 | |
|         this._emitAllEvents(this._frameTree.mainFrame());
 | |
|     }
 | |
| 
 | |
|     this._eventListeners = [
 | |
|       helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'),
 | |
|       helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'),
 | |
|       helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'),
 | |
|       helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
 | |
|       helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
 | |
|       helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
 | |
|       helper.addEventListener(this._messageManager, 'error', this._onError.bind(this)),
 | |
|       helper.on(this._frameTree, 'load', this._onLoad.bind(this)),
 | |
|       helper.on(this._frameTree, 'bindingcalled', this._onBindingCalled.bind(this)),
 | |
|       helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
 | |
|       helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
 | |
|       helper.on(this._frameTree, 'globalobjectcreated', this._onGlobalObjectCreated.bind(this)),
 | |
|       helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
 | |
|       helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
 | |
|       helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
 | |
|       helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
 | |
|       helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})),
 | |
|       helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
 | |
|       helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
 | |
|       helper.on(this._frameTree, 'websocketcreated', event => this._browserPage.emit('webSocketCreated', event)),
 | |
|       helper.on(this._frameTree, 'websocketopened', event => this._browserPage.emit('webSocketOpened', event)),
 | |
|       helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)),
 | |
|       helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)),
 | |
|       helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)),
 | |
|       helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
 | |
|       this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
 | |
|         const frame = this._frameTree.frameForDocShell(domWindow.docShell);
 | |
|         if (!frame)
 | |
|           return;
 | |
|         this._browserPage.emit('pageUncaughtError', {
 | |
|           frameId: frame.id(),
 | |
|           message,
 | |
|           stack,
 | |
|         });
 | |
|       }),
 | |
|       this._runtime.events.onConsoleMessage(msg => this._browserPage.emit('runtimeConsole', msg)),
 | |
|       this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
 | |
|       this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
 | |
|       browserChannel.register('page', {
 | |
|         addBinding: ({ name, script }) => this._frameTree.addBinding(name, script),
 | |
|         addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this),
 | |
|         adoptNode: this._adoptNode.bind(this),
 | |
|         crash: this._crash.bind(this),
 | |
|         describeNode: this._describeNode.bind(this),
 | |
|         dispatchKeyEvent: this._dispatchKeyEvent.bind(this),
 | |
|         dispatchMouseEvent: this._dispatchMouseEvent.bind(this),
 | |
|         dispatchTouchEvent: this._dispatchTouchEvent.bind(this),
 | |
|         dispatchTapEvent: this._dispatchTapEvent.bind(this),
 | |
|         getBoundingBox: this._getBoundingBox.bind(this),
 | |
|         getContentQuads: this._getContentQuads.bind(this),
 | |
|         getFullAXTree: this._getFullAXTree.bind(this),
 | |
|         goBack: this._goBack.bind(this),
 | |
|         goForward: this._goForward.bind(this),
 | |
|         insertText: this._insertText.bind(this),
 | |
|         navigate: this._navigate.bind(this),
 | |
|         reload: this._reload.bind(this),
 | |
|         removeScriptToEvaluateOnNewDocument: this._removeScriptToEvaluateOnNewDocument.bind(this),
 | |
|         screenshot: this._screenshot.bind(this),
 | |
|         scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
 | |
|         setCacheDisabled: this._setCacheDisabled.bind(this),
 | |
|         setFileInputFiles: this._setFileInputFiles.bind(this),
 | |
|         setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this),
 | |
|         evaluate: this._runtime.evaluate.bind(this._runtime),
 | |
|         callFunction: this._runtime.callFunction.bind(this._runtime),
 | |
|         getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
 | |
|         disposeObject: this._runtime.disposeObject.bind(this._runtime),
 | |
|       }),
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   _addScriptToEvaluateOnNewDocument({script, worldName}) {
 | |
|     if (worldName)
 | |
|       return this._createIsolatedWorld({script, worldName});
 | |
|     return {scriptId: this._frameTree.addScriptToEvaluateOnNewDocument(script)};
 | |
|   }
 | |
| 
 | |
|   _createIsolatedWorld({script, worldName}) {
 | |
|     const scriptId = helper.generateId();
 | |
|     this._isolatedWorlds.set(scriptId, {script, worldName});
 | |
|     for (const frameData of this._frameData.values())
 | |
|       frameData.createIsolatedWorld(worldName);
 | |
|     return {scriptId};
 | |
|   }
 | |
| 
 | |
|   _removeScriptToEvaluateOnNewDocument({scriptId}) {
 | |
|     if (this._isolatedWorlds.has(scriptId))
 | |
|       this._isolatedWorlds.delete(scriptId);
 | |
|     else
 | |
|       this._frameTree.removeScriptToEvaluateOnNewDocument(scriptId);
 | |
|   }
 | |
| 
 | |
|   _setCacheDisabled({cacheDisabled}) {
 | |
|     const enable = Ci.nsIRequest.LOAD_NORMAL;
 | |
|     const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
 | |
|                   Ci.nsIRequest.INHIBIT_CACHING;
 | |
| 
 | |
|     const docShell = this._frameTree.mainFrame().docShell();
 | |
|     docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
 | |
|   }
 | |
| 
 | |
|   _emitAllEvents(frame) {
 | |
|     this._browserPage.emit('pageEventFired', {
 | |
|       frameId: frame.id(),
 | |
|       name: 'DOMContentLoaded',
 | |
|     });
 | |
|     this._browserPage.emit('pageEventFired', {
 | |
|       frameId: frame.id(),
 | |
|       name: 'load',
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onExecutionContextCreated(executionContext) {
 | |
|     this._browserPage.emit('runtimeExecutionContextCreated', {
 | |
|       executionContextId: executionContext.id(),
 | |
|       auxData: executionContext.auxData(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onExecutionContextDestroyed(executionContext) {
 | |
|     this._browserPage.emit('runtimeExecutionContextDestroyed', {
 | |
|       executionContextId: executionContext.id(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onWorkerCreated(worker) {
 | |
|     const workerData = new WorkerData(this, this._browserChannel, worker);
 | |
|     this._workerData.set(worker.id(), workerData);
 | |
|     this._browserPage.emit('pageWorkerCreated', {
 | |
|       workerId: worker.id(),
 | |
|       frameId: worker.frame().id(),
 | |
|       url: worker.url(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onWorkerDestroyed(worker) {
 | |
|     const workerData = this._workerData.get(worker.id());
 | |
|     if (!workerData)
 | |
|       return;
 | |
|     this._workerData.delete(worker.id());
 | |
|     workerData.dispose();
 | |
|     this._browserPage.emit('pageWorkerDestroyed', {
 | |
|       workerId: worker.id(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onWindowOpen(subject) {
 | |
|     if (!(subject instanceof Ci.nsIPropertyBag2))
 | |
|       return;
 | |
|     const props = subject.QueryInterface(Ci.nsIPropertyBag2);
 | |
|     const hasUrl = props.hasKey('url');
 | |
|     const createdDocShell = props.getPropertyAsInterface('createdTabDocShell', Ci.nsIDocShell);
 | |
|     if (!hasUrl && createdDocShell === this._docShell && this._frameTree.forcePageReady())
 | |
|       this._emitAllEvents(this._frameTree.mainFrame());
 | |
|   }
 | |
| 
 | |
|   _setInterceptFileChooserDialog({enabled}) {
 | |
|     this._docShell.fileInputInterceptionEnabled = !!enabled;
 | |
|   }
 | |
| 
 | |
|   _linkClicked(sync, anchorElement) {
 | |
|     if (anchorElement.ownerGlobal.docShell !== this._docShell)
 | |
|       return;
 | |
|     this._browserPage.emit('pageLinkClicked', { phase: sync ? 'after' : 'before' });
 | |
|   }
 | |
| 
 | |
|   _onWindowOpenInNewContext(docShell) {
 | |
|     // TODO: unify this with _onWindowOpen if possible.
 | |
|     const frame = this._frameTree.frameForDocShell(docShell);
 | |
|     if (!frame)
 | |
|       return;
 | |
|     this._browserPage.emit('pageWillOpenNewWindowAsynchronously');
 | |
|   }
 | |
| 
 | |
|   _filePickerShown(inputElement) {
 | |
|     if (inputElement.ownerGlobal.docShell !== this._docShell)
 | |
|       return;
 | |
|     const frameData = this._findFrameForNode(inputElement);
 | |
|     this._browserPage.emit('pageFileChooserOpened', {
 | |
|       executionContextId: frameData._frame.executionContext().id(),
 | |
|       element: frameData._frame.executionContext().rawValueToRemoteObject(inputElement)
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _findFrameForNode(node) {
 | |
|     return Array.from(this._frameData.values()).find(data => {
 | |
|       const doc = data._frame.domWindow().document;
 | |
|       return node === doc || node.ownerDocument === doc;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onDOMContentLoaded(event) {
 | |
|     if (!event.target.ownerGlobal)
 | |
|       return;
 | |
|     const docShell = event.target.ownerGlobal.docShell;
 | |
|     const frame = this._frameTree.frameForDocShell(docShell);
 | |
|     if (!frame)
 | |
|       return;
 | |
|     this._browserPage.emit('pageEventFired', {
 | |
|       frameId: frame.id(),
 | |
|       name: 'DOMContentLoaded',
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onError(errorEvent) {
 | |
|     const docShell = errorEvent.target.ownerGlobal.docShell;
 | |
|     const frame = this._frameTree.frameForDocShell(docShell);
 | |
|     if (!frame)
 | |
|       return;
 | |
|     this._browserPage.emit('pageUncaughtError', {
 | |
|       frameId: frame.id(),
 | |
|       message: errorEvent.message,
 | |
|       stack: errorEvent.error && typeof errorEvent.error.stack === 'string' ? errorEvent.error.stack : '',
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onDocumentOpenLoad(document) {
 | |
|     const docShell = document.ownerGlobal.docShell;
 | |
|     const frame = this._frameTree.frameForDocShell(docShell);
 | |
|     if (!frame)
 | |
|       return;
 | |
|     this._browserPage.emit('pageEventFired', {
 | |
|       frameId: frame.id(),
 | |
|       name: 'load'
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onLoad(frame) {
 | |
|     this._browserPage.emit('pageEventFired', {
 | |
|       frameId: frame.id(),
 | |
|       name: 'load'
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onNavigationStarted(frame) {
 | |
|     this._browserPage.emit('pageNavigationStarted', {
 | |
|       frameId: frame.id(),
 | |
|       navigationId: frame.pendingNavigationId(),
 | |
|       url: frame.pendingNavigationURL(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onNavigationAborted(frame, navigationId, errorText) {
 | |
|     this._browserPage.emit('pageNavigationAborted', {
 | |
|       frameId: frame.id(),
 | |
|       navigationId,
 | |
|       errorText,
 | |
|     });
 | |
|     const frameData = this._frameData.get(frame);
 | |
|     if (!frameData._initialNavigationDone && frame !== this._frameTree.mainFrame())
 | |
|       this._emitAllEvents(frame);
 | |
|     frameData._initialNavigationDone = true;
 | |
|   }
 | |
| 
 | |
|   _onSameDocumentNavigation(frame) {
 | |
|     this._browserPage.emit('pageSameDocumentNavigation', {
 | |
|       frameId: frame.id(),
 | |
|       url: frame.url(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onNavigationCommitted(frame) {
 | |
|     this._browserPage.emit('pageNavigationCommitted', {
 | |
|       frameId: frame.id(),
 | |
|       navigationId: frame.lastCommittedNavigationId() || undefined,
 | |
|       url: frame.url(),
 | |
|       name: frame.name(),
 | |
|     });
 | |
|     this._frameData.get(frame)._initialNavigationDone = true;
 | |
|   }
 | |
| 
 | |
|   _onGlobalObjectCreated({ frame }) {
 | |
|     this._frameData.get(frame).reset();
 | |
|   }
 | |
| 
 | |
|   _onFrameAttached(frame) {
 | |
|     this._browserPage.emit('pageFrameAttached', {
 | |
|       frameId: frame.id(),
 | |
|       parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
 | |
|     });
 | |
|     this._frameData.set(frame, new FrameData(this, this._runtime, frame));
 | |
|   }
 | |
| 
 | |
|   _onFrameDetached(frame) {
 | |
|     this._frameData.delete(frame);
 | |
|     this._browserPage.emit('pageFrameDetached', {
 | |
|       frameId: frame.id(),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onBindingCalled({frame, name, payload}) {
 | |
|     this._browserPage.emit('pageBindingCalled', {
 | |
|       executionContextId: frame.executionContext().id(),
 | |
|       name,
 | |
|       payload
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   dispose() {
 | |
|     for (const workerData of this._workerData.values())
 | |
|       workerData.dispose();
 | |
|     this._workerData.clear();
 | |
|     for (const frameData of this._frameData.values())
 | |
|       frameData.dispose();
 | |
|     this._frameData.clear();
 | |
|     helper.removeListeners(this._eventListeners);
 | |
|   }
 | |
| 
 | |
|   async _navigate({frameId, url, referer}) {
 | |
|     try {
 | |
|       const uri = NetUtil.newURI(url);
 | |
|     } catch (e) {
 | |
|       throw new Error(`Invalid url: "${url}"`);
 | |
|     }
 | |
|     let referrerURI = null;
 | |
|     let referrerInfo = null;
 | |
|     if (referer) {
 | |
|       try {
 | |
|         referrerURI = NetUtil.newURI(referer);
 | |
|         const ReferrerInfo = Components.Constructor(
 | |
|           '@mozilla.org/referrer-info;1',
 | |
|           'nsIReferrerInfo',
 | |
|           'init'
 | |
|         );
 | |
|         referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI);
 | |
|       } catch (e) {
 | |
|         throw new Error(`Invalid referer: "${referer}"`);
 | |
|       }
 | |
|     }
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
 | |
|     docShell.loadURI(url, {
 | |
|       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
 | |
|       flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
 | |
|       referrerInfo,
 | |
|       postData: null,
 | |
|       headers: null,
 | |
|     });
 | |
|     return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
 | |
|   }
 | |
| 
 | |
|   async _reload({frameId, url}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
 | |
|     docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
 | |
|   }
 | |
| 
 | |
|   async _goBack({frameId, url}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     const docShell = frame.docShell();
 | |
|     if (!docShell.canGoBack)
 | |
|       return {success: false};
 | |
|     docShell.goBack();
 | |
|     return {success: true};
 | |
|   }
 | |
| 
 | |
|   async _goForward({frameId, url}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     const docShell = frame.docShell();
 | |
|     if (!docShell.canGoForward)
 | |
|       return {success: false};
 | |
|     docShell.goForward();
 | |
|     return {success: true};
 | |
|   }
 | |
| 
 | |
|   async _adoptNode({frameId, objectId, executionContextId}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     if (!frame)
 | |
|       throw new Error('Failed to find frame with id = ' + frameId);
 | |
|     const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
|     const context = this._runtime.findExecutionContext(executionContextId);
 | |
|     const fromPrincipal = unsafeObject.nodePrincipal;
 | |
|     const toFrame = this._frameTree.frame(context.auxData().frameId);
 | |
|     const toPrincipal = toFrame.domWindow().document.nodePrincipal;
 | |
|     if (!toPrincipal.subsumes(fromPrincipal))
 | |
|       return { remoteObject: null };
 | |
|     return { remoteObject: context.rawValueToRemoteObject(unsafeObject) };
 | |
|   }
 | |
| 
 | |
|   async _setFileInputFiles({objectId, frameId, files}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     if (!frame)
 | |
|       throw new Error('Failed to find frame with id = ' + frameId);
 | |
|     const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
|     if (!unsafeObject)
 | |
|       throw new Error('Object is not input!');
 | |
|     const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
 | |
|     unsafeObject.mozSetFileArray(nsFiles);
 | |
|   }
 | |
| 
 | |
|   _getContentQuads({objectId, frameId}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     if (!frame)
 | |
|       throw new Error('Failed to find frame with id = ' + frameId);
 | |
|     const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
|     if (!unsafeObject.getBoxQuads)
 | |
|       throw new Error('RemoteObject is not a node');
 | |
|     const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => {
 | |
|       return {
 | |
|         p1: {x: quad.p1.x, y: quad.p1.y},
 | |
|         p2: {x: quad.p2.x, y: quad.p2.y},
 | |
|         p3: {x: quad.p3.x, y: quad.p3.y},
 | |
|         p4: {x: quad.p4.x, y: quad.p4.y},
 | |
|       };
 | |
|     });
 | |
|     return {quads};
 | |
|   }
 | |
| 
 | |
|   _describeNode({objectId, frameId}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     if (!frame)
 | |
|       throw new Error('Failed to find frame with id = ' + frameId);
 | |
|     const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
|     const browsingContextGroup = frame.docShell().browsingContext.group;
 | |
|     const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup);
 | |
|     let contentFrame;
 | |
|     let ownerFrame;
 | |
|     for (const frame of frames) {
 | |
|       if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell)
 | |
|         contentFrame = frame;
 | |
|       const document = frame.domWindow().document;
 | |
|       if (unsafeObject === document || unsafeObject.ownerDocument === document)
 | |
|         ownerFrame = frame;
 | |
|     }
 | |
|     return {
 | |
|       contentFrameId: contentFrame ? contentFrame.id() : undefined,
 | |
|       ownerFrameId: ownerFrame ? ownerFrame.id() : undefined,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   async _scrollIntoViewIfNeeded({objectId, frameId, rect}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     if (!frame)
 | |
|       throw new Error('Failed to find frame with id = ' + frameId);
 | |
|     const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
|     if (!unsafeObject.isConnected)
 | |
|       throw new Error('Node is detached from document');
 | |
|     if (!rect)
 | |
|       rect = { x: -1, y: -1, width: -1, height: -1};
 | |
|     if (unsafeObject.scrollRectIntoViewIfNeeded)
 | |
|       unsafeObject.scrollRectIntoViewIfNeeded(rect.x, rect.y, rect.width, rect.height);
 | |
|     else
 | |
|       throw new Error('Node does not have a layout object');
 | |
|   }
 | |
| 
 | |
|   _getNodeBoundingBox(unsafeObject) {
 | |
|     if (!unsafeObject.getBoxQuads)
 | |
|       throw new Error('RemoteObject is not a node');
 | |
|     const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document});
 | |
|     if (!quads.length)
 | |
|       return;
 | |
|     let x1 = Infinity;
 | |
|     let y1 = Infinity;
 | |
|     let x2 = -Infinity;
 | |
|     let y2 = -Infinity;
 | |
|     for (const quad of quads) {
 | |
|       const boundingBox = quad.getBounds();
 | |
|       x1 = Math.min(boundingBox.x, x1);
 | |
|       y1 = Math.min(boundingBox.y, y1);
 | |
|       x2 = Math.max(boundingBox.x + boundingBox.width, x2);
 | |
|       y2 = Math.max(boundingBox.y + boundingBox.height, y2);
 | |
|     }
 | |
|     return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
 | |
|   }
 | |
| 
 | |
|   async _getBoundingBox({frameId, objectId}) {
 | |
|     const frame = this._frameTree.frame(frameId);
 | |
|     if (!frame)
 | |
|       throw new Error('Failed to find frame with id = ' + frameId);
 | |
|     const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
|     const box = this._getNodeBoundingBox(unsafeObject);
 | |
|     if (!box)
 | |
|       return {boundingBox: null};
 | |
|     return {boundingBox: {x: box.x + frame.domWindow().scrollX, y: box.y + frame.domWindow().scrollY, width: box.width, height: box.height}};
 | |
|   }
 | |
| 
 | |
|   async _screenshot({mimeType, fullPage, clip}) {
 | |
|     const content = this._messageManager.content;
 | |
|     if (clip) {
 | |
|       const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType);
 | |
|       return {data};
 | |
|     }
 | |
|     if (fullPage) {
 | |
|       const rect = content.document.documentElement.getBoundingClientRect();
 | |
|       const width = content.innerWidth + content.scrollMaxX - content.scrollMinX;
 | |
|       const height = content.innerHeight + content.scrollMaxY - content.scrollMinY;
 | |
|       const data = takeScreenshot(content, 0, 0, width, height, mimeType);
 | |
|       return {data};
 | |
|     }
 | |
|     const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType);
 | |
|     return {data};
 | |
|   }
 | |
| 
 | |
|   async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
 | |
|     // key events don't fire if we are dragging.
 | |
|     if (this._dragging) {
 | |
|       if (type === 'keydown' && key === 'Escape')
 | |
|         this._cancelDragIfNeeded();
 | |
|       return;
 | |
|     }
 | |
|     const frame = this._frameTree.mainFrame();
 | |
|     const tip = frame.textInputProcessor();
 | |
|     if (key === 'Meta' && Services.appinfo.OS !== 'Darwin')
 | |
|       key = 'OS';
 | |
|     else if (key === 'OS' && Services.appinfo.OS === 'Darwin')
 | |
|       key = 'Meta';
 | |
|     let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
 | |
|       key,
 | |
|       code,
 | |
|       location,
 | |
|       repeat,
 | |
|       keyCode
 | |
|     });
 | |
|     if (type === 'keydown') {
 | |
|       if (text && text !== key) {
 | |
|         tip.commitCompositionWith(text, keyEvent);
 | |
|       } else {
 | |
|         const flags = 0;
 | |
|         tip.keydown(keyEvent, flags);
 | |
|       }
 | |
|     } else if (type === 'keyup') {
 | |
|       if (text)
 | |
|         throw new Error(`keyup does not support text option`);
 | |
|       const flags = 0;
 | |
|       tip.keyup(keyEvent, flags);
 | |
|     } else {
 | |
|       throw new Error(`Unknown type ${type}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async _dispatchTouchEvent({type, touchPoints, modifiers}) {
 | |
|     const frame = this._frameTree.mainFrame();
 | |
|     const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent(
 | |
|       type.toLowerCase(),
 | |
|       touchPoints.map((point, id) => id),
 | |
|       touchPoints.map(point => point.x),
 | |
|       touchPoints.map(point => point.y),
 | |
|       touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX),
 | |
|       touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
 | |
|       touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
 | |
|       touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
 | |
|       touchPoints.length,
 | |
|       modifiers);
 | |
|     return {defaultPrevented};
 | |
|   }
 | |
| 
 | |
|   async _dispatchTapEvent({x, y, modifiers}) {
 | |
|     // Force a layout at the point in question, because touch events
 | |
|     // do not seem to trigger one like mouse events.
 | |
|     this._frameTree.mainFrame().domWindow().windowUtils.elementFromPoint(
 | |
|       x,
 | |
|       y,
 | |
|       false /* aIgnoreRootScrollFrame */,
 | |
|       true /* aFlushLayout */);
 | |
| 
 | |
|     const {defaultPrevented: startPrevented} = await this._dispatchTouchEvent({
 | |
|       type: 'touchstart',
 | |
|       modifiers,
 | |
|       touchPoints: [{x, y}]
 | |
|     });
 | |
|     const {defaultPrevented: endPrevented} = await this._dispatchTouchEvent({
 | |
|       type: 'touchend',
 | |
|       modifiers,
 | |
|       touchPoints: [{x, y}]
 | |
|     });
 | |
|     if (startPrevented || endPrevented)
 | |
|       return;
 | |
| 
 | |
|     const frame = this._frameTree.mainFrame();
 | |
|     frame.domWindow().windowUtils.sendMouseEvent(
 | |
|       'mousemove',
 | |
|       x,
 | |
|       y,
 | |
|       0 /*button*/,
 | |
|       0 /*clickCount*/,
 | |
|       modifiers,
 | |
|       false /*aIgnoreRootScrollFrame*/,
 | |
|       undefined /*pressure*/,
 | |
|       5 /*inputSource*/,
 | |
|       undefined /*isDOMEventSynthesized*/,
 | |
|       false /*isWidgetEventSynthesized*/,
 | |
|       0 /*buttons*/,
 | |
|       undefined /*pointerIdentifier*/,
 | |
|       true /*disablePointerEvent*/);
 | |
| 
 | |
|     frame.domWindow().windowUtils.sendMouseEvent(
 | |
|       'mousedown',
 | |
|       x,
 | |
|       y,
 | |
|       0 /*button*/,
 | |
|       1 /*clickCount*/,
 | |
|       modifiers,
 | |
|       false /*aIgnoreRootScrollFrame*/,
 | |
|       undefined /*pressure*/,
 | |
|       5 /*inputSource*/,
 | |
|       undefined /*isDOMEventSynthesized*/,
 | |
|       false /*isWidgetEventSynthesized*/,
 | |
|       1 /*buttons*/,
 | |
|       undefined /*pointerIdentifier*/,
 | |
|       true /*disablePointerEvent*/);
 | |
| 
 | |
|     frame.domWindow().windowUtils.sendMouseEvent(
 | |
|       'mouseup',
 | |
|       x,
 | |
|       y,
 | |
|       0 /*button*/,
 | |
|       1 /*clickCount*/,
 | |
|       modifiers,
 | |
|       false /*aIgnoreRootScrollFrame*/,
 | |
|       undefined /*pressure*/,
 | |
|       5 /*inputSource*/,
 | |
|       undefined /*isDOMEventSynthesized*/,
 | |
|       false /*isWidgetEventSynthesized*/,
 | |
|       0 /*buttons*/,
 | |
|       undefined /*pointerIdentifier*/,
 | |
|       true /*disablePointerEvent*/);
 | |
|   }
 | |
| 
 | |
|   _startDragSessionIfNeeded() {
 | |
|     const sess = dragService.getCurrentSession();
 | |
|     if (sess) return;
 | |
|     dragService.startDragSessionForTests(
 | |
|       Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
 | |
|         Ci.nsIDragService.DRAGDROP_ACTION_COPY |
 | |
|         Ci.nsIDragService.DRAGDROP_ACTION_LINK
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _simulateDragEvent(type, x, y, modifiers) {
 | |
|     const window = this._frameTree.mainFrame().domWindow();
 | |
|     const element = window.windowUtils.elementFromPoint(x, y, false, false);
 | |
|     const event = window.document.createEvent('DragEvent');
 | |
| 
 | |
|     event.initDragEvent(
 | |
|       type,
 | |
|       true /* bubble */,
 | |
|       true /* cancelable */,
 | |
|       window,
 | |
|       0 /* clickCount */,
 | |
|       window.mozInnerScreenX + x,
 | |
|       window.mozInnerScreenY + y,
 | |
|       x,
 | |
|       y,
 | |
|       modifiers & 2 /* ctrlkey */,
 | |
|       modifiers & 1 /* altKey */,
 | |
|       modifiers & 4 /* shiftKey */,
 | |
|       modifiers & 8 /* metaKey */,
 | |
|       0 /* button */, // firefox always has the button as 0 on drops, regardless of which was pressed
 | |
|       null /* relatedTarget */,
 | |
|       null,
 | |
|     );
 | |
|     if (type !== 'drop' || dragService.dragAction)
 | |
|       window.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event);
 | |
|     if (type === 'drop')
 | |
|       this._cancelDragIfNeeded();
 | |
|   }
 | |
| 
 | |
|   _cancelDragIfNeeded() {
 | |
|     this._dragging = false;
 | |
|     const sess = dragService.getCurrentSession();
 | |
|     if (sess)
 | |
|       dragService.endDragSession(true);
 | |
|   }
 | |
| 
 | |
|   async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
 | |
|     this._startDragSessionIfNeeded();
 | |
|     const trapDrag = subject => {
 | |
|       this._dragging = true;
 | |
|     }
 | |
| 
 | |
|     // Don't send mouse events if there is an active drag
 | |
|     if (!this._dragging) {
 | |
|       const frame = this._frameTree.mainFrame();
 | |
| 
 | |
|       obs.addObserver(trapDrag, 'on-datatransfer-available');
 | |
|       frame.domWindow().windowUtils.sendMouseEvent(
 | |
|         type,
 | |
|         x,
 | |
|         y,
 | |
|         button,
 | |
|         clickCount,
 | |
|         modifiers,
 | |
|         false /*aIgnoreRootScrollFrame*/,
 | |
|         undefined /*pressure*/,
 | |
|         undefined /*inputSource*/,
 | |
|         undefined /*isDOMEventSynthesized*/,
 | |
|         undefined /*isWidgetEventSynthesized*/,
 | |
|         buttons);
 | |
|       obs.removeObserver(trapDrag, 'on-datatransfer-available');
 | |
| 
 | |
|       if (type === 'mousedown' && button === 2) {
 | |
|         frame.domWindow().windowUtils.sendMouseEvent(
 | |
|           'contextmenu',
 | |
|           x,
 | |
|           y,
 | |
|           button,
 | |
|           clickCount,
 | |
|           modifiers,
 | |
|           false /*aIgnoreRootScrollFrame*/,
 | |
|           undefined /*pressure*/,
 | |
|           undefined /*inputSource*/,
 | |
|           undefined /*isDOMEventSynthesized*/,
 | |
|           undefined /*isWidgetEventSynthesized*/,
 | |
|           buttons);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // update drag state
 | |
|     if (this._dragging) {
 | |
|       if (type === 'mousemove')
 | |
|         this._simulateDragEvent('dragover', x, y, modifiers);
 | |
|       else if (type === 'mouseup') // firefox will do drops when any mouse button is released
 | |
|         this._simulateDragEvent('drop', x, y, modifiers);
 | |
|     } else {
 | |
|       this._cancelDragIfNeeded();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async _insertText({text}) {
 | |
|     const frame = this._frameTree.mainFrame();
 | |
|     frame.textInputProcessor().commitCompositionWith(text);
 | |
|   }
 | |
| 
 | |
|   async _crash() {
 | |
|     dump(`Crashing intentionally\n`);
 | |
|     // This is to intentionally crash the frame.
 | |
|     // We crash by using js-ctypes and dereferencing
 | |
|     // a bad pointer. The crash should happen immediately
 | |
|     // upon loading this frame script.
 | |
|     const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm');
 | |
|     ChromeUtils.privateNoteIntentionalCrash();
 | |
|     const zero = new ctypes.intptr_t(8);
 | |
|     const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
 | |
|     badptr.contents;
 | |
|   }
 | |
| 
 | |
|   async _getFullAXTree({objectId}) {
 | |
|     let unsafeObject = null;
 | |
|     if (objectId) {
 | |
|       unsafeObject = this._frameData.get(this._frameTree.mainFrame()).unsafeObject(objectId);
 | |
|       if (!unsafeObject)
 | |
|         throw new Error(`No object found for id "${objectId}"`);
 | |
|     }
 | |
| 
 | |
|     const service = Cc["@mozilla.org/accessibilityService;1"]
 | |
|       .getService(Ci.nsIAccessibilityService);
 | |
|     const document = this._frameTree.mainFrame().domWindow().document;
 | |
|     const docAcc = service.getAccessibleFor(document);
 | |
| 
 | |
|     while (docAcc.document.isUpdatePendingForJugglerAccessibility)
 | |
|       await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x));
 | |
| 
 | |
|     async function waitForQuiet() {
 | |
|       let state = {};
 | |
|       docAcc.getState(state, {});
 | |
|       if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0)
 | |
|         return;
 | |
|       let resolve, reject;
 | |
|       const promise = new Promise((x, y) => {resolve = x, reject = y});
 | |
|       let eventObserver = {
 | |
|         observe(subject, topic) {
 | |
|           if (topic !== "accessible-event") {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // If event type does not match expected type, skip the event.
 | |
|           let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
 | |
|           if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // If event's accessible does not match expected accessible,
 | |
|           // skip the event.
 | |
|           if (event.accessible !== docAcc) {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           Services.obs.removeObserver(this, "accessible-event");
 | |
|           resolve();
 | |
|         },
 | |
|       };
 | |
|       Services.obs.addObserver(eventObserver, "accessible-event");
 | |
|       return promise;
 | |
|     }
 | |
|     function buildNode(accElement) {
 | |
|       let a = {}, b = {};
 | |
|       accElement.getState(a, b);
 | |
|       const tree = {
 | |
|         role: service.getStringRole(accElement.role),
 | |
|         name: accElement.name || '',
 | |
|       };
 | |
|       if (unsafeObject && unsafeObject === accElement.DOMNode)
 | |
|         tree.foundObject = true;
 | |
|       for (const userStringProperty of [
 | |
|         'value',
 | |
|         'description'
 | |
|       ]) {
 | |
|         tree[userStringProperty] = accElement[userStringProperty] || undefined;
 | |
|       }
 | |
| 
 | |
|       const states = {};
 | |
|       for (const name of service.getStringStates(a.value, b.value))
 | |
|         states[name] = true;
 | |
|       for (const name of ['selected',
 | |
|         'focused',
 | |
|         'pressed',
 | |
|         'focusable',
 | |
|         'haspopup',
 | |
|         'required',
 | |
|         'invalid',
 | |
|         'modal',
 | |
|         'editable',
 | |
|         'busy',
 | |
|         'checked',
 | |
|         'multiselectable']) {
 | |
|         if (states[name])
 | |
|           tree[name] = true;
 | |
|       }
 | |
| 
 | |
|       if (states['multi line'])
 | |
|         tree['multiline'] = true;
 | |
|       if (states['editable'] && states['readonly'])
 | |
|         tree['readonly'] = true;
 | |
|       if (states['checked'])
 | |
|         tree['checked'] = true;
 | |
|       if (states['mixed'])
 | |
|         tree['checked'] = 'mixed';
 | |
|       if (states['expanded'])
 | |
|         tree['expanded'] = true;
 | |
|       else if (states['collapsed'])
 | |
|         tree['expanded'] = false;
 | |
|       if (!states['enabled'])
 | |
|         tree['disabled'] = true;
 | |
| 
 | |
|       const attributes = {};
 | |
|       if (accElement.attributes) {
 | |
|         for (const { key, value } of accElement.attributes.enumerate()) {
 | |
|           attributes[key] = value;
 | |
|         }
 | |
|       }
 | |
|       for (const numericalProperty of ['level']) {
 | |
|         if (numericalProperty in attributes)
 | |
|           tree[numericalProperty] = parseFloat(attributes[numericalProperty]);
 | |
|       }
 | |
|       for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) {
 | |
|         if (stringProperty in attributes)
 | |
|           tree[stringProperty] = attributes[stringProperty];
 | |
|       }
 | |
|       const children = [];
 | |
| 
 | |
|       for (let child = accElement.firstChild; child; child = child.nextSibling) {
 | |
|         children.push(buildNode(child));
 | |
|       }
 | |
|       if (children.length)
 | |
|         tree.children = children;
 | |
|       return tree;
 | |
|     }
 | |
|     await waitForQuiet();
 | |
|     return {
 | |
|       tree: buildNode(docAcc)
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| function takeScreenshot(win, left, top, width, height, mimeType) {
 | |
|   const MAX_SKIA_DIMENSIONS = 32767;
 | |
| 
 | |
|   const scale = win.devicePixelRatio;
 | |
|   const canvasWidth = width * scale;
 | |
|   const canvasHeight = height * scale;
 | |
| 
 | |
|   if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS)
 | |
|     throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS);
 | |
| 
 | |
|   const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
 | |
|   canvas.width = canvasWidth;
 | |
|   canvas.height = canvasHeight;
 | |
| 
 | |
|   let ctx = canvas.getContext('2d');
 | |
|   ctx.scale(scale, scale);
 | |
|   ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET);
 | |
|   const dataURL = canvas.toDataURL(mimeType);
 | |
|   return dataURL.substring(dataURL.indexOf(',') + 1);
 | |
| };
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ['PageAgent'];
 | |
| this.PageAgent = PageAgent;
 | |
| 
 | 
