1320 lines
45 KiB
JavaScript
1320 lines
45 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.)
|
|
// ----------------------------------------------------------------------
|
|
|
|
// getHitTestConfig() expects apz_test_native_event_utils.js to be loaded as well.
|
|
/* import-globals-from apz_test_native_event_utils.js */
|
|
|
|
function convertEntries(entries) {
|
|
var result = {};
|
|
for (var i = 0; i < entries.length; ++i) {
|
|
result[entries[i].key] = entries[i].value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function parsePoint(str) {
|
|
var pieces = str.replace(/[()\s]+/g, "").split(",");
|
|
SimpleTest.is(pieces.length, 2, "expected string of form (x,y)");
|
|
for (var i = 0; i < 2; i++) {
|
|
var eq = pieces[i].indexOf("=");
|
|
if (eq >= 0) {
|
|
pieces[i] = pieces[i].substring(eq + 1);
|
|
}
|
|
}
|
|
return {
|
|
x: parseInt(pieces[0]),
|
|
y: parseInt(pieces[1]),
|
|
};
|
|
}
|
|
|
|
// Given a VisualViewport object, return the visual viewport
|
|
// rect relative to the page.
|
|
function getVisualViewportRect(vv) {
|
|
return {
|
|
x: vv.pageLeft,
|
|
y: vv.pageTop,
|
|
width: vv.width,
|
|
height: vv.height,
|
|
};
|
|
}
|
|
|
|
// Return the offset of the visual viewport relative to the layout viewport.
|
|
function getRelativeViewportOffset(window) {
|
|
const offsetX = {};
|
|
const offsetY = {};
|
|
const utils = SpecialPowers.getDOMWindowUtils(window);
|
|
utils.getVisualViewportOffsetRelativeToLayoutViewport(offsetX, offsetY);
|
|
return {
|
|
x: offsetX.value,
|
|
y: offsetY.value,
|
|
};
|
|
}
|
|
|
|
function parseRect(str) {
|
|
var pieces = str.replace(/[()\s]+/g, "").split(",");
|
|
SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)");
|
|
for (var i = 0; i < 4; i++) {
|
|
var eq = pieces[i].indexOf("=");
|
|
if (eq >= 0) {
|
|
pieces[i] = pieces[i].substring(eq + 1);
|
|
}
|
|
}
|
|
return {
|
|
x: parseInt(pieces[0]),
|
|
y: parseInt(pieces[1]),
|
|
width: parseInt(pieces[2]),
|
|
height: parseInt(pieces[3]),
|
|
};
|
|
}
|
|
|
|
// These functions expect rects with fields named x/y/width/height, such as
|
|
// that returned by parseRect().
|
|
function rectContains(haystack, needle) {
|
|
return (
|
|
haystack.x <= needle.x &&
|
|
haystack.y <= needle.y &&
|
|
haystack.x + haystack.width >= needle.x + needle.width &&
|
|
haystack.y + haystack.height >= needle.y + needle.height
|
|
);
|
|
}
|
|
function rectToString(rect) {
|
|
return (
|
|
"(" + rect.x + "," + rect.y + "," + rect.width + "," + rect.height + ")"
|
|
);
|
|
}
|
|
function assertRectContainment(
|
|
haystackRect,
|
|
haystackDesc,
|
|
needleRect,
|
|
needleDesc
|
|
) {
|
|
SimpleTest.ok(
|
|
rectContains(haystackRect, needleRect),
|
|
haystackDesc +
|
|
" " +
|
|
rectToString(haystackRect) +
|
|
" should contain " +
|
|
needleDesc +
|
|
" " +
|
|
rectToString(needleRect)
|
|
);
|
|
}
|
|
|
|
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];
|
|
return parseRect(value);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Returns the last bucket that has at least one scrollframe. This
|
|
// is useful for skipping over buckets that are from empty transactions,
|
|
// because those don't contain any useful data.
|
|
function getLastNonemptyBucket(buckets) {
|
|
for (var i = buckets.length - 1; i >= 0; --i) {
|
|
if (buckets[i].scrollFrames.length) {
|
|
return buckets[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Takes something like "matrix(1, 0, 0, 1, 234.024, 528.29023)"" and returns a number array
|
|
function parseTransform(transform) {
|
|
return /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/
|
|
.exec(transform)
|
|
.slice(1)
|
|
.map(parseFloat);
|
|
}
|
|
|
|
function isTransformClose(a, b, name) {
|
|
is(
|
|
a.length,
|
|
b.length,
|
|
`expected transforms ${a} and ${b} to be the same length`
|
|
);
|
|
for (let i = 0; i < a.length; i++) {
|
|
ok(Math.abs(a[i] - b[i]) < 0.01, name);
|
|
}
|
|
}
|
|
|
|
// 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 (let scrollId in paint) {
|
|
paint[scrollId].children = [];
|
|
paint[scrollId].scrollId = scrollId;
|
|
}
|
|
for (let 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) {
|
|
// isRootContent will be undefined or "1"
|
|
if (apzcTree.isRootContent) {
|
|
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();
|
|
var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
|
|
ok(nonEmptyBucket != null, "expected at least one nonempty paint");
|
|
var seqno = nonEmptyBucket.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;
|
|
}
|
|
|
|
// Return a rect (or null) that holds the last known content-side displayport
|
|
// for a given element. (The element selection works the same way, and with
|
|
// the same assumptions as the isLayerized function above).
|
|
function getLastContentDisplayportFor(
|
|
aElementId,
|
|
aOptions = { expectPainted: true, popupElement: null }
|
|
) {
|
|
var contentTestData = SpecialPowers.getDOMWindowUtils(
|
|
aOptions.popupElement ? aOptions.popupElement.ownerGlobal : window
|
|
).getContentAPZTestData(aOptions.popupElement);
|
|
if (contentTestData == undefined) {
|
|
ok(!aOptions.expectPainted, "expected to have apz test data (1)");
|
|
return null;
|
|
}
|
|
var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
|
|
if (nonEmptyBucket == null) {
|
|
ok(!aOptions.expectPainted, "expected to have apz test data (2)");
|
|
return null;
|
|
}
|
|
var seqno = nonEmptyBucket.sequenceNumber;
|
|
contentTestData = convertTestData(contentTestData);
|
|
var paint = contentTestData.paints[seqno];
|
|
for (var scrollId in paint) {
|
|
if ("contentDescription" in paint[scrollId]) {
|
|
if (paint[scrollId].contentDescription.includes(aElementId)) {
|
|
if ("displayport" in paint[scrollId]) {
|
|
return parseRect(paint[scrollId].displayport);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Return the APZC tree (as produced by buildApzcTree) for the last
|
|
// non-empty paint received by the compositor.
|
|
function getLastApzcTree() {
|
|
let data = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
|
|
if (data == undefined) {
|
|
ok(false, "expected to have compositor apz test data");
|
|
return null;
|
|
}
|
|
if (!data.paints.length) {
|
|
ok(false, "expected to have at least one compositor paint bucket");
|
|
return null;
|
|
}
|
|
var seqno = data.paints[data.paints.length - 1].sequenceNumber;
|
|
data = convertTestData(data);
|
|
return buildApzcTree(data.paints[seqno]);
|
|
}
|
|
|
|
// Return a promise that is resolved on the next rAF callback
|
|
function promiseFrame(aWindow = window) {
|
|
return new Promise(resolve => {
|
|
aWindow.requestAnimationFrame(resolve);
|
|
});
|
|
}
|
|
|
|
// Return a promise that is resolved on the next MozAfterPaint event
|
|
function promiseAfterPaint() {
|
|
return new Promise(resolve => {
|
|
window.addEventListener("MozAfterPaint", resolve, { once: true });
|
|
});
|
|
}
|
|
|
|
// This waits until any pending events on the APZ controller thread are
|
|
// processed, and any resulting repaint requests are received by the main
|
|
// thread. Note that while the repaint requests do get processed by the
|
|
// APZ handler on the main thread, the repaints themselves may not have
|
|
// occurred by the the returned promise resolves. If you want to wait
|
|
// for those repaints, consider using promiseApzFlushedRepaints instead.
|
|
function promiseOnlyApzControllerFlushedWithoutSetTimeout(
|
|
aWindow = window,
|
|
aElement
|
|
) {
|
|
return new Promise(function (resolve) {
|
|
var fail = false;
|
|
var repaintDone = function () {
|
|
dump("PromiseApzRepaintsFlushed: APZ flush done\n");
|
|
SpecialPowers.Services.obs.removeObserver(
|
|
repaintDone,
|
|
"apz-repaints-flushed"
|
|
);
|
|
resolve(!fail);
|
|
};
|
|
SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
|
|
if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints(aElement)) {
|
|
dump(
|
|
"PromiseApzRepaintsFlushed: Flushed APZ repaints, waiting for callback...\n"
|
|
);
|
|
} else {
|
|
dump(
|
|
"PromiseApzRepaintsFlushed: Flushing APZ repaints was a no-op, triggering callback directly...\n"
|
|
);
|
|
fail = true;
|
|
repaintDone();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Another variant of the above promiseOnlyApzControllerFlushedWithoutSetTimeout
|
|
// but with a setTimeout(0) callback.
|
|
// |aElement| is an optional argument to do
|
|
// promiseOnlyApzControllerFlushedWithoutSetTimeout for the given |aElement|
|
|
// rather than |aWindow|. If you want to do "apz-repaints-flushed" in popup
|
|
// windows, you need to specify the element inside the popup window.
|
|
function promiseOnlyApzControllerFlushed(aWindow = window, aElement) {
|
|
return new Promise(resolve => {
|
|
promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow, aElement).then(
|
|
result => {
|
|
setTimeout(() => resolve(result), 0);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
// 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 waiting for all paints
|
|
// 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.
|
|
// If you want to do the flush in popup windows, you need to specify |aPopupElement|.
|
|
async function promiseApzFlushedRepaints(aPopupElement = null) {
|
|
if (aPopupElement) {
|
|
SimpleTest.ok(XULPopupElement.isInstance(aPopupElement));
|
|
}
|
|
await promiseAllPaintsDone();
|
|
await promiseOnlyApzControllerFlushed(
|
|
aPopupElement ? aPopupElement.ownerGlobal : window,
|
|
aPopupElement
|
|
);
|
|
await promiseAllPaintsDone();
|
|
}
|
|
|
|
// 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.
|
|
// windowFeatures: optional, will be passed to as the third argument of `window.open`.
|
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#windowfeatures
|
|
// 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 one of the subtestDone() or subtestFailed()
|
|
// functions when it is done, to indicate that the window should be torn
|
|
// down and the next test should run.
|
|
// These functions are 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;
|
|
|
|
// If the "apz.subtest" pref has been set, only a single subtest whose name matches
|
|
// the pref's value (if any) will be run.
|
|
var onlyOneSubtest = SpecialPowers.getCharPref(
|
|
"apz.subtest",
|
|
/* default = */ ""
|
|
);
|
|
|
|
function advanceSubtestExecutionWithFailure(msg) {
|
|
SimpleTest.ok(false, msg);
|
|
advanceSubtestExecution();
|
|
}
|
|
|
|
async function advanceSubtestExecution() {
|
|
var test = aSubtests[testIndex];
|
|
if (w) {
|
|
// Run any cleanup functions registered in the subtest
|
|
// Guard against the subtest not loading apz_test_utils.js
|
|
if (w.ApzCleanup) {
|
|
w.ApzCleanup.execute();
|
|
}
|
|
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;
|
|
}
|
|
|
|
await SimpleTest.promiseFocus(window);
|
|
|
|
test = aSubtests[testIndex];
|
|
|
|
let recognizedProps = [
|
|
"file",
|
|
"prefs",
|
|
"dp_suppression",
|
|
"onload",
|
|
"windowFeatures",
|
|
];
|
|
for (let prop in test) {
|
|
if (!recognizedProps.includes(prop)) {
|
|
SimpleTest.ok(
|
|
false,
|
|
"Subtest " + test.file + " has unrecognized property '" + prop + "'"
|
|
);
|
|
setTimeout(function () {
|
|
advanceSubtestExecution();
|
|
}, 0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (onlyOneSubtest && onlyOneSubtest != test.file) {
|
|
SimpleTest.ok(
|
|
true,
|
|
"Skipping " +
|
|
test.file +
|
|
" because only " +
|
|
onlyOneSubtest +
|
|
" is being run"
|
|
);
|
|
setTimeout(function () {
|
|
advanceSubtestExecution();
|
|
}, 0);
|
|
return;
|
|
}
|
|
|
|
SimpleTest.ok(true, "Starting subtest " + test.file);
|
|
|
|
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",
|
|
test.windowFeatures ? test.windowFeatures : ""
|
|
);
|
|
w.subtestDone = advanceSubtestExecution;
|
|
w.subtestFailed = advanceSubtestExecutionWithFailure;
|
|
w.isApzSubtest = true;
|
|
w.SimpleTest = SimpleTest;
|
|
w.dump = function (msg) {
|
|
return dump(aFile + " | " + msg);
|
|
};
|
|
w.info = function (msg) {
|
|
return info(aFile + " | " + msg);
|
|
};
|
|
w.is = function (a, b, msg) {
|
|
return is(a, b, aFile + " | " + msg);
|
|
};
|
|
w.isnot = function (a, b, msg) {
|
|
return isnot(a, b, aFile + " | " + msg);
|
|
};
|
|
w.isfuzzy = function (a, b, eps, msg) {
|
|
return isfuzzy(a, b, eps, aFile + " | " + msg);
|
|
};
|
|
w.ok = function (cond, msg) {
|
|
arguments[1] = aFile + " | " + msg;
|
|
// Forward all arguments to SimpleTest.ok where we will check that ok() was
|
|
// called with at most 2 arguments.
|
|
return SimpleTest.ok.apply(SimpleTest, arguments);
|
|
};
|
|
w.todo_is = function (a, b, msg) {
|
|
return todo_is(a, b, aFile + " | " + msg);
|
|
};
|
|
w.todo = function (cond, msg) {
|
|
return todo(cond, aFile + " | " + msg);
|
|
};
|
|
if (test.onload) {
|
|
w.addEventListener(
|
|
"load",
|
|
function () {
|
|
test.onload(w);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
var subtestUrl =
|
|
location.href.substring(0, location.href.lastIndexOf("/") + 1) +
|
|
aFile;
|
|
function urlResolves(url) {
|
|
var request = new XMLHttpRequest();
|
|
request.open("GET", url, false);
|
|
request.send();
|
|
return request.status !== 404;
|
|
}
|
|
if (!urlResolves(subtestUrl)) {
|
|
SimpleTest.ok(
|
|
false,
|
|
"Subtest URL " +
|
|
subtestUrl +
|
|
" does not resolve. " +
|
|
"Be sure it's present in the support-files section of mochitest.ini."
|
|
);
|
|
reject();
|
|
return undefined;
|
|
}
|
|
w.location = subtestUrl;
|
|
return w;
|
|
}
|
|
|
|
if (test.prefs) {
|
|
// Got some prefs for this subtest, push them
|
|
await SpecialPowers.pushPrefEnv({ set: test.prefs });
|
|
}
|
|
w = spawnTest(test.file);
|
|
}
|
|
|
|
advanceSubtestExecution();
|
|
}).catch(function (e) {
|
|
SimpleTest.ok(false, "Error occurred while running subtests: " + e);
|
|
});
|
|
}
|
|
|
|
function pushPrefs(prefs) {
|
|
return SpecialPowers.pushPrefEnv({ set: prefs });
|
|
}
|
|
|
|
async function waitUntilApzStable() {
|
|
await SimpleTest.promiseFocus(window);
|
|
dump("WaitUntilApzStable: done promiseFocus\n");
|
|
await promiseAllPaintsDone();
|
|
dump("WaitUntilApzStable: done promiseAllPaintsDone\n");
|
|
await promiseOnlyApzControllerFlushed();
|
|
dump("WaitUntilApzStable: all done\n");
|
|
}
|
|
|
|
// This function returns a promise that is resolved after at least one paint
|
|
// has been sent and processed by the compositor. This function can force
|
|
// such a paint to happen if none are pending. This is useful to run after
|
|
// the waitUntilApzStable() but before reading the compositor-side APZ test
|
|
// data, because the test data for the content layers id only gets populated
|
|
// on content layer tree updates *after* the root layer tree has a RefLayer
|
|
// pointing to the contnet layer tree. waitUntilApzStable itself guarantees
|
|
// that the root layer tree is pointing to the content layer tree, but does
|
|
// not guarantee the subsequent paint; this function does that job.
|
|
async function forceLayerTreeToCompositor() {
|
|
// Modify a style property to force a layout flush
|
|
document.body.style.boxSizing = "border-box";
|
|
var utils = SpecialPowers.getDOMWindowUtils(window);
|
|
if (!utils.isMozAfterPaintPending) {
|
|
dump("Forcing a paint since none was pending already...\n");
|
|
var testMode = utils.isTestControllingRefreshes;
|
|
utils.advanceTimeAndRefresh(0);
|
|
if (!testMode) {
|
|
utils.restoreNormalRefresh();
|
|
}
|
|
}
|
|
await promiseAllPaintsDone(null, true);
|
|
await promiseOnlyApzControllerFlushed();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function isKeyApzEnabled() {
|
|
return isApzEnabled() && SpecialPowers.getBoolPref("apz.keyboard.enabled");
|
|
}
|
|
|
|
// 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() {
|
|
/* eslint-env mozilla/chrome-script */
|
|
addMessageListener("snapshot", function (parentRect) {
|
|
var topWin = Services.wm.getMostRecentWindow("navigator:browser");
|
|
if (!topWin) {
|
|
topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
|
|
}
|
|
|
|
// reposition the rect relative to the top-level browser window
|
|
parentRect = JSON.parse(parentRect);
|
|
parentRect.x -= topWin.mozInnerScreenX;
|
|
parentRect.y -= topWin.mozInnerScreenY;
|
|
|
|
// take the snapshot
|
|
var canvas = topWin.document.createElementNS(
|
|
"http://www.w3.org/1999/xhtml",
|
|
"canvas"
|
|
);
|
|
canvas.width = parentRect.width;
|
|
canvas.height = parentRect.height;
|
|
var ctx = canvas.getContext("2d");
|
|
ctx.drawWindow(
|
|
topWin,
|
|
parentRect.x,
|
|
parentRect.y,
|
|
parentRect.width,
|
|
parentRect.height,
|
|
"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
|
|
);
|
|
ApzCleanup.register(function () {
|
|
getSnapshot.chromeHelper.destroy();
|
|
});
|
|
}
|
|
|
|
return getSnapshot.chromeHelper.sendQuery("snapshot", JSON.stringify(rect));
|
|
}
|
|
|
|
// 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) {
|
|
var params = location.search.substr(1).split("&");
|
|
for (var p of params) {
|
|
var [k, v] = p.split("=");
|
|
args[k] = JSON.parse(v);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
// An async function that inserts a script element with the given URI into
|
|
// the head of the document of the given window. This function returns when
|
|
// the load or error event fires on the script element, indicating completion.
|
|
async function injectScript(aScript, aWindow = window) {
|
|
var e = aWindow.document.createElement("script");
|
|
e.type = "text/javascript";
|
|
let loadPromise = new Promise((resolve, reject) => {
|
|
e.onload = function () {
|
|
resolve();
|
|
};
|
|
e.onerror = function () {
|
|
dump("Script [" + aScript + "] errored out\n");
|
|
reject();
|
|
};
|
|
});
|
|
e.src = aScript;
|
|
aWindow.document.getElementsByTagName("head")[0].appendChild(e);
|
|
await loadPromise;
|
|
}
|
|
|
|
// Compute some configuration information used for hit testing.
|
|
// The computed information is cached to avoid recomputing it
|
|
// each time this function is called.
|
|
// The computed information is an object with three fields:
|
|
// utils: the nsIDOMWindowUtils instance for this window
|
|
// isWindow: true if the platform is Windows
|
|
// activateAllScrollFrames: true if prefs indicate all scroll frames are
|
|
// activated with at least a minimal display port
|
|
function getHitTestConfig() {
|
|
if (!("hitTestConfig" in window)) {
|
|
var utils = SpecialPowers.getDOMWindowUtils(window);
|
|
var isWindows = getPlatform() == "windows";
|
|
let activateAllScrollFrames =
|
|
SpecialPowers.getBoolPref("apz.wr.activate_all_scroll_frames") ||
|
|
(SpecialPowers.getBoolPref(
|
|
"apz.wr.activate_all_scroll_frames_when_fission"
|
|
) &&
|
|
SpecialPowers.Services.appinfo.fissionAutostart);
|
|
|
|
window.hitTestConfig = {
|
|
utils,
|
|
isWindows,
|
|
activateAllScrollFrames,
|
|
};
|
|
}
|
|
return window.hitTestConfig;
|
|
}
|
|
|
|
// Compute the coordinates of the center of the given element. The argument
|
|
// can either be a string (the id of the element desired) or the element
|
|
// itself.
|
|
function centerOf(element) {
|
|
if (typeof element === "string") {
|
|
element = document.getElementById(element);
|
|
}
|
|
var bounds = element.getBoundingClientRect();
|
|
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
}
|
|
|
|
// Peform a compositor hit test at the given point and return the result.
|
|
// |point| is expected to be in CSS coordinates relative to the layout
|
|
// viewport, since this is what sendMouseEvent() expects. (Note that this
|
|
// is different from sendNativeMouseEvent() which expects screen coordinates
|
|
// relative to the screen.)
|
|
// The returned object has two fields:
|
|
// hitInfo: a combination of APZHitResultFlags
|
|
// scrollId: the view-id of the scroll frame that was hit
|
|
function hitTest(point, popupElement = null) {
|
|
var utils = getHitTestConfig().utils;
|
|
dump("Hit-testing point (" + point.x + ", " + point.y + ")\n");
|
|
utils.sendMozMouseHitTestEvent(point.x, point.y, popupElement);
|
|
var data = utils.getCompositorAPZTestData(popupElement);
|
|
ok(
|
|
data.hitResults.length >= 1,
|
|
"Expected at least one hit result in the APZTestData"
|
|
);
|
|
var result = data.hitResults[data.hitResults.length - 1];
|
|
return {
|
|
hitInfo: result.hitResult,
|
|
scrollId: result.scrollId,
|
|
layersId: result.layersId,
|
|
};
|
|
}
|
|
|
|
// Returns a canonical stringification of the hitInfo bitfield.
|
|
function hitInfoToString(hitInfo) {
|
|
var strs = [];
|
|
for (var flag in APZHitResultFlags) {
|
|
if ((hitInfo & APZHitResultFlags[flag]) != 0) {
|
|
strs.push(flag);
|
|
}
|
|
}
|
|
if (!strs.length) {
|
|
return "INVISIBLE";
|
|
}
|
|
strs.sort(function (a, b) {
|
|
return APZHitResultFlags[a] - APZHitResultFlags[b];
|
|
});
|
|
return strs.join(" | ");
|
|
}
|
|
|
|
// Takes an object returned by hitTest, along with the expected values, and
|
|
// asserts that they match. Notably, it uses hitInfoToString to provide a
|
|
// more useful message for the case that the hit info doesn't match
|
|
function checkHitResult(
|
|
hitResult,
|
|
expectedHitInfo,
|
|
expectedScrollId,
|
|
expectedLayersId,
|
|
desc
|
|
) {
|
|
is(
|
|
hitInfoToString(hitResult.hitInfo),
|
|
hitInfoToString(expectedHitInfo),
|
|
desc + " hit info"
|
|
);
|
|
is(hitResult.scrollId, expectedScrollId, desc + " scrollid");
|
|
is(hitResult.layersId, expectedLayersId, desc + " layersid");
|
|
}
|
|
|
|
// Symbolic constants used by hitTestScrollbar().
|
|
var ScrollbarTrackLocation = {
|
|
START: 1,
|
|
END: 2,
|
|
};
|
|
var LayerState = {
|
|
ACTIVE: 1,
|
|
INACTIVE: 2,
|
|
};
|
|
|
|
// Perform a hit test on the scrollbar(s) of a scroll frame.
|
|
// This function takes a single argument which is expected to be
|
|
// an object with the following fields:
|
|
// element: The scroll frame to perform the hit test on.
|
|
// directions: The direction(s) of scrollbars to test.
|
|
// If directions.vertical is true, the vertical scrollbar will be tested.
|
|
// If directions.horizontal is true, the horizontal scrollbar will be tested.
|
|
// Both may be true in a single call (in which case two tests are performed).
|
|
// expectedScrollId: The scroll id that is expected to be hit, if activateAllScrollFrames is false.
|
|
// expectedLayersId: The layers id that is expected to be hit.
|
|
// trackLocation: One of ScrollbarTrackLocation.{START, END}.
|
|
// Determines which end of the scrollbar track is targeted.
|
|
// expectThumb: Whether the scrollbar thumb is expected to be present
|
|
// at the targeted end of the scrollbar track.
|
|
// layerState: Whether the scroll frame is active or inactive.
|
|
// The function performs the hit tests and asserts that the returned
|
|
// hit test information is consistent with the passed parameters.
|
|
// There is no return value.
|
|
// Tests that use this function must set the pref
|
|
// "layout.scrollbars.always-layerize-track".
|
|
function hitTestScrollbar(params) {
|
|
var config = getHitTestConfig();
|
|
|
|
var elem = params.element;
|
|
|
|
var boundingClientRect = elem.getBoundingClientRect();
|
|
|
|
var verticalScrollbarWidth = boundingClientRect.width - elem.clientWidth;
|
|
var horizontalScrollbarHeight = boundingClientRect.height - elem.clientHeight;
|
|
|
|
// On windows, the scrollbar tracks have buttons on the end. When computing
|
|
// coordinates for hit-testing we need to account for this. We assume the
|
|
// buttons are square, and so can use the scrollbar width/height to estimate
|
|
// the size of the buttons
|
|
var scrollbarArrowButtonHeight = config.isWindows
|
|
? verticalScrollbarWidth
|
|
: 0;
|
|
var scrollbarArrowButtonWidth = config.isWindows
|
|
? horizontalScrollbarHeight
|
|
: 0;
|
|
|
|
// Compute the expected hit result flags.
|
|
// The direction flag (APZHitResultFlags.SCROLLBAR_VERTICAL) is added in
|
|
// later, for the vertical test only.
|
|
// The APZHitResultFlags.SCROLLBAR flag will be present regardless of whether
|
|
// the layer is active or inactive because we force layerization of scrollbar
|
|
// tracks. Unfortunately not forcing the layerization results in different
|
|
// behaviour on different platforms which makes testing harder.
|
|
var expectedHitInfo = APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR;
|
|
if (params.expectThumb) {
|
|
// The thumb has listeners which are APZ-aware.
|
|
expectedHitInfo |= APZHitResultFlags.APZ_AWARE_LISTENERS;
|
|
var expectActive =
|
|
config.activateAllScrollFrames || params.layerState == LayerState.ACTIVE;
|
|
if (!expectActive) {
|
|
expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
|
|
}
|
|
// We do not generate the layers for thumbs on inactive scrollframes.
|
|
if (expectActive) {
|
|
expectedHitInfo |= APZHitResultFlags.SCROLLBAR_THUMB;
|
|
}
|
|
}
|
|
|
|
var expectedScrollId = params.expectedScrollId;
|
|
if (config.activateAllScrollFrames) {
|
|
expectedScrollId = config.utils.getViewId(params.element);
|
|
if (params.layerState == LayerState.ACTIVE) {
|
|
is(
|
|
expectedScrollId,
|
|
params.expectedScrollId,
|
|
"Expected scrollId for active scrollframe should match"
|
|
);
|
|
}
|
|
}
|
|
|
|
var scrollframeMsg =
|
|
params.layerState == LayerState.ACTIVE
|
|
? "active scrollframe"
|
|
: "inactive scrollframe";
|
|
|
|
// Hit-test the targeted areas, assuming we don't have overlay scrollbars
|
|
// with zero dimensions.
|
|
if (params.directions.vertical && verticalScrollbarWidth > 0) {
|
|
var verticalScrollbarPoint = {
|
|
x: boundingClientRect.right - verticalScrollbarWidth / 2,
|
|
y:
|
|
params.trackLocation == ScrollbarTrackLocation.START
|
|
? boundingClientRect.y + scrollbarArrowButtonHeight + 5
|
|
: boundingClientRect.bottom -
|
|
horizontalScrollbarHeight -
|
|
scrollbarArrowButtonHeight -
|
|
5,
|
|
};
|
|
checkHitResult(
|
|
hitTest(verticalScrollbarPoint),
|
|
expectedHitInfo | APZHitResultFlags.SCROLLBAR_VERTICAL,
|
|
expectedScrollId,
|
|
params.expectedLayersId,
|
|
scrollframeMsg + " - vertical scrollbar"
|
|
);
|
|
}
|
|
|
|
if (params.directions.horizontal && horizontalScrollbarHeight > 0) {
|
|
var horizontalScrollbarPoint = {
|
|
x:
|
|
params.trackLocation == ScrollbarTrackLocation.START
|
|
? boundingClientRect.x + scrollbarArrowButtonWidth + 5
|
|
: boundingClientRect.right -
|
|
verticalScrollbarWidth -
|
|
scrollbarArrowButtonWidth -
|
|
5,
|
|
y: boundingClientRect.bottom - horizontalScrollbarHeight / 2,
|
|
};
|
|
checkHitResult(
|
|
hitTest(horizontalScrollbarPoint),
|
|
expectedHitInfo,
|
|
expectedScrollId,
|
|
params.expectedLayersId,
|
|
scrollframeMsg + " - horizontal scrollbar"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Return a list of prefs for the given test identifier.
|
|
function getPrefs(ident) {
|
|
switch (ident) {
|
|
case "TOUCH_EVENTS:PAN":
|
|
return [
|
|
// Dropping the touch slop to 0 makes the tests easier to write because
|
|
// we can just do a one-pixel drag to get over the pan threshold rather
|
|
// than having to hard-code some larger value.
|
|
["apz.touch_start_tolerance", "0.0"],
|
|
// The touchstart from the drag can turn into a long-tap if the touch-move
|
|
// events get held up. Try to prevent that by making long-taps require
|
|
// a 10 second hold. Note that we also cannot enable chaos mode on this
|
|
// test for this reason, since chaos mode can cause the long-press timer
|
|
// to fire sooner than the pref dictates.
|
|
["ui.click_hold_context_menus.delay", 10000],
|
|
// The subtests in this test do touch-drags to pan the page, but we don't
|
|
// want those pans to turn into fling animations, so we increase the
|
|
// fling min velocity requirement absurdly high.
|
|
["apz.fling_min_velocity_threshold", "10000"],
|
|
// The helper_div_pan's div gets a displayport on scroll, but if the
|
|
// test takes too long the displayport can expire before the new scroll
|
|
// position is synced back to the main thread. So we disable displayport
|
|
// expiry for these tests.
|
|
["apz.displayport_expiry_ms", 0],
|
|
// We need to disable touch resampling during these tests because we
|
|
// rely on touch move events being processed without delay. Touch
|
|
// resampling only processes them once vsync fires.
|
|
["android.touch_resampling.enabled", false],
|
|
];
|
|
case "TOUCH_ACTION":
|
|
return [
|
|
...getPrefs("TOUCH_EVENTS:PAN"),
|
|
["apz.test.fails_with_native_injection", getPlatform() == "windows"],
|
|
];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
var ApzCleanup = {
|
|
_cleanups: [],
|
|
|
|
register(func) {
|
|
if (!this._cleanups.length) {
|
|
if (!window.isApzSubtest) {
|
|
SimpleTest.registerCleanupFunction(this.execute.bind(this));
|
|
} // else ApzCleanup.execute is called from runSubtestsSeriallyInFreshWindows
|
|
}
|
|
this._cleanups.push(func);
|
|
},
|
|
|
|
execute() {
|
|
while (this._cleanups.length) {
|
|
var func = this._cleanups.pop();
|
|
try {
|
|
func();
|
|
} catch (ex) {
|
|
SimpleTest.ok(
|
|
false,
|
|
"Subtest cleanup function [" +
|
|
func.toString() +
|
|
"] threw exception [" +
|
|
ex +
|
|
"] on page [" +
|
|
location.href +
|
|
"]"
|
|
);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Returns a promise that will resolve if `eventTarget` receives an event of the
|
|
* given type that passes the given filter. Only the first matching message is
|
|
* used. The filter must be a function (or null); it is called with the event
|
|
* object and the call must return true to resolve the promise.
|
|
*/
|
|
function promiseOneEvent(eventTarget, eventType, filter) {
|
|
return new Promise((resolve, reject) => {
|
|
eventTarget.addEventListener(eventType, function listener(e) {
|
|
let success = false;
|
|
if (filter == null) {
|
|
success = true;
|
|
} else if (typeof filter == "function") {
|
|
try {
|
|
success = filter(e);
|
|
} catch (ex) {
|
|
dump(
|
|
`ERROR: Filter passed to promiseOneEvent threw exception: ${ex}\n`
|
|
);
|
|
reject();
|
|
return;
|
|
}
|
|
} else {
|
|
dump(
|
|
"ERROR: Filter passed to promiseOneEvent was neither null nor a function\n"
|
|
);
|
|
reject();
|
|
return;
|
|
}
|
|
if (success) {
|
|
eventTarget.removeEventListener(eventType, listener);
|
|
resolve(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function visualViewportAsZoomedRect() {
|
|
let vv = window.visualViewport;
|
|
return {
|
|
x: vv.pageLeft,
|
|
y: vv.pageTop,
|
|
w: vv.width,
|
|
h: vv.height,
|
|
z: vv.scale,
|
|
};
|
|
}
|
|
|
|
// Pulls the latest compositor APZ test data and checks to see if the
|
|
// scroller with id `scrollerId` was checkerboarding. It also ensures that
|
|
// a scroller with id `scrollerId` was actually found in the test data.
|
|
// This function requires that "apz.test.logging_enabled" be set to true,
|
|
// in order for the test data to be logged.
|
|
function assertNotCheckerboarded(utils, scrollerId, msgPrefix) {
|
|
utils.advanceTimeAndRefresh(0);
|
|
var data = utils.getCompositorAPZTestData();
|
|
//dump(JSON.stringify(data, null, 4));
|
|
var found = false;
|
|
for (apzcData of data.additionalData) {
|
|
if (apzcData.key == scrollerId) {
|
|
var checkerboarding = apzcData.value
|
|
.split(",")
|
|
.includes("checkerboarding");
|
|
ok(!checkerboarding, `${msgPrefix}: scroller is not checkerboarding`);
|
|
found = true;
|
|
}
|
|
}
|
|
ok(found, `${msgPrefix}: Found the scroller in the APZ data`);
|
|
utils.restoreNormalRefresh();
|
|
}
|
|
|
|
async function waitToClearOutAnyPotentialScrolls(aWindow) {
|
|
await promiseFrame(aWindow);
|
|
await promiseFrame(aWindow);
|
|
await promiseOnlyApzControllerFlushed(aWindow);
|
|
await promiseFrame(aWindow);
|
|
await promiseFrame(aWindow);
|
|
}
|
|
|
|
function waitForScrollEvent(target) {
|
|
return new Promise(resolve => {
|
|
target.addEventListener("scroll", resolve, { once: true });
|
|
});
|
|
}
|
|
|
|
// This is another variant of promiseApzFlushedRepaints.
|
|
// We need this function because, unfortunately, there is no easy way to use
|
|
// paint_listeners.js' functions and apz_test_utils.js' functions in popup
|
|
// contents opened by extensions either as scripts in the popup contents or
|
|
// scripts inside SpecialPowers.spawn because we can't use privileged functions
|
|
// in the popup contents' script, we can't use functions basically as it as in
|
|
// the sandboxed context either.
|
|
async function promiseApzFlushedRepaintsInPopup(popup) {
|
|
// Flush APZ repaints and waits for MozAfterPaint.
|
|
await SpecialPowers.spawn(popup, [], async () => {
|
|
const utils = SpecialPowers.getDOMWindowUtils(content.window);
|
|
|
|
async function promiseAllPaintsDone() {
|
|
return new Promise(resolve => {
|
|
function waitForPaints() {
|
|
if (utils.isMozAfterPaintPending) {
|
|
dump("Waits for a MozAfterPaint event\n");
|
|
content.window.addEventListener(
|
|
"MozAfterPaint",
|
|
() => {
|
|
dump("Got a MozAfterPaint event\n");
|
|
waitForPaints();
|
|
},
|
|
{ once: true }
|
|
);
|
|
} else {
|
|
dump("No more pending MozAfterPaint\n");
|
|
content.window.setTimeout(resolve, 0);
|
|
}
|
|
}
|
|
waitForPaints();
|
|
});
|
|
}
|
|
await promiseAllPaintsDone();
|
|
|
|
await new Promise(resolve => {
|
|
var repaintDone = function () {
|
|
dump("APZ flush done\n");
|
|
SpecialPowers.Services.obs.removeObserver(
|
|
repaintDone,
|
|
"apz-repaints-flushed"
|
|
);
|
|
content.window.setTimeout(resolve, 0);
|
|
};
|
|
SpecialPowers.Services.obs.addObserver(
|
|
repaintDone,
|
|
"apz-repaints-flushed"
|
|
);
|
|
if (utils.flushApzRepaints()) {
|
|
dump("Flushed APZ repaints, waiting for callback...\n");
|
|
} else {
|
|
dump(
|
|
"Flushing APZ repaints was a no-op, triggering callback directly...\n"
|
|
);
|
|
repaintDone();
|
|
}
|
|
});
|
|
|
|
await promiseAllPaintsDone();
|
|
});
|
|
}
|
|
|
|
// A utility function to make sure there's no scroll animation on the given
|
|
// |aElement|.
|
|
async function cancelScrollAnimation(aElement, aWindow = window) {
|
|
// In fact there's no good way to directly cancel the active animation on the
|
|
// element, so we destroy the corresponding scrollable frame then reconstruct
|
|
// a new scrollable frame so that it clobbers the animation.
|
|
const originalStyle = aElement.style.display;
|
|
aElement.style.display = "none";
|
|
await aWindow.promiseApzFlushedRepaints();
|
|
aElement.style.display = originalStyle;
|
|
await aWindow.promiseApzFlushedRepaints();
|
|
}
|
|
|
|
function collectSampledScrollOffsets(aElement, aPopupElement = null) {
|
|
const utils = SpecialPowers.getDOMWindowUtils(
|
|
aPopupElement ? aPopupElement.ownerGlobal : window
|
|
);
|
|
let data = utils.getCompositorAPZTestData(aPopupElement);
|
|
let sampledResults = data.sampledResults;
|
|
|
|
const layersId = utils.getLayersId(aPopupElement);
|
|
const scrollId = utils.getViewId(aElement);
|
|
|
|
return sampledResults.filter(
|
|
result =>
|
|
SpecialPowers.wrap(result).layersId == layersId &&
|
|
SpecialPowers.wrap(result).scrollId == scrollId
|
|
);
|
|
}
|
|
|
|
function cloneVisualViewport() {
|
|
return {
|
|
offsetLeft: visualViewport.offsetLeft,
|
|
offsetTop: visualViewport.offsetTop,
|
|
pageLeft: visualViewport.pageLeft,
|
|
pageTop: visualViewport.pageTop,
|
|
width: visualViewport.width,
|
|
height: visualViewport.height,
|
|
scale: visualViewport.scale,
|
|
};
|
|
}
|
|
|
|
function compareVisualViewport(
|
|
aVisualViewportValue1,
|
|
aVisualViewportValue2,
|
|
aMessage
|
|
) {
|
|
for (let p in aVisualViewportValue1) {
|
|
// Due to the method difference of the calculation for double-tap-zoom in
|
|
// OOP iframes, we allow 1.0 difference in each visualViewport value.
|
|
// NOTE: Because of our layer pixel snapping (bug 1774315 and bug 1852884)
|
|
// the visual viewport metrics can have one more pixel difference so we
|
|
// allow it here.
|
|
const tolerance = 1.0 + 1.0;
|
|
isfuzzy(
|
|
aVisualViewportValue1[p],
|
|
aVisualViewportValue2[p],
|
|
aVisualViewportValue1.scale > 1.0
|
|
? tolerance
|
|
: tolerance / aVisualViewportValue1.scale,
|
|
`${p} should be same on ${aMessage}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Loads a URL in an iframe and waits until APZ is stable
|
|
async function setupIframe(aIFrame, aURL, aIsOffScreen = false) {
|
|
const iframeLoadPromise = promiseOneEvent(aIFrame, "load", null);
|
|
aIFrame.src = aURL;
|
|
await iframeLoadPromise;
|
|
|
|
if (!aIsOffScreen) {
|
|
await SpecialPowers.spawn(aIFrame, [], async () => {
|
|
await content.wrappedJSObject.waitUntilApzStable();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Loads a URL in an iframe and replaces its origin to
|
|
// create an out-of-process iframe
|
|
async function setupCrossOriginIFrame(aIFrame, aUrl, aIsOffScreen = false) {
|
|
let iframeURL = SimpleTest.getTestFileURL(aUrl);
|
|
iframeURL = iframeURL.replace(window.location.origin, "https://example.com");
|
|
await setupIframe(aIFrame, iframeURL, aIsOffScreen);
|
|
if (!aIsOffScreen) {
|
|
await SpecialPowers.spawn(aIFrame, [], async () => {
|
|
await SpecialPowers.contentTransformsReceived(content);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Make sure APZ is ready for the popup.
|
|
// With enabling GPU process initiating APZ in the popup takes some time.
|
|
// Before the APZ has been initiated, calling flushApzRepaints() for the popup
|
|
// returns false.
|
|
async function ensureApzReadyForPopup(
|
|
aPopupElement,
|
|
aWindow = window,
|
|
aRetry = 10
|
|
) {
|
|
let retry = 0;
|
|
while (
|
|
!SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints(aPopupElement)
|
|
) {
|
|
await promiseFrame();
|
|
retry++;
|
|
if (retry > aRetry) {
|
|
ok(false, "The popup didn't initialize APZ");
|
|
return;
|
|
}
|
|
}
|
|
}
|