summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
blob: 6a8a09e55a5bbb0b35fd5b643d0c16d20d1c40d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
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>