Bug 1747222 - [remote] Move browsing context APIs from browser.js and WindowManager to TabManager r=webdriver-reviewers,whimboo

Depends on D135414

Differential Revision: https://phabricator.services.mozilla.com/D135415
This commit is contained in:
Julian Descottes 2022-01-14 21:19:53 +00:00
parent 7d8052e4a0
commit 892ae10673
8 changed files with 240 additions and 209 deletions

View File

@ -15,6 +15,7 @@ remote.jar:
# shared modules (all protocols)
content/shared/Format.jsm (shared/Format.jsm)
content/shared/Log.jsm (shared/Log.jsm)
content/shared/MobileTabBrowser.jsm (shared/MobileTabBrowser.jsm)
content/shared/Navigate.jsm (shared/Navigate.jsm)
content/shared/RecommendedPreferences.jsm (shared/RecommendedPreferences.jsm)
content/shared/Stack.jsm (shared/Stack.jsm)

View File

@ -14,6 +14,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AppInfo: "chrome://remote/content/marionette/appinfo.js",
error: "chrome://remote/content/shared/webdriver/Errors.jsm",
MessageManagerDestroyedPromise: "chrome://remote/content/marionette/sync.js",
TabManager: "chrome://remote/content/shared/TabManager.jsm",
waitForEvent: "chrome://remote/content/marionette/sync.js",
WebElementEventTarget: "chrome://remote/content/marionette/dom.js",
windowManager: "chrome://remote/content/shared/WindowManager.jsm",
@ -61,91 +62,6 @@ Context.Chrome = "chrome";
Context.Content = "content";
this.Context = Context;
// GeckoView shim for Desktop's gBrowser
class MobileTabBrowser {
constructor(window) {
this.window = window;
}
get tabs() {
return [this.window.tab];
}
get selectedTab() {
return this.window.tab;
}
set selectedTab(tab) {
if (tab != this.selectedTab) {
throw new Error("GeckoView only supports a single tab");
}
// Synthesize a custom TabSelect event to indicate that a tab has been
// selected even when we don't change it.
const event = this.window.CustomEvent("TabSelect", {
bubbles: true,
cancelable: false,
detail: {
previousTab: this.selectedTab,
},
});
this.window.document.dispatchEvent(event);
}
get selectedBrowser() {
return this.selectedTab.linkedBrowser;
}
addEventListener() {
this.window.addEventListener(...arguments);
}
removeEventListener() {
this.window.removeEventListener(...arguments);
}
}
/**
* Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
*
* @param {Tab} tab
* The tab whose browser needs to be returned.
*
* @return {Browser}
* The linked browser for the tab or null if no browser can be found.
*/
browser.getBrowserForTab = function(tab) {
if (tab && "linkedBrowser" in tab) {
return tab.linkedBrowser;
}
return null;
};
/**
* Return the tab browser for the specified chrome window.
*
* @param {ChromeWindow} win
* Window whose <code>tabbrowser</code> needs to be accessed.
*
* @return {Tab}
* Tab browser or null if it's not a browser window.
*/
browser.getTabBrowser = function(window) {
// GeckoView
if (AppInfo.isAndroid) {
return new MobileTabBrowser(window);
// Firefox
} else if ("gBrowser" in window) {
return window.gBrowser;
// Thunderbird
} else if (window.document.getElementById("tabmail")) {
return window.document.getElementById("tabmail");
}
return null;
};
/**
* Creates a browsing context wrapper.
*
@ -165,7 +81,7 @@ browser.Context = class {
// In Firefox this is <xul:tabbrowser> (not <xul:browser>!)
// and MobileTabBrowser in GeckoView.
this.tabBrowser = browser.getTabBrowser(this.window);
this.tabBrowser = TabManager.getTabBrowser(this.window);
// Used to set curFrameId upon new session
this.newSession = true;
@ -186,7 +102,7 @@ browser.Context = class {
*/
get contentBrowser() {
if (this.tab) {
return browser.getBrowserForTab(this.tab);
return TabManager.getBrowserForTab(this.tab);
} else if (
this.tabBrowser &&
this.driver.isReftestBrowser(this.tabBrowser)
@ -374,7 +290,7 @@ browser.Context = class {
if (window) {
this.window = window;
this.tabBrowser = browser.getTabBrowser(this.window);
this.tabBrowser = TabManager.getTabBrowser(this.window);
}
if (!this.tabBrowser) {

View File

@ -48,6 +48,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
registerCommandsActor:
"chrome://remote/content/marionette/actors/MarionetteCommandsParent.jsm",
RemoteAgent: "chrome://remote/content/components/RemoteAgent.jsm",
TabManager: "chrome://remote/content/shared/TabManager.jsm",
TimedPromise: "chrome://remote/content/marionette/sync.js",
Timeouts: "chrome://remote/content/shared/webdriver/Capabilities.jsm",
UnhandledPromptBehavior:
@ -442,11 +443,11 @@ GeckoDriver.prototype.newSession = async function(cmd) {
this.dialogObserver.add(this.handleModalDialog.bind(this));
for (let win of windowManager.windows) {
const tabBrowser = browser.getTabBrowser(win);
const tabBrowser = TabManager.getTabBrowser(win);
if (tabBrowser) {
for (const tab of tabBrowser.tabs) {
const contentBrowser = browser.getBrowserForTab(tab);
const contentBrowser = TabManager.getBrowserForTab(tab);
this.registerBrowser(contentBrowser);
}
}
@ -489,7 +490,7 @@ GeckoDriver.prototype.newSession = async function(cmd) {
* Chrome window to register event listeners for.
*/
GeckoDriver.prototype.registerListenersForWindow = function(win) {
const tabBrowser = browser.getTabBrowser(win);
const tabBrowser = TabManager.getTabBrowser(win);
// Listen for any kind of top-level process switch
tabBrowser?.addEventListener("XULFrameLoaderCreated", this);
@ -502,7 +503,7 @@ GeckoDriver.prototype.registerListenersForWindow = function(win) {
* Chrome window to unregister event listeners for.
*/
GeckoDriver.prototype.unregisterListenersForWindow = function(win) {
const tabBrowser = browser.getTabBrowser(win);
const tabBrowser = TabManager.getTabBrowser(win);
tabBrowser?.removeEventListener("XULFrameLoaderCreated", this);
};
@ -984,7 +985,7 @@ GeckoDriver.prototype.getWindowHandle = function() {
if (this.context == Context.Chrome) {
return windowManager.getIdForWindow(this.curBrowser.window);
}
return windowManager.getIdForBrowser(this.curBrowser.contentBrowser);
return TabManager.getIdForBrowser(this.curBrowser.contentBrowser);
};
/**
@ -1005,7 +1006,7 @@ GeckoDriver.prototype.getWindowHandles = function() {
if (this.context == Context.Chrome) {
return windowManager.chromeWindowHandles.map(String);
}
return windowManager.windowHandles.map(String);
return TabManager.allBrowserUniqueIds.map(String);
};
/**
@ -1171,7 +1172,7 @@ GeckoDriver.prototype.setWindowHandle = async function(
if (!winProperties.hasTabBrowser) {
this.currentSession.contentBrowsingContext = null;
} else {
const tabBrowser = browser.getTabBrowser(winProperties.win);
const tabBrowser = TabManager.getTabBrowser(winProperties.win);
// For chrome windows such as a reftest window, `getTabBrowser` is not
// a tabbrowser, it is the content browser which should be used here.
@ -2010,21 +2011,21 @@ GeckoDriver.prototype.newWindow = async function(cmd) {
switch (type) {
case "window":
let win = await this.curBrowser.openBrowserWindow(focus, isPrivate);
contentBrowser = browser.getTabBrowser(win).selectedBrowser;
contentBrowser = TabManager.getTabBrowser(win).selectedBrowser;
break;
default:
// To not fail if a new type gets added in the future, make opening
// a new tab the default action.
let tab = await this.curBrowser.openTab(focus);
contentBrowser = browser.getBrowserForTab(tab);
contentBrowser = TabManager.getBrowserForTab(tab);
}
// Actors need the new window to be loaded to safely execute queries.
// Wait until the initial page load has been finished.
await waitForInitialNavigationCompleted(contentBrowser.browsingContext);
const id = windowManager.getIdForBrowser(contentBrowser);
const id = TabManager.getIdForBrowser(contentBrowser);
return { handle: id.toString(), type };
};
@ -2050,29 +2051,17 @@ GeckoDriver.prototype.close = async function() {
assert.open(this.getBrowsingContext({ context: Context.Content, top: true }));
await this._handleUserPrompts();
let nwins = 0;
for (let win of windowManager.windows) {
// For browser windows count the tabs. Otherwise take the window itself.
let tabbrowser = browser.getTabBrowser(win);
if (tabbrowser && tabbrowser.tabs) {
nwins += tabbrowser.tabs.length;
} else {
nwins += 1;
}
}
// If there is only one window left, do not close it. Instead return
// a faked empty array of window handles. This will instruct geckodriver
// to terminate the application.
if (nwins === 1) {
if (TabManager.getTabCount() === 1) {
return [];
}
await this.curBrowser.closeTab();
this.currentSession.contentBrowsingContext = null;
return windowManager.windowHandles.map(String);
return TabManager.allBrowserUniqueIds.map(String);
};
/**

View File

@ -0,0 +1,51 @@
/* 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";
var EXPORTED_SYMBOLS = ["MobileTabBrowser"];
// GeckoView shim for Desktop's gBrowser
class MobileTabBrowser {
constructor(window) {
this.window = window;
}
get tabs() {
return [this.window.tab];
}
get selectedTab() {
return this.window.tab;
}
set selectedTab(tab) {
if (tab != this.selectedTab) {
throw new Error("GeckoView only supports a single tab");
}
// Synthesize a custom TabSelect event to indicate that a tab has been
// selected even when we don't change it.
const event = this.window.CustomEvent("TabSelect", {
bubbles: true,
cancelable: false,
detail: {
previousTab: this.selectedTab,
},
});
this.window.document.dispatchEvent(event);
}
get selectedBrowser() {
return this.selectedTab.linkedBrowser;
}
addEventListener() {
this.window.addEventListener(...arguments);
}
removeEventListener() {
this.window.removeEventListener(...arguments);
}
}

View File

@ -12,12 +12,94 @@ var { XPCOMUtils } = ChromeUtils.import(
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
MobileTabBrowser: "chrome://remote/content/shared/MobileTabBrowser.jsm",
});
// Maps browser's permanentKey to uuid: WeakMap.<Object, string>
const browserUniqueIds = new WeakMap();
var TabManager = {
get gBrowser() {
const window = Services.wm.getMostRecentWindow("navigator:browser");
return window.gBrowser;
return this.getTabBrowser(window);
},
get windows() {
return Services.wm.getEnumerator(null);
},
/**
* Array of unique browser ids (UUIDs) for all content browsers of all
* windows.
*
* TODO: Similarly to getBrowserById, we should improve the performance of
* this getter in Bug 1750065.
*
* @return {Array<String>}
* Array of UUIDs for all content browsers.
*/
get allBrowserUniqueIds() {
const browserIds = [];
for (const win of this.windows) {
const tabBrowser = this.getTabBrowser(win);
// Only return handles for browser windows
if (tabBrowser && tabBrowser.tabs) {
for (const tab of tabBrowser.tabs) {
const contentBrowser = this.getBrowserForTab(tab);
const winId = this.getIdForBrowser(contentBrowser);
if (winId !== null) {
browserIds.push(winId);
}
}
}
}
return browserIds;
},
/**
* Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
*
* @param {Tab} tab
* The tab whose browser needs to be returned.
*
* @return {xul:browser}
* The linked browser for the tab or null if no browser can be found.
*/
getBrowserForTab(tab) {
if (tab && "linkedBrowser" in tab) {
return tab.linkedBrowser;
}
return null;
},
/**
* Return the tab browser for the specified chrome window.
*
* @param {ChromeWindow} win
* Window whose <code>tabbrowser</code> needs to be accessed.
*
* @return {Tab}
* Tab browser or null if it's not a browser window.
*/
getTabBrowser(win) {
// GeckoView
// TODO: Migrate to AppInfo.isAndroid after AppInfo moves to shared/
if (Services.appinfo.OS === "Android") {
return new MobileTabBrowser(win);
// Firefox
} else if ("gBrowser" in win) {
return win.gBrowser;
// Thunderbird
} else if (win.document.getElementById("tabmail")) {
return win.document.getElementById("tabmail");
}
return null;
},
addTab({ userContextId }) {
@ -30,6 +112,84 @@ var TabManager = {
return tab;
},
/**
* Retrieve a the browser element corresponding to the provided unique id,
* previously generated via getIdForBrowser.
*
* TODO: To avoid creating strong references on browser elements and
* potentially leaking those elements, this method loops over all windows and
* all tabs. It should be replaced by a faster implementation in Bug 1750065.
*
* @param {String} id
* A browser unique id created by getIdForBrowser.
* @return {xul:browser}
* The <xul:browser> corresponding to the provided id. Will return null if
* no matching browser element is found.
*/
getBrowserById(id) {
for (const win of this.windows) {
const tabBrowser = this.getTabBrowser(win);
if (tabBrowser && tabBrowser.tabs) {
for (let i = 0; i < tabBrowser.tabs.length; ++i) {
const contentBrowser = this.getBrowserForTab(tabBrowser.tabs[i]);
if (this.getIdForBrowser(contentBrowser) == id) {
return contentBrowser;
}
}
}
}
return null;
},
/**
* Retrieve the unique id for the given xul browser element. The id is a
* dynamically generated uuid associated with the permanentKey property of the
* given browser element.
*
* @param {xul:browser} browserElement
* The <xul:browser> for which we want to retrieve the id.
* @return {String} The unique id for this browser.
*/
getIdForBrowser(browserElement) {
if (browserElement === null) {
return null;
}
const key = browserElement.permanentKey;
if (!browserUniqueIds.has(key)) {
const uuid = Services.uuid.generateUUID().toString();
browserUniqueIds.set(key, uuid.substring(1, uuid.length - 1));
}
return browserUniqueIds.get(key);
},
/**
* Retrieve the unique id for the browser element owning the provided browsing
* context.
*
* @param {BrowsingContext} browsingContext
* The browsing context for which we want to retrieve the (browser) uuid.
* @return {String} The unique id for the browser owning the browsing context.
*/
getBrowserIdForBrowsingContext(browsingContext) {
const contentBrowser = browsingContext.top.embedderElement;
return this.getIdForBrowser(contentBrowser);
},
getTabCount() {
let count = 0;
for (const win of this.windows) {
// For browser windows count the tabs. Otherwise take the window itself.
const tabbrowser = this.getTabBrowser(win);
if (tabbrowser?.tabs) {
count += tabbrowser.tabs.length;
} else {
count += 1;
}
}
return count;
},
removeTab(tab) {
this.gBrowser.removeTab(tab);
},

View File

@ -14,8 +14,8 @@ XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
AppInfo: "chrome://remote/content/marionette/appinfo.js",
browser: "chrome://remote/content/marionette/browser.js",
error: "chrome://remote/content/shared/webdriver/Errors.jsm",
TabManager: "chrome://remote/content/shared/TabManager.jsm",
TimedPromise: "chrome://remote/content/marionette/sync.js",
waitForEvent: "chrome://remote/content/marionette/sync.js",
waitForObserverTopic: "chrome://remote/content/marionette/sync.js",
@ -28,32 +28,10 @@ XPCOMUtils.defineLazyModuleGetters(this, {
*/
class WindowManager {
constructor() {
// Maps browser's permanentKey to uuid: WeakMap.<Object, string>
this._windowHandles = new WeakMap();
// Maps ChromeWindow to uuid: WeakMap.<Object, string>
this._chromeWindowHandles = new WeakMap();
}
get windowHandles() {
const windowHandles = [];
for (const win of this.windows) {
const tabBrowser = browser.getTabBrowser(win);
// Only return handles for browser windows
if (tabBrowser && tabBrowser.tabs) {
for (const tab of tabBrowser.tabs) {
const winId = this.getIdForBrowser(browser.getBrowserForTab(tab));
if (winId !== null) {
windowHandles.push(winId);
}
}
}
}
return windowHandles;
}
get chromeWindowHandles() {
const chromeWindowHandles = [];
@ -88,11 +66,11 @@ class WindowManager {
// Otherwise check if the chrome window has a tab browser, and that it
// contains a tab with the wanted window handle.
const tabBrowser = browser.getTabBrowser(win);
const tabBrowser = TabManager.getTabBrowser(win);
if (tabBrowser && tabBrowser.tabs) {
for (let i = 0; i < tabBrowser.tabs.length; ++i) {
let contentBrowser = browser.getBrowserForTab(tabBrowser.tabs[i]);
let contentWindowId = this.getIdForBrowser(contentBrowser);
let contentBrowser = TabManager.getBrowserForTab(tabBrowser.tabs[i]);
let contentWindowId = TabManager.getIdForBrowser(contentBrowser);
if (contentWindowId == handle) {
return this.getWindowProperties(win, { tabIndex: i });
@ -104,35 +82,6 @@ class WindowManager {
return null;
}
/**
* Retrieve a the browser element corresponding to the provided unique id,
* previously generated via getIdForBrowser.
*
* TODO: To avoid creating strong references on browser elements and
* potentially leaking those elements, this method loops over all windows and
* all tabs. It should be replaced by a faster implementation in Bug 1750065.
*
* @param {String} id
* A browser unique id created by getIdForBrowser.
* @return {xul:browser}
* The <xul:browser> corresponding to the provided id. Will return null if
* no matching browser element is found.
*/
getBrowserById(id) {
for (const win of this.windows) {
const tabBrowser = browser.getTabBrowser(win);
if (tabBrowser && tabBrowser.tabs) {
for (let i = 0; i < tabBrowser.tabs.length; ++i) {
const contentBrowser = browser.getBrowserForTab(tabBrowser.tabs[i]);
if (this.getIdForBrowser(contentBrowser) == id) {
return contentBrowser;
}
}
}
}
return null;
}
/**
* A set of properties describing a window and that should allow to uniquely
* identify it. The described window can either be a Chrome Window or a
@ -166,46 +115,11 @@ class WindowManager {
return {
win,
id: this.getIdForWindow(win),
hasTabBrowser: !!browser.getTabBrowser(win),
hasTabBrowser: !!TabManager.getTabBrowser(win),
tabIndex: options.tabIndex,
};
}
/**
* Retrieves an id for the given xul browser element. The id is a dynamically
* generated uuid associated with the permanentKey property of the given
* browser element.
*
* @param {xul:browser} browserElement
* The <xul:browser> for which we want to retrieve the id.
* @return {String} The unique id for this browser.
*/
getIdForBrowser(browserElement) {
if (browserElement === null) {
return null;
}
const key = browserElement.permanentKey;
if (!this._windowHandles.has(key)) {
const uuid = Services.uuid.generateUUID().toString();
this._windowHandles.set(key, uuid.substring(1, uuid.length - 1));
}
return this._windowHandles.get(key);
}
/**
* Retrieve an id for the browser element owning the provided browsing
* context.
*
* @param {BrowsingContext} browsingContext
* The browsing context for which we want to retrieve the (browser) uuid.
* @return {String} The unique id for the browser owning the browsing context.
*/
getBrowserIdForBrowsingContext(browsingContext) {
const contentBrowser = browsingContext.top.embedderElement;
return this.getIdForBrowser(contentBrowser);
}
/**
* Retrieves an id for the given chrome window. The id is a dynamically
* generated uuid associated with the window object.

View File

@ -3,8 +3,8 @@
"use strict";
const { windowManager } = ChromeUtils.import(
"chrome://remote/content/shared/WindowManager.jsm"
const { TabManager } = ChromeUtils.import(
"chrome://remote/content/shared/TabManager.jsm"
);
const COM_TEST_PAGE = "https://example.com/document-builder.sjs?html=COM";
@ -80,7 +80,7 @@ function sendBroadcastForTopBrowsingContext(
{},
{
type: CONTEXT_DESCRIPTOR_TYPES.TOP_BROWSING_CONTEXT,
id: windowManager.getBrowserIdForBrowsingContext(topBrowsingContext),
id: TabManager.getBrowserIdForBrowsingContext(topBrowsingContext),
},
rootMessageHandler
);

View File

@ -19,7 +19,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
"chrome://remote/content/shared/messagehandler/transports/FrameContextUtils.jsm",
MessageHandlerFrameActor:
"chrome://remote/content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.jsm",
windowManager: "chrome://remote/content/shared/WindowManager.jsm",
TabManager: "chrome://remote/content/shared/TabManager.jsm",
});
/**
@ -119,7 +119,7 @@ class FrameTransport {
}
if (type === CONTEXT_DESCRIPTOR_TYPES.TOP_BROWSING_CONTEXT) {
const { browserId } = windowManager.getBrowserById(id);
const { browserId } = TabManager.getBrowserById(id);
return this._getBrowsingContexts({ browserId });
}