head.js (41300B)
1 var { PermissionTestUtils } = ChromeUtils.importESModule( 2 "resource://testing-common/PermissionTestUtils.sys.mjs" 3 ); 4 5 const PREF_PERMISSION_FAKE = "media.navigator.permission.fake"; 6 const PREF_AUDIO_LOOPBACK = "media.audio_loopback_dev"; 7 const PREF_VIDEO_LOOPBACK = "media.video_loopback_dev"; 8 const PREF_FAKE_STREAMS = "media.navigator.streams.fake"; 9 const PREF_FOCUS_SOURCE = "media.getusermedia.window.focus_source.enabled"; 10 11 const STATE_CAPTURE_ENABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED; 12 const STATE_CAPTURE_DISABLED = Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED; 13 14 const ALLOW_SILENCING_NOTIFICATIONS = Services.prefs.getBoolPref( 15 "privacy.webrtc.allowSilencingNotifications", 16 false 17 ); 18 19 const SHOW_GLOBAL_MUTE_TOGGLES = Services.prefs.getBoolPref( 20 "privacy.webrtc.globalMuteToggles", 21 false 22 ); 23 24 const SHOW_ALWAYS_ASK = Services.prefs.getBoolPref( 25 "permissions.media.show_always_ask.enabled", 26 false 27 ); 28 29 let IsIndicatorDisabled = 30 AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) && 31 !Services.prefs.getBoolPref( 32 "privacy.webrtc.showIndicatorsOnMacos14AndAbove", 33 false 34 ); 35 36 const INDICATOR_PATH = "chrome://browser/content/webrtcIndicator.xhtml"; 37 38 const IS_MAC = AppConstants.platform == "macosx"; 39 40 const SHARE_SCREEN = 1; 41 const SHARE_WINDOW = 2; 42 43 let observerTopics = [ 44 "getUserMedia:response:allow", 45 "getUserMedia:revoke", 46 "getUserMedia:response:deny", 47 "getUserMedia:request", 48 "recording-device-events", 49 "recording-window-ended", 50 ]; 51 52 // Structured hierarchy of subframes. Keys are frame id:s, The children member 53 // contains nested sub frames if any. The noTest member make a frame be ignored 54 // for testing if true. 55 let gObserveSubFrames = {}; 56 // Object of subframes to test. Each element contains the members bc and id, for 57 // the frames BrowsingContext and id, respectively. 58 let gSubFramesToTest = []; 59 let gBrowserContextsToObserve = []; 60 61 function whenDelayedStartupFinished(aWindow) { 62 return TestUtils.topicObserved( 63 "browser-delayed-startup-finished", 64 subject => subject == aWindow 65 ); 66 } 67 68 function promiseIndicatorWindow() { 69 let startTime = ChromeUtils.now(); 70 71 return new Promise(resolve => { 72 Services.obs.addObserver(function obs(win) { 73 win.addEventListener( 74 "load", 75 function () { 76 if (win.location.href !== INDICATOR_PATH) { 77 info("ignoring a window with this url: " + win.location.href); 78 return; 79 } 80 81 Services.obs.removeObserver(obs, "domwindowopened"); 82 executeSoon(() => { 83 ChromeUtils.addProfilerMarker("promiseIndicatorWindow", { 84 startTime, 85 category: "Test", 86 }); 87 resolve(win); 88 }); 89 }, 90 { once: true } 91 ); 92 }, "domwindowopened"); 93 }); 94 } 95 96 async function assertWebRTCIndicatorStatus(expected) { 97 let ui = ChromeUtils.importESModule( 98 "resource:///modules/webrtcUI.sys.mjs" 99 ).webrtcUI; 100 let expectedState = expected ? "visible" : "hidden"; 101 let msg = "WebRTC indicator " + expectedState; 102 if (!expected && ui.showGlobalIndicator) { 103 // It seems the global indicator is not always removed synchronously 104 // in some cases. 105 await TestUtils.waitForCondition( 106 () => !ui.showGlobalIndicator, 107 "waiting for the global indicator to be hidden" 108 ); 109 } 110 is(ui.showGlobalIndicator, !!expected, msg); 111 112 let expectVideo = false, 113 expectAudio = false, 114 expectScreen = ""; 115 if (expected && !IsIndicatorDisabled) { 116 if (expected.video) { 117 expectVideo = true; 118 } 119 if (expected.audio) { 120 expectAudio = true; 121 } 122 if (expected.screen) { 123 expectScreen = expected.screen; 124 } 125 } 126 is( 127 Boolean(ui.showCameraIndicator), 128 expectVideo, 129 "camera global indicator as expected" 130 ); 131 is( 132 Boolean(ui.showMicrophoneIndicator), 133 expectAudio, 134 "microphone global indicator as expected" 135 ); 136 is( 137 ui.showScreenSharingIndicator, 138 expectScreen, 139 "screen global indicator as expected" 140 ); 141 142 for (let win of Services.wm.getEnumerator("navigator:browser")) { 143 let menu = win.document.getElementById("tabSharingMenu"); 144 is( 145 !!menu && !menu.hidden, 146 !!expected, 147 "WebRTC menu should be " + expectedState 148 ); 149 } 150 151 if (!expected) { 152 let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"); 153 if (win) { 154 await new Promise(resolve => { 155 win.addEventListener("unload", function listener(e) { 156 if (e.target == win.document) { 157 win.removeEventListener("unload", listener); 158 executeSoon(resolve); 159 } 160 }); 161 }); 162 } 163 } 164 165 let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator"); 166 let hasWindow = indicator.hasMoreElements(); 167 is(hasWindow, !!expected, "popup " + msg); 168 if (hasWindow) { 169 let document = indicator.getNext().document; 170 let docElt = document.documentElement; 171 172 if (document.readyState != "complete") { 173 info("Waiting for the sharing indicator's document to load"); 174 await new Promise(resolve => { 175 document.addEventListener( 176 "readystatechange", 177 function onReadyStateChange() { 178 if (document.readyState != "complete") { 179 return; 180 } 181 document.removeEventListener( 182 "readystatechange", 183 onReadyStateChange 184 ); 185 executeSoon(resolve); 186 } 187 ); 188 }); 189 } 190 191 if (expected.screen && expected.screen.startsWith("Window")) { 192 // These tests were originally written to express window sharing by 193 // having expected.screen start with "Window". This meant that the 194 // legacy indicator is expected to have the "sharingscreen" attribute 195 // set to true when sharing a window. 196 // 197 // The new indicator, however, differentiates between screen, window 198 // and browser window sharing. If we're using the new indicator, we 199 // update the expectations accordingly. This can be removed once we 200 // are able to remove the tests for the legacy indicator. 201 expected.screen = null; 202 expected.window = true; 203 } 204 205 if (!SHOW_GLOBAL_MUTE_TOGGLES) { 206 expected.video = false; 207 expected.audio = false; 208 209 let visible = docElt.getAttribute("visible") == "true"; 210 211 if (!expected.screen && !expected.window && !expected.browserwindow) { 212 ok(!visible, "Indicator should not be visible in this configuation."); 213 } else { 214 ok(visible, "Indicator should be visible."); 215 } 216 } 217 218 for (let item of ["video", "audio", "screen", "window", "browserwindow"]) { 219 let expectedValue; 220 221 expectedValue = expected && expected[item] ? "true" : null; 222 223 is( 224 docElt.getAttribute("sharing" + item), 225 expectedValue, 226 item + " global indicator attribute as expected" 227 ); 228 } 229 230 ok(!indicator.hasMoreElements(), "only one global indicator window"); 231 } 232 } 233 234 function promiseNotificationShown(notification) { 235 let win = notification.browser.ownerGlobal; 236 if (win.PopupNotifications.panel.state == "open") { 237 return Promise.resolve(); 238 } 239 let panelPromise = BrowserTestUtils.waitForPopupEvent( 240 win.PopupNotifications.panel, 241 "shown" 242 ); 243 notification.reshow(); 244 return panelPromise; 245 } 246 247 function ignoreEvent(aSubject, aTopic, aData) { 248 // With e10s disabled, our content script receives notifications for the 249 // preview displayed in our screen sharing permission prompt; ignore them. 250 const kBrowserURL = AppConstants.BROWSER_CHROME_URL; 251 const nsIPropertyBag = Ci.nsIPropertyBag; 252 if ( 253 aTopic == "recording-device-events" && 254 aSubject.QueryInterface(nsIPropertyBag).getProperty("requestURL") == 255 kBrowserURL 256 ) { 257 return true; 258 } 259 if (aTopic == "recording-window-ended") { 260 let win = Services.wm.getOuterWindowWithId(aData).top; 261 if (win.document.documentURI == kBrowserURL) { 262 return true; 263 } 264 } 265 return false; 266 } 267 268 function expectObserverCalledInProcess(aTopic, aCount = 1) { 269 let promises = []; 270 for (let count = aCount; count > 0; count--) { 271 promises.push(TestUtils.topicObserved(aTopic, ignoreEvent)); 272 } 273 return promises; 274 } 275 276 function expectObserverCalled( 277 aTopic, 278 aCount = 1, 279 browser = gBrowser.selectedBrowser 280 ) { 281 if (!gMultiProcessBrowser) { 282 return expectObserverCalledInProcess(aTopic, aCount); 283 } 284 285 let browsingContext = Element.isInstance(browser) 286 ? browser.browsingContext 287 : browser; 288 289 return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic, aCount); 290 } 291 292 // This is a special version of expectObserverCalled that should only 293 // be used when expecting a notification upon closing a window. It uses 294 // the per-process message manager instead of actors to send the 295 // notifications. 296 function expectObserverCalledOnClose( 297 aTopic, 298 aCount = 1, 299 browser = gBrowser.selectedBrowser 300 ) { 301 if (!gMultiProcessBrowser) { 302 return expectObserverCalledInProcess(aTopic, aCount); 303 } 304 305 let browsingContext = Element.isInstance(browser) 306 ? browser.browsingContext 307 : browser; 308 309 return new Promise(resolve => { 310 BrowserTestUtils.sendAsyncMessage( 311 browsingContext, 312 "BrowserTestUtils:ObserveTopic", 313 { 314 topic: aTopic, 315 count: 1, 316 filterFunctionSource: ((subject, topic) => { 317 Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", { 318 topic, 319 }); 320 return true; 321 }).toSource(), 322 } 323 ); 324 325 function observerCalled(message) { 326 if (message.data.topic == aTopic) { 327 Services.ppmm.removeMessageListener( 328 "WebRTCTest:ObserverCalled", 329 observerCalled 330 ); 331 resolve(); 332 } 333 } 334 Services.ppmm.addMessageListener( 335 "WebRTCTest:ObserverCalled", 336 observerCalled 337 ); 338 }); 339 } 340 341 function promiseMessage( 342 aMessage, 343 aAction, 344 aCount = 1, 345 browser = gBrowser.selectedBrowser 346 ) { 347 let startTime = ChromeUtils.now(); 348 let promise = ContentTask.spawn( 349 browser, 350 [aMessage, aCount], 351 async function ([expectedMessage, expectedCount]) { 352 return new Promise(resolve => { 353 function listenForMessage({ data }) { 354 if ( 355 (!expectedMessage || data == expectedMessage) && 356 --expectedCount == 0 357 ) { 358 content.removeEventListener("message", listenForMessage); 359 resolve(data); 360 } 361 } 362 content.addEventListener("message", listenForMessage); 363 }); 364 } 365 ); 366 if (aAction) { 367 aAction(); 368 } 369 return promise.then(data => { 370 ChromeUtils.addProfilerMarker( 371 "promiseMessage", 372 { startTime, category: "Test" }, 373 data 374 ); 375 return data; 376 }); 377 } 378 379 function promisePopupNotificationShown(aName, aAction, aWindow = window) { 380 let startTime = ChromeUtils.now(); 381 return new Promise(resolve => { 382 aWindow.PopupNotifications.panel.addEventListener( 383 "popupshown", 384 function () { 385 ok( 386 !!aWindow.PopupNotifications.getNotification(aName), 387 aName + " notification shown" 388 ); 389 ok(aWindow.PopupNotifications.isPanelOpen, "notification panel open"); 390 ok( 391 !!aWindow.PopupNotifications.panel.firstElementChild, 392 "notification panel populated" 393 ); 394 395 executeSoon(() => { 396 ChromeUtils.addProfilerMarker( 397 "promisePopupNotificationShown", 398 { startTime, category: "Test" }, 399 aName 400 ); 401 resolve(); 402 }); 403 }, 404 { once: true } 405 ); 406 407 if (aAction) { 408 aAction(); 409 } 410 }); 411 } 412 413 async function promisePopupNotification(aName) { 414 return TestUtils.waitForCondition( 415 () => PopupNotifications.getNotification(aName), 416 aName + " notification appeared" 417 ); 418 } 419 420 async function promiseNoPopupNotification(aName) { 421 return TestUtils.waitForCondition( 422 () => !PopupNotifications.getNotification(aName), 423 aName + " notification removed" 424 ); 425 } 426 427 const kActionAlways = 1; 428 const kActionDeny = 2; 429 const kActionNever = 3; 430 431 async function activateSecondaryAction(aAction) { 432 let notification = PopupNotifications.panel.firstElementChild; 433 switch (aAction) { 434 case kActionNever: 435 if (notification.notification.secondaryActions.length > 1) { 436 // "Always Block" is the first (and only) item in the menupopup. 437 await Promise.all([ 438 BrowserTestUtils.waitForEvent(notification.menupopup, "popupshown"), 439 notification.menubutton.click(), 440 ]); 441 notification.menupopup.querySelector("menuitem").click(); 442 return; 443 } 444 if (!notification.checkbox.checked) { 445 notification.checkbox.click(); 446 } 447 // fallthrough 448 case kActionDeny: 449 notification.secondaryButton.click(); 450 break; 451 case kActionAlways: 452 if (!notification.checkbox.checked) { 453 notification.checkbox.click(); 454 } 455 notification.button.click(); 456 break; 457 } 458 } 459 460 async function getMediaCaptureState() { 461 let startTime = ChromeUtils.now(); 462 463 function gatherBrowsingContexts(aBrowsingContext) { 464 let list = [aBrowsingContext]; 465 466 let children = aBrowsingContext.children; 467 for (let child of children) { 468 list.push(...gatherBrowsingContexts(child)); 469 } 470 471 return list; 472 } 473 474 function combine(x, y) { 475 if ( 476 x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED || 477 y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED 478 ) { 479 return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED; 480 } 481 if ( 482 x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED || 483 y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED 484 ) { 485 return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED; 486 } 487 return Ci.nsIMediaManagerService.STATE_NOCAPTURE; 488 } 489 490 let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE; 491 let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE; 492 let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE; 493 let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE; 494 let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE; 495 496 for (let bc of gatherBrowsingContexts( 497 gBrowser.selectedBrowser.browsingContext 498 )) { 499 let state = await SpecialPowers.spawn(bc, [], async function () { 500 let mediaManagerService = Cc[ 501 "@mozilla.org/mediaManagerService;1" 502 ].getService(Ci.nsIMediaManagerService); 503 504 let hasCamera = {}; 505 let hasMicrophone = {}; 506 let hasScreenShare = {}; 507 let hasWindowShare = {}; 508 let hasBrowserShare = {}; 509 let devices = {}; 510 mediaManagerService.mediaCaptureWindowState( 511 content, 512 hasCamera, 513 hasMicrophone, 514 hasScreenShare, 515 hasWindowShare, 516 hasBrowserShare, 517 devices, 518 false 519 ); 520 521 return { 522 video: hasCamera.value, 523 audio: hasMicrophone.value, 524 screen: hasScreenShare.value, 525 window: hasWindowShare.value, 526 browser: hasBrowserShare.value, 527 }; 528 }); 529 530 video = combine(state.video, video); 531 audio = combine(state.audio, audio); 532 screen = combine(state.screen, screen); 533 window = combine(state.window, window); 534 browser = combine(state.browser, browser); 535 } 536 537 let result = {}; 538 539 if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { 540 result.video = true; 541 } 542 if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { 543 result.audio = true; 544 } 545 546 if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { 547 result.screen = "Screen"; 548 } else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { 549 result.window = true; 550 } else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) { 551 result.browserwindow = true; 552 } 553 554 ChromeUtils.addProfilerMarker("getMediaCaptureState", { 555 startTime, 556 category: "Test", 557 }); 558 return result; 559 } 560 561 async function stopSharing( 562 aType = "camera", 563 aShouldKeepSharing = false, 564 aFrameBC, 565 aWindow = window 566 ) { 567 let promiseRecordingEvent = expectObserverCalled( 568 "recording-device-events", 569 1, 570 aFrameBC 571 ); 572 let observerPromise1 = expectObserverCalled( 573 "getUserMedia:revoke", 574 1, 575 aFrameBC 576 ); 577 578 // If we are stopping screen sharing and expect to still have another stream, 579 // "recording-window-ended" won't be fired. 580 let observerPromise2 = null; 581 if (!aShouldKeepSharing) { 582 observerPromise2 = expectObserverCalled( 583 "recording-window-ended", 584 1, 585 aFrameBC 586 ); 587 } 588 589 await revokePermission(aType, aShouldKeepSharing, aFrameBC, aWindow); 590 await promiseRecordingEvent; 591 await observerPromise1; 592 await observerPromise2; 593 594 if (!aShouldKeepSharing) { 595 await checkNotSharing(); 596 } 597 } 598 599 async function revokePermission( 600 aType = "camera", 601 aShouldKeepSharing = false, 602 aFrameBC, 603 aWindow = window 604 ) { 605 aWindow.gPermissionPanel._identityPermissionBox.click(); 606 let popup = aWindow.gPermissionPanel._permissionPopup; 607 // If the popup gets hidden before being shown, by stray focus/activate 608 // events, don't bother failing the test. It's enough to know that we 609 // started showing the popup. 610 let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden"); 611 let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown"); 612 await Promise.race([hiddenEvent, shownEvent]); 613 let doc = aWindow.document; 614 let permissions = doc.getElementById("permission-popup-permission-list"); 615 let cancelButton = permissions.querySelector( 616 ".permission-popup-permission-icon." + 617 aType + 618 "-icon ~ " + 619 ".permission-popup-permission-remove-button" 620 ); 621 622 cancelButton.click(); 623 popup.hidePopup(); 624 625 if (!aShouldKeepSharing) { 626 await checkNotSharing(); 627 } 628 } 629 630 function getBrowsingContextForFrame(aBrowsingContext, aFrameId) { 631 if (!aFrameId) { 632 return aBrowsingContext; 633 } 634 635 return SpecialPowers.spawn(aBrowsingContext, [aFrameId], frameId => { 636 return content.document.getElementById(frameId).browsingContext; 637 }); 638 } 639 640 async function getBrowsingContextsAndFrameIdsForSubFrames( 641 aBrowsingContext, 642 aSubFrames 643 ) { 644 let pendingBrowserSubFrames = [ 645 { bc: aBrowsingContext, subFrames: aSubFrames }, 646 ]; 647 let browsingContextsAndFrames = []; 648 while (pendingBrowserSubFrames.length) { 649 let { bc, subFrames } = pendingBrowserSubFrames.shift(); 650 for (let id of Object.keys(subFrames)) { 651 let subBc = await getBrowsingContextForFrame(bc, id); 652 if (subFrames[id].children) { 653 pendingBrowserSubFrames.push({ 654 bc: subBc, 655 subFrames: subFrames[id].children, 656 }); 657 } 658 if (subFrames[id].noTest) { 659 continue; 660 } 661 let observeBC = subFrames[id].observe ? subBc : undefined; 662 browsingContextsAndFrames.push({ bc: subBc, id, observeBC }); 663 } 664 } 665 return browsingContextsAndFrames; 666 } 667 668 /** 669 * Test helper for getUserMedia calls. 670 * 671 * @param {boolean} aRequestAudio - Whether to request audio 672 * @param {boolean} aRequestVideo - Whether to request video 673 * @param {string} aFrameId - The ID of the frame 674 * @param {string} aType - The type of screen sharing. 675 * @param {BrowsingContext} aBrowsingContext - The browsing context 676 * @param {boolean} [aBadDevice=false] - Whether to use a bad device 677 * @param {boolean} [viaButtonClick=false] - Whether to call gUM directly or to 678 * request via simulated button click. 679 * @returns {Promise} - Resolves when the gUM request has been made. 680 */ 681 async function promiseRequestDevice( 682 aRequestAudio, 683 aRequestVideo, 684 aFrameId, 685 aType, 686 aBrowsingContext, 687 aBadDevice = false, 688 viaButtonClick = false 689 ) { 690 info("requesting devices"); 691 let bc = 692 aBrowsingContext ?? 693 (await getBrowsingContextForFrame(gBrowser.selectedBrowser, aFrameId)); 694 695 if (viaButtonClick) { 696 return SpecialPowers.spawn( 697 bc, 698 [{ aRequestAudio, aRequestVideo, aType, aBadDevice }], 699 async function (args) { 700 let global = content.wrappedJSObject; 701 global.queueRequestDeviceViaBtn( 702 args.aRequestAudio, 703 args.aRequestVideo, 704 args.aType, 705 args.aBadDevice 706 ); 707 await EventUtils.synthesizeMouseAtCenter( 708 global.document.getElementById("gum"), 709 {}, 710 content 711 ); 712 } 713 ); 714 } 715 716 return SpecialPowers.spawn( 717 bc, 718 [{ aRequestAudio, aRequestVideo, aType, aBadDevice }], 719 async function (args) { 720 let global = content.wrappedJSObject; 721 global.requestDevice( 722 args.aRequestAudio, 723 args.aRequestVideo, 724 args.aType, 725 args.aBadDevice, 726 args.withUserActivation 727 ); 728 } 729 ); 730 } 731 732 async function promiseRequestAudioOutput(options) { 733 info("requesting audio output"); 734 const bc = gBrowser.selectedBrowser; 735 return SpecialPowers.spawn(bc, [options], async function (opts) { 736 const global = content.wrappedJSObject; 737 global.requestAudioOutput(Cu.cloneInto(opts, content)); 738 }); 739 } 740 741 async function stopTracks( 742 aKind, 743 aAlreadyStopped, 744 aLastTracks, 745 aFrameId, 746 aBrowsingContext, 747 aBrowsingContextToObserve 748 ) { 749 // If the observers are listening to other frames, listen for a notification 750 // on the right subframe. 751 let frameBC = 752 aBrowsingContext ?? 753 (await getBrowsingContextForFrame( 754 gBrowser.selectedBrowser.browsingContext, 755 aFrameId 756 )); 757 758 let observerPromises = []; 759 if (!aAlreadyStopped) { 760 observerPromises.push( 761 expectObserverCalled( 762 "recording-device-events", 763 1, 764 aBrowsingContextToObserve 765 ) 766 ); 767 } 768 if (aLastTracks) { 769 observerPromises.push( 770 expectObserverCalled( 771 "recording-window-ended", 772 1, 773 aBrowsingContextToObserve 774 ) 775 ); 776 } 777 778 info(`Stopping all ${aKind} tracks`); 779 await SpecialPowers.spawn(frameBC, [aKind], async function (kind) { 780 content.wrappedJSObject.stopTracks(kind); 781 }); 782 783 await Promise.all(observerPromises); 784 } 785 786 async function closeStream( 787 aAlreadyClosed, 788 aFrameId, 789 aDontFlushObserverVerification, 790 aBrowsingContext, 791 aBrowsingContextToObserve 792 ) { 793 // Check that spurious notifications that occur while closing the 794 // stream are handled separately. Tests that use skipObserverVerification 795 // should pass true for aDontFlushObserverVerification. 796 if (!aDontFlushObserverVerification) { 797 await disableObserverVerification(); 798 await enableObserverVerification(); 799 } 800 801 // If the observers are listening to other frames, listen for a notification 802 // on the right subframe. 803 let frameBC = 804 aBrowsingContext ?? 805 (await getBrowsingContextForFrame( 806 gBrowser.selectedBrowser.browsingContext, 807 aFrameId 808 )); 809 810 let observerPromises = []; 811 if (!aAlreadyClosed) { 812 observerPromises.push( 813 expectObserverCalled( 814 "recording-device-events", 815 1, 816 aBrowsingContextToObserve 817 ) 818 ); 819 observerPromises.push( 820 expectObserverCalled( 821 "recording-window-ended", 822 1, 823 aBrowsingContextToObserve 824 ) 825 ); 826 } 827 828 info("closing the stream"); 829 await SpecialPowers.spawn(frameBC, [], async function () { 830 content.wrappedJSObject.closeStream(); 831 }); 832 833 await Promise.all(observerPromises); 834 835 await assertWebRTCIndicatorStatus(null); 836 } 837 838 async function reloadAsUser() { 839 info("reloading as a user"); 840 841 const reloadButton = document.getElementById("reload-button"); 842 await TestUtils.waitForCondition(() => !reloadButton.disabled); 843 // Disable observers as the page is being reloaded which can destroy 844 // the actors listening to the notifications. 845 await disableObserverVerification(); 846 847 let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 848 reloadButton.click(); 849 await loadedPromise; 850 851 await enableObserverVerification(); 852 } 853 854 async function reloadFromContent() { 855 info("reloading from content"); 856 857 // Disable observers as the page is being reloaded which can destroy 858 // the actors listening to the notifications. 859 await disableObserverVerification(); 860 861 let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 862 await ContentTask.spawn(gBrowser.selectedBrowser, null, () => 863 content.location.reload() 864 ); 865 866 await loadedPromise; 867 868 await enableObserverVerification(); 869 } 870 871 async function reloadAndAssertClosedStreams() { 872 await reloadFromContent(); 873 await checkNotSharing(); 874 } 875 876 /** 877 * @param {("microphone"|"camera"|"screen")[]} aExpectedTypes 878 * @param {Window} [aWindow] 879 */ 880 function checkDeviceSelectors(aExpectedTypes, aWindow = window) { 881 for (const type of aExpectedTypes) { 882 if (!["microphone", "camera", "screen", "speaker"].includes(type)) { 883 throw new Error(`Bad device type name ${type}`); 884 } 885 } 886 let document = aWindow.document; 887 888 let expectedDescribedBy = "webRTC-shareDevices-notification-description"; 889 for (let type of ["Camera", "Microphone", "Speaker"]) { 890 let selector = document.getElementById(`webRTC-select${type}`); 891 if (!aExpectedTypes.includes(type.toLowerCase())) { 892 ok(selector.hidden, `${type} selector hidden`); 893 continue; 894 } 895 ok(!selector.hidden, `${type} selector visible`); 896 let tagName = type == "Speaker" ? "richlistbox" : "menulist"; 897 let selectorList = document.getElementById( 898 `webRTC-select${type}-${tagName}` 899 ); 900 let label = document.getElementById( 901 `webRTC-select${type}-single-device-label` 902 ); 903 // If there's only 1 device listed, then we should show the label 904 // instead of the menulist. 905 if (selectorList.itemCount == 1) { 906 ok(selectorList.hidden, `${type} selector list should be hidden.`); 907 ok(!label.hidden, `${type} selector label should not be hidden.`); 908 let itemLabel = 909 tagName == "richlistbox" 910 ? selectorList.selectedItem.firstElementChild.getAttribute("value") 911 : selectorList.selectedItem.getAttribute("label"); 912 is( 913 label.value, 914 itemLabel, 915 `${type} label should be showing the lone device label.` 916 ); 917 expectedDescribedBy += ` webRTC-select${type}-icon webRTC-select${type}-single-device-label`; 918 } else { 919 ok(!selectorList.hidden, `${type} selector list should not be hidden.`); 920 ok(label.hidden, `${type} selector label should be hidden.`); 921 } 922 } 923 let ariaDescribedby = 924 aWindow.PopupNotifications.panel.getAttribute("aria-describedby"); 925 is(ariaDescribedby, expectedDescribedBy, "aria-describedby"); 926 927 let screenSelector = document.getElementById("webRTC-selectWindowOrScreen"); 928 if (aExpectedTypes.includes("screen")) { 929 ok(!screenSelector.hidden, "screen selector visible"); 930 } else { 931 ok(screenSelector.hidden, "screen selector hidden"); 932 } 933 } 934 935 /** 936 * Tests the siteIdentity icons, the permission panel and the global indicator 937 * UI state. 938 * 939 * @param {object} aExpected - Expected state for the current tab. 940 * @param {window} [aWin] - Top level chrome window to test state of. 941 * @param {object} [aExpectedGlobal] - Expected state for all tabs. 942 * @param {object} [aExpectedPerm] - Expected permission states keyed by device 943 * type. 944 */ 945 async function checkSharingUI( 946 aExpected, 947 aWin = window, 948 aExpectedGlobal = null, 949 aExpectedPerm = null 950 ) { 951 function isPaused(streamState) { 952 if (typeof streamState == "string") { 953 return streamState.includes("Paused"); 954 } 955 return streamState == STATE_CAPTURE_DISABLED; 956 } 957 958 let doc = aWin.document; 959 // First check the icon above the control center (i) icon. 960 let permissionBox = doc.getElementById("identity-permission-box"); 961 let webrtcSharingIcon = doc.getElementById("webrtc-sharing-icon"); 962 let expectOn = aExpected.audio || aExpected.video || aExpected.screen; 963 if (expectOn) { 964 ok(webrtcSharingIcon.hasAttribute("sharing"), "sharing attribute is set"); 965 } else { 966 ok( 967 !webrtcSharingIcon.hasAttribute("sharing"), 968 "sharing attribute is not set" 969 ); 970 } 971 let sharing = webrtcSharingIcon.getAttribute("sharing"); 972 if (!IsIndicatorDisabled) { 973 if (aExpected.screen) { 974 is(sharing, "screen", "showing screen icon in the identity block"); 975 } else if (aExpected.video == STATE_CAPTURE_ENABLED) { 976 is(sharing, "camera", "showing camera icon in the identity block"); 977 } else if (aExpected.audio == STATE_CAPTURE_ENABLED) { 978 is(sharing, "microphone", "showing mic icon in the identity block"); 979 } else if (aExpected.video) { 980 is(sharing, "camera", "showing camera icon in the identity block"); 981 } else if (aExpected.audio) { 982 is(sharing, "microphone", "showing mic icon in the identity block"); 983 } 984 } 985 986 let allStreamsPaused = Object.values(aExpected).every(isPaused); 987 is( 988 webrtcSharingIcon.hasAttribute("paused"), 989 allStreamsPaused, 990 "sharing icon(s) should be in paused state when paused" 991 ); 992 993 // Then check the sharing indicators inside the permission popup. 994 permissionBox.click(); 995 let popup = aWin.gPermissionPanel._permissionPopup; 996 // If the popup gets hidden before being shown, by stray focus/activate 997 // events, don't bother failing the test. It's enough to know that we 998 // started showing the popup. 999 let hiddenEvent = BrowserTestUtils.waitForEvent(popup, "popuphidden"); 1000 let shownEvent = BrowserTestUtils.waitForEvent(popup, "popupshown"); 1001 await Promise.race([hiddenEvent, shownEvent]); 1002 let permissions = doc.getElementById("permission-popup-permission-list"); 1003 for (let id of ["microphone", "camera", "screen"]) { 1004 let convertId = idToConvert => { 1005 if (idToConvert == "camera") { 1006 return "video"; 1007 } 1008 if (idToConvert == "microphone") { 1009 return "audio"; 1010 } 1011 return idToConvert; 1012 }; 1013 let expected = aExpected[convertId(id)]; 1014 1015 // Extract the expected permission for the device type. 1016 // Defaults to temporary allow. 1017 let { state, scope } = aExpectedPerm?.[convertId(id)] || {}; 1018 if (state == null) { 1019 state = SitePermissions.ALLOW; 1020 } 1021 if (scope == null) { 1022 scope = SitePermissions.SCOPE_TEMPORARY; 1023 } 1024 1025 is( 1026 !!aWin.gPermissionPanel._sharingState.webRTC[id], 1027 !!expected, 1028 "sharing state for " + id + " as expected" 1029 ); 1030 let item = permissions.querySelectorAll( 1031 ".permission-popup-permission-item-" + id 1032 ); 1033 let stateLabel = item?.[0]?.querySelector( 1034 ".permission-popup-permission-state-label" 1035 ); 1036 let icon = permissions.querySelectorAll( 1037 ".permission-popup-permission-icon." + id + "-icon" 1038 ); 1039 if (expected) { 1040 is(item.length, 1, "should show " + id + " item in permission panel"); 1041 is( 1042 stateLabel?.textContent, 1043 SitePermissions.getCurrentStateLabel(state, id, scope), 1044 "should show correct item label for " + id 1045 ); 1046 is(icon.length, 1, "should show " + id + " icon in permission panel"); 1047 is( 1048 icon[0].classList.contains("in-use"), 1049 expected && !isPaused(expected), 1050 "icon should have the in-use class, unless paused" 1051 ); 1052 } else if (!icon.length && !item.length && !stateLabel) { 1053 ok(true, "should not show " + id + " item in the permission panel"); 1054 ok(true, "should not show " + id + " icon in the permission panel"); 1055 ok( 1056 true, 1057 "should not show " + id + " state label in the permission panel" 1058 ); 1059 if (state != SitePermissions.PROMPT || SHOW_ALWAYS_ASK) { 1060 isnot( 1061 scope, 1062 SitePermissions.SCOPE_PERSISTENT, 1063 "persistent permission not shown" 1064 ); 1065 } 1066 } else { 1067 // This will happen if there are persistent permissions set. 1068 ok( 1069 !icon[0].classList.contains("in-use"), 1070 "if shown, the " + id + " icon should not have the in-use class" 1071 ); 1072 is(item.length, 1, "should not show more than 1 " + id + " item"); 1073 is(icon.length, 1, "should not show more than 1 " + id + " icon"); 1074 1075 // Note: To pass, this one needs state and/or scope passed into 1076 // checkSharingUI() for values other than ALLOW and SCOPE_TEMPORARY 1077 is( 1078 stateLabel?.textContent, 1079 SitePermissions.getCurrentStateLabel(state, id, scope), 1080 "should show correct item label for " + id 1081 ); 1082 if (!SHOW_ALWAYS_ASK) { 1083 isnot( 1084 state, 1085 state == SitePermissions.PROMPT, 1086 "always ask permission should be hidden" 1087 ); 1088 } 1089 } 1090 } 1091 aWin.gPermissionPanel._permissionPopup.hidePopup(); 1092 await TestUtils.waitForCondition( 1093 () => permissionPopupHidden(aWin), 1094 "identity popup should be hidden" 1095 ); 1096 1097 // Check the global indicators. 1098 if (expectOn) { 1099 await assertWebRTCIndicatorStatus(aExpectedGlobal || aExpected); 1100 } 1101 } 1102 1103 async function checkNotSharing() { 1104 Assert.deepEqual( 1105 await getMediaCaptureState(), 1106 {}, 1107 "expected nothing to be shared" 1108 ); 1109 1110 ok( 1111 !document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"), 1112 "no sharing indicator on the control center icon" 1113 ); 1114 1115 await assertWebRTCIndicatorStatus(null); 1116 } 1117 1118 async function checkNotSharingWithinGracePeriod() { 1119 Assert.deepEqual( 1120 await getMediaCaptureState(), 1121 {}, 1122 "expected nothing to be shared" 1123 ); 1124 1125 ok( 1126 document.getElementById("webrtc-sharing-icon").hasAttribute("sharing"), 1127 "has sharing indicator on the control center icon" 1128 ); 1129 ok( 1130 document.getElementById("webrtc-sharing-icon").hasAttribute("paused"), 1131 "sharing indicator is paused" 1132 ); 1133 1134 await assertWebRTCIndicatorStatus(null); 1135 } 1136 1137 async function promiseReloadFrame(aFrameId, aBrowsingContext) { 1138 let loadedPromise = BrowserTestUtils.browserLoaded( 1139 gBrowser.selectedBrowser, 1140 true, 1141 () => { 1142 return true; 1143 } 1144 ); 1145 let bc = 1146 aBrowsingContext ?? 1147 (await getBrowsingContextForFrame( 1148 gBrowser.selectedBrowser.browsingContext, 1149 aFrameId 1150 )); 1151 await SpecialPowers.spawn(bc, [], async function () { 1152 content.location.reload(); 1153 }); 1154 return loadedPromise; 1155 } 1156 1157 function promiseChangeLocationFrame(aFrameId, aNewLocation) { 1158 return SpecialPowers.spawn( 1159 gBrowser.selectedBrowser.browsingContext, 1160 [{ aFrameId, aNewLocation }], 1161 async function (args) { 1162 let frame = content.wrappedJSObject.document.getElementById( 1163 args.aFrameId 1164 ); 1165 return new Promise(resolve => { 1166 function listener() { 1167 frame.removeEventListener("load", listener, true); 1168 resolve(); 1169 } 1170 frame.addEventListener("load", listener, true); 1171 1172 content.wrappedJSObject.document.getElementById( 1173 args.aFrameId 1174 ).contentWindow.location = args.aNewLocation; 1175 }); 1176 } 1177 ); 1178 } 1179 1180 async function openNewTestTab(leaf = "get_user_media.html") { 1181 let rootDir = getRootDirectory(gTestPath); 1182 rootDir = rootDir.replace( 1183 "chrome://mochitests/content/", 1184 "https://example.com/" 1185 ); 1186 let absoluteURI = rootDir + leaf; 1187 1188 let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, absoluteURI); 1189 return tab.linkedBrowser; 1190 } 1191 1192 // Enabling observer verification adds listeners for all of the webrtc 1193 // observer topics. If any notifications occur for those topics that 1194 // were not explicitly requested, a failure will occur. 1195 async function enableObserverVerification(browser = gBrowser.selectedBrowser) { 1196 // Skip these checks in single process mode as it isn't worth implementing it. 1197 if (!gMultiProcessBrowser) { 1198 return; 1199 } 1200 1201 gBrowserContextsToObserve = [browser.browsingContext]; 1202 1203 // A list of subframe indicies to also add observers to. This only 1204 // supports one nested level. 1205 if (gObserveSubFrames) { 1206 let bcsAndFrameIds = await getBrowsingContextsAndFrameIdsForSubFrames( 1207 browser, 1208 gObserveSubFrames 1209 ); 1210 for (let { observeBC } of bcsAndFrameIds) { 1211 if (observeBC) { 1212 gBrowserContextsToObserve.push(observeBC); 1213 } 1214 } 1215 } 1216 1217 for (let bc of gBrowserContextsToObserve) { 1218 await BrowserTestUtils.startObservingTopics(bc, observerTopics); 1219 } 1220 } 1221 1222 async function disableObserverVerification() { 1223 if (!gMultiProcessBrowser) { 1224 return; 1225 } 1226 1227 for (let bc of gBrowserContextsToObserve) { 1228 await BrowserTestUtils.stopObservingTopics(bc, observerTopics).catch( 1229 reason => { 1230 ok(false, "Failed " + reason); 1231 } 1232 ); 1233 } 1234 } 1235 1236 function permissionPopupHidden(win = window) { 1237 let popup = win.gPermissionPanel._permissionPopup; 1238 return !popup || popup.state == "closed"; 1239 } 1240 1241 async function runTests(tests, options = {}) { 1242 let browser = await openNewTestTab(options.relativeURI); 1243 1244 is( 1245 PopupNotifications._currentNotifications.length, 1246 0, 1247 "should start the test without any prior popup notification" 1248 ); 1249 ok( 1250 permissionPopupHidden(), 1251 "should start the test with the permission panel hidden" 1252 ); 1253 1254 // Set prefs so that permissions prompts are shown and loopback devices 1255 // are not used. To test the chrome we want prompts to be shown, and 1256 // these tests are flakey when using loopback devices (though it would 1257 // be desirable to make them work with loopback in future). See bug 1643711. 1258 let prefs = [ 1259 [PREF_PERMISSION_FAKE, true], 1260 [PREF_AUDIO_LOOPBACK, ""], 1261 [PREF_VIDEO_LOOPBACK, ""], 1262 [PREF_FAKE_STREAMS, true], 1263 [PREF_FOCUS_SOURCE, false], 1264 ]; 1265 await SpecialPowers.pushPrefEnv({ set: prefs }); 1266 1267 // When the frames are in different processes, add observers to each frame, 1268 // to ensure that the notifications don't get sent in the wrong process. 1269 gObserveSubFrames = SpecialPowers.useRemoteSubframes ? options.subFrames : {}; 1270 1271 for (let testCase of tests) { 1272 let startTime = ChromeUtils.now(); 1273 info(testCase.desc); 1274 if ( 1275 !testCase.skipObserverVerification && 1276 !options.skipObserverVerification 1277 ) { 1278 await enableObserverVerification(); 1279 } 1280 await testCase.run(browser, options.subFrames); 1281 if ( 1282 !testCase.skipObserverVerification && 1283 !options.skipObserverVerification 1284 ) { 1285 await disableObserverVerification(); 1286 } 1287 if (options.cleanup) { 1288 await options.cleanup(); 1289 } 1290 ChromeUtils.addProfilerMarker( 1291 "browser-test", 1292 { startTime, category: "Test" }, 1293 testCase.desc 1294 ); 1295 } 1296 1297 // Some tests destroy the original tab and leave a new one in its place. 1298 BrowserTestUtils.removeTab(gBrowser.selectedTab); 1299 } 1300 1301 /** 1302 * Given a browser from a tab in this window, chooses to share 1303 * some combination of camera, mic or screen. 1304 * 1305 * @param {<xul:browser} browser - The browser to share devices with. 1306 * @param {boolean} camera - True to share a camera device. 1307 * @param {boolean} mic - True to share a microphone device. 1308 * @param {number} [screenOrWin] - One of either SHARE_WINDOW or SHARE_SCREEN 1309 * to share a window or screen. Defaults to neither. 1310 * @param {boolean} remember - True to persist the permission to the 1311 * SitePermissions database as SitePermissions.SCOPE_PERSISTENT. Note that 1312 * callers are responsible for clearing this persistent permission. 1313 * @returns {Promise<void>} 1314 * Resolves once sharing is complete. 1315 */ 1316 async function shareDevices( 1317 browser, 1318 camera, 1319 mic, 1320 screenOrWin = 0, 1321 remember = false 1322 ) { 1323 if (camera || mic) { 1324 let promise = promisePopupNotificationShown( 1325 "webRTC-shareDevices", 1326 null, 1327 window 1328 ); 1329 1330 await promiseRequestDevice(mic, camera, null, null, browser); 1331 await promise; 1332 1333 const expectedDeviceSelectorTypes = [ 1334 camera && "camera", 1335 mic && "microphone", 1336 ].filter(x => x); 1337 checkDeviceSelectors(expectedDeviceSelectorTypes); 1338 let observerPromise1 = expectObserverCalled("getUserMedia:response:allow"); 1339 let observerPromise2 = expectObserverCalled("recording-device-events"); 1340 1341 let rememberCheck = PopupNotifications.panel.querySelector( 1342 ".popup-notification-checkbox" 1343 ); 1344 rememberCheck.checked = remember; 1345 1346 promise = promiseMessage("ok", () => { 1347 PopupNotifications.panel.firstElementChild.button.click(); 1348 }); 1349 1350 await observerPromise1; 1351 await observerPromise2; 1352 await promise; 1353 } 1354 1355 if (screenOrWin) { 1356 let promise = promisePopupNotificationShown( 1357 "webRTC-shareDevices", 1358 null, 1359 window 1360 ); 1361 1362 await promiseRequestDevice(false, true, null, "screen", browser); 1363 await promise; 1364 1365 checkDeviceSelectors(["screen"], window); 1366 1367 let document = window.document; 1368 1369 let menulist = document.getElementById("webRTC-selectWindow-menulist"); 1370 let displayMediaSource; 1371 1372 if (screenOrWin == SHARE_SCREEN) { 1373 displayMediaSource = "screen"; 1374 } else if (screenOrWin == SHARE_WINDOW) { 1375 displayMediaSource = "window"; 1376 } else { 1377 throw new Error("Got an invalid argument to shareDevices."); 1378 } 1379 1380 let menuitem = null; 1381 for (let i = 0; i < menulist.itemCount; ++i) { 1382 let current = menulist.getItemAtIndex(i); 1383 if (current.mediaSource == displayMediaSource) { 1384 menuitem = current; 1385 break; 1386 } 1387 } 1388 1389 Assert.ok(menuitem, "Should have found an appropriate display menuitem"); 1390 menuitem.doCommand(); 1391 1392 let notification = window.PopupNotifications.panel.firstElementChild; 1393 1394 let observerPromise1 = expectObserverCalled("getUserMedia:response:allow"); 1395 let observerPromise2 = expectObserverCalled("recording-device-events"); 1396 await promiseMessage( 1397 "ok", 1398 () => { 1399 notification.button.click(); 1400 }, 1401 1, 1402 browser 1403 ); 1404 await observerPromise1; 1405 await observerPromise2; 1406 } 1407 }