tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>