summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-animations/testcommon.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/web-animations/testcommon.js')
-rw-r--r--testing/web-platform/tests/web-animations/testcommon.js323
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;
+}