browser_devices_select_audio_output.js (8611B)
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 requestLongerTimeout(2); 6 7 const permissionError = 8 "error: NotAllowedError: The request is not allowed " + 9 "by the user agent or the platform in the current context."; 10 11 async function requestAudioOutput(options) { 12 await Promise.all([ 13 expectObserverCalled("getUserMedia:request"), 14 expectObserverCalled("recording-window-ended"), 15 promiseRequestAudioOutput(options), 16 ]); 17 } 18 19 async function requestAudioOutputExpectingPrompt(options) { 20 await Promise.all([ 21 promisePopupNotificationShown("webRTC-shareDevices"), 22 requestAudioOutput(options), 23 ]); 24 25 is( 26 PopupNotifications.getNotification("webRTC-shareDevices").anchorID, 27 "webRTC-shareSpeaker-notification-icon", 28 "anchored to device icon" 29 ); 30 checkDeviceSelectors(["speaker"]); 31 } 32 33 async function requestAudioOutputExpectingDeny(options) { 34 await Promise.all([ 35 requestAudioOutput(options), 36 expectObserverCalled("getUserMedia:response:deny"), 37 promiseMessage(permissionError), 38 ]); 39 } 40 41 async function simulateAudioOutputRequest(options) { 42 await SpecialPowers.spawn( 43 gBrowser.selectedBrowser, 44 [options], 45 function simPrompt({ deviceCount, deviceId }) { 46 const nsIMediaDeviceQI = ChromeUtils.generateQI([Ci.nsIMediaDevice]); 47 const devices = [...Array(deviceCount).keys()].map(i => ({ 48 type: "audiooutput", 49 rawName: `name ${i}`, 50 rawId: `rawId ${i}`, 51 id: `id ${i}`, 52 QueryInterface: nsIMediaDeviceQI, 53 })); 54 const req = { 55 type: "selectaudiooutput", 56 windowID: content.windowGlobalChild.outerWindowId, 57 devices, 58 getConstraints: () => ({}), 59 getAudioOutputOptions: () => ({ deviceId }), 60 isSecure: true, 61 isHandlingUserInput: true, 62 }; 63 const { WebRTCChild } = SpecialPowers.ChromeUtils.importESModule( 64 "resource:///actors/WebRTCChild.sys.mjs" 65 ); 66 WebRTCChild.observe(req, "getUserMedia:request"); 67 } 68 ); 69 } 70 71 async function allowPrompt() { 72 const observerPromise = expectObserverCalled("getUserMedia:response:allow"); 73 PopupNotifications.panel.firstElementChild.button.click(); 74 await observerPromise; 75 } 76 77 async function allow() { 78 await Promise.all([promiseMessage("ok"), allowPrompt()]); 79 } 80 81 async function denyPrompt() { 82 const observerPromise = expectObserverCalled("getUserMedia:response:deny"); 83 activateSecondaryAction(kActionDeny); 84 await observerPromise; 85 } 86 87 async function deny() { 88 await Promise.all([promiseMessage(permissionError), denyPrompt()]); 89 } 90 91 async function escapePrompt() { 92 const observerPromise = expectObserverCalled("getUserMedia:response:deny"); 93 EventUtils.synthesizeKey("KEY_Escape"); 94 await observerPromise; 95 } 96 97 async function escape() { 98 await Promise.all([promiseMessage(permissionError), escapePrompt()]); 99 } 100 101 var gTests = [ 102 { 103 desc: 'User clicks "Allow" and revokes', 104 run: async function checkAllow() { 105 await requestAudioOutputExpectingPrompt(); 106 await allow(); 107 108 info("selectAudioOutput() with no deviceId again should prompt again."); 109 await requestAudioOutputExpectingPrompt(); 110 await allow(); 111 112 info("selectAudioOutput() with same deviceId should not prompt again."); 113 await Promise.all([ 114 expectObserverCalled("getUserMedia:response:allow"), 115 promiseMessage("ok"), 116 requestAudioOutput({ requestSameDevice: true }), 117 ]); 118 119 await revokePermission("speaker", true); 120 info("Same deviceId should prompt again after revoked permission."); 121 await requestAudioOutputExpectingPrompt({ requestSameDevice: true }); 122 await allow(); 123 await revokePermission("speaker", true); 124 }, 125 }, 126 { 127 desc: 'User clicks "Not Now"', 128 run: async function checkNotNow() { 129 await requestAudioOutputExpectingPrompt(); 130 is( 131 PopupNotifications.getNotification("webRTC-shareDevices") 132 .secondaryActions[0].label, 133 "Not now", 134 "first secondary action label" 135 ); 136 await deny(); 137 info("selectAudioOutput() after Not Now should prompt again."); 138 await requestAudioOutputExpectingPrompt(); 139 await escape(); 140 }, 141 }, 142 { 143 desc: 'User presses "Esc"', 144 run: async function checkEsc() { 145 await requestAudioOutputExpectingPrompt(); 146 await escape(); 147 info("selectAudioOutput() after Esc should prompt again."); 148 await requestAudioOutputExpectingPrompt(); 149 await allow(); 150 await revokePermission("speaker", true); 151 }, 152 }, 153 { 154 desc: 'User clicks "Always Block"', 155 run: async function checkAlwaysBlock() { 156 await requestAudioOutputExpectingPrompt(); 157 await Promise.all([ 158 expectObserverCalled("getUserMedia:response:deny"), 159 promiseMessage(permissionError), 160 activateSecondaryAction(kActionNever), 161 ]); 162 info("selectAudioOutput() after Always Block should not prompt again."); 163 await requestAudioOutputExpectingDeny(); 164 await revokePermission("speaker", true); 165 }, 166 }, 167 { 168 desc: "Single Device", 169 run: async function checkSingle() { 170 await Promise.all([ 171 promisePopupNotificationShown("webRTC-shareDevices"), 172 simulateAudioOutputRequest({ deviceCount: 1 }), 173 ]); 174 is( 175 document.activeElement.className, 176 "popup-notification-primary-button primary footer-button", 177 "popup button focus" 178 ); 179 checkDeviceSelectors(["speaker"]); 180 await escapePrompt(); 181 }, 182 }, 183 { 184 desc: "Multi Device with deviceId", 185 run: async function checkMulti() { 186 const deviceCount = 4; 187 await Promise.all([ 188 promisePopupNotificationShown("webRTC-shareDevices"), 189 simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }), 190 ]); 191 const selectorList = document.getElementById( 192 `webRTC-selectSpeaker-richlistbox` 193 ); 194 is(selectorList.selectedIndex, 2, "pre-selected index"); 195 ok(selectorList.contains(document.activeElement), "richlistbox focus"); 196 checkDeviceSelectors(["speaker"]); 197 await allowPrompt(); 198 199 info("Expect same-device request allowed without prompt"); 200 await Promise.all([ 201 expectObserverCalled("getUserMedia:response:allow"), 202 simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }), 203 ]); 204 205 info("Expect prompt for different-device request"); 206 await Promise.all([ 207 promisePopupNotificationShown("webRTC-shareDevices"), 208 simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }), 209 ]); 210 await denyPrompt(); 211 212 info("Expect prompt again for denied-device request"); 213 await Promise.all([ 214 promisePopupNotificationShown("webRTC-shareDevices"), 215 simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }), 216 ]); 217 is(selectorList.selectedIndex, 1, "pre-selected index"); 218 info("Expect allow from double click"); 219 const targetIndex = 2; 220 const target = selectorList.getItemAtIndex(targetIndex); 221 EventUtils.synthesizeMouseAtCenter(target, { clickCount: 1 }); 222 is(selectorList.selectedIndex, targetIndex, "selected index after click"); 223 const messagePromise = promiseMessage(); 224 const observerPromise = BrowserTestUtils.contentTopicObserved( 225 gBrowser.selectedBrowser.browsingContext, 226 "getUserMedia:response:allow", 227 1, 228 aSubject => { 229 const device = aSubject 230 .QueryInterface(Ci.nsIArrayExtensions) 231 .GetElementAt(0).wrappedJSObject; 232 // content defined by BrowserTestUtilsChild. 233 content.wrappedJSObject.message(device.id); 234 return true; 235 } 236 ); 237 EventUtils.synthesizeMouseAtCenter(target, { clickCount: 2 }); 238 await observerPromise; 239 const id = await messagePromise; 240 is(id, `id ${targetIndex}`, "selected device"); 241 242 await revokePermission("speaker", true); 243 }, 244 }, 245 { 246 desc: "SitePermissions speaker block", 247 run: async function checkPermissionsBlock() { 248 SitePermissions.setForPrincipal( 249 gBrowser.contentPrincipal, 250 "speaker", 251 SitePermissions.BLOCK 252 ); 253 await requestAudioOutputExpectingDeny(); 254 SitePermissions.removeFromPrincipal(gBrowser.contentPrincipal, "speaker"); 255 }, 256 }, 257 ]; 258 259 add_task(async function test() { 260 await SpecialPowers.pushPrefEnv({ set: [["media.setsinkid.enabled", true]] }); 261 await runTests(gTests); 262 });