Automatic update from web-platform-tests CSSTransitionDiscrete: Fix retarget and unset-start for unregistered custom properties (#58489) `ComputedValuesEqual` relies on `GetVariableValue`, which always returns null for unregistered custom properties. As a result, running-transition retarget checks silently skipped value changes. Additionally, transitions starting from an unset state produced a null start keyframe in `KeyframeValueFromComputedStyle`, causing an early return before the transition was scheduled. Extract `ComputedTransitionValuesEqual` to centralize the `GetVariableData` fallback logic for unregistered custom properties and reuse it in both retarget checks and before/after comparisons. Also substitute an empty `CSSUnparsedDeclarationValue` for missing start keyframes so discrete transitions from unset work correctly. Bug: 360159391 Change-Id: I686840f723e00a21028021a615ec67168c132a28 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7651277 Reviewed-by: Robert Flack <flackr@chromium.org> Commit-Queue: Peng Zhou <pengzhou@chromium.org> Cr-Commit-Position: refs/heads/main@{#1599471} Co-authored-by: Peng Zhou <pengzhou@chromium.org> -- wpt-commits: 15f1eab5d197f597142d5a1fe54dbe8749c3ef94 wpt-pr: 58489
369 lines
11 KiB
JavaScript
369 lines
11 KiB
JavaScript
//
|
|
// Simple Helper Functions For Testing CSS
|
|
//
|
|
|
|
(function(root) {
|
|
'use strict';
|
|
|
|
// serialize styles object and dump to dom
|
|
// appends <style id="dynamic-style"> to <head>
|
|
// setStyle("#some-selector", {"some-style" : "value"})
|
|
// setStyle({"#some-selector": {"some-style" : "value"}})
|
|
root.setStyle = function(selector, styles) {
|
|
var target = document.getElementById('dynamic-style');
|
|
if (!target) {
|
|
target = document.createElement('style');
|
|
target.id = 'dynamic-style';
|
|
target.type = "text/css";
|
|
document.getElementsByTagName('head')[0].appendChild(target);
|
|
}
|
|
|
|
var data = [];
|
|
// single selector/styles
|
|
if (typeof selector === 'string' && styles !== undefined) {
|
|
data = [selector, '{', serializeStyles(styles), '}'];
|
|
target.textContent = data.join("\n");
|
|
return;
|
|
}
|
|
// map of selector/styles
|
|
for (var key in selector) {
|
|
if (Object.prototype.hasOwnProperty.call(selector, key)) {
|
|
var _data = [key, '{', serializeStyles(selector[key]), '}'];
|
|
data.push(_data.join('\n'));
|
|
}
|
|
}
|
|
|
|
target.textContent = data.join("\n");
|
|
};
|
|
|
|
function serializeStyles(styles) {
|
|
var data = [];
|
|
for (var property in styles) {
|
|
if (Object.prototype.hasOwnProperty.call(styles, property)) {
|
|
var prefixedProperty = addVendorPrefix(property);
|
|
data.push(prefixedProperty + ":" + styles[property] + ";");
|
|
}
|
|
}
|
|
|
|
return data.join('\n');
|
|
}
|
|
|
|
|
|
// shorthand for computed style
|
|
root.computedStyle = function(element, property, pseudo) {
|
|
var prefixedProperty = addVendorPrefix(property);
|
|
return window
|
|
.getComputedStyle(element, pseudo || null)
|
|
.getPropertyValue(prefixedProperty);
|
|
};
|
|
|
|
// flush rendering buffer
|
|
root.reflow = function() {
|
|
document.body.offsetWidth;
|
|
};
|
|
|
|
// merge objects
|
|
root.extend = function(target /*, ..rest */) {
|
|
Array.prototype.slice.call(arguments, 1).forEach(function(obj) {
|
|
Object.keys(obj).forEach(function(key) {
|
|
target[key] = obj[key];
|
|
});
|
|
});
|
|
|
|
return target;
|
|
};
|
|
|
|
// dom fixture helper ("resetting dom test elements")
|
|
var _domFixture;
|
|
var _domFixtureSelector;
|
|
root.domFixture = function(selector) {
|
|
var fixture = document.querySelector(selector || _domFixtureSelector);
|
|
if (!fixture) {
|
|
throw new Error('fixture ' + (selector || _domFixtureSelector) + ' not found!');
|
|
}
|
|
if (!_domFixture && selector) {
|
|
// save a copy
|
|
_domFixture = fixture.cloneNode(true);
|
|
_domFixtureSelector = selector;
|
|
} else if (_domFixture) {
|
|
// restore the copy
|
|
var tmp = _domFixture.cloneNode(true);
|
|
fixture.parentNode.replaceChild(tmp, fixture);
|
|
} else {
|
|
throw new Error('domFixture must be initialized first!');
|
|
}
|
|
};
|
|
|
|
root.MS_PER_SEC = 1000;
|
|
|
|
/*
|
|
* The recommended minimum precision to use for time values.
|
|
*
|
|
* Based on Web Animations:
|
|
* https://w3c.github.io/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.
|
|
*/
|
|
root.assert_times_equal = function(actual, expected, description) {
|
|
assert_approx_equals(actual, expected, TIME_PRECISION, description);
|
|
};
|
|
|
|
/*
|
|
* Compare a time value based on its precision requirements with a fixed value.
|
|
*/
|
|
root.assert_time_equals_literal = (actual, expected, description) => {
|
|
assert_approx_equals(actual, expected, TIME_PRECISION, description);
|
|
};
|
|
|
|
/**
|
|
* Assert that CSSTransition event, |evt|, has the expected property values
|
|
* defined by |propertyName|, |elapsedTime|, and |pseudoElement|.
|
|
*/
|
|
root.assert_end_events_equal = function(evt, propertyName, elapsedTime,
|
|
pseudoElement = '') {
|
|
assert_equals(evt.propertyName, propertyName);
|
|
assert_times_equal(evt.elapsedTime, elapsedTime);
|
|
assert_equals(evt.pseudoElement, pseudoElement);
|
|
};
|
|
|
|
/**
|
|
* Assert that array of simultaneous CSSTransition events, |evts|, have the
|
|
* corresponding property names listed in |propertyNames|, and the expected
|
|
* |elapsedTimes| and |pseudoElement| members.
|
|
*
|
|
* |elapsedTimes| may be a single value if all events are expected to have the
|
|
* same elapsedTime, or an array parallel to |propertyNames|.
|
|
*/
|
|
root.assert_end_event_batch_equal = function(evts, propertyNames, elapsedTimes,
|
|
pseudoElement = '') {
|
|
assert_equals(
|
|
evts.length,
|
|
propertyNames.length,
|
|
'Test harness error: should have waited for the correct number of events'
|
|
);
|
|
assert_true(
|
|
typeof elapsedTimes === 'number' ||
|
|
(Array.isArray(elapsedTimes) &&
|
|
elapsedTimes.length === propertyNames.length),
|
|
'Test harness error: elapsedTimes must either be a number or an array of' +
|
|
' numbers with the same length as propertyNames'
|
|
);
|
|
|
|
if (typeof elapsedTimes === 'number') {
|
|
elapsedTimes = Array(propertyNames.length).fill(elapsedTimes);
|
|
}
|
|
const testPairs = propertyNames.map((propertyName, index) => ({
|
|
propertyName,
|
|
elapsedTime: elapsedTimes[index]
|
|
}));
|
|
|
|
const sortByPropertyName = (a, b) =>
|
|
a.propertyName.localeCompare(b.propertyName);
|
|
evts.sort(sortByPropertyName);
|
|
testPairs.sort(sortByPropertyName);
|
|
|
|
for (let evt of evts) {
|
|
const expected = testPairs.shift();
|
|
assert_end_events_equal(
|
|
evt,
|
|
expected.propertyName,
|
|
expected.elapsedTime,
|
|
pseudoElement
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends a div to the document body.
|
|
*
|
|
* @param t The testharness.js Test object. If provided, this will be used
|
|
* to register a cleanup callback to remove the div when the test
|
|
* finishes.
|
|
*
|
|
* @param attrs A dictionary object with attribute names and values to set on
|
|
* the div.
|
|
*/
|
|
root.addDiv = function(t, attrs) {
|
|
var div = document.createElement('div');
|
|
if (attrs) {
|
|
for (var attrName in attrs) {
|
|
div.setAttribute(attrName, attrs[attrName]);
|
|
}
|
|
}
|
|
document.body.appendChild(div);
|
|
if (t && typeof t.add_cleanup === 'function') {
|
|
t.add_cleanup(function() {
|
|
if (div.parentNode) {
|
|
div.remove();
|
|
}
|
|
});
|
|
}
|
|
return div;
|
|
};
|
|
|
|
/**
|
|
* Appends a style div to the document head.
|
|
*
|
|
* @param t The testharness.js Test object. If provided, this will be used
|
|
* to register a cleanup callback to remove the style element
|
|
* when the test finishes.
|
|
*
|
|
* @param rules A dictionary object with selector names and rules to set on
|
|
* the style sheet.
|
|
*/
|
|
root.addStyle = (t, rules) => {
|
|
const extraStyle = document.createElement('style');
|
|
document.head.appendChild(extraStyle);
|
|
if (rules) {
|
|
const sheet = extraStyle.sheet;
|
|
for (const selector in rules) {
|
|
sheet.insertRule(selector + '{' + rules[selector] + '}',
|
|
sheet.cssRules.length);
|
|
}
|
|
}
|
|
|
|
if (t && typeof t.add_cleanup === 'function') {
|
|
t.add_cleanup(() => {
|
|
extraStyle.remove();
|
|
});
|
|
}
|
|
return extraStyle;
|
|
};
|
|
|
|
/**
|
|
* Promise wrapper for requestAnimationFrame.
|
|
*/
|
|
root.waitForFrame = () => {
|
|
return new Promise(resolve => {
|
|
window.requestAnimationFrame(resolve);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns a Promise that is resolved after the given number of consecutive
|
|
* animation frames have occured (using requestAnimationFrame callbacks).
|
|
*
|
|
* @param frameCount The number of animation frames.
|
|
* @param onFrame An optional function to be processed in each animation frame.
|
|
*/
|
|
root.waitForAnimationFrames = (frameCount, onFrame) => {
|
|
const timeAtStart = document.timeline.currentTime;
|
|
return new Promise(resolve => {
|
|
function handleFrame() {
|
|
if (onFrame && typeof onFrame === 'function') {
|
|
onFrame();
|
|
}
|
|
if (timeAtStart != document.timeline.currentTime &&
|
|
--frameCount <= 0) {
|
|
resolve();
|
|
} else {
|
|
window.requestAnimationFrame(handleFrame); // wait another frame
|
|
}
|
|
}
|
|
window.requestAnimationFrame(handleFrame);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Wrapper that takes a sequence of N animations and returns:
|
|
*
|
|
* Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]);
|
|
*/
|
|
root.waitForAllAnimations = animations =>
|
|
Promise.all(animations.map(animation => animation.ready));
|
|
|
|
/**
|
|
* Utility that takes a Promise and a maximum number of frames to wait and
|
|
* returns a new Promise that behaves as follows:
|
|
*
|
|
* - If the provided Promise resolves _before_ the specified number of frames
|
|
* have passed, resolves with the result of the provided Promise.
|
|
* - If the provided Promise rejects _before_ the specified number of frames
|
|
* have passed, rejects with the error result of the provided Promise.
|
|
* - Otherwise, rejects with a 'Timed out' error message. If |message| is
|
|
* provided, it will be appended to the error message.
|
|
*/
|
|
root.frameTimeout = (promiseToWaitOn, framesToWait, message) => {
|
|
let framesRemaining = framesToWait;
|
|
let aborted = false;
|
|
|
|
const timeoutPromise = new Promise(function waitAFrame(resolve, reject) {
|
|
if (aborted) {
|
|
resolve();
|
|
return;
|
|
}
|
|
if (framesRemaining-- > 0) {
|
|
requestAnimationFrame(() => {
|
|
waitAFrame(resolve, reject);
|
|
});
|
|
return;
|
|
}
|
|
let errorMessage = 'Timed out waiting for Promise to resolve';
|
|
if (message) {
|
|
errorMessage += `: ${message}`;
|
|
}
|
|
reject(new Error(errorMessage));
|
|
});
|
|
|
|
const wrappedPromiseToWaitOn = promiseToWaitOn.then(result => {
|
|
aborted = true;
|
|
return result;
|
|
});
|
|
|
|
return Promise.race([timeoutPromise, wrappedPromiseToWaitOn]);
|
|
};
|
|
|
|
root.supportsStartingStyle = () => {
|
|
let sheet = new CSSStyleSheet();
|
|
sheet.replaceSync("@starting-style{}");
|
|
return sheet.cssRules.length == 1;
|
|
};
|
|
|
|
/**
|
|
* Waits for a 'transitionend' event to fire on the given element.
|
|
*
|
|
* @param element The DOM element to listen for the transitionend event on.
|
|
* @returns {Promise<void>} A promise that resolves when the transitionend event is fired.
|
|
*/
|
|
root.waitForTransitionEnd = function(element) {
|
|
return new Promise(resolve => {
|
|
element.addEventListener('transitionend', resolve, { once: true });
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Asserts that exactly one transition is running on |div| for |property|,
|
|
* with the expected start and end keyframe values.
|
|
*
|
|
* @param div The element to inspect.
|
|
* @param property The CSS property name (e.g. '--x').
|
|
* @param startValue Expected value of the start keyframe, or null to skip
|
|
* the check (useful when the start value is indeterminate,
|
|
* e.g. during a retargeted transition).
|
|
* @param endValue Expected value of the end keyframe.
|
|
* @returns The running Animation object.
|
|
*/
|
|
root.assertTransitionKeyframes = function(div, property, startValue, endValue) {
|
|
const animations = div.getAnimations();
|
|
assert_equals(animations.length, 1, 'exactly one transition is running');
|
|
assert_equals(animations[0].transitionProperty, property,
|
|
'transition is for ' + property);
|
|
|
|
const frames = animations[0].effect.getKeyframes();
|
|
assert_equals(frames.length, 2, 'Transition has 2 keyframes');
|
|
if (startValue !== null) {
|
|
assert_equals(frames[0][property].trim(), startValue,
|
|
'Start keyframe has the old value');
|
|
}
|
|
assert_equals(frames[1][property].trim(), endValue,
|
|
'End keyframe has the new value');
|
|
|
|
return animations[0];
|
|
};
|
|
|
|
})(window);
|