browsingContext.sys.mjs (82883B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", 11 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 12 BrowsingContextListener: 13 "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs", 14 capture: "chrome://remote/content/shared/Capture.sys.mjs", 15 ContextDescriptorType: 16 "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", 17 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 18 EventPromise: "chrome://remote/content/shared/Sync.sys.mjs", 19 getTimeoutMultiplier: "chrome://remote/content/shared/AppInfo.sys.mjs", 20 getWebDriverSessionById: 21 "chrome://remote/content/shared/webdriver/Session.sys.mjs", 22 Log: "chrome://remote/content/shared/Log.sys.mjs", 23 modal: "chrome://remote/content/shared/Prompt.sys.mjs", 24 registerNavigationId: 25 "chrome://remote/content/shared/NavigationManager.sys.mjs", 26 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 27 NavigationListener: 28 "chrome://remote/content/shared/listeners/NavigationListener.sys.mjs", 29 PollPromise: "chrome://remote/content/shared/Sync.sys.mjs", 30 pprint: "chrome://remote/content/shared/Format.sys.mjs", 31 print: "chrome://remote/content/shared/PDF.sys.mjs", 32 ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs", 33 PromptListener: 34 "chrome://remote/content/shared/listeners/PromptListener.sys.mjs", 35 RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", 36 SessionDataMethod: 37 "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs", 38 setDefaultAndAssertSerializationOptions: 39 "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 40 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 41 UserContextManager: 42 "chrome://remote/content/shared/UserContextManager.sys.mjs", 43 waitForInitialNavigationCompleted: 44 "chrome://remote/content/shared/Navigate.sys.mjs", 45 WindowGlobalMessageHandler: 46 "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", 47 windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs", 48 }); 49 50 ChromeUtils.defineLazyGetter(lazy, "logger", () => 51 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI) 52 ); 53 54 // Maximal window dimension allowed when emulating a viewport. 55 const MAX_WINDOW_SIZE = 10000000; 56 57 /** 58 * @typedef {string} ClipRectangleType 59 */ 60 61 /** 62 * Enum of possible clip rectangle types supported by the 63 * browsingContext.captureScreenshot command. 64 * 65 * @readonly 66 * @enum {ClipRectangleType} 67 */ 68 export const ClipRectangleType = { 69 Box: "box", 70 Element: "element", 71 }; 72 73 /** 74 * @typedef {object} CreateType 75 */ 76 77 /** 78 * Enum of types supported by the browsingContext.create command. 79 * 80 * @readonly 81 * @enum {CreateType} 82 */ 83 const CreateType = { 84 tab: "tab", 85 window: "window", 86 }; 87 88 /** 89 * @typedef {object} DownloadEndStatus 90 */ 91 92 /** 93 * Enum of values for the status of the browsingContext.downloadEnd event. 94 * 95 * @readonly 96 * @enum {DownloadStatus} 97 */ 98 const DownloadEndStatus = { 99 canceled: "canceled", 100 complete: "complete", 101 }; 102 103 /** 104 * @typedef {string} LocatorType 105 */ 106 107 /** 108 * Enum of types supported by the browsingContext.locateNodes command. 109 * 110 * @readonly 111 * @enum {LocatorType} 112 */ 113 export const LocatorType = { 114 accessibility: "accessibility", 115 context: "context", 116 css: "css", 117 innerText: "innerText", 118 xpath: "xpath", 119 }; 120 121 /** 122 * @typedef {string} OriginType 123 */ 124 125 /** 126 * Enum of origin type supported by the 127 * browsingContext.captureScreenshot command. 128 * 129 * @readonly 130 * @enum {OriginType} 131 */ 132 export const OriginType = { 133 document: "document", 134 viewport: "viewport", 135 }; 136 137 const TIMEOUT_SET_HISTORY_INDEX = 1000; 138 const TIMEOUT_WAIT_FOR_VISIBILITY = 250; 139 140 /** 141 * Enum of user prompt types supported by the browsingContext.handleUserPrompt 142 * command, these types can be retrieved from `dialog.args.promptType`. 143 * 144 * @readonly 145 * @enum {UserPromptType} 146 */ 147 const UserPromptType = { 148 alert: "alert", 149 confirm: "confirm", 150 prompt: "prompt", 151 beforeunload: "beforeunload", 152 }; 153 154 /** 155 * An object that contains details of a viewport. 156 * 157 * @typedef {object} Viewport 158 * 159 * @property {number} height 160 * The height of the viewport. 161 * @property {number} width 162 * The width of the viewport. 163 */ 164 165 /** 166 * @typedef {string} WaitCondition 167 */ 168 169 /** 170 * Wait conditions supported by WebDriver BiDi for navigation. 171 * 172 * @enum {WaitCondition} 173 */ 174 const WaitCondition = { 175 None: "none", 176 Interactive: "interactive", 177 Complete: "complete", 178 }; 179 180 /** 181 * An enum that specifies the scope of a browsing context. 182 * 183 * @readonly 184 * @enum {string} 185 */ 186 export const MozContextScope = { 187 CHROME: "chrome", 188 CONTENT: "content", 189 }; 190 191 /** 192 * Used as an argument for browsingContext._updateNavigableViewport command 193 * to represent an object which holds viewport settings which should be applied. 194 * 195 * @typedef ViewportOverride 196 * 197 * @property {number|null} devicePixelRatio 198 * A value to override device pixel ratio, or `null` to reset it to 199 * the original value. 200 * @property {Viewport|null} viewport 201 * Dimensions to set the viewport to, or `null` to reset it 202 * to the original dimensions. 203 */ 204 205 class BrowsingContextModule extends RootBiDiModule { 206 #blockedCreateCommands; 207 #contextListener; 208 #navigationListener; 209 #promptListener; 210 #subscribedEvents; 211 212 /** 213 * Create a new module instance. 214 * 215 * @param {MessageHandler} messageHandler 216 * The MessageHandler instance which owns this Module instance. 217 */ 218 constructor(messageHandler) { 219 super(messageHandler); 220 221 this.#contextListener = new lazy.BrowsingContextListener(); 222 this.#contextListener.on("attached", this.#onContextAttached); 223 this.#contextListener.on("discarded", this.#onContextDiscarded); 224 225 this.#navigationListener = new lazy.NavigationListener( 226 this.messageHandler.navigationManager 227 ); 228 this.#navigationListener.on("download-end", this.#onDownloadEnd); 229 this.#navigationListener.on("download-started", this.#onDownloadStarted); 230 this.#navigationListener.on( 231 "fragment-navigated", 232 this.#onFragmentNavigated 233 ); 234 this.#navigationListener.on("history-updated", this.#onHistoryUpdated); 235 this.#navigationListener.on( 236 "navigation-committed", 237 this.#onNavigationCommitted 238 ); 239 this.#navigationListener.on("navigation-failed", this.#onNavigationFailed); 240 this.#navigationListener.on( 241 "navigation-started", 242 this.#onNavigationStarted 243 ); 244 245 // Create the prompt listener and listen to "closed" and "opened" events. 246 this.#promptListener = new lazy.PromptListener(); 247 this.#promptListener.on("closed", this.#onPromptClosed); 248 this.#promptListener.on("opened", this.#onPromptOpened); 249 250 // Set of event names which have active subscriptions. 251 this.#subscribedEvents = new Set(); 252 253 // Treat the event of moving a page to BFCache as context discarded event for iframes. 254 this.messageHandler.on("windowglobal-pagehide", this.#onPageHideEvent); 255 256 // Maps browsers to a promise and resolver that is used to block the create method. 257 this.#blockedCreateCommands = new WeakMap(); 258 } 259 260 destroy() { 261 this.#blockedCreateCommands = new WeakMap(); 262 263 this.#contextListener.off("attached", this.#onContextAttached); 264 this.#contextListener.off("discarded", this.#onContextDiscarded); 265 this.#contextListener.destroy(); 266 267 this.#navigationListener.off( 268 "fragment-navigated", 269 this.#onFragmentNavigated 270 ); 271 this.#navigationListener.off("history-updated", this.#onHistoryUpdated); 272 this.#navigationListener.off( 273 "navigation-committed", 274 this.#onNavigationCommitted 275 ); 276 this.#navigationListener.off("navigation-failed", this.#onNavigationFailed); 277 this.#navigationListener.off( 278 "navigation-started", 279 this.#onNavigationStarted 280 ); 281 this.#navigationListener.destroy(); 282 283 this.#promptListener.off("closed", this.#onPromptClosed); 284 this.#promptListener.off("opened", this.#onPromptOpened); 285 this.#promptListener.destroy(); 286 287 this.#subscribedEvents = null; 288 289 this.messageHandler.off("windowglobal-pagehide", this.#onPageHideEvent); 290 } 291 292 /** 293 * Activates and focuses the given top-level browsing context. 294 * 295 * @param {object=} options 296 * @param {string} options.context 297 * Id of the browsing context. 298 * 299 * @throws {InvalidArgumentError} 300 * Raised if an argument is of an invalid type or value. 301 * @throws {NoSuchFrameError} 302 * If the browsing context cannot be found. 303 */ 304 async activate(options = {}) { 305 const { context: contextId } = options; 306 307 lazy.assert.string( 308 contextId, 309 lazy.pprint`Expected "context" to be a string, got ${contextId}` 310 ); 311 const context = this._getNavigable(contextId); 312 313 lazy.assert.topLevel( 314 context, 315 lazy.pprint`Browsing context with id ${contextId} is not top-level` 316 ); 317 318 const targetTab = lazy.TabManager.getTabForBrowsingContext(context); 319 const targetWindow = lazy.TabManager.getWindowForTab(targetTab); 320 const selectedTab = lazy.TabManager.getTabBrowser(targetWindow).selectedTab; 321 322 const activated = [ 323 lazy.windowManager.focusWindow(targetWindow), 324 lazy.TabManager.selectTab(targetTab), 325 ]; 326 327 if (targetTab !== selectedTab && !lazy.AppInfo.isAndroid) { 328 // We need to wait until the "document.visibilityState" of the currently 329 // selected tab in the target window is marked as "hidden". 330 // 331 // Bug 1884142: It's not supported on Android for the TestRunner package. 332 const selectedBrowser = lazy.TabManager.getBrowserForTab(selectedTab); 333 activated.push( 334 this.#waitForVisibilityState(selectedBrowser.browsingContext, "hidden") 335 ); 336 } 337 338 await Promise.all(activated); 339 } 340 341 /** 342 * Used as an argument for browsingContext.captureScreenshot command, as one of the available variants 343 * {BoxClipRectangle} or {ElementClipRectangle}, to represent a target of the command. 344 * 345 * @typedef ClipRectangle 346 */ 347 348 /** 349 * Used as an argument for browsingContext.captureScreenshot command 350 * to represent a box which is going to be a target of the command. 351 * 352 * @typedef BoxClipRectangle 353 * 354 * @property {ClipRectangleType} [type=ClipRectangleType.Box] 355 * @property {number} x 356 * @property {number} y 357 * @property {number} width 358 * @property {number} height 359 */ 360 361 /** 362 * Used as an argument for the browsingContext.captureScreenshot command to 363 * represent the output image format. 364 * 365 * @typedef ImageFormat 366 * 367 * @property {string} type 368 * The output screenshot format such as `image/png`. 369 * @property {number=} quality 370 * A number between 0 and 1 representing the screenshot quality. 371 */ 372 373 /** 374 * Used as an argument for browsingContext.captureScreenshot command 375 * to represent an element which is going to be a target of the command. 376 * 377 * @typedef ElementClipRectangle 378 * 379 * @property {ClipRectangleType} [type=ClipRectangleType.Element] 380 * @property {SharedReference} element 381 */ 382 383 /** 384 * Capture a base64-encoded screenshot of the provided browsing context. 385 * 386 * @param {object=} options 387 * @param {string} options.context 388 * Id of the browsing context to screenshot. 389 * @param {ClipRectangle=} options.clip 390 * A box or an element of which a screenshot should be taken. 391 * If not present, take a screenshot of the whole viewport. 392 * @param {OriginType=} options.origin 393 * @param {ImageFormat=} options.format 394 * Configuration options for the output image. 395 * 396 * @throws {NoSuchFrameError} 397 * If the browsing context cannot be found. 398 */ 399 async captureScreenshot(options = {}) { 400 const { 401 clip = null, 402 context: contextId, 403 origin = OriginType.viewport, 404 format = { type: "image/png", quality: undefined }, 405 } = options; 406 407 lazy.assert.string( 408 contextId, 409 lazy.pprint`Expected "context" to be a string, got ${contextId}` 410 ); 411 const context = this._getNavigable(contextId); 412 413 const originTypeValues = Object.values(OriginType); 414 lazy.assert.that( 415 value => originTypeValues.includes(value), 416 `Expected "origin" to be one of ${originTypeValues}, ` + 417 lazy.pprint`got ${origin}` 418 )(origin); 419 420 lazy.assert.object( 421 format, 422 lazy.pprint`Expected "format" to be an object, got ${format}` 423 ); 424 425 const { quality, type: formatType } = format; 426 427 lazy.assert.string( 428 formatType, 429 lazy.pprint`Expected "type" to be a string, got ${formatType}` 430 ); 431 432 if (quality !== undefined) { 433 lazy.assert.number( 434 quality, 435 lazy.pprint`Expected "quality" to be a number, got ${quality}` 436 ); 437 438 lazy.assert.that( 439 imageQuality => imageQuality >= 0 && imageQuality <= 1, 440 lazy.pprint`Expected "quality" to be in the range of 0 to 1, got ${quality}` 441 )(quality); 442 } 443 444 if (clip !== null) { 445 lazy.assert.object( 446 clip, 447 lazy.pprint`Expected "clip" to be an object, got ${clip}` 448 ); 449 450 const { type: clipType } = clip; 451 switch (clipType) { 452 case ClipRectangleType.Box: { 453 const { x, y, width, height } = clip; 454 455 lazy.assert.number( 456 x, 457 lazy.pprint`Expected "x" to be a number, got ${x}` 458 ); 459 lazy.assert.number( 460 y, 461 lazy.pprint`Expected "y" to be a number, got ${y}` 462 ); 463 lazy.assert.number( 464 width, 465 lazy.pprint`Expected "width" to be a number, got ${width}` 466 ); 467 lazy.assert.number( 468 height, 469 lazy.pprint`Expected "height" to be a number, got ${height}` 470 ); 471 472 break; 473 } 474 475 case ClipRectangleType.Element: { 476 const { element } = clip; 477 478 lazy.assert.object( 479 element, 480 lazy.pprint`Expected "element" to be an object, got ${element}` 481 ); 482 483 break; 484 } 485 486 default: 487 throw new lazy.error.InvalidArgumentError( 488 `Expected "type" to be one of ${Object.values( 489 ClipRectangleType 490 )}, ` + lazy.pprint`got ${clipType}` 491 ); 492 } 493 } 494 495 const rect = await this.messageHandler.handleCommand({ 496 moduleName: "browsingContext", 497 commandName: "_getScreenshotRect", 498 destination: { 499 type: lazy.WindowGlobalMessageHandler.type, 500 id: context.id, 501 }, 502 params: { 503 clip, 504 origin, 505 }, 506 retryOnAbort: true, 507 }); 508 509 if (rect.width === 0 || rect.height === 0) { 510 throw new lazy.error.UnableToCaptureScreen( 511 `The dimensions of requested screenshot are incorrect, got width: ${rect.width} and height: ${rect.height}.` 512 ); 513 } 514 515 const canvas = await lazy.capture.canvas( 516 context.topChromeWindow, 517 context, 518 rect.x, 519 rect.y, 520 rect.width, 521 rect.height 522 ); 523 524 return { 525 data: lazy.capture.toBase64(canvas, formatType, quality), 526 }; 527 } 528 529 /** 530 * Close the provided browsing context. 531 * 532 * @param {object=} options 533 * @param {string} options.context 534 * Id of the browsing context to close. 535 * @param {boolean=} options.promptUnload 536 * Flag to indicate if a potential beforeunload prompt should be shown 537 * when closing the browsing context. Defaults to false. 538 * 539 * @throws {NoSuchFrameError} 540 * If the browsing context cannot be found. 541 * @throws {InvalidArgumentError} 542 * If the browsing context is not a top-level one. 543 */ 544 async close(options = {}) { 545 const { context: contextId, promptUnload = false } = options; 546 547 lazy.assert.string( 548 contextId, 549 lazy.pprint`Expected "context" to be a string, got ${contextId}` 550 ); 551 552 lazy.assert.boolean( 553 promptUnload, 554 lazy.pprint`Expected "promptUnload" to be a boolean, got ${promptUnload}` 555 ); 556 557 const context = this._getNavigable(contextId); 558 lazy.assert.topLevel( 559 context, 560 lazy.pprint`Browsing context with id ${contextId} is not top-level` 561 ); 562 563 if (lazy.TabManager.getTabCount() === 1) { 564 // The behavior when closing the very last tab is currently unspecified. 565 // As such behave like Marionette and don't allow closing it. 566 // See: https://github.com/w3c/webdriver-bidi/issues/187 567 return; 568 } 569 570 const tab = lazy.TabManager.getTabForBrowsingContext(context); 571 await lazy.TabManager.removeTab(tab, { skipPermitUnload: !promptUnload }); 572 } 573 574 /** 575 * Create a new browsing context using the provided type "tab" or "window". 576 * 577 * @param {object=} options 578 * @param {boolean=} options.background 579 * Whether the tab/window should be open in the background. Defaults to false, 580 * which means that the tab/window will be open in the foreground. 581 * @param {string=} options.referenceContext 582 * Id of the top-level browsing context to use as reference. 583 * If options.type is "tab", the new tab will open in the same window as 584 * the reference context, and will be added next to the reference context. 585 * If options.type is "window", the reference context is ignored. 586 * @param {CreateType} options.type 587 * Type of browsing context to create. 588 * @param {string=} options.userContext 589 * The id of the user context which should own the browsing context. 590 * Defaults to the default user context. 591 * 592 * @throws {InvalidArgumentError} 593 * If the browsing context is not a top-level one. 594 * @throws {NoSuchFrameError} 595 * If the browsing context cannot be found. 596 */ 597 async create(options = {}) { 598 const { 599 background = false, 600 referenceContext: referenceContextId = null, 601 type: typeHint, 602 userContext: userContextId = null, 603 } = options; 604 605 if (![CreateType.tab, CreateType.window].includes(typeHint)) { 606 throw new lazy.error.InvalidArgumentError( 607 `Expected "type" to be one of ${Object.values(CreateType)}, ` + 608 lazy.pprint`got ${typeHint}` 609 ); 610 } 611 612 lazy.assert.boolean( 613 background, 614 lazy.pprint`Expected "background" to be a boolean, got ${background}` 615 ); 616 617 let referenceContext = null; 618 if (referenceContextId !== null) { 619 lazy.assert.string( 620 referenceContextId, 621 lazy.pprint`Expected "referenceContext" to be a string, got ${referenceContextId}` 622 ); 623 624 referenceContext = 625 lazy.NavigableManager.getBrowsingContextById(referenceContextId); 626 if (!referenceContext) { 627 throw new lazy.error.NoSuchFrameError( 628 `Browsing Context with id ${referenceContextId} not found` 629 ); 630 } 631 632 if (referenceContext.parent) { 633 throw new lazy.error.InvalidArgumentError( 634 `referenceContext with id ${referenceContextId} is not a top-level browsing context` 635 ); 636 } 637 } 638 639 let userContext = lazy.UserContextManager.defaultUserContextId; 640 if (referenceContext !== null) { 641 userContext = 642 lazy.UserContextManager.getIdByBrowsingContext(referenceContext); 643 } 644 645 if (userContextId !== null) { 646 lazy.assert.string( 647 userContextId, 648 lazy.pprint`Expected "userContext" to be a string, got ${userContextId}` 649 ); 650 651 if (!lazy.UserContextManager.hasUserContextId(userContextId)) { 652 throw new lazy.error.NoSuchUserContextError( 653 `User Context with id ${userContextId} was not found` 654 ); 655 } 656 657 userContext = userContextId; 658 659 if ( 660 lazy.AppInfo.isAndroid && 661 userContext != lazy.UserContextManager.defaultUserContextId 662 ) { 663 throw new lazy.error.UnsupportedOperationError( 664 `browsingContext.create with non-default "userContext" not supported for ${lazy.AppInfo.name}` 665 ); 666 } 667 } 668 669 let browser; 670 671 // Since each tab in GeckoView has its own Gecko instance running, 672 // which means also its own window object, for Android we will need to focus 673 // a previously focused window in case of opening the tab in the background. 674 const previousWindow = Services.wm.getMostRecentBrowserWindow(); 675 const previousTab = 676 lazy.TabManager.getTabBrowser(previousWindow).selectedTab; 677 678 // The type supported varies by platform, as Android can only support one window. 679 // As such, type compatibility will need to be checked and will fallback if necessary. 680 let type; 681 if ( 682 (typeHint == "tab" && lazy.TabManager.supportsTabs()) || 683 (typeHint == "window" && lazy.windowManager.supportsWindows()) 684 ) { 685 type = typeHint; 686 } else if (lazy.TabManager.supportsTabs()) { 687 type = "tab"; 688 } else if (lazy.windowManager.supportsWindows()) { 689 type = "window"; 690 } else { 691 throw new lazy.error.UnsupportedOperationError( 692 `Not supported in ${lazy.AppInfo.name}` 693 ); 694 } 695 696 let waitForVisibilityStatePromise; 697 switch (type) { 698 case "window": { 699 const newWindow = await lazy.windowManager.openBrowserWindow({ 700 focus: !background, 701 userContextId: userContext, 702 }); 703 browser = lazy.TabManager.getTabBrowser(newWindow).selectedBrowser; 704 break; 705 } 706 case "tab": { 707 // The window to open the new tab in. 708 let window = Services.wm.getMostRecentBrowserWindow(); 709 710 let referenceTab; 711 if (referenceContext !== null) { 712 referenceTab = 713 lazy.TabManager.getTabForBrowsingContext(referenceContext); 714 window = lazy.TabManager.getWindowForTab(referenceTab); 715 } 716 717 if (!background && !lazy.AppInfo.isAndroid) { 718 // When opening a new foreground tab we need to wait until the 719 // "document.visibilityState" of the currently selected tab in this 720 // window is marked as "hidden". 721 // 722 // Bug 1884142: It's not supported on Android for the TestRunner package. 723 const selectedTab = lazy.TabManager.getTabBrowser(window).selectedTab; 724 725 // Create the promise immediately, but await it later in parallel with 726 // waitForInitialNavigationCompleted. 727 waitForVisibilityStatePromise = this.#waitForVisibilityState( 728 lazy.TabManager.getBrowserForTab(selectedTab).browsingContext, 729 "hidden" 730 ); 731 } 732 733 const tab = await lazy.TabManager.addTab({ 734 focus: !background, 735 referenceTab, 736 userContextId: userContext, 737 }); 738 browser = lazy.TabManager.getBrowserForTab(tab); 739 } 740 } 741 742 // ConfigurationModule cannot block parsing for initial about:blank load, so we block 743 // browsing_context.create till configuration is applied. 744 let blocker = this.#blockedCreateCommands.get(browser); 745 // If the configuration is done before we have a browser, a resolved blocker already exists. 746 if (!blocker) { 747 blocker = Promise.withResolvers(); 748 if (!this.#hasConfigurationForContext(userContext)) { 749 blocker.resolve(); 750 } 751 this.#blockedCreateCommands.set(browser, blocker); 752 } 753 754 await Promise.all([ 755 lazy.waitForInitialNavigationCompleted( 756 browser.browsingContext.webProgress, 757 { 758 unloadTimeout: 5000, 759 } 760 ), 761 waitForVisibilityStatePromise, 762 blocker.promise, 763 ]); 764 765 this.#blockedCreateCommands.delete(browser); 766 767 // The tab on Android is always opened in the foreground, 768 // so we need to select the previous tab, 769 // and we have to wait until is fully loaded. 770 // TODO: Bug 1845559. This workaround can be removed, 771 // when the API to create a tab for Android supports the background option. 772 if (lazy.AppInfo.isAndroid && background) { 773 await lazy.windowManager.focusWindow(previousWindow); 774 await lazy.TabManager.selectTab(previousTab); 775 } 776 777 // Force a reflow by accessing `clientHeight` (see Bug 1847044). 778 browser.parentElement.clientHeight; 779 780 if (!background && !lazy.AppInfo.isAndroid) { 781 // See Bug 2002097, on slow platforms, the newly created tab might not be 782 // visible immediately. 783 await this.#waitForVisibilityState( 784 browser.browsingContext, 785 "visible", 786 // Waiting for visibility can potentially be racy. If several contexts 787 // are created in parallel, we might not be able to catch the document 788 // in the expected state. 789 { timeout: TIMEOUT_WAIT_FOR_VISIBILITY * lazy.getTimeoutMultiplier() } 790 ); 791 } 792 793 return { 794 context: lazy.NavigableManager.getIdForBrowser(browser), 795 }; 796 } 797 798 /* eslint-disable jsdoc/valid-types */ 799 /** 800 * An object that holds the WebDriver Bidi browsing context information. 801 * 802 * @typedef BrowsingContextInfo 803 * 804 * @property {string} context 805 * The id of the browsing context. 806 * @property {string=} parent 807 * The parent of the browsing context if it's the root browsing context 808 * of the to be processed browsing context tree. 809 * @property {string} url 810 * The current documents location. 811 * @property {string} userContext 812 * The id of the user context owning this browsing context. 813 * @property {Array<BrowsingContextInfo>=} children 814 * List of child browsing contexts. Only set if maxDepth hasn't been 815 * reached yet. 816 * @property {string} clientWindow 817 * The id of the window the browsing context belongs to. 818 * @property {string=} "moz:name" 819 * Name of the browsing context. 820 * @property {MozContextScope=} "moz:scope" 821 * The scope of the browsing context. 822 */ 823 /* eslint-enable jsdoc/valid-types */ 824 825 /** 826 * An object that holds the WebDriver Bidi browsing context tree information. 827 * 828 * @typedef BrowsingContextGetTreeResult 829 * 830 * @property {Array<BrowsingContextInfo>} contexts 831 * List of child browsing contexts. 832 */ 833 834 /** 835 * Returns a tree of all browsing contexts that are descendants of the 836 * given context, or all top-level contexts when no root is provided. 837 * 838 * @param {object=} options 839 * @param {number=} options.maxDepth 840 * Depth of the browsing context tree to traverse. If not specified 841 * the whole tree is returned. 842 * @param {string=} options.root 843 * Id of the root browsing context. 844 * @param {MozContextScope=} options."moz:scope" 845 * The scope from which browsing contexts are retrieved. This 846 * parameter cannot be used when a root browsing context is specified. 847 * 848 * @returns {BrowsingContextGetTreeResult} 849 * Tree of browsing context information. 850 * @throws {NoSuchFrameError} 851 * If the browsing context cannot be found. 852 */ 853 getTree(options = {}) { 854 const { 855 maxDepth = null, 856 root: rootId = null, 857 "moz:scope": scope = null, 858 } = options; 859 860 if (maxDepth !== null) { 861 lazy.assert.positiveInteger( 862 maxDepth, 863 lazy.pprint`Expected "maxDepth" to be a positive integer, got ${maxDepth}` 864 ); 865 } 866 867 if (scope !== null) { 868 const contextScopes = Object.values(MozContextScope); 869 lazy.assert.that( 870 _scope => contextScopes.includes(_scope), 871 `Expected "moz:scope" to be one of ${contextScopes}, ` + 872 lazy.pprint`got ${scope}` 873 )(scope); 874 875 if (scope != MozContextScope.CONTENT) { 876 // By default only content browsing contexts are allowed. 877 lazy.assert.hasSystemAccess(); 878 } 879 } 880 881 let contexts; 882 if (rootId !== null) { 883 // With a root id specified return the context info for itself 884 // and the full tree. 885 lazy.assert.string( 886 rootId, 887 lazy.pprint`Expected "root" to be a string, got ${rootId}` 888 ); 889 890 if (scope) { 891 // At the moment we only allow to set a specific scope 892 // when querying at the top-level. 893 throw new lazy.error.InvalidArgumentError( 894 `"root" and "moz:scope" are mutual exclusive` 895 ); 896 } 897 898 contexts = [this._getNavigable(rootId, { supportsChromeScope: true })]; 899 } else { 900 switch (scope) { 901 case MozContextScope.CHROME: { 902 // Return all browsing contexts related to chrome windows. 903 contexts = lazy.windowManager.windows.map(win => win.browsingContext); 904 break; 905 } 906 default: { 907 // Return all top-level browsing contexts. 908 contexts = lazy.TabManager.getBrowsers().map( 909 browser => browser.browsingContext 910 ); 911 } 912 } 913 } 914 915 const contextsInfo = contexts.map(context => { 916 return getBrowsingContextInfo(context, { maxDepth }); 917 }); 918 919 return { contexts: contextsInfo }; 920 } 921 922 /** 923 * Closes an open prompt. 924 * 925 * @param {object=} options 926 * @param {string} options.context 927 * Id of the browsing context. 928 * @param {boolean=} options.accept 929 * Whether user prompt should be accepted or dismissed. 930 * Defaults to true. 931 * @param {string=} options.userText 932 * Input to the user prompt's value field. 933 * Defaults to an empty string. 934 * 935 * @throws {InvalidArgumentError} 936 * Raised if an argument is of an invalid type or value. 937 * @throws {NoSuchAlertError} 938 * If there is no current user prompt. 939 * @throws {NoSuchFrameError} 940 * If the browsing context cannot be found. 941 * @throws {UnsupportedOperationError} 942 * Raised when the command is called for "beforeunload" prompt. 943 */ 944 async handleUserPrompt(options = {}) { 945 const { accept = true, context: contextId, userText = "" } = options; 946 947 lazy.assert.string( 948 contextId, 949 lazy.pprint`Expected "context" to be a string, got ${contextId}` 950 ); 951 952 const context = this._getNavigable(contextId); 953 954 lazy.assert.boolean( 955 accept, 956 lazy.pprint`Expected "accept" to be a boolean, got ${accept}` 957 ); 958 959 lazy.assert.string( 960 userText, 961 lazy.pprint`Expected "userText" to be a string, got ${userText}` 962 ); 963 964 const tab = lazy.TabManager.getTabForBrowsingContext(context); 965 const browser = lazy.TabManager.getBrowserForTab(tab); 966 const window = lazy.TabManager.getWindowForTab(tab); 967 const dialog = lazy.modal.findPrompt({ 968 window, 969 contentBrowser: browser, 970 }); 971 972 const closePrompt = async callback => { 973 const dialogClosed = new lazy.EventPromise( 974 window, 975 "DOMModalDialogClosed" 976 ); 977 callback(); 978 await dialogClosed; 979 }; 980 981 if (dialog && dialog.isOpen) { 982 switch (dialog.promptType) { 983 case UserPromptType.alert: 984 await closePrompt(() => dialog.accept()); 985 return; 986 987 case UserPromptType.beforeunload: 988 case UserPromptType.confirm: 989 await closePrompt(() => { 990 if (accept) { 991 dialog.accept(); 992 } else { 993 dialog.dismiss(); 994 } 995 }); 996 997 return; 998 999 case UserPromptType.prompt: 1000 await closePrompt(() => { 1001 if (accept) { 1002 dialog.text = userText; 1003 dialog.accept(); 1004 } else { 1005 dialog.dismiss(); 1006 } 1007 }); 1008 1009 return; 1010 1011 default: 1012 throw new lazy.error.UnsupportedOperationError( 1013 `Prompts of type "${dialog.promptType}" are not supported` 1014 ); 1015 } 1016 } 1017 1018 throw new lazy.error.NoSuchAlertError(); 1019 } 1020 1021 /** 1022 * Used as an argument for browsingContext.locateNodes command, as one of the available variants 1023 * {AccessibilityLocator}, {ContextLocator}, {CssLocator}, {InnerTextLocator} or {XPathLocator}, 1024 * to represent a way of how lookup of nodes is going to be performed. 1025 * 1026 * @typedef Locator 1027 */ 1028 1029 /** 1030 * Used as a value argument for browsingContext.locateNodes command 1031 * in case of a lookup by accessibility attributes. 1032 * 1033 * @typedef AccessibilityLocatorValue 1034 * 1035 * @property {string=} name 1036 * @property {string=} role 1037 */ 1038 1039 /** 1040 * Used as an argument for browsingContext.locateNodes command 1041 * to represent a lookup by accessibility attributes. 1042 * 1043 * @typedef AccessibilityLocator 1044 * 1045 * @property {LocatorType} [type=LocatorType.accessibility] 1046 * @property {AccessibilityLocatorValue} value 1047 */ 1048 1049 /** 1050 * Used as a value argument for browsingContext.locateNodes command 1051 * in case of a lookup for a context container. 1052 * 1053 * @typedef ContextLocatorValue 1054 * 1055 * @property {string} context 1056 */ 1057 1058 /** 1059 * Used as an argument for browsingContext.locateNodes command 1060 * to represent a lookup for a context container. 1061 * 1062 * @typedef ContextLocator 1063 * 1064 * @property {LocatorType} [type=LocatorType.context] 1065 * @property {ContextLocatorValue} value 1066 */ 1067 1068 /** 1069 * Used as an argument for browsingContext.locateNodes command 1070 * to represent a lookup by css selector. 1071 * 1072 * @typedef CssLocator 1073 * 1074 * @property {LocatorType} [type=LocatorType.css] 1075 * @property {string} value 1076 */ 1077 1078 /** 1079 * Used as an argument for browsingContext.locateNodes command 1080 * to represent a lookup by inner text. 1081 * 1082 * @typedef InnerTextLocator 1083 * 1084 * @property {LocatorType} [type=LocatorType.innerText] 1085 * @property {string} value 1086 * @property {boolean=} ignoreCase 1087 * @property {("full"|"partial")=} matchType 1088 * @property {number=} maxDepth 1089 */ 1090 1091 /** 1092 * Used as an argument for browsingContext.locateNodes command 1093 * to represent a lookup by xpath. 1094 * 1095 * @typedef XPathLocator 1096 * 1097 * @property {LocatorType} [type=LocatorType.xpath] 1098 * @property {string} value 1099 */ 1100 1101 /** 1102 * Returns a list of all nodes matching 1103 * the specified locator. 1104 * 1105 * @param {object} options 1106 * @param {string} options.context 1107 * Id of the browsing context. 1108 * @param {Locator} options.locator 1109 * The type of lookup which is going to be used. 1110 * @param {number=} options.maxNodeCount 1111 * The maximum amount of nodes which is going to be returned. 1112 * Defaults to return all the found nodes. 1113 * @property {SerializationOptions=} serializationOptions 1114 * An object which holds the information of how the DOM nodes 1115 * should be serialized. 1116 * @property {Array<SharedReference>=} startNodes 1117 * A list of references to nodes, which are used as 1118 * starting points for lookup. 1119 * 1120 * @throws {InvalidArgumentError} 1121 * Raised if an argument is of an invalid type or value. 1122 * @throws {InvalidSelectorError} 1123 * Raised if a locator value is invalid. 1124 * @throws {NoSuchFrameError} 1125 * If the browsing context cannot be found. 1126 * @throws {UnsupportedOperationError} 1127 * Raised when unsupported lookup types are used. 1128 */ 1129 async locateNodes(options = {}) { 1130 const { 1131 context: navigableId, 1132 locator, 1133 maxNodeCount = null, 1134 serializationOptions, 1135 startNodes = null, 1136 } = options; 1137 1138 lazy.assert.string( 1139 navigableId, 1140 lazy.pprint`Expected "context" to be a string, got ${navigableId}` 1141 ); 1142 1143 const context = this._getNavigable(navigableId); 1144 1145 lazy.assert.object( 1146 locator, 1147 lazy.pprint`Expected "locator" to be an object, got ${locator}` 1148 ); 1149 1150 const locatorTypes = Object.values(LocatorType); 1151 1152 lazy.assert.that( 1153 locatorType => locatorTypes.includes(locatorType), 1154 `Expected "locator.type" to be one of ${locatorTypes}, ` + 1155 lazy.pprint`got ${locator.type}` 1156 )(locator.type); 1157 1158 if ( 1159 [LocatorType.css, LocatorType.innerText, LocatorType.xpath].includes( 1160 locator.type 1161 ) 1162 ) { 1163 lazy.assert.string( 1164 locator.value, 1165 `Expected "locator.value" of "locator.type" "${locator.type}" to be a string, ` + 1166 lazy.pprint`got ${locator.value}` 1167 ); 1168 } 1169 if (locator.type == LocatorType.accessibility) { 1170 lazy.assert.object( 1171 locator.value, 1172 `Expected "locator.value" of "locator.type" "${locator.type}" to be an object, ` + 1173 lazy.pprint`got ${locator.value}` 1174 ); 1175 1176 const { name = null, role = null } = locator.value; 1177 if (name !== null) { 1178 lazy.assert.string( 1179 locator.value.name, 1180 `Expected "locator.value.name" of "locator.type" "${locator.type}" to be a string, ` + 1181 lazy.pprint`got ${name}` 1182 ); 1183 } 1184 if (role !== null) { 1185 lazy.assert.string( 1186 locator.value.role, 1187 `Expected "locator.value.role" of "locator.type" "${locator.type}" to be a string, ` + 1188 lazy.pprint`got ${role}` 1189 ); 1190 } 1191 } 1192 1193 if (locator.type == LocatorType.context) { 1194 if (startNodes !== null) { 1195 throw new lazy.error.InvalidArgumentError( 1196 `Expected "startNodes" to be null when using "locator.type" "${locator.type}", ` + 1197 lazy.pprint`got ${startNodes}` 1198 ); 1199 } 1200 1201 lazy.assert.object( 1202 locator.value, 1203 `Expected "locator.value" of "locator.type" "${locator.type}" to be an object, ` + 1204 lazy.pprint`got ${locator.value}` 1205 ); 1206 const selector = locator.value; 1207 const contextId = selector.context; 1208 lazy.assert.string( 1209 contextId, 1210 `Expected "locator.value.context" of "locator.type" "${locator.type}" to be a string, ` + 1211 lazy.pprint`got ${contextId}` 1212 ); 1213 1214 const childContext = this._getNavigable(contextId); 1215 if (childContext.parent !== context) { 1216 throw new lazy.error.InvalidArgumentError( 1217 `Expected "locator.context" (${contextId}) to be a direct child context of "context" (${navigableId})` 1218 ); 1219 } 1220 1221 // Replace the locator selector context value by the internal browsing 1222 // context id. 1223 locator.value.context = childContext.id; 1224 } 1225 1226 if ( 1227 ![ 1228 LocatorType.accessibility, 1229 LocatorType.context, 1230 LocatorType.css, 1231 LocatorType.xpath, 1232 ].includes(locator.type) 1233 ) { 1234 throw new lazy.error.UnsupportedOperationError( 1235 `"locator.type" argument with value: ${locator.type} is not supported yet.` 1236 ); 1237 } 1238 1239 if (maxNodeCount != null) { 1240 const maxNodeCountErrorMsg = lazy.pprint`Expected "maxNodeCount" to be an integer and greater than 0, got ${maxNodeCount}`; 1241 lazy.assert.that(maxNodeCount => { 1242 lazy.assert.integer(maxNodeCount, maxNodeCountErrorMsg); 1243 return maxNodeCount > 0; 1244 }, maxNodeCountErrorMsg)(maxNodeCount); 1245 } 1246 1247 const serializationOptionsWithDefaults = 1248 lazy.setDefaultAndAssertSerializationOptions(serializationOptions); 1249 1250 if (startNodes != null) { 1251 lazy.assert.isNonEmptyArray( 1252 startNodes, 1253 lazy.pprint`Expected "startNodes" to be a non-empty array, got ${startNodes}` 1254 ); 1255 } 1256 1257 const result = await this._forwardToWindowGlobal( 1258 "_locateNodes", 1259 context.id, 1260 { 1261 locator, 1262 maxNodeCount, 1263 serializationOptions: serializationOptionsWithDefaults, 1264 startNodes, 1265 }, 1266 { retryOnAbort: true } 1267 ); 1268 1269 return { 1270 nodes: result.serializedNodes, 1271 }; 1272 } 1273 1274 /** 1275 * An object that holds the WebDriver Bidi navigation information. 1276 * 1277 * @typedef BrowsingContextNavigateResult 1278 * 1279 * @property {string} navigation 1280 * Unique id for this navigation. 1281 * @property {string} url 1282 * The requested or reached URL. 1283 */ 1284 1285 /** 1286 * Navigate the given context to the provided url, with the provided wait condition. 1287 * 1288 * @param {object=} options 1289 * @param {string} options.context 1290 * Id of the browsing context to navigate. 1291 * @param {string} options.url 1292 * Url for the navigation. 1293 * @param {WaitCondition=} options.wait 1294 * Wait condition for the navigation, one of "none", "interactive", "complete". 1295 * Defaults to "none". 1296 * 1297 * @returns {BrowsingContextNavigateResult} 1298 * Navigation result. 1299 * 1300 * @throws {InvalidArgumentError} 1301 * Raised if an argument is of an invalid type or value. 1302 * @throws {NoSuchFrameError} 1303 * If the browsing context for context cannot be found. 1304 */ 1305 async navigate(options = {}) { 1306 const { context: contextId, url, wait = WaitCondition.None } = options; 1307 1308 lazy.assert.string( 1309 contextId, 1310 lazy.pprint`Expected "context" to be a string, got ${contextId}` 1311 ); 1312 1313 lazy.assert.string( 1314 url, 1315 lazy.pprint`Expected "url" to be string, got ${url}` 1316 ); 1317 1318 const waitConditions = Object.values(WaitCondition); 1319 if (!waitConditions.includes(wait)) { 1320 throw new lazy.error.InvalidArgumentError( 1321 `Expected "wait" to be one of ${waitConditions}, ` + 1322 lazy.pprint`got ${wait}` 1323 ); 1324 } 1325 1326 const context = this._getNavigable(contextId); 1327 1328 // webProgress will be stable even if the context navigates, retrieve it 1329 // immediately before doing any asynchronous call. 1330 const webProgress = context.webProgress; 1331 1332 const base = await this.messageHandler.handleCommand({ 1333 moduleName: "browsingContext", 1334 commandName: "_getBaseURL", 1335 destination: { 1336 type: lazy.WindowGlobalMessageHandler.type, 1337 id: context.id, 1338 }, 1339 retryOnAbort: true, 1340 }); 1341 1342 let targetURI; 1343 try { 1344 const baseURI = Services.io.newURI(base); 1345 targetURI = Services.io.newURI(url, null, baseURI); 1346 } catch (e) { 1347 throw new lazy.error.InvalidArgumentError( 1348 `Expected "url" to be a valid URL (${e.message})` 1349 ); 1350 } 1351 1352 return this.#awaitNavigation( 1353 webProgress, 1354 () => { 1355 context.loadURI(targetURI, { 1356 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK, 1357 // Fake user activation. 1358 hasValidUserGestureActivation: true, 1359 // Prevent HTTPS-First upgrades. 1360 schemelessInput: Ci.nsILoadInfo.SchemelessInputTypeSchemeful, 1361 triggeringPrincipal: 1362 Services.scriptSecurityManager.getSystemPrincipal(), 1363 }); 1364 }, 1365 { 1366 targetURI, 1367 wait, 1368 } 1369 ); 1370 } 1371 1372 /** 1373 * An object that holds the information about margins 1374 * for Webdriver BiDi browsingContext.print command. 1375 * 1376 * @typedef BrowsingContextPrintMarginParameters 1377 * 1378 * @property {number=} bottom 1379 * Bottom margin in cm. Defaults to 1cm (~0.4 inches). 1380 * @property {number=} left 1381 * Left margin in cm. Defaults to 1cm (~0.4 inches). 1382 * @property {number=} right 1383 * Right margin in cm. Defaults to 1cm (~0.4 inches). 1384 * @property {number=} top 1385 * Top margin in cm. Defaults to 1cm (~0.4 inches). 1386 */ 1387 1388 /** 1389 * An object that holds the information about paper size 1390 * for Webdriver BiDi browsingContext.print command. 1391 * 1392 * @typedef BrowsingContextPrintPageParameters 1393 * 1394 * @property {number=} height 1395 * Paper height in cm. Defaults to US letter height (27.94cm / 11 inches). 1396 * @property {number=} width 1397 * Paper width in cm. Defaults to US letter width (21.59cm / 8.5 inches). 1398 */ 1399 1400 /** 1401 * Used as return value for Webdriver BiDi browsingContext.print command. 1402 * 1403 * @typedef BrowsingContextPrintResult 1404 * 1405 * @property {string} data 1406 * Base64 encoded PDF representing printed document. 1407 */ 1408 1409 /** 1410 * Creates a paginated PDF representation of a document 1411 * of the provided browsing context, and returns it 1412 * as a Base64-encoded string. 1413 * 1414 * @param {object=} options 1415 * @param {string} options.context 1416 * Id of the browsing context. 1417 * @param {boolean=} options.background 1418 * Whether or not to print background colors and images. 1419 * Defaults to false, which prints without background graphics. 1420 * @param {BrowsingContextPrintMarginParameters=} options.margin 1421 * Paper margins. 1422 * @param {('landscape'|'portrait')=} options.orientation 1423 * Paper orientation. Defaults to 'portrait'. 1424 * @param {BrowsingContextPrintPageParameters=} options.page 1425 * Paper size. 1426 * @param {Array<number|string>=} options.pageRanges 1427 * Paper ranges to print, e.g., ['1-5', 8, '11-13']. 1428 * Defaults to the empty array, which means print all pages. 1429 * @param {number=} options.scale 1430 * Scale of the webpage rendering. Defaults to 1.0. 1431 * @param {boolean=} options.shrinkToFit 1432 * Whether or not to override page size as defined by CSS. 1433 * Defaults to true, in which case the content will be scaled 1434 * to fit the paper size. 1435 * 1436 * @returns {BrowsingContextPrintResult} 1437 * 1438 * @throws {InvalidArgumentError} 1439 * Raised if an argument is of an invalid type or value. 1440 * @throws {NoSuchFrameError} 1441 * If the browsing context cannot be found. 1442 */ 1443 async print(options = {}) { 1444 const { 1445 context: contextId, 1446 background, 1447 margin, 1448 orientation, 1449 page, 1450 pageRanges, 1451 scale, 1452 shrinkToFit, 1453 } = options; 1454 1455 lazy.assert.string( 1456 contextId, 1457 lazy.pprint`Expected "context" to be a string, got ${contextId}` 1458 ); 1459 const context = this._getNavigable(contextId); 1460 1461 const settings = lazy.print.addDefaultSettings({ 1462 background, 1463 margin, 1464 orientation, 1465 page, 1466 pageRanges, 1467 scale, 1468 shrinkToFit, 1469 }); 1470 1471 for (const prop of ["top", "bottom", "left", "right"]) { 1472 lazy.assert.positiveNumber( 1473 settings.margin[prop], 1474 `Expected "margin.${prop}" to be a positive number, ` + 1475 lazy.pprint`got ${settings.margin[prop]}` 1476 ); 1477 } 1478 for (const prop of ["width", "height"]) { 1479 lazy.assert.positiveNumber( 1480 settings.page[prop], 1481 `Expected "page.${prop}" to be a positive number, ` + 1482 lazy.pprint`got ${settings.page[prop]}` 1483 ); 1484 } 1485 lazy.assert.positiveNumber( 1486 settings.scale, 1487 `Expected "scale" to be a positive number, ` + 1488 lazy.pprint`got ${settings.scale}` 1489 ); 1490 lazy.assert.that( 1491 scale => 1492 scale >= lazy.print.minScaleValue && scale <= lazy.print.maxScaleValue, 1493 `scale ${settings.scale} is outside the range ${lazy.print.minScaleValue}-${lazy.print.maxScaleValue}` 1494 )(settings.scale); 1495 lazy.assert.boolean( 1496 settings.shrinkToFit, 1497 lazy.pprint`Expected "shrinkToFit" to be a boolean, got ${settings.shrinkToFit}` 1498 ); 1499 lazy.assert.that( 1500 orientation => lazy.print.defaults.orientationValue.includes(orientation), 1501 `Expected "orientation" to be one of ${lazy.print.defaults.orientationValue}", ` + 1502 lazy.pprint`got {settings.orientation}` 1503 )(settings.orientation); 1504 lazy.assert.boolean( 1505 settings.background, 1506 lazy.pprint`Expected "background" to be a boolean, got ${settings.background}` 1507 ); 1508 lazy.assert.array( 1509 settings.pageRanges, 1510 lazy.pprint`Expected "pageRanges" to be an array, got ${settings.pageRanges}` 1511 ); 1512 1513 const printSettings = await lazy.print.getPrintSettings(settings); 1514 const binaryString = await lazy.print.printToBinaryString( 1515 context, 1516 printSettings 1517 ); 1518 1519 return { 1520 data: btoa(binaryString), 1521 }; 1522 } 1523 1524 /** 1525 * Reload the given context's document, with the provided wait condition. 1526 * 1527 * @param {object=} options 1528 * @param {string} options.context 1529 * Id of the browsing context to navigate. 1530 * @param {bool=} options.ignoreCache 1531 * If true ignore the browser cache. [Not yet supported] 1532 * @param {WaitCondition=} options.wait 1533 * Wait condition for the navigation, one of "none", "interactive", "complete". 1534 * Defaults to "none". 1535 * 1536 * @returns {BrowsingContextNavigateResult} 1537 * Navigation result. 1538 * 1539 * @throws {InvalidArgumentError} 1540 * Raised if an argument is of an invalid type or value. 1541 * @throws {NoSuchFrameError} 1542 * If the browsing context for context cannot be found. 1543 */ 1544 async reload(options = {}) { 1545 const { 1546 context: contextId, 1547 ignoreCache, 1548 wait = WaitCondition.None, 1549 } = options; 1550 1551 lazy.assert.string( 1552 contextId, 1553 lazy.pprint`Expected "context" to be a string, got ${contextId}` 1554 ); 1555 1556 if (typeof ignoreCache != "undefined") { 1557 throw new lazy.error.UnsupportedOperationError( 1558 `Argument "ignoreCache" is not supported yet.` 1559 ); 1560 } 1561 1562 const waitConditions = Object.values(WaitCondition); 1563 if (!waitConditions.includes(wait)) { 1564 throw new lazy.error.InvalidArgumentError( 1565 `Expected "wait" to be one of ${waitConditions}, ` + 1566 lazy.pprint`got ${wait}` 1567 ); 1568 } 1569 1570 const context = this._getNavigable(contextId); 1571 1572 // webProgress will be stable even if the context navigates, retrieve it 1573 // immediately before doing any asynchronous call. 1574 const webProgress = context.webProgress; 1575 1576 return this.#awaitNavigation( 1577 webProgress, 1578 () => { 1579 context.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); 1580 }, 1581 { wait } 1582 ); 1583 } 1584 1585 /** 1586 * Set the top-level browsing context's viewport to a given dimension. 1587 * 1588 * @param {object=} options 1589 * @param {string=} options.context 1590 * Id of the browsing context. 1591 * @param {(number|null)=} options.devicePixelRatio 1592 * A value to override device pixel ratio, or `null` to reset it to 1593 * the original value. Different values will not cause the rendering to change, 1594 * only image srcsets and media queries will be applied as if DPR is redefined. 1595 * @param {(Viewport|null)=} options.viewport 1596 * Dimensions to set the viewport to, or `null` to reset it 1597 * to the original dimensions. 1598 * @param {Array<string>=} options.userContexts 1599 * Optional list of user context ids. 1600 * 1601 * @throws {InvalidArgumentError} 1602 * Raised if an argument is of an invalid type or value. 1603 * @throws {UnsupportedOperationError} 1604 * Raised when the command is called on Android. 1605 */ 1606 async setViewport(options = {}) { 1607 const { 1608 context: contextId = null, 1609 devicePixelRatio, 1610 viewport, 1611 userContexts: userContextIds = null, 1612 } = options; 1613 1614 const userContexts = new Set(); 1615 1616 if (contextId !== null) { 1617 lazy.assert.string( 1618 contextId, 1619 lazy.pprint`Expected "context" to be a string, got ${contextId}` 1620 ); 1621 } else if (userContextIds !== null) { 1622 lazy.assert.isNonEmptyArray( 1623 userContextIds, 1624 lazy.pprint`Expected "userContexts" to be a non-empty array, got ${userContextIds}` 1625 ); 1626 1627 for (const userContextId of userContextIds) { 1628 lazy.assert.string( 1629 userContextId, 1630 lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}` 1631 ); 1632 1633 const internalId = 1634 lazy.UserContextManager.getInternalIdById(userContextId); 1635 1636 if (internalId === null) { 1637 throw new lazy.error.NoSuchUserContextError( 1638 `User context with id: ${userContextId} doesn't exist` 1639 ); 1640 } 1641 1642 userContexts.add(internalId); 1643 } 1644 } else { 1645 throw new lazy.error.InvalidArgumentError( 1646 `At least one of "context" or "userContexts" arguments should be provided` 1647 ); 1648 } 1649 1650 if (contextId !== null && userContextIds !== null) { 1651 throw new lazy.error.InvalidArgumentError( 1652 `Providing both "context" and "userContexts" arguments is not supported` 1653 ); 1654 } 1655 1656 if (viewport !== undefined && viewport !== null) { 1657 lazy.assert.object( 1658 viewport, 1659 lazy.pprint`Expected "viewport" to be an object, got ${viewport}` 1660 ); 1661 1662 const { height, width } = viewport; 1663 lazy.assert.positiveInteger( 1664 height, 1665 lazy.pprint`Expected viewport's "height" to be a positive integer, got ${height}` 1666 ); 1667 lazy.assert.positiveInteger( 1668 width, 1669 lazy.pprint`Expected viewport's "width" to be a positive integer, got ${width}` 1670 ); 1671 1672 if (height > MAX_WINDOW_SIZE || width > MAX_WINDOW_SIZE) { 1673 throw new lazy.error.UnsupportedOperationError( 1674 `"width" or "height" cannot be larger than ${MAX_WINDOW_SIZE} px` 1675 ); 1676 } 1677 } 1678 1679 if (devicePixelRatio !== undefined && devicePixelRatio !== null) { 1680 lazy.assert.number( 1681 devicePixelRatio, 1682 lazy.pprint`Expected "devicePixelRatio" to be a number or null, got ${devicePixelRatio}` 1683 ); 1684 lazy.assert.that( 1685 value => value > 0, 1686 lazy.pprint`Expected "devicePixelRatio" to be greater than 0, got ${devicePixelRatio}` 1687 )(devicePixelRatio); 1688 } 1689 1690 const navigables = new Set(); 1691 if (contextId !== null) { 1692 const navigable = this._getNavigable(contextId); 1693 lazy.assert.topLevel( 1694 navigable, 1695 `Browsing context with id ${contextId} is not top-level` 1696 ); 1697 1698 navigables.add(navigable); 1699 } 1700 1701 if (lazy.AppInfo.isAndroid) { 1702 // Bug 1840084: Add Android support for modifying the viewport. 1703 throw new lazy.error.UnsupportedOperationError( 1704 `Command not yet supported for ${lazy.AppInfo.name}` 1705 ); 1706 } 1707 1708 const viewportOverride = { 1709 devicePixelRatio, 1710 viewport, 1711 }; 1712 1713 const sessionDataItems = []; 1714 if (userContextIds !== null) { 1715 for (const userContext of userContexts) { 1716 // Prepare the list of navigables to update. 1717 lazy.UserContextManager.getTabsForUserContext(userContext).forEach( 1718 tab => { 1719 const contentBrowser = lazy.TabManager.getBrowserForTab(tab); 1720 navigables.add(contentBrowser.browsingContext); 1721 } 1722 ); 1723 sessionDataItems.push({ 1724 category: "viewport-overrides", 1725 moduleName: "_configuration", 1726 values: [viewportOverride], 1727 contextDescriptor: { 1728 type: lazy.ContextDescriptorType.UserContext, 1729 id: userContext, 1730 }, 1731 method: lazy.SessionDataMethod.Add, 1732 }); 1733 } 1734 } else { 1735 for (const navigable of navigables) { 1736 sessionDataItems.push({ 1737 category: "viewport-overrides", 1738 moduleName: "_configuration", 1739 values: [viewportOverride], 1740 contextDescriptor: { 1741 type: lazy.ContextDescriptorType.TopBrowsingContext, 1742 id: navigable.browserId, 1743 }, 1744 method: lazy.SessionDataMethod.Add, 1745 }); 1746 } 1747 } 1748 1749 if (sessionDataItems.length) { 1750 // TODO: Bug 1953079. Saving the viewport overrides in the session data works fine 1751 // with one session, but when we start supporting multiple BiDi session, we will 1752 // have to rethink this approach. 1753 await this.messageHandler.updateSessionData(sessionDataItems); 1754 } 1755 1756 const commands = []; 1757 1758 for (const navigable of navigables) { 1759 commands.push( 1760 this._updateNavigableViewport({ 1761 navigable, 1762 viewportOverride, 1763 }) 1764 ); 1765 } 1766 1767 await Promise.all(commands); 1768 } 1769 1770 /** 1771 * Traverses the history of a given context by a given delta. 1772 * 1773 * @param {object=} options 1774 * @param {string} options.context 1775 * Id of the browsing context. 1776 * @param {number} options.delta 1777 * The number of steps we have to traverse. 1778 * 1779 * @throws {InvalidArgumentError} 1780 * Raised if an argument is of an invalid type or value. 1781 * @throws {NoSuchFrameException} 1782 * When a context is not available. 1783 * @throws {NoSuchHistoryEntryError} 1784 * When a requested history entry does not exist. 1785 */ 1786 async traverseHistory(options = {}) { 1787 const { context: contextId, delta } = options; 1788 1789 lazy.assert.string( 1790 contextId, 1791 lazy.pprint`Expected "context" to be a string, got ${contextId}` 1792 ); 1793 1794 const context = this._getNavigable(contextId); 1795 1796 lazy.assert.topLevel( 1797 context, 1798 lazy.pprint`Browsing context with id ${contextId} is not top-level` 1799 ); 1800 1801 lazy.assert.integer( 1802 delta, 1803 lazy.pprint`Expected "delta" to be an integer, got ${delta}` 1804 ); 1805 1806 const sessionHistory = context.sessionHistory; 1807 const allSteps = sessionHistory.count; 1808 const currentIndex = sessionHistory.index; 1809 const targetIndex = currentIndex + delta; 1810 const validEntry = targetIndex >= 0 && targetIndex < allSteps; 1811 1812 if (!validEntry) { 1813 throw new lazy.error.NoSuchHistoryEntryError( 1814 `History entry with delta ${delta} not found` 1815 ); 1816 } 1817 1818 context.goToIndex(targetIndex); 1819 1820 // On some platforms the requested index isn't set immediately. 1821 await lazy.PollPromise( 1822 (resolve, reject) => { 1823 if (sessionHistory.index == targetIndex) { 1824 resolve(); 1825 } else { 1826 reject(); 1827 } 1828 }, 1829 { 1830 errorMessage: `History was not updated for index "${targetIndex}"`, 1831 timeout: TIMEOUT_SET_HISTORY_INDEX * lazy.getTimeoutMultiplier(), 1832 } 1833 ); 1834 } 1835 1836 /** 1837 * Start and await a navigation on the provided BrowsingContext. Returns a 1838 * promise which resolves when the navigation is done according to the provided 1839 * navigation strategy. 1840 * 1841 * @param {WebProgress} webProgress 1842 * The WebProgress instance to observe for this navigation. 1843 * @param {Function} startNavigationFn 1844 * A callback that starts a navigation. 1845 * @param {object} options 1846 * @param {string=} options.targetURI 1847 * The target URI for the navigation. 1848 * @param {WaitCondition} options.wait 1849 * The WaitCondition to use to wait for the navigation. 1850 * 1851 * @returns {Promise<BrowsingContextNavigateResult>} 1852 * A Promise that resolves to navigate results when the navigation is done. 1853 */ 1854 async #awaitNavigation(webProgress, startNavigationFn, options) { 1855 const { targetURI, wait } = options; 1856 1857 const context = webProgress.browsingContext; 1858 const browserId = context.browserId; 1859 1860 const resolveWhenCommitted = wait === WaitCondition.None; 1861 const listener = new lazy.ProgressListener(webProgress, { 1862 expectNavigation: true, 1863 navigationManager: this.messageHandler.navigationManager, 1864 resolveWhenCommitted, 1865 targetURI, 1866 // In case the webprogress is already navigating, always wait for an 1867 // explicit start flag. 1868 waitForExplicitStart: true, 1869 }); 1870 1871 const onDocumentInteractive = (evtName, wrappedEvt) => { 1872 if (webProgress.browsingContext.id !== wrappedEvt.contextId) { 1873 // Ignore load events for unrelated browsing contexts. 1874 return; 1875 } 1876 1877 if (wrappedEvt.readyState === "interactive") { 1878 listener.stopIfStarted(); 1879 } 1880 }; 1881 1882 const contextDescriptor = { 1883 type: lazy.ContextDescriptorType.TopBrowsingContext, 1884 id: browserId, 1885 }; 1886 1887 // For the Interactive wait condition, resolve as soon as 1888 // the document becomes interactive. 1889 if (wait === WaitCondition.Interactive) { 1890 await this.messageHandler.eventsDispatcher.on( 1891 "browsingContext._documentInteractive", 1892 contextDescriptor, 1893 onDocumentInteractive 1894 ); 1895 } 1896 1897 const navigationId = lazy.registerNavigationId({ 1898 contextDetails: { context: webProgress.browsingContext }, 1899 }); 1900 const navigated = listener.start(navigationId); 1901 1902 try { 1903 await startNavigationFn(); 1904 await navigated; 1905 1906 let url; 1907 if (wait === WaitCondition.None) { 1908 // If wait condition is None, the navigation resolved before the current 1909 // context has navigated. 1910 url = listener.targetURI.spec; 1911 } else { 1912 url = listener.currentURI.spec; 1913 } 1914 1915 return { 1916 navigation: navigationId, 1917 url, 1918 }; 1919 } catch (e) { 1920 // Get the current navigation object for the browsing context. 1921 const navigation = 1922 this.messageHandler.navigationManager.getNavigationForBrowsingContext( 1923 webProgress.browsingContext 1924 ); 1925 1926 // NavigationError with isBindingAborted represent navigations aborted by 1927 // another navigation. If the navigation was committed and matches the 1928 // navigationId, consider the navigation as successful. 1929 if ( 1930 e?.isNavigationError && 1931 e.isBindingAborted && 1932 navigation && 1933 navigation.committed && 1934 navigation.navigationId == navigationId 1935 ) { 1936 return { 1937 navigation: navigationId, 1938 url: navigation.url, 1939 }; 1940 } 1941 1942 // Otherwise, bubble the error from the Navigation helper. 1943 throw e; 1944 } finally { 1945 if (listener.isStarted) { 1946 listener.stop(); 1947 } 1948 listener.destroy(); 1949 1950 if (wait === WaitCondition.Interactive) { 1951 await this.messageHandler.eventsDispatcher.off( 1952 "browsingContext._documentInteractive", 1953 contextDescriptor, 1954 onDocumentInteractive 1955 ); 1956 } 1957 } 1958 } 1959 1960 /** 1961 * Wrapper around RootBiDiModule._emitEventForBrowsingContext to additionally 1962 * check that the payload of the event contains a valid `context` id. 1963 * 1964 * All browsingContext module events should have such a property set, and a 1965 * missing id usually indicates that the browsing context which triggered the 1966 * event is out of scope for the current WebDriver BiDi session (eg. chrome or 1967 * webextension). 1968 * 1969 * @param {string} browsingContextId 1970 * The ID of the browsing context to which the event should be emitted. 1971 * @param {string} eventName 1972 * The name of the event to be emitted. 1973 * @param {object} eventPayload 1974 * The payload to be sent with the event. 1975 * @param {number|string} eventPayload.context 1976 * A unique context id computed by the TabManager. 1977 */ 1978 #emitContextEventForBrowsingContext( 1979 browsingContextId, 1980 eventName, 1981 eventPayload 1982 ) { 1983 // All browsingContext events should include a context id in the payload. 1984 const { context = null } = eventPayload; 1985 if (context === null) { 1986 // If the context could not be found by the TabManager, the event is most 1987 // likely related to an unsupported context: eg chrome (bug 1722679) or 1988 // webextension (bug 1755014). 1989 lazy.logger.trace( 1990 `[${browsingContextId}] Skipping event ${eventName} because of a missing unique context id` 1991 ); 1992 return; 1993 } 1994 1995 this._emitEventForBrowsingContext( 1996 browsingContextId, 1997 eventName, 1998 eventPayload 1999 ); 2000 } 2001 2002 #hasConfigurationForContext(userContext) { 2003 const internalId = lazy.UserContextManager.getInternalIdById(userContext); 2004 const contextDescriptor = { 2005 type: lazy.ContextDescriptorType.UserContext, 2006 id: internalId, 2007 }; 2008 return this.messageHandler.sessionData.hasSessionData( 2009 "_configuration", 2010 undefined, 2011 contextDescriptor 2012 ); 2013 } 2014 2015 #onContextAttached = async (eventName, data = {}) => { 2016 if (this.#subscribedEvents.has("browsingContext.contextCreated")) { 2017 const { browsingContext, why } = data; 2018 2019 // Filter out top-level browsing contexts that are created because of a 2020 // cross-group navigation. 2021 if (why === "replace") { 2022 return; 2023 } 2024 2025 // TODO: Bug 1852941. We should also filter out events which are emitted 2026 // for DevTools frames. 2027 2028 // Filter out notifications for chrome context until support gets 2029 // added (bug 1722679). 2030 if (!browsingContext.webProgress) { 2031 return; 2032 } 2033 2034 // Filter out notifications for webextension contexts until support gets 2035 // added (bug 1755014). 2036 if (browsingContext.currentRemoteType === "extension") { 2037 return; 2038 } 2039 2040 const browsingContextInfo = getBrowsingContextInfo(browsingContext, { 2041 maxDepth: 0, 2042 }); 2043 2044 this.#emitContextEventForBrowsingContext( 2045 browsingContext.id, 2046 "browsingContext.contextCreated", 2047 browsingContextInfo 2048 ); 2049 2050 // This is an internal event is used by the script module 2051 // to ensure that "script.realmCreated" event is emitted 2052 // after "browsingContext.contextCreated". 2053 this.messageHandler.emitEvent( 2054 "browsingContext._contextCreatedEmitted", 2055 { browsingContext }, 2056 browsingContextInfo 2057 ); 2058 } 2059 }; 2060 2061 #onContextDiscarded = async (eventName, data = {}) => { 2062 if (this.#subscribedEvents.has("browsingContext.contextDestroyed")) { 2063 const { browsingContext, why } = data; 2064 2065 // Filter out top-level browsing contexts that are destroyed because of a 2066 // cross-group navigation. 2067 if (why === "replace") { 2068 return; 2069 } 2070 2071 // TODO: Bug 1852941. We should also filter out events which are emitted 2072 // for DevTools frames. 2073 2074 // Filter out notifications for chrome context until support gets 2075 // added (bug 1722679). 2076 if (!browsingContext.webProgress) { 2077 return; 2078 } 2079 2080 // Filter out notifications for webextension contexts until support gets 2081 // added (bug 1755014). 2082 if (browsingContext.currentRemoteType === "extension") { 2083 return; 2084 } 2085 2086 // If this event is for a child context whose top or parent context is also destroyed, 2087 // we don't need to send it, in this case the event for the top/parent context is enough. 2088 if ( 2089 browsingContext.parent && 2090 (browsingContext.top.isDiscarded || browsingContext.parent.isDiscarded) 2091 ) { 2092 return; 2093 } 2094 2095 const browsingContextInfo = getBrowsingContextInfo(browsingContext); 2096 2097 this.#emitContextEventForBrowsingContext( 2098 browsingContext.id, 2099 "browsingContext.contextDestroyed", 2100 browsingContextInfo 2101 ); 2102 } 2103 }; 2104 2105 #onDownloadEnd = async (eventName, data) => { 2106 if (this.#subscribedEvents.has("browsingContext.downloadEnd")) { 2107 const { 2108 canceled, 2109 contextId, 2110 filepath, 2111 navigableId, 2112 navigationId, 2113 timestamp, 2114 url, 2115 } = data; 2116 2117 const browsingContextInfo = { 2118 context: navigableId, 2119 navigation: navigationId, 2120 status: canceled 2121 ? DownloadEndStatus.canceled 2122 : DownloadEndStatus.complete, 2123 timestamp, 2124 url, 2125 }; 2126 2127 if (!canceled) { 2128 // Note: filepath should not be set for canceled downloads. 2129 // https://www.w3.org/TR/webdriver-bidi/#cddl-type-browsingcontextdownloadcanceledparams 2130 browsingContextInfo.filepath = filepath; 2131 } 2132 2133 this.#emitContextEventForBrowsingContext( 2134 contextId, 2135 "browsingContext.downloadEnd", 2136 browsingContextInfo 2137 ); 2138 } 2139 }; 2140 2141 #onDownloadStarted = async (eventName, data) => { 2142 if (this.#subscribedEvents.has("browsingContext.downloadWillBegin")) { 2143 const { 2144 contextId, 2145 navigationId, 2146 navigableId, 2147 suggestedFilename, 2148 timestamp, 2149 url, 2150 } = data; 2151 2152 const browsingContextInfo = { 2153 context: navigableId, 2154 navigation: navigationId, 2155 suggestedFilename, 2156 timestamp, 2157 url, 2158 }; 2159 2160 this.#emitContextEventForBrowsingContext( 2161 contextId, 2162 "browsingContext.downloadWillBegin", 2163 browsingContextInfo 2164 ); 2165 } 2166 }; 2167 2168 #onFragmentNavigated = async (eventName, data) => { 2169 if (this.#subscribedEvents.has("browsingContext.fragmentNavigated")) { 2170 const { contextId, navigationId, navigableId, url } = data; 2171 2172 const browsingContextInfo = { 2173 context: navigableId, 2174 navigation: navigationId, 2175 timestamp: Date.now(), 2176 url, 2177 }; 2178 2179 this.#emitContextEventForBrowsingContext( 2180 contextId, 2181 "browsingContext.fragmentNavigated", 2182 browsingContextInfo 2183 ); 2184 } 2185 }; 2186 2187 #onHistoryUpdated = async (eventName, data) => { 2188 if (this.#subscribedEvents.has("browsingContext.historyUpdated")) { 2189 const { contextId, navigableId, url } = data; 2190 2191 const browsingContextInfo = { 2192 context: navigableId, 2193 timestamp: Date.now(), 2194 url, 2195 }; 2196 2197 this.#emitContextEventForBrowsingContext( 2198 contextId, 2199 "browsingContext.historyUpdated", 2200 browsingContextInfo 2201 ); 2202 } 2203 }; 2204 2205 #onPromptClosed = (eventName, data) => { 2206 if (this.#subscribedEvents.has("browsingContext.userPromptClosed")) { 2207 const { contentBrowser, detail } = data; 2208 // TODO: Bug 2007385. Use only browsingContext from event details when the support for Android is added. 2209 const browsingContext = lazy.AppInfo.isAndroid 2210 ? contentBrowser.browsingContext 2211 : detail.browsingContext; 2212 2213 const navigableId = 2214 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 2215 2216 if (navigableId === null) { 2217 return; 2218 } 2219 2220 lazy.logger.trace( 2221 `[${browsingContext.id}] Prompt closed (type: "${ 2222 detail.promptType 2223 }", accepted: "${detail.accepted}")` 2224 ); 2225 2226 const params = { 2227 context: navigableId, 2228 accepted: detail.accepted, 2229 type: detail.promptType, 2230 userText: detail.userText, 2231 }; 2232 2233 this.#emitContextEventForBrowsingContext( 2234 browsingContext.id, 2235 "browsingContext.userPromptClosed", 2236 params 2237 ); 2238 } 2239 }; 2240 2241 #onPromptOpened = async (eventName, data) => { 2242 if (this.#subscribedEvents.has("browsingContext.userPromptOpened")) { 2243 const { contentBrowser, prompt } = data; 2244 const type = prompt.promptType; 2245 2246 // TODO: Bug 2007385. We can remove this fallback 2247 // when we have support for browsing context property on Android. 2248 const browsingContext = lazy.AppInfo.isAndroid 2249 ? contentBrowser.browsingContext 2250 : data.browsingContext; 2251 2252 prompt.getText().then(text => { 2253 // We need the text to identify a user prompt when it gets 2254 // randomly opened. Because on Android the text is asynchronously 2255 // retrieved lets delay the logging without making the handler async. 2256 lazy.logger.trace( 2257 `[${browsingContext.id}] Prompt opened (type: "${ 2258 prompt.promptType 2259 }", text: "${text}")` 2260 ); 2261 }); 2262 2263 // Do not send opened event for unsupported prompt types. 2264 if (!(type in UserPromptType)) { 2265 lazy.logger.trace(`Prompt type "${type}" not supported`); 2266 return; 2267 } 2268 2269 const navigableId = 2270 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 2271 2272 const session = lazy.getWebDriverSessionById( 2273 this.messageHandler.sessionId 2274 ); 2275 const handlerConfig = session.userPromptHandler.getPromptHandler(type); 2276 2277 const eventPayload = { 2278 context: navigableId, 2279 handler: handlerConfig.handler, 2280 message: await prompt.getText(), 2281 type, 2282 }; 2283 2284 if (type === "prompt") { 2285 eventPayload.defaultValue = await prompt.getInputText(); 2286 } 2287 2288 this.#emitContextEventForBrowsingContext( 2289 browsingContext.id, 2290 "browsingContext.userPromptOpened", 2291 eventPayload 2292 ); 2293 } 2294 }; 2295 2296 #onNavigationCommitted = async (eventName, data) => { 2297 if (this.#subscribedEvents.has("browsingContext.navigationCommitted")) { 2298 const { contextId, navigableId, navigationId, url } = data; 2299 2300 const eventPayload = { 2301 context: navigableId, 2302 navigation: navigationId, 2303 timestamp: Date.now(), 2304 url, 2305 }; 2306 2307 this.#emitContextEventForBrowsingContext( 2308 contextId, 2309 "browsingContext.navigationCommitted", 2310 eventPayload 2311 ); 2312 } 2313 }; 2314 2315 #onNavigationFailed = async (eventName, data) => { 2316 if (this.#subscribedEvents.has("browsingContext.navigationFailed")) { 2317 const { contextId, navigableId, navigationId, url } = data; 2318 2319 const eventPayload = { 2320 context: navigableId, 2321 navigation: navigationId, 2322 timestamp: Date.now(), 2323 url, 2324 }; 2325 2326 this.#emitContextEventForBrowsingContext( 2327 contextId, 2328 "browsingContext.navigationFailed", 2329 eventPayload 2330 ); 2331 } 2332 }; 2333 2334 #onNavigationStarted = async (eventName, data) => { 2335 if (this.#subscribedEvents.has("browsingContext.navigationStarted")) { 2336 const { contextId, navigableId, navigationId, url } = data; 2337 2338 const eventPayload = { 2339 context: navigableId, 2340 navigation: navigationId, 2341 timestamp: Date.now(), 2342 url, 2343 }; 2344 2345 this.#emitContextEventForBrowsingContext( 2346 contextId, 2347 "browsingContext.navigationStarted", 2348 eventPayload 2349 ); 2350 } 2351 }; 2352 2353 #onPageHideEvent = (name, eventPayload) => { 2354 const { context } = eventPayload; 2355 if (context.parent) { 2356 this.#onContextDiscarded("windowglobal-pagehide", { 2357 browsingContext: context, 2358 }); 2359 } 2360 }; 2361 2362 #stopListeningToContextEvent(event) { 2363 this.#subscribedEvents.delete(event); 2364 2365 const hasContextEvent = 2366 this.#subscribedEvents.has("browsingContext.contextCreated") || 2367 this.#subscribedEvents.has("browsingContext.contextDestroyed"); 2368 2369 if (!hasContextEvent) { 2370 this.#contextListener.stopListening(); 2371 } 2372 } 2373 2374 #stopListeningToNavigationEvent(event) { 2375 this.#subscribedEvents.delete(event); 2376 2377 const hasNavigationEvent = 2378 this.#subscribedEvents.has("browsingContext.downloadEnd") || 2379 this.#subscribedEvents.has("browsingContext.downloadWillBegin") || 2380 this.#subscribedEvents.has("browsingContext.fragmentNavigated") || 2381 this.#subscribedEvents.has("browsingContext.historyUpdated") || 2382 this.#subscribedEvents.has("browsingContext.navigationFailed") || 2383 this.#subscribedEvents.has("browsingContext.navigationStarted"); 2384 2385 if (!hasNavigationEvent) { 2386 this.#navigationListener.stopListening(); 2387 } 2388 } 2389 2390 #stopListeningToPromptEvent(event) { 2391 this.#subscribedEvents.delete(event); 2392 2393 const hasPromptEvent = 2394 this.#subscribedEvents.has("browsingContext.userPromptClosed") || 2395 this.#subscribedEvents.has("browsingContext.userPromptOpened"); 2396 2397 if (!hasPromptEvent) { 2398 this.#promptListener.stopListening(); 2399 } 2400 } 2401 2402 #subscribeEvent(event) { 2403 switch (event) { 2404 case "browsingContext.contextCreated": 2405 case "browsingContext.contextDestroyed": { 2406 this.#contextListener.startListening(); 2407 this.#subscribedEvents.add(event); 2408 break; 2409 } 2410 case "browsingContext.downloadEnd": 2411 case "browsingContext.downloadWillBegin": 2412 case "browsingContext.fragmentNavigated": 2413 case "browsingContext.historyUpdated": 2414 case "browsingContext.navigationCommitted": 2415 case "browsingContext.navigationFailed": 2416 case "browsingContext.navigationStarted": { 2417 this.#navigationListener.startListening(); 2418 this.#subscribedEvents.add(event); 2419 break; 2420 } 2421 case "browsingContext.userPromptClosed": 2422 case "browsingContext.userPromptOpened": { 2423 this.#promptListener.startListening(); 2424 this.#subscribedEvents.add(event); 2425 break; 2426 } 2427 } 2428 } 2429 2430 #unsubscribeEvent(event) { 2431 switch (event) { 2432 case "browsingContext.contextCreated": 2433 case "browsingContext.contextDestroyed": { 2434 this.#stopListeningToContextEvent(event); 2435 break; 2436 } 2437 case "browsingContext.downloadEnd": 2438 case "browsingContext.downloadWillBegin": 2439 case "browsingContext.fragmentNavigated": 2440 case "browsingContext.historyUpdated": 2441 case "browsingContext.navigationCommitted": 2442 case "browsingContext.navigationFailed": 2443 case "browsingContext.navigationStarted": { 2444 this.#stopListeningToNavigationEvent(event); 2445 break; 2446 } 2447 case "browsingContext.userPromptClosed": 2448 case "browsingContext.userPromptOpened": { 2449 this.#stopListeningToPromptEvent(event); 2450 break; 2451 } 2452 } 2453 } 2454 2455 #waitForVisibilityState(browsingContext, expectedState, options = {}) { 2456 const { timeout } = options; 2457 return this._forwardToWindowGlobal( 2458 "_awaitVisibilityState", 2459 browsingContext.id, 2460 { value: expectedState, timeout }, 2461 { retryOnAbort: true } 2462 ); 2463 } 2464 2465 /** 2466 * Internal commands 2467 */ 2468 2469 _applySessionData(params) { 2470 // TODO: Bug 1775231. Move this logic to a shared module or an abstract 2471 // class. 2472 const { category } = params; 2473 if (category === "event") { 2474 const filteredSessionData = params.sessionData.filter(item => 2475 this.messageHandler.matchesContext(item.contextDescriptor) 2476 ); 2477 for (const event of this.#subscribedEvents.values()) { 2478 const hasSessionItem = filteredSessionData.some( 2479 item => item.value === event 2480 ); 2481 // If there are no session items for this context, we should unsubscribe from the event. 2482 if (!hasSessionItem) { 2483 this.#unsubscribeEvent(event); 2484 } 2485 } 2486 2487 // Subscribe to all events, which have an item in SessionData. 2488 for (const { value } of filteredSessionData) { 2489 this.#subscribeEvent(value); 2490 } 2491 } 2492 } 2493 2494 /** 2495 * Communicate to this module that the _ConfigurationModule is done. 2496 * 2497 * @param {BrowsingContext} navigable 2498 * Browsing context for which the configuration completed. 2499 */ 2500 _onConfigurationComplete({ navigable }) { 2501 const browser = navigable.embedderElement; 2502 2503 if (!this.#blockedCreateCommands.has(browser)) { 2504 this.#blockedCreateCommands.set(browser, Promise.withResolvers()); 2505 } 2506 2507 const blocker = this.#blockedCreateCommands.get(browser); 2508 blocker.resolve(); 2509 } 2510 2511 /** 2512 * Update the viewport of the navigable. 2513 * 2514 * @param {object} options 2515 * @param {BrowsingContext} options.navigable 2516 * Navigable whose viewport should be updated. 2517 * @param {ViewportOverride} options.viewportOverride 2518 * Object which holds viewport settings 2519 * which should be applied. 2520 */ 2521 async _updateNavigableViewport(options) { 2522 const { navigable, viewportOverride } = options; 2523 const { devicePixelRatio, viewport } = viewportOverride; 2524 2525 const browser = navigable.embedderElement; 2526 const currentHeight = browser.clientHeight; 2527 const currentWidth = browser.clientWidth; 2528 2529 let targetHeight, targetWidth; 2530 if (viewport === undefined) { 2531 // Don't modify the viewport's size. 2532 targetHeight = currentHeight; 2533 targetWidth = currentWidth; 2534 } else if (viewport === null) { 2535 // Reset viewport to the original dimensions. 2536 targetHeight = browser.parentElement.clientHeight; 2537 targetWidth = browser.parentElement.clientWidth; 2538 2539 browser.style.removeProperty("height"); 2540 browser.style.removeProperty("width"); 2541 } else { 2542 const { height, width } = viewport; 2543 2544 targetHeight = height; 2545 targetWidth = width; 2546 2547 browser.style.setProperty("height", targetHeight + "px"); 2548 browser.style.setProperty("width", targetWidth + "px"); 2549 } 2550 2551 if (devicePixelRatio !== undefined) { 2552 if (devicePixelRatio !== null) { 2553 navigable.overrideDPPX = devicePixelRatio; 2554 } else { 2555 // Will reset to use the global default scaling factor. 2556 navigable.overrideDPPX = 0; 2557 } 2558 } 2559 2560 if (targetHeight !== currentHeight || targetWidth !== currentWidth) { 2561 if (!navigable.isActive) { 2562 // Force a synchronous update of the remote browser dimensions so that 2563 // background tabs get resized. 2564 browser.ownerDocument.synchronouslyUpdateRemoteBrowserDimensions( 2565 /* aIncludeInactive = */ true 2566 ); 2567 } 2568 // Wait until the viewport has been resized 2569 await this._forwardToWindowGlobal( 2570 "_awaitViewportDimensions", 2571 navigable.id, 2572 { 2573 height: targetHeight, 2574 width: targetWidth, 2575 }, 2576 { retryOnAbort: true } 2577 ); 2578 } 2579 } 2580 2581 static get supportedEvents() { 2582 return [ 2583 "browsingContext.contextCreated", 2584 "browsingContext.contextDestroyed", 2585 "browsingContext.domContentLoaded", 2586 "browsingContext.downloadEnd", 2587 "browsingContext.downloadWillBegin", 2588 "browsingContext.fragmentNavigated", 2589 "browsingContext.historyUpdated", 2590 "browsingContext.load", 2591 "browsingContext.navigationCommitted", 2592 "browsingContext.navigationFailed", 2593 "browsingContext.navigationStarted", 2594 "browsingContext.userPromptClosed", 2595 "browsingContext.userPromptOpened", 2596 ]; 2597 } 2598 } 2599 2600 /** 2601 * Get the WebDriver BiDi browsing context information. 2602 * 2603 * @param {BrowsingContext} context 2604 * The browsing context to get the information from. 2605 * @param {object=} options 2606 * @param {boolean=} options.includeParentId 2607 * Flag that indicates if the parent ID should be included. 2608 * Defaults to true. 2609 * @param {number=} options.maxDepth 2610 * Depth of the browsing context tree to traverse. If not specified 2611 * the whole tree is returned. 2612 * 2613 * @returns {BrowsingContextInfo} 2614 * The information about the browsing context. 2615 */ 2616 export const getBrowsingContextInfo = (context, options = {}) => { 2617 const { includeParentId = true, maxDepth = null } = options; 2618 2619 let children = null; 2620 if (maxDepth === null || maxDepth > 0) { 2621 // Bug 1996311: When executed for chrome browsing contexts as 2622 // well include embedded browsers and their browsing context tree. 2623 children = context.children.map(childContext => 2624 getBrowsingContextInfo(childContext, { 2625 maxDepth: maxDepth === null ? maxDepth : maxDepth - 1, 2626 includeParentId: false, 2627 }) 2628 ); 2629 } 2630 2631 const chromeWindow = 2632 lazy.windowManager.getChromeWindowForBrowsingContext(context); 2633 const originalOpener = 2634 context.crossGroupOpener !== null 2635 ? lazy.NavigableManager.getIdForBrowsingContext(context.crossGroupOpener) 2636 : null; 2637 const userContext = lazy.UserContextManager.getIdByBrowsingContext(context); 2638 2639 const contextInfo = { 2640 children, 2641 context: lazy.NavigableManager.getIdForBrowsingContext(context), 2642 // TODO: Bug 1904641. If a browsing context was not tracked in TabManager, 2643 // because it was created and discarded before the WebDriver BiDi session was 2644 // started, we get undefined as id for this browsing context. 2645 // We should remove this condition, when we can provide a correct id here. 2646 originalOpener: originalOpener === undefined ? null : originalOpener, 2647 url: context.currentURI.spec, 2648 userContext, 2649 clientWindow: lazy.windowManager.getIdForWindow(chromeWindow), 2650 }; 2651 2652 if (includeParentId) { 2653 // Only emit the parent id for the top-most browsing context. 2654 const parentId = lazy.NavigableManager.getIdForBrowsingContext( 2655 context.parent 2656 ); 2657 contextInfo.parent = parentId; 2658 } 2659 2660 if (lazy.RemoteAgent.allowSystemAccess) { 2661 contextInfo["moz:scope"] = context.isContent 2662 ? MozContextScope.CONTENT 2663 : MozContextScope.CHROME; 2664 2665 if ("name" in context) { 2666 contextInfo["moz:name"] = context.name; 2667 } 2668 } 2669 2670 return contextInfo; 2671 }; 2672 2673 export const browsingContext = BrowsingContextModule;