testcommon.js (15718B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /** 5 * Use this variable if you specify duration or some other properties 6 * for script animation. 7 * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); 8 * 9 * NOTE: Creating animations with short duration may cause intermittent 10 * failures in asynchronous test. For example, the short duration animation 11 * might be finished when animation.ready has been fulfilled because of slow 12 * platforms or busyness of the main thread. 13 * Setting short duration to cancel its animation does not matter but 14 * if you don't want to cancel the animation, consider using longer duration. 15 */ 16 const MS_PER_SEC = 1000; 17 18 /* The recommended minimum precision to use for time values[1]. 19 * 20 * [1] https://drafts.csswg.org/web-animations/#precision-of-time-values 21 */ 22 var TIME_PRECISION = 0.0005; // ms 23 24 /* 25 * Allow implementations to substitute an alternative method for comparing 26 * times based on their precision requirements. 27 */ 28 function assert_times_equal(actual, expected, description) { 29 assert_approx_equals(actual, expected, TIME_PRECISION * 2, description); 30 } 31 32 /* 33 * Compare a time value based on its precision requirements with a fixed value. 34 */ 35 function assert_time_equals_literal(actual, expected, description) { 36 assert_approx_equals(actual, expected, TIME_PRECISION, description); 37 } 38 39 /* 40 * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'. 41 * This function allows error, 0.01, because on Android when we are scaling down 42 * the document, it results in some errors. 43 */ 44 function assert_matrix_equals(actual, expected, description) { 45 var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/; 46 assert_regexp_match(actual, matrixRegExp, "Actual value should be a matrix"); 47 assert_regexp_match( 48 expected, 49 matrixRegExp, 50 "Expected value should be a matrix" 51 ); 52 53 var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number); 54 var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number); 55 56 assert_equals( 57 actualMatrixArray.length, 58 expectedMatrixArray.length, 59 "Array lengths should be equal (got '" + 60 expected + 61 "' and '" + 62 actual + 63 "'): " + 64 description 65 ); 66 for (var i = 0; i < actualMatrixArray.length; i++) { 67 assert_approx_equals( 68 actualMatrixArray[i], 69 expectedMatrixArray[i], 70 0.01, 71 "Matrix array should be equal (got '" + 72 expected + 73 "' and '" + 74 actual + 75 "'): " + 76 description 77 ); 78 } 79 } 80 81 /** 82 * Compare given values which are same format of 83 * KeyframeEffectReadonly::GetProperties. 84 */ 85 function assert_properties_equal(actual, expected) { 86 assert_equals(actual.length, expected.length); 87 88 const compareProperties = (a, b) => 89 a.property == b.property ? 0 : a.property < b.property ? -1 : 1; 90 91 const sortedActual = actual.sort(compareProperties); 92 const sortedExpected = expected.sort(compareProperties); 93 94 const serializeValues = values => 95 values 96 .map( 97 value => 98 "{ " + 99 ["offset", "value", "easing", "composite"] 100 .map(member => `${member}: ${value[member]}`) 101 .join(", ") + 102 " }" 103 ) 104 .join(", "); 105 106 for (let i = 0; i < sortedActual.length; i++) { 107 assert_equals( 108 sortedActual[i].property, 109 sortedExpected[i].property, 110 "CSS property name should match" 111 ); 112 assert_equals( 113 serializeValues(sortedActual[i].values), 114 serializeValues(sortedExpected[i].values), 115 `Values arrays do not match for ` + `${sortedActual[i].property} property` 116 ); 117 } 118 } 119 120 /** 121 * Construct a object which is same to a value of 122 * KeyframeEffectReadonly::GetProperties(). 123 * The method returns undefined as a value in case of missing keyframe. 124 * Therefor, we can use undefined for |value| and |easing| parameter. 125 * 126 * @param offset - keyframe offset. e.g. 0.1 127 * @param value - any keyframe value. e.g. undefined '1px', 'center', 0.5 128 * @param composite - 'replace', 'add', 'accumulate' 129 * @param easing - e.g. undefined, 'linear', 'ease' and so on 130 * @return Object - 131 * e.g. { offset: 0.1, value: '1px', composite: 'replace', easing: 'ease'} 132 */ 133 function valueFormat(offset, value, composite, easing) { 134 return { offset, value, easing, composite }; 135 } 136 137 /** 138 * Appends a div to the document body and creates an animation on the div. 139 * NOTE: This function asserts when trying to create animations with durations 140 * shorter than 100s because the shorter duration may cause intermittent 141 * failures. If you are not sure how long it is suitable, use 100s; it's 142 * long enough but shorter than our test framework timeout (330s). 143 * If you really need to use shorter durations, use animate() function directly. 144 * 145 * @param t The testharness.js Test object. If provided, this will be used 146 * to register a cleanup callback to remove the div when the test 147 * finishes. 148 * @param attrs A dictionary object with attribute names and values to set on 149 * the div. 150 * @param frames The keyframes passed to Element.animate(). 151 * @param options The options passed to Element.animate(). 152 */ 153 function addDivAndAnimate(t, attrs, frames, options) { 154 let animDur = typeof options === "object" ? options.duration : options; 155 assert_greater_than_equal( 156 animDur, 157 100 * MS_PER_SEC, 158 "Clients of this addDivAndAnimate API must request a duration " + 159 "of at least 100s, to avoid intermittent failures from e.g." + 160 "the main thread being busy for an extended period" 161 ); 162 163 return addDiv(t, attrs).animate(frames, options); 164 } 165 166 /** 167 * Appends a div to the document body. 168 * 169 * @param t The testharness.js Test object. If provided, this will be used 170 * to register a cleanup callback to remove the div when the test 171 * finishes. 172 * 173 * @param attrs A dictionary object with attribute names and values to set on 174 * the div. 175 */ 176 function addDiv(t, attrs) { 177 var div = document.createElement("div"); 178 if (attrs) { 179 for (var attrName in attrs) { 180 div.setAttribute(attrName, attrs[attrName]); 181 } 182 } 183 document.body.appendChild(div); 184 if (t && typeof t.add_cleanup === "function") { 185 t.add_cleanup(function () { 186 if (div.parentNode) { 187 div.remove(); 188 } 189 }); 190 } 191 return div; 192 } 193 194 /** 195 * Appends a style div to the document head. 196 * 197 * @param t The testharness.js Test object. If provided, this will be used 198 * to register a cleanup callback to remove the style element 199 * when the test finishes. 200 * 201 * @param rules A dictionary object with selector names and rules to set on 202 * the style sheet. 203 */ 204 function addStyle(t, rules) { 205 var extraStyle = document.createElement("style"); 206 document.head.appendChild(extraStyle); 207 if (rules) { 208 var sheet = extraStyle.sheet; 209 for (var selector in rules) { 210 sheet.insertRule( 211 selector + "{" + rules[selector] + "}", 212 sheet.cssRules.length 213 ); 214 } 215 } 216 217 if (t && typeof t.add_cleanup === "function") { 218 t.add_cleanup(function () { 219 extraStyle.remove(); 220 }); 221 } 222 } 223 224 /** 225 * Takes a CSS property (e.g. margin-left) and returns the equivalent IDL 226 * name (e.g. marginLeft). 227 */ 228 function propertyToIDL(property) { 229 var prefixMatch = property.match(/^-(\w+)-/); 230 if (prefixMatch) { 231 var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1]; 232 property = prefix + property.substring(prefixMatch[0].length - 1); 233 } 234 // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute 235 return property.replace(/-([a-z])/gi, function (str, group) { 236 return group.toUpperCase(); 237 }); 238 } 239 240 /** 241 * Promise wrapper for requestAnimationFrame. 242 */ 243 function waitForFrame() { 244 return new Promise(function (resolve, reject) { 245 window.requestAnimationFrame(resolve); 246 }); 247 } 248 249 /** 250 * Waits for a requestAnimationFrame callback in the next refresh driver tick. 251 */ 252 function waitForNextFrame(aWindow = window) { 253 const timeAtStart = aWindow.document.timeline.currentTime; 254 return new Promise(resolve => { 255 aWindow.requestAnimationFrame(() => { 256 if (timeAtStart === aWindow.document.timeline.currentTime) { 257 aWindow.requestAnimationFrame(resolve); 258 } else { 259 resolve(); 260 } 261 }); 262 }); 263 } 264 265 /** 266 * Returns a Promise that is resolved after the given number of consecutive 267 * animation frames have occured (using requestAnimationFrame callbacks). 268 * 269 * @param aFrameCount The number of animation frames. 270 * @param aOnFrame An optional function to be processed in each animation frame. 271 * @param aWindow An optional window object to be used for requestAnimationFrame. 272 */ 273 function waitForAnimationFrames(aFrameCount, aOnFrame, aWindow = window) { 274 const timeAtStart = aWindow.document.timeline.currentTime; 275 return new Promise(function (resolve, reject) { 276 function handleFrame() { 277 if (aOnFrame && typeof aOnFrame === "function") { 278 aOnFrame(); 279 } 280 if ( 281 timeAtStart != aWindow.document.timeline.currentTime && 282 --aFrameCount <= 0 283 ) { 284 resolve(); 285 } else { 286 aWindow.requestAnimationFrame(handleFrame); // wait another frame 287 } 288 } 289 aWindow.requestAnimationFrame(handleFrame); 290 }); 291 } 292 293 /** 294 * Promise wrapper for requestIdleCallback. 295 */ 296 function waitForIdle() { 297 return new Promise(resolve => { 298 requestIdleCallback(resolve); 299 }); 300 } 301 302 /** 303 * Wrapper that takes a sequence of N animations and returns: 304 * 305 * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]); 306 */ 307 function waitForAllAnimations(animations) { 308 return Promise.all( 309 animations.map(function (animation) { 310 return animation.ready; 311 }) 312 ); 313 } 314 315 /** 316 * Flush the computed style for the given element. This is useful, for example, 317 * when we are testing a transition and need the initial value of a property 318 * to be computed so that when we synchronouslyet set it to a different value 319 * we actually get a transition instead of that being the initial value. 320 */ 321 function flushComputedStyle(elem) { 322 var cs = getComputedStyle(elem); 323 cs.marginLeft; 324 } 325 326 if (opener) { 327 for (var funcName of [ 328 "async_test", 329 "assert_not_equals", 330 "assert_equals", 331 "assert_approx_equals", 332 "assert_less_than", 333 "assert_less_than_equal", 334 "assert_greater_than", 335 "assert_between_inclusive", 336 "assert_true", 337 "assert_false", 338 "assert_class_string", 339 "assert_throws", 340 "assert_unreached", 341 "assert_regexp_match", 342 "promise_test", 343 "test", 344 ]) { 345 if (opener[funcName]) { 346 window[funcName] = opener[funcName].bind(opener); 347 } 348 } 349 350 window.EventWatcher = opener.EventWatcher; 351 352 function done() { 353 opener.add_completion_callback(function () { 354 self.close(); 355 }); 356 opener.done(); 357 } 358 } 359 360 /* 361 * Returns a promise that is resolved when the document has finished loading. 362 */ 363 function waitForDocumentLoad() { 364 return new Promise(function (resolve, reject) { 365 if (document.readyState === "complete") { 366 resolve(); 367 } else { 368 window.addEventListener("load", resolve); 369 } 370 }); 371 } 372 373 /* 374 * Enters test refresh mode, and restores the mode when |t| finishes. 375 */ 376 function useTestRefreshMode(t) { 377 function ensureNoSuppressedPaints() { 378 return new Promise(resolve => { 379 function checkSuppressedPaints() { 380 if (!SpecialPowers.DOMWindowUtils.paintingSuppressed) { 381 resolve(); 382 } else { 383 window.requestAnimationFrame(checkSuppressedPaints); 384 } 385 } 386 checkSuppressedPaints(); 387 }); 388 } 389 390 return ensureNoSuppressedPaints().then(() => { 391 SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); 392 t.add_cleanup(() => { 393 SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); 394 }); 395 }); 396 } 397 398 /** 399 * Returns true if off-main-thread animations. 400 */ 401 function isOMTAEnabled() { 402 const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations"; 403 return ( 404 SpecialPowers.DOMWindowUtils.layerManagerRemote && 405 SpecialPowers.getBoolPref(OMTAPrefKey) 406 ); 407 } 408 409 /** 410 * Append an SVG element to the target element. 411 * 412 * @param target The element which want to append. 413 * @param attrs A array object with attribute name and values to set on 414 * the SVG element. 415 * @return An SVG outer element. 416 */ 417 function addSVGElement(target, tag, attrs) { 418 if (!target) { 419 return null; 420 } 421 var element = document.createElementNS("http://www.w3.org/2000/svg", tag); 422 if (attrs) { 423 for (var attrName in attrs) { 424 element.setAttributeNS(null, attrName, attrs[attrName]); 425 } 426 } 427 target.appendChild(element); 428 return element; 429 } 430 431 /** 432 * Get Animation distance between two specified values for a specific property. 433 * 434 * @param target The target element. 435 * @param prop The CSS property. 436 * @param v1 The first property value. 437 * @param v2 The Second property value. 438 * 439 * @return The distance between |v1| and |v2| for |prop| on |target|. 440 */ 441 function getDistance(target, prop, v1, v2) { 442 if (!target) { 443 return 0.0; 444 } 445 return SpecialPowers.DOMWindowUtils.computeAnimationDistance( 446 target, 447 prop, 448 v1, 449 v2 450 ); 451 } 452 453 /* 454 * A promise wrapper for waiting MozAfterPaint. 455 */ 456 function waitForPaints() { 457 // FIXME: Bug 1415065. Instead waiting for two requestAnimationFrames, we 458 // should wait for MozAfterPaint once after MozAfterPaint is fired properly 459 // (bug 1341294). 460 return waitForAnimationFrames(2); 461 } 462 463 // Returns true if |aAnimation| begins at the current timeline time. We 464 // sometimes need to detect this case because if we started an animation 465 // asynchronously (e.g. using play()) and then ended up running the next frame 466 // at precisely the time the animation started (due to aligning with vsync 467 // refresh rate) then we won't end up restyling in that frame. 468 function animationStartsRightNow(aAnimation) { 469 return ( 470 aAnimation.startTime === aAnimation.timeline.currentTime && 471 aAnimation.currentTime === 0 472 ); 473 } 474 475 // Waits for a given animation being ready to restyle. 476 async function waitForAnimationReadyToRestyle(aAnimation) { 477 await aAnimation.ready; 478 // If |aAnimation| begins at the current timeline time, we will not process 479 // restyling in the initial frame because of aligning with the refresh driver, 480 // the animation frame in which the ready promise is resolved happens to 481 // coincide perfectly with the start time of the animation. In this case no 482 // restyling is needed in the frame so we have to wait one more frame. 483 if (animationStartsRightNow(aAnimation)) { 484 await waitForNextFrame(aAnimation.ownerGlobal); 485 } 486 } 487 488 // Returns the animation restyle markers observed during |frameCount| refresh 489 // driver ticks in this `window`. This function is typically used to count the 490 // number of restyles that take place as part of the style update that happens 491 // on each refresh driver tick, as opposed to synchronous restyles triggered by 492 // script. 493 // 494 // For the latter observeAnimSyncStyling (below) should be used. 495 function observeStyling(frameCount, onFrame) { 496 return observeStylingInTargetWindow(window, frameCount, onFrame); 497 } 498 499 // As with observeStyling but applied to target window |aWindow|. 500 function observeStylingInTargetWindow(aWindow, aFrameCount, aOnFrame) { 501 let priorAnimationTriggeredRestyles = 502 SpecialPowers.wrap(aWindow).windowUtils.animationTriggeredRestyles; 503 504 return new Promise(resolve => { 505 return waitForAnimationFrames(aFrameCount, aOnFrame, aWindow).then(() => { 506 let restyleCount = 507 SpecialPowers.wrap(aWindow).windowUtils.animationTriggeredRestyles - 508 priorAnimationTriggeredRestyles; 509 510 resolve(restyleCount); 511 }); 512 }); 513 }