summaryrefslogtreecommitdiffstats
path: root/testing/marionette/frame.js
blob: fc713eb97b4913e1d964910ae4fa3e2eaafc05d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/* 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"];

this.frame = {};

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

// list of OOP frames that has the frame script loaded
var remoteFrames = [];

/**
 * 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.
   */
  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;
        }
    }
  }

  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.QueryInterface(Ci.nsIFrameLoaderOwner)
        .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 < remoteFrames.length; i++) {
      let f = remoteFrames[i];
      let fmm = f.messageManager.get();
      try {
        fmm.sendAsyncMessage("aliveCheck", {});
      } catch (e) {
        if (e.result == Cr.NS_ERROR_NOT_INITIALIZED) {
          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);
    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:getImportedScripts", this.driver.importedScripts);
    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]);