Source code

Revision control

Copy as Markdown

Other Tools

'use strict';
/**
ResizeTestHelper is a framework to test ResizeObserver
notifications. Use it to make assertions about ResizeObserverEntries.
This framework is needed because ResizeObservations are
delivered asynchronously inside the event loop.
Features:
- can queue multiple notification steps in a test
- handles timeouts
- returns Promise that is fulfilled when test completes.
Use to chain tests (since parallel async ResizeObserver tests
would conflict if reusing same DOM elements).
Usage:
create ResizeTestHelper for every test.
Make assertions inside notify, timeout callbacks.
Start tests with helper.start()
Chain tests with Promises.
Counts animation frames, see startCountingRaf
*/
/*
@param name: test name
@param steps:
{
setup: function(ResizeObserver) {
// called at the beginning of the test step
// your observe/resize code goes here
},
notify: function(entries, observer) {
// ResizeObserver callback.
// Make assertions here.
// Return true if next step should start on the next event loop.
},
timeout: function() {
// Define this if your test expects to time out, and the expected timeout
// value will be 100ms.
// If undefined, timeout is assert_unreached after 1000ms.
}
}
*/
function ResizeTestHelper(name, steps)
{
this._name = name;
this._steps = steps || [];
this._stepIdx = -1;
this._harnessTest = null;
this._observer = new ResizeObserver(this._handleNotification.bind(this));
this._timeoutBind = this._handleTimeout.bind(this);
this._nextStepBind = this._nextStep.bind(this);
}
// The default timeout value in ms.
// We expect TIMEOUT to be longer than we would ever have to wait for notify()
// to be fired. This is used for tests which are not expected to time out, so
// it can be large without slowing down the test.
ResizeTestHelper.TIMEOUT = 1000;
// A reasonable short timeout value in ms.
// We expect SHORT_TIMEOUT to be long enough that notify() would usually get a
// chance to fire before SHORT_TIMEOUT expires. This is used for tests which
// *are* expected to time out (with no callbacks fired), so we'd like to keep
// it relatively short to avoid slowing down the test.
ResizeTestHelper.SHORT_TIMEOUT = 100;
ResizeTestHelper.prototype = {
get _currentStep() {
return this._steps[this._stepIdx];
},
_nextStep: function() {
if (++this._stepIdx == this._steps.length)
return this._done();
// Use SHORT_TIMEOUT if this step expects timeout.
let timeoutValue = this._steps[this._stepIdx].timeout ?
ResizeTestHelper.SHORT_TIMEOUT :
ResizeTestHelper.TIMEOUT;
this._timeoutId = this._harnessTest.step_timeout(
this._timeoutBind, timeoutValue);
try {
this._steps[this._stepIdx].setup(this._observer);
}
catch(err) {
this._harnessTest.step(() => {
assert_unreached("Caught a throw, possible syntax error");
});
}
},
_handleNotification: function(entries) {
if (this._timeoutId) {
window.clearTimeout(this._timeoutId);
delete this._timeoutId;
}
this._harnessTest.step(() => {
try {
let rafDelay = this._currentStep.notify(entries, this._observer);
if (rafDelay)
window.requestAnimationFrame(this._nextStepBind);
else
this._nextStep();
}
catch(err) {
this._harnessTest.step(() => {
throw err;
});
// Force to _done() the current test.
this._done();
}
});
},
_handleTimeout: function() {
delete this._timeoutId;
this._harnessTest.step(() => {
if (this._currentStep.timeout) {
this._currentStep.timeout();
}
else {
this._harnessTest.step(() => {
assert_unreached("Timed out waiting for notification. (" + ResizeTestHelper.TIMEOUT + "ms)");
});
}
this._nextStep();
});
},
_done: function() {
this._observer.disconnect();
delete this._observer;
this._harnessTest.done();
if (this._rafCountRequest) {
window.cancelAnimationFrame(this._rafCountRequest);
delete this._rafCountRequest;
}
window.requestAnimationFrame(() => { this._resolvePromise(); });
},
start: function(cleanup) {
this._harnessTest = async_test(this._name);
if (cleanup) {
this._harnessTest.add_cleanup(cleanup);
}
this._harnessTest.step(() => {
assert_equals(this._stepIdx, -1, "start can only be called once");
this._nextStep();
});
return new Promise( (resolve, reject) => {
this._resolvePromise = resolve;
this._rejectPromise = reject;
});
},
get rafCount() {
if (!this._rafCountRequest)
throw "rAF count is not active";
return this._rafCount;
},
get test() {
if (!this._harnessTest) {
throw "_harnessTest is not initialized";
}
return this._harnessTest;
},
_incrementRaf: function() {
if (this._rafCountRequest) {
this._rafCount++;
this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind);
}
},
startCountingRaf: function() {
if (this._rafCountRequest)
window.cancelAnimationFrame(this._rafCountRequest);
if (!this._incrementRafBind)
this._incrementRafBind = this._incrementRaf.bind(this);
this._rafCount = 0;
this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind);
}
}
function createAndAppendElement(tagName, parent) {
if (!parent) {
parent = document.body;
}
const element = document.createElement(tagName);
parent.appendChild(element);
return element;
}