test_fullscreen-api-rapid-cycle.html (5418B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Test for rapid cycling of Fullscreen API requests</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <script src="/tests/SimpleTest/EventUtils.js"></script> 7 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> 8 </head> 9 <body> 10 <script> 11 12 // There are two ways that web content should be able to reliably 13 // request and respond to fullscreen: 14 // 15 // 1) Wait on the requestFullscreen() and exitFullscreen() promises. 16 // 2) Respond to the "fullscreenchange" and "fullscreenerror" events 17 // after calling requestFullscreen() or exitFullscreen(). 18 // 19 // This test exercises both methods rapidly, while checking to see 20 // if any expected signal is taking too long. If awaiting a promise 21 // or waiting for an event takes longer than some number of seconds, 22 // the test will fail instead of timing out. This is to help detect 23 // vulnerabilities in the implementation which would slow down the 24 // test harness waiting for the timeout. 25 26 // How many enter-exit cycles we run for each method of detecting a 27 // fullscreen transition. 28 const CYCLE_COUNT = 3; 29 30 // How long do we wait for one transition before considering it as 31 // an error. 32 const TOO_LONG_SECONDS = 3; 33 34 SimpleTest.requestFlakyTimeout("We race against Promises to turn possible timeouts into errors."); 35 36 function rejectAfterTooLong() { 37 return new Promise((resolve, reject) => { 38 const fail = () => { 39 reject(`timeout after ${TOO_LONG_SECONDS} seconds`); 40 } 41 setTimeout(fail, TOO_LONG_SECONDS * 1000); 42 }); 43 } 44 45 add_setup(async () => { 46 await SpecialPowers.pushPrefEnv({ 47 "set": [ 48 // Keep the test structure simple. 49 ["full-screen-api.allow-trusted-requests-only", false], 50 51 // Make macOS fullscreen transitions asynchronous. 52 ["full-screen-api.macos-native-full-screen", true], 53 54 // Clarify that even no-duration async transitions are vulnerable. 55 ["full-screen-api.transition-duration.enter", "0 0"], 56 ["full-screen-api.transition-duration.leave", "0 0"], 57 ] 58 }); 59 }); 60 61 add_task(ensureOutOfFullscreen); 62 63 // It is an implementation detail that promises resolve first, and 64 // then events are fired on a later event loop. For this reason, 65 // it's very important that we do the rapidCycleAwaitEvents task 66 // first, because we don't want to have any "stray" fullscreenchange 67 // events in the pipeline when we start that task. Conversely, 68 // there's really no way for the rapidCycleAwaitEvents to poison 69 // the environment for the next task, which waits on promises. 70 add_task(rapidCycleAwaitEvents); 71 72 add_task(ensureOutOfFullscreen); 73 74 add_task(rapidCycleAwaitPromises); 75 76 add_task(() => { ok(true, "Completed test with one expected result."); }); 77 78 // This is a helper function to repeatedly invoke a Promise generator 79 // until the Promise resolves, delaying by one event loop on each 80 // attempt. 81 async function repeatUntilSuccessful(f) { 82 let successful = false; 83 do { 84 try { 85 // Delay one event loop. 86 await new Promise(r => SimpleTest.executeSoon(r)); 87 await f(); 88 successful = true; 89 } catch (error) { 90 info(`repeatUntilSuccessful: error ${error}.`); 91 } 92 } while(!successful); 93 } 94 95 async function ensureOutOfFullscreen() { 96 // Repeatedly call exitFullscreen until we get out. 97 await repeatUntilSuccessful(async () => { 98 if (document.fullscreenElement) { 99 await document.exitFullscreen(); 100 } 101 if (document.fullscreenElement) { 102 throw new Error("still in fullscreen"); 103 } 104 }); 105 } 106 107 async function rapidCycleAwaitEvents() { 108 const receiveOneFullscreenchange = () => { 109 return new Promise(resolve => { 110 document.addEventListener("fullscreenchange", resolve, { once: true }); 111 }); 112 }; 113 114 let gotError = false; 115 for (let cycle = 0; cycle < CYCLE_COUNT; cycle++) { 116 info(`Event cycle ${cycle} request fullscreen.`); 117 const enterPromise = receiveOneFullscreenchange(); 118 document.documentElement.requestFullscreen(); 119 await Promise.race([enterPromise, rejectAfterTooLong()]).catch(error => { 120 ok(false, `Event cycle ${cycle} requestFullscreen errored with ${error}.`); 121 gotError = true; 122 }); 123 if (gotError) { 124 break; 125 } 126 127 info(`Event cycle ${cycle} exit fullscreen.`); 128 const exitPromise = receiveOneFullscreenchange(); 129 document.exitFullscreen(); 130 await Promise.race([exitPromise, rejectAfterTooLong()]).catch(error => { 131 ok(false, `Event cycle ${cycle} exitFullscreen errored with ${error}.`); 132 gotError = true; 133 }); 134 if (gotError) { 135 break; 136 } 137 } 138 } 139 140 async function rapidCycleAwaitPromises() { 141 let gotError = false; 142 for (let cycle = 0; cycle < CYCLE_COUNT; cycle++) { 143 info(`Promise cycle ${cycle} request fullscreen.`); 144 const enterPromise = document.documentElement.requestFullscreen(); 145 await Promise.race([enterPromise, rejectAfterTooLong()]).catch(error => { 146 ok(false, `Promise cycle ${cycle} requestFullscreen errored with ${error}.`); 147 gotError = true; 148 }); 149 if (gotError) { 150 break; 151 } 152 153 info(`Promise cycle ${cycle} exit fullscreen.`); 154 const exitPromise = document.exitFullscreen(); 155 await Promise.race([exitPromise, rejectAfterTooLong()]).catch(error => { 156 ok(false, `Promise cycle ${cycle} exitFullscreen errored with ${error}.`); 157 gotError = true; 158 }); 159 if (gotError) { 160 break; 161 } 162 } 163 } 164 165 </script> 166 </body> 167 </html>