From 4ea382dcbaaf985350cda089239e416326f646dc Mon Sep 17 00:00:00 2001 From: Jonathan Griffin Date: Fri, 28 Sep 2012 16:16:22 -0700 Subject: [PATCH] Bug 775116 - Fix frame switching to work with id, name, and remote frames, r=mdas, DONTBUILD(NPOTB) --- testing/marionette/marionette-actors.js | 209 +++++++++++++++++----- testing/marionette/marionette-listener.js | 32 ++-- 2 files changed, 190 insertions(+), 51 deletions(-) diff --git a/testing/marionette/marionette-actors.js b/testing/marionette/marionette-actors.js index 7e3bda71d317..b8a04ce71933 100644 --- a/testing/marionette/marionette-actors.js +++ b/testing/marionette/marionette-actors.js @@ -8,6 +8,8 @@ * Gecko-specific actors. */ +const FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js"; + let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] @@ -94,8 +96,8 @@ MarionetteRootActor.prototype = { getMarionetteID: function MRA_getMarionette() { return { "from": "root", "id": this._marionetteActor.actorID } ; - }, -} + } +}; // register the calls MarionetteRootActor.prototype.requestTypes = { @@ -103,9 +105,22 @@ MarionetteRootActor.prototype.requestTypes = { "sayHello": MarionetteRootActor.prototype.sayHello }; +/** + * An object representing a frame that Marionette has loaded a + * frame script in. + */ +function MarionetteRemoteFrame(windowId, frameId) { + this.windowId = windowId; + this.frameId = frameId; + this.targetFrameId = null; + this.messageManager = null; +} +// persistent list of remote frames that Marionette has loaded a frame script in +let remoteFrames = []; + /** * This actor is responsible for all marionette API calls. It gets created - * for each connection and manages all chrome and browser based calls. It + * for each connection and manages all chrome and browser based calls. It * mediates content calls by issuing appropriate messages to the content process. */ function MarionetteDriverActor(aConnection) @@ -114,8 +129,9 @@ function MarionetteDriverActor(aConnection) .getService(Ci.nsIUUIDGenerator); this.conn = aConnection; - this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"] - .getService(Ci.nsIMessageBroadcaster); + this.globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageBroadcaster); + this.messageManager = this.globalMessageManager; this.browsers = {}; //holds list of BrowserObjs this.curBrowser = null; // points to current browser this.context = "content"; @@ -127,16 +143,10 @@ function MarionetteDriverActor(aConnection) this.mainFrame = null; //topmost chrome frame this.curFrame = null; //subframe that currently has focus this.importedScripts = FileUtils.getFile('TmpD', ['marionettescriptchrome']); - + this.currentRemoteFrame = null; // a member of remoteFrames + //register all message listeners - this.messageManager.addMessageListener("Marionette:ok", this); - this.messageManager.addMessageListener("Marionette:done", this); - this.messageManager.addMessageListener("Marionette:error", this); - this.messageManager.addMessageListener("Marionette:log", this); - this.messageManager.addMessageListener("Marionette:shareData", this); - this.messageManager.addMessageListener("Marionette:register", this); - this.messageManager.addMessageListener("Marionette:goUrl", this); - this.messageManager.addMessageListener("Marionette:runEmulatorCmd", this); + this.addMessageManagerListeners(this.messageManager); } MarionetteDriverActor.prototype = { @@ -144,6 +154,26 @@ MarionetteDriverActor.prototype = { //name of the actor actorPrefix: "marionette", + /** + * Helper methods: + */ + + /** + * Switches to the global ChromeMessageBroadcaster, potentially replacing a frame-specific + * ChromeMessageSender. Has no effect if the global ChromeMessageBroadcaster is already + * in use. If this replaces a frame-specific ChromeMessageSender, it removes the message + * listeners from that sender, and then puts the corresponding frame script "to sleep", + * which removes most of the message listeners from it as well. + */ + switchToGlobalMessageManager: function MDA_switchToGlobalMM() { + if (this.currentRemoteFrame !== null) { + this.removeMessageManagerListeners(this.messageManager); + this.sendAsync("sleepSession"); + } + this.messageManager = this.globalMessageManager; + this.currentRemoteFrame = null; + }, + /** * Helper method to send async messages to the content listener * @@ -153,16 +183,55 @@ MarionetteDriverActor.prototype = { * Object to send to the listener */ sendAsync: function MDA_sendAsync(name, values) { - this.messageManager.broadcastAsyncMessage("Marionette:" + name + this.curBrowser.curFrameId, values); + if (this.currentRemoteFrame !== null) { + this.messageManager.sendAsyncMessage( + "Marionette:" + name + this.currentRemoteFrame.targetFrameId, values); + } + else { + this.messageManager.broadcastAsyncMessage( + "Marionette:" + name + this.curBrowser.curFrameId, values); + } }, /** - * Helper methods: + * Adds listeners for messages from content frame scripts. + * + * @param object messageManager + * The messageManager object (ChromeMessageBroadcaster or ChromeMessageSender) + * to which the listeners should be added. */ + addMessageManagerListeners: function MDA_addMessageManagerListeners(messageManager) { + messageManager.addMessageListener("Marionette:ok", this); + messageManager.addMessageListener("Marionette:done", this); + messageManager.addMessageListener("Marionette:error", this); + messageManager.addMessageListener("Marionette:log", this); + messageManager.addMessageListener("Marionette:shareData", this); + messageManager.addMessageListener("Marionette:register", this); + messageManager.addMessageListener("Marionette:runEmulatorCmd", this); + messageManager.addMessageListener("Marionette:switchToFrame", this); + }, + + /** + * Removes listeners for messages from content frame scripts. + * + * @param object messageManager + * The messageManager object (ChromeMessageBroadcaster or ChromeMessageSender) + * from which the listeners should be removed. + */ + removeMessageManagerListeners: function MDA_removeMessageManagerListeners(messageManager) { + messageManager.removeMessageListener("Marionette:ok", this); + messageManager.removeMessageListener("Marionette:done", this); + messageManager.removeMessageListener("Marionette:error", this); + messageManager.removeMessageListener("Marionette:log", this); + messageManager.removeMessageListener("Marionette:shareData", this); + messageManager.removeMessageListener("Marionette:register", this); + messageManager.removeMessageListener("Marionette:runEmulatorCmd", this); + messageManager.removeMessageListener("Marionette:switchToFrame", this); + }, /** * Generic method to pass a response to the client - * + * * @param object msg * Response to send back to client * @param string command_id @@ -316,7 +385,7 @@ MarionetteDriverActor.prototype = { whenBrowserStarted: function MDA_whenBrowserStarted(win, newSession) { try { if (!Services.prefs.getBoolPref("marionette.contentListener") || !newSession) { - this.curBrowser.loadFrameScript("chrome://marionette/content/marionette-listener.js", win); + this.curBrowser.loadFrameScript(FRAME_SCRIPT, win); } } catch (e) { @@ -377,6 +446,8 @@ MarionetteDriverActor.prototype = { } } + this.switchToGlobalMessageManager(); + if (!Services.prefs.getBoolPref("marionette.contentListener")) { waitForWindow.call(this); } @@ -968,6 +1039,14 @@ MarionetteDriverActor.prototype = { } } else { + if ((aRequest.value == null) && (aRequest.element == null) && + (this.currentRemoteFrame !== null)) { + // We're currently using a ChromeMessageSender for a remote frame, so this + // request indicates we need to switch back to the top-level (parent) frame. + // We'll first switch to the parent's (global) ChromeMessageBroadcaster, so + // we send the message to the right listener. + this.switchToGlobalMessageManager(); + } this.sendAsync("switchToFrame", aRequest); } }, @@ -1302,7 +1381,7 @@ MarionetteDriverActor.prototype = { } try{ - this.messageManager.removeDelayedFrameScript("chrome://marionette/content/marionette-listener.js"); + this.messageManager.removeDelayedFrameScript(FRAME_SCRIPT); this.getCurrentWindow().close(); this.sendOk(); } @@ -1324,7 +1403,7 @@ MarionetteDriverActor.prototype = { deleteSession: function MDA_deleteSession() { if (this.curBrowser != null) { if (appName == "B2G") { - this.messageManager.broadcastAsyncMessage("Marionette:sleepSession" + this.curBrowser.mainContentId, {}); + this.globalMessageManager.broadcastAsyncMessage("Marionette:sleepSession" + this.curBrowser.mainContentId, {}); this.curBrowser.knownFrames.splice(this.curBrowser.knownFrames.indexOf(this.curBrowser.mainContentId), 1); } else { @@ -1335,23 +1414,17 @@ MarionetteDriverActor.prototype = { //delete session in each frame in each browser for (let win in this.browsers) { for (let i in this.browsers[win].knownFrames) { - this.messageManager.broadcastAsyncMessage("Marionette:deleteSession" + this.browsers[win].knownFrames[i], {}); + this.globalMessageManager.broadcastAsyncMessage("Marionette:deleteSession" + this.browsers[win].knownFrames[i], {}); } } let winEnum = this.getWinEnumerator(); while (winEnum.hasMoreElements()) { - winEnum.getNext().messageManager.removeDelayedFrameScript("chrome://marionette/content/marionette-listener.js"); + winEnum.getNext().messageManager.removeDelayedFrameScript(FRAME_SCRIPT); } } this.sendOk(); - this.messageManager.removeMessageListener("Marionette:ok", this); - this.messageManager.removeMessageListener("Marionette:done", this); - this.messageManager.removeMessageListener("Marionette:error", this); - this.messageManager.removeMessageListener("Marionette:log", this); - this.messageManager.removeMessageListener("Marionette:shareData", this); - this.messageManager.removeMessageListener("Marionette:register", this); - this.messageManager.removeMessageListener("Marionette:goUrl", this); - this.messageManager.removeMessageListener("Marionette:runEmulatorCmd", this); + this.removeMessageManagerListeners(this.globalMessageManager); + this.switchToGlobalMessageManager(); this.curBrowser = null; try { this.importedScripts.remove(false); @@ -1415,6 +1488,15 @@ MarionetteDriverActor.prototype = { } }, + /** + * Helper function to convert an outerWindowID into a UID that Marionette + * tracks. + */ + generateFrameId: function MDA_generateFrameId(id) { + let uid = id + (appName == "B2G" ? "-b2g" : ""); + return uid; + }, + /** * Receives all messages from content messageManager */ @@ -1449,12 +1531,58 @@ MarionetteDriverActor.prototype = { case "Marionette:runEmulatorCmd": this.sendToClient(message.json); break; + case "Marionette:switchToFrame": + // Switch to a remote frame. + + for (let i = 0; i < remoteFrames.length; i++) { + let frame = remoteFrames[i]; + if ((frame.windowId == message.json.win) && (frame.frameId == message.json.frame)) { + // The frame script has already been loaded in this frame, so just wake it up. + this.currentRemoteFrame = frame; + this.messageManager = frame.messageManager; + this.addMessageManagerListeners(this.messageManager); + this.messageManager.sendAsyncMessage("Marionette:restart", {}); + return; + } + } + + // Load the frame script in this frame, and set the frame's ChromeMessageSender + // as the active message manager. + let thisWin = this.getCurrentWindow(); + let frameWindow = thisWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .getOuterWindowWithId(message.json.win); + let thisFrame = frameWindow.document.getElementsByTagName("iframe")[message.json.frame]; + let mm = thisFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager + this.addMessageManagerListeners(mm); + mm.loadFrameScript(FRAME_SCRIPT, true); + this.messageManager = mm; + let aFrame = new MarionetteRemoteFrame(message.json.win, message.json.frame); + aFrame.messageManager = this.messageManager; + remoteFrames.push(aFrame); + this.currentRemoteFrame = aFrame; + break; case "Marionette:register": // This code processes the content listener's registration information // and either accepts the listener, or ignores it let nullPrevious = (this.curBrowser.curFrameId == null); let curWin = this.getCurrentWindow(); - let frameObject = curWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).getOuterWindowWithId(message.json.value); + let listenerWindow = curWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .getOuterWindowWithId(message.json.value); + + if (listenerWindow.location.href != message.json.href) { + // If there is a mismatch between the calculated href and the one + // sent from the frame script, it means that the frame script is + // running in a separate process. Currently this only happens + // in B2G for OOP frames registered in Marionette:switchToFrame, so + // we'll acknowledge the switchToFrame message here. + // XXX: Should have a better way of determining that this message + // is from a remote frame. + this.sendOk(); + this.currentRemoteFrame.targetFrameId = this.generateFrameId(message.json.value); + } + let browserType; try { browserType = message.target.getAttribute("type"); @@ -1463,9 +1591,10 @@ MarionetteDriverActor.prototype = { } let reg; if (!browserType || browserType != "content") { - reg = this.curBrowser.register(message.json.value, message.json.href); + reg = this.curBrowser.register(this.generateFrameId(message.json.value), + message.json.href); } - this.curBrowser.elementManager.seenItems[reg] = frameObject; //add to seenItems + this.curBrowser.elementManager.seenItems[reg] = listenerWindow; //add to seenItems if (nullPrevious && (this.curBrowser.curFrameId != null)) { this.sendAsync("newSession", {B2G: (appName == "B2G")}); if (this.curBrowser.newSession) { @@ -1545,8 +1674,6 @@ function BrowserObj(win) { this.curFrameId = null; this.startPage = "about:blank"; this.mainContentId = null; // used in B2G to identify the homescreen content page - this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"] - .getService(Ci.nsIMessageBroadcaster); this.newSession = true; //used to set curFrameId upon new session this.elementManager = new ElementManager([SELECTOR, NAME, LINK_TEXT, PARTIAL_LINK_TEXT]); this.setBrowser(win); @@ -1642,20 +1769,20 @@ BrowserObj.prototype = { * if it is not already assigned, and if a) we already have a session * or b) we're starting a new session and it is the right start frame. * - * @param string id - * frame id + * @param string uid + * frame uid * @param string href * frame's href */ - register: function BO_register(id, href) { - let uid = id + ((appName == "B2G") ? '-b2g' : ''); + register: function BO_register(uid, href) { if (this.curFrameId == null) { - if ((!this.newSession) || (this.newSession && ((appName != "Firefox") || href.indexOf(this.startPage) > -1))) { + if ((!this.newSession) || (this.newSession && + ((appName != "Firefox") || href.indexOf(this.startPage) > -1))) { this.curFrameId = uid; this.mainContentId = uid; } } - this.knownFrames.push(uid); //used to deletesessions + this.knownFrames.push(uid); //used to delete sessions return uid; }, } diff --git a/testing/marionette/marionette-listener.js b/testing/marionette/marionette-listener.js index 64ce1c7b1a23..bc0d081cf4c1 100644 --- a/testing/marionette/marionette-listener.js +++ b/testing/marionette/marionette-listener.js @@ -747,28 +747,31 @@ function switchToFrame(msg) { } } } + let frames = curWindow.document.getElementsByTagName("iframe"); switch(typeof(msg.json.value)) { case "string" : let foundById = null; - let numFrames = curWindow.frames.length; - for (let i = 0; i < numFrames; i++) { + for (let i = 0; i < frames.length; i++) { //give precedence to name - let frame = curWindow.frames[i]; - let frameElement = frame.frameElement; - if (frameElement.name == msg.json.value) { + let frame = frames[i]; + let name = utils.getElementAttribute(frame, 'name'); + let id = utils.getElementAttribute(frame, 'id'); + if (name == msg.json.value) { foundFrame = i; break; - } else if ((foundById == null) && (frameElement.id == msg.json.value)) { + } else if ((foundById == null) && (id == msg.json.value)) { foundById = i; } } if ((foundFrame == null) && (foundById != null)) { foundFrame = foundById; + curWindow = frames[foundFrame]; } break; case "number": - if (curWindow.frames[msg.json.value] != undefined) { + if (frames[msg.json.value] != undefined) { foundFrame = msg.json.value; + curWindow = frames[foundFrame]; } break; } @@ -776,11 +779,20 @@ function switchToFrame(msg) { sendError("Unable to locate frame: " + msg.json.value, 8, null); return; } - curWindow = curWindow.frames[foundFrame]; - curWindow.focus(); - sendOk(); sandbox = null; + + if (curWindow.contentWindow == null) { + // The frame we want to switch to is a remote frame; notify our parent to handle + // the switch. + curWindow = content; + sendToServer('Marionette:switchToFrame', {win: winUtil.outerWindowID, frame: foundFrame}); + } + else { + curWindow = curWindow.contentWindow; + curWindow.focus(); + sendOk(); + } } // emulator callbacks