Commit 4ea382dc authored by Jonathan Griffin's avatar Jonathan Griffin
Browse files

Bug 775116 - Fix frame switching to work with id, name, and remote frames, r=mdas, DONTBUILD(NPOTB)

parent 4bf64bd6
Loading
Loading
Loading
Loading
+168 −41
Original line number Diff line number Diff line
@@ -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,6 +105,19 @@ 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
@@ -114,8 +129,9 @@ function MarionetteDriverActor(aConnection)
                   .getService(Ci.nsIUUIDGenerator);

  this.conn = aConnection;
  this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
  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,12 +183,51 @@ 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
@@ -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,15 +1769,15 @@ 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;
      }
+22 −10
Original line number Diff line number Diff line
@@ -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