1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
8 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9 const { XPCOMUtils } = ChromeUtils.import(
10 "resource://gre/modules/XPCOMUtils.jsm"
13 const { WebElementEventTarget } = ChromeUtils.import(
14 "chrome://marionette/content/dom.js"
16 const { element } = ChromeUtils.import(
17 "chrome://marionette/content/element.js"
19 const { NoSuchWindowError, UnsupportedOperationError } = ChromeUtils.import(
20 "chrome://marionette/content/error.js"
22 const { Log } = ChromeUtils.import("chrome://marionette/content/log.js");
24 MessageManagerDestroyedPromise,
27 } = ChromeUtils.import("chrome://marionette/content/sync.js");
29 XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
31 this.EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"];
36 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
39 * Variations of Marionette contexts.
41 * Choosing a context through the <tt>Marionette:SetContext</tt>
42 * command directs all subsequent browsing context scoped commands
47 * Gets the correct context from a string.
50 * Context string serialisation.
56 * If <var>s</var> is not a context.
58 static fromString(s) {
61 return Context.Chrome;
64 return Context.Content;
67 throw new TypeError(`Unknown context: ${s}`);
71 Context.Chrome = "chrome";
72 Context.Content = "content";
73 this.Context = Context;
76 * Get the <code><xul:browser></code> for the specified tab.
79 * The tab whose browser needs to be returned.
82 * The linked browser for the tab or null if no browser can be found.
84 browser.getBrowserForTab = function(tab) {
86 if (tab && "browser" in tab) {
90 } else if (tab && "linkedBrowser" in tab) {
91 return tab.linkedBrowser;
98 * Return the tab browser for the specified chrome window.
100 * @param {ChromeWindow} win
101 * Window whose <code>tabbrowser</code> needs to be accessed.
104 * Tab browser or null if it's not a browser window.
106 browser.getTabBrowser = function(window) {
108 if ("BrowserApp" in window) {
109 return window.BrowserApp;
112 } else if ("gBrowser" in window) {
113 return window.gBrowser;
116 } else if (window.document.getElementById("tabmail")) {
117 return window.document.getElementById("tabmail");
124 * Creates a browsing context wrapper.
126 * Browsing contexts handle interactions with the browser, according to
127 * the current environment (Firefox, Fennec).
129 browser.Context = class {
131 * @param {ChromeWindow} win
132 * ChromeWindow that contains the top-level browsing context.
133 * @param {GeckoDriver} driver
134 * Reference to driver instance.
136 constructor(window, driver) {
137 this.window = window;
138 this.driver = driver;
140 // In Firefox this is <xul:tabbrowser> (not <xul:browser>!)
141 // and BrowserApp in Fennec
142 this.tabBrowser = browser.getTabBrowser(this.window);
144 this.knownFrames = [];
146 // Used to set curFrameId upon new session
147 this.newSession = true;
149 this.seenEls = new element.Store();
151 // A reference to the tab corresponding to the current window handle,
152 // if any. Specifically, this.tab refers to the last tab that Marionette
153 // switched to in this browser window. Note that this may not equal the
154 // currently selected tab. For example, if Marionette switches to tab
155 // A, and then clicks on a button that opens a new tab B in the same
156 // browser window, this.tab will still point to tab A, despite tab B
157 // being the currently selected tab.
160 // Commands which trigger a navigation can cause the frame script to be
161 // moved to a different process. To not loose the currently active
162 // command, or any other already pushed following command, store them as
163 // long as they haven't been fully processed. The commands get flushed
164 // after a new browser has been registered.
165 this.pendingCommands = [];
166 this._needsFlushPendingCommands = false;
168 this.frameRegsPending = 0;
170 this.getIdForBrowser = driver.getIdForBrowser.bind(driver);
171 this.updateIdForBrowser = driver.updateIdForBrowser.bind(driver);
175 * Returns the content browser for the currently selected tab.
176 * If there is no tab selected, null will be returned.
178 get contentBrowser() {
180 return browser.getBrowserForTab(this.tab);
183 this.driver.isReftestBrowser(this.tabBrowser)
185 return this.tabBrowser;
191 get messageManager() {
192 if (this.contentBrowser) {
193 return this.contentBrowser.messageManager;
200 * Checks if the browsing context has been discarded.
202 * The browsing context will have been discarded if the content
203 * browser, represented by the <code><xul:browser></code>,
207 * True if browsing context has been discarded, false otherwise.
210 return this.contentBrowser === null;
214 * The current frame ID is managed per browser element on desktop in
215 * case the ID needs to be refreshed. The currently selected window is
216 * identified by a tab.
220 if (this.tab || this.driver.isReftestBrowser(this.contentBrowser)) {
221 rv = this.getIdForBrowser(this.contentBrowser);
227 * Returns the current title of the content browser.
230 * Read-only property containing the current title.
232 * @throws {NoSuchWindowError}
233 * If the current ChromeWindow does not have a content browser.
236 // Bug 1363368 - contentBrowser could be null until we wait for its
237 // initialization been finished
238 if (this.contentBrowser) {
239 return this.contentBrowser.contentTitle;
241 throw new NoSuchWindowError(
242 "Current window does not have a content browser"
247 * Returns the current URI of the content browser.
250 * Read-only property containing the currently loaded URL.
252 * @throws {NoSuchWindowError}
253 * If the current ChromeWindow does not have a content browser.
256 // Bug 1363368 - contentBrowser could be null until we wait for its
257 // initialization been finished
258 if (this.contentBrowser) {
259 return this.contentBrowser.currentURI;
261 throw new NoSuchWindowError(
262 "Current window does not have a content browser"
267 * Gets the position and dimensions of the top-level browsing context.
269 * @return {Map.<string, number>}
270 * Object with |x|, |y|, |width|, and |height| properties.
274 x: this.window.screenX,
275 y: this.window.screenY,
276 width: this.window.outerWidth,
277 height: this.window.outerHeight,
282 * Retrieves the current tabmodal UI object. According to the browser
283 * associated with the currently selected tab.
286 let br = this.contentBrowser;
287 if (!br.hasAttribute("tabmodalPromptShowing")) {
291 // The modal is a direct sibling of the browser element.
292 // See tabbrowser.xml's getTabModalPromptBox.
293 let modalElements = br.parentNode.getElementsByTagNameNS(
298 return br.tabModalPromptBox.prompts.get(modalElements[0]);
302 * Close the current window.
305 * A promise which is resolved when the current window has been closed.
308 let destroyed = new MessageManagerDestroyedPromise(
309 this.window.messageManager
311 let unloaded = waitForEvent(this.window, "unload");
315 return Promise.all([destroyed, unloaded]);
319 * Focus the current window.
322 * A promise which is resolved when the current window has been focused.
324 async focusWindow() {
325 if (Services.focus.activeWindow != this.window) {
326 let activated = waitForEvent(this.window, "activate");
327 let focused = waitForEvent(this.window, "focus", { capture: true });
331 await Promise.all([activated, focused]);
336 * Open a new browser window.
339 * A promise resolving to the newly created chrome window.
341 async openBrowserWindow(focus = false, isPrivate = false) {
342 switch (this.driver.appName) {
344 // Open new browser window, and wait until it is fully loaded.
345 // Also wait for the window to be focused and activated to prevent a
346 // race condition when promptly focusing to the original window again.
347 let win = this.window.OpenBrowserWindow({ private: isPrivate });
349 let activated = waitForEvent(win, "activate");
350 let focused = waitForEvent(win, "focus", { capture: true });
351 let startup = waitForObserverTopic(
352 "browser-delayed-startup-finished",
353 subject => subject == win
357 await Promise.all([activated, focused, startup]);
359 // The new window shouldn't get focused. As such set the
360 // focus back to the opening window.
362 await this.focusWindow();
368 throw new UnsupportedOperationError(
369 `openWindow() not supported in ${this.driver.appName}`
375 * Close the current tab.
378 * A promise which is resolved when the current tab has been closed.
380 * @throws UnsupportedOperationError
381 * If tab handling for the current application isn't supported.
384 // If the current window is not a browser then close it directly. Do the
385 // same if only one remaining tab is open, or no tab selected at all.
388 !this.tabBrowser.tabs ||
389 this.tabBrowser.tabs.length === 1 ||
392 return this.closeWindow();
395 let destroyed = new MessageManagerDestroyedPromise(this.messageManager);
398 switch (this.driver.appName) {
401 tabClosed = waitForEvent(this.tabBrowser.deck, "TabClose");
402 this.tabBrowser.closeTab(this.tab);
406 tabClosed = waitForEvent(this.tab, "TabClose");
407 this.tabBrowser.removeTab(this.tab);
411 throw new UnsupportedOperationError(
412 `closeTab() not supported in ${this.driver.appName}`
416 return Promise.all([destroyed, tabClosed]);
420 * Open a new tab in the currently selected chrome window.
422 async openTab(focus = false) {
424 let tabOpened = waitForEvent(this.window, "TabOpen");
426 switch (this.driver.appName) {
428 tab = this.tabBrowser.addTab(null);
429 this.tabBrowser.selectTab(focus ? tab : this.tab);
433 this.window.BrowserOpenTab();
434 tab = this.tabBrowser.selectedTab;
436 // The new tab is always selected by default. If focus is not wanted,
437 // the previously tab needs to be selected again.
439 this.tabBrowser.selectedTab = this.tab;
445 throw new UnsupportedOperationError(
446 `openTab() not supported in ${this.driver.appName}`
456 * Set the current tab.
458 * @param {number=} index
459 * Tab index to switch to. If the parameter is undefined,
460 * the currently selected tab will be used.
461 * @param {ChromeWindow=} window
462 * Switch to this window before selecting the tab.
463 * @param {boolean=} focus
464 * A boolean value which determins whether to focus
465 * the window. Defaults to true.
467 * @throws UnsupportedOperationError
468 * If tab handling for the current application isn't supported.
470 async switchToTab(index, window = undefined, focus = true) {
471 let currentTab = this.tabBrowser.selectedTab;
474 this.window = window;
475 this.tabBrowser = browser.getTabBrowser(this.window);
478 if (!this.tabBrowser) {
482 if (typeof index == "undefined") {
483 this.tab = this.tabBrowser.selectedTab;
485 this.tab = this.tabBrowser.tabs[index];
488 if (focus && this.tab != currentTab) {
489 let tabSelected = waitForEvent(this.window, "TabSelect");
491 switch (this.driver.appName) {
493 this.tabBrowser.selectTab(this.tab);
498 this.tabBrowser.selectedTab = this.tab;
503 throw new UnsupportedOperationError(
504 `switchToTab() not supported in ${this.driver.appName}`
509 // TODO(ato): Currently tied to curBrowser, but should be moved to
510 // WebElement when introduced by https://bugzil.la/1400256.
511 this.eventObserver = new WebElementEventTarget(this.messageManager);
515 * Registers a new frame, and sets its current frame id to this frame
516 * if it is not already assigned, and if a) we already have a session
517 * or b) we're starting a new session and it is the right start frame.
519 * @param {string} uid
520 * Frame uid for use by Marionette.
521 * @param {xul:browser} target
522 * The <xul:browser> that was the target of the originating message.
524 register(uid, target) {
525 if (this.tabBrowser) {
526 // If we're setting up a new session on Firefox, we only process the
527 // registration for this frame if it belongs to the current tab.
532 if (target === this.contentBrowser) {
533 this.updateIdForBrowser(this.contentBrowser, uid);
534 this._needsFlushPendingCommands = true;
538 // used to delete sessions
539 this.knownFrames.push(uid);
543 * Flush any queued pending commands.
545 * Needs to be run after a process change for the frame script.
547 flushPendingCommands() {
548 if (!this._needsFlushPendingCommands) {
552 this.pendingCommands.forEach(cb => cb());
553 this._needsFlushPendingCommands = false;
557 * This function intercepts commands interacting with content and queues
558 * or executes them as needed.
560 * No commands interacting with content are safe to process until
561 * the new listener script is loaded and registered itself.
562 * This occurs when a command whose effect is asynchronous (such
563 * as goBack) results in process change of the frame script and new
564 * commands are subsequently posted to the server.
566 executeWhenReady(cb) {
567 if (this._needsFlushPendingCommands) {
568 this.pendingCommands.push(cb);
576 * The window storage is used to save outer window IDs mapped to weak
577 * references of Window objects.
581 * let wins = new browser.Windows();
582 * wins.set(browser.outerWindowID, window);
586 * let win = wins.get(browser.outerWindowID);
589 browser.Windows = class extends Map {
591 * Save a weak reference to the Window object.
595 * @param {Window} win
596 * Window object to save.
598 * @return {browser.Windows}
602 let wref = Cu.getWeakReference(win);
608 * Get the window object stored by provided |id|.
614 * Saved window object.
616 * @throws {RangeError}
617 * If |id| is not in the store.
620 let wref = super.get(id);
622 throw new RangeError();
629 * Marionette representation of the {@link ChromeWindow} window state.
633 const WindowState = {
634 Maximized: "maximized",
635 Minimized: "minimized",
637 Fullscreen: "fullscreen",
640 * Converts {@link nsIDOMChromeWindow.windowState} to WindowState.
642 * @param {number} windowState
643 * Attribute from {@link nsIDOMChromeWindow.windowState}.
645 * @return {WindowState}
646 * JSON representation.
648 * @throws {TypeError}
649 * If <var>windowState</var> was unknown.
652 switch (windowState) {
654 return WindowState.Maximized;
657 return WindowState.Minimized;
660 return WindowState.Normal;
663 return WindowState.Fullscreen;
666 throw new TypeError(`Unknown window state: ${windowState}`);
670 this.WindowState = WindowState;