JSDoc: Source: frame.js

# Source: frame.js

    /* 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 {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;

    Cu.import("resource://gre/modules/Services.jsm");
    Cu.import("resource://gre/modules/XPCOMUtils.jsm");

    this.EXPORTED_SYMBOLS = ["frame"];

    /** @namespace */
    this.frame = {};

    const FRAME_SCRIPT = "chrome://marionette/content/listener.js";

    /**
     * An object representing a frame that Marionette has loaded a
     * frame script in.
     */
    frame.RemoteFrame = function(windowId, frameId) {
      // outerWindowId relative to main process
      this.windowId = windowId;
      // actual frame relative to the windowId's frames list
      this.frameId = frameId;
      // assigned frame ID, used for messaging
      this.targetFrameId = this.frameId;
      // list of OOP frames that has the frame script loaded
      this.remoteFrames = [];
    };

    /**
     * The FrameManager will maintain the list of Out Of Process (OOP)
     * frames and will handle frame switching between them.
     *
     * It handles explicit frame switching (switchToFrame), and implicit
     * frame switching, which occurs when a modal dialog is triggered in B2G.
     *
     * @param {GeckoDriver} driver
     *     Reference to the driver instance.
     */
    frame.Manager = class {
      constructor(driver) {
        // messageManager maintains the messageManager
        // for the current process' chrome frame or the global message manager

        // holds a member of the remoteFrames (for an OOP frame)
        // or null (for the main process)
        this.currentRemoteFrame = null;
        // frame we'll need to restore once interrupt is gone
        this.previousRemoteFrame = null;
        // set to true when we have been interrupted by a modal
        this.handledModal = false;
        this.driver = driver;
      }

      /**
       * Receives all messages from content messageManager.
       */
      /*eslint-disable*/
      receiveMessage(message) {
        switch (message.name) {
          case "MarionetteFrame:getInterruptedState":
            // this will return true if the calling frame was interrupted by a modal dialog
            if (this.previousRemoteFrame) {
              // get the frame window of the interrupted frame
              let interruptedFrame = Services.wm.getOuterWindowWithId(
                  this.previousRemoteFrame.windowId);

              if (this.previousRemoteFrame.frameId !== null) {
                // find OOP frame
                let iframes = interruptedFrame.document.getElementsByTagName("iframe");
                interruptedFrame = iframes[this.previousRemoteFrame.frameId];
              }

              // check if the interrupted frame is the same as the calling frame
              if (interruptedFrame.src == message.target.src) {
                return {value: this.handledModal};
              }

            // we get here if previousRemoteFrame and currentRemoteFrame are null,
            // i.e. if we're in a non-OOP process, or we haven't switched into an OOP frame,
            // in which case, handledModal can't be set to true
            } else if (this.currentRemoteFrame === null) {
              return {value: this.handledModal};
            }
            return {value: false};

          // handleModal is called when we need to switch frames to the main
          // process due to a modal dialog interrupt
          case "MarionetteFrame:handleModal":
            // If previousRemoteFrame was set, that means we switched into a
            // remote frame.  If this is the case, then we want to switch back
            // into the system frame.  If it isn't the case, then we're in a
            // non-OOP environment, so we don't need to handle remote frames.
            let isLocal = true;
            if (this.currentRemoteFrame !== null) {
              isLocal = false;
              this.removeMessageManagerListeners(
                  this.currentRemoteFrame.messageManager.get());

              // store the previous frame so we can switch back to it when
              // the modal is dismissed
              this.previousRemoteFrame = this.currentRemoteFrame;

              // by setting currentRemoteFrame to null,
              // it signifies we're in the main process
              this.currentRemoteFrame = null;
              this.driver.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
                  .getService(Ci.nsIMessageBroadcaster);
            }

            this.handledModal = true;
            this.driver.sendOk(this.driver.command_id);
            return {value: isLocal};

          case "MarionetteFrame:getCurrentFrameId":
            if (this.currentRemoteFrame !== null) {
              return this.currentRemoteFrame.frameId;
            }
        }
      }
      /*eslint-enable*/

      getOopFrame(winId, frameId) {
        // get original frame window
        let outerWin = Services.wm.getOuterWindowWithId(winId);
        // find the OOP frame
        let f = outerWin.document.getElementsByTagName("iframe")[frameId];
        return f;
      }

      getFrameMM(winId, frameId) {
        let oopFrame = this.getOopFrame(winId, frameId);
        let mm = oopFrame.frameLoader.messageManager;
        return mm;
      }

      /**
       * Switch to OOP frame.  We're handling this here so we can maintain
       * a list of remote frames.
       */
      switchToFrame(winId, frameId) {
        let oopFrame = this.getOopFrame(winId, frameId);
        let mm = this.getFrameMM(winId, frameId);

        // see if this frame already has our frame script loaded in it;
        // if so, just wake it up
        for (let i = 0; i < this.remoteFrames.length; i++) {
          let f = this.remoteFrames[i];
          let fmm = f.messageManager.get();
          try {
            fmm.sendAsyncMessage("aliveCheck", {});
          } catch (e) {
            if (e.result == Cr.NS_ERROR_NOT_INITIALIZED) {
              this.remoteFrames.splice(i--, 1);
              continue;
            }
          }

          if (fmm == mm) {
            this.currentRemoteFrame = f;
            this.addMessageManagerListeners(mm);

            mm.sendAsyncMessage("Marionette:restart");
            return oopFrame.id;
          }
        }

        // if we get here, then we need to load the frame script in this frame,
        // and set the frame's ChromeMessageSender as the active message manager
        // the driver will listen to.
        this.addMessageManagerListeners(mm);
        let f = new frame.RemoteFrame(winId, frameId);
        f.messageManager = Cu.getWeakReference(mm);
        this.remoteFrames.push(f);
        this.currentRemoteFrame = f;

        mm.loadFrameScript(FRAME_SCRIPT, true, true);

        return oopFrame.id;
      }

      /*
       * This function handles switching back to the frame that was
       * interrupted by the modal dialog.  It gets called by the interrupted
       * frame once the dialog is dismissed and the frame resumes its process.
       */
      switchToModalOrigin() {
        // only handle this if we indeed switched out of the modal's
        // originating frame
        if (this.previousRemoteFrame !== null) {
          this.currentRemoteFrame = this.previousRemoteFrame;
          let mm = this.currentRemoteFrame.messageManager.get();
          this.addMessageManagerListeners(mm);
        }
        this.handledModal = false;
      }

      /**
       * Adds message listeners to the driver,  listening for
       * messages from content frame scripts.  It also adds a
       * MarionetteFrame:getInterruptedState message listener to the
       * FrameManager, so the frame manager's state can be checked by the frame.
       *
       * @param {nsIMessageListenerManager} mm
       *     The message manager object, typically
       *     ChromeMessageBroadcaster or ChromeMessageSender.
       */
      addMessageManagerListeners(mm) {
        mm.addWeakMessageListener("Marionette:ok", this.driver);
        mm.addWeakMessageListener("Marionette:done", this.driver);
        mm.addWeakMessageListener("Marionette:error", this.driver);
        mm.addWeakMessageListener("Marionette:emitTouchEvent", this.driver);
        mm.addWeakMessageListener("Marionette:log", this.driver);
        mm.addWeakMessageListener("Marionette:shareData", this.driver);
        mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.driver);
        mm.addWeakMessageListener("Marionette:switchedToFrame", this.driver);
        mm.addWeakMessageListener("Marionette:getVisibleCookies", this.driver);
        mm.addWeakMessageListener("Marionette:register", this.driver);
        mm.addWeakMessageListener("Marionette:listenersAttached", this.driver);
        mm.addWeakMessageListener("MarionetteFrame:handleModal", this);
        mm.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
        mm.addWeakMessageListener("MarionetteFrame:getInterruptedState", this);
      }

      /**
       * Removes listeners for messages from content frame scripts.
       * We do not remove the MarionetteFrame:getInterruptedState or
       * the Marionette:switchToModalOrigin message listener, because we
       * want to allow all known frames to contact the frame manager so
       * that it can check if it was interrupted, and if so, it will call
       * switchToModalOrigin when its process gets resumed.
       *
       * @param {nsIMessageListenerManager} mm
       *     The message manager object, typically
       *     ChromeMessageBroadcaster or ChromeMessageSender.
       */
      removeMessageManagerListeners(mm) {
        mm.removeWeakMessageListener("Marionette:ok", this.driver);
        mm.removeWeakMessageListener("Marionette:done", this.driver);
        mm.removeWeakMessageListener("Marionette:error", this.driver);
        mm.removeWeakMessageListener("Marionette:log", this.driver);
        mm.removeWeakMessageListener("Marionette:shareData", this.driver);
        mm.removeWeakMessageListener("Marionette:switchedToFrame", this.driver);
        mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.driver);
        mm.removeWeakMessageListener(
            "Marionette:getImportedScripts", this.driver.importedScripts);
        mm.removeWeakMessageListener("Marionette:listenersAttached", this.driver);
        mm.removeWeakMessageListener("Marionette:register", this.driver);
        mm.removeWeakMessageListener("MarionetteFrame:handleModal", this);
        mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
      }
    };

    frame.Manager.prototype.QueryInterface = XPCOMUtils.generateQI(
        [Ci.nsIMessageListener, Ci.nsISupportsWeakReference]);