| '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(async () => { | |
| await 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, except for progress-based timelines where | |
| // currentTime = 100% is still 'active'. | |
| animation.effect.updateTiming({ fill: 'none' }); | |
| if ('ScrollTimeline' in window && animation.timeline instanceof ScrollTimeline) { | |
| const isActive = animation.currentTime?.toString() == "100%" || | |
| animation.effect.getComputedTiming().progress != null; | |
| assert_true(isActive, | |
| 'Animation effect is in active phase when current time ' + | |
| `is ${currentTime}.`); | |
| } else { | |
| 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: [ 0, 1 ] }, {duration: 1 }); | |
| return animation.finished; | |
| } | |
| /** | |
| * Wrapper that takes a sequence of N animations and returns: | |
| * | |
| * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]); | |
| */ | |
| function waitForAllAnimations(animations) { | |
| return Promise.all(animations.map(animation => animation.ready)); | |
| } | |
| async function takeScreenshotOnAnimationsReady() { | |
| await waitForAllAnimations(document.getAnimations()); | |
| requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); | |
| } |