Bug 1625482 [wpt PR 22496] - [ScrollTimeline] Do not show scrollbar to bypass flakine...
[gecko.git] / testing / marionette / browser.js
blob8c4f2a814e80e19a9672e1b6d2bd12546c932bc4
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/. */
5 "use strict";
6 /* global frame */
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");
23 const {
24   MessageManagerDestroyedPromise,
25   waitForEvent,
26   waitForObserverTopic,
27 } = ChromeUtils.import("chrome://marionette/content/sync.js");
29 XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
31 this.EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"];
33 /** @namespace */
34 this.browser = {};
36 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
38 /**
39  * Variations of Marionette contexts.
40  *
41  * Choosing a context through the <tt>Marionette:SetContext</tt>
42  * command directs all subsequent browsing context scoped commands
43  * to that context.
44  */
45 class Context {
46   /**
47    * Gets the correct context from a string.
48    *
49    * @param {string} s
50    *     Context string serialisation.
51    *
52    * @return {Context}
53    *     Context.
54    *
55    * @throws {TypeError}
56    *     If <var>s</var> is not a context.
57    */
58   static fromString(s) {
59     switch (s) {
60       case "chrome":
61         return Context.Chrome;
63       case "content":
64         return Context.Content;
66       default:
67         throw new TypeError(`Unknown context: ${s}`);
68     }
69   }
71 Context.Chrome = "chrome";
72 Context.Content = "content";
73 this.Context = Context;
75 /**
76  * Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
77  *
78  * @param {Tab} tab
79  *     The tab whose browser needs to be returned.
80  *
81  * @return {Browser}
82  *     The linked browser for the tab or null if no browser can be found.
83  */
84 browser.getBrowserForTab = function(tab) {
85   // Fennec
86   if (tab && "browser" in tab) {
87     return tab.browser;
89     // Firefox
90   } else if (tab && "linkedBrowser" in tab) {
91     return tab.linkedBrowser;
92   }
94   return null;
97 /**
98  * Return the tab browser for the specified chrome window.
99  *
100  * @param {ChromeWindow} win
101  *     Window whose <code>tabbrowser</code> needs to be accessed.
103  * @return {Tab}
104  *     Tab browser or null if it's not a browser window.
105  */
106 browser.getTabBrowser = function(window) {
107   // Fennec
108   if ("BrowserApp" in window) {
109     return window.BrowserApp;
111     // Firefox
112   } else if ("gBrowser" in window) {
113     return window.gBrowser;
115     // Thunderbird
116   } else if (window.document.getElementById("tabmail")) {
117     return window.document.getElementById("tabmail");
118   }
120   return null;
124  * Creates a browsing context wrapper.
126  * Browsing contexts handle interactions with the browser, according to
127  * the current environment (Firefox, Fennec).
128  */
129 browser.Context = class {
130   /**
131    * @param {ChromeWindow} win
132    *     ChromeWindow that contains the top-level browsing context.
133    * @param {GeckoDriver} driver
134    *     Reference to driver instance.
135    */
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.
158     this.tab = null;
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);
172   }
174   /**
175    * Returns the content browser for the currently selected tab.
176    * If there is no tab selected, null will be returned.
177    */
178   get contentBrowser() {
179     if (this.tab) {
180       return browser.getBrowserForTab(this.tab);
181     } else if (
182       this.tabBrowser &&
183       this.driver.isReftestBrowser(this.tabBrowser)
184     ) {
185       return this.tabBrowser;
186     }
188     return null;
189   }
191   get messageManager() {
192     if (this.contentBrowser) {
193       return this.contentBrowser.messageManager;
194     }
196     return null;
197   }
199   /**
200    * Checks if the browsing context has been discarded.
201    *
202    * The browsing context will have been discarded if the content
203    * browser, represented by the <code>&lt;xul:browser&gt;</code>,
204    * has been detached.
205    *
206    * @return {boolean}
207    *     True if browsing context has been discarded, false otherwise.
208    */
209   get closed() {
210     return this.contentBrowser === null;
211   }
213   /**
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.
217    */
218   get curFrameId() {
219     let rv = null;
220     if (this.tab || this.driver.isReftestBrowser(this.contentBrowser)) {
221       rv = this.getIdForBrowser(this.contentBrowser);
222     }
223     return rv;
224   }
226   /**
227    * Returns the current title of the content browser.
228    *
229    * @return {string}
230    *     Read-only property containing the current title.
231    *
232    * @throws {NoSuchWindowError}
233    *     If the current ChromeWindow does not have a content browser.
234    */
235   get currentTitle() {
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;
240     }
241     throw new NoSuchWindowError(
242       "Current window does not have a content browser"
243     );
244   }
246   /**
247    * Returns the current URI of the content browser.
248    *
249    * @return {nsIURI}
250    *     Read-only property containing the currently loaded URL.
251    *
252    * @throws {NoSuchWindowError}
253    *     If the current ChromeWindow does not have a content browser.
254    */
255   get currentURI() {
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;
260     }
261     throw new NoSuchWindowError(
262       "Current window does not have a content browser"
263     );
264   }
266   /**
267    * Gets the position and dimensions of the top-level browsing context.
268    *
269    * @return {Map.<string, number>}
270    *     Object with |x|, |y|, |width|, and |height| properties.
271    */
272   get rect() {
273     return {
274       x: this.window.screenX,
275       y: this.window.screenY,
276       width: this.window.outerWidth,
277       height: this.window.outerHeight,
278     };
279   }
281   /**
282    * Retrieves the current tabmodal UI object.  According to the browser
283    * associated with the currently selected tab.
284    */
285   getTabModal() {
286     let br = this.contentBrowser;
287     if (!br.hasAttribute("tabmodalPromptShowing")) {
288       return null;
289     }
291     // The modal is a direct sibling of the browser element.
292     // See tabbrowser.xml's getTabModalPromptBox.
293     let modalElements = br.parentNode.getElementsByTagNameNS(
294       XUL_NS,
295       "tabmodalprompt"
296     );
298     return br.tabModalPromptBox.prompts.get(modalElements[0]);
299   }
301   /**
302    * Close the current window.
303    *
304    * @return {Promise}
305    *     A promise which is resolved when the current window has been closed.
306    */
307   closeWindow() {
308     let destroyed = new MessageManagerDestroyedPromise(
309       this.window.messageManager
310     );
311     let unloaded = waitForEvent(this.window, "unload");
313     this.window.close();
315     return Promise.all([destroyed, unloaded]);
316   }
318   /**
319    * Focus the current window.
320    *
321    * @return {Promise}
322    *     A promise which is resolved when the current window has been focused.
323    */
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 });
329       this.window.focus();
331       await Promise.all([activated, focused]);
332     }
333   }
335   /**
336    * Open a new browser window.
337    *
338    * @return {Promise}
339    *     A promise resolving to the newly created chrome window.
340    */
341   async openBrowserWindow(focus = false, isPrivate = false) {
342     switch (this.driver.appName) {
343       case "firefox":
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
354         );
356         win.focus();
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.
361         if (!focus) {
362           await this.focusWindow();
363         }
365         return win;
367       default:
368         throw new UnsupportedOperationError(
369           `openWindow() not supported in ${this.driver.appName}`
370         );
371     }
372   }
374   /**
375    * Close the current tab.
376    *
377    * @return {Promise}
378    *     A promise which is resolved when the current tab has been closed.
379    *
380    * @throws UnsupportedOperationError
381    *     If tab handling for the current application isn't supported.
382    */
383   closeTab() {
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.
386     if (
387       !this.tabBrowser ||
388       !this.tabBrowser.tabs ||
389       this.tabBrowser.tabs.length === 1 ||
390       !this.tab
391     ) {
392       return this.closeWindow();
393     }
395     let destroyed = new MessageManagerDestroyedPromise(this.messageManager);
396     let tabClosed;
398     switch (this.driver.appName) {
399       case "fennec":
400         // Fennec
401         tabClosed = waitForEvent(this.tabBrowser.deck, "TabClose");
402         this.tabBrowser.closeTab(this.tab);
403         break;
405       case "firefox":
406         tabClosed = waitForEvent(this.tab, "TabClose");
407         this.tabBrowser.removeTab(this.tab);
408         break;
410       default:
411         throw new UnsupportedOperationError(
412           `closeTab() not supported in ${this.driver.appName}`
413         );
414     }
416     return Promise.all([destroyed, tabClosed]);
417   }
419   /**
420    * Open a new tab in the currently selected chrome window.
421    */
422   async openTab(focus = false) {
423     let tab = null;
424     let tabOpened = waitForEvent(this.window, "TabOpen");
426     switch (this.driver.appName) {
427       case "fennec":
428         tab = this.tabBrowser.addTab(null);
429         this.tabBrowser.selectTab(focus ? tab : this.tab);
430         break;
432       case "firefox":
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.
438         if (!focus) {
439           this.tabBrowser.selectedTab = this.tab;
440         }
442         break;
444       default:
445         throw new UnsupportedOperationError(
446           `openTab() not supported in ${this.driver.appName}`
447         );
448     }
450     await tabOpened;
452     return tab;
453   }
455   /**
456    * Set the current tab.
457    *
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.
466    *
467    * @throws UnsupportedOperationError
468    *     If tab handling for the current application isn't supported.
469    */
470   async switchToTab(index, window = undefined, focus = true) {
471     let currentTab = this.tabBrowser.selectedTab;
473     if (window) {
474       this.window = window;
475       this.tabBrowser = browser.getTabBrowser(this.window);
476     }
478     if (!this.tabBrowser) {
479       return;
480     }
482     if (typeof index == "undefined") {
483       this.tab = this.tabBrowser.selectedTab;
484     } else {
485       this.tab = this.tabBrowser.tabs[index];
486     }
488     if (focus && this.tab != currentTab) {
489       let tabSelected = waitForEvent(this.window, "TabSelect");
491       switch (this.driver.appName) {
492         case "fennec":
493           this.tabBrowser.selectTab(this.tab);
494           await tabSelected;
495           break;
497         case "firefox":
498           this.tabBrowser.selectedTab = this.tab;
499           await tabSelected;
500           break;
502         default:
503           throw new UnsupportedOperationError(
504             `switchToTab() not supported in ${this.driver.appName}`
505           );
506       }
507     }
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);
512   }
514   /**
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.
518    *
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.
523    */
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.
528       if (!this.tab) {
529         this.switchToTab();
530       }
532       if (target === this.contentBrowser) {
533         this.updateIdForBrowser(this.contentBrowser, uid);
534         this._needsFlushPendingCommands = true;
535       }
536     }
538     // used to delete sessions
539     this.knownFrames.push(uid);
540   }
542   /**
543    * Flush any queued pending commands.
544    *
545    * Needs to be run after a process change for the frame script.
546    */
547   flushPendingCommands() {
548     if (!this._needsFlushPendingCommands) {
549       return;
550     }
552     this.pendingCommands.forEach(cb => cb());
553     this._needsFlushPendingCommands = false;
554   }
556   /**
557    * This function intercepts commands interacting with content and queues
558    * or executes them as needed.
559    *
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.
565    */
566   executeWhenReady(cb) {
567     if (this._needsFlushPendingCommands) {
568       this.pendingCommands.push(cb);
569     } else {
570       cb();
571     }
572   }
576  * The window storage is used to save outer window IDs mapped to weak
577  * references of Window objects.
579  * Usage:
581  *     let wins = new browser.Windows();
582  *     wins.set(browser.outerWindowID, window);
584  *     ...
586  *     let win = wins.get(browser.outerWindowID);
588  */
589 browser.Windows = class extends Map {
590   /**
591    * Save a weak reference to the Window object.
592    *
593    * @param {string} id
594    *     Outer window ID.
595    * @param {Window} win
596    *     Window object to save.
597    *
598    * @return {browser.Windows}
599    *     Instance of self.
600    */
601   set(id, win) {
602     let wref = Cu.getWeakReference(win);
603     super.set(id, wref);
604     return this;
605   }
607   /**
608    * Get the window object stored by provided |id|.
609    *
610    * @param {string} id
611    *     Outer window ID.
612    *
613    * @return {Window}
614    *     Saved window object.
615    *
616    * @throws {RangeError}
617    *     If |id| is not in the store.
618    */
619   get(id) {
620     let wref = super.get(id);
621     if (!wref) {
622       throw new RangeError();
623     }
624     return wref.get();
625   }
629  * Marionette representation of the {@link ChromeWindow} window state.
631  * @enum {string}
632  */
633 const WindowState = {
634   Maximized: "maximized",
635   Minimized: "minimized",
636   Normal: "normal",
637   Fullscreen: "fullscreen",
639   /**
640    * Converts {@link nsIDOMChromeWindow.windowState} to WindowState.
641    *
642    * @param {number} windowState
643    *     Attribute from {@link nsIDOMChromeWindow.windowState}.
644    *
645    * @return {WindowState}
646    *     JSON representation.
647    *
648    * @throws {TypeError}
649    *     If <var>windowState</var> was unknown.
650    */
651   from(windowState) {
652     switch (windowState) {
653       case 1:
654         return WindowState.Maximized;
656       case 2:
657         return WindowState.Minimized;
659       case 3:
660         return WindowState.Normal;
662       case 4:
663         return WindowState.Fullscreen;
665       default:
666         throw new TypeError(`Unknown window state: ${windowState}`);
667     }
668   },
670 this.WindowState = WindowState;