summaryrefslogtreecommitdiffstats
path: root/remote/cdp/domains/parent/Page.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--remote/cdp/domains/parent/Page.sys.mjs775
1 files changed, 775 insertions, 0 deletions
diff --git a/remote/cdp/domains/parent/Page.sys.mjs b/remote/cdp/domains/parent/Page.sys.mjs
new file mode 100644
index 0000000000..fd2a747721
--- /dev/null
+++ b/remote/cdp/domains/parent/Page.sys.mjs
@@ -0,0 +1,775 @@
+/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
+
+ DialogHandler:
+ "chrome://remote/content/cdp/domains/parent/page/DialogHandler.sys.mjs",
+ PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
+ streamRegistry: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ UnsupportedError: "chrome://remote/content/cdp/Error.sys.mjs",
+ windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+const MAX_CANVAS_DIMENSION = 32767;
+const MAX_CANVAS_AREA = 472907776;
+
+const PRINT_MAX_SCALE_VALUE = 2.0;
+const PRINT_MIN_SCALE_VALUE = 0.1;
+
+const PDF_TRANSFER_MODES = {
+ base64: "ReturnAsBase64",
+ stream: "ReturnAsStream",
+};
+
+const TIMEOUT_SET_HISTORY_INDEX = 1000;
+
+export class Page extends Domain {
+ constructor(session) {
+ super(session);
+
+ this._onDialogLoaded = this._onDialogLoaded.bind(this);
+ this._onRequest = this._onRequest.bind(this);
+
+ this.enabled = false;
+
+ this.session.networkObserver.startTrackingBrowserNetwork(
+ this.session.target.browser
+ );
+ this.session.networkObserver.on("request", this._onRequest);
+ }
+
+ destructor() {
+ // Flip a flag to avoid to disable the content domain from this.disable()
+ this._isDestroyed = false;
+ this.disable();
+
+ this.session.networkObserver.off("request", this._onRequest);
+ this.session.networkObserver.stopTrackingBrowserNetwork(
+ this.session.target.browser
+ );
+ super.destructor();
+ }
+
+ // commands
+
+ /**
+ * Navigates current page to given URL.
+ *
+ * @param {Object} options
+ * @param {string} options.url
+ * destination URL
+ * @param {string=} options.frameId
+ * frame id to navigate (not supported),
+ * if not specified navigate top frame
+ * @param {string=} options.referrer
+ * referred URL (optional)
+ * @param {string=} options.transitionType
+ * intended transition type
+ * @return {Object}
+ * - frameId {string} frame id that has navigated (or failed to)
+ * - errorText {string=} error message if navigation has failed
+ * - loaderId {string} (not supported)
+ */
+ async navigate(options = {}) {
+ const { url, frameId, referrer, transitionType } = options;
+ if (typeof url != "string") {
+ throw new TypeError("url: string value expected");
+ }
+ let validURL;
+ try {
+ validURL = Services.io.newURI(url);
+ } catch (e) {
+ throw new Error("Error: Cannot navigate to invalid URL");
+ }
+ const topFrameId = this.session.browsingContext.id.toString();
+ if (frameId && frameId != topFrameId) {
+ throw new lazy.UnsupportedError("frameId not supported");
+ }
+
+ const hitsNetwork = ["https", "http"].includes(validURL.scheme);
+ let networkLessLoaderId;
+ if (!hitsNetwork) {
+ // This navigation will not hit the network, use a randomly generated id.
+ const uuid = Services.uuid.generateUUID().toString();
+ networkLessLoaderId = uuid.substring(1, uuid.length - 1);
+
+ // Update the content process map of loader ids.
+ await this.executeInChild("_updateLoaderId", {
+ frameId: this.session.browsingContext.id,
+ loaderId: networkLessLoaderId,
+ });
+ }
+
+ const currentURI = this.session.browsingContext.currentURI;
+
+ const isSameDocumentNavigation =
+ // The "host", "query" and "ref" getters can throw if the URLs are not
+ // http/https, so verify first that both currentURI and validURL are
+ // using http/https.
+ hitsNetwork &&
+ ["https", "http"].includes(currentURI.scheme) &&
+ currentURI.host === validURL.host &&
+ currentURI.query === validURL.query &&
+ !!validURL.ref;
+
+ const requestDone = new Promise(resolve => {
+ if (isSameDocumentNavigation) {
+ // Per CDP documentation, same-document navigations should not emit any
+ // loader id (https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate)
+ resolve({});
+ return;
+ }
+
+ if (!hitsNetwork) {
+ // This navigation will not hit the network, use a randomly generated id.
+ resolve({ navigationRequestId: networkLessLoaderId });
+ return;
+ }
+ let navigationRequestId, redirectedRequestId;
+ const _onNavigationRequest = function(_type, _ch, data) {
+ const {
+ url: requestURL,
+ requestId,
+ redirectedFrom = null,
+ isNavigationRequest,
+ } = data;
+ if (!isNavigationRequest) {
+ return;
+ }
+ if (validURL.spec === requestURL) {
+ navigationRequestId = redirectedRequestId = requestId;
+ } else if (redirectedFrom === redirectedRequestId) {
+ redirectedRequestId = requestId;
+ }
+ };
+
+ const _onRequestFinished = function(_type, _ch, data) {
+ const { requestId, errorCode } = data;
+ if (
+ redirectedRequestId !== requestId ||
+ errorCode == "NS_BINDING_REDIRECTED"
+ ) {
+ // handle next request in redirection chain
+ return;
+ }
+ this.session.networkObserver.off("request", _onNavigationRequest);
+ this.session.networkObserver.off("requestfinished", _onRequestFinished);
+ resolve({ errorCode, navigationRequestId });
+ }.bind(this);
+
+ this.session.networkObserver.on("request", _onNavigationRequest);
+ this.session.networkObserver.on("requestfinished", _onRequestFinished);
+ });
+
+ const opts = {
+ loadFlags: transitionToLoadFlag(transitionType),
+ referrerURI: referrer,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ this.session.browsingContext.loadURI(url, opts);
+ // clients expect loaderId == requestId for a document navigation request
+ const { navigationRequestId: loaderId, errorCode } = await requestDone;
+ const result = {
+ frameId: topFrameId,
+ loaderId,
+ };
+ if (errorCode) {
+ result.errorText = errorCode;
+ }
+ return result;
+ }
+
+ /**
+ * Capture page screenshot.
+ *
+ * @param {Object} options
+ * @param {Viewport=} options.clip
+ * Capture the screenshot of a given region only.
+ * @param {string=} options.format
+ * Image compression format. Defaults to "png".
+ * @param {number=} options.quality
+ * Compression quality from range [0..100] (jpeg only). Defaults to 80.
+ *
+ * @return {string}
+ * Base64-encoded image data.
+ */
+ async captureScreenshot(options = {}) {
+ const { clip, format = "png", quality = 80 } = options;
+
+ if (options.fromSurface) {
+ throw new lazy.UnsupportedError("fromSurface not supported");
+ }
+
+ let rect;
+ let scale = await this.executeInChild("_devicePixelRatio");
+
+ if (clip) {
+ for (const prop of ["x", "y", "width", "height", "scale"]) {
+ if (clip[prop] == undefined) {
+ throw new TypeError(`clip.${prop}: double value expected`);
+ }
+ }
+
+ const contentRect = await this.executeInChild("_contentRect");
+
+ // For invalid scale values default to full page
+ if (clip.scale <= 0) {
+ Object.assign(clip, {
+ x: 0,
+ y: 0,
+ width: contentRect.width,
+ height: contentRect.height,
+ scale: 1,
+ });
+ } else {
+ if (clip.x < 0 || clip.x > contentRect.width - 1) {
+ clip.x = 0;
+ }
+ if (clip.y < 0 || clip.y > contentRect.height - 1) {
+ clip.y = 0;
+ }
+ if (clip.width <= 0) {
+ clip.width = contentRect.width;
+ }
+ if (clip.height <= 0) {
+ clip.height = contentRect.height;
+ }
+ }
+
+ rect = new DOMRect(clip.x, clip.y, clip.width, clip.height);
+ scale *= clip.scale;
+ } else {
+ // If no specific clipping region has been specified,
+ // fallback to the layout (fixed) viewport, and the
+ // default pixel ratio.
+ const {
+ pageX,
+ pageY,
+ clientWidth,
+ clientHeight,
+ } = await this.executeInChild("_layoutViewport");
+
+ rect = new DOMRect(pageX, pageY, clientWidth, clientHeight);
+ }
+
+ let canvasWidth = rect.width * scale;
+ let canvasHeight = rect.height * scale;
+
+ // Cap the screenshot size based on maximum allowed canvas sizes.
+ // Using higher dimensions would trigger exceptions in Gecko.
+ //
+ // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#Maximum_canvas_size
+ if (canvasWidth > MAX_CANVAS_DIMENSION) {
+ rect.width = Math.floor(MAX_CANVAS_DIMENSION / scale);
+ canvasWidth = rect.width * scale;
+ }
+ if (canvasHeight > MAX_CANVAS_DIMENSION) {
+ rect.height = Math.floor(MAX_CANVAS_DIMENSION / scale);
+ canvasHeight = rect.height * scale;
+ }
+ // If the area is larger, reduce the height to keep the full width.
+ if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) {
+ rect.height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale));
+ canvasHeight = rect.height * scale;
+ }
+
+ const { browsingContext, window } = this.session.target;
+ const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ scale,
+ "rgb(255,255,255)"
+ );
+
+ const canvas = window.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(snapshot, 0, 0);
+
+ // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
+ // of the bitmap will exist in memory. Force the removal of the snapshot
+ // because it is no longer needed.
+ snapshot.close();
+
+ const url = canvas.toDataURL(`image/${format}`, quality / 100);
+ if (!url.startsWith(`data:image/${format}`)) {
+ throw new lazy.UnsupportedError(`Unsupported MIME type: image/${format}`);
+ }
+
+ // only return the base64 encoded data without the data URL prefix
+ const data = url.substring(url.indexOf(",") + 1);
+
+ return { data };
+ }
+
+ async enable() {
+ if (this.enabled) {
+ return;
+ }
+
+ this.enabled = true;
+
+ const { browser } = this.session.target;
+ this._dialogHandler = new lazy.DialogHandler(browser);
+ this._dialogHandler.on("dialog-loaded", this._onDialogLoaded);
+ await this.executeInChild("enable");
+ }
+
+ async disable() {
+ if (!this.enabled) {
+ return;
+ }
+
+ this._dialogHandler.destructor();
+ this._dialogHandler = null;
+ this.enabled = false;
+
+ if (!this._isDestroyed) {
+ // Only call disable in the content domain if we are not destroying the domain.
+ // If we are destroying the domain, the content domains will be destroyed
+ // independently after firing the remote:destroy event.
+ await this.executeInChild("disable");
+ }
+ }
+
+ async bringToFront() {
+ const { tab, window } = this.session.target;
+
+ // Focus the window, and select the corresponding tab
+ await lazy.windowManager.focusWindow(window);
+ await lazy.TabManager.selectTab(tab);
+ }
+
+ /**
+ * Return metrics relating to the layouting of the page.
+ *
+ * The returned object contains the following entries:
+ *
+ * layoutViewport:
+ * {number} pageX
+ * Horizontal offset relative to the document (CSS pixels)
+ * {number} pageY
+ * Vertical offset relative to the document (CSS pixels)
+ * {number} clientWidth
+ * Width (CSS pixels), excludes scrollbar if present
+ * {number} clientHeight
+ * Height (CSS pixels), excludes scrollbar if present
+ *
+ * visualViewport:
+ * {number} offsetX
+ * Horizontal offset relative to the layout viewport (CSS pixels)
+ * {number} offsetY
+ * Vertical offset relative to the layout viewport (CSS pixels)
+ * {number} pageX
+ * Horizontal offset relative to the document (CSS pixels)
+ * {number} pageY
+ * Vertical offset relative to the document (CSS pixels)
+ * {number} clientWidth
+ * Width (CSS pixels), excludes scrollbar if present
+ * {number} clientHeight
+ * Height (CSS pixels), excludes scrollbar if present
+ * {number} scale
+ * Scale relative to the ideal viewport (size at width=device-width)
+ * {number} zoom
+ * Page zoom factor (CSS to device independent pixels ratio)
+ *
+ * contentSize:
+ * {number} x
+ * X coordinate
+ * {number} y
+ * Y coordinate
+ * {number} width
+ * Width of scrollable area
+ * {number} height
+ * Height of scrollable area
+ *
+ * @return {Promise}
+ * @resolves {layoutViewport, visualViewport, contentSize}
+ */
+ async getLayoutMetrics() {
+ return {
+ layoutViewport: await this.executeInChild("_layoutViewport"),
+ contentSize: await this.executeInChild("_contentRect"),
+ };
+ }
+
+ /**
+ * Returns navigation history for the current page.
+ *
+ * @return {currentIndex:number, entries:Array<NavigationEntry>}
+ */
+ async getNavigationHistory() {
+ const { window } = this.session.target;
+
+ return new Promise(resolve => {
+ function updateSessionHistory(sessionHistory) {
+ const entries = sessionHistory.entries.map(entry => {
+ return {
+ id: entry.ID,
+ url: entry.url,
+ userTypedURL: entry.originalURI || entry.url,
+ title: entry.title,
+ // TODO: Bug 1609514
+ transitionType: null,
+ };
+ });
+
+ resolve({
+ currentIndex: sessionHistory.index,
+ entries,
+ });
+ }
+
+ lazy.SessionStore.getSessionHistory(
+ window.gBrowser.selectedTab,
+ updateSessionHistory
+ );
+ });
+ }
+
+ /**
+ * Interact with the currently opened JavaScript dialog (alert, confirm,
+ * prompt) for this page. This will always close the dialog, either accepting
+ * or rejecting it, with the optional prompt filled.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.accept
+ * for "confirm", "prompt", "beforeunload" dialogs true will accept
+ * the dialog, false will cancel it. For "alert" dialogs, true or
+ * false closes the dialog in the same way.
+ * @param {string=} options.promptText
+ * for "prompt" dialogs, used to fill the prompt input.
+ */
+ async handleJavaScriptDialog(options = {}) {
+ const { accept, promptText } = options;
+
+ if (!this.enabled) {
+ throw new Error("Page domain is not enabled");
+ }
+ await this._dialogHandler.handleJavaScriptDialog({ accept, promptText });
+ }
+
+ /**
+ * Navigates current page to the given history entry.
+ *
+ * @param {Object} options
+ * @param {number} options.entryId
+ * Unique id of the entry to navigate to.
+ */
+ async navigateToHistoryEntry(options = {}) {
+ const { entryId } = options;
+
+ const index = await this._getIndexForHistoryEntryId(entryId);
+
+ if (index == null) {
+ throw new Error("No entry with passed id");
+ }
+
+ const { window } = this.session.target;
+ window.gBrowser.gotoIndex(index);
+
+ // On some platforms the requested index isn't set immediately.
+ await lazy.PollPromise(
+ async (resolve, reject) => {
+ const currentIndex = await this._getCurrentHistoryIndex();
+ if (currentIndex == index) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ { timeout: TIMEOUT_SET_HISTORY_INDEX }
+ );
+ }
+
+ /**
+ * Print page as PDF.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.displayHeaderFooter
+ * Display header and footer. Defaults to false.
+ * @param {string=} options.footerTemplate (not supported)
+ * HTML template for the print footer.
+ * @param {string=} options.headerTemplate (not supported)
+ * HTML template for the print header. Should use the same format
+ * as the footerTemplate.
+ * @param {boolean=} options.ignoreInvalidPageRanges
+ * Whether to silently ignore invalid but successfully parsed page ranges,
+ * such as '3-2'. Defaults to false.
+ * @param {boolean=} options.landscape
+ * Paper orientation. Defaults to false.
+ * @param {number=} options.marginBottom
+ * Bottom margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {number=} options.marginLeft
+ * Left margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {number=} options.marginRight
+ * Right margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {number=} options.marginTop
+ * Top margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {string=} options.pageRanges (not supported)
+ * Paper ranges to print, e.g., '1-5, 8, 11-13'.
+ * Defaults to the empty string, which means print all pages.
+ * @param {number=} options.paperHeight
+ * Paper height in inches. Defaults to 11 inches.
+ * @param {number=} options.paperWidth
+ * Paper width in inches. Defaults to 8.5 inches.
+ * @param {boolean=} options.preferCSSPageSize
+ * Whether or not to prefer page size as defined by CSS.
+ * Defaults to false, in which case the content will be scaled
+ * to fit the paper size.
+ * @param {boolean=} options.printBackground
+ * Print background graphics. Defaults to false.
+ * @param {number=} options.scale
+ * Scale of the webpage rendering. Defaults to 1.
+ * @param {string=} options.transferMode
+ * Return as base64-encoded string (ReturnAsBase64),
+ * or stream (ReturnAsStream). Defaults to ReturnAsBase64.
+ *
+ * @return {Promise<{data:string, stream:string}>
+ * Based on the transferMode setting data is a base64-encoded string,
+ * or stream is a handle to a OS.File stream.
+ */
+ async printToPDF(options = {}) {
+ const {
+ displayHeaderFooter = false,
+ // Bug 1601570 - Implement templates for header and footer
+ // headerTemplate = "",
+ // footerTemplate = "",
+ landscape = false,
+ marginBottom = 0.39,
+ marginLeft = 0.39,
+ marginRight = 0.39,
+ marginTop = 0.39,
+ // Bug 1601571 - Implement handling of page ranges
+ // TODO: pageRanges = "",
+ // TODO: ignoreInvalidPageRanges = false,
+ paperHeight = 11.0,
+ paperWidth = 8.5,
+ preferCSSPageSize = false,
+ printBackground = false,
+ scale = 1.0,
+ transferMode = PDF_TRANSFER_MODES.base64,
+ } = options;
+
+ if (marginBottom < 0) {
+ throw new TypeError("marginBottom is negative");
+ }
+ if (marginLeft < 0) {
+ throw new TypeError("marginLeft is negative");
+ }
+ if (marginRight < 0) {
+ throw new TypeError("marginRight is negative");
+ }
+ if (marginTop < 0) {
+ throw new TypeError("marginTop is negative");
+ }
+ if (scale < PRINT_MIN_SCALE_VALUE || scale > PRINT_MAX_SCALE_VALUE) {
+ throw new TypeError("scale is outside [0.1 - 2] range");
+ }
+ if (paperHeight <= 0) {
+ throw new TypeError("paperHeight is zero or negative");
+ }
+ if (paperWidth <= 0) {
+ throw new TypeError("paperWidth is zero or negative");
+ }
+
+ // Create a unique filename for the temporary PDF file
+ const basePath = lazy.OS.Path.join(
+ lazy.OS.Constants.Path.tmpDir,
+ "remote-agent.pdf"
+ );
+ const { file, path: filePath } = await lazy.OS.File.openUnique(basePath);
+ await file.close();
+
+ const psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+
+ const printSettings = psService.createNewPrintSettings();
+ printSettings.isInitializedFromPrinter = true;
+ printSettings.isInitializedFromPrefs = true;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+ printSettings.printerName = "";
+ printSettings.printSilent = true;
+ printSettings.outputDestination =
+ Ci.nsIPrintSettings.kOutputDestinationFile;
+ printSettings.toFileName = filePath;
+
+ printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches;
+ printSettings.paperWidth = paperWidth;
+ printSettings.paperHeight = paperHeight;
+
+ printSettings.marginBottom = marginBottom;
+ printSettings.marginLeft = marginLeft;
+ printSettings.marginRight = marginRight;
+ printSettings.marginTop = marginTop;
+
+ printSettings.printBGColors = printBackground;
+ printSettings.printBGImages = printBackground;
+ printSettings.scaling = scale;
+ printSettings.shrinkToFit = preferCSSPageSize;
+
+ if (!displayHeaderFooter) {
+ printSettings.headerStrCenter = "";
+ printSettings.headerStrLeft = "";
+ printSettings.headerStrRight = "";
+ printSettings.footerStrCenter = "";
+ printSettings.footerStrLeft = "";
+ printSettings.footerStrRight = "";
+ }
+
+ if (landscape) {
+ printSettings.orientation = Ci.nsIPrintSettings.kLandscapeOrientation;
+ }
+
+ const { linkedBrowser } = this.session.target.tab;
+
+ await linkedBrowser.browsingContext.print(printSettings);
+
+ // Bug 1603739 - With e10s enabled the promise returned by print() resolves
+ // too early, which means the file hasn't been completely written.
+ await new Promise(resolve => {
+ const DELAY_CHECK_FILE_COMPLETELY_WRITTEN = 100;
+
+ let lastSize = 0;
+ const timerId = lazy.setInterval(async () => {
+ const fileInfo = await lazy.OS.File.stat(filePath);
+ if (lastSize > 0 && fileInfo.size == lastSize) {
+ lazy.clearInterval(timerId);
+ resolve();
+ }
+ lastSize = fileInfo.size;
+ }, DELAY_CHECK_FILE_COMPLETELY_WRITTEN);
+ });
+
+ const fp = await lazy.OS.File.open(filePath);
+
+ const retval = { data: null, stream: null };
+ if (transferMode == PDF_TRANSFER_MODES.stream) {
+ retval.stream = lazy.streamRegistry.add(fp);
+ } else {
+ // return all data as a base64 encoded string
+ let bytes;
+ try {
+ bytes = await fp.read();
+ } finally {
+ fp.close();
+ await lazy.OS.File.remove(filePath);
+ }
+
+ // Each UCS2 character has an upper byte of 0 and a lower byte matching
+ // the binary data
+ retval.data = btoa(String.fromCharCode.apply(null, bytes));
+ }
+
+ return retval;
+ }
+
+ /**
+ * Intercept file chooser requests and transfer control to protocol clients.
+ *
+ * When file chooser interception is enabled,
+ * the native file chooser dialog is not shown.
+ * Instead, a protocol event Page.fileChooserOpened is emitted.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.enabled
+ * Enabled state of file chooser interception.
+ */
+ setInterceptFileChooserDialog(options = {}) {}
+
+ _getCurrentHistoryIndex() {
+ const { window } = this.session.target;
+
+ return new Promise(resolve => {
+ lazy.SessionStore.getSessionHistory(
+ window.gBrowser.selectedTab,
+ history => {
+ resolve(history.index);
+ }
+ );
+ });
+ }
+
+ _getIndexForHistoryEntryId(id) {
+ const { window } = this.session.target;
+
+ return new Promise(resolve => {
+ function updateSessionHistory(sessionHistory) {
+ sessionHistory.entries.forEach((entry, index) => {
+ if (entry.ID == id) {
+ resolve(index);
+ }
+ });
+
+ resolve(null);
+ }
+
+ lazy.SessionStore.getSessionHistory(
+ window.gBrowser.selectedTab,
+ updateSessionHistory
+ );
+ });
+ }
+
+ /**
+ * Emit the proper CDP event javascriptDialogOpening when a javascript dialog
+ * opens for the current target.
+ */
+ _onDialogLoaded(e, data) {
+ const { message, type } = data;
+ // XXX: We rely on the tabmodal-dialog-loaded event (see DialogHandler.jsm)
+ // which is inconsistent with the name "javascriptDialogOpening".
+ // For correctness we should rely on an event fired _before_ the prompt is
+ // visible, such as DOMWillOpenModalDialog. However the payload of this
+ // event does not contain enough data to populate javascriptDialogOpening.
+ //
+ // Since the event is fired asynchronously, this should not have an impact
+ // on the actual tests relying on this API.
+ this.emit("Page.javascriptDialogOpening", { message, type });
+ }
+
+ /**
+ * Handles HTTP request to propagate loaderId to events emitted from
+ * content process
+ */
+ _onRequest(_type, _ch, data) {
+ if (!data.loaderId) {
+ return;
+ }
+ this.executeInChild("_updateLoaderId", {
+ loaderId: data.loaderId,
+ frameId: data.frameId,
+ });
+ }
+}
+
+function transitionToLoadFlag(transitionType) {
+ switch (transitionType) {
+ case "reload":
+ return Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH;
+ case "link":
+ default:
+ return Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK;
+ }
+}