'use strict'; const MS_PER_SEC = 1000; // The recommended minimum precision to use for time values[1]. // // [1] https://drafts.csswg.org/web-animations/#precision-of-time-values const TIME_PRECISION = 0.0005; // ms // Allow implementations to substitute an alternative method for comparing // times based on their precision requirements. if (!window.assert_times_equal) { window.assert_times_equal = (actual, expected, description) => { assert_approx_equals(actual, expected, TIME_PRECISION * 2, description); }; } // Allow implementations to substitute an alternative method for comparing // times based on their precision requirements. if (!window.assert_time_greater_than_equal) { window.assert_time_greater_than_equal = (actual, expected, description) => { assert_greater_than_equal(actual, expected - 2 * TIME_PRECISION, description); }; } // Allow implementations to substitute an alternative method for comparing // a time value based on its precision requirements with a fixed value. if (!window.assert_time_equals_literal) { window.assert_time_equals_literal = (actual, expected, description) => { if (Math.abs(expected) === Infinity) { assert_equals(actual, expected, description); } else { assert_approx_equals(actual, expected, TIME_PRECISION, description); } } } // creates div element, appends it to the document body and // removes the created element during test cleanup function createDiv(test, doc) { return createElement(test, 'div', doc); } // creates element of given tagName, appends it to the document body and // removes the created element during test cleanup // if tagName is null or undefined, returns div element function createElement(test, tagName, doc) { if (!doc) { doc = document; } const element = doc.createElement(tagName || 'div'); doc.body.appendChild(element); test.add_cleanup(() => { element.remove(); }); return element; } // Creates a style element with the specified rules, appends it to the document // head and removes the created element during test cleanup. // |rules| is an object. For example: // { '@keyframes anim': '' , // '.className': 'animation: anim 100s;' }; // or // { '.className1::before': 'content: ""; width: 0px; transition: all 10s;', // '.className2::before': 'width: 100px;' }; // The object property name could be a keyframes name, or a selector. // The object property value is declarations which are property:value pairs // split by a space. function createStyle(test, rules, doc) { if (!doc) { doc = document; } const extraStyle = doc.createElement('style'); doc.head.appendChild(extraStyle); if (rules) { const sheet = extraStyle.sheet; for (const selector in rules) { sheet.insertRule(`${selector}{${rules[selector]}}`, sheet.cssRules.length); } } test.add_cleanup(() => { extraStyle.remove(); }); } // Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1). function cubicBezier(x1, y1, x2, y2) { const xForT = t => { const omt = 1-t; return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t; }; const yForT = t => { const omt = 1-t; return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t; }; const tForX = x => { // Binary subdivision. let mint = 0, maxt = 1; for (let i = 0; i < 30; ++i) { const guesst = (mint + maxt) / 2; const guessx = xForT(guesst); if (x < guessx) { maxt = guesst; } else { mint = guesst; } } return (mint + maxt) / 2; }; return x => { if (x == 0) { return 0; } if (x == 1) { return 1; } return yForT(tForX(x)); }; } function stepEnd(nsteps) { return x => Math.floor(x * nsteps) / nsteps; } function stepStart(nsteps) { return x => { const result = Math.floor(x * nsteps + 1.0) / nsteps; return (result > 1.0) ? 1.0 : result; }; } function waitForAnimationFrames(frameCount) { return new Promise(resolve => { function handleFrame() { if (--frameCount <= 0) { resolve(); } else { window.requestAnimationFrame(handleFrame); // wait another frame } } window.requestAnimationFrame(handleFrame); }); } // Continually calls requestAnimationFrame until |minDelay| has elapsed // as recorded using document.timeline.currentTime (i.e. frame time not // wall-clock time). function waitForAnimationFramesWithDelay(minDelay) { const startTime = document.timeline.currentTime; return new Promise(resolve => { (function handleFrame() { if (document.timeline.currentTime - startTime >= minDelay) { resolve(); } else { window.requestAnimationFrame(handleFrame); } }()); }); } function runAndWaitForFrameUpdate(callback) { return new Promise(resolve => { window.requestAnimationFrame(() => { callback(); window.requestAnimationFrame(resolve); }); }); } // Waits for a requestAnimationFrame callback in the next refresh driver tick. function waitForNextFrame() { const timeAtStart = document.timeline.currentTime; return new Promise(resolve => { (function handleFrame() { if (timeAtStart === document.timeline.currentTime) { window.requestAnimationFrame(handleFrame); } else { resolve(); } }()); }); } async function insertFrameAndAwaitLoad(test, iframe, doc) { const eventWatcher = new EventWatcher(test, iframe, ['load']); const event_promise = eventWatcher.wait_for('load'); doc.body.appendChild(iframe); test.add_cleanup(() => { doc.body.removeChild(iframe); }); await event_promise; } // Returns 'matrix()' or 'matrix3d()' function string generated from an array. function createMatrixFromArray(array) { return (array.length == 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`; } // Returns 'matrix3d()' function string equivalent to // 'rotate3d(x, y, z, radian)'. function rotate3dToMatrix3d(x, y, z, radian) { return createMatrixFromArray(rotate3dToMatrix(x, y, z, radian)); } // Returns an array of the 4x4 matrix equivalent to 'rotate3d(x, y, z, radian)'. // https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined function rotate3dToMatrix(x, y, z, radian) { const sc = Math.sin(radian / 2) * Math.cos(radian / 2); const sq = Math.sin(radian / 2) * Math.sin(radian / 2); // Normalize the vector. const length = Math.sqrt(x*x + y*y + z*z); x /= length; y /= length; z /= length; return [ 1 - 2 * (y*y + z*z) * sq, 2 * (x * y * sq + z * sc), 2 * (x * z * sq - y * sc), 0, 2 * (x * y * sq - z * sc), 1 - 2 * (x*x + z*z) * sq, 2 * (y * z * sq + x * sc), 0, 2 * (x * z * sq + y * sc), 2 * (y * z * sq - x * sc), 1 - 2 * (x*x + y*y) * sq, 0, 0, 0, 0, 1 ]; } // Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)' with tolerances. function assert_matrix_equals(actual, expected, description) { const matrixRegExp = /^matrix(?:3d)*\((.+)\)/; assert_regexp_match(actual, matrixRegExp, 'Actual value is not a matrix') assert_regexp_match(expected, matrixRegExp, 'Expected value is not a matrix'); const actualMatrixArray = actual.match(matrixRegExp)[1].split(',').map(Number); const expectedMatrixArray = expected.match(matrixRegExp)[1].split(',').map(Number); assert_equals(actualMatrixArray.length, expectedMatrixArray.length, `dimension of the matrix: ${description}`); for (let i = 0; i < actualMatrixArray.length; i++) { assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.0001, `expected ${expected} but got ${actual}: ${description}`); } } // Compare rotate3d vector like '0 1 0 45deg' with tolerances. function assert_rotate3d_equals(actual, expected, description) { const rotationRegExp =/^((([+-]?\d+(\.+\d+)?\s){3})?\d+(\.+\d+)?)deg/; assert_regexp_match(actual, rotationRegExp, 'Actual value is not a rotate3d vector') assert_regexp_match(expected, rotationRegExp, 'Expected value is not a rotate3d vector'); const actualRotationVector = actual.match(rotationRegExp)[1].split(' ').map(Number); const expectedRotationVector = expected.match(rotationRegExp)[1].split(' ').map(Number); assert_equals(actualRotationVector.length, expectedRotationVector.length, `dimension of the matrix: ${description}`); for (let i = 0; i < actualRotationVector.length; i++) { assert_approx_equals( actualRotationVector[i], expectedRotationVector[i], 0.0001, `expected ${expected} but got ${actual}: ${description}`); } } function assert_phase_at_time(animation, phase, currentTime) { animation.currentTime = currentTime; assert_phase(animation, phase); } function assert_phase(animation, phase) { const fillMode = animation.effect.getTiming().fill; const currentTime = animation.currentTime; if (phase === 'active') { // If the fill mode is 'none', then progress will only be non-null if we // are in the active phase. animation.effect.updateTiming({ fill: 'none' }); assert_not_equals(animation.effect.getComputedTiming().progress, null, 'Animation effect is in active phase when current time ' + `is ${currentTime}.`); } else { // The easiest way to distinguish between the 'before' phase and the 'after' // phase is to toggle the fill mode. For example, if the progress is null // when the fill mode is 'none' but non-null when the fill mode is // 'backwards' then we are in the before phase. animation.effect.updateTiming({ fill: 'none' }); assert_equals(animation.effect.getComputedTiming().progress, null, `Animation effect is in ${phase} phase when current time ` + `is ${currentTime} (progress is null with 'none' fill mode)`); animation.effect.updateTiming({ fill: phase === 'before' ? 'backwards' : 'forwards', }); assert_not_equals(animation.effect.getComputedTiming().progress, null, `Animation effect is in ${phase} phase when current ` + `time is ${currentTime} (progress is non-null with ` + `appropriate fill mode)`); } // Reset fill mode to avoid side-effects. animation.effect.updateTiming({ fill: fillMode }); } // Use with reftest-wait to wait until compositor commits are no longer deferred // before taking the screenshot. // crbug.com/1378671 async function waitForCompositorReady() { const animation = document.body.animate({ opacity: [ 1, 1 ] }, {duration: 1 }); return animation.finished; } async function takeScreenshotOnAnimationsReady() { await Promise.all(document.getAnimations().map(a => a.ready)); requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); }