testcommon.js (11409B)
1 'use strict'; 2 3 const MS_PER_SEC = 1000; 4 5 // The recommended minimum precision to use for time values[1]. 6 // 7 // [1] https://drafts.csswg.org/web-animations/#precision-of-time-values 8 const TIME_PRECISION = 0.0005; // ms 9 10 // Allow implementations to substitute an alternative method for comparing 11 // times based on their precision requirements. 12 if (!window.assert_times_equal) { 13 window.assert_times_equal = (actual, expected, description) => { 14 assert_approx_equals(actual, expected, TIME_PRECISION * 2, description); 15 }; 16 } 17 18 // Allow implementations to substitute an alternative method for comparing 19 // times based on their precision requirements. 20 if (!window.assert_time_greater_than_equal) { 21 window.assert_time_greater_than_equal = (actual, expected, description) => { 22 assert_greater_than_equal(actual, expected - 2 * TIME_PRECISION, 23 description); 24 }; 25 } 26 27 // Allow implementations to substitute an alternative method for comparing 28 // a time value based on its precision requirements with a fixed value. 29 if (!window.assert_time_equals_literal) { 30 window.assert_time_equals_literal = (actual, expected, description) => { 31 if (Math.abs(expected) === Infinity) { 32 assert_equals(actual, expected, description); 33 } else { 34 assert_approx_equals(actual, expected, TIME_PRECISION, description); 35 } 36 } 37 } 38 39 // creates div element, appends it to the document body and 40 // removes the created element during test cleanup 41 function createDiv(test, doc) { 42 return createElement(test, 'div', doc); 43 } 44 45 // creates element of given tagName, appends it to the document body and 46 // removes the created element during test cleanup 47 // if tagName is null or undefined, returns div element 48 function createElement(test, tagName, doc) { 49 if (!doc) { 50 doc = document; 51 } 52 const element = doc.createElement(tagName || 'div'); 53 doc.body.appendChild(element); 54 test.add_cleanup(() => { 55 element.remove(); 56 }); 57 return element; 58 } 59 60 // Creates a style element with the specified rules, appends it to the document 61 // head and removes the created element during test cleanup. 62 // |rules| is an object. For example: 63 // { '@keyframes anim': '' , 64 // '.className': 'animation: anim 100s;' }; 65 // or 66 // { '.className1::before': 'content: ""; width: 0px; transition: all 10s;', 67 // '.className2::before': 'width: 100px;' }; 68 // The object property name could be a keyframes name, or a selector. 69 // The object property value is declarations which are property:value pairs 70 // split by a space. 71 function createStyle(test, rules, doc) { 72 if (!doc) { 73 doc = document; 74 } 75 const extraStyle = doc.createElement('style'); 76 doc.head.appendChild(extraStyle); 77 if (rules) { 78 const sheet = extraStyle.sheet; 79 for (const selector in rules) { 80 sheet.insertRule(`${selector}{${rules[selector]}}`, 81 sheet.cssRules.length); 82 } 83 } 84 test.add_cleanup(() => { 85 extraStyle.remove(); 86 }); 87 } 88 89 // Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1). 90 function cubicBezier(x1, y1, x2, y2) { 91 const xForT = t => { 92 const omt = 1-t; 93 return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t; 94 }; 95 96 const yForT = t => { 97 const omt = 1-t; 98 return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t; 99 }; 100 101 const tForX = x => { 102 // Binary subdivision. 103 let mint = 0, maxt = 1; 104 for (let i = 0; i < 30; ++i) { 105 const guesst = (mint + maxt) / 2; 106 const guessx = xForT(guesst); 107 if (x < guessx) { 108 maxt = guesst; 109 } else { 110 mint = guesst; 111 } 112 } 113 return (mint + maxt) / 2; 114 }; 115 116 return x => { 117 if (x == 0) { 118 return 0; 119 } 120 if (x == 1) { 121 return 1; 122 } 123 return yForT(tForX(x)); 124 }; 125 } 126 127 function stepEnd(nsteps) { 128 return x => Math.floor(x * nsteps) / nsteps; 129 } 130 131 function stepStart(nsteps) { 132 return x => { 133 const result = Math.floor(x * nsteps + 1.0) / nsteps; 134 return (result > 1.0) ? 1.0 : result; 135 }; 136 } 137 138 function waitForAnimationFrames(frameCount) { 139 return new Promise(resolve => { 140 function handleFrame() { 141 if (--frameCount <= 0) { 142 resolve(); 143 } else { 144 window.requestAnimationFrame(handleFrame); // wait another frame 145 } 146 } 147 window.requestAnimationFrame(handleFrame); 148 }); 149 } 150 151 // Continually calls requestAnimationFrame until |minDelay| has elapsed 152 // as recorded using document.timeline.currentTime (i.e. frame time not 153 // wall-clock time). 154 function waitForAnimationFramesWithDelay(minDelay) { 155 const startTime = document.timeline.currentTime; 156 return new Promise(resolve => { 157 (function handleFrame() { 158 if (document.timeline.currentTime - startTime >= minDelay) { 159 resolve(); 160 } else { 161 window.requestAnimationFrame(handleFrame); 162 } 163 }()); 164 }); 165 } 166 167 function runAndWaitForFrameUpdate(callback) { 168 return new Promise(resolve => { 169 window.requestAnimationFrame(() => { 170 callback(); 171 window.requestAnimationFrame(resolve); 172 }); 173 }); 174 } 175 176 // Waits for a requestAnimationFrame callback in the next refresh driver tick. 177 function waitForNextFrame() { 178 const timeAtStart = document.timeline.currentTime; 179 return new Promise(resolve => { 180 (function handleFrame() { 181 if (timeAtStart === document.timeline.currentTime) { 182 window.requestAnimationFrame(handleFrame); 183 } else { 184 resolve(); 185 } 186 }()); 187 }); 188 } 189 190 async function insertFrameAndAwaitLoad(test, iframe, doc) { 191 const eventWatcher = new EventWatcher(test, iframe, ['load']); 192 const event_promise = eventWatcher.wait_for('load'); 193 194 doc.body.appendChild(iframe); 195 test.add_cleanup(() => { doc.body.removeChild(iframe); }); 196 197 await event_promise; 198 } 199 200 // Returns 'matrix()' or 'matrix3d()' function string generated from an array. 201 function createMatrixFromArray(array) { 202 return (array.length == 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`; 203 } 204 205 // Returns 'matrix3d()' function string equivalent to 206 // 'rotate3d(x, y, z, radian)'. 207 function rotate3dToMatrix3d(x, y, z, radian) { 208 return createMatrixFromArray(rotate3dToMatrix(x, y, z, radian)); 209 } 210 211 // Returns an array of the 4x4 matrix equivalent to 'rotate3d(x, y, z, radian)'. 212 // https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined 213 function rotate3dToMatrix(x, y, z, radian) { 214 const sc = Math.sin(radian / 2) * Math.cos(radian / 2); 215 const sq = Math.sin(radian / 2) * Math.sin(radian / 2); 216 217 // Normalize the vector. 218 const length = Math.sqrt(x*x + y*y + z*z); 219 x /= length; 220 y /= length; 221 z /= length; 222 223 return [ 224 1 - 2 * (y*y + z*z) * sq, 225 2 * (x * y * sq + z * sc), 226 2 * (x * z * sq - y * sc), 227 0, 228 2 * (x * y * sq - z * sc), 229 1 - 2 * (x*x + z*z) * sq, 230 2 * (y * z * sq + x * sc), 231 0, 232 2 * (x * z * sq + y * sc), 233 2 * (y * z * sq - x * sc), 234 1 - 2 * (x*x + y*y) * sq, 235 0, 236 0, 237 0, 238 0, 239 1 240 ]; 241 } 242 243 // Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)' with tolerances. 244 function assert_matrix_equals(actual, expected, description) { 245 const matrixRegExp = /^matrix(?:3d)*\((.+)\)/; 246 assert_regexp_match(actual, matrixRegExp, 247 'Actual value is not a matrix') 248 assert_regexp_match(expected, matrixRegExp, 249 'Expected value is not a matrix'); 250 251 const actualMatrixArray = 252 actual.match(matrixRegExp)[1].split(',').map(Number); 253 const expectedMatrixArray = 254 expected.match(matrixRegExp)[1].split(',').map(Number); 255 256 assert_equals(actualMatrixArray.length, expectedMatrixArray.length, 257 `dimension of the matrix: ${description}`); 258 for (let i = 0; i < actualMatrixArray.length; i++) { 259 assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.0001, 260 `expected ${expected} but got ${actual}: ${description}`); 261 } 262 } 263 264 // Compare rotate3d vector like '0 1 0 45deg' with tolerances. 265 function assert_rotate3d_equals(actual, expected, description) { 266 const rotationRegExp =/^((([+-]?\d+(\.+\d+)?\s){3})?\d+(\.+\d+)?)deg/; 267 268 assert_regexp_match(actual, rotationRegExp, 269 'Actual value is not a rotate3d vector') 270 assert_regexp_match(expected, rotationRegExp, 271 'Expected value is not a rotate3d vector'); 272 273 const actualRotationVector = 274 actual.match(rotationRegExp)[1].split(' ').map(Number); 275 const expectedRotationVector = 276 expected.match(rotationRegExp)[1].split(' ').map(Number); 277 278 assert_equals(actualRotationVector.length, expectedRotationVector.length, 279 `dimension of the matrix: ${description}`); 280 for (let i = 0; i < actualRotationVector.length; i++) { 281 assert_approx_equals( 282 actualRotationVector[i], 283 expectedRotationVector[i], 284 0.0001, 285 `expected ${expected} but got ${actual}: ${description}`); 286 } 287 } 288 289 function assert_phase_at_time(animation, phase, currentTime) { 290 animation.currentTime = currentTime; 291 assert_phase(animation, phase); 292 } 293 294 function assert_phase(animation, phase) { 295 const fillMode = animation.effect.getTiming().fill; 296 const currentTime = animation.currentTime; 297 298 if (phase === 'active') { 299 // If the fill mode is 'none', then progress will only be non-null if we 300 // are in the active phase, except for progress-based timelines where 301 // currentTime = 100% is still 'active'. 302 animation.effect.updateTiming({ fill: 'none' }); 303 if ('ScrollTimeline' in window && animation.timeline instanceof ScrollTimeline) { 304 const isActive = animation.currentTime?.toString() == "100%" || 305 animation.effect.getComputedTiming().progress != null; 306 assert_true(isActive, 307 'Animation effect is in active phase when current time ' + 308 `is ${currentTime}.`); 309 } else { 310 assert_not_equals(animation.effect.getComputedTiming().progress, null, 311 'Animation effect is in active phase when current time ' + 312 `is ${currentTime}.`); 313 } 314 } else { 315 // The easiest way to distinguish between the 'before' phase and the 'after' 316 // phase is to toggle the fill mode. For example, if the progress is null 317 // when the fill mode is 'none' but non-null when the fill mode is 318 // 'backwards' then we are in the before phase. 319 animation.effect.updateTiming({ fill: 'none' }); 320 assert_equals(animation.effect.getComputedTiming().progress, null, 321 `Animation effect is in ${phase} phase when current time ` + 322 `is ${currentTime} (progress is null with 'none' fill mode)`); 323 324 animation.effect.updateTiming({ 325 fill: phase === 'before' ? 'backwards' : 'forwards', 326 }); 327 assert_not_equals(animation.effect.getComputedTiming().progress, null, 328 `Animation effect is in ${phase} phase when current ` + 329 `time is ${currentTime} (progress is non-null with ` + 330 `appropriate fill mode)`); 331 } 332 333 // Reset fill mode to avoid side-effects. 334 animation.effect.updateTiming({ fill: fillMode }); 335 } 336 337 338 // Use with reftest-wait to wait until compositor commits are no longer deferred 339 // before taking the screenshot. 340 // crbug.com/1378671 341 async function waitForCompositorReady() { 342 const animation = 343 document.body.animate({ opacity: [ 0, 1 ] }, {duration: 1 }); 344 return animation.finished; 345 } 346 347 async function takeScreenshotOnAnimationsReady() { 348 await Promise.all(document.getAnimations().map(a => a.ready)); 349 requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); 350 }