mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-16 15:52:24 +03:00
c4e3ed85c0
When innerWindow is restored from the history state, we do not receive content-document-global-created notification, but would still like to know that window is now using a different inner window to reset the state. This introduces a new notification juggler-dom-window-reused. At the same time, goBack()/goForward() sometimes do not initiate navigation synchronously, so our check for pendingNaivgationId() does not work. Instead, we rely on canGoBack, and assume that client will not need the navigationId synchronously.
481 lines
15 KiB
JavaScript
481 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(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'),
|
|
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;
|
|
|