LibWeb: Account for non-shorthand sub-properties when serializing border
Some checks are pending
CI / macOS, arm64, Sanitizer, Clang (push) Waiting to run
CI / Linux, x86_64, Fuzzers, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer, GNU (push) Waiting to run
CI / Linux, x86_64, Sanitizer, Clang (push) Waiting to run
Package the js repl as a binary artifact / Linux, arm64 (push) Waiting to run
Package the js repl as a binary artifact / macOS, arm64 (push) Waiting to run
Package the js repl as a binary artifact / Linux, x86_64 (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

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:
Callum Law 2025-07-16 09:46:04 +12:00 committed by Sam Atkins
commit a1c9b86ad3
Notes: github-actions[bot] 2025-07-16 05:50:48 +00:00
13 changed files with 2497 additions and 0 deletions

View file

@ -241,6 +241,11 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
case PropertyID::Border: { case PropertyID::Border: {
auto all_longhands_same_value = [](ValueComparingRefPtr<CSSStyleValue const> const& shorthand) -> bool { auto all_longhands_same_value = [](ValueComparingRefPtr<CSSStyleValue const> const& shorthand) -> bool {
VERIFY(shorthand); VERIFY(shorthand);
// FIXME: This can be removed once we parse border-width, border-style and border-color directly to ShorthandStyleValue
if (!shorthand->is_shorthand())
return !shorthand->is_value_list();
VERIFY(shorthand->is_shorthand()); VERIFY(shorthand->is_shorthand());
auto longhands = shorthand->as_shorthand().values(); auto longhands = shorthand->as_shorthand().values();

View file

@ -0,0 +1,159 @@
Harness status: OK
Found 153 tests
139 Pass
14 Fail
Pass Element.animate() creates an Animation object
Pass Element.animate() creates an Animation object in the relevant realm of the target element
Pass Element.animate() creates an Animation object with a KeyframeEffect
Pass Element.animate() creates an Animation object with a KeyframeEffect that is created in the relevant realm of the target element
Pass Element.animate() accepts empty keyframe lists (input: [])
Pass Element.animate() accepts empty keyframe lists (input: null)
Pass Element.animate() accepts empty keyframe lists (input: undefined)
Pass Element.animate() accepts a one property two value property-indexed keyframes specification
Pass Element.animate() accepts a one shorthand property two value property-indexed keyframes specification
Pass Element.animate() accepts a two property (one shorthand and one of its longhand components) two value property-indexed keyframes specification
Pass Element.animate() accepts a two property (one shorthand and one of its shorthand components) two value property-indexed keyframes specification
Pass Element.animate() accepts a two property two value property-indexed keyframes specification
Pass Element.animate() accepts a two property property-indexed keyframes specification with different numbers of values
Pass Element.animate() accepts a property-indexed keyframes specification with an invalid value
Pass Element.animate() accepts a one property two value property-indexed keyframes specification that needs to stringify its values
Pass Element.animate() accepts a property-indexed keyframes specification with a CSS variable reference
Pass Element.animate() accepts a property-indexed keyframes specification with a CSS variable reference in a shorthand property
Pass Element.animate() accepts a one property one value property-indexed keyframes specification
Pass Element.animate() accepts a one property one non-array value property-indexed keyframes specification
Pass Element.animate() accepts a one property two value property-indexed keyframes specification where the first value is invalid
Pass Element.animate() accepts a one property two value property-indexed keyframes specification where the second value is invalid
Fail Element.animate() accepts a property-indexed keyframes specification with a CSS variable as the property
Fail Element.animate() accepts a property-indexed keyframe with a single offset
Pass Element.animate() accepts a property-indexed keyframe with an array of offsets
Pass Element.animate() accepts a property-indexed keyframe with an array of offsets that is too short
Pass Element.animate() accepts a property-indexed keyframe with an array of offsets that is too long
Pass Element.animate() accepts a property-indexed keyframe with an empty array of offsets
Fail Element.animate() accepts a property-indexed keyframe with an array of offsets with an embedded null value
Fail Element.animate() accepts a property-indexed keyframe with an array of offsets with a trailing null value
Fail Element.animate() accepts a property-indexed keyframe with an array of offsets with leading and trailing null values
Fail Element.animate() accepts a property-indexed keyframe with an array of offsets with adjacent null values
Fail Element.animate() accepts a property-indexed keyframe with an array of offsets with all null values (and too many at that)
Pass Element.animate() accepts a property-indexed keyframe with a single null offset
Pass Element.animate() accepts a property-indexed keyframe with an array of offsets that is not strictly ascending in the unused part of the array
Pass Element.animate() accepts a property-indexed keyframe without any specified easing
Pass Element.animate() accepts a property-indexed keyframe with a single easing
Pass Element.animate() accepts a property-indexed keyframe with an array of easings
Pass Element.animate() accepts a property-indexed keyframe with an array of easings that is too short
Pass Element.animate() accepts a property-indexed keyframe with a single-element array of easings
Pass Element.animate() accepts a property-indexed keyframe with an empty array of easings
Pass Element.animate() accepts a property-indexed keyframe with an array of easings that is too long
Pass Element.animate() accepts a property-indexed keyframe with a single composite operation
Pass Element.animate() accepts a property-indexed keyframe with a composite array
Pass Element.animate() accepts a property-indexed keyframe with a composite array that is too short
Pass Element.animate() accepts a property-indexed keyframe with a composite array that is too long
Pass Element.animate() accepts a property-indexed keyframe with a single-element composite array
Pass Element.animate() accepts a one property one keyframe sequence
Pass Element.animate() accepts a one property two keyframe sequence
Pass Element.animate() accepts a two property two keyframe sequence
Pass Element.animate() accepts a one shorthand property two keyframe sequence
Pass Element.animate() accepts a two property (a shorthand and one of its component longhands) two keyframe sequence
Pass Element.animate() accepts a two property keyframe sequence where one property is missing from the first keyframe
Pass Element.animate() accepts a two property keyframe sequence where one property is missing from the last keyframe
Pass Element.animate() accepts a one property two keyframe sequence that needs to stringify its values
Pass Element.animate() accepts a keyframe sequence with a CSS variable reference
Pass Element.animate() accepts a keyframe sequence with a CSS variable reference in a shorthand property
Fail Element.animate() accepts a keyframe sequence with a CSS variable as its property
Pass Element.animate() accepts a keyframe sequence with duplicate values for a given interior offset
Pass Element.animate() accepts a keyframe sequence with duplicate values for offsets 0 and 1
Pass Element.animate() accepts a two property four keyframe sequence
Pass Element.animate() accepts a single keyframe sequence with omitted offset
Fail Element.animate() accepts a single keyframe sequence with null offset
Pass Element.animate() accepts a single keyframe sequence with string offset
Fail Element.animate() accepts a single keyframe sequence with a single calc() offset
Fail Element.animate() accepts a one property keyframe sequence with some omitted offsets
Fail Element.animate() accepts a one property keyframe sequence with some null offsets
Fail Element.animate() accepts a two property keyframe sequence with some omitted offsets
Pass Element.animate() accepts a one property keyframe sequence with all omitted offsets
Pass Element.animate() accepts a keyframe sequence with different easing values, but the same easing value for a given offset
Pass Element.animate() accepts a keyframe sequence with different composite values, but the same composite value for a given offset
Pass Element.animate() does not accept keyframes with an out-of-bounded positive offset
Pass Element.animate() does not accept keyframes with an out-of-bounded negative offset
Pass Element.animate() does not accept property-indexed keyframes not loosely sorted by offset
Pass Element.animate() does not accept property-indexed keyframes not loosely sorted by offset even though not all offsets are specified
Pass Element.animate() does not accept property-indexed keyframes with offsets out of range
Pass Element.animate() does not accept keyframes not loosely sorted by offset
Pass Element.animate() does not accept property-indexed keyframes with an invalid easing value
Pass Element.animate() does not accept property-indexed keyframes with an invalid easing value as one of the array values
Pass Element.animate() does not accept property-indexed keyframe with an invalid easing in the unused part of the array of easings
Pass Element.animate() does not accept empty property-indexed keyframe with an invalid easing
Pass Element.animate() does not accept empty property-indexed keyframe with an invalid easings array
Pass Element.animate() does not accept a keyframe sequence with an invalid easing value
Pass Element.animate() does not accept property-indexed keyframes with an invalid composite value
Pass Element.animate() does not accept property-indexed keyframes with an invalid composite value as one of the array values
Pass Element.animate() does not accept keyframes with an invalid composite value
Pass Element.animate() accepts a double as an options argument
Pass Element.animate() accepts a KeyframeAnimationOptions argument
Pass Element.animate() accepts an absent options argument
Pass Element.animate() does not accept invalid delay value: NaN
Pass Element.animate() does not accept invalid delay value: Infinity
Pass Element.animate() does not accept invalid delay value: -Infinity
Pass Element.animate() accepts a duration of 'auto' using a dictionary object
Pass Element.animate() does not accept invalid duration value: -1
Pass Element.animate() does not accept invalid duration value: NaN
Pass Element.animate() does not accept invalid duration value: -Infinity
Pass Element.animate() does not accept invalid duration value: "abc"
Pass Element.animate() does not accept invalid duration value: -1 using a dictionary object
Pass Element.animate() does not accept invalid duration value: NaN using a dictionary object
Pass Element.animate() does not accept invalid duration value: -Infinity using a dictionary object
Pass Element.animate() does not accept invalid duration value: "abc" using a dictionary object
Pass Element.animate() does not accept invalid duration value: "100" using a dictionary object
Pass Element.animate() does not accept invalid easing: ''
Pass Element.animate() does not accept invalid easing: '7'
Pass Element.animate() does not accept invalid easing: 'test'
Pass Element.animate() does not accept invalid easing: 'initial'
Pass Element.animate() does not accept invalid easing: 'inherit'
Pass Element.animate() does not accept invalid easing: 'unset'
Pass Element.animate() does not accept invalid easing: 'unrecognized'
Pass Element.animate() does not accept invalid easing: 'var(--x)'
Pass Element.animate() does not accept invalid easing: 'ease-in-out, ease-out'
Pass Element.animate() does not accept invalid easing: 'cubic-bezier(1.1, 0, 1, 1)'
Pass Element.animate() does not accept invalid easing: 'cubic-bezier(0, 0, 1.1, 1)'
Pass Element.animate() does not accept invalid easing: 'cubic-bezier(-0.1, 0, 1, 1)'
Pass Element.animate() does not accept invalid easing: 'cubic-bezier(0, 0, -0.1, 1)'
Pass Element.animate() does not accept invalid easing: 'cubic-bezier(0.1, 0, 4, 0.4)'
Pass Element.animate() does not accept invalid easing: 'steps(-1, start)'
Pass Element.animate() does not accept invalid easing: 'steps(0.1, start)'
Pass Element.animate() does not accept invalid easing: 'steps(3, nowhere)'
Pass Element.animate() does not accept invalid easing: 'steps(-3, end)'
Pass Element.animate() does not accept invalid easing: 'function (a){return a}'
Pass Element.animate() does not accept invalid easing: 'function (x){return x}'
Pass Element.animate() does not accept invalid easing: 'function(x, y){return 0.3}'
Pass Element.animate() does not accept invalid iterationStart value: -1
Pass Element.animate() does not accept invalid iterationStart value: NaN
Pass Element.animate() does not accept invalid iterationStart value: Infinity
Pass Element.animate() does not accept invalid iterationStart value: -Infinity
Pass Element.animate() does not accept invalid iterations value: -1
Pass Element.animate() does not accept invalid iterations value: -Infinity
Pass Element.animate() does not accept invalid iterations value: NaN
Pass Element.animate() correctly sets the id attribute when no id is specified
Pass Element.animate() correctly sets the id attribute
Pass Element.animate() correctly sets the Animation's timeline
Pass Element.animate() correctly sets the Animation's timeline when triggered on an element in a different document
Pass Element.animate() correctly sets the Animation's timeline with no timeline parameter in KeyframeAnimationOptions.
Pass Element.animate() correctly sets the Animation's timeline with undefined timeline in KeyframeAnimationOptions.
Pass Element.animate() correctly sets the Animation's timeline with null timeline in KeyframeAnimationOptions.
Pass Element.animate() correctly sets the Animation's timeline with DocumentTimeline in KeyframeAnimationOptions.
Pass Element.animate() calls play on the Animation
Pass Element.animate() does NOT trigger a style change event
Pass animate() with pseudoElement parameter creates an Animation object
Pass animate() with pseudoElement parameter without content creates an Animation object
Pass animate() with pseudoElement parameter creates an Animation object for ::marker
Pass animate() with pseudoElement parameter creates an Animation object for ::first-line
Pass animate() with pseudoElement an Animation object targeting the correct pseudo-element
Pass animate() with pseudoElement without content creates an Animation object targeting the correct pseudo-element
Pass animate() with pseudoElement an Animation object targeting the correct pseudo-element for ::marker
Pass animate() with pseudoElement an Animation object targeting the correct pseudo-element for ::first-line
Pass animate() with a non-null invalid pseudoElement '' throws a SyntaxError
Pass animate() with a non-null invalid pseudoElement 'before' throws a SyntaxError
Pass animate() with a non-null invalid pseudoElement ':abc' throws a SyntaxError
Pass animate() with a non-null invalid pseudoElement '::abc' throws a SyntaxError
Pass animate() with pseudoElement ::placeholder does not throw
Fail Finished fill animation doesn't replace animation on a different pseudoElement

View file

@ -0,0 +1,181 @@
Harness status: OK
Found 175 tests
120 Pass
55 Fail
Pass A KeyframeEffect can be constructed with no frames
Pass easing values are parsed correctly when passed to the KeyframeEffect constructor in KeyframeEffectOptions
Pass Invalid easing values are correctly rejected when passed to the KeyframeEffect constructor in KeyframeEffectOptions
Pass composite values are parsed correctly when passed to the KeyframeEffect constructor in property-indexed keyframes
Pass composite values are parsed correctly when passed to the KeyframeEffect constructor in regular keyframes
Pass composite value is auto if the composite operation specified on the keyframe effect is being used
Pass A KeyframeEffect can be constructed with a one property two value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a one property two value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a one shorthand property two value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a one shorthand property two value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a two property (one shorthand and one of its longhand components) two value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a two property (one shorthand and one of its longhand components) two value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a two property (one shorthand and one of its shorthand components) two value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a two property (one shorthand and one of its shorthand components) two value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a two property two value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a two property two value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a two property property-indexed keyframes specification with different numbers of values
Fail A KeyframeEffect constructed with a two property property-indexed keyframes specification with different numbers of values roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframes specification with an invalid value
Fail A KeyframeEffect constructed with a property-indexed keyframes specification with an invalid value roundtrips
Pass A KeyframeEffect can be constructed with a one property two value property-indexed keyframes specification that needs to stringify its values
Fail A KeyframeEffect constructed with a one property two value property-indexed keyframes specification that needs to stringify its values roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframes specification with a CSS variable reference
Fail A KeyframeEffect constructed with a property-indexed keyframes specification with a CSS variable reference roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframes specification with a CSS variable reference in a shorthand property
Fail A KeyframeEffect constructed with a property-indexed keyframes specification with a CSS variable reference in a shorthand property roundtrips
Pass A KeyframeEffect can be constructed with a one property one value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a one property one value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a one property one non-array value property-indexed keyframes specification
Fail A KeyframeEffect constructed with a one property one non-array value property-indexed keyframes specification roundtrips
Pass A KeyframeEffect can be constructed with a one property two value property-indexed keyframes specification where the first value is invalid
Fail A KeyframeEffect constructed with a one property two value property-indexed keyframes specification where the first value is invalid roundtrips
Pass A KeyframeEffect can be constructed with a one property two value property-indexed keyframes specification where the second value is invalid
Fail A KeyframeEffect constructed with a one property two value property-indexed keyframes specification where the second value is invalid roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframes specification with a CSS variable as the property
Pass A KeyframeEffect constructed with a property-indexed keyframes specification with a CSS variable as the property roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframe with a single offset
Fail A KeyframeEffect constructed with a property-indexed keyframe with a single offset roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets
Pass A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets that is too short
Fail A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets that is too short roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets that is too long
Pass A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets that is too long roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an empty array of offsets
Fail A KeyframeEffect constructed with a property-indexed keyframe with an empty array of offsets roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets with an embedded null value
Pass A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets with an embedded null value roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets with a trailing null value
Fail A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets with a trailing null value roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets with leading and trailing null values
Fail A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets with leading and trailing null values roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets with adjacent null values
Pass A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets with adjacent null values roundtrips
Fail A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets with all null values (and too many at that)
Pass A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets with all null values (and too many at that) roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a single null offset
Fail A KeyframeEffect constructed with a property-indexed keyframe with a single null offset roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of offsets that is not strictly ascending in the unused part of the array
Pass A KeyframeEffect constructed with a property-indexed keyframe with an array of offsets that is not strictly ascending in the unused part of the array roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe without any specified easing
Fail A KeyframeEffect constructed with a property-indexed keyframe without any specified easing roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a single easing
Fail A KeyframeEffect constructed with a property-indexed keyframe with a single easing roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of easings
Fail A KeyframeEffect constructed with a property-indexed keyframe with an array of easings roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of easings that is too short
Fail A KeyframeEffect constructed with a property-indexed keyframe with an array of easings that is too short roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a single-element array of easings
Fail A KeyframeEffect constructed with a property-indexed keyframe with a single-element array of easings roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an empty array of easings
Fail A KeyframeEffect constructed with a property-indexed keyframe with an empty array of easings roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with an array of easings that is too long
Fail A KeyframeEffect constructed with a property-indexed keyframe with an array of easings that is too long roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a single composite operation
Fail A KeyframeEffect constructed with a property-indexed keyframe with a single composite operation roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a composite array
Fail A KeyframeEffect constructed with a property-indexed keyframe with a composite array roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a composite array that is too short
Fail A KeyframeEffect constructed with a property-indexed keyframe with a composite array that is too short roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a composite array that is too long
Fail A KeyframeEffect constructed with a property-indexed keyframe with a composite array that is too long roundtrips
Pass A KeyframeEffect can be constructed with a property-indexed keyframe with a single-element composite array
Fail A KeyframeEffect constructed with a property-indexed keyframe with a single-element composite array roundtrips
Pass A KeyframeEffect can be constructed with a one property one keyframe sequence
Pass A KeyframeEffect constructed with a one property one keyframe sequence roundtrips
Pass A KeyframeEffect can be constructed with a one property two keyframe sequence
Pass A KeyframeEffect constructed with a one property two keyframe sequence roundtrips
Pass A KeyframeEffect can be constructed with a two property two keyframe sequence
Pass A KeyframeEffect constructed with a two property two keyframe sequence roundtrips
Pass A KeyframeEffect can be constructed with a one shorthand property two keyframe sequence
Pass A KeyframeEffect constructed with a one shorthand property two keyframe sequence roundtrips
Pass A KeyframeEffect can be constructed with a two property (a shorthand and one of its component longhands) two keyframe sequence
Pass A KeyframeEffect constructed with a two property (a shorthand and one of its component longhands) two keyframe sequence roundtrips
Pass A KeyframeEffect can be constructed with a two property keyframe sequence where one property is missing from the first keyframe
Pass A KeyframeEffect constructed with a two property keyframe sequence where one property is missing from the first keyframe roundtrips
Pass A KeyframeEffect can be constructed with a two property keyframe sequence where one property is missing from the last keyframe
Pass A KeyframeEffect constructed with a two property keyframe sequence where one property is missing from the last keyframe roundtrips
Pass A KeyframeEffect can be constructed with a one property two keyframe sequence that needs to stringify its values
Pass A KeyframeEffect constructed with a one property two keyframe sequence that needs to stringify its values roundtrips
Pass A KeyframeEffect can be constructed with a keyframe sequence with a CSS variable reference
Fail A KeyframeEffect constructed with a keyframe sequence with a CSS variable reference roundtrips
Pass A KeyframeEffect can be constructed with a keyframe sequence with a CSS variable reference in a shorthand property
Fail A KeyframeEffect constructed with a keyframe sequence with a CSS variable reference in a shorthand property roundtrips
Fail A KeyframeEffect can be constructed with a keyframe sequence with a CSS variable as its property
Fail A KeyframeEffect constructed with a keyframe sequence with a CSS variable as its property roundtrips
Pass A KeyframeEffect can be constructed with a keyframe sequence with duplicate values for a given interior offset
Pass A KeyframeEffect constructed with a keyframe sequence with duplicate values for a given interior offset roundtrips
Pass A KeyframeEffect can be constructed with a keyframe sequence with duplicate values for offsets 0 and 1
Pass A KeyframeEffect constructed with a keyframe sequence with duplicate values for offsets 0 and 1 roundtrips
Pass A KeyframeEffect can be constructed with a two property four keyframe sequence
Pass A KeyframeEffect constructed with a two property four keyframe sequence roundtrips
Pass A KeyframeEffect can be constructed with a single keyframe sequence with omitted offset
Fail A KeyframeEffect constructed with a single keyframe sequence with omitted offset roundtrips
Fail A KeyframeEffect can be constructed with a single keyframe sequence with null offset
Pass A KeyframeEffect constructed with a single keyframe sequence with null offset roundtrips
Pass A KeyframeEffect can be constructed with a single keyframe sequence with string offset
Pass A KeyframeEffect constructed with a single keyframe sequence with string offset roundtrips
Fail A KeyframeEffect can be constructed with a single keyframe sequence with a single calc() offset
Fail A KeyframeEffect constructed with a single keyframe sequence with a single calc() offset roundtrips
Fail A KeyframeEffect can be constructed with a one property keyframe sequence with some omitted offsets
Fail A KeyframeEffect constructed with a one property keyframe sequence with some omitted offsets roundtrips
Fail A KeyframeEffect can be constructed with a one property keyframe sequence with some null offsets
Fail A KeyframeEffect constructed with a one property keyframe sequence with some null offsets roundtrips
Fail A KeyframeEffect can be constructed with a two property keyframe sequence with some omitted offsets
Fail A KeyframeEffect constructed with a two property keyframe sequence with some omitted offsets roundtrips
Pass A KeyframeEffect can be constructed with a one property keyframe sequence with all omitted offsets
Fail A KeyframeEffect constructed with a one property keyframe sequence with all omitted offsets roundtrips
Pass A KeyframeEffect can be constructed with a keyframe sequence with different easing values, but the same easing value for a given offset
Pass A KeyframeEffect constructed with a keyframe sequence with different easing values, but the same easing value for a given offset roundtrips
Pass A KeyframeEffect can be constructed with a keyframe sequence with different composite values, but the same composite value for a given offset
Pass A KeyframeEffect constructed with a keyframe sequence with different composite values, but the same composite value for a given offset roundtrips
Pass KeyframeEffect constructor throws with keyframes with an out-of-bounded positive offset
Pass KeyframeEffect constructor throws with keyframes with an out-of-bounded negative offset
Pass KeyframeEffect constructor throws with property-indexed keyframes not loosely sorted by offset
Pass KeyframeEffect constructor throws with property-indexed keyframes not loosely sorted by offset even though not all offsets are specified
Pass KeyframeEffect constructor throws with property-indexed keyframes with offsets out of range
Pass KeyframeEffect constructor throws with keyframes not loosely sorted by offset
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid easing value
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid easing value as one of the array values
Pass KeyframeEffect constructor throws with property-indexed keyframe with an invalid easing in the unused part of the array of easings
Pass KeyframeEffect constructor throws with empty property-indexed keyframe with an invalid easing
Pass KeyframeEffect constructor throws with empty property-indexed keyframe with an invalid easings array
Pass KeyframeEffect constructor throws with a keyframe sequence with an invalid easing value
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid composite value
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid composite value as one of the array values
Pass KeyframeEffect constructor throws with keyframes with an invalid composite value
Fail A KeyframeEffect constructed without any KeyframeEffectOptions object
Pass A KeyframeEffect constructed by an empty KeyframeEffectOptions object
Pass A KeyframeEffect constructed by a normal KeyframeEffectOptions object
Pass A KeyframeEffect constructed by a double value
Pass A KeyframeEffect constructed by +Infinity
Pass A KeyframeEffect constructed by an Infinity duration
Pass A KeyframeEffect constructed by an auto duration
Pass A KeyframeEffect constructed by an Infinity iterations
Pass A KeyframeEffect constructed by an auto fill
Pass A KeyframeEffect constructed by a forwards fill
Pass Invalid KeyframeEffect option by -Infinity
Pass Invalid KeyframeEffect option by NaN
Pass Invalid KeyframeEffect option by a negative value
Pass Invalid KeyframeEffect option by a negative Infinity duration
Pass Invalid KeyframeEffect option by a NaN duration
Pass Invalid KeyframeEffect option by a negative duration
Pass Invalid KeyframeEffect option by a string duration
Pass Invalid KeyframeEffect option by a negative Infinity iterations
Pass Invalid KeyframeEffect option by a NaN iterations
Pass Invalid KeyframeEffect option by a negative iterations
Pass Invalid KeyframeEffect option by a blank easing
Pass Invalid KeyframeEffect option by an unrecognized easing
Pass Invalid KeyframeEffect option by an 'initial' easing
Pass Invalid KeyframeEffect option by an 'inherit' easing
Pass Invalid KeyframeEffect option by a variable easing
Pass Invalid KeyframeEffect option by a multi-value easing
Pass A KeyframeEffect constructed with null target
Pass KeyframeEffect constructor propagates exceptions generated by accessing the options object

View file

@ -0,0 +1,86 @@
Harness status: OK
Found 80 tests
66 Pass
14 Fail
Pass Keyframes can be replaced with an empty keyframe
Pass Keyframes can be replaced with a one property two value property-indexed keyframes specification
Pass Keyframes can be replaced with a one shorthand property two value property-indexed keyframes specification
Pass Keyframes can be replaced with a two property (one shorthand and one of its longhand components) two value property-indexed keyframes specification
Pass Keyframes can be replaced with a two property (one shorthand and one of its shorthand components) two value property-indexed keyframes specification
Pass Keyframes can be replaced with a two property two value property-indexed keyframes specification
Pass Keyframes can be replaced with a two property property-indexed keyframes specification with different numbers of values
Pass Keyframes can be replaced with a property-indexed keyframes specification with an invalid value
Pass Keyframes can be replaced with a one property two value property-indexed keyframes specification that needs to stringify its values
Pass Keyframes can be replaced with a property-indexed keyframes specification with a CSS variable reference
Pass Keyframes can be replaced with a property-indexed keyframes specification with a CSS variable reference in a shorthand property
Pass Keyframes can be replaced with a one property one value property-indexed keyframes specification
Pass Keyframes can be replaced with a one property one non-array value property-indexed keyframes specification
Pass Keyframes can be replaced with a one property two value property-indexed keyframes specification where the first value is invalid
Pass Keyframes can be replaced with a one property two value property-indexed keyframes specification where the second value is invalid
Fail Keyframes can be replaced with a property-indexed keyframes specification with a CSS variable as the property
Fail Keyframes can be replaced with a property-indexed keyframe with a single offset
Pass Keyframes can be replaced with a property-indexed keyframe with an array of offsets
Pass Keyframes can be replaced with a property-indexed keyframe with an array of offsets that is too short
Pass Keyframes can be replaced with a property-indexed keyframe with an array of offsets that is too long
Pass Keyframes can be replaced with a property-indexed keyframe with an empty array of offsets
Fail Keyframes can be replaced with a property-indexed keyframe with an array of offsets with an embedded null value
Fail Keyframes can be replaced with a property-indexed keyframe with an array of offsets with a trailing null value
Fail Keyframes can be replaced with a property-indexed keyframe with an array of offsets with leading and trailing null values
Fail Keyframes can be replaced with a property-indexed keyframe with an array of offsets with adjacent null values
Fail Keyframes can be replaced with a property-indexed keyframe with an array of offsets with all null values (and too many at that)
Pass Keyframes can be replaced with a property-indexed keyframe with a single null offset
Pass Keyframes can be replaced with a property-indexed keyframe with an array of offsets that is not strictly ascending in the unused part of the array
Pass Keyframes can be replaced with a property-indexed keyframe without any specified easing
Pass Keyframes can be replaced with a property-indexed keyframe with a single easing
Pass Keyframes can be replaced with a property-indexed keyframe with an array of easings
Pass Keyframes can be replaced with a property-indexed keyframe with an array of easings that is too short
Pass Keyframes can be replaced with a property-indexed keyframe with a single-element array of easings
Pass Keyframes can be replaced with a property-indexed keyframe with an empty array of easings
Pass Keyframes can be replaced with a property-indexed keyframe with an array of easings that is too long
Pass Keyframes can be replaced with a property-indexed keyframe with a single composite operation
Pass Keyframes can be replaced with a property-indexed keyframe with a composite array
Pass Keyframes can be replaced with a property-indexed keyframe with a composite array that is too short
Pass Keyframes can be replaced with a property-indexed keyframe with a composite array that is too long
Pass Keyframes can be replaced with a property-indexed keyframe with a single-element composite array
Pass Keyframes can be replaced with a one property one keyframe sequence
Pass Keyframes can be replaced with a one property two keyframe sequence
Pass Keyframes can be replaced with a two property two keyframe sequence
Pass Keyframes can be replaced with a one shorthand property two keyframe sequence
Pass Keyframes can be replaced with a two property (a shorthand and one of its component longhands) two keyframe sequence
Pass Keyframes can be replaced with a two property keyframe sequence where one property is missing from the first keyframe
Pass Keyframes can be replaced with a two property keyframe sequence where one property is missing from the last keyframe
Pass Keyframes can be replaced with a one property two keyframe sequence that needs to stringify its values
Pass Keyframes can be replaced with a keyframe sequence with a CSS variable reference
Pass Keyframes can be replaced with a keyframe sequence with a CSS variable reference in a shorthand property
Fail Keyframes can be replaced with a keyframe sequence with a CSS variable as its property
Pass Keyframes can be replaced with a keyframe sequence with duplicate values for a given interior offset
Pass Keyframes can be replaced with a keyframe sequence with duplicate values for offsets 0 and 1
Pass Keyframes can be replaced with a two property four keyframe sequence
Pass Keyframes can be replaced with a single keyframe sequence with omitted offset
Fail Keyframes can be replaced with a single keyframe sequence with null offset
Pass Keyframes can be replaced with a single keyframe sequence with string offset
Fail Keyframes can be replaced with a single keyframe sequence with a single calc() offset
Fail Keyframes can be replaced with a one property keyframe sequence with some omitted offsets
Fail Keyframes can be replaced with a one property keyframe sequence with some null offsets
Fail Keyframes can be replaced with a two property keyframe sequence with some omitted offsets
Pass Keyframes can be replaced with a one property keyframe sequence with all omitted offsets
Pass Keyframes can be replaced with a keyframe sequence with different easing values, but the same easing value for a given offset
Pass Keyframes can be replaced with a keyframe sequence with different composite values, but the same composite value for a given offset
Pass KeyframeEffect constructor throws with keyframes with an out-of-bounded positive offset
Pass KeyframeEffect constructor throws with keyframes with an out-of-bounded negative offset
Pass KeyframeEffect constructor throws with property-indexed keyframes not loosely sorted by offset
Pass KeyframeEffect constructor throws with property-indexed keyframes not loosely sorted by offset even though not all offsets are specified
Pass KeyframeEffect constructor throws with property-indexed keyframes with offsets out of range
Pass KeyframeEffect constructor throws with keyframes not loosely sorted by offset
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid easing value
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid easing value as one of the array values
Pass KeyframeEffect constructor throws with property-indexed keyframe with an invalid easing in the unused part of the array of easings
Pass KeyframeEffect constructor throws with empty property-indexed keyframe with an invalid easing
Pass KeyframeEffect constructor throws with empty property-indexed keyframe with an invalid easings array
Pass KeyframeEffect constructor throws with a keyframe sequence with an invalid easing value
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid composite value
Pass KeyframeEffect constructor throws with property-indexed keyframes with an invalid composite value as one of the array values
Pass KeyframeEffect constructor throws with keyframes with an invalid composite value
Fail Changes made via setKeyframes should be immediately visible in style

View file

@ -0,0 +1,363 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Animatable.animate</title>
<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animatable-animate">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/easing-tests.js"></script>
<script src="../../resources/keyframe-utils.js"></script>
<script src="../../resources/keyframe-tests.js"></script>
<script src="../../resources/timing-utils.js"></script>
<script src="../../resources/timing-tests.js"></script>
<style>
.pseudo::before {content: '';}
.pseudo::after {content: '';}
.pseudo::marker {content: '';}
</style>
<body>
<div id="log"></div>
<iframe width="10" height="10" id="iframe"></iframe>
<script>
'use strict';
// Tests on Element
test(t => {
const anim = createDiv(t).animate(null);
assert_class_string(anim, 'Animation', 'Returned object is an Animation');
}, 'Element.animate() creates an Animation object');
test(t => {
const iframe = window.frames[0];
const div = createDiv(t, iframe.document);
const anim = Element.prototype.animate.call(div, null);
assert_equals(Object.getPrototypeOf(anim), iframe.Animation.prototype,
'The prototype of the created Animation is that defined on'
+ ' the relevant global for the target element');
assert_not_equals(Object.getPrototypeOf(anim), Animation.prototype,
'The prototype of the created Animation is NOT that of'
+ ' the current global');
}, 'Element.animate() creates an Animation object in the relevant realm of'
+ ' the target element');
test(t => {
const div = createDiv(t);
const anim = Element.prototype.animate.call(div, null);
assert_class_string(anim.effect, 'KeyframeEffect',
'Returned Animation has a KeyframeEffect');
}, 'Element.animate() creates an Animation object with a KeyframeEffect');
test(t => {
const iframe = window.frames[0];
const div = createDiv(t, iframe.document);
const anim = Element.prototype.animate.call(div, null);
assert_equals(Object.getPrototypeOf(anim.effect),
iframe.KeyframeEffect.prototype,
'The prototype of the created KeyframeEffect is that defined on'
+ ' the relevant global for the target element');
assert_not_equals(Object.getPrototypeOf(anim.effect),
KeyframeEffect.prototype,
'The prototype of the created KeyframeEffect is NOT that of'
+ ' the current global');
}, 'Element.animate() creates an Animation object with a KeyframeEffect'
+ ' that is created in the relevant realm of the target element');
for (const subtest of gEmptyKeyframeListTests) {
test(t => {
const anim = createDiv(t).animate(subtest, 2000);
assert_not_equals(anim, null);
}, 'Element.animate() accepts empty keyframe lists ' +
`(input: ${JSON.stringify(subtest)})`);
}
for (const subtest of gKeyframesTests) {
test(t => {
const anim = createDiv(t).animate(subtest.input, 2000);
assert_frame_lists_equal(anim.effect.getKeyframes(), subtest.output);
}, `Element.animate() accepts ${subtest.desc}`);
}
for (const subtest of gInvalidKeyframesTests) {
test(t => {
const div = createDiv(t);
assert_throws_js(TypeError, () => {
div.animate(subtest.input, 2000);
});
}, `Element.animate() does not accept ${subtest.desc}`);
}
test(t => {
const anim = createDiv(t).animate(null, 2000);
assert_equals(anim.effect.getTiming().duration, 2000);
assert_default_timing_except(anim.effect, ['duration']);
}, 'Element.animate() accepts a double as an options argument');
test(t => {
const anim = createDiv(t).animate(null,
{ duration: Infinity, fill: 'forwards' });
assert_equals(anim.effect.getTiming().duration, Infinity);
assert_equals(anim.effect.getTiming().fill, 'forwards');
assert_default_timing_except(anim.effect, ['duration', 'fill']);
}, 'Element.animate() accepts a KeyframeAnimationOptions argument');
test(t => {
const anim = createDiv(t).animate(null);
assert_default_timing_except(anim.effect, []);
}, 'Element.animate() accepts an absent options argument');
for (const invalid of gBadDelayValues) {
test(t => {
assert_throws_js(TypeError, () => {
createDiv(t).animate(null, { delay: invalid });
});
}, `Element.animate() does not accept invalid delay value: ${invalid}`);
}
test(t => {
const anim = createDiv(t).animate(null, { duration: 'auto' });
assert_equals(anim.effect.getTiming().duration, 'auto', 'set duration \'auto\'');
assert_equals(anim.effect.getComputedTiming().duration, 0,
'getComputedTiming() after set duration \'auto\'');
}, 'Element.animate() accepts a duration of \'auto\' using a dictionary'
+ ' object');
for (const invalid of gBadDurationValues) {
if (typeof invalid === 'string' && !isNaN(parseFloat(invalid))) {
continue;
}
test(t => {
assert_throws_js(TypeError, () => {
createDiv(t).animate(null, invalid);
});
}, 'Element.animate() does not accept invalid duration value: '
+ (typeof invalid === 'string' ? `"${invalid}"` : invalid));
}
for (const invalid of gBadDurationValues) {
test(t => {
assert_throws_js(TypeError, () => {
createDiv(t).animate(null, { duration: invalid });
});
}, 'Element.animate() does not accept invalid duration value: '
+ (typeof invalid === 'string' ? `"${invalid}"` : invalid)
+ ' using a dictionary object');
}
for (const invalidEasing of gInvalidEasings) {
test(t => {
assert_throws_js(TypeError, () => {
createDiv(t).animate({ easing: invalidEasing }, 2000);
});
}, `Element.animate() does not accept invalid easing: '${invalidEasing}'`);
}
for (const invalid of gBadIterationStartValues) {
test(t => {
assert_throws_js(TypeError, () => {
createDiv(t).animate(null, { iterationStart: invalid });
});
}, 'Element.animate() does not accept invalid iterationStart value: ' +
invalid);
}
for (const invalid of gBadIterationsValues) {
test(t => {
assert_throws_js(TypeError, () => {
createDiv(t).animate(null, { iterations: invalid });
});
}, 'Element.animate() does not accept invalid iterations value: ' +
invalid);
}
test(t => {
const anim = createDiv(t).animate(null, 2000);
assert_equals(anim.id, '');
}, 'Element.animate() correctly sets the id attribute when no id is specified');
test(t => {
const anim = createDiv(t).animate(null, { id: 'test' });
assert_equals(anim.id, 'test');
}, 'Element.animate() correctly sets the id attribute');
test(t => {
const anim = createDiv(t).animate(null, 2000);
assert_equals(anim.timeline, document.timeline);
}, 'Element.animate() correctly sets the Animation\'s timeline');
async_test(t => {
const iframe = document.createElement('iframe');
iframe.width = 10;
iframe.height = 10;
iframe.addEventListener('load', t.step_func(() => {
const div = createDiv(t, iframe.contentDocument);
const anim = div.animate(null, 2000);
assert_equals(anim.timeline, iframe.contentDocument.timeline);
iframe.remove();
t.done();
}));
document.body.appendChild(iframe);
}, 'Element.animate() correctly sets the Animation\'s timeline when ' +
'triggered on an element in a different document');
for (const subtest of gAnimationTimelineTests) {
test(t => {
const anim = createDiv(t).animate(null, { timeline: subtest.timeline });
assert_not_equals(anim, null,
'An animation sohuld be created');
assert_equals(anim.timeline, subtest.expectedTimeline,
'Animation timeline should be '+
subtest.expectedTimelineDescription);
}, 'Element.animate() correctly sets the Animation\'s timeline '
+ subtest.description + ' in KeyframeAnimationOptions.');
}
test(t => {
const anim = createDiv(t).animate(null, 2000);
assert_equals(anim.playState, 'running');
}, 'Element.animate() calls play on the Animation');
promise_test(async t => {
const div = createDiv(t);
let gotTransition = false;
div.addEventListener('transitionrun', () => {
gotTransition = true;
});
// Setup transition start point.
div.style.transition = 'opacity 100s';
getComputedStyle(div).opacity;
// Update specified style but don't flush style.
div.style.opacity = '0.5';
// Trigger a new animation at the same time.
const anim = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
// If Element.animate() produces a style change event it will have triggered
// a transition.
//
// If it does NOT produce a style change event, the animation will override
// the before-change style and after-change style such that a transition is
// never triggered.
// Wait for the animation to start and then for one more animation
// frame to give the transitionrun event a chance to be dispatched.
await anim.ready;
await waitForAnimationFrames(1);
assert_false(gotTransition, 'A transition should NOT have been triggered');
}, 'Element.animate() does NOT trigger a style change event');
// Tests on pseudo-elements
// Some tests occur twice (on pseudo-elements with and without content)
// in order to test both code paths for tree-abiding pseudo-elements in blink.
test(t => {
const div = createDiv(t);
div.classList.add('pseudo');
const anim = div.animate(null, {pseudoElement: '::before'});
assert_class_string(anim, 'Animation', 'The returned object is an Animation');
}, 'animate() with pseudoElement parameter creates an Animation object');
test(t => {
const div = createDiv(t);
const anim = div.animate(null, {pseudoElement: '::before'});
assert_class_string(anim, 'Animation', 'The returned object is an Animation');
}, 'animate() with pseudoElement parameter without content creates an Animation object');
test(t => {
const div = createDiv(t);
div.classList.add('pseudo');
div.style.display = 'list-item';
const anim = div.animate(null, {pseudoElement: '::marker'});
assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::marker');
}, 'animate() with pseudoElement parameter creates an Animation object for ::marker');
test(t => {
const div = createDiv(t);
div.classList.add('pseudo');
div.textContent = 'foo';
const anim = div.animate(null, {pseudoElement: '::first-line'});
assert_class_string(anim, 'Animation', 'The returned object is an Animation for ::first-line');
}, 'animate() with pseudoElement parameter creates an Animation object for ::first-line');
test(t => {
const div = createDiv(t);
div.classList.add('pseudo');
const anim = div.animate(null, {pseudoElement: '::before'});
assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
assert_equals(anim.effect.pseudoElement, '::before',
'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement an Animation object targeting ' +
'the correct pseudo-element');
test(t => {
const div = createDiv(t);
const anim = div.animate(null, {pseudoElement: '::before'});
assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
assert_equals(anim.effect.pseudoElement, '::before',
'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement without content creates an Animation object targeting ' +
'the correct pseudo-element');
test(t => {
const div = createDiv(t);
div.classList.add('pseudo');
div.style.display = 'list-item';
const anim = div.animate(null, {pseudoElement: '::marker'});
assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
assert_equals(anim.effect.pseudoElement, '::marker',
'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement an Animation object targeting ' +
'the correct pseudo-element for ::marker');
test(t => {
const div = createDiv(t);
div.classList.add('pseudo');
div.textContent = 'foo';
const anim = div.animate(null, {pseudoElement: '::first-line'});
assert_equals(anim.effect.target, div, 'The returned element has the correct target element');
assert_equals(anim.effect.pseudoElement, '::first-line',
'The returned Animation targets the correct selector');
}, 'animate() with pseudoElement an Animation object targeting ' +
'the correct pseudo-element for ::first-line');
for (const pseudo of [
'',
'before',
':abc',
'::abc',
]) {
test(t => {
const div = createDiv(t);
assert_throws_dom("SyntaxError", () => {
div.animate(null, {pseudoElement: pseudo});
});
}, `animate() with a non-null invalid pseudoElement '${pseudo}' throws a ` +
`SyntaxError`);
}
test(t => {
const div = createDiv(t);
div.animate(null, { pseudoElement: '::placeHOLDER' });
}, `animate() with pseudoElement ::placeholder does not throw`);
promise_test(async t => {
const div = createDiv(t);
div.classList.add('pseudo');
let animBefore = div.animate({opacity: [1, 0]}, {duration: 1, pseudoElement: '::before', fill: 'both'});
let animAfter = div.animate({opacity: [1, 0]}, {duration: 1, pseudoElement: '::after', fill: 'both'});
await animBefore.finished;
await animAfter.finished;
// The animation on ::before should not be replaced as it targets a different
// pseudo-element.
assert_equals(animBefore.replaceState, 'active');
assert_equals(animAfter.replaceState, 'active');
}, 'Finished fill animation doesn\'t replace animation on a different pseudoElement');
</script>
</body>

View file

@ -0,0 +1,195 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>KeyframeEffect constructor</title>
<link rel="help"
href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-keyframeeffect">
<link rel="help"
href="https://drafts.csswg.org/web-animations/#dom-keyframeeffectreadonly-keyframeeffectreadonly">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/easing-tests.js"></script>
<script src="../../resources/keyframe-utils.js"></script>
<script src="../../resources/keyframe-tests.js"></script>
<body>
<div id="log"></div>
<div id="target"></div>
<script>
'use strict';
const target = document.getElementById('target');
test(t => {
for (const frames of gEmptyKeyframeListTests) {
assert_equals(new KeyframeEffect(target, frames).getKeyframes().length,
0, `number of frames for ${JSON.stringify(frames)}`);
}
}, 'A KeyframeEffect can be constructed with no frames');
test(t => {
for (const subtest of gEasingParsingTests) {
const easing = subtest[0];
const expected = subtest[1];
const effect = new KeyframeEffect(target, {
left: ['10px', '20px']
}, { easing: easing });
assert_equals(effect.getTiming().easing, expected,
`resulting easing for '${easing}'`);
}
}, 'easing values are parsed correctly when passed to the ' +
'KeyframeEffect constructor in KeyframeEffectOptions');
test(t => {
for (const invalidEasing of gInvalidEasings) {
assert_throws_js(TypeError, () => {
new KeyframeEffect(target, null, { easing: invalidEasing });
}, `TypeError is thrown for easing '${invalidEasing}'`);
}
}, 'Invalid easing values are correctly rejected when passed to the ' +
'KeyframeEffect constructor in KeyframeEffectOptions');
test(t => {
const getKeyframe =
composite => ({ left: [ '10px', '20px' ], composite: composite });
for (const composite of gGoodKeyframeCompositeValueTests) {
const effect = new KeyframeEffect(target, getKeyframe(composite));
assert_equals(effect.getKeyframes()[0].composite, composite,
`resulting composite for '${composite}'`);
}
for (const composite of gBadKeyframeCompositeValueTests) {
assert_throws_js(TypeError, () => {
new KeyframeEffect(target, getKeyframe(composite));
});
}
}, 'composite values are parsed correctly when passed to the ' +
'KeyframeEffect constructor in property-indexed keyframes');
test(t => {
const getKeyframes = composite =>
[
{ offset: 0, left: '10px', composite: composite },
{ offset: 1, left: '20px' }
];
for (const composite of gGoodKeyframeCompositeValueTests) {
const effect = new KeyframeEffect(target, getKeyframes(composite));
assert_equals(effect.getKeyframes()[0].composite, composite,
`resulting composite for '${composite}'`);
}
for (const composite of gBadKeyframeCompositeValueTests) {
assert_throws_js(TypeError, () => {
new KeyframeEffect(target, getKeyframes(composite));
});
}
}, 'composite values are parsed correctly when passed to the ' +
'KeyframeEffect constructor in regular keyframes');
test(t => {
for (const composite of gGoodOptionsCompositeValueTests) {
const effect = new KeyframeEffect(target, {
left: ['10px', '20px']
}, { composite });
assert_equals(effect.getKeyframes()[0].composite, 'auto',
`resulting composite for '${composite}'`);
}
for (const composite of gBadOptionsCompositeValueTests) {
assert_throws_js(TypeError, () => {
new KeyframeEffect(target, {
left: ['10px', '20px']
}, { composite: composite });
});
}
}, 'composite value is auto if the composite operation specified on the ' +
'keyframe effect is being used');
for (const subtest of gKeyframesTests) {
test(t => {
const effect = new KeyframeEffect(target, subtest.input);
assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
}, `A KeyframeEffect can be constructed with ${subtest.desc}`);
test(t => {
const effect = new KeyframeEffect(target, subtest.input);
const secondEffect = new KeyframeEffect(target, effect.getKeyframes());
assert_frame_lists_equal(secondEffect.getKeyframes(),
effect.getKeyframes());
}, `A KeyframeEffect constructed with ${subtest.desc} roundtrips`);
}
for (const subtest of gInvalidKeyframesTests) {
test(t => {
assert_throws_js(TypeError, () => {
new KeyframeEffect(target, subtest.input);
});
}, `KeyframeEffect constructor throws with ${subtest.desc}`);
}
test(t => {
const effect = new KeyframeEffect(target, { left: ['10px', '20px'] });
const timing = effect.getTiming();
assert_equals(timing.delay, 0, 'default delay');
assert_equals(timing.endDelay, 0, 'default endDelay');
assert_equals(timing.fill, 'auto', 'default fill');
assert_equals(timing.iterations, 1.0, 'default iterations');
assert_equals(timing.iterationStart, 0.0, 'default iterationStart');
assert_equals(timing.duration, 'auto', 'default duration');
assert_equals(timing.direction, 'normal', 'default direction');
assert_equals(timing.easing, 'linear', 'default easing');
assert_equals(effect.composite, 'replace', 'default composite');
assert_equals(effect.iterationComposite, 'replace',
'default iterationComposite');
}, 'A KeyframeEffect constructed without any KeyframeEffectOptions object');
for (const subtest of gKeyframeEffectOptionTests) {
test(t => {
const effect = new KeyframeEffect(target, { left: ['10px', '20px'] },
subtest.input);
// Helper function to provide default expected values when the test does
// not supply them.
const expected = (field, defaultValue) => {
return field in subtest.expected ? subtest.expected[field] : defaultValue;
};
const timing = effect.getTiming();
assert_equals(timing.delay, expected('delay', 0),
'timing delay');
assert_equals(timing.fill, expected('fill', 'auto'),
'timing fill');
assert_equals(timing.iterations, expected('iterations', 1),
'timing iterations');
assert_equals(timing.duration, expected('duration', 'auto'),
'timing duration');
assert_equals(timing.direction, expected('direction', 'normal'),
'timing direction');
}, `A KeyframeEffect constructed by ${subtest.desc}`);
}
for (const subtest of gInvalidKeyframeEffectOptionTests) {
test(t => {
assert_throws_js(TypeError, () => {
new KeyframeEffect(target, { left: ['10px', '20px'] }, subtest.input);
});
}, `Invalid KeyframeEffect option by ${subtest.desc}`);
}
test(t => {
const effect = new KeyframeEffect(null, { left: ['10px', '20px'] },
{ duration: 100 * MS_PER_SEC,
fill: 'forwards' });
assert_equals(effect.target, null,
'Effect created with null target has correct target');
}, 'A KeyframeEffect constructed with null target');
test(t => {
const test_error = { name: 'test' };
assert_throws_exactly(test_error, () => {
new KeyframeEffect(target, { get left() { throw test_error }})
});
}, 'KeyframeEffect constructor propagates exceptions generated by accessing'
+ ' the options object');
</script>
</body>

View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>KeyframeEffect.setKeyframes</title>
<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-keyframeeffect-setkeyframes">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/keyframe-utils.js"></script>
<script src="../../resources/keyframe-tests.js"></script>
<body>
<div id="log"></div>
<div id="target"></div>
<script>
'use strict';
const target = document.getElementById('target');
test(t => {
for (const frame of gEmptyKeyframeListTests) {
const effect = new KeyframeEffect(target, {});
effect.setKeyframes(frame);
assert_frame_lists_equal(effect.getKeyframes(), []);
}
}, 'Keyframes can be replaced with an empty keyframe');
for (const subtest of gKeyframesTests) {
test(t => {
const effect = new KeyframeEffect(target, {});
effect.setKeyframes(subtest.input);
assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
}, `Keyframes can be replaced with ${subtest.desc}`);
}
for (const subtest of gInvalidKeyframesTests) {
test(t => {
const effect = new KeyframeEffect(target, {});
assert_throws_js(TypeError, () => {
effect.setKeyframes(subtest.input);
});
}, `KeyframeEffect constructor throws with ${subtest.desc}`);
}
test(t => {
const frames1 = [ { left: '100px' }, { left: '200px' } ];
const frames2 = [ { left: '200px' }, { left: '300px' } ];
const animation = target.animate(frames1, 1000);
animation.currentTime = 500;
assert_equals(getComputedStyle(target).left, "150px");
animation.effect.setKeyframes(frames2);
assert_equals(getComputedStyle(target).left, "250px");
}, 'Changes made via setKeyframes should be immediately visible in style');
</script>
</body>

View file

@ -0,0 +1,121 @@
'use strict';
const gEasingTests = [
{
desc: 'step-start function',
easing: 'step-start',
easingFunction: stepStart(1),
serialization: 'steps(1, start)'
},
{
desc: 'steps(1, start) function',
easing: 'steps(1, start)',
easingFunction: stepStart(1)
},
{
desc: 'steps(2, start) function',
easing: 'steps(2, start)',
easingFunction: stepStart(2)
},
{
desc: 'step-end function',
easing: 'step-end',
easingFunction: stepEnd(1),
serialization: 'steps(1)'
},
{
desc: 'steps(1) function',
easing: 'steps(1)',
easingFunction: stepEnd(1)
},
{
desc: 'steps(1, end) function',
easing: 'steps(1, end)',
easingFunction: stepEnd(1),
serialization: 'steps(1)'
},
{
desc: 'steps(2, end) function',
easing: 'steps(2, end)',
easingFunction: stepEnd(2),
serialization: 'steps(2)'
},
{
desc: 'linear function',
easing: 'linear', // cubic-bezier(0, 0, 1.0, 1.0)
easingFunction: cubicBezier(0, 0, 1.0, 1.0)
},
{
desc: 'ease function',
easing: 'ease', // cubic-bezier(0.25, 0.1, 0.25, 1.0)
easingFunction: cubicBezier(0.25, 0.1, 0.25, 1.0)
},
{
desc: 'ease-in function',
easing: 'ease-in', // cubic-bezier(0.42, 0, 1.0, 1.0)
easingFunction: cubicBezier(0.42, 0, 1.0, 1.0)
},
{
desc: 'ease-in-out function',
easing: 'ease-in-out', // cubic-bezier(0.42, 0, 0.58, 1.0)
easingFunction: cubicBezier(0.42, 0, 0.58, 1.0)
},
{
desc: 'ease-out function',
easing: 'ease-out', // cubic-bezier(0, 0, 0.58, 1.0)
easingFunction: cubicBezier(0, 0, 0.58, 1.0)
},
{
desc: 'easing function which produces values greater than 1',
easing: 'cubic-bezier(0, 1.5, 1, 1.5)',
easingFunction: cubicBezier(0, 1.5, 1, 1.5)
},
{
desc: 'easing function which produces values less than 1',
easing: 'cubic-bezier(0, -0.5, 1, -0.5)',
easingFunction: cubicBezier(0, -0.5, 1, -0.5)
}
];
const gEasingParsingTests = [
['linear', 'linear'],
['ease-in-out', 'ease-in-out'],
['Ease\\2d in-out', 'ease-in-out'],
['ease /**/', 'ease'],
];
const gInvalidEasings = [
'',
'7',
'test',
'initial',
'inherit',
'unset',
'unrecognized',
'var(--x)',
'ease-in-out, ease-out',
'cubic-bezier(1.1, 0, 1, 1)',
'cubic-bezier(0, 0, 1.1, 1)',
'cubic-bezier(-0.1, 0, 1, 1)',
'cubic-bezier(0, 0, -0.1, 1)',
'cubic-bezier(0.1, 0, 4, 0.4)',
'steps(-1, start)',
'steps(0.1, start)',
'steps(3, nowhere)',
'steps(-3, end)',
'function (a){return a}',
'function (x){return x}',
'function(x, y){return 0.3}',
];
// Easings that should serialize to the same string
const gRoundtripEasings = [
'ease',
'linear',
'ease-in',
'ease-out',
'ease-in-out',
'cubic-bezier(0.1, 5, 0.23, 0)',
'steps(3, start)',
'steps(3)',
];

View file

@ -0,0 +1,832 @@
'use strict';
// ==============================
//
// Common keyframe test data
//
// ==============================
// ------------------------------
// Composite values
// ------------------------------
const gGoodKeyframeCompositeValueTests = [
'replace', 'add', 'accumulate', 'auto'
];
const gBadKeyframeCompositeValueTests = [
'unrecognised', 'replace ', 'Replace', null
];
const gGoodOptionsCompositeValueTests = [
'replace', 'add', 'accumulate'
];
const gBadOptionsCompositeValueTests = [
'unrecognised', 'replace ', 'Replace', null
];
// ------------------------------
// Keyframes
// ------------------------------
const gEmptyKeyframeListTests = [
[],
null,
undefined,
];
// Helper methods to make defining computed keyframes more readable.
const offset = offset => ({
offset,
computedOffset: offset,
});
const computedOffset = computedOffset => ({
offset: null,
computedOffset,
});
const keyframe = (offset, props, easing='linear', composite) => {
// The object spread operator is not yet available in all browsers so we use
// Object.assign instead.
const result = {};
Object.assign(result, offset, props, { easing });
result.composite = composite || 'auto';
return result;
};
const gKeyframesTests = [
// ----------- Property-indexed keyframes: property handling -----------
{
desc: 'a one property two value property-indexed keyframes specification',
input: { left: ['10px', '20px'] },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(1), { left: '20px' })],
},
{
desc: 'a one shorthand property two value property-indexed keyframes'
+ ' specification',
input: { margin: ['10px', '10px 20px 30px 40px'] },
output: [keyframe(computedOffset(0), { margin: '10px' }),
keyframe(computedOffset(1), { margin: '10px 20px 30px 40px' })],
},
{
desc: 'a two property (one shorthand and one of its longhand components)'
+ ' two value property-indexed keyframes specification',
input: { marginTop: ['50px', '60px'],
margin: ['10px', '10px 20px 30px 40px'] },
output: [keyframe(computedOffset(0),
{ marginTop: '50px', margin: '10px' }),
keyframe(computedOffset(1),
{ marginTop: '60px', margin: '10px 20px 30px 40px' })],
},
{
desc: 'a two property (one shorthand and one of its shorthand components)'
+ ' two value property-indexed keyframes specification',
input: { border: ['pink', '2px'],
borderColor: ['green', 'blue'] },
output: [keyframe(computedOffset(0),
{ border: 'pink', borderColor: 'green' }),
keyframe(computedOffset(1),
{ border: '2px', borderColor: 'blue' })],
},
{
desc: 'a two property two value property-indexed keyframes specification',
input: { left: ['10px', '20px'],
top: ['30px', '40px'] },
output: [keyframe(computedOffset(0), { left: '10px', top: '30px' }),
keyframe(computedOffset(1), { left: '20px', top: '40px' })],
},
{
desc: 'a two property property-indexed keyframes specification with'
+ ' different numbers of values',
input: { left: ['10px', '20px', '30px'],
top: ['40px', '50px'] },
output: [keyframe(computedOffset(0), { left: '10px', top: '40px' }),
keyframe(computedOffset(0.5), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px', top: '50px' })],
},
{
desc: 'a property-indexed keyframes specification with an invalid value',
input: { left: ['10px', '20px', '30px', '40px', '50px'],
top: ['15px', '25px', 'invalid', '45px', '55px'] },
output: [keyframe(computedOffset(0), { left: '10px', top: '15px' }),
keyframe(computedOffset(0.25), { left: '20px', top: '25px' }),
keyframe(computedOffset(0.5), { left: '30px' }),
keyframe(computedOffset(0.75), { left: '40px', top: '45px' }),
keyframe(computedOffset(1), { left: '50px', top: '55px' })],
},
{
desc: 'a one property two value property-indexed keyframes specification'
+ ' that needs to stringify its values',
input: { opacity: [0, 1] },
output: [keyframe(computedOffset(0), { opacity: '0' }),
keyframe(computedOffset(1), { opacity: '1' })],
},
{
desc: 'a property-indexed keyframes specification with a CSS variable'
+ ' reference',
input: { left: [ 'var(--dist)', 'calc(var(--dist) + 100px)' ] },
output: [keyframe(computedOffset(0), { left: 'var(--dist)' }),
keyframe(computedOffset(1), { left: 'calc(var(--dist) + 100px)' })]
},
{
desc: 'a property-indexed keyframes specification with a CSS variable'
+ ' reference in a shorthand property',
input: { margin: [ 'var(--dist)', 'calc(var(--dist) + 100px)' ] },
output: [keyframe(computedOffset(0),
{ margin: 'var(--dist)' }),
keyframe(computedOffset(1),
{ margin: 'calc(var(--dist) + 100px)' })],
},
{
desc: 'a one property one value property-indexed keyframes specification',
input: { left: ['10px'] },
output: [keyframe(computedOffset(1), { left: '10px' })],
},
{
desc: 'a one property one non-array value property-indexed keyframes'
+ ' specification',
input: { left: '10px' },
output: [keyframe(computedOffset(1), { left: '10px' })],
},
{
desc: 'a one property two value property-indexed keyframes specification'
+ ' where the first value is invalid',
input: { left: ['invalid', '10px'] },
output: [keyframe(computedOffset(0), {}),
keyframe(computedOffset(1), { left: '10px' })]
},
{
desc: 'a one property two value property-indexed keyframes specification'
+ ' where the second value is invalid',
input: { left: ['10px', 'invalid'] },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(1), {})]
},
{
desc: 'a property-indexed keyframes specification with a CSS variable as'
+ ' the property',
input: { '--custom': ['1', '2'] },
output: [keyframe(computedOffset(0), { '--custom': '1' }),
keyframe(computedOffset(1), { '--custom': '2' })]
},
// ----------- Property-indexed keyframes: offset handling -----------
{
desc: 'a property-indexed keyframe with a single offset',
input: { left: ['10px', '20px', '30px'], offset: 0.5 },
output: [keyframe(offset(0.5), { left: '10px' }),
keyframe(computedOffset(0.75), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets',
input: { left: ['10px', '20px', '30px'], offset: [ 0.1, 0.25, 0.8 ] },
output: [keyframe(offset(0.1), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(offset(0.8), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets that is too'
+ ' short',
input: { left: ['10px', '20px', '30px'], offset: [ 0, 0.25 ] },
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets that is too'
+ ' long',
input: { left: ['10px', '20px', '30px'],
offset: [ 0, 0.25, 0.5, 0.75, 1 ] },
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(offset(0.5), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an empty array of offsets',
input: { left: ['10px', '20px', '30px'], offset: [] },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(0.5), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets with an'
+ ' embedded null value',
input: { left: ['10px', '20px', '30px'],
offset: [ 0, null, 0.5 ] },
output: [keyframe(offset(0), { left: '10px' }),
keyframe(computedOffset(0.25), { left: '20px' }),
keyframe(offset(0.5), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets with a'
+ ' trailing null value',
input: { left: ['10px', '20px', '30px'],
offset: [ 0, 0.25, null ] },
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets with leading'
+ ' and trailing null values',
input: { left: ['10px', '20px', '30px'],
offset: [ null, 0.25, null ] },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets with'
+ ' adjacent null values',
input: { left: ['10px', '20px', '30px'],
offset: [ null, null, 0.5 ] },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(0.25), { left: '20px' }),
keyframe(offset(0.5), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets with'
+ ' all null values (and too many at that)',
input: { left: ['10px', '20px', '30px'],
offset: [ null, null, null, null, null ] },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(0.5), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with a single null offset',
input: { left: ['10px', '20px', '30px'], offset: null },
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(0.5), { left: '20px' }),
keyframe(computedOffset(1), { left: '30px' })],
},
{
desc: 'a property-indexed keyframe with an array of offsets that is not'
+ ' strictly ascending in the unused part of the array',
input: { left: ['10px', '20px', '30px'],
offset: [ 0, 0.2, 0.8, 0.6 ] },
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.2), { left: '20px' }),
keyframe(offset(0.8), { left: '30px' })],
},
// ----------- Property-indexed keyframes: easing handling -----------
{
desc: 'a property-indexed keyframe without any specified easing',
input: { left: ['10px', '20px', '30px'] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'linear'),
keyframe(computedOffset(0.5), { left: '20px' }, 'linear'),
keyframe(computedOffset(1), { left: '30px' }, 'linear')],
},
{
desc: 'a property-indexed keyframe with a single easing',
input: { left: ['10px', '20px', '30px'], easing: 'ease-in' },
output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
keyframe(computedOffset(0.5), { left: '20px' }, 'ease-in'),
keyframe(computedOffset(1), { left: '30px' }, 'ease-in')],
},
{
desc: 'a property-indexed keyframe with an array of easings',
input: { left: ['10px', '20px', '30px'],
easing: ['ease-in', 'ease-out', 'ease-in-out'] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
keyframe(computedOffset(0.5), { left: '20px' }, 'ease-out'),
keyframe(computedOffset(1), { left: '30px' }, 'ease-in-out')],
},
{
desc: 'a property-indexed keyframe with an array of easings that is too'
+ ' short',
input: { left: ['10px', '20px', '30px'],
easing: ['ease-in', 'ease-out'] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
keyframe(computedOffset(0.5), { left: '20px' }, 'ease-out'),
keyframe(computedOffset(1), { left: '30px' }, 'ease-in')],
},
{
desc: 'a property-indexed keyframe with a single-element array of'
+ ' easings',
input: { left: ['10px', '20px', '30px'], easing: ['ease-in'] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'ease-in'),
keyframe(computedOffset(0.5), { left: '20px' }, 'ease-in'),
keyframe(computedOffset(1), { left: '30px' }, 'ease-in')],
},
{
desc: 'a property-indexed keyframe with an empty array of easings',
input: { left: ['10px', '20px', '30px'], easing: [] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'linear'),
keyframe(computedOffset(0.5), { left: '20px' }, 'linear'),
keyframe(computedOffset(1), { left: '30px' }, 'linear')],
},
{
desc: 'a property-indexed keyframe with an array of easings that is too'
+ ' long',
input: { left: ['10px', '20px', '30px'],
easing: ['steps(1)', 'steps(2)', 'steps(3)', 'steps(4)'] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'steps(1)'),
keyframe(computedOffset(0.5), { left: '20px' }, 'steps(2)'),
keyframe(computedOffset(1), { left: '30px' }, 'steps(3)')],
},
// ----------- Property-indexed keyframes: composite handling -----------
{
desc: 'a property-indexed keyframe with a single composite operation',
input: { left: ['10px', '20px', '30px'], composite: 'add' },
output: [keyframe(computedOffset(0), { left: '10px' }, 'linear', 'add'),
keyframe(computedOffset(0.5), { left: '20px' }, 'linear', 'add'),
keyframe(computedOffset(1), { left: '30px' }, 'linear', 'add')],
},
{
desc: 'a property-indexed keyframe with a composite array',
input: { left: ['10px', '20px', '30px'],
composite: ['add', 'replace', 'accumulate'] },
output: [keyframe(computedOffset(0), { left: '10px' },
'linear', 'add'),
keyframe(computedOffset(0.5), { left: '20px' },
'linear', 'replace'),
keyframe(computedOffset(1), { left: '30px' },
'linear', 'accumulate')],
},
{
desc: 'a property-indexed keyframe with a composite array that is too'
+ ' short',
input: { left: ['10px', '20px', '30px', '40px', '50px'],
composite: ['add', 'replace'] },
output: [keyframe(computedOffset(0), { left: '10px' },
'linear', 'add'),
keyframe(computedOffset(0.25), { left: '20px' },
'linear', 'replace'),
keyframe(computedOffset(0.5), { left: '30px' },
'linear', 'add'),
keyframe(computedOffset(0.75), { left: '40px' },
'linear', 'replace'),
keyframe(computedOffset(1), { left: '50px' },
'linear', 'add')],
},
{
desc: 'a property-indexed keyframe with a composite array that is too'
+ ' long',
input: { left: ['10px', '20px'],
composite: ['add', 'replace', 'accumulate'] },
output: [keyframe(computedOffset(0), { left: '10px' },
'linear', 'add'),
keyframe(computedOffset(1), { left: '20px' },
'linear', 'replace')],
},
{
desc: 'a property-indexed keyframe with a single-element composite array',
input: { left: ['10px', '20px', '30px'],
composite: ['add'] },
output: [keyframe(computedOffset(0), { left: '10px' }, 'linear', 'add'),
keyframe(computedOffset(0.5), { left: '20px' }, 'linear', 'add'),
keyframe(computedOffset(1), { left: '30px' }, 'linear', 'add')],
},
// ----------- Keyframe sequence: property handling -----------
{
desc: 'a one property one keyframe sequence',
input: [{ offset: 1, left: '10px' }],
output: [keyframe(offset(1), { left: '10px' })],
},
{
desc: 'a one property two keyframe sequence',
input: [{ offset: 0, left: '10px' },
{ offset: 1, left: '20px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(1), { left: '20px' })],
},
{
desc: 'a two property two keyframe sequence',
input: [{ offset: 0, left: '10px', top: '30px' },
{ offset: 1, left: '20px', top: '40px' }],
output: [keyframe(offset(0), { left: '10px', top: '30px' }),
keyframe(offset(1), { left: '20px', top: '40px' })],
},
{
desc: 'a one shorthand property two keyframe sequence',
input: [{ offset: 0, margin: '10px' },
{ offset: 1, margin: '20px 30px 40px 50px' }],
output: [keyframe(offset(0), { margin: '10px' }),
keyframe(offset(1), { margin: '20px 30px 40px 50px' })],
},
{
desc: 'a two property (a shorthand and one of its component longhands)'
+ ' two keyframe sequence',
input: [{ offset: 0, margin: '10px', marginTop: '20px' },
{ offset: 1, marginTop: '70px', margin: '30px 40px 50px 60px' }],
output: [keyframe(offset(0), { margin: '10px', marginTop: '20px' }),
keyframe(offset(1), { marginTop: '70px',
margin: '30px 40px 50px 60px' })],
},
{
desc: 'a two property keyframe sequence where one property is missing'
+ ' from the first keyframe',
input: [{ offset: 0, left: '10px' },
{ offset: 1, left: '20px', top: '30px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(1), { left: '20px', top: '30px' })],
},
{
desc: 'a two property keyframe sequence where one property is missing'
+ ' from the last keyframe',
input: [{ offset: 0, left: '10px', top: '20px' },
{ offset: 1, left: '30px' }],
output: [keyframe(offset(0), { left: '10px', top: '20px' }),
keyframe(offset(1), { left: '30px' })],
},
{
desc: 'a one property two keyframe sequence that needs to stringify'
+ ' its values',
input: [{ offset: 0, opacity: 0 },
{ offset: 1, opacity: 1 }],
output: [keyframe(offset(0), { opacity: '0' }),
keyframe(offset(1), { opacity: '1' })],
},
{
desc: 'a keyframe sequence with a CSS variable reference',
input: [{ left: 'var(--dist)' },
{ left: 'calc(var(--dist) + 100px)' }],
output: [keyframe(computedOffset(0), { left: 'var(--dist)' }),
keyframe(computedOffset(1), { left: 'calc(var(--dist) + 100px)' })]
},
{
desc: 'a keyframe sequence with a CSS variable reference in a shorthand'
+ ' property',
input: [{ margin: 'var(--dist)' },
{ margin: 'calc(var(--dist) + 100px)' }],
output: [keyframe(computedOffset(0),
{ margin: 'var(--dist)' }),
keyframe(computedOffset(1),
{ margin: 'calc(var(--dist) + 100px)' })],
},
{
desc: 'a keyframe sequence with a CSS variable as its property',
input: [{ '--custom': 'a' },
{ '--custom': 'b' }],
output: [keyframe(computedOffset(0), { '--custom': 'a' }),
keyframe(computedOffset(1), { '--custom': 'b' })]
},
// ----------- Keyframe sequence: offset handling -----------
{
desc: 'a keyframe sequence with duplicate values for a given interior'
+ ' offset',
input: [{ offset: 0.0, left: '10px' },
{ offset: 0.5, left: '20px' },
{ offset: 0.5, left: '30px' },
{ offset: 0.5, left: '40px' },
{ offset: 1.0, left: '50px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.5), { left: '20px' }),
keyframe(offset(0.5), { left: '30px' }),
keyframe(offset(0.5), { left: '40px' }),
keyframe(offset(1), { left: '50px' })],
},
{
desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
input: [{ offset: 0, left: '10px' },
{ offset: 0, left: '20px' },
{ offset: 0, left: '30px' },
{ offset: 1, left: '40px' },
{ offset: 1, left: '50px' },
{ offset: 1, left: '60px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0), { left: '20px' }),
keyframe(offset(0), { left: '30px' }),
keyframe(offset(1), { left: '40px' }),
keyframe(offset(1), { left: '50px' }),
keyframe(offset(1), { left: '60px' })],
},
{
desc: 'a two property four keyframe sequence',
input: [{ offset: 0, left: '10px' },
{ offset: 0, top: '20px' },
{ offset: 1, top: '30px' },
{ offset: 1, left: '40px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0), { top: '20px' }),
keyframe(offset(1), { top: '30px' }),
keyframe(offset(1), { left: '40px' })],
},
{
desc: 'a single keyframe sequence with omitted offset',
input: [{ left: '10px' }],
output: [keyframe(computedOffset(1), { left: '10px' })],
},
{
desc: 'a single keyframe sequence with null offset',
input: [{ offset: null, left: '10px' }],
output: [keyframe(computedOffset(1), { left: '10px' })],
},
{
desc: 'a single keyframe sequence with string offset',
input: [{ offset: '0.5', left: '10px' }],
output: [keyframe(offset(0.5), { left: '10px' })],
},
{
desc: 'a single keyframe sequence with a single calc() offset',
input: [{ offset: 'calc(0.5)', left: '10px' }],
output: [keyframe(offset(0.5), { left: '10px' })],
},
{
desc: 'a one property keyframe sequence with some omitted offsets',
input: [{ offset: 0.00, left: '10px' },
{ offset: 0.25, left: '20px' },
{ left: '30px' },
{ left: '40px' },
{ offset: 1.00, left: '50px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(computedOffset(0.5), { left: '30px' }),
keyframe(computedOffset(0.75), { left: '40px' }),
keyframe(offset(1), { left: '50px' })],
},
{
desc: 'a one property keyframe sequence with some null offsets',
input: [{ offset: 0.00, left: '10px' },
{ offset: 0.25, left: '20px' },
{ offset: null, left: '30px' },
{ offset: null, left: '40px' },
{ offset: 1.00, left: '50px' }],
output: [keyframe(offset(0), { left: '10px' }),
keyframe(offset(0.25), { left: '20px' }),
keyframe(computedOffset(0.5), { left: '30px' }),
keyframe(computedOffset(0.75), { left: '40px' }),
keyframe(offset(1), { left: '50px' })],
},
{
desc: 'a two property keyframe sequence with some omitted offsets',
input: [{ offset: 0.00, left: '10px', top: '20px' },
{ offset: 0.25, left: '30px' },
{ left: '40px' },
{ left: '50px', top: '60px' },
{ offset: 1.00, left: '70px', top: '80px' }],
output: [keyframe(offset(0), { left: '10px', top: '20px' }),
keyframe(offset(0.25), { left: '30px' }),
keyframe(computedOffset(0.5), { left: '40px' }),
keyframe(computedOffset(0.75), { left: '50px', top: '60px' }),
keyframe(offset(1), { left: '70px', top: '80px' })],
},
{
desc: 'a one property keyframe sequence with all omitted offsets',
input: [{ left: '10px' },
{ left: '20px' },
{ left: '30px' },
{ left: '40px' },
{ left: '50px' }],
output: [keyframe(computedOffset(0), { left: '10px' }),
keyframe(computedOffset(0.25), { left: '20px' }),
keyframe(computedOffset(0.5), { left: '30px' }),
keyframe(computedOffset(0.75), { left: '40px' }),
keyframe(computedOffset(1), { left: '50px' })],
},
// ----------- Keyframe sequence: easing handling -----------
{
desc: 'a keyframe sequence with different easing values, but the same'
+ ' easing value for a given offset',
input: [{ offset: 0.0, easing: 'ease', left: '10px'},
{ offset: 0.0, easing: 'ease', top: '20px'},
{ offset: 0.5, easing: 'linear', left: '30px' },
{ offset: 0.5, easing: 'linear', top: '40px' },
{ offset: 1.0, easing: 'step-end', left: '50px' },
{ offset: 1.0, easing: 'step-end', top: '60px' }],
output: [keyframe(offset(0), { left: '10px' }, 'ease'),
keyframe(offset(0), { top: '20px' }, 'ease'),
keyframe(offset(0.5), { left: '30px' }, 'linear'),
keyframe(offset(0.5), { top: '40px' }, 'linear'),
keyframe(offset(1), { left: '50px' }, 'steps(1)'),
keyframe(offset(1), { top: '60px' }, 'steps(1)')],
},
// ----------- Keyframe sequence: composite handling -----------
{
desc: 'a keyframe sequence with different composite values, but the'
+ ' same composite value for a given offset',
input: [{ offset: 0.0, composite: 'replace', left: '10px' },
{ offset: 0.0, composite: 'replace', top: '20px' },
{ offset: 0.5, composite: 'add', left: '30px' },
{ offset: 0.5, composite: 'add', top: '40px' },
{ offset: 1.0, composite: 'replace', left: '50px' },
{ offset: 1.0, composite: 'replace', top: '60px' }],
output: [keyframe(offset(0), { left: '10px' }, 'linear', 'replace'),
keyframe(offset(0), { top: '20px' }, 'linear', 'replace'),
keyframe(offset(0.5), { left: '30px' }, 'linear', 'add'),
keyframe(offset(0.5), { top: '40px' }, 'linear', 'add'),
keyframe(offset(1), { left: '50px' }, 'linear', 'replace'),
keyframe(offset(1), { top: '60px' }, 'linear', 'replace')],
},
];
const gInvalidKeyframesTests = [
{
desc: 'keyframes with an out-of-bounded positive offset',
input: [ { opacity: 0 },
{ opacity: 0.5, offset: 2 },
{ opacity: 1 } ],
},
{
desc: 'keyframes with an out-of-bounded negative offset',
input: [ { opacity: 0 },
{ opacity: 0.5, offset: -1 },
{ opacity: 1 } ],
},
{
desc: 'property-indexed keyframes not loosely sorted by offset',
input: { opacity: [ 0, 1 ], offset: [ 1, 0 ] },
},
{
desc: 'property-indexed keyframes not loosely sorted by offset even'
+ ' though not all offsets are specified',
input: { opacity: [ 0, 0.5, 1 ], offset: [ 0.5, 0 ] },
},
{
desc: 'property-indexed keyframes with offsets out of range',
input: { opacity: [ 0, 0.5, 1 ], offset: [ 0, 1.1 ] },
},
{
desc: 'keyframes not loosely sorted by offset',
input: [ { opacity: 0, offset: 1 },
{ opacity: 1, offset: 0 } ],
},
{
desc: 'property-indexed keyframes with an invalid easing value',
input: { opacity: [ 0, 0.5, 1 ],
easing: 'inherit' },
},
{
desc: 'property-indexed keyframes with an invalid easing value as one of'
+ ' the array values',
input: { opacity: [ 0, 0.5, 1 ],
easing: [ 'ease-in', 'inherit' ] },
},
{
desc: 'property-indexed keyframe with an invalid easing in the unused'
+ ' part of the array of easings',
input: { left: ['10px', '20px', '30px'],
easing: ['steps(1)', 'steps(2)', 'steps(3)', 'invalid'] },
},
{
desc: 'empty property-indexed keyframe with an invalid easing',
input: { easing: 'invalid' },
},
{
desc: 'empty property-indexed keyframe with an invalid easings array',
input: { easing: ['invalid'] },
},
{
desc: 'a keyframe sequence with an invalid easing value',
input: [ { opacity: 0, easing: 'jumpy' },
{ opacity: 1 } ],
},
{
desc: 'property-indexed keyframes with an invalid composite value',
input: { opacity: [ 0, 0.5, 1 ],
composite: 'alternate' },
},
{
desc: 'property-indexed keyframes with an invalid composite value as one'
+ ' of the array values',
input: { opacity: [ 0, 0.5, 1 ],
composite: [ 'add', 'alternate' ] },
},
{
desc: 'keyframes with an invalid composite value',
input: [ { opacity: 0, composite: 'alternate' },
{ opacity: 1 } ],
},
];
const gKeyframeSerializationTests = [
{
desc: 'a on keyframe sequence which requires value serilaization of its'
+ ' values',
input: [{offset: 0, backgroundColor: 'rgb(1,2,3)' }],
output: [keyframe(offset(0), { backgroundColor: 'rgb(1, 2, 3)' })],
},
];
// ------------------------------
// KeyframeEffectOptions
// ------------------------------
const gKeyframeEffectOptionTests = [
{
desc: 'an empty KeyframeEffectOptions object',
input: { },
expected: { },
},
{
desc: 'a normal KeyframeEffectOptions object',
input: { delay: 1000,
fill: 'auto',
iterations: 5.5,
duration: 'auto',
direction: 'alternate' },
expected: { delay: 1000,
fill: 'auto',
iterations: 5.5,
duration: 'auto',
direction: 'alternate' },
},
{
desc: 'a double value',
input: 3000,
expected: { duration: 3000 },
},
{
desc: '+Infinity',
input: Infinity,
expected: { duration: Infinity },
},
{
desc: 'an Infinity duration',
input: { duration: Infinity },
expected: { duration: Infinity },
},
{
desc: 'an auto duration',
input: { duration: 'auto' },
expected: { duration: 'auto' },
},
{
desc: 'an Infinity iterations',
input: { iterations: Infinity },
expected: { iterations: Infinity },
},
{
desc: 'an auto fill',
input: { fill: 'auto' },
expected: { fill: 'auto' },
},
{
desc: 'a forwards fill',
input: { fill: 'forwards' },
expected: { fill: 'forwards' },
}
];
const gInvalidKeyframeEffectOptionTests = [
{ desc: '-Infinity', input: -Infinity },
{ desc: 'NaN', input: NaN },
{ desc: 'a negative value', input: -1 },
{ desc: 'a negative Infinity duration', input: { duration: -Infinity } },
{ desc: 'a NaN duration', input: { duration: NaN } },
{ desc: 'a negative duration', input: { duration: -1 } },
{ desc: 'a string duration', input: { duration: 'merrychristmas' } },
{ desc: 'a negative Infinity iterations', input: { iterations: -Infinity} },
{ desc: 'a NaN iterations', input: { iterations: NaN } },
{ desc: 'a negative iterations', input: { iterations: -1 } },
{ desc: 'a blank easing', input: { easing: '' } },
{ desc: 'an unrecognized easing', input: { easing: 'unrecognised' } },
{ desc: 'an \'initial\' easing', input: { easing: 'initial' } },
{ desc: 'an \'inherit\' easing', input: { easing: 'inherit' } },
{ desc: 'a variable easing', input: { easing: 'var(--x)' } },
{ desc: 'a multi-value easing', input: { easing: 'ease-in-out, ease-out' } },
];
// There is currently only ScrollTimeline that can be constructed and used here
// beyond document timeline. Given that ScrollTimeline is not stable as of yet
// it's tested in scroll-animations/animation-with-animatable-interface.html.
const gAnimationTimelineTests = [
{
expectedTimeline: document.timeline,
expectedTimelineDescription: 'document.timeline',
description: 'with no timeline parameter'
},
{
timeline: undefined,
expectedTimeline: document.timeline,
expectedTimelineDescription: 'document.timeline',
description: 'with undefined timeline'
},
{
timeline: null,
expectedTimeline: null,
expectedTimelineDescription: 'null',
description: 'with null timeline'
},
{
timeline: document.timeline,
expectedTimeline: document.timeline,
expectedTimelineDescription: 'document.timeline',
description: 'with DocumentTimeline'
},
];

View file

@ -0,0 +1,51 @@
'use strict';
// =======================================
//
// Utility functions for testing keyframes
//
// =======================================
// ------------------------------
// Helper functions
// ------------------------------
/**
* Test equality between two lists of computed keyframes
* @param {Array.<ComputedKeyframe>} a - actual computed keyframes
* @param {Array.<ComputedKeyframe>} b - expected computed keyframes
*/
function assert_frame_lists_equal(a, b, message) {
assert_equals(a.length, b.length, `number of frames: ${(message || '')}`);
for (let i = 0; i < Math.min(a.length, b.length); i++) {
assert_frames_equal(a[i], b[i],
`ComputedKeyframe #${i}: ${(message || '')}`);
}
}
/** Helper for assert_frame_lists_equal */
function assert_frames_equal(a, b, name) {
assert_equals(Object.keys(a).sort().toString(),
Object.keys(b).sort().toString(),
`properties on ${name} should match`);
// Iterates sorted keys to ensure stable failures.
for (const p of Object.keys(a).sort()) {
if (typeof b[p] == 'number')
assert_approx_equals(a[p], b[p], 1e-6, `value for '${p}' on ${name}`);
else if (typeof b[p] == 'object') {
for (const key in b[p]) {
if (typeof b[p][key] == 'number') {
assert_approx_equals(a[p][key], b[p][key], 1e-6,
`value for '${p}.${key}' on ${name}`);
} else {
assert_equals((a[p][key] || 'undefined').toString(),
b[p][key].toString(),
`value for '${p}.${key}' on ${name}`);
}
}
}
else
assert_equals(a[p], b[p], `value for '${p}' on ${name}`);
}
}

View file

@ -0,0 +1,46 @@
'use strict';
// =================================
//
// Common timing parameter test data
//
// =================================
// ------------------------------
// Delay values
// ------------------------------
const gBadDelayValues = [
NaN, Infinity, -Infinity
];
// ------------------------------
// Duration values
// ------------------------------
const gGoodDurationValues = [
{ specified: 123.45, computed: 123.45 },
{ specified: 'auto', computed: 0 },
{ specified: Infinity, computed: Infinity },
];
const gBadDurationValues = [
-1, NaN, -Infinity, 'abc', '100'
];
// ------------------------------
// iterationStart values
// ------------------------------
const gBadIterationStartValues = [
-1, NaN, Infinity, -Infinity
];
// ------------------------------
// iterations values
// ------------------------------
const gBadIterationsValues = [
-1, -Infinity, NaN
];

View file

@ -0,0 +1,52 @@
'use strict';
// =======================================
//
// Utility functions for testing timing
//
// =======================================
// ------------------------------
// Helper functions
// ------------------------------
// Utility function to check that a subset of timing properties have their
// default values.
function assert_default_timing_except(effect, propertiesToSkip) {
const defaults = {
delay: 0,
endDelay: 0,
fill: 'auto',
iterationStart: 0,
iterations: 1,
duration: 'auto',
direction: 'normal',
easing: 'linear',
};
for (const prop of Object.keys(defaults)) {
if (propertiesToSkip.includes(prop)) {
continue;
}
assert_equals(
effect.getTiming()[prop],
defaults[prop],
`${prop} parameter has default value:`
);
}
}
function waitForAnimationTime(animation, time) {
return new Promise((resolve) => {
function raf() {
if (animation.currentTime < time) {
requestAnimationFrame(raf);
} else {
resolve();
}
}
requestAnimationFrame(raf);
});
}

View 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));
}