diff options
Diffstat (limited to 'testing/web-platform/tests/web-animations/testcommon.js')
-rw-r--r-- | testing/web-platform/tests/web-animations/testcommon.js | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-animations/testcommon.js b/testing/web-platform/tests/web-animations/testcommon.js new file mode 100644 index 0000000000..0b318714a5 --- /dev/null +++ b/testing/web-platform/tests/web-animations/testcommon.js @@ -0,0 +1,323 @@ +'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); + } + }()); + }); +} + + +// 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; + const fillMode = animation.effect.getTiming().fill; + + 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(target) { + const animation = + document.body.animate({ opacity: [ 1, 1 ] }, {duration: 1 }); + return animation.finished; +} |