bug 1492499: marionette: add debounce sync primitive; r=automatedtester

This adds a new synchronisation primitive to Marionette which will
allow us to wait for the last in a sequence of events to fire.
This is especially useful for high-frequency events such as the DOM
resize event, where multiple resize events may fire as the window
dimensions are being changed.

Depends on D8411

Differential Revision: https://phabricator.services.mozilla.com/D8412

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Andreas Tolfsen 2018-11-08 13:11:14 +00:00
parent 5e151181ca
commit 78a0a14d3d
2 changed files with 119 additions and 0 deletions

View File

@ -17,6 +17,7 @@ const {Log} = ChromeUtils.import("chrome://marionette/content/log.js", {});
XPCOMUtils.defineLazyGetter(this, "log", Log.get); XPCOMUtils.defineLazyGetter(this, "log", Log.get);
this.EXPORTED_SYMBOLS = [ this.EXPORTED_SYMBOLS = [
"DebounceCallback",
"IdlePromise", "IdlePromise",
"MessageManagerDestroyedPromise", "MessageManagerDestroyedPromise",
"PollPromise", "PollPromise",
@ -285,3 +286,62 @@ function IdlePromise(win) {
}); });
}); });
} }
/**
* Wraps a callback function, that, as long as it continues to be
* invoked, will not be triggered. The given function will be
* called after the timeout duration is reached, after no more
* events fire.
*
* This class implements the {@link EventListener} interface,
* which means it can be used interchangably with `addEventHandler`.
*
* Debouncing events can be useful when dealing with e.g. DOM events
* that fire at a high rate. It is generally advisable to avoid
* computationally expensive operations such as DOM modifications
* under these circumstances.
*
* One such high frequenecy event is `resize` that can fire multiple
* times before the window reaches its final dimensions. In order
* to delay an operation until the window has completed resizing,
* it is possible to use this technique to only invoke the callback
* after the last event has fired::
*
* let cb = new DebounceCallback(event => {
* // fires after the final resize event
* console.log("resize", event);
* });
* window.addEventListener("resize", cb);
*
* Note that it is not possible to use this synchronisation primitive
* with `addEventListener(..., {once: true})`.
*
* @param {function(Event)} fn
* Callback function that is guaranteed to be invoked once only,
* after `timeout`.
* @param {number=} [timeout = 250] timeout
* Time since last event firing, before `fn` will be invoked.
*/
class DebounceCallback {
constructor(fn, {timeout = 250} = {}) {
if (typeof fn != "function" || typeof timeout != "number") {
throw new TypeError();
}
if (!Number.isInteger(timeout) || timeout < 0) {
throw new RangeError();
}
this.fn = fn;
this.timeout = timeout;
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
}
handleEvent(ev) {
this.timer.cancel();
this.timer.initWithCallback(() => {
this.timer.cancel();
this.fn(ev);
}, this.timeout, TYPE_ONE_SHOT);
}
}
this.DebounceCallback = DebounceCallback;

View File

@ -3,6 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
const { const {
DebounceCallback,
IdlePromise, IdlePromise,
PollPromise, PollPromise,
Sleep, Sleep,
@ -11,6 +12,29 @@ const {
const DEFAULT_TIMEOUT = 2000; const DEFAULT_TIMEOUT = 2000;
/**
* Mimics nsITimer, but instead of using a system clock you can
* preprogram it to invoke the callback after a given number of ticks.
*/
class MockTimer {
constructor(ticksBeforeFiring) {
this.goal = ticksBeforeFiring;
this.ticks = 0;
this.cancelled = false;
}
initWithCallback(cb, timeout, type) {
this.ticks++;
if (this.ticks >= this.goal) {
cb();
}
}
cancel() {
this.cancelled = true;
}
}
add_test(function test_PollPromise_funcTypes() { add_test(function test_PollPromise_funcTypes() {
for (let type of ["foo", 42, null, undefined, true, [], {}]) { for (let type of ["foo", 42, null, undefined, true, [], {}]) {
Assert.throws(() => new PollPromise(type), /TypeError/); Assert.throws(() => new PollPromise(type), /TypeError/);
@ -154,3 +178,38 @@ add_task(async function test_IdlePromise() {
await IdlePromise(win); await IdlePromise(win);
ok(called); ok(called);
}); });
add_test(function test_DebounceCallback_constructor() {
for (let cb of [42, "foo", true, null, undefined, [], {}]) {
Assert.throws(() => new DebounceCallback(cb), /TypeError/);
}
for (let timeout of ["foo", true, [], {}, () => {}]) {
Assert.throws(() => new DebounceCallback(() => {}, {timeout}), /TypeError/);
}
for (let timeout of [-1, 2.3, NaN]) {
Assert.throws(() => new DebounceCallback(() => {}, {timeout}), /RangeError/);
}
run_next_test();
});
add_task(async function test_DebounceCallback_repeatedCallback() {
let uniqueEvent = {};
let ncalls = 0;
let cb = ev => {
ncalls++;
equal(ev, uniqueEvent);
};
let debouncer = new DebounceCallback(cb);
debouncer.timer = new MockTimer(3);
// flood the debouncer with events,
// we only expect the last one to fire
debouncer.handleEvent(uniqueEvent);
debouncer.handleEvent(uniqueEvent);
debouncer.handleEvent(uniqueEvent);
equal(ncalls, 1);
ok(debouncer.timer.cancelled);
});