summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers/apz/test/mochitest/helper_touch_action_regions.html')
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_regions.html345
1 files changed, 345 insertions, 0 deletions
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
new file mode 100644
index 0000000000..6a8a09e55a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test to ensure APZ doesn't always wait for touch-action</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function failure(e) {
+ ok(false, "This event listener should not have triggered: " + e.type);
+}
+
+function listener(callback) {
+ return function(e) {
+ ok(e.type == "touchstart", "The touchstart event handler was triggered after snapshotting completed");
+ setTimeout(callback, 0);
+ };
+}
+
+// This helper function provides a way for the child process to synchronously
+// check how many touch events the chrome process main-thread has processed. This
+// function can be called with three values: 'start', 'report', and 'end'.
+// The 'start' invocation sets up the listeners, and should be invoked before
+// the touch events of interest are generated. This should only be called once.
+// This returns true on success, and false on failure.
+// The 'report' invocation can be invoked multiple times, and returns an object
+// (in JSON string format) containing the counters.
+// The 'end' invocation tears down the listeners, and should be invoked once
+// at the end to clean up. Returns true on success, false on failure.
+function chromeTouchEventCounter(operation) {
+ function chromeProcessCounter() {
+ /* eslint-env mozilla/chrome-script */
+ const PREFIX = "apz:ctec:";
+
+ const LISTENERS = {
+ "start": function() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ if (typeof topWin.eventCounts != "undefined") {
+ dump("Found pre-existing eventCounts object on the top window!\n");
+ return false;
+ }
+ topWin.eventCounts = { "touchstart": 0, "touchmove": 0, "touchend": 0 };
+ topWin.counter = function(e) {
+ topWin.eventCounts[e.type]++;
+ };
+
+ topWin.addEventListener("touchstart", topWin.counter, { passive: true });
+ topWin.addEventListener("touchmove", topWin.counter, { passive: true });
+ topWin.addEventListener("touchend", topWin.counter, { passive: true });
+
+ return true;
+ },
+
+ "report": function() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ return JSON.stringify(topWin.eventCounts);
+ },
+
+ "end": function() {
+ for (let [msg, func] of Object.entries(LISTENERS)) {
+ Services.ppmm.removeMessageListener(PREFIX + msg, func);
+ }
+
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ if (typeof topWin.eventCounts == "undefined") {
+ dump("The eventCounts object was not found on the top window!\n");
+ return false;
+ }
+ topWin.removeEventListener("touchstart", topWin.counter);
+ topWin.removeEventListener("touchmove", topWin.counter);
+ topWin.removeEventListener("touchend", topWin.counter);
+ delete topWin.counter;
+ delete topWin.eventCounts;
+ return true;
+ },
+ };
+
+ for (let [msg, func] of Object.entries(LISTENERS)) {
+ Services.ppmm.addMessageListener(PREFIX + msg, func);
+ }
+ }
+
+ if (typeof chromeTouchEventCounter.chromeHelper == "undefined") {
+ // This is the first time chromeTouchEventCounter is being called; do initialization
+ chromeTouchEventCounter.chromeHelper = SpecialPowers.loadChromeScript(chromeProcessCounter);
+ ApzCleanup.register(function() { chromeTouchEventCounter.chromeHelper.destroy(); });
+ }
+
+ return SpecialPowers.Services.cpmm.sendSyncMessage(`apz:ctec:${operation}`, "")[0];
+}
+
+// Simple wrapper that waits until the chrome process has seen |count| instances
+// of the |eventType| event. Returns true on success, and false if 10 seconds
+// go by without the condition being satisfied.
+function waitFor(eventType, count) {
+ var start = Date.now();
+ while (JSON.parse(chromeTouchEventCounter("report"))[eventType] != count) {
+ if (Date.now() - start > 10000) {
+ // It's taking too long, let's abort
+ return false;
+ }
+ }
+ return true;
+}
+
+function RunAfterProcessedQueuedInputEvents(aCallback) {
+ let tm = SpecialPowers.Services.tm;
+ tm.dispatchToMainThread(aCallback, SpecialPowers.Ci.nsIRunnablePriority.PRIORITY_INPUT_HIGH);
+}
+
+var scrollerPosition;
+async function getScrollerPosition() {
+ const scroller = document.getElementById("scroller");
+ scrollerPosition = await coordinatesRelativeToScreen({
+ offsetX: 0,
+ offsetY: 0,
+ target: scroller,
+ });
+}
+
+function* test(testDriver) {
+ // The main part of this test should run completely before the child process'
+ // main-thread deals with the touch event, so check to make sure that happens.
+ document.body.addEventListener("touchstart", failure, { passive: true });
+
+ // What we want here is to synthesize all of the touch events (from this code in
+ // the child process), and have the chrome process generate and process them,
+ // but not allow the events to be dispatched back into the child process until
+ // later. This allows us to ensure that the APZ in the chrome process is not
+ // waiting for the child process to send notifications upon processing the
+ // events. If it were doing so, the APZ would block and this test would fail.
+
+ // In order to actually implement this, we call the synthesize functions with
+ // a async callback in between. The synthesize functions just queue up a
+ // runnable on the child process main thread and return immediately, so with
+ // the async callbacks, the child process main thread queue looks like
+ // this after we're done setting it up:
+ // synthesizeTouchStart
+ // callback testDriver
+ // synthesizeTouchMove
+ // callback testDriver
+ // ...
+ // synthesizeTouchEnd
+ // callback testDriver
+ //
+ // If, after setting up this queue, we yield once, the first synthesization and
+ // callback will run - this will send a synthesization message to the chrome
+ // process, and return control back to us right away. When the chrome process
+ // processes with the synthesized event, it will dispatch the DOM touch event
+ // back to the child process over IPC, which will go into the end of the child
+ // process main thread queue, like so:
+ // synthesizeTouchStart (done)
+ // invoke testDriver (done)
+ // synthesizeTouchMove
+ // invoke testDriver
+ // ...
+ // synthesizeTouchEnd
+ // invoke testDriver
+ // handle DOM touchstart <-- touchstart goes at end of queue
+ //
+ // As we continue yielding one at a time, the synthesizations run, and the
+ // touch events get added to the end of the queue. As we yield, we take
+ // snapshots in the chrome process, to make sure that the APZ has started
+ // scrolling even though we know we haven't yet processed the DOM touch events
+ // in the child process yet.
+ //
+ // Note that the "async callback" we use here is SpecialPowers.tm.dispatchToMainThread
+ // with priority = input, because nothing else does exactly what we want:
+ // - setTimeout(..., 0) does not maintain ordering, because it respects the
+ // time delta provided (i.e. the callback can jump the queue to meet its
+ // deadline).
+ // - SpecialPowers.spinEventLoop and SpecialPowers.executeAfterFlushingMessageQueue
+ // are not e10s friendly, and can get arbitrarily delayed due to IPC
+ // round-trip time.
+ // - SimpleTest.executeSoon has a codepath that delegates to setTimeout, so
+ // is less reliable if it ever decides to switch to that codepath.
+ // - SpecialPowers.executeSoon dispatches a task to main thread. However,
+ // normal runnables may be preempted by input events and be executed in an
+ // unexpected order.
+
+ // Also note that this test is intentionally kept as a yield-style test using
+ // the runContinuation helper, even though all other similar tests have since
+ // been migrated to using async/await and Promise-based architectures. This is
+ // because yield and async/await have different semantics with respect to
+ // timing, and this test requires very specific timing behaviour (as described
+ // above).
+
+ // The other problem we need to deal with is the asynchronicity in the chrome
+ // process. That is, we might request a snapshot before the chrome process has
+ // actually synthesized the event and processed it. To guard against this, we
+ // register a thing in the chrome process that counts the touch events that
+ // have been dispatched, and poll that thing synchronously in order to make
+ // sure we only snapshot after the event in question has been processed.
+ // That's what the chromeTouchEventCounter business is all about. The sync
+ // polling looks bad but in practice only ends up needing to poll once or
+ // twice before the condition is satisfied, and as an extra precaution we add
+ // a time guard so it fails after 10s of polling.
+
+ // So, here we go...
+
+ // Set up the chrome process touch listener
+ ok(chromeTouchEventCounter("start"), "Chrome touch counter registered");
+
+ // Set up the child process events and callbacks
+ var scroller = document.getElementById("scroller");
+ var utils = utilsForTarget(window);
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ scrollerPosition.x + 10, scrollerPosition.y + 110,
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ for (let i = 1; i < 10; i++) {
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ scrollerPosition.x + 10,
+ scrollerPosition.y + 110 - (i * 10),
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ }
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ scrollerPosition.x + 10,
+ scrollerPosition.y + 10,
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ ok(true, "Finished setting up event queue");
+
+ // Get our baseline snapshot
+ var rect = rectRelativeToScreen(scroller);
+ var lastSnapshot = getSnapshot(rect);
+ ok(true, "Got baseline snapshot");
+ var numDifferentSnapshotPairs = 0;
+
+ yield; // this will tell the chrome process to synthesize the touchstart event
+ // and then we wait to make sure it got processed:
+ ok(waitFor("touchstart", 1), "Touchstart processed in chrome process");
+
+ // Loop through the touchmove events
+ for (let i = 1; i < 10; i++) {
+ yield;
+ ok(waitFor("touchmove", i), "Touchmove processed in chrome process");
+
+ // Take a snapshot after each touch move event. This forces
+ // a composite each time, even we don't get a vsync in this
+ // interval.
+ var snapshot = getSnapshot(rect);
+ if (lastSnapshot != snapshot) {
+ numDifferentSnapshotPairs += 1;
+ }
+ lastSnapshot = snapshot;
+ }
+
+ // Check that the snapshot has changed since the baseline, indicating
+ // that the touch events caused async scrolling. Note that, since we
+ // orce a composite after each touch event, even if there is a frame
+ // of delay between APZ processing a touch event and the compositor
+ // applying the async scroll (bug 1375949), by the end of the gesture
+ // the snapshot should have changed.
+ ok(numDifferentSnapshotPairs > 0,
+ "The number of different snapshot pairs was " + numDifferentSnapshotPairs);
+
+ // Wait for the touchend as well, to clear all pending testDriver resumes
+ yield;
+ ok(waitFor("touchend", 1), "Touchend processed in chrome process");
+
+ // Clean up the chrome process hooks
+ chromeTouchEventCounter("end");
+
+ // Now we are going to release our grip on the child process main thread,
+ // so that all the DOM events that were queued up can be processed. We
+ // register a touchstart listener to make sure this happens.
+ document.body.removeEventListener("touchstart", failure);
+ var listenerFunc = listener(testDriver);
+ document.body.addEventListener("touchstart", listenerFunc, { passive: true });
+ dump("done registering listener, going to yield\n");
+ yield;
+ document.body.removeEventListener("touchstart", listenerFunc);
+}
+
+// Despite what this function name says, this does not *directly* run the
+// provided continuation testFunction. Instead, it returns a function that
+// can be used to run the continuation. The extra level of indirection allows
+// it to be more easily added to a promise chain, like so:
+// waitUntilApzStable().then(runContinuation(myTest));
+function runContinuation(testFunction) {
+ return function() {
+ return new Promise(function(resolve, reject) {
+ var testContinuation = null;
+
+ function driveTest() {
+ if (!testContinuation) {
+ testContinuation = testFunction(driveTest);
+ }
+ var ret = testContinuation.next();
+ if (ret.done) {
+ resolve();
+ }
+ }
+
+ try {
+ driveTest();
+ } catch (ex) {
+ ok(
+ false,
+ "APZ test continuation failed with exception: " + ex
+ );
+ }
+ });
+ };
+}
+
+if (SpecialPowers.isMainProcess()) {
+ // This is probably android, where everything is single-process. The
+ // test structure depends on e10s, so the test won't run properly on
+ // this platform. Skip it
+ ok(true, "Skipping test because it is designed to run from the content process");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(async () => { await getScrollerPosition(); })
+ .then(runContinuation(test))
+ .then(subtestDone, subtestFailed);
+}
+
+ </script>
+</head>
+<body>
+ <div id="scroller" style="width: 400px; height: 400px; overflow: scroll; touch-action: pan-y">
+ <div style="width: 200px; height: 200px; background-color: lightgreen;">
+ This is a colored div that will move on the screen as the scroller scrolls.
+ </div>
+ <div style="width: 1000px; height: 1000px; background-color: lightblue">
+ This is a large div to make the scroller scrollable.
+ </div>
+</body>
+</html>