helper_touch_action_regions.html (14565B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width; initial-scale=1.0"> 6 <title>Test to ensure APZ doesn't always wait for touch-action</title> 7 <script type="application/javascript" src="apz_test_native_event_utils.js"></script> 8 <script type="application/javascript" src="apz_test_utils.js"></script> 9 <script src="/tests/SimpleTest/paint_listener.js"></script> 10 <script type="application/javascript"> 11 12 function failure(e) { 13 ok(false, "This event listener should not have triggered: " + e.type); 14 } 15 16 function listener(callback) { 17 return function(e) { 18 ok(e.type == "touchstart", "The touchstart event handler was triggered after snapshotting completed"); 19 setTimeout(callback, 0); 20 }; 21 } 22 23 // This helper function provides a way for the child process to synchronously 24 // check how many touch events the chrome process main-thread has processed. This 25 // function can be called with three values: 'start', 'report', and 'end'. 26 // The 'start' invocation sets up the listeners, and should be invoked before 27 // the touch events of interest are generated. This should only be called once. 28 // This returns true on success, and false on failure. 29 // The 'report' invocation can be invoked multiple times, and returns an object 30 // (in JSON string format) containing the counters. 31 // The 'end' invocation tears down the listeners, and should be invoked once 32 // at the end to clean up. Returns true on success, false on failure. 33 function chromeTouchEventCounter(operation) { 34 function chromeProcessCounter() { 35 /* eslint-env mozilla/chrome-script */ 36 const PREFIX = "apz:ctec:"; 37 38 const LISTENERS = { 39 "start": function() { 40 var topWin = Services.wm.getMostRecentWindow("navigator:browser"); 41 if (!topWin) { 42 topWin = Services.wm.getMostRecentWindow("navigator:geckoview"); 43 } 44 if (typeof topWin.eventCounts != "undefined") { 45 dump("Found pre-existing eventCounts object on the top window!\n"); 46 return false; 47 } 48 topWin.eventCounts = { "touchstart": 0, "touchmove": 0, "touchend": 0 }; 49 topWin.counter = function(e) { 50 topWin.eventCounts[e.type]++; 51 }; 52 53 topWin.addEventListener("touchstart", topWin.counter, { passive: true }); 54 topWin.addEventListener("touchmove", topWin.counter, { passive: true }); 55 topWin.addEventListener("touchend", topWin.counter, { passive: true }); 56 57 return true; 58 }, 59 60 "report": function() { 61 var topWin = Services.wm.getMostRecentWindow("navigator:browser"); 62 if (!topWin) { 63 topWin = Services.wm.getMostRecentWindow("navigator:geckoview"); 64 } 65 return JSON.stringify(topWin.eventCounts); 66 }, 67 68 "end": function() { 69 for (let [msg, func] of Object.entries(LISTENERS)) { 70 Services.ppmm.removeMessageListener(PREFIX + msg, func); 71 } 72 73 var topWin = Services.wm.getMostRecentWindow("navigator:browser"); 74 if (!topWin) { 75 topWin = Services.wm.getMostRecentWindow("navigator:geckoview"); 76 } 77 if (typeof topWin.eventCounts == "undefined") { 78 dump("The eventCounts object was not found on the top window!\n"); 79 return false; 80 } 81 topWin.removeEventListener("touchstart", topWin.counter); 82 topWin.removeEventListener("touchmove", topWin.counter); 83 topWin.removeEventListener("touchend", topWin.counter); 84 delete topWin.counter; 85 delete topWin.eventCounts; 86 return true; 87 }, 88 }; 89 90 for (let [msg, func] of Object.entries(LISTENERS)) { 91 Services.ppmm.addMessageListener(PREFIX + msg, func); 92 } 93 } 94 95 if (typeof chromeTouchEventCounter.chromeHelper == "undefined") { 96 // This is the first time chromeTouchEventCounter is being called; do initialization 97 chromeTouchEventCounter.chromeHelper = SpecialPowers.loadChromeScript(chromeProcessCounter); 98 ApzCleanup.register(function() { chromeTouchEventCounter.chromeHelper.destroy(); }); 99 } 100 101 return SpecialPowers.Services.cpmm.sendSyncMessage(`apz:ctec:${operation}`, "")[0]; 102 } 103 104 // Simple wrapper that waits until the chrome process has seen |count| instances 105 // of the |eventType| event. Returns true on success, and false if 10 seconds 106 // go by without the condition being satisfied. 107 function waitFor(eventType, count) { 108 var start = Date.now(); 109 while (JSON.parse(chromeTouchEventCounter("report"))[eventType] != count) { 110 if (Date.now() - start > 10000) { 111 // It's taking too long, let's abort 112 return false; 113 } 114 } 115 return true; 116 } 117 118 function RunAfterProcessedQueuedInputEvents(aCallback) { 119 let tm = SpecialPowers.Services.tm; 120 tm.dispatchToMainThread(aCallback, SpecialPowers.Ci.nsIRunnablePriority.PRIORITY_INPUT_HIGH); 121 } 122 123 var scrollerPosition; 124 async function getScrollerPosition() { 125 const scroller = document.getElementById("scroller"); 126 scrollerPosition = await coordinatesRelativeToScreen({ 127 offsetX: 0, 128 offsetY: 0, 129 target: scroller, 130 }); 131 } 132 133 function* test(testDriver) { 134 // The main part of this test should run completely before the child process' 135 // main-thread deals with the touch event, so check to make sure that happens. 136 document.body.addEventListener("touchstart", failure, { passive: true }); 137 138 // What we want here is to synthesize all of the touch events (from this code in 139 // the child process), and have the chrome process generate and process them, 140 // but not allow the events to be dispatched back into the child process until 141 // later. This allows us to ensure that the APZ in the chrome process is not 142 // waiting for the child process to send notifications upon processing the 143 // events. If it were doing so, the APZ would block and this test would fail. 144 145 // In order to actually implement this, we call the synthesize functions with 146 // a async callback in between. The synthesize functions just queue up a 147 // runnable on the child process main thread and return immediately, so with 148 // the async callbacks, the child process main thread queue looks like 149 // this after we're done setting it up: 150 // synthesizeTouchStart 151 // callback testDriver 152 // synthesizeTouchMove 153 // callback testDriver 154 // ... 155 // synthesizeTouchEnd 156 // callback testDriver 157 // 158 // If, after setting up this queue, we yield once, the first synthesization and 159 // callback will run - this will send a synthesization message to the chrome 160 // process, and return control back to us right away. When the chrome process 161 // processes with the synthesized event, it will dispatch the DOM touch event 162 // back to the child process over IPC, which will go into the end of the child 163 // process main thread queue, like so: 164 // synthesizeTouchStart (done) 165 // invoke testDriver (done) 166 // synthesizeTouchMove 167 // invoke testDriver 168 // ... 169 // synthesizeTouchEnd 170 // invoke testDriver 171 // handle DOM touchstart <-- touchstart goes at end of queue 172 // 173 // As we continue yielding one at a time, the synthesizations run, and the 174 // touch events get added to the end of the queue. As we yield, we take 175 // snapshots in the chrome process, to make sure that the APZ has started 176 // scrolling even though we know we haven't yet processed the DOM touch events 177 // in the child process yet. 178 // 179 // Note that the "async callback" we use here is SpecialPowers.tm.dispatchToMainThread 180 // with priority = input, because nothing else does exactly what we want: 181 // - setTimeout(..., 0) does not maintain ordering, because it respects the 182 // time delta provided (i.e. the callback can jump the queue to meet its 183 // deadline). 184 // - SpecialPowers.spinEventLoop and SpecialPowers.executeAfterFlushingMessageQueue 185 // are not e10s friendly, and can get arbitrarily delayed due to IPC 186 // round-trip time. 187 // - SimpleTest.executeSoon has a codepath that delegates to setTimeout, so 188 // is less reliable if it ever decides to switch to that codepath. 189 // - SpecialPowers.executeSoon dispatches a task to main thread. However, 190 // normal runnables may be preempted by input events and be executed in an 191 // unexpected order. 192 193 // Also note that this test is intentionally kept as a yield-style test using 194 // the runContinuation helper, even though all other similar tests have since 195 // been migrated to using async/await and Promise-based architectures. This is 196 // because yield and async/await have different semantics with respect to 197 // timing, and this test requires very specific timing behaviour (as described 198 // above). 199 200 // The other problem we need to deal with is the asynchronicity in the chrome 201 // process. That is, we might request a snapshot before the chrome process has 202 // actually synthesized the event and processed it. To guard against this, we 203 // register a thing in the chrome process that counts the touch events that 204 // have been dispatched, and poll that thing synchronously in order to make 205 // sure we only snapshot after the event in question has been processed. 206 // That's what the chromeTouchEventCounter business is all about. The sync 207 // polling looks bad but in practice only ends up needing to poll once or 208 // twice before the condition is satisfied, and as an extra precaution we add 209 // a time guard so it fails after 10s of polling. 210 211 // So, here we go... 212 213 // Set up the chrome process touch listener 214 ok(chromeTouchEventCounter("start"), "Chrome touch counter registered"); 215 216 // Set up the child process events and callbacks 217 var scroller = document.getElementById("scroller"); 218 var utils = utilsForTarget(window); 219 utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, 220 scrollerPosition.x + 10, scrollerPosition.y + 110, 221 1, 90, null); 222 RunAfterProcessedQueuedInputEvents(testDriver); 223 for (let i = 1; i < 10; i++) { 224 utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, 225 scrollerPosition.x + 10, 226 scrollerPosition.y + 110 - (i * 10), 227 1, 90, null); 228 RunAfterProcessedQueuedInputEvents(testDriver); 229 } 230 utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, 231 scrollerPosition.x + 10, 232 scrollerPosition.y + 10, 233 1, 90, null); 234 RunAfterProcessedQueuedInputEvents(testDriver); 235 ok(true, "Finished setting up event queue"); 236 237 // Get our baseline snapshot 238 var rect = rectRelativeToScreen(scroller); 239 var lastSnapshot = getSnapshot(rect); 240 ok(true, "Got baseline snapshot"); 241 var numDifferentSnapshotPairs = 0; 242 243 yield; // this will tell the chrome process to synthesize the touchstart event 244 // and then we wait to make sure it got processed: 245 ok(waitFor("touchstart", 1), "Touchstart processed in chrome process"); 246 247 // Loop through the touchmove events 248 for (let i = 1; i < 10; i++) { 249 yield; 250 ok(waitFor("touchmove", i), "Touchmove processed in chrome process"); 251 252 // Take a snapshot after each touch move event. This forces 253 // a composite each time, even we don't get a vsync in this 254 // interval. 255 var snapshot = getSnapshot(rect); 256 if (lastSnapshot != snapshot) { 257 numDifferentSnapshotPairs += 1; 258 } 259 lastSnapshot = snapshot; 260 } 261 262 // Check that the snapshot has changed since the baseline, indicating 263 // that the touch events caused async scrolling. Note that, since we 264 // orce a composite after each touch event, even if there is a frame 265 // of delay between APZ processing a touch event and the compositor 266 // applying the async scroll (bug 1375949), by the end of the gesture 267 // the snapshot should have changed. 268 ok(numDifferentSnapshotPairs > 0, 269 "The number of different snapshot pairs was " + numDifferentSnapshotPairs); 270 271 // Wait for the touchend as well, to clear all pending testDriver resumes 272 yield; 273 ok(waitFor("touchend", 1), "Touchend processed in chrome process"); 274 275 // Clean up the chrome process hooks 276 chromeTouchEventCounter("end"); 277 278 // Now we are going to release our grip on the child process main thread, 279 // so that all the DOM events that were queued up can be processed. We 280 // register a touchstart listener to make sure this happens. 281 document.body.removeEventListener("touchstart", failure); 282 var listenerFunc = listener(testDriver); 283 document.body.addEventListener("touchstart", listenerFunc, { passive: true }); 284 dump("done registering listener, going to yield\n"); 285 yield; 286 document.body.removeEventListener("touchstart", listenerFunc); 287 } 288 289 // Despite what this function name says, this does not *directly* run the 290 // provided continuation testFunction. Instead, it returns a function that 291 // can be used to run the continuation. The extra level of indirection allows 292 // it to be more easily added to a promise chain, like so: 293 // waitUntilApzStable().then(runContinuation(myTest)); 294 function runContinuation(testFunction) { 295 return function() { 296 return new Promise(function(resolve) { 297 var testContinuation = null; 298 299 function driveTest() { 300 if (!testContinuation) { 301 testContinuation = testFunction(driveTest); 302 } 303 var ret = testContinuation.next(); 304 if (ret.done) { 305 resolve(); 306 } 307 } 308 309 try { 310 driveTest(); 311 } catch (ex) { 312 ok( 313 false, 314 "APZ test continuation failed with exception: " + ex 315 ); 316 } 317 }); 318 }; 319 } 320 321 if (SpecialPowers.isMainProcess()) { 322 // This is probably android, where everything is single-process. The 323 // test structure depends on e10s, so the test won't run properly on 324 // this platform. Skip it 325 ok(true, "Skipping test because it is designed to run from the content process"); 326 subtestDone(); 327 } else { 328 waitUntilApzStable() 329 .then(async () => { await getScrollerPosition(); }) 330 .then(runContinuation(test)) 331 .then(subtestDone, subtestFailed); 332 } 333 334 </script> 335 </head> 336 <body> 337 <div id="scroller" style="width: 400px; height: 400px; overflow: scroll; touch-action: pan-y"> 338 <div style="width: 200px; height: 200px; background-color: lightgreen;"> 339 This is a colored div that will move on the screen as the scroller scrolls. 340 </div> 341 <div style="width: 1000px; height: 1000px; background-color: lightblue"> 342 This is a large div to make the scroller scrollable. 343 </div> 344 </body> 345 </html>