mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-24 09:27:31 +09:00
403 lines
16 KiB
JavaScript
403 lines
16 KiB
JavaScript
// Utilities for writing APZ tests using the framework added in bug 961289
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Functions that convert the APZ test data into a more usable form.
|
|
// Every place we have a WebIDL sequence whose elements are dictionaries
|
|
// with two elements, a key, and a value, we convert this into a JS
|
|
// object with a property for each key/value pair. (This is the structure
|
|
// we really want, but we can't express in directly in WebIDL.)
|
|
// ----------------------------------------------------------------------
|
|
|
|
function convertEntries(entries) {
|
|
var result = {};
|
|
for (var i = 0; i < entries.length; ++i) {
|
|
result[entries[i].key] = entries[i].value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getPropertyAsRect(scrollFrames, scrollId, prop) {
|
|
SimpleTest.ok(scrollId in scrollFrames,
|
|
'expected scroll frame data for scroll id ' + scrollId);
|
|
var scrollFrameData = scrollFrames[scrollId];
|
|
SimpleTest.ok('displayport' in scrollFrameData,
|
|
'expected a ' + prop + ' for scroll id ' + scrollId);
|
|
var value = scrollFrameData[prop];
|
|
var pieces = value.replace(/[()\s]+/g, '').split(',');
|
|
SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)");
|
|
return { x: parseInt(pieces[0]),
|
|
y: parseInt(pieces[1]),
|
|
w: parseInt(pieces[2]),
|
|
h: parseInt(pieces[3]) };
|
|
}
|
|
|
|
function convertScrollFrameData(scrollFrames) {
|
|
var result = {};
|
|
for (var i = 0; i < scrollFrames.length; ++i) {
|
|
result[scrollFrames[i].scrollId] = convertEntries(scrollFrames[i].entries);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function convertBuckets(buckets) {
|
|
var result = {};
|
|
for (var i = 0; i < buckets.length; ++i) {
|
|
result[buckets[i].sequenceNumber] = convertScrollFrameData(buckets[i].scrollFrames);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function convertTestData(testData) {
|
|
var result = {};
|
|
result.paints = convertBuckets(testData.paints);
|
|
result.repaintRequests = convertBuckets(testData.repaintRequests);
|
|
return result;
|
|
}
|
|
|
|
// Given APZ test data for a single paint on the compositor side,
|
|
// reconstruct the APZC tree structure from the 'parentScrollId'
|
|
// entries that were logged. More specifically, the subset of the
|
|
// APZC tree structure corresponding to the layer subtree for the
|
|
// content process that triggered the paint, is reconstructed (as
|
|
// the APZ test data only contains information abot this subtree).
|
|
function buildApzcTree(paint) {
|
|
// The APZC tree can potentially have multiple root nodes,
|
|
// so we invent a node that is the parent of all roots.
|
|
// This 'root' does not correspond to an APZC.
|
|
var root = {scrollId: -1, children: []};
|
|
for (var scrollId in paint) {
|
|
paint[scrollId].children = [];
|
|
paint[scrollId].scrollId = scrollId;
|
|
}
|
|
for (var scrollId in paint) {
|
|
var parentNode = null;
|
|
if ("hasNoParentWithSameLayersId" in paint[scrollId]) {
|
|
parentNode = root;
|
|
} else if ("parentScrollId" in paint[scrollId]) {
|
|
parentNode = paint[paint[scrollId].parentScrollId];
|
|
}
|
|
parentNode.children.push(paint[scrollId]);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
// Given an APZC tree produced by buildApzcTree, return the RCD node in
|
|
// the tree, or null if there was none.
|
|
function findRcdNode(apzcTree) {
|
|
if (!!apzcTree.isRootContent) { // isRootContent will be undefined or "1"
|
|
return apzcTree;
|
|
}
|
|
for (var i = 0; i < apzcTree.children.length; i++) {
|
|
var rcd = findRcdNode(apzcTree.children[i]);
|
|
if (rcd != null) {
|
|
return rcd;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Return whether an element whose id includes |elementId| has been layerized.
|
|
// Assumes |elementId| will be present in the content description for the
|
|
// element, and not in the content descriptions of other elements.
|
|
function isLayerized(elementId) {
|
|
var contentTestData = SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
|
|
ok(contentTestData.paints.length > 0, "expected at least one paint");
|
|
var seqno = contentTestData.paints[contentTestData.paints.length - 1].sequenceNumber;
|
|
contentTestData = convertTestData(contentTestData);
|
|
var paint = contentTestData.paints[seqno];
|
|
for (var scrollId in paint) {
|
|
if ("contentDescription" in paint[scrollId]) {
|
|
if (paint[scrollId]["contentDescription"].includes(elementId)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function flushApzRepaints(aCallback, aWindow = window) {
|
|
if (!aCallback) {
|
|
throw "A callback must be provided!";
|
|
}
|
|
var repaintDone = function() {
|
|
SpecialPowers.Services.obs.removeObserver(repaintDone, "apz-repaints-flushed", false);
|
|
setTimeout(aCallback, 0);
|
|
};
|
|
SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed", false);
|
|
if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints()) {
|
|
dump("Flushed APZ repaints, waiting for callback...\n");
|
|
} else {
|
|
dump("Flushing APZ repaints was a no-op, triggering callback directly...\n");
|
|
repaintDone();
|
|
}
|
|
}
|
|
|
|
// Flush repaints, APZ pending repaints, and any repaints resulting from that
|
|
// flush. This is particularly useful if the test needs to reach some sort of
|
|
// "idle" state in terms of repaints. Usually just doing waitForAllPaints
|
|
// followed by flushApzRepaints is sufficient to flush all APZ state back to
|
|
// the main thread, but it can leave a paint scheduled which will get triggered
|
|
// at some later time. For tests that specifically test for painting at
|
|
// specific times, this method is the way to go. Even if in doubt, this is the
|
|
// preferred method as the extra step is "safe" and shouldn't interfere with
|
|
// most tests.
|
|
function waitForApzFlushedRepaints(aCallback) {
|
|
// First flush the main-thread paints and send transactions to the APZ
|
|
waitForAllPaints(function() {
|
|
// Then flush the APZ to make sure any repaint requests have been sent
|
|
// back to the main thread
|
|
flushApzRepaints(function() {
|
|
// Then flush the main-thread again to process the repaint requests.
|
|
// Once this is done, we should be in a stable state with nothing
|
|
// pending, so we can trigger the callback.
|
|
waitForAllPaints(aCallback);
|
|
});
|
|
});
|
|
}
|
|
|
|
// This function takes a set of subtests to run one at a time in new top-level
|
|
// windows, and returns a Promise that is resolved once all the subtests are
|
|
// done running.
|
|
//
|
|
// The aSubtests array is an array of objects with the following keys:
|
|
// file: required, the filename of the subtest.
|
|
// prefs: optional, an array of arrays containing key-value prefs to set.
|
|
// dp_suppression: optional, a boolean on whether or not to respect displayport
|
|
// suppression during the test.
|
|
// onload: optional, a function that will be registered as a load event listener
|
|
// for the child window that will hold the subtest. the function will be
|
|
// passed exactly one argument, which will be the child window.
|
|
// An example of an array is:
|
|
// aSubtests = [
|
|
// { 'file': 'test_file_name.html' },
|
|
// { 'file': 'test_file_2.html', 'prefs': [['pref.name', true], ['other.pref', 1000]], 'dp_suppression': false }
|
|
// { 'file': 'file_3.html', 'onload': function(w) { w.subtestDone(); } }
|
|
// ];
|
|
//
|
|
// Each subtest should call the subtestDone() function when it is done, to
|
|
// indicate that the window should be torn down and the next text should run.
|
|
// The subtestDone() function is injected into the subtest's window by this
|
|
// function prior to loading the subtest. For convenience, the |is| and |ok|
|
|
// functions provided by SimpleTest are also mapped into the subtest's window.
|
|
// For other things from the parent, the subtest can use window.opener.<whatever>
|
|
// to access objects.
|
|
function runSubtestsSeriallyInFreshWindows(aSubtests) {
|
|
return new Promise(function(resolve, reject) {
|
|
var testIndex = -1;
|
|
var w = null;
|
|
|
|
function advanceSubtestExecution() {
|
|
var test = aSubtests[testIndex];
|
|
if (w) {
|
|
if (typeof test.dp_suppression != 'undefined') {
|
|
// We modified the suppression when starting the test, so now undo that.
|
|
SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(!test.dp_suppression);
|
|
}
|
|
if (test.prefs) {
|
|
// We pushed some prefs for this test, pop them, and re-invoke
|
|
// advanceSubtestExecution() after that's been processed
|
|
SpecialPowers.popPrefEnv(function() {
|
|
w.close();
|
|
w = null;
|
|
advanceSubtestExecution();
|
|
});
|
|
return;
|
|
}
|
|
|
|
w.close();
|
|
}
|
|
|
|
testIndex++;
|
|
if (testIndex >= aSubtests.length) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
test = aSubtests[testIndex];
|
|
if (typeof test.dp_suppression != 'undefined') {
|
|
// Normally during a test, the displayport will get suppressed during page
|
|
// load, and unsuppressed at a non-deterministic time during the test. The
|
|
// unsuppression can trigger a repaint which interferes with the test, so
|
|
// to avoid that we can force the displayport to be unsuppressed for the
|
|
// entire test which is more deterministic.
|
|
SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(test.dp_suppression);
|
|
}
|
|
|
|
function spawnTest(aFile) {
|
|
w = window.open('', "_blank");
|
|
w.subtestDone = advanceSubtestExecution;
|
|
w.SimpleTest = SimpleTest;
|
|
w.is = function(a, b, msg) { return is(a, b, aFile + " | " + msg); };
|
|
w.ok = function(cond, name, diag) { return ok(cond, aFile + " | " + name, diag); };
|
|
if (test.onload) {
|
|
w.addEventListener('load', function(e) { test.onload(w); }, { once: true });
|
|
}
|
|
w.location = location.href.substring(0, location.href.lastIndexOf('/') + 1) + aFile;
|
|
return w;
|
|
}
|
|
|
|
if (test.prefs) {
|
|
// Got some prefs for this subtest, push them
|
|
SpecialPowers.pushPrefEnv({"set": test.prefs}, function() {
|
|
w = spawnTest(test.file);
|
|
});
|
|
} else {
|
|
w = spawnTest(test.file);
|
|
}
|
|
}
|
|
|
|
advanceSubtestExecution();
|
|
});
|
|
}
|
|
|
|
function pushPrefs(prefs) {
|
|
return SpecialPowers.pushPrefEnv({'set': prefs});
|
|
}
|
|
|
|
function waitUntilApzStable() {
|
|
return new Promise(function(resolve, reject) {
|
|
SimpleTest.waitForFocus(function() {
|
|
waitForAllPaints(function() {
|
|
flushApzRepaints(resolve);
|
|
});
|
|
}, window);
|
|
});
|
|
}
|
|
|
|
function isApzEnabled() {
|
|
var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled;
|
|
if (!enabled) {
|
|
// All tests are required to have at least one assertion. Since APZ is
|
|
// disabled, and the main test is presumably not going to run, we stick in
|
|
// a dummy assertion here to keep the test passing.
|
|
SimpleTest.ok(true, "APZ is not enabled; this test will be skipped");
|
|
}
|
|
return enabled;
|
|
}
|
|
|
|
// 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));
|
|
//
|
|
// If you want to run the continuation directly, outside of a promise chain,
|
|
// you can invoke the return value of this function, like so:
|
|
// runContinuation(myTest)();
|
|
function runContinuation(testFunction) {
|
|
// We need to wrap this in an extra function, so that the call site can
|
|
// be more readable without running the promise too early. In other words,
|
|
// if we didn't have this extra function, the promise would start running
|
|
// during construction of the promise chain, concurrently with the first
|
|
// promise in the chain.
|
|
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();
|
|
}
|
|
}
|
|
|
|
driveTest();
|
|
});
|
|
};
|
|
}
|
|
|
|
// Take a snapshot of the given rect, *including compositor transforms* (i.e.
|
|
// includes async scroll transforms applied by APZ). If you don't need the
|
|
// compositor transforms, you can probably get away with using
|
|
// SpecialPowers.snapshotWindowWithOptions or one of the friendlier wrappers.
|
|
// The rect provided is expected to be relative to the screen, for example as
|
|
// returned by rectRelativeToScreen in apz_test_native_event_utils.js.
|
|
// Example usage:
|
|
// var snapshot = getSnapshot(rectRelativeToScreen(myDiv));
|
|
// which will take a snapshot of the 'myDiv' element. Note that if part of the
|
|
// element is obscured by other things on top, the snapshot will include those
|
|
// things. If it is clipped by a scroll container, the snapshot will include
|
|
// that area anyway, so you will probably get parts of the scroll container in
|
|
// the snapshot. If the rect extends outside the browser window then the
|
|
// results are undefined.
|
|
// The snapshot is returned in the form of a data URL.
|
|
function getSnapshot(rect) {
|
|
function parentProcessSnapshot() {
|
|
addMessageListener('snapshot', function(rect) {
|
|
Components.utils.import('resource://gre/modules/Services.jsm');
|
|
var topWin = Services.wm.getMostRecentWindow('navigator:browser');
|
|
|
|
// reposition the rect relative to the top-level browser window
|
|
rect = JSON.parse(rect);
|
|
rect.x -= topWin.mozInnerScreenX;
|
|
rect.y -= topWin.mozInnerScreenY;
|
|
|
|
// take the snapshot
|
|
var canvas = topWin.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
canvas.width = rect.w;
|
|
canvas.height = rect.h;
|
|
var ctx = canvas.getContext("2d");
|
|
ctx.drawWindow(topWin, rect.x, rect.y, rect.w, rect.h, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_VIEW | ctx.DRAWWINDOW_USE_WIDGET_LAYERS | ctx.DRAWWINDOW_DRAW_CARET);
|
|
return canvas.toDataURL();
|
|
});
|
|
}
|
|
|
|
if (typeof getSnapshot.chromeHelper == 'undefined') {
|
|
// This is the first time getSnapshot is being called; do initialization
|
|
getSnapshot.chromeHelper = SpecialPowers.loadChromeScript(parentProcessSnapshot);
|
|
SimpleTest.registerCleanupFunction(function() { getSnapshot.chromeHelper.destroy() });
|
|
}
|
|
|
|
return getSnapshot.chromeHelper.sendSyncMessage('snapshot', JSON.stringify(rect)).toString();
|
|
}
|
|
|
|
// Takes the document's query string and parses it, assuming the query string
|
|
// is composed of key-value pairs where the value is in JSON format. The object
|
|
// returned contains the various values indexed by their respective keys. In
|
|
// case of duplicate keys, the last value be used.
|
|
// Examples:
|
|
// ?key="value"&key2=false&key3=500
|
|
// produces { "key": "value", "key2": false, "key3": 500 }
|
|
// ?key={"x":0,"y":50}&key2=[1,2,true]
|
|
// produces { "key": { "x": 0, "y": 0 }, "key2": [1, 2, true] }
|
|
function getQueryArgs() {
|
|
var args = {};
|
|
if (location.search.length > 0) {
|
|
var params = location.search.substr(1).split('&');
|
|
for (var p of params) {
|
|
var [k, v] = p.split('=');
|
|
args[k] = JSON.parse(v);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
// Return a function that returns a promise to create a script element with the
|
|
// given URI and append it to the head of the document in the given window.
|
|
// As with runContinuation(), the extra function wrapper is for convenience
|
|
// at the call site, so that this can be chained with other promises:
|
|
// waitUntilApzStable().then(injectScript('foo'))
|
|
// .then(injectScript('bar'));
|
|
// If you want to do the injection right away, run the function returned by
|
|
// this function:
|
|
// injectScript('foo')();
|
|
function injectScript(aScript, aWindow = window) {
|
|
return function() {
|
|
return new Promise(function(resolve, reject) {
|
|
var e = aWindow.document.createElement('script');
|
|
e.type = 'text/javascript';
|
|
e.onload = function() {
|
|
resolve();
|
|
};
|
|
e.onerror = function() {
|
|
dump('Script [' + aScript + '] errored out\n');
|
|
reject();
|
|
};
|
|
e.src = aScript;
|
|
aWindow.document.getElementsByTagName('head')[0].appendChild(e);
|
|
});
|
|
};
|
|
}
|