mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 17:25:36 +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);
|
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;
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user