playwright/browser_patches/firefox/juggler/content/FrameTree.js

480 lines
15 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 Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js');
const helper = new Helper();
class FrameTree {
constructor(rootDocShell) {
EventEmitter.decorate(this);
this._browsingContextGroup = rootDocShell.browsingContext.group;
if (!this._browsingContextGroup.__jugglerFrameTrees)
this._browsingContextGroup.__jugglerFrameTrees = new Set();
this._browsingContextGroup.__jugglerFrameTrees.add(this);
this._scriptsToEvaluateOnNewDocument = new Map();
this._bindings = new Map();
this._runtime = new Runtime(false /* isWorker */);
this._workers = new Map();
this._docShellToFrame = new Map();
this._frameIdToFrame = new Map();
this._pageReady = false;
this._mainFrame = this._createFrame(rootDocShell);
const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this.QueryInterface = ChromeUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsIWebProgressListener2,
Ci.nsISupportsWeakReference,
]);
this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
this._wdmListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
onRegister: this._onWorkerCreated.bind(this),
onUnregister: this._onWorkerDestroyed.bind(this),
};
this._wdm.addListener(this._wdmListener);
for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator())
this._onWorkerCreated(workerDebugger);
const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION;
this._eventListeners = [
helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'),
helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'),
helper.addProgressListener(webProgress, this, flags),
];
}
workers() {
return [...this._workers.values()];
}
runtime() {
return this._runtime;
}
_frameForWorker(workerDebugger) {
if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED)
return null;
if (!workerDebugger.window)
return null;
const docShell = workerDebugger.window.docShell;
return this._docShellToFrame.get(docShell) || null;
}
_onDOMWindowCreated(window) {
const frame = this._docShellToFrame.get(window.docShell) || null;
if (!frame)
return;
frame._onGlobalObjectCleared();
this.emit(FrameTree.Events.GlobalObjectCreated, { frame, window });
}
_onWorkerCreated(workerDebugger) {
// Note: we do not interoperate with firefox devtools.
if (workerDebugger.isInitialized)
return;
const frame = this._frameForWorker(workerDebugger);
if (!frame)
return;
const worker = new Worker(frame, workerDebugger);
this._workers.set(workerDebugger, worker);
this.emit(FrameTree.Events.WorkerCreated, worker);
}
_onWorkerDestroyed(workerDebugger) {
const worker = this._workers.get(workerDebugger);
if (!worker)
return;
worker.dispose();
this._workers.delete(workerDebugger);
this.emit(FrameTree.Events.WorkerDestroyed, worker);
}
allFramesInBrowsingContextGroup(group) {
const frames = [];
for (const frameTree of (group.__jugglerFrameTrees || []))
frames.push(...frameTree.frames());
return frames;
}
isPageReady() {
return this._pageReady;
}
forcePageReady() {
if (this._pageReady)
return false;
this._pageReady = true;
this.emit(FrameTree.Events.PageReady);
return true;
}
addScriptToEvaluateOnNewDocument(script) {
const scriptId = helper.generateId();
this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
return scriptId;
}
removeScriptToEvaluateOnNewDocument(scriptId) {
this._scriptsToEvaluateOnNewDocument.delete(scriptId);
}
addBinding(name, script) {
this._bindings.set(name, script);
for (const frame of this.frames())
frame._addBinding(name, script);
}
setColorScheme(colorScheme) {
const docShell = this._mainFrame._docShell;
switch (colorScheme) {
case 'light': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_LIGHT; break;
case 'dark': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_DARK; break;
case 'no-preference': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NO_PREFERENCE; break;
default: docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NONE; break;
}
}
frameForDocShell(docShell) {
return this._docShellToFrame.get(docShell) || null;
}
frame(frameId) {
return this._frameIdToFrame.get(frameId) || null;
}
frames() {
let result = [];
collect(this._mainFrame);
return result;
function collect(frame) {
result.push(frame);
for (const subframe of frame._children)
collect(subframe);
}
}
mainFrame() {
return this._mainFrame;
}
dispose() {
this._browsingContextGroup.__jugglerFrameTrees.delete(this);
this._wdm.removeListener(this._wdmListener);
this._runtime.dispose();
helper.removeListeners(this._eventListeners);
}
onStateChange(progress, request, flag, status) {
if (!(request instanceof Ci.nsIChannel))
return;
const channel = request.QueryInterface(Ci.nsIChannel);
const docShell = progress.DOMWindow.docShell;
const frame = this._docShellToFrame.get(docShell);
if (!frame) {
dump(`ERROR: got a state changed event for un-tracked docshell!\n`);
return;
}
const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING;
const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
let isDownload = false;
try {
isDownload = (channel.contentDisposition === Ci.nsIChannel.DISPOSITION_ATTACHMENT);
} catch(e) {
// The method is expected to throw if it's not an attachment.
}
if (isStart) {
// Starting a new navigation.
frame._pendingNavigationId = this._channelId(channel);
frame._pendingNavigationURL = channel.URI.spec;
this.emit(FrameTree.Events.NavigationStarted, frame);
} else if (isTransferring || (isStop && frame._pendingNavigationId && !status && !isDownload)) {
// Navigation is committed.
for (const subframe of frame._children)
this._detachFrame(subframe);
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
frame._lastCommittedNavigationId = navigationId;
frame._url = channel.URI.spec;
this.emit(FrameTree.Events.NavigationCommitted, frame);
if (frame === this._mainFrame)
this.forcePageReady();
} else if (isStop && frame._pendingNavigationId && (status || isDownload)) {
// Navigation is aborted.
const navigationId = frame._pendingNavigationId;
frame._pendingNavigationId = null;
frame._pendingNavigationURL = null;
// Always report download navigation as failure to match other browsers.
const errorText = isDownload ? 'Will download to file' : helper.getNetworkErrorStatusText(status);
this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText);
if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED && !isDownload)
this.forcePageReady();
}
}
onFrameLocationChange(progress, request, location, flags) {
const docShell = progress.DOMWindow.docShell;
const frame = this._docShellToFrame.get(docShell);
const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
if (frame && sameDocumentNavigation) {
frame._url = location.spec;
this.emit(FrameTree.Events.SameDocumentNavigation, frame);
}
}
_channelId(channel) {
if (channel instanceof Ci.nsIHttpChannel) {
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
return String(httpChannel.channelId);
}
return helper.generateId();
}
_onDocShellCreated(docShell) {
// Bug 1142752: sometimes, the docshell appears to be immediately
// destroyed, bailout early to prevent random exceptions.
if (docShell.isBeingDestroyed())
return;
// If this docShell doesn't belong to our frame tree - do nothing.
let root = docShell;
while (root.parent)
root = root.parent;
if (root === this._mainFrame._docShell)
this._createFrame(docShell);
}
_createFrame(docShell) {
const parentFrame = this._docShellToFrame.get(docShell.parent) || null;
const frame = new Frame(this, this._runtime, docShell, parentFrame);
this._docShellToFrame.set(docShell, frame);
this._frameIdToFrame.set(frame.id(), frame);
this.emit(FrameTree.Events.FrameAttached, frame);
// Create execution context **after** reporting frame.
// This is our protocol contract.
if (frame.domWindow())
frame._onGlobalObjectCleared();
return frame;
}
_onDocShellDestroyed(docShell) {
const frame = this._docShellToFrame.get(docShell);
if (frame)
this._detachFrame(frame);
}
_detachFrame(frame) {
// Detach all children first
for (const subframe of frame._children)
this._detachFrame(subframe);
this._docShellToFrame.delete(frame._docShell);
this._frameIdToFrame.delete(frame.id());
if (frame._parentFrame)
frame._parentFrame._children.delete(frame);
frame._parentFrame = null;
frame.dispose();
this.emit(FrameTree.Events.FrameDetached, frame);
}
}
FrameTree.Events = {
BindingCalled: 'bindingcalled',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
GlobalObjectCreated: 'globalobjectcreated',
WorkerCreated: 'workercreated',
WorkerDestroyed: 'workerdestroyed',
NavigationStarted: 'navigationstarted',
NavigationCommitted: 'navigationcommitted',
NavigationAborted: 'navigationaborted',
SameDocumentNavigation: 'samedocumentnavigation',
PageReady: 'pageready',
};
class Frame {
constructor(frameTree, runtime, docShell, parentFrame) {
this._frameTree = frameTree;
this._runtime = runtime;
this._docShell = docShell;
this._children = new Set();
this._frameId = helper.generateId();
this._parentFrame = null;
this._url = '';
if (docShell.domWindow && docShell.domWindow.location)
this._url = docShell.domWindow.location.href;
if (parentFrame) {
this._parentFrame = parentFrame;
parentFrame._children.add(this);
}
this._lastCommittedNavigationId = null;
this._pendingNavigationId = null;
this._pendingNavigationURL = null;
this._textInputProcessor = null;
this._executionContext = null;
}
dispose() {
if (this._executionContext)
this._runtime.destroyExecutionContext(this._executionContext);
this._executionContext = null;
}
_addBinding(name, script) {
Cu.exportFunction((...args) => {
this._frameTree.emit(FrameTree.Events.BindingCalled, {
frame: this,
name,
payload: args[0]
});
}, this.domWindow(), {
defineAs: name,
});
this.domWindow().eval(script);
}
_onGlobalObjectCleared() {
if (this._executionContext)
this._runtime.destroyExecutionContext(this._executionContext);
this._executionContext = this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
frameId: this._frameId,
name: '',
});
for (const [name, script] of this._frameTree._bindings)
this._addBinding(name, script);
for (const script of this._frameTree._scriptsToEvaluateOnNewDocument.values()) {
try {
const result = this._executionContext.evaluateScript(script);
if (result && result.objectId)
this._executionContext.disposeObject(result.objectId);
} catch (e) {
dump(`ERROR: ${e.message}\n${e.stack}\n`);
}
}
}
executionContext() {
return this._executionContext;
}
textInputProcessor() {
if (!this._textInputProcessor) {
this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow);
}
return this._textInputProcessor;
}
pendingNavigationId() {
return this._pendingNavigationId;
}
pendingNavigationURL() {
return this._pendingNavigationURL;
}
lastCommittedNavigationId() {
return this._lastCommittedNavigationId;
}
docShell() {
return this._docShell;
}
domWindow() {
return this._docShell.domWindow;
}
name() {
const frameElement = this._docShell.domWindow.frameElement;
let name = '';
if (frameElement)
name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
return name;
}
parentFrame() {
return this._parentFrame;
}
id() {
return this._frameId;
}
url() {
return this._url;
}
}
class Worker {
constructor(frame, workerDebugger) {
this._frame = frame;
this._workerId = helper.generateId();
this._workerDebugger = workerDebugger;
workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
this._channel = new SimpleChannel(`content::worker[${this._workerId}]`);
this._channel.transport = {
sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
dispose: () => {},
};
this._workerDebuggerListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]),
onMessage: msg => void this._channel._onMessage(JSON.parse(msg)),
onClose: () => void this._channel.dispose(),
onError: (filename, lineno, message) => {
dump(`Error in worker: ${message} @${filename}:${lineno}\n`);
},
};
workerDebugger.addListener(this._workerDebuggerListener);
}
channel() {
return this._channel;
}
frame() {
return this._frame;
}
id() {
return this._workerId;
}
url() {
return this._workerDebugger.url;
}
dispose() {
this._channel.dispose();
this._workerDebugger.removeListener(this._workerDebuggerListener);
}
}
var EXPORTED_SYMBOLS = ['FrameTree'];
this.FrameTree = FrameTree;