browser_devices_get_user_media_grace.js (13895B)
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 "use strict"; 6 7 requestLongerTimeout(2); 8 9 const permissionError = 10 "error: NotAllowedError: The request is not allowed " + 11 "by the user agent or the platform in the current context."; 12 13 const SAME_ORIGIN = "https://example.com"; 14 const CROSS_ORIGIN = "https://example.org"; 15 16 const PATH = "/browser/browser/base/content/test/webrtc/get_user_media.html"; 17 const PATH2 = "/browser/browser/base/content/test/webrtc/get_user_media2.html"; 18 19 const GRACE_PERIOD_MS = 3000; 20 const WAIT_PERIOD_MS = GRACE_PERIOD_MS + 500; 21 22 // We're inherently testing timeouts (grace periods) 23 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 24 const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 25 const perms = SitePermissions; 26 27 // These tests focus on camera and microphone, so we define some helpers. 28 29 async function prompt(audio, video) { 30 let observerPromise = expectObserverCalled("getUserMedia:request"); 31 let promise = promisePopupNotificationShown("webRTC-shareDevices"); 32 await promiseRequestDevice(audio, video); 33 await promise; 34 await observerPromise; 35 const expectedDeviceSelectorTypes = [ 36 audio && "microphone", 37 video && "camera", 38 ].filter(x => x); 39 checkDeviceSelectors(expectedDeviceSelectorTypes); 40 } 41 42 async function allow(audio, video) { 43 let indicator = promiseIndicatorWindow(); 44 let observerPromise1 = expectObserverCalled("getUserMedia:response:allow"); 45 let observerPromise2 = expectObserverCalled("recording-device-events"); 46 await promiseMessage("ok", () => { 47 PopupNotifications.panel.firstElementChild.button.click(); 48 }); 49 await observerPromise1; 50 await observerPromise2; 51 Assert.deepEqual( 52 Object.assign({ audio: false, video: false }, await getMediaCaptureState()), 53 { audio, video }, 54 `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared` 55 ); 56 await indicator; 57 await checkSharingUI({ audio, video }); 58 } 59 60 async function deny(action) { 61 let observerPromise1 = expectObserverCalled("getUserMedia:response:deny"); 62 let observerPromise2 = expectObserverCalled("recording-window-ended"); 63 await promiseMessage(permissionError, () => { 64 activateSecondaryAction(action); 65 }); 66 await observerPromise1; 67 await observerPromise2; 68 await checkNotSharing(); 69 } 70 71 async function noPrompt(audio, video) { 72 let observerPromises = [ 73 expectObserverCalled("getUserMedia:request"), 74 expectObserverCalled("getUserMedia:response:allow"), 75 expectObserverCalled("recording-device-events"), 76 ]; 77 let promise = promiseMessage("ok"); 78 await promiseRequestDevice(audio, video); 79 await promise; 80 await Promise.all(observerPromises); 81 await promiseNoPopupNotification("webRTC-shareDevices"); 82 Assert.deepEqual( 83 Object.assign({ audio: false, video: false }, await getMediaCaptureState()), 84 { audio, video }, 85 `expected ${video ? "camera " : ""} ${audio ? "microphone " : ""}shared` 86 ); 87 await checkSharingUI({ audio, video }); 88 } 89 90 async function navigate(browser, url) { 91 await disableObserverVerification(); 92 let loaded = BrowserTestUtils.browserLoaded(browser, false, url); 93 await SpecialPowers.spawn( 94 browser, 95 [url], 96 u => (content.document.location = u) 97 ); 98 await loaded; 99 await enableObserverVerification(); 100 } 101 102 var gTests = [ 103 { 104 desc: "getUserMedia camera+mic survives track.stop but not past grace", 105 run: async function checkAudioVideoGracePastStop() { 106 await prompt(true, true); 107 await allow(true, true); 108 109 info( 110 "After closing all streams, gUM(camera+mic) returns a stream " + 111 "without prompting within grace period." 112 ); 113 await closeStream(); 114 await checkNotSharingWithinGracePeriod(); 115 await noPrompt(true, true); 116 117 info( 118 "After closing all streams, gUM(mic) returns a stream " + 119 "without prompting within grace period." 120 ); 121 await closeStream(); 122 await checkNotSharingWithinGracePeriod(); 123 await noPrompt(true, false); 124 125 info( 126 "After closing all streams, gUM(camera) returns a stream " + 127 "without prompting within grace period." 128 ); 129 await closeStream(); 130 await checkNotSharingWithinGracePeriod(); 131 await noPrompt(false, true); 132 133 info("gUM(screen) still causes a prompt."); 134 let observerPromise = expectObserverCalled("getUserMedia:request"); 135 let promise = promisePopupNotificationShown("webRTC-shareDevices"); 136 await promiseRequestDevice(false, true, null, "screen"); 137 await promise; 138 await observerPromise; 139 140 is( 141 PopupNotifications.getNotification("webRTC-shareDevices").anchorID, 142 "webRTC-shareScreen-notification-icon", 143 "anchored to device icon" 144 ); 145 checkDeviceSelectors(["screen"]); 146 147 observerPromise = expectObserverCalled("getUserMedia:response:deny"); 148 await promiseMessage(permissionError, () => { 149 activateSecondaryAction(kActionDeny); 150 }); 151 await observerPromise; 152 perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser); 153 154 await closeStream(); 155 info("Closed stream. Waiting past grace period."); 156 await checkNotSharingWithinGracePeriod(); 157 await wait(WAIT_PERIOD_MS); 158 await checkNotSharing(); 159 160 info("After grace period expires, gUM(camera) causes a prompt."); 161 await prompt(false, true); 162 await deny(kActionDeny); 163 perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); 164 165 info("After grace period expires, gUM(mic) causes a prompt."); 166 await prompt(true, false); 167 await deny(kActionDeny); 168 perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); 169 }, 170 }, 171 172 { 173 desc: "getUserMedia camera+mic survives page reload but not past grace", 174 run: async function checkAudioVideoGracePastReload() { 175 await prompt(true, true); 176 await allow(true, true); 177 await closeStream(); 178 179 await reloadFromContent(); 180 info( 181 "After page reload, gUM(camera+mic) returns a stream " + 182 "without prompting within grace period." 183 ); 184 await checkNotSharingWithinGracePeriod(); 185 await noPrompt(true, true); 186 await closeStream(); 187 188 await reloadAsUser(); 189 info( 190 "After user page reload, gUM(camera+mic) returns a stream " + 191 "without prompting within grace period." 192 ); 193 await checkNotSharingWithinGracePeriod(); 194 await noPrompt(true, true); 195 196 info("gUM(screen) still causes a prompt."); 197 let observerPromise = expectObserverCalled("getUserMedia:request"); 198 let promise = promisePopupNotificationShown("webRTC-shareDevices"); 199 await promiseRequestDevice(false, true, null, "screen"); 200 await promise; 201 await observerPromise; 202 203 is( 204 PopupNotifications.getNotification("webRTC-shareDevices").anchorID, 205 "webRTC-shareScreen-notification-icon", 206 "anchored to device icon" 207 ); 208 checkDeviceSelectors(["screen"]); 209 210 observerPromise = expectObserverCalled("getUserMedia:response:deny"); 211 await promiseMessage(permissionError, () => { 212 activateSecondaryAction(kActionDeny); 213 }); 214 await observerPromise; 215 perms.removeFromPrincipal(null, "screen", gBrowser.selectedBrowser); 216 217 await closeStream(); 218 info("Closed stream. Waiting past grace period."); 219 await checkNotSharingWithinGracePeriod(); 220 await wait(WAIT_PERIOD_MS); 221 await checkNotSharing(); 222 223 info("After grace period expires, gUM(camera) causes a prompt."); 224 await prompt(false, true); 225 await deny(kActionDeny); 226 perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); 227 228 info("After grace period expires, gUM(mic) causes a prompt."); 229 await prompt(true, false); 230 await deny(kActionDeny); 231 perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); 232 }, 233 }, 234 235 { 236 desc: "getUserMedia camera+mic grace period does not carry over to new tab", 237 run: async function checkAudioVideoGraceEndsNewTab() { 238 await prompt(true, true); 239 await allow(true, true); 240 241 info("Open same page in a new tab"); 242 await disableObserverVerification(); 243 await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async () => { 244 info("In new tab, gUM(camera+mic) causes a prompt."); 245 await prompt(true, true); 246 }); 247 info("Closed tab"); 248 await enableObserverVerification(); 249 await closeStream(); 250 info("Closed stream. Waiting past grace period."); 251 await checkNotSharingWithinGracePeriod(); 252 await wait(WAIT_PERIOD_MS); 253 await checkNotSharing(); 254 255 info("After grace period expires, gUM(camera+mic) causes a prompt."); 256 await prompt(true, true); 257 await deny(kActionDeny); 258 perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); 259 perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); 260 }, 261 }, 262 263 { 264 desc: "getUserMedia camera+mic survives navigation but not past grace", 265 run: async function checkAudioVideoGracePastNavigation(browser) { 266 // Use longer grace period in this test to accommodate navigation 267 const LONG_GRACE_PERIOD_MS = 9000; 268 const LONG_WAIT_PERIOD_MS = LONG_GRACE_PERIOD_MS + 500; 269 await SpecialPowers.pushPrefEnv({ 270 set: [ 271 ["privacy.webrtc.deviceGracePeriodTimeoutMs", LONG_GRACE_PERIOD_MS], 272 ], 273 }); 274 await prompt(true, true); 275 await allow(true, true); 276 await closeStream(); 277 278 info("Navigate to a second same-origin page"); 279 await navigate(browser, SAME_ORIGIN + PATH2); 280 info( 281 "After navigating to second same-origin page, gUM(camera+mic) " + 282 "returns a stream without prompting within grace period." 283 ); 284 await checkNotSharingWithinGracePeriod(); 285 await noPrompt(true, true); 286 await closeStream(); 287 288 info("Closed stream. Waiting past grace period."); 289 await checkNotSharingWithinGracePeriod(); 290 await wait(LONG_WAIT_PERIOD_MS); 291 await checkNotSharing(); 292 293 info("After grace period expires, gUM(camera+mic) causes a prompt."); 294 await prompt(true, true); 295 await allow(true, true); 296 297 info("Navigate to a different-origin page"); 298 await navigate(browser, CROSS_ORIGIN + PATH2); 299 info( 300 "After navigating to a different-origin page, gUM(camera+mic) " + 301 "causes a prompt." 302 ); 303 await prompt(true, true); 304 await deny(kActionDeny); 305 perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); 306 perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); 307 308 info("Navigate back to the first page"); 309 await navigate(browser, SAME_ORIGIN + PATH); 310 info( 311 "After navigating back to the first page, gUM(camera+mic) " + 312 "returns a stream without prompting within grace period." 313 ); 314 await checkNotSharingWithinGracePeriod(); 315 await noPrompt(true, true); 316 await closeStream(); 317 info("Closed stream. Waiting past grace period."); 318 await checkNotSharingWithinGracePeriod(); 319 await wait(LONG_WAIT_PERIOD_MS); 320 await checkNotSharing(); 321 322 info("After grace period expires, gUM(camera+mic) causes a prompt."); 323 await prompt(true, true); 324 await deny(kActionDeny); 325 perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); 326 perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); 327 }, 328 }, 329 330 { 331 desc: "getUserMedia camera+mic grace period cleared on permission block", 332 run: async function checkAudioVideoGraceEndsNewTab() { 333 await SpecialPowers.pushPrefEnv({ 334 set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", 10000]], 335 }); 336 info("Set up longer camera grace period."); 337 await prompt(false, true); 338 await allow(false, true); 339 await closeStream(); 340 let principal = gBrowser.selectedBrowser.contentPrincipal; 341 info("Request both to get prompted so we can block both."); 342 await prompt(true, true); 343 // We need to remember this decision to set a block permission here and not just 'Not now' the request, see Bug:1609578 344 await deny(kActionNever); 345 // Clear the block so we can prompt again. 346 perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser); 347 perms.removeFromPrincipal( 348 principal, 349 "microphone", 350 gBrowser.selectedBrowser 351 ); 352 353 info("Revoking permission clears camera grace period."); 354 await prompt(false, true); 355 await deny(kActionDeny); 356 perms.removeFromPrincipal(null, "camera", gBrowser.selectedBrowser); 357 358 info("Set up longer microphone grace period."); 359 await prompt(true, false); 360 await allow(true, false); 361 await closeStream(); 362 363 info("Request both to get prompted so we can block both."); 364 await prompt(true, true); 365 // We need to remember this decision to be able to set a block permission here 366 await deny(kActionNever); 367 perms.removeFromPrincipal(principal, "camera", gBrowser.selectedBrowser); 368 perms.removeFromPrincipal( 369 principal, 370 "microphone", 371 gBrowser.selectedBrowser 372 ); 373 374 info("Revoking permission clears microphone grace period."); 375 await prompt(true, false); 376 // We need to remember this decision to be able to set a block permission here 377 await deny(kActionNever); 378 perms.removeFromPrincipal(null, "microphone", gBrowser.selectedBrowser); 379 }, 380 }, 381 ]; 382 383 add_task(async function test() { 384 await SpecialPowers.pushPrefEnv({ 385 set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", GRACE_PERIOD_MS]], 386 }); 387 await runTests(gTests); 388 });