test_permissions_api.html (9194B)
1 <!-- 2 Any copyright is dedicated to the Public Domain. 3 http://creativecommons.org/publicdomain/zero/1.0/ 4 --> 5 <!DOCTYPE HTML> 6 <html> 7 8 <head> 9 <meta charset="utf-8"> 10 <title>Test for Permissions API</title> 11 <script src="/tests/SimpleTest/SimpleTest.js"></script> 12 <link rel="stylesheet" href="/tests/SimpleTest/test.css"> 13 </head> 14 15 <body> 16 <pre id="test"></pre> 17 <script type="application/javascript"> 18 /*globals SpecialPowers, SimpleTest, is, ok, */ 19 'use strict'; 20 21 const { 22 UNKNOWN_ACTION, 23 PROMPT_ACTION, 24 ALLOW_ACTION, 25 DENY_ACTION 26 } = SpecialPowers.Ci.nsIPermissionManager; 27 28 SimpleTest.waitForExplicitFinish(); 29 30 const OTHER_PERMISSIONS = [{ 31 name: 'geolocation', 32 type: 'geo' 33 }, { 34 name: 'notifications', 35 type: 'desktop-notification' 36 }, { 37 name: 'push', 38 type: 'desktop-notification' 39 }, { 40 name: 'persistent-storage', 41 type: 'persistent-storage' 42 }, { 43 name: 'midi', 44 type: 'midi' 45 }, ]; 46 47 const MEDIA_PERMISSIONS = [{ 48 name: 'camera', 49 type: 'camera' 50 }, { 51 name: 'microphone', 52 type: 'microphone' 53 }, ]; 54 55 const PERMISSIONS = [...OTHER_PERMISSIONS, ...MEDIA_PERMISSIONS]; 56 57 const UNSUPPORTED_PERMISSIONS = [ 58 { name: 'foobarbaz' }, // Not in spec, for testing only. 59 ]; 60 61 // Create a closure, so that tests are run on the correct window object. 62 function createPermissionTester(iframe) { 63 const iframeWindow = iframe.contentWindow; 64 return { 65 async setPermissions(allow, context = iframeWindow.document) { 66 const permissions = PERMISSIONS.map(({ type }) => { 67 return { 68 type, 69 allow, 70 context, 71 }; 72 }); 73 await SpecialPowers.popPermissions(); 74 return SpecialPowers.pushPermissions(permissions); 75 }, 76 checkPermissions(permissions, expectedState, mediaExpectedState = expectedState) { 77 const promisesToQuery = permissions.map(({ name: expectedName }) => { 78 return iframeWindow.navigator.permissions 79 .query({ name: expectedName }) 80 .then( 81 ({ state, name }) => { 82 is(name, expectedName, `correct name for '${expectedName}'`); 83 if (['camera', 'microphone'].includes(expectedName)) { 84 is(state, mediaExpectedState, `correct state for '${expectedName}'`); 85 } else { 86 is(state, expectedState, `correct state for '${expectedName}'`); 87 } 88 }, 89 () => ok(false, `query should not have rejected for '${name}'`) 90 ); 91 }); 92 return Promise.all(promisesToQuery); 93 }, 94 checkUnsupportedPermissions(permissions) { 95 const promisesToQuery = permissions.map(({ name }) => { 96 return iframeWindow.navigator.permissions 97 .query({ name }) 98 .then( 99 () => ok(false, `query should not have resolved for '${name}'`), 100 error => { 101 is(error.name, 'TypeError', 102 `query should have thrown TypeError for '${name}'`); 103 } 104 ); 105 }); 106 return Promise.all(promisesToQuery); 107 }, 108 promiseStateChanged(name, state) { 109 return iframeWindow.navigator.permissions 110 .query({ name }) 111 .then(status => { 112 return new Promise( resolve => { 113 status.onchange = () => { 114 status.onchange = null; 115 is(status.state, state, `state changed for '${name}'`); 116 resolve(); 117 }; 118 }); 119 }, 120 () => ok(false, `query should not have rejected for '${name}'`)); 121 }, 122 testStatusOnChange() { 123 return new Promise((resolve) => { 124 SpecialPowers.popPermissions(() => { 125 const permission = 'geolocation'; 126 const promiseGranted = this.promiseStateChanged(permission, 'granted'); 127 this.setPermissions(ALLOW_ACTION); 128 promiseGranted.then(async () => { 129 const promisePrompt = this.promiseStateChanged(permission, 'prompt'); 130 await SpecialPowers.popPermissions(); 131 return promisePrompt; 132 }).then(resolve); 133 }); 134 }); 135 }, 136 testInvalidQuery() { 137 return iframeWindow.navigator.permissions 138 .query({ name: 'invalid' }) 139 .then( 140 () => ok(false, 'invalid query should not have resolved'), 141 () => ok(true, 'invalid query should have rejected') 142 ); 143 }, 144 async testNotFullyActiveDoc() { 145 const iframe1 = await createIframe(); 146 const expectedErrorClass = iframe1.contentWindow.DOMException; 147 const permAPI = iframe1.contentWindow.navigator.permissions; 148 // Document no longer fully active 149 iframe1.remove(); 150 await new Promise((res) => { 151 permAPI.query({ name: "geolocation" }).catch((error) => { 152 ok( 153 error instanceof expectedErrorClass, 154 "DOMException from other realm" 155 ); 156 is( 157 error.name, 158 "InvalidStateError", 159 "Must reject with a InvalidStateError" 160 ); 161 iframe1.remove(); 162 res(); 163 }); 164 }); 165 }, 166 async testNotFullyActiveChange() { 167 await SpecialPowers.popPermissions(); 168 const iframe2 = await createIframe(); 169 const initialStatus = await iframe2.contentWindow.navigator.permissions.query( 170 { name: "geolocation" } 171 ); 172 await SpecialPowers.pushPermissions([ 173 { 174 type: "geo", 175 allow: PROMPT_ACTION, 176 context: iframe2.contentWindow.document, 177 }, 178 ]); 179 is( 180 initialStatus.state, 181 "prompt", 182 "Initially the iframe's permission is prompt" 183 ); 184 185 // Document no longer fully active 186 const stolenDoc = iframe2.contentWindow.document; 187 iframe2.remove(); 188 initialStatus.onchange = () => { 189 ok(false, "onchange must not fire when document is not fully active."); 190 }; 191 // We set it to grant for this origin, but the PermissionStatus doesn't change. 192 await SpecialPowers.pushPermissions([ 193 { 194 type: "geo", 195 allow: ALLOW_ACTION, 196 context: stolenDoc, 197 }, 198 ]); 199 is( 200 initialStatus.state, 201 "prompt", 202 "Inactive document's permission must not change" 203 ); 204 205 // Re-attach the iframe 206 document.body.appendChild(iframe2); 207 await new Promise((res) => (iframe2.onload = res)); 208 // Fully active again 209 const newStatus = await iframe2.contentWindow.navigator.permissions.query({ 210 name: "geolocation", 211 }); 212 is(newStatus.state, "granted", "Reflect that we are granted"); 213 214 const newEventPromise = new Promise((res) => (newStatus.onchange = res)); 215 await SpecialPowers.pushPermissions([ 216 { 217 type: "geo", 218 allow: DENY_ACTION, 219 context: iframe2.contentWindow.document, 220 }, 221 ]); 222 // Event fires... 223 await newEventPromise; 224 is(initialStatus.state, "prompt", "Remains prompt, as it's actually dead."); 225 is(newStatus.state, "denied", "New status must be 'denied'."); 226 iframe2.remove(); 227 }, 228 }; 229 } 230 231 function createIframe() { 232 return new Promise((resolve) => { 233 const iframe = document.createElement('iframe'); 234 iframe.src = 'file_empty.html'; 235 iframe.onload = () => resolve(iframe); 236 document.body.appendChild(iframe); 237 }); 238 } 239 240 window.onload = async () => { 241 try { 242 const tester = createPermissionTester(await createIframe()); 243 await tester.checkUnsupportedPermissions(UNSUPPORTED_PERMISSIONS); 244 await tester.setPermissions(UNKNOWN_ACTION); 245 await tester.checkPermissions(PERMISSIONS, 'prompt'); 246 await tester.setPermissions(PROMPT_ACTION); 247 await tester.checkPermissions(PERMISSIONS, 'prompt', 'granted'); 248 await tester.setPermissions(ALLOW_ACTION); 249 await tester.checkPermissions(PERMISSIONS, 'granted'); 250 await tester.setPermissions(DENY_ACTION); 251 await tester.checkPermissions(PERMISSIONS, 'denied'); 252 await tester.testStatusOnChange(); 253 await tester.testInvalidQuery(); 254 await tester.testNotFullyActiveDoc(); 255 await tester.testNotFullyActiveChange(); 256 257 await SpecialPowers.pushPrefEnv({ 258 set: [ 259 ["privacy.resistFingerprinting", true] 260 ] 261 }); 262 await tester.setPermissions(PROMPT_ACTION); 263 await tester.checkPermissions(PERMISSIONS, 'prompt', 'prompt'); 264 await SpecialPowers.popPrefEnv(); 265 266 await SpecialPowers.pushPrefEnv({ 267 set: [ 268 ["permissions.media.query.enabled", false] 269 ] 270 }); 271 await tester.setPermissions(UNKNOWN_ACTION); 272 await tester.checkUnsupportedPermissions(MEDIA_PERMISSIONS); 273 await tester.checkPermissions(OTHER_PERMISSIONS, 'prompt'); 274 } finally { 275 SimpleTest.finish(); 276 } 277 }; 278 </script> 279 </body> 280 281 </html>