forked from mirrors/gecko-dev
Backed out changeset 9bda3647e32c (bug 1877469) Backed out changeset 4aa0d79f3e06 (bug 1877469)
1964 lines
59 KiB
JavaScript
1964 lines
59 KiB
JavaScript
/* 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/. */
|
|
|
|
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
|
|
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
|
|
BrowsingContextListener:
|
|
"chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
|
|
capture: "chrome://remote/content/shared/Capture.sys.mjs",
|
|
ContextDescriptorType:
|
|
"chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
|
|
getTimeoutMultiplier: "chrome://remote/content/shared/AppInfo.sys.mjs",
|
|
modal: "chrome://remote/content/shared/Prompt.sys.mjs",
|
|
registerNavigationId:
|
|
"chrome://remote/content/shared/NavigationManager.sys.mjs",
|
|
NavigationListener:
|
|
"chrome://remote/content/shared/listeners/NavigationListener.sys.mjs",
|
|
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
|
|
PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
|
|
pprint: "chrome://remote/content/shared/Format.sys.mjs",
|
|
print: "chrome://remote/content/shared/PDF.sys.mjs",
|
|
ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs",
|
|
PromptListener:
|
|
"chrome://remote/content/shared/listeners/PromptListener.sys.mjs",
|
|
setDefaultAndAssertSerializationOptions:
|
|
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
|
|
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
|
|
UserContextManager:
|
|
"chrome://remote/content/shared/UserContextManager.sys.mjs",
|
|
waitForInitialNavigationCompleted:
|
|
"chrome://remote/content/shared/Navigate.sys.mjs",
|
|
WindowGlobalMessageHandler:
|
|
"chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
|
|
windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
|
|
});
|
|
|
|
// Maximal window dimension allowed when emulating a viewport.
|
|
const MAX_WINDOW_SIZE = 10000000;
|
|
|
|
/**
|
|
* @typedef {string} ClipRectangleType
|
|
*/
|
|
|
|
/**
|
|
* Enum of possible clip rectangle types supported by the
|
|
* browsingContext.captureScreenshot command.
|
|
*
|
|
* @readonly
|
|
* @enum {ClipRectangleType}
|
|
*/
|
|
export const ClipRectangleType = {
|
|
Box: "box",
|
|
Element: "element",
|
|
};
|
|
|
|
/**
|
|
* @typedef {object} CreateType
|
|
*/
|
|
|
|
/**
|
|
* Enum of types supported by the browsingContext.create command.
|
|
*
|
|
* @readonly
|
|
* @enum {CreateType}
|
|
*/
|
|
const CreateType = {
|
|
tab: "tab",
|
|
window: "window",
|
|
};
|
|
|
|
/**
|
|
* @typedef {string} LocatorType
|
|
*/
|
|
|
|
/**
|
|
* Enum of types supported by the browsingContext.locateNodes command.
|
|
*
|
|
* @readonly
|
|
* @enum {LocatorType}
|
|
*/
|
|
export const LocatorType = {
|
|
css: "css",
|
|
innerText: "innerText",
|
|
xpath: "xpath",
|
|
};
|
|
|
|
/**
|
|
* @typedef {string} OriginType
|
|
*/
|
|
|
|
/**
|
|
* Enum of origin type supported by the
|
|
* browsingContext.captureScreenshot command.
|
|
*
|
|
* @readonly
|
|
* @enum {OriginType}
|
|
*/
|
|
export const OriginType = {
|
|
document: "document",
|
|
viewport: "viewport",
|
|
};
|
|
|
|
const TIMEOUT_SET_HISTORY_INDEX = 1000;
|
|
|
|
/**
|
|
* Enum of user prompt types supported by the browsingContext.handleUserPrompt
|
|
* command, these types can be retrieved from `dialog.args.promptType`.
|
|
*
|
|
* @readonly
|
|
* @enum {UserPromptType}
|
|
*/
|
|
const UserPromptType = {
|
|
alert: "alert",
|
|
confirm: "confirm",
|
|
prompt: "prompt",
|
|
beforeunload: "beforeunload",
|
|
};
|
|
|
|
/**
|
|
* An object that contains details of a viewport.
|
|
*
|
|
* @typedef {object} Viewport
|
|
*
|
|
* @property {number} height
|
|
* The height of the viewport.
|
|
* @property {number} width
|
|
* The width of the viewport.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {string} WaitCondition
|
|
*/
|
|
|
|
/**
|
|
* Wait conditions supported by WebDriver BiDi for navigation.
|
|
*
|
|
* @enum {WaitCondition}
|
|
*/
|
|
const WaitCondition = {
|
|
None: "none",
|
|
Interactive: "interactive",
|
|
Complete: "complete",
|
|
};
|
|
|
|
class BrowsingContextModule extends Module {
|
|
#contextListener;
|
|
#navigationListener;
|
|
#promptListener;
|
|
#subscribedEvents;
|
|
|
|
/**
|
|
* Create a new module instance.
|
|
*
|
|
* @param {MessageHandler} messageHandler
|
|
* The MessageHandler instance which owns this Module instance.
|
|
*/
|
|
constructor(messageHandler) {
|
|
super(messageHandler);
|
|
|
|
this.#contextListener = new lazy.BrowsingContextListener();
|
|
this.#contextListener.on("attached", this.#onContextAttached);
|
|
this.#contextListener.on("discarded", this.#onContextDiscarded);
|
|
|
|
// Create the navigation listener and listen to "navigation-started" and
|
|
// "location-changed" events.
|
|
this.#navigationListener = new lazy.NavigationListener(
|
|
this.messageHandler.navigationManager
|
|
);
|
|
this.#navigationListener.on("location-changed", this.#onLocationChanged);
|
|
this.#navigationListener.on(
|
|
"navigation-started",
|
|
this.#onNavigationStarted
|
|
);
|
|
|
|
// Create the prompt listener and listen to "closed" and "opened" events.
|
|
this.#promptListener = new lazy.PromptListener();
|
|
this.#promptListener.on("closed", this.#onPromptClosed);
|
|
this.#promptListener.on("opened", this.#onPromptOpened);
|
|
|
|
// Set of event names which have active subscriptions.
|
|
this.#subscribedEvents = new Set();
|
|
|
|
// Treat the event of moving a page to BFCache as context discarded event for iframes.
|
|
this.messageHandler.on("windowglobal-pagehide", this.#onPageHideEvent);
|
|
}
|
|
|
|
destroy() {
|
|
this.#contextListener.off("attached", this.#onContextAttached);
|
|
this.#contextListener.off("discarded", this.#onContextDiscarded);
|
|
this.#contextListener.destroy();
|
|
|
|
this.#promptListener.off("closed", this.#onPromptClosed);
|
|
this.#promptListener.off("opened", this.#onPromptOpened);
|
|
this.#promptListener.destroy();
|
|
|
|
this.#subscribedEvents = null;
|
|
|
|
this.messageHandler.off("windowglobal-pagehide", this.#onPageHideEvent);
|
|
}
|
|
|
|
/**
|
|
* Activates and focuses the given top-level browsing context.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
*/
|
|
async activate(options = {}) {
|
|
const { context: contextId } = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
if (context.parent) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Browsing Context with id ${contextId} is not top-level`
|
|
);
|
|
}
|
|
|
|
const tab = lazy.TabManager.getTabForBrowsingContext(context);
|
|
const window = lazy.TabManager.getWindowForTab(tab);
|
|
|
|
await lazy.windowManager.focusWindow(window);
|
|
await lazy.TabManager.selectTab(tab);
|
|
}
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.captureScreenshot command, as one of the available variants
|
|
* {BoxClipRectangle} or {ElementClipRectangle}, to represent a target of the command.
|
|
*
|
|
* @typedef ClipRectangle
|
|
*/
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.captureScreenshot command
|
|
* to represent a box which is going to be a target of the command.
|
|
*
|
|
* @typedef BoxClipRectangle
|
|
*
|
|
* @property {ClipRectangleType} [type=ClipRectangleType.Box]
|
|
* @property {number} x
|
|
* @property {number} y
|
|
* @property {number} width
|
|
* @property {number} height
|
|
*/
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.captureScreenshot command
|
|
* to represent an element which is going to be a target of the command.
|
|
*
|
|
* @typedef ElementClipRectangle
|
|
*
|
|
* @property {ClipRectangleType} [type=ClipRectangleType.Element]
|
|
* @property {SharedReference} element
|
|
*/
|
|
|
|
/**
|
|
* Capture a base64-encoded screenshot of the provided browsing context.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context to screenshot.
|
|
* @param {ClipRectangle=} options.clip
|
|
* A box or an element of which a screenshot should be taken.
|
|
* If not present, take a screenshot of the whole viewport.
|
|
* @param {OriginType=} options.origin
|
|
*
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
*/
|
|
async captureScreenshot(options = {}) {
|
|
const {
|
|
clip = null,
|
|
context: contextId,
|
|
origin = OriginType.viewport,
|
|
} = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
const originTypeValues = Object.values(OriginType);
|
|
lazy.assert.that(
|
|
value => originTypeValues.includes(value),
|
|
`Expected "origin" to be one of ${originTypeValues}, got ${origin}`
|
|
)(origin);
|
|
|
|
if (clip !== null) {
|
|
lazy.assert.object(clip, `Expected "clip" to be a object, got ${clip}`);
|
|
|
|
const { type } = clip;
|
|
switch (type) {
|
|
case ClipRectangleType.Box: {
|
|
const { x, y, width, height } = clip;
|
|
|
|
lazy.assert.number(x, `Expected "x" to be a number, got ${x}`);
|
|
lazy.assert.number(y, `Expected "y" to be a number, got ${y}`);
|
|
lazy.assert.number(
|
|
width,
|
|
`Expected "width" to be a number, got ${width}`
|
|
);
|
|
lazy.assert.number(
|
|
height,
|
|
`Expected "height" to be a number, got ${height}`
|
|
);
|
|
|
|
break;
|
|
}
|
|
|
|
case ClipRectangleType.Element: {
|
|
const { element } = clip;
|
|
|
|
lazy.assert.object(
|
|
element,
|
|
`Expected "element" to be an object, got ${element}`
|
|
);
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected "type" to be one of ${Object.values(
|
|
ClipRectangleType
|
|
)}, got ${type}`
|
|
);
|
|
}
|
|
}
|
|
|
|
const rect = await this.messageHandler.handleCommand({
|
|
moduleName: "browsingContext",
|
|
commandName: "_getScreenshotRect",
|
|
destination: {
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
id: context.id,
|
|
},
|
|
params: {
|
|
clip,
|
|
origin,
|
|
},
|
|
retryOnAbort: true,
|
|
});
|
|
|
|
if (rect.width === 0 || rect.height === 0) {
|
|
throw new lazy.error.UnableToCaptureScreen(
|
|
`The dimensions of requested screenshot are incorrect, got width: ${rect.width} and height: ${rect.height}.`
|
|
);
|
|
}
|
|
|
|
const canvas = await lazy.capture.canvas(
|
|
context.topChromeWindow,
|
|
context,
|
|
rect.x,
|
|
rect.y,
|
|
rect.width,
|
|
rect.height
|
|
);
|
|
|
|
return {
|
|
data: lazy.capture.toBase64(canvas),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Close the provided browsing context.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context to close.
|
|
*
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
* @throws {InvalidArgumentError}
|
|
* If the browsing context is not a top-level one.
|
|
*/
|
|
async close(options = {}) {
|
|
const { context: contextId } = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
const context = lazy.TabManager.getBrowsingContextById(contextId);
|
|
if (!context) {
|
|
throw new lazy.error.NoSuchFrameError(
|
|
`Browsing Context with id ${contextId} not found`
|
|
);
|
|
}
|
|
|
|
if (context.parent) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Browsing Context with id ${contextId} is not top-level`
|
|
);
|
|
}
|
|
|
|
if (lazy.TabManager.getTabCount() === 1) {
|
|
// The behavior when closing the very last tab is currently unspecified.
|
|
// As such behave like Marionette and don't allow closing it.
|
|
// See: https://github.com/w3c/webdriver-bidi/issues/187
|
|
return;
|
|
}
|
|
|
|
const tab = lazy.TabManager.getTabForBrowsingContext(context);
|
|
await lazy.TabManager.removeTab(tab);
|
|
}
|
|
|
|
/**
|
|
* Create a new browsing context using the provided type "tab" or "window".
|
|
*
|
|
* @param {object=} options
|
|
* @param {boolean=} options.background
|
|
* Whether the tab/window should be open in the background. Defaults to false,
|
|
* which means that the tab/window will be open in the foreground.
|
|
* @param {string=} options.referenceContext
|
|
* Id of the top-level browsing context to use as reference.
|
|
* If options.type is "tab", the new tab will open in the same window as
|
|
* the reference context, and will be added next to the reference context.
|
|
* If options.type is "window", the reference context is ignored.
|
|
* @param {CreateType} options.type
|
|
* Type of browsing context to create.
|
|
* @param {string=} options.userContext
|
|
* The id of the user context which should own the browsing context.
|
|
* Defaults to the default user context.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* If the browsing context is not a top-level one.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
*/
|
|
async create(options = {}) {
|
|
const {
|
|
background = false,
|
|
referenceContext: referenceContextId = null,
|
|
type: typeHint,
|
|
userContext: userContextId = null,
|
|
} = options;
|
|
|
|
if (![CreateType.tab, CreateType.window].includes(typeHint)) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected "type" to be one of ${Object.values(
|
|
CreateType
|
|
)}, got ${typeHint}`
|
|
);
|
|
}
|
|
|
|
lazy.assert.boolean(
|
|
background,
|
|
lazy.pprint`Expected "background" to be a boolean, got ${background}`
|
|
);
|
|
|
|
let referenceContext = null;
|
|
if (referenceContextId !== null) {
|
|
lazy.assert.string(
|
|
referenceContextId,
|
|
lazy.pprint`Expected "referenceContext" to be a string, got ${referenceContextId}`
|
|
);
|
|
|
|
referenceContext =
|
|
lazy.TabManager.getBrowsingContextById(referenceContextId);
|
|
if (!referenceContext) {
|
|
throw new lazy.error.NoSuchFrameError(
|
|
`Browsing Context with id ${referenceContextId} not found`
|
|
);
|
|
}
|
|
|
|
if (referenceContext.parent) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`referenceContext with id ${referenceContextId} is not a top-level browsing context`
|
|
);
|
|
}
|
|
}
|
|
|
|
let userContext = lazy.UserContextManager.defaultUserContextId;
|
|
if (referenceContext !== null) {
|
|
userContext =
|
|
lazy.UserContextManager.getIdByBrowsingContext(referenceContext);
|
|
}
|
|
|
|
if (userContextId !== null) {
|
|
lazy.assert.string(
|
|
userContextId,
|
|
lazy.pprint`Expected "userContext" to be a string, got ${userContextId}`
|
|
);
|
|
|
|
if (!lazy.UserContextManager.hasUserContextId(userContextId)) {
|
|
throw new lazy.error.NoSuchUserContextError(
|
|
`User Context with id ${userContextId} was not found`
|
|
);
|
|
}
|
|
|
|
userContext = userContextId;
|
|
|
|
if (
|
|
lazy.AppInfo.isAndroid &&
|
|
userContext != lazy.UserContextManager.defaultUserContextId
|
|
) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`browsingContext.create with non-default "userContext" not supported for ${lazy.AppInfo.name}`
|
|
);
|
|
}
|
|
}
|
|
|
|
let browser;
|
|
|
|
// Since each tab in GeckoView has its own Gecko instance running,
|
|
// which means also its own window object, for Android we will need to focus
|
|
// a previously focused window in case of opening the tab in the background.
|
|
const previousWindow = Services.wm.getMostRecentBrowserWindow();
|
|
const previousTab =
|
|
lazy.TabManager.getTabBrowser(previousWindow).selectedTab;
|
|
|
|
// On Android there is only a single window allowed. As such fallback to
|
|
// open a new tab instead.
|
|
const type = lazy.AppInfo.isAndroid ? "tab" : typeHint;
|
|
|
|
switch (type) {
|
|
case "window":
|
|
const newWindow = await lazy.windowManager.openBrowserWindow({
|
|
focus: !background,
|
|
userContextId: userContext,
|
|
});
|
|
browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser;
|
|
break;
|
|
|
|
case "tab":
|
|
if (!lazy.TabManager.supportsTabs()) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`browsingContext.create with type "tab" not supported in ${lazy.AppInfo.name}`
|
|
);
|
|
}
|
|
|
|
let referenceTab;
|
|
if (referenceContext !== null) {
|
|
referenceTab =
|
|
lazy.TabManager.getTabForBrowsingContext(referenceContext);
|
|
}
|
|
|
|
const tab = await lazy.TabManager.addTab({
|
|
focus: !background,
|
|
referenceTab,
|
|
userContextId: userContext,
|
|
});
|
|
browser = lazy.TabManager.getBrowserForTab(tab);
|
|
}
|
|
|
|
await lazy.waitForInitialNavigationCompleted(
|
|
browser.browsingContext.webProgress,
|
|
{
|
|
unloadTimeout: 5000,
|
|
}
|
|
);
|
|
|
|
// The tab on Android is always opened in the foreground,
|
|
// so we need to select the previous tab,
|
|
// and we have to wait until is fully loaded.
|
|
// TODO: Bug 1845559. This workaround can be removed,
|
|
// when the API to create a tab for Android supports the background option.
|
|
if (lazy.AppInfo.isAndroid && background) {
|
|
await lazy.windowManager.focusWindow(previousWindow);
|
|
await lazy.TabManager.selectTab(previousTab);
|
|
}
|
|
|
|
// Force a reflow by accessing `clientHeight` (see Bug 1847044).
|
|
browser.parentElement.clientHeight;
|
|
|
|
return {
|
|
context: lazy.TabManager.getIdForBrowser(browser),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* An object that holds the WebDriver Bidi browsing context information.
|
|
*
|
|
* @typedef BrowsingContextInfo
|
|
*
|
|
* @property {string} context
|
|
* The id of the browsing context.
|
|
* @property {string=} parent
|
|
* The parent of the browsing context if it's the root browsing context
|
|
* of the to be processed browsing context tree.
|
|
* @property {string} url
|
|
* The current documents location.
|
|
* @property {string} userContext
|
|
* The id of the user context owning this browsing context.
|
|
* @property {Array<BrowsingContextInfo>=} children
|
|
* List of child browsing contexts. Only set if maxDepth hasn't been
|
|
* reached yet.
|
|
*/
|
|
|
|
/**
|
|
* An object that holds the WebDriver Bidi browsing context tree information.
|
|
*
|
|
* @typedef BrowsingContextGetTreeResult
|
|
*
|
|
* @property {Array<BrowsingContextInfo>} contexts
|
|
* List of child browsing contexts.
|
|
*/
|
|
|
|
/**
|
|
* Returns a tree of all browsing contexts that are descendents of the
|
|
* given context, or all top-level contexts when no root is provided.
|
|
*
|
|
* @param {object=} options
|
|
* @param {number=} options.maxDepth
|
|
* Depth of the browsing context tree to traverse. If not specified
|
|
* the whole tree is returned.
|
|
* @param {string=} options.root
|
|
* Id of the root browsing context.
|
|
*
|
|
* @returns {BrowsingContextGetTreeResult}
|
|
* Tree of browsing context information.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
*/
|
|
getTree(options = {}) {
|
|
const { maxDepth = null, root: rootId = null } = options;
|
|
|
|
if (maxDepth !== null) {
|
|
lazy.assert.positiveInteger(
|
|
maxDepth,
|
|
`Expected "maxDepth" to be a positive integer, got ${maxDepth}`
|
|
);
|
|
}
|
|
|
|
let contexts;
|
|
if (rootId !== null) {
|
|
// With a root id specified return the context info for itself
|
|
// and the full tree.
|
|
lazy.assert.string(
|
|
rootId,
|
|
`Expected "root" to be a string, got ${rootId}`
|
|
);
|
|
contexts = [this.#getBrowsingContext(rootId)];
|
|
} else {
|
|
// Return all top-level browsing contexts.
|
|
contexts = lazy.TabManager.browsers.map(
|
|
browser => browser.browsingContext
|
|
);
|
|
}
|
|
|
|
const contextsInfo = contexts.map(context => {
|
|
return this.#getBrowsingContextInfo(context, { maxDepth });
|
|
});
|
|
|
|
return { contexts: contextsInfo };
|
|
}
|
|
|
|
/**
|
|
* Closes an open prompt.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context.
|
|
* @param {boolean=} options.accept
|
|
* Whether user prompt should be accepted or dismissed.
|
|
* Defaults to true.
|
|
* @param {string=} options.userText
|
|
* Input to the user prompt's value field.
|
|
* Defaults to an empty string.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {NoSuchAlertError}
|
|
* If there is no current user prompt.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
* @throws {UnsupportedOperationError}
|
|
* Raised when the command is called for "beforeunload" prompt.
|
|
*/
|
|
async handleUserPrompt(options = {}) {
|
|
const { accept = true, context: contextId, userText = "" } = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
lazy.assert.boolean(
|
|
accept,
|
|
`Expected "accept" to be a boolean, got ${accept}`
|
|
);
|
|
|
|
lazy.assert.string(
|
|
userText,
|
|
`Expected "userText" to be a string, got ${userText}`
|
|
);
|
|
|
|
const tab = lazy.TabManager.getTabForBrowsingContext(context);
|
|
const browser = lazy.TabManager.getBrowserForTab(tab);
|
|
const window = lazy.TabManager.getWindowForTab(tab);
|
|
const dialog = lazy.modal.findPrompt({
|
|
window,
|
|
contentBrowser: browser,
|
|
});
|
|
|
|
const closePrompt = async callback => {
|
|
const dialogClosed = new lazy.EventPromise(
|
|
window,
|
|
"DOMModalDialogClosed"
|
|
);
|
|
callback();
|
|
await dialogClosed;
|
|
};
|
|
|
|
if (dialog && dialog.isOpen) {
|
|
switch (dialog.promptType) {
|
|
case UserPromptType.alert: {
|
|
await closePrompt(() => dialog.accept());
|
|
return;
|
|
}
|
|
case UserPromptType.confirm: {
|
|
await closePrompt(() => {
|
|
if (accept) {
|
|
dialog.accept();
|
|
} else {
|
|
dialog.dismiss();
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
case UserPromptType.prompt: {
|
|
await closePrompt(() => {
|
|
if (accept) {
|
|
dialog.text = userText;
|
|
dialog.accept();
|
|
} else {
|
|
dialog.dismiss();
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
case UserPromptType.beforeunload: {
|
|
// TODO: Bug 1824220. Implement support for "beforeunload" prompts.
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
'"beforeunload" prompts are not supported yet.'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new lazy.error.NoSuchAlertError();
|
|
}
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.locateNodes command, as one of the available variants
|
|
* {CssLocator}, {InnerTextLocator} or {XPathLocator}, to represent a way of how lookup of nodes
|
|
* is going to be performed.
|
|
*
|
|
* @typedef Locator
|
|
*/
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.locateNodes command
|
|
* to represent a lookup by css selector.
|
|
*
|
|
* @typedef CssLocator
|
|
*
|
|
* @property {LocatorType} [type=LocatorType.css]
|
|
* @property {string} value
|
|
*/
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.locateNodes command
|
|
* to represent a lookup by inner text.
|
|
*
|
|
* @typedef InnerTextLocator
|
|
*
|
|
* @property {LocatorType} [type=LocatorType.innerText]
|
|
* @property {string} value
|
|
* @property {boolean=} ignoreCase
|
|
* @property {("full"|"partial")=} matchType
|
|
* @property {number=} maxDepth
|
|
*/
|
|
|
|
/**
|
|
* Used as an argument for browsingContext.locateNodes command
|
|
* to represent a lookup by xpath.
|
|
*
|
|
* @typedef XPathLocator
|
|
*
|
|
* @property {LocatorType} [type=LocatorType.xpath]
|
|
* @property {string} value
|
|
*/
|
|
|
|
/**
|
|
* Returns a list of all nodes matching
|
|
* the specified locator.
|
|
*
|
|
* @param {object} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context.
|
|
* @param {Locator} options.locator
|
|
* The type of lookup which is going to be used.
|
|
* @param {number=} options.maxNodeCount
|
|
* The maximum amount of nodes which is going to be returned.
|
|
* Defaults to return all the found nodes.
|
|
* @param {OwnershipModel=} options.ownership
|
|
* The ownership model to use for the serialization
|
|
* of the DOM nodes. Defaults to `OwnershipModel.None`.
|
|
* @property {string=} sandbox
|
|
* The name of the sandbox. If the value is null or empty
|
|
* string, the default realm will be used.
|
|
* @property {SerializationOptions=} serializationOptions
|
|
* An object which holds the information of how the DOM nodes
|
|
* should be serialized.
|
|
* @property {Array<SharedReference>=} startNodes
|
|
* A list of references to nodes, which are used as
|
|
* starting points for lookup.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {InvalidSelectorError}
|
|
* Raised if a locator value is invalid.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
* @throws {UnsupportedOperationError}
|
|
* Raised when unsupported lookup types are used.
|
|
*/
|
|
async locateNodes(options = {}) {
|
|
const {
|
|
context: contextId,
|
|
locator,
|
|
maxNodeCount = null,
|
|
ownership = lazy.OwnershipModel.None,
|
|
sandbox = null,
|
|
serializationOptions,
|
|
startNodes = null,
|
|
} = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
lazy.assert.object(
|
|
locator,
|
|
`Expected "locator" to be an object, got ${locator}`
|
|
);
|
|
|
|
const locatorTypes = Object.values(LocatorType);
|
|
|
|
lazy.assert.that(
|
|
locatorType => locatorTypes.includes(locatorType),
|
|
`Expected "locator.type" to be one of ${locatorTypes}, got ${locator.type}`
|
|
)(locator.type);
|
|
|
|
if (![LocatorType.css, LocatorType.xpath].includes(locator.type)) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`"locator.type" argument with value: ${locator.type} is not supported yet.`
|
|
);
|
|
}
|
|
|
|
if (maxNodeCount != null) {
|
|
const maxNodeCountErrorMsg = `Expected "maxNodeCount" to be an integer and greater than 0, got ${maxNodeCount}`;
|
|
lazy.assert.that(maxNodeCount => {
|
|
lazy.assert.integer(maxNodeCount, maxNodeCountErrorMsg);
|
|
return maxNodeCount > 0;
|
|
}, maxNodeCountErrorMsg)(maxNodeCount);
|
|
}
|
|
|
|
const ownershipTypes = Object.values(lazy.OwnershipModel);
|
|
lazy.assert.that(
|
|
ownership => ownershipTypes.includes(ownership),
|
|
`Expected "ownership" to be one of ${ownershipTypes}, got ${ownership}`
|
|
)(ownership);
|
|
|
|
if (sandbox != null) {
|
|
lazy.assert.string(
|
|
sandbox,
|
|
`Expected "sandbox" to be a string, got ${sandbox}`
|
|
);
|
|
}
|
|
|
|
const serializationOptionsWithDefaults =
|
|
lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
|
|
|
|
if (startNodes != null) {
|
|
lazy.assert.that(startNodes => {
|
|
lazy.assert.array(
|
|
startNodes,
|
|
`Expected "startNodes" to be an array, got ${startNodes}`
|
|
);
|
|
return !!startNodes.length;
|
|
}, `Expected "startNodes" to have at least one element, got ${startNodes}`)(
|
|
startNodes
|
|
);
|
|
}
|
|
|
|
const result = await this.messageHandler.forwardCommand({
|
|
moduleName: "browsingContext",
|
|
commandName: "_locateNodes",
|
|
destination: {
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
id: context.id,
|
|
},
|
|
params: {
|
|
locator,
|
|
maxNodeCount,
|
|
resultOwnership: ownership,
|
|
sandbox,
|
|
serializationOptions: serializationOptionsWithDefaults,
|
|
startNodes,
|
|
},
|
|
});
|
|
|
|
return {
|
|
nodes: result.serializedNodes,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* An object that holds the WebDriver Bidi navigation information.
|
|
*
|
|
* @typedef BrowsingContextNavigateResult
|
|
*
|
|
* @property {string} navigation
|
|
* Unique id for this navigation.
|
|
* @property {string} url
|
|
* The requested or reached URL.
|
|
*/
|
|
|
|
/**
|
|
* Navigate the given context to the provided url, with the provided wait condition.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context to navigate.
|
|
* @param {string} options.url
|
|
* Url for the navigation.
|
|
* @param {WaitCondition=} options.wait
|
|
* Wait condition for the navigation, one of "none", "interactive", "complete".
|
|
*
|
|
* @returns {BrowsingContextNavigateResult}
|
|
* Navigation result.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context for context cannot be found.
|
|
*/
|
|
async navigate(options = {}) {
|
|
const { context: contextId, url, wait = WaitCondition.None } = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
lazy.assert.string(url, `Expected "url" to be string, got ${url}`);
|
|
|
|
const waitConditions = Object.values(WaitCondition);
|
|
if (!waitConditions.includes(wait)) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected "wait" to be one of ${waitConditions}, got ${wait}`
|
|
);
|
|
}
|
|
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
// webProgress will be stable even if the context navigates, retrieve it
|
|
// immediately before doing any asynchronous call.
|
|
const webProgress = context.webProgress;
|
|
|
|
const base = await this.messageHandler.handleCommand({
|
|
moduleName: "browsingContext",
|
|
commandName: "_getBaseURL",
|
|
destination: {
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
id: context.id,
|
|
},
|
|
retryOnAbort: true,
|
|
});
|
|
|
|
let targetURI;
|
|
try {
|
|
const baseURI = Services.io.newURI(base);
|
|
targetURI = Services.io.newURI(url, null, baseURI);
|
|
} catch (e) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected "url" to be a valid URL (${e.message})`
|
|
);
|
|
}
|
|
|
|
return this.#awaitNavigation(
|
|
webProgress,
|
|
() => {
|
|
context.loadURI(targetURI, {
|
|
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
|
|
triggeringPrincipal:
|
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
|
hasValidUserGestureActivation: true,
|
|
});
|
|
},
|
|
{
|
|
wait,
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* An object that holds the information about margins
|
|
* for Webdriver BiDi browsingContext.print command.
|
|
*
|
|
* @typedef BrowsingContextPrintMarginParameters
|
|
*
|
|
* @property {number=} bottom
|
|
* Bottom margin in cm. Defaults to 1cm (~0.4 inches).
|
|
* @property {number=} left
|
|
* Left margin in cm. Defaults to 1cm (~0.4 inches).
|
|
* @property {number=} right
|
|
* Right margin in cm. Defaults to 1cm (~0.4 inches).
|
|
* @property {number=} top
|
|
* Top margin in cm. Defaults to 1cm (~0.4 inches).
|
|
*/
|
|
|
|
/**
|
|
* An object that holds the information about paper size
|
|
* for Webdriver BiDi browsingContext.print command.
|
|
*
|
|
* @typedef BrowsingContextPrintPageParameters
|
|
*
|
|
* @property {number=} height
|
|
* Paper height in cm. Defaults to US letter height (27.94cm / 11 inches).
|
|
* @property {number=} width
|
|
* Paper width in cm. Defaults to US letter width (21.59cm / 8.5 inches).
|
|
*/
|
|
|
|
/**
|
|
* Used as return value for Webdriver BiDi browsingContext.print command.
|
|
*
|
|
* @typedef BrowsingContextPrintResult
|
|
*
|
|
* @property {string} data
|
|
* Base64 encoded PDF representing printed document.
|
|
*/
|
|
|
|
/**
|
|
* Creates a paginated PDF representation of a document
|
|
* of the provided browsing context, and returns it
|
|
* as a Base64-encoded string.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context.
|
|
* @param {boolean=} options.background
|
|
* Whether or not to print background colors and images.
|
|
* Defaults to false, which prints without background graphics.
|
|
* @param {BrowsingContextPrintMarginParameters=} options.margin
|
|
* Paper margins.
|
|
* @param {('landscape'|'portrait')=} options.orientation
|
|
* Paper orientation. Defaults to 'portrait'.
|
|
* @param {BrowsingContextPrintPageParameters=} options.page
|
|
* Paper size.
|
|
* @param {Array<number|string>=} options.pageRanges
|
|
* Paper ranges to print, e.g., ['1-5', 8, '11-13'].
|
|
* Defaults to the empty array, which means print all pages.
|
|
* @param {number=} options.scale
|
|
* Scale of the webpage rendering. Defaults to 1.0.
|
|
* @param {boolean=} options.shrinkToFit
|
|
* Whether or not to override page size as defined by CSS.
|
|
* Defaults to true, in which case the content will be scaled
|
|
* to fit the paper size.
|
|
*
|
|
* @returns {BrowsingContextPrintResult}
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
*/
|
|
async print(options = {}) {
|
|
const {
|
|
context: contextId,
|
|
background,
|
|
margin,
|
|
orientation,
|
|
page,
|
|
pageRanges,
|
|
scale,
|
|
shrinkToFit,
|
|
} = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
const settings = lazy.print.addDefaultSettings({
|
|
background,
|
|
margin,
|
|
orientation,
|
|
page,
|
|
pageRanges,
|
|
scale,
|
|
shrinkToFit,
|
|
});
|
|
|
|
for (const prop of ["top", "bottom", "left", "right"]) {
|
|
lazy.assert.positiveNumber(
|
|
settings.margin[prop],
|
|
lazy.pprint`margin.${prop} is not a positive number`
|
|
);
|
|
}
|
|
for (const prop of ["width", "height"]) {
|
|
lazy.assert.positiveNumber(
|
|
settings.page[prop],
|
|
lazy.pprint`page.${prop} is not a positive number`
|
|
);
|
|
}
|
|
lazy.assert.positiveNumber(
|
|
settings.scale,
|
|
`scale ${settings.scale} is not a positive number`
|
|
);
|
|
lazy.assert.that(
|
|
scale =>
|
|
scale >= lazy.print.minScaleValue && scale <= lazy.print.maxScaleValue,
|
|
`scale ${settings.scale} is outside the range ${lazy.print.minScaleValue}-${lazy.print.maxScaleValue}`
|
|
)(settings.scale);
|
|
lazy.assert.boolean(settings.shrinkToFit);
|
|
lazy.assert.that(
|
|
orientation => lazy.print.defaults.orientationValue.includes(orientation),
|
|
`orientation ${
|
|
settings.orientation
|
|
} doesn't match allowed values "${lazy.print.defaults.orientationValue.join(
|
|
"/"
|
|
)}"`
|
|
)(settings.orientation);
|
|
lazy.assert.boolean(
|
|
settings.background,
|
|
`background ${settings.background} is not boolean`
|
|
);
|
|
lazy.assert.array(settings.pageRanges);
|
|
|
|
const printSettings = await lazy.print.getPrintSettings(settings);
|
|
const binaryString = await lazy.print.printToBinaryString(
|
|
context,
|
|
printSettings
|
|
);
|
|
|
|
return {
|
|
data: btoa(binaryString),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reload the given context's document, with the provided wait condition.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context to navigate.
|
|
* @param {bool=} options.ignoreCache
|
|
* If true ignore the browser cache. [Not yet supported]
|
|
* @param {WaitCondition=} options.wait
|
|
* Wait condition for the navigation, one of "none", "interactive", "complete".
|
|
*
|
|
* @returns {BrowsingContextNavigateResult}
|
|
* Navigation result.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context for context cannot be found.
|
|
*/
|
|
async reload(options = {}) {
|
|
const {
|
|
context: contextId,
|
|
ignoreCache,
|
|
wait = WaitCondition.None,
|
|
} = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
if (typeof ignoreCache != "undefined") {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`Argument "ignoreCache" is not supported yet.`
|
|
);
|
|
}
|
|
|
|
const waitConditions = Object.values(WaitCondition);
|
|
if (!waitConditions.includes(wait)) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Expected "wait" to be one of ${waitConditions}, got ${wait}`
|
|
);
|
|
}
|
|
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
// webProgress will be stable even if the context navigates, retrieve it
|
|
// immediately before doing any asynchronous call.
|
|
const webProgress = context.webProgress;
|
|
|
|
return this.#awaitNavigation(
|
|
webProgress,
|
|
() => {
|
|
context.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
|
|
},
|
|
{ wait }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set the top-level browsing context's viewport to a given dimension.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context.
|
|
* @param {Viewport|null} options.viewport
|
|
* Dimensions to set the viewport to, or `null` to reset it
|
|
* to the original dimensions.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {UnsupportedOperationError}
|
|
* Raised when the command is called on Android.
|
|
*/
|
|
async setViewport(options = {}) {
|
|
const { context: contextId, viewport } = options;
|
|
|
|
if (lazy.AppInfo.isAndroid) {
|
|
// Bug 1840084: Add Android support for modifying the viewport.
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`Command not yet supported for ${lazy.AppInfo.name}`
|
|
);
|
|
}
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
const context = this.#getBrowsingContext(contextId);
|
|
if (context.parent) {
|
|
throw new lazy.error.InvalidArgumentError(
|
|
`Browsing Context with id ${contextId} is not top-level`
|
|
);
|
|
}
|
|
|
|
const browser = context.embedderElement;
|
|
const currentHeight = browser.clientHeight;
|
|
const currentWidth = browser.clientWidth;
|
|
|
|
let targetHeight, targetWidth;
|
|
if (viewport === undefined) {
|
|
// Don't modify the viewport's size.
|
|
targetHeight = currentHeight;
|
|
targetWidth = currentWidth;
|
|
} else if (viewport === null) {
|
|
// Reset viewport to the original dimensions.
|
|
targetHeight = browser.parentElement.clientHeight;
|
|
targetWidth = browser.parentElement.clientWidth;
|
|
|
|
browser.style.removeProperty("height");
|
|
browser.style.removeProperty("width");
|
|
} else {
|
|
lazy.assert.object(
|
|
viewport,
|
|
`Expected "viewport" to be an object, got ${viewport}`
|
|
);
|
|
|
|
const { height, width } = viewport;
|
|
targetHeight = lazy.assert.positiveInteger(
|
|
height,
|
|
`Expected viewport's "height" to be a positive integer, got ${height}`
|
|
);
|
|
targetWidth = lazy.assert.positiveInteger(
|
|
width,
|
|
`Expected viewport's "width" to be a positive integer, got ${width}`
|
|
);
|
|
|
|
if (targetHeight > MAX_WINDOW_SIZE || targetWidth > MAX_WINDOW_SIZE) {
|
|
throw new lazy.error.UnsupportedOperationError(
|
|
`"width" or "height" cannot be larger than ${MAX_WINDOW_SIZE} px`
|
|
);
|
|
}
|
|
|
|
browser.style.setProperty("height", targetHeight + "px");
|
|
browser.style.setProperty("width", targetWidth + "px");
|
|
}
|
|
|
|
if (targetHeight !== currentHeight || targetWidth !== currentWidth) {
|
|
// Wait until the viewport has been resized
|
|
await this.messageHandler.forwardCommand({
|
|
moduleName: "browsingContext",
|
|
commandName: "_awaitViewportDimensions",
|
|
destination: {
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
id: context.id,
|
|
},
|
|
params: {
|
|
height: targetHeight,
|
|
width: targetWidth,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Traverses the history of a given context by a given delta.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.context
|
|
* Id of the browsing context.
|
|
* @param {number} options.delta
|
|
* The number of steps we have to traverse.
|
|
*
|
|
* @throws {InvalidArgumentError}
|
|
* Raised if an argument is of an invalid type or value.
|
|
* @throws {NoSuchFrameException}
|
|
* When a context is not available.
|
|
* @throws {NoSuchHistoryEntryError}
|
|
* When a requested history entry does not exist.
|
|
*/
|
|
async traverseHistory(options = {}) {
|
|
const { context: contextId, delta } = options;
|
|
|
|
lazy.assert.string(
|
|
contextId,
|
|
`Expected "context" to be a string, got ${contextId}`
|
|
);
|
|
|
|
const context = this.#getBrowsingContext(contextId);
|
|
|
|
lazy.assert.integer(
|
|
delta,
|
|
`Expected "delta" to be an integer, got ${delta}`
|
|
);
|
|
|
|
const sessionHistory = context.sessionHistory;
|
|
const allSteps = sessionHistory.count;
|
|
const currentIndex = sessionHistory.index;
|
|
const targetIndex = currentIndex + delta;
|
|
const validEntry = targetIndex >= 0 && targetIndex < allSteps;
|
|
|
|
if (!validEntry) {
|
|
throw new lazy.error.NoSuchHistoryEntryError(
|
|
`History entry with delta ${delta} not found`
|
|
);
|
|
}
|
|
|
|
context.goToIndex(targetIndex);
|
|
|
|
// On some platforms the requested index isn't set immediately.
|
|
await lazy.PollPromise(
|
|
(resolve, reject) => {
|
|
if (sessionHistory.index == targetIndex) {
|
|
resolve();
|
|
} else {
|
|
reject();
|
|
}
|
|
},
|
|
{
|
|
errorMessage: `History was not updated for index "${targetIndex}"`,
|
|
timeout: TIMEOUT_SET_HISTORY_INDEX * lazy.getTimeoutMultiplier(),
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Start and await a navigation on the provided BrowsingContext. Returns a
|
|
* promise which resolves when the navigation is done according to the provided
|
|
* navigation strategy.
|
|
*
|
|
* @param {WebProgress} webProgress
|
|
* The WebProgress instance to observe for this navigation.
|
|
* @param {Function} startNavigationFn
|
|
* A callback that starts a navigation.
|
|
* @param {object} options
|
|
* @param {WaitCondition} options.wait
|
|
* The WaitCondition to use to wait for the navigation.
|
|
*
|
|
* @returns {Promise<BrowsingContextNavigateResult>}
|
|
* A Promise that resolves to navigate results when the navigation is done.
|
|
*/
|
|
async #awaitNavigation(webProgress, startNavigationFn, options) {
|
|
const { wait } = options;
|
|
|
|
const context = webProgress.browsingContext;
|
|
const browserId = context.browserId;
|
|
|
|
const resolveWhenStarted = wait === WaitCondition.None;
|
|
const listener = new lazy.ProgressListener(webProgress, {
|
|
expectNavigation: true,
|
|
resolveWhenStarted,
|
|
// In case the webprogress is already navigating, always wait for an
|
|
// explicit start flag.
|
|
waitForExplicitStart: true,
|
|
});
|
|
|
|
const onDocumentInteractive = (evtName, wrappedEvt) => {
|
|
if (webProgress.browsingContext.id !== wrappedEvt.contextId) {
|
|
// Ignore load events for unrelated browsing contexts.
|
|
return;
|
|
}
|
|
|
|
if (wrappedEvt.readyState === "interactive") {
|
|
listener.stopIfStarted();
|
|
}
|
|
};
|
|
|
|
const contextDescriptor = {
|
|
type: lazy.ContextDescriptorType.TopBrowsingContext,
|
|
id: browserId,
|
|
};
|
|
|
|
// For the Interactive wait condition, resolve as soon as
|
|
// the document becomes interactive.
|
|
if (wait === WaitCondition.Interactive) {
|
|
await this.messageHandler.eventsDispatcher.on(
|
|
"browsingContext._documentInteractive",
|
|
contextDescriptor,
|
|
onDocumentInteractive
|
|
);
|
|
}
|
|
|
|
// If WaitCondition is Complete, we should try to wait for the corresponding
|
|
// responseCompleted event to be received.
|
|
let onNavigationRequestCompleted;
|
|
|
|
// However, a navigation will not necessarily have network events.
|
|
// For instance: same document navigation, or when using file or data
|
|
// protocols (for which we don't have network events yet).
|
|
// Therefore we will not unconditionally wait for a navigation request and
|
|
// this flag should only be set when a responseCompleted event should be
|
|
// expected.
|
|
let shouldWaitForNavigationRequest = false;
|
|
|
|
// Cleaning up the listeners will be done at the end of this method.
|
|
let unsubscribeNavigationListeners;
|
|
|
|
if (wait === WaitCondition.Complete) {
|
|
let resolveOnNetworkEvent;
|
|
onNavigationRequestCompleted = new Promise(
|
|
r => (resolveOnNetworkEvent = r)
|
|
);
|
|
const onBeforeRequestSent = (name, data) => {
|
|
if (data.navigation) {
|
|
shouldWaitForNavigationRequest = true;
|
|
}
|
|
};
|
|
const onNetworkRequestCompleted = (name, data) => {
|
|
if (data.navigation) {
|
|
resolveOnNetworkEvent();
|
|
}
|
|
};
|
|
|
|
// The network request can either end with _responseCompleted or _fetchError
|
|
await this.messageHandler.eventsDispatcher.on(
|
|
"network._beforeRequestSent",
|
|
contextDescriptor,
|
|
onBeforeRequestSent
|
|
);
|
|
await this.messageHandler.eventsDispatcher.on(
|
|
"network._responseCompleted",
|
|
contextDescriptor,
|
|
onNetworkRequestCompleted
|
|
);
|
|
await this.messageHandler.eventsDispatcher.on(
|
|
"network._fetchError",
|
|
contextDescriptor,
|
|
onNetworkRequestCompleted
|
|
);
|
|
|
|
unsubscribeNavigationListeners = async () => {
|
|
await this.messageHandler.eventsDispatcher.off(
|
|
"network._beforeRequestSent",
|
|
contextDescriptor,
|
|
onBeforeRequestSent
|
|
);
|
|
await this.messageHandler.eventsDispatcher.off(
|
|
"network._responseCompleted",
|
|
contextDescriptor,
|
|
onNetworkRequestCompleted
|
|
);
|
|
await this.messageHandler.eventsDispatcher.off(
|
|
"network._fetchError",
|
|
contextDescriptor,
|
|
onNetworkRequestCompleted
|
|
);
|
|
};
|
|
}
|
|
|
|
const navigated = listener.start();
|
|
|
|
try {
|
|
const navigationId = lazy.registerNavigationId({
|
|
contextDetails: { context: webProgress.browsingContext },
|
|
});
|
|
|
|
await startNavigationFn();
|
|
await navigated;
|
|
|
|
if (shouldWaitForNavigationRequest) {
|
|
await onNavigationRequestCompleted;
|
|
}
|
|
|
|
let url;
|
|
if (wait === WaitCondition.None) {
|
|
// If wait condition is None, the navigation resolved before the current
|
|
// context has navigated.
|
|
url = listener.targetURI.spec;
|
|
} else {
|
|
url = listener.currentURI.spec;
|
|
}
|
|
|
|
return {
|
|
navigation: navigationId,
|
|
url,
|
|
};
|
|
} finally {
|
|
if (listener.isStarted) {
|
|
listener.stop();
|
|
}
|
|
|
|
if (wait === WaitCondition.Interactive) {
|
|
await this.messageHandler.eventsDispatcher.off(
|
|
"browsingContext._documentInteractive",
|
|
contextDescriptor,
|
|
onDocumentInteractive
|
|
);
|
|
} else if (
|
|
wait === WaitCondition.Complete &&
|
|
shouldWaitForNavigationRequest
|
|
) {
|
|
await unsubscribeNavigationListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves a browsing context based on its id.
|
|
*
|
|
* @param {number} contextId
|
|
* Id of the browsing context.
|
|
* @returns {BrowsingContext=}
|
|
* The browsing context or null if <var>contextId</var> is null.
|
|
* @throws {NoSuchFrameError}
|
|
* If the browsing context cannot be found.
|
|
*/
|
|
#getBrowsingContext(contextId) {
|
|
// The WebDriver BiDi specification expects null to be
|
|
// returned if no browsing context id has been specified.
|
|
if (contextId === null) {
|
|
return null;
|
|
}
|
|
|
|
const context = lazy.TabManager.getBrowsingContextById(contextId);
|
|
if (context === null) {
|
|
throw new lazy.error.NoSuchFrameError(
|
|
`Browsing Context with id ${contextId} not found`
|
|
);
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Get the WebDriver BiDi browsing context information.
|
|
*
|
|
* @param {BrowsingContext} context
|
|
* The browsing context to get the information from.
|
|
* @param {object=} options
|
|
* @param {boolean=} options.isRoot
|
|
* Flag that indicates if this browsing context is the root of all the
|
|
* browsing contexts to be returned. Defaults to true.
|
|
* @param {number=} options.maxDepth
|
|
* Depth of the browsing context tree to traverse. If not specified
|
|
* the whole tree is returned.
|
|
* @returns {BrowsingContextInfo}
|
|
* The information about the browsing context.
|
|
*/
|
|
#getBrowsingContextInfo(context, options = {}) {
|
|
const { isRoot = true, maxDepth = null } = options;
|
|
|
|
let children = null;
|
|
if (maxDepth === null || maxDepth > 0) {
|
|
children = context.children.map(context =>
|
|
this.#getBrowsingContextInfo(context, {
|
|
maxDepth: maxDepth === null ? maxDepth : maxDepth - 1,
|
|
isRoot: false,
|
|
})
|
|
);
|
|
}
|
|
|
|
const userContext = lazy.UserContextManager.getIdByBrowsingContext(context);
|
|
const contextInfo = {
|
|
children,
|
|
context: lazy.TabManager.getIdForBrowsingContext(context),
|
|
url: context.currentURI.spec,
|
|
userContext,
|
|
};
|
|
|
|
if (isRoot) {
|
|
// Only emit the parent id for the top-most browsing context.
|
|
const parentId = lazy.TabManager.getIdForBrowsingContext(context.parent);
|
|
contextInfo.parent = parentId;
|
|
}
|
|
|
|
return contextInfo;
|
|
}
|
|
|
|
#onContextAttached = async (eventName, data = {}) => {
|
|
if (this.#subscribedEvents.has("browsingContext.contextCreated")) {
|
|
const { browsingContext, why } = data;
|
|
|
|
// Filter out top-level browsing contexts that are created because of a
|
|
// cross-group navigation.
|
|
if (why === "replace") {
|
|
return;
|
|
}
|
|
|
|
// TODO: Bug 1852941. We should also filter out events which are emitted
|
|
// for DevTools frames.
|
|
|
|
// Filter out notifications for chrome context until support gets
|
|
// added (bug 1722679).
|
|
if (!browsingContext.webProgress) {
|
|
return;
|
|
}
|
|
|
|
const browsingContextInfo = this.#getBrowsingContextInfo(
|
|
browsingContext,
|
|
{
|
|
maxDepth: 0,
|
|
}
|
|
);
|
|
|
|
// This event is emitted from the parent process but for a given browsing
|
|
// context. Set the event's contextInfo to the message handler corresponding
|
|
// to this browsing context.
|
|
const contextInfo = {
|
|
contextId: browsingContext.id,
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
};
|
|
this.emitEvent(
|
|
"browsingContext.contextCreated",
|
|
browsingContextInfo,
|
|
contextInfo
|
|
);
|
|
}
|
|
};
|
|
|
|
#onContextDiscarded = async (eventName, data = {}) => {
|
|
if (this.#subscribedEvents.has("browsingContext.contextDestroyed")) {
|
|
const { browsingContext, why } = data;
|
|
|
|
// Filter out top-level browsing contexts that are destroyed because of a
|
|
// cross-group navigation.
|
|
if (why === "replace") {
|
|
return;
|
|
}
|
|
|
|
// TODO: Bug 1852941. We should also filter out events which are emitted
|
|
// for DevTools frames.
|
|
|
|
// Filter out notifications for chrome context until support gets
|
|
// added (bug 1722679).
|
|
if (!browsingContext.webProgress) {
|
|
return;
|
|
}
|
|
|
|
// If this event is for a child context whose top or parent context is also destroyed,
|
|
// we don't need to send it, in this case the event for the top/parent context is enough.
|
|
if (
|
|
browsingContext.parent &&
|
|
(browsingContext.top.isDiscarded || browsingContext.parent.isDiscarded)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const browsingContextInfo = this.#getBrowsingContextInfo(
|
|
browsingContext,
|
|
{
|
|
maxDepth: 0,
|
|
}
|
|
);
|
|
|
|
// This event is emitted from the parent process but for a given browsing
|
|
// context. Set the event's contextInfo to the message handler corresponding
|
|
// to this browsing context.
|
|
const contextInfo = {
|
|
contextId: browsingContext.id,
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
};
|
|
this.emitEvent(
|
|
"browsingContext.contextDestroyed",
|
|
browsingContextInfo,
|
|
contextInfo
|
|
);
|
|
}
|
|
};
|
|
|
|
#onLocationChanged = async (eventName, data) => {
|
|
const { navigationId, navigableId, url } = data;
|
|
const context = this.#getBrowsingContext(navigableId);
|
|
|
|
if (this.#subscribedEvents.has("browsingContext.fragmentNavigated")) {
|
|
const contextInfo = {
|
|
contextId: context.id,
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
};
|
|
this.emitEvent(
|
|
"browsingContext.fragmentNavigated",
|
|
{
|
|
context: navigableId,
|
|
navigation: navigationId,
|
|
timestamp: Date.now(),
|
|
url,
|
|
},
|
|
contextInfo
|
|
);
|
|
}
|
|
};
|
|
|
|
#onPromptClosed = async (eventName, data) => {
|
|
if (this.#subscribedEvents.has("browsingContext.userPromptClosed")) {
|
|
const { contentBrowser, detail } = data;
|
|
const contextId = lazy.TabManager.getIdForBrowser(contentBrowser);
|
|
|
|
if (contextId === null) {
|
|
return;
|
|
}
|
|
|
|
// This event is emitted from the parent process but for a given browsing
|
|
// context. Set the event's contextInfo to the message handler corresponding
|
|
// to this browsing context.
|
|
const contextInfo = {
|
|
contextId,
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
};
|
|
|
|
const params = {
|
|
context: contextId,
|
|
...detail,
|
|
};
|
|
|
|
this.emitEvent("browsingContext.userPromptClosed", params, contextInfo);
|
|
}
|
|
};
|
|
|
|
#onPromptOpened = async (eventName, data) => {
|
|
if (this.#subscribedEvents.has("browsingContext.userPromptOpened")) {
|
|
const { contentBrowser, prompt } = data;
|
|
|
|
// Do not send opened event for unsupported prompt types.
|
|
if (!(prompt.promptType in UserPromptType)) {
|
|
return;
|
|
}
|
|
|
|
const contextId = lazy.TabManager.getIdForBrowser(contentBrowser);
|
|
// This event is emitted from the parent process but for a given browsing
|
|
// context. Set the event's contextInfo to the message handler corresponding
|
|
// to this browsing context.
|
|
const contextInfo = {
|
|
contextId,
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
};
|
|
|
|
const eventPayload = {
|
|
context: contextId,
|
|
type: prompt.promptType,
|
|
message: await prompt.getText(),
|
|
};
|
|
|
|
// Bug 1859814: Since the platform doesn't provide the access to the `defaultValue` of the prompt,
|
|
// we use prompt the `value` instead. The `value` is set to `defaultValue` when `defaultValue` is provided.
|
|
// This approach doesn't allow us to distinguish between the `defaultValue` being set to an empty string and
|
|
// `defaultValue` not set, because `value` is always defaulted to an empty string.
|
|
// We should switch to using the actual `defaultValue` when it's available and check for the `null` here.
|
|
const defaultValue = await prompt.getInputText();
|
|
if (defaultValue) {
|
|
eventPayload.defaultValue = defaultValue;
|
|
}
|
|
|
|
this.emitEvent(
|
|
"browsingContext.userPromptOpened",
|
|
eventPayload,
|
|
contextInfo
|
|
);
|
|
}
|
|
};
|
|
|
|
#onNavigationStarted = async (eventName, data) => {
|
|
const { navigableId, navigationId, url } = data;
|
|
const context = this.#getBrowsingContext(navigableId);
|
|
|
|
if (this.#subscribedEvents.has("browsingContext.navigationStarted")) {
|
|
const contextInfo = {
|
|
contextId: context.id,
|
|
type: lazy.WindowGlobalMessageHandler.type,
|
|
};
|
|
|
|
this.emitEvent(
|
|
"browsingContext.navigationStarted",
|
|
{
|
|
context: navigableId,
|
|
navigation: navigationId,
|
|
timestamp: Date.now(),
|
|
url,
|
|
},
|
|
contextInfo
|
|
);
|
|
}
|
|
};
|
|
|
|
#onPageHideEvent = (name, eventPayload) => {
|
|
const { context } = eventPayload;
|
|
if (context.parent) {
|
|
this.#onContextDiscarded("windowglobal-pagehide", {
|
|
browsingContext: context,
|
|
});
|
|
}
|
|
};
|
|
|
|
#stopListeningToContextEvent(event) {
|
|
this.#subscribedEvents.delete(event);
|
|
|
|
const hasContextEvent =
|
|
this.#subscribedEvents.has("browsingContext.contextCreated") ||
|
|
this.#subscribedEvents.has("browsingContext.contextDestroyed");
|
|
|
|
if (!hasContextEvent) {
|
|
this.#contextListener.stopListening();
|
|
}
|
|
}
|
|
|
|
#stopListeningToNavigationEvent(event) {
|
|
this.#subscribedEvents.delete(event);
|
|
|
|
const hasNavigationEvent =
|
|
this.#subscribedEvents.has("browsingContext.fragmentNavigated") ||
|
|
this.#subscribedEvents.has("browsingContext.navigationStarted");
|
|
|
|
if (!hasNavigationEvent) {
|
|
this.#navigationListener.stopListening();
|
|
}
|
|
}
|
|
|
|
#stopListeningToPromptEvent(event) {
|
|
this.#subscribedEvents.delete(event);
|
|
|
|
const hasPromptEvent =
|
|
this.#subscribedEvents.has("browsingContext.userPromptClosed") ||
|
|
this.#subscribedEvents.has("browsingContext.userPromptOpened");
|
|
|
|
if (!hasPromptEvent) {
|
|
this.#promptListener.stopListening();
|
|
}
|
|
}
|
|
|
|
#subscribeEvent(event) {
|
|
switch (event) {
|
|
case "browsingContext.contextCreated":
|
|
case "browsingContext.contextDestroyed": {
|
|
this.#contextListener.startListening();
|
|
this.#subscribedEvents.add(event);
|
|
break;
|
|
}
|
|
case "browsingContext.fragmentNavigated":
|
|
case "browsingContext.navigationStarted": {
|
|
this.#navigationListener.startListening();
|
|
this.#subscribedEvents.add(event);
|
|
break;
|
|
}
|
|
case "browsingContext.userPromptClosed":
|
|
case "browsingContext.userPromptOpened": {
|
|
this.#promptListener.startListening();
|
|
this.#subscribedEvents.add(event);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#unsubscribeEvent(event) {
|
|
switch (event) {
|
|
case "browsingContext.contextCreated":
|
|
case "browsingContext.contextDestroyed": {
|
|
this.#stopListeningToContextEvent(event);
|
|
break;
|
|
}
|
|
case "browsingContext.fragmentNavigated":
|
|
case "browsingContext.navigationStarted": {
|
|
this.#stopListeningToNavigationEvent(event);
|
|
break;
|
|
}
|
|
case "browsingContext.userPromptClosed":
|
|
case "browsingContext.userPromptOpened": {
|
|
this.#stopListeningToPromptEvent(event);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal commands
|
|
*/
|
|
|
|
_applySessionData(params) {
|
|
// TODO: Bug 1775231. Move this logic to a shared module or an abstract
|
|
// class.
|
|
const { category } = params;
|
|
if (category === "event") {
|
|
const filteredSessionData = params.sessionData.filter(item =>
|
|
this.messageHandler.matchesContext(item.contextDescriptor)
|
|
);
|
|
for (const event of this.#subscribedEvents.values()) {
|
|
const hasSessionItem = filteredSessionData.some(
|
|
item => item.value === event
|
|
);
|
|
// If there are no session items for this context, we should unsubscribe from the event.
|
|
if (!hasSessionItem) {
|
|
this.#unsubscribeEvent(event);
|
|
}
|
|
}
|
|
|
|
// Subscribe to all events, which have an item in SessionData.
|
|
for (const { value } of filteredSessionData) {
|
|
this.#subscribeEvent(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
static get supportedEvents() {
|
|
return [
|
|
"browsingContext.contextCreated",
|
|
"browsingContext.contextDestroyed",
|
|
"browsingContext.domContentLoaded",
|
|
"browsingContext.fragmentNavigated",
|
|
"browsingContext.load",
|
|
"browsingContext.navigationStarted",
|
|
"browsingContext.userPromptClosed",
|
|
"browsingContext.userPromptOpened",
|
|
];
|
|
}
|
|
}
|
|
|
|
export const browsingContext = BrowsingContextModule;
|