mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 09:15:35 +00:00
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:
parent
5e151181ca
commit
78a0a14d3d
@ -17,6 +17,7 @@ const {Log} = ChromeUtils.import("chrome://marionette/content/log.js", {});
|
||||
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"DebounceCallback",
|
||||
"IdlePromise",
|
||||
"MessageManagerDestroyedPromise",
|
||||
"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;
|
||||
|
@ -3,6 +3,7 @@
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {
|
||||
DebounceCallback,
|
||||
IdlePromise,
|
||||
PollPromise,
|
||||
Sleep,
|
||||
@ -11,6 +12,29 @@ const {
|
||||
|
||||
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() {
|
||||
for (let type of ["foo", 42, null, undefined, true, [], {}]) {
|
||||
Assert.throws(() => new PollPromise(type), /TypeError/);
|
||||
@ -154,3 +178,38 @@ add_task(async function test_IdlePromise() {
|
||||
await IdlePromise(win);
|
||||
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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user