Bug 1096488 - Detect and handle switching from remote to non-remote pages and back in marionette.;r=automatedtester

This commit is contained in:
Chris Manchester 2015-03-10 16:19:40 -07:00
parent b6bcb7bcea
commit 744c5468c0
3 changed files with 246 additions and 78 deletions

View File

@ -208,6 +208,7 @@ FrameManager.prototype = {
messageManager.addWeakMessageListener("Marionette:addCookie", this.server);
messageManager.addWeakMessageListener("Marionette:getVisibleCookies", this.server);
messageManager.addWeakMessageListener("Marionette:deleteCookie", this.server);
messageManager.addWeakMessageListener("Marionette:listenersAttached", this.server);
messageManager.addWeakMessageListener("MarionetteFrame:handleModal", this);
messageManager.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
messageManager.addWeakMessageListener("MarionetteFrame:getInterruptedState", this);
@ -239,8 +240,8 @@ FrameManager.prototype = {
messageManager.removeWeakMessageListener("Marionette:addCookie", this.server);
messageManager.removeWeakMessageListener("Marionette:getVisibleCookies", this.server);
messageManager.removeWeakMessageListener("Marionette:deleteCookie", this.server);
messageManager.removeWeakMessageListener("Marionette:listenersAttached", this.server);
messageManager.removeWeakMessageListener("MarionetteFrame:handleModal", this);
messageManager.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
},
};

View File

@ -63,6 +63,9 @@ let originalOnError;
let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
//timer for readystate
let readyStateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
// timer for navigation commands.
let navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let onDOMContentLoaded;
// Send move events about this often
let EVENT_INTERVAL = 30; // milliseconds
// For assigning unique ids to all touches
@ -97,13 +100,14 @@ let modalHandler = function() {
* If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
*/
function registerSelf() {
let msg = {value: winUtil.outerWindowID, href: content.location.href};
let msg = {value: winUtil.outerWindowID}
// register will have the ID and a boolean describing if this is the main process or not
let register = sendSyncMessage("Marionette:register", msg);
if (register[0]) {
listenerId = register[0][0].id;
if (typeof listenerId != "undefined") {
let {id, remotenessChange} = register[0][0];
listenerId = id;
if (typeof id != "undefined") {
// check if we're the main process
if (register[0][1] == true) {
addMessageListener("MarionetteMainListener:emitTouchEvent", emitTouchEventForIFrame);
@ -111,6 +115,9 @@ function registerSelf() {
importedScripts = FileUtils.getDir('TmpD', [], false);
importedScripts.append('marionetteContentScripts');
startListeners();
if (remotenessChange) {
sendAsyncMessage("Marionette:listenersAttached", {listenerId: id});
}
}
}
}
@ -170,6 +177,8 @@ function startListeners() {
addMessageListenerId("Marionette:actionChain", actionChain);
addMessageListenerId("Marionette:multiAction", multiAction);
addMessageListenerId("Marionette:get", get);
addMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
addMessageListenerId("Marionette:cancelRequest", cancelRequest);
addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrl);
addMessageListenerId("Marionette:getTitle", getTitle);
addMessageListenerId("Marionette:getPageSource", getPageSource);
@ -273,6 +282,8 @@ function deleteSession(msg) {
removeMessageListenerId("Marionette:actionChain", actionChain);
removeMessageListenerId("Marionette:multiAction", multiAction);
removeMessageListenerId("Marionette:get", get);
removeMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
removeMessageListenerId("Marionette:cancelRequest", cancelRequest);
removeMessageListenerId("Marionette:getTitle", getTitle);
removeMessageListenerId("Marionette:getPageSource", getPageSource);
removeMessageListenerId("Marionette:getCurrentUrl", getCurrentUrl);
@ -1404,6 +1415,52 @@ function multiAction(msg) {
}
}
/*
* This implements the latter part of a get request (for the case we need to resume one
* when a remoteness update happens in the middle of a navigate request). This is most of
* of the work of a navigate request, but doesn't assume DOMContentLoaded is yet to fire.
*/
function pollForReadyState(msg, start, callback) {
let {pageTimeout, url, command_id} = msg.json;
start = start ? start : new Date().getTime();
if (!callback) {
callback = () => {};
}
let end = null;
function checkLoad() {
navTimer.cancel();
end = new Date().getTime();
let aboutErrorRegex = /about:.+(error)\?/;
let elapse = end - start;
if (pageTimeout == null || elapse <= pageTimeout) {
if (curFrame.document.readyState == "complete") {
callback();
sendOk(command_id);
} else if (curFrame.document.readyState == "interactive" &&
aboutErrorRegex.exec(curFrame.document.baseURI) &&
!curFrame.document.baseURI.startsWith(url)) {
// We have reached an error url without requesting it.
callback();
sendError("Error loading page", 13, null, command_id);
} else if (curFrame.document.readyState == "interactive" &&
curFrame.document.baseURI.startsWith("about:")) {
callback();
sendOk(command_id);
} else {
navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
}
}
else {
callback();
sendError("Error loading page, timed out (checkLoad)", 21, null,
command_id);
}
}
checkLoad();
}
/**
* Navigate to the given URL. The operation will be performed on the
* current browser context, and handles the case where we navigate
@ -1411,67 +1468,56 @@ function multiAction(msg) {
* (in chrome space).
*/
function get(msg) {
let command_id = msg.json.command_id;
let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let start = new Date().getTime();
let end = null;
function checkLoad() {
checkTimer.cancel();
end = new Date().getTime();
let aboutErrorRegex = /about:.+(error)\?/;
let elapse = end - start;
if (msg.json.pageTimeout == null || elapse <= msg.json.pageTimeout) {
if (curFrame.document.readyState == "complete") {
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
sendOk(command_id);
} else if (curFrame.document.readyState == "interactive" &&
aboutErrorRegex.exec(curFrame.document.baseURI) &&
!curFrame.document.baseURI.startsWith(msg.json.url)) {
// We have reached an error url without requesting it.
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
sendError("Error loading page", 13, null, command_id);
} else if (curFrame.document.readyState == "interactive" &&
curFrame.document.baseURI.startsWith("about:")) {
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
sendOk(command_id);
} else {
checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
}
}
else {
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
sendError("Error loading page, timed out (checkLoad)", 21, null,
command_id);
}
}
// Prevent DOMContentLoaded events from frames from invoking this
// code, unless the event is coming from the frame associated with
// the current window (i.e. someone has used switch_to_frame).
let onDOMContentLoaded = function onDOMContentLoaded(event) {
onDOMContentLoaded = function onDOMContentLoaded(event) {
if (!event.originalTarget.defaultView.frameElement ||
event.originalTarget.defaultView.frameElement == curFrame.frameElement) {
checkLoad();
pollForReadyState(msg, start, () => {
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
onDOMContentLoaded = null;
});
}
};
function timerFunc() {
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
sendError("Error loading page, timed out (onDOMContentLoaded)", 21,
null, command_id);
null, msg.json.command_id);
}
if (msg.json.pageTimeout != null) {
checkTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
navTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
}
addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
curFrame.location = msg.json.url;
}
/**
* Cancel the polling and remove the event listener associated with a current
* navigation request in case we're interupted by an onbeforeunload handler
* and navigation doesn't complete.
*/
function cancelRequest() {
navTimer.cancel();
if (onDOMContentLoaded) {
removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
}
}
/**
* Get URL of the top level browsing context.
*/
function getCurrentUrl(msg) {
sendResponse({value: curFrame.location.href}, msg.json.command_id);
let url;
if (msg.json.isB2G) {
url = curFrame.location.href;
} else {
url = content.location.href;
}
sendResponse({value: url}, msg.json.command_id);
}
/**

View File

@ -288,8 +288,10 @@ MarionetteServerConnection.prototype = {
}
}
else {
this.messageManager.broadcastAsyncMessage(
"Marionette:" + name + this.curBrowser.curFrameId, values);
this.curBrowser.executeWhenReady(() => {
this.messageManager.broadcastAsyncMessage(
"Marionette:" + name + this.curBrowser.curFrameId, values);
});
}
return success;
},
@ -329,6 +331,11 @@ MarionetteServerConnection.prototype = {
return;
}
}
if (this.curBrowser !== null) {
this.curBrowser.pendingCommands = [];
}
this.conn.send(msg);
if (command_id != -1) {
// Don't unset this.command_id if this message is to process an
@ -1278,7 +1285,17 @@ MarionetteServerConnection.prototype = {
*/
get: function MDA_get(aRequest) {
let command_id = this.command_id = this.getCommandId();
if (this.context != "chrome") {
// If a remoteness update interrupts our page load, this will never return
// We need to re-issue this request to correctly poll for readyState and
// send errors.
this.curBrowser.pendingCommands.push(() => {
aRequest.parameters.command_id = command_id;
this.messageManager.broadcastAsyncMessage(
"Marionette:pollForReadyState" + this.curBrowser.curFrameId,
aRequest.parameters);
});
aRequest.command_id = command_id;
aRequest.parameters.pageTimeout = this.pageTimeout;
this.sendAsync("get", aRequest.parameters, command_id);
@ -1337,15 +1354,7 @@ MarionetteServerConnection.prototype = {
this.sendResponse(this.getCurrentWindow().location.href, this.command_id);
}
else {
if (isB2G) {
this.sendAsync("getCurrentUrl", {}, this.command_id);
}
else {
this.sendResponse(this.curBrowser
.tab
.linkedBrowser
.contentWindowAsCPOW.location.href, this.command_id);
}
this.sendAsync("getCurrentUrl", {isB2G: isB2G}, this.command_id);
}
},
@ -1439,6 +1448,13 @@ MarionetteServerConnection.prototype = {
}
},
/**
* Forces an update for the given browser's id.
*/
updateIdForBrowser: function (browser, newId) {
this._browserIds.set(browser.permanentKey, newId);
},
/**
* Retrieves a listener id for the given xul browser element. In case
* the browser is not known, an attempt is made to retrieve the id from
@ -1601,8 +1617,7 @@ MarionetteServerConnection.prototype = {
this.curBrowser = this.browsers[outerId];
if (contentWindowId) {
// The updated id corresponds to switching to a new tab.
this.curBrowser.curFrameId = contentWindowId;
win.gBrowser.selectTabAtIndex(ind);
this.curBrowser.switchToTab(ind);
}
this.sendOk(command_id);
}
@ -3072,6 +3087,7 @@ MarionetteServerConnection.prototype = {
}
if (this.command_id) {
this.sendAsync("cancelRequest", {});
// This is a shortcut to get the client to accept our response whether
// the expected key is 'ok' (in case a click or similar got us here)
// or 'value' (in case an execute script or similar got us here).
@ -3214,8 +3230,9 @@ MarionetteServerConnection.prototype = {
let mainContent = (this.curBrowser.mainContentId == null);
if (!browserType || browserType != "content") {
//curBrowser holds all the registered frames in knownFrames
let listenerId = this.generateFrameId(message.json.value);
reg.id = this.curBrowser.register(listenerId);
let uid = this.generateFrameId(message.json.value);
reg.id = uid;
reg.remotenessChange = this.curBrowser.register(uid, message.target);
}
// set to true if we updated mainContentId
mainContent = ((mainContent == true) && (this.curBrowser.mainContentId != null));
@ -3253,6 +3270,19 @@ MarionetteServerConnection.prototype = {
globalMessageManager.broadcastAsyncMessage(
"MarionetteMainListener:emitTouchEvent", message.json);
return;
case "Marionette:listenersAttached":
if (message.json.listenerId === this.curBrowser.curFrameId) {
// If remoteness gets updated we need to call newSession. In the case
// of desktop this just sets up a small amount of state that doesn't
// change over the course of a session.
let newSessionValues = {
B2G: (appName == "B2G"),
raisesAccessibilityExceptions: this.sessionCapabilities.raisesAccessibilityExceptions
};
this.sendAsync("newSession", newSessionValues);
this.curBrowser.flushPendingCommands();
}
return;
}
}
};
@ -3370,15 +3400,49 @@ function BrowserObj(win, server) {
this.setBrowser(win);
this.frameManager = new FrameManager(server); //We should have one FM per BO so that we can handle modals in each Browser
// A reference to the tab corresponding to the current window handle, if any.
this.tab = null;
this.pendingCommands = [];
//register all message listeners
this.frameManager.addMessageManagerListeners(server.messageManager);
this.getIdForBrowser = server.getIdForBrowser.bind(server);
this.updateIdForBrowser = server.updateIdForBrowser.bind(server);
this._curFrameId = null;
this._browserWasRemote = null;
this._hasRemotenessChange = false;
}
BrowserObj.prototype = {
get tab () {
// A reference to the currently selected tab, if any
return this.browser ? this.browser.selectedTab : null;
/**
* This function intercepts commands interacting with content and queues
* or executes them as needed.
*
* No commands interacting with content are safe to process until
* the new listener script is loaded and registers itself.
* This occurs when a command whose effect is asynchronous (such
* as goBack) results in a remoteness change and new commands
* are subsequently posted to the server.
*/
executeWhenReady: function (callback) {
if (this.hasRemotenessChange()) {
this.pendingCommands.push(callback);
} else {
callback();
}
},
/**
* Re-sets this BrowserObject's current tab and updates remoteness tracking.
*/
switchToTab: function (ind) {
if (this.browser) {
this.browser.selectTabAtIndex(ind);
this.tab = this.browser.selectedTab;
}
this._browserWasRemote = this.browser.getBrowserForTab(this.tab).isRemoteBrowser;
this._hasRemotenessChange = false;
},
/**
@ -3418,15 +3482,31 @@ BrowserObj.prototype = {
break;
}
},
// The current frame id is managed per browser element on desktop in case
// the id needs to be refreshed. The currently selected window is identified
// within BrowserObject by a tab.
get curFrameId () {
if (appName != "Firefox") {
return this._curFrameId;
}
if (this.tab) {
let browser = this.browser.getBrowserForTab(this.tab);
return this.getIdForBrowser(browser);
}
return null;
},
set curFrameId (id) {
if (appName != "Firefox") {
this._curFrameId = id;
}
},
/**
* Called when we start a session with this browser.
*/
startSession: function BO_startSession(newSession, win, callback) {
if (appName == "Firefox" &&
win.gMultiProcessBrowser &&
!win.gBrowser.selectedBrowser.isRemoteBrowser) {
win.XULBrowserWindow.forceInitialBrowserRemote();
}
callback(win, newSession);
},
@ -3458,28 +3538,69 @@ BrowserObj.prototype = {
*
* @param string uid
* frame uid for use by marionette
* @param the XUL <browser> that was the target of the originating message.
*/
register: function BO_register(uid) {
if (this.curFrameId == null) {
let currWinId = null;
register: function BO_register(uid, target) {
let remotenessChange = this.hasRemotenessChange();
if (this.curFrameId === null || remotenessChange) {
if (this.browser) {
// If we're setting up a new session on Firefox, we only process the
// registration for this frame if it belongs to the tab we've just
// created.
// registration for this frame if it belongs to the current tab.
if (!this.tab) {
this.switchToTab(this.browser.selectedIndex);
}
let browser = this.browser.getBrowserForTab(this.tab);
currWinId = this.getIdForBrowser(browser);
}
if ((!this.newSession) ||
(this.newSession &&
((appName != "Firefox") ||
uid === currWinId))) {
this.curFrameId = uid;
if (target == browser) {
this.updateIdForBrowser(browser, uid);
this.mainContentId = uid;
}
} else {
this._curFrameId = uid;
this.mainContentId = uid;
}
}
this.knownFrames.push(uid); //used to delete sessions
return uid;
return remotenessChange;
},
/**
* When navigating between pages results in changing a browser's process, we
* need to take measures not to lose contact with a listener script. This
* function does the necessary bookkeeping.
*/
hasRemotenessChange: function () {
// None of these checks are relevant on b2g or if we don't have a tab yet,
// and may not apply on Fennec.
if (appName != "Firefox" || this.tab === null) {
return false;
}
if (this._hasRemotenessChange) {
return true;
}
let currentIsRemote = this.browser.getBrowserForTab(this.tab).isRemoteBrowser;
this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote;
this._browserWasRemote = currentIsRemote;
return this._hasRemotenessChange;
},
/**
* Flushes any pending commands queued when a remoteness change is being
* processed and mark this remotenessUpdate as complete.
*/
flushPendingCommands: function () {
if (!this._hasRemotenessChange) {
return;
}
this._hasRemotenessChange = false;
this.pendingCommands.forEach((callback) => {
callback();
});
this.pendingCommands = [];
}
}
/**