mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-05-05 06:32:30 +02:00
LibWeb: Account for non-shorthand sub-properties when serializing border
When parsing values in `process_a_keyframes_argument` we don't expand properties using `StyleComputer::for_each_property_expanding_shorthands` unlike most other places - this means that if we parse a `border` we end up with the `border`'s sub-properties (`border-width`, `border-style`, `border-color`) still in their unexpanded forms (`CSSKeywordValue`, `LengthStyleValue`, `StyleValueList`, etc) rather than `ShorthandStyleValue`s which causes a crash when serializing the `border` value in `KeyframeEffect::get_keyframes`. The proper fix here is to parse `border`'s sub-properties directly to `ShorthandStyleValue`s instead of relying on `StyleComputer::for_each_property_expanding_shorthand` to do this conversion for us but this may be a while off. This commit also imports the previously crashing tests.
This commit is contained in:
Notes:
github-actions[bot]
2025-07-16 05:50:48 +00:00
Author: https://github.com/Calme1709 Commit: https://github.com/LadybirdBrowser/ladybird/commit/a1c9b86ad37 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5467 Reviewed-by: https://github.com/AtkinsSJ ✅
351
Tests/LibWeb/Text/input/wpt-import/web-animations/testcommon.js
Normal file
351
Tests/LibWeb/Text/input/wpt-import/web-animations/testcommon.js
Normal file
@@ -0,0 +1,351 @@
|
||||
'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, 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;
|
||||
}
|
||||
|
||||
async function takeScreenshotOnAnimationsReady() {
|
||||
await Promise.all(document.getAnimations().map(a => a.ready));
|
||||
requestAnimationFrame(() => requestAnimationFrame(takeScreenshot));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user