diff options
author | bors-servo <servo-ops@mozilla.com> | 2020-05-17 15:09:49 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-17 15:09:49 -0400 |
commit | 50bd5c3e0f9166a9fa049b95f23251ad7626f8de (patch) | |
tree | 1c817c288530e4115609daa87ede4f85cfe825b0 | |
parent | 619e0bceaf750ec862169a6a02cfbf82ba9be0a8 (diff) | |
parent | 183f15d5aacd290aa2bd2c3a2397cd456d170161 (diff) | |
download | servo-50bd5c3e0f9166a9fa049b95f23251ad7626f8de.tar.gz servo-50bd5c3e0f9166a9fa049b95f23251ad7626f8de.zip |
Auto merge of #26519 - mrobinson:animation-animation-fill-mode, r=jdm
Implement `animation-fill-mode`
Fixes #26460.
<!-- Please describe your changes on the following line: -->
---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] There are tests for these changes
<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->
<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
16 files changed, 240 insertions, 95 deletions
diff --git a/components/style/animation.rs b/components/style/animation.rs index f52e9b42a51..b30ff303165 100644 --- a/components/style/animation.rs +++ b/components/style/animation.rs @@ -13,6 +13,7 @@ use crate::dom::{OpaqueNode, TElement, TNode}; use crate::font_metrics::FontMetricsProvider; use crate::properties::animated_properties::AnimationValue; use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection; +use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode; use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState; use crate::properties::LonghandIdSet; use crate::properties::{self, CascadeMode, ComputedValues, LonghandId}; @@ -184,6 +185,9 @@ pub struct Animation { /// The delay of the animation. pub delay: f64, + /// The `animation-fill-mode` property of this animation. + pub fill_mode: AnimationFillMode, + /// The current iteration state for the animation. pub iteration_state: KeyframesIterationState, @@ -324,9 +328,6 @@ impl Animation { (&mut Paused(ref mut progress), Running) => { *progress = (now - old_started_at) / old_duration }, - // TODO(mrobinson): We should handle the case where a new animation replaces - // a finished one. - (_, Finished) | (Finished, _) => unreachable!("Did not expect Finished animation."), _ => {}, } @@ -361,7 +362,7 @@ impl Animation { fn update_style<E>( &self, context: &SharedStyleContext, - style: &mut ComputedValues, + style: &mut Arc<ComputedValues>, font_metrics_provider: &dyn FontMetricsProvider, ) where E: TElement, @@ -370,46 +371,54 @@ impl Animation { let started_at = self.started_at; let now = match self.state { - AnimationState::Running => context.current_time_for_animations, + AnimationState::Running | AnimationState::Finished => { + context.current_time_for_animations + }, AnimationState::Paused(progress) => started_at + duration * progress, - AnimationState::Canceled | AnimationState::Finished => return, + AnimationState::Canceled => return, }; debug_assert!(!self.keyframes_animation.steps.is_empty()); + let mut total_progress = (now - started_at) / duration; - if total_progress < 0. { - warn!("Negative progress found for animation {:?}", self.name); + if total_progress < 0. && + self.fill_mode != AnimationFillMode::Backwards && + self.fill_mode != AnimationFillMode::Both + { return; } - if total_progress > 1. { - total_progress = 1.; + + if total_progress > 1. && + self.fill_mode != AnimationFillMode::Forwards && + self.fill_mode != AnimationFillMode::Both + { + return; } + total_progress = total_progress.min(1.0).max(0.0); - // Get the target and the last keyframe position. - let last_keyframe_position; - let target_keyframe_position; + // Get the indices of the previous (from) keyframe and the next (to) keyframe. + let next_keyframe_index; + let prev_keyframe_index; match self.current_direction { AnimationDirection::Normal => { - target_keyframe_position = self + next_keyframe_index = self .keyframes_animation .steps .iter() .position(|step| total_progress as f32 <= step.start_percentage.0); - - last_keyframe_position = target_keyframe_position + prev_keyframe_index = next_keyframe_index .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) .unwrap_or(0); }, AnimationDirection::Reverse => { - target_keyframe_position = self + next_keyframe_index = self .keyframes_animation .steps .iter() .rev() .position(|step| total_progress as f32 <= 1. - step.start_percentage.0) .map(|pos| self.keyframes_animation.steps.len() - pos - 1); - - last_keyframe_position = target_keyframe_position + prev_keyframe_index = next_keyframe_index .and_then(|pos| { if pos != self.keyframes_animation.steps.len() - 1 { Some(pos + 1) @@ -417,52 +426,83 @@ impl Animation { None } }) - .unwrap_or(self.keyframes_animation.steps.len() - 1); + .unwrap_or(self.keyframes_animation.steps.len() - 1) }, _ => unreachable!(), } debug!( "Animation::update_style: keyframe from {:?} to {:?}", - last_keyframe_position, target_keyframe_position + prev_keyframe_index, next_keyframe_index ); - let target_keyframe = match target_keyframe_position { + let prev_keyframe = &self.keyframes_animation.steps[prev_keyframe_index]; + let next_keyframe = match next_keyframe_index { Some(target) => &self.keyframes_animation.steps[target], None => return, }; - let last_keyframe = &self.keyframes_animation.steps[last_keyframe_position]; + let update_with_single_keyframe_style = |style, computed_style: &Arc<ComputedValues>| { + let mutable_style = Arc::make_mut(style); + for property in self + .keyframes_animation + .properties_changed + .iter() + .filter_map(|longhand| { + AnimationValue::from_computed_values(longhand, &**computed_style) + }) + { + property.set_in_style_for_servo(mutable_style); + } + }; + + // TODO: How could we optimise it? Is it such a big deal? + let prev_keyframe_style = compute_style_for_animation_step::<E>( + context, + prev_keyframe, + style, + &self.cascade_style, + font_metrics_provider, + ); + if total_progress <= 0.0 { + update_with_single_keyframe_style(style, &prev_keyframe_style); + return; + } + + let next_keyframe_style = compute_style_for_animation_step::<E>( + context, + next_keyframe, + &prev_keyframe_style, + &self.cascade_style, + font_metrics_provider, + ); + if total_progress >= 1.0 { + update_with_single_keyframe_style(style, &next_keyframe_style); + return; + } let relative_timespan = - (target_keyframe.start_percentage.0 - last_keyframe.start_percentage.0).abs(); + (next_keyframe.start_percentage.0 - prev_keyframe.start_percentage.0).abs(); let relative_duration = relative_timespan as f64 * duration; let last_keyframe_ended_at = match self.current_direction { AnimationDirection::Normal => { - self.started_at + (duration * last_keyframe.start_percentage.0 as f64) + self.started_at + (duration * prev_keyframe.start_percentage.0 as f64) }, AnimationDirection::Reverse => { - self.started_at + (duration * (1. - last_keyframe.start_percentage.0 as f64)) + self.started_at + (duration * (1. - prev_keyframe.start_percentage.0 as f64)) }, _ => unreachable!(), }; let relative_progress = (now - last_keyframe_ended_at) / relative_duration; - // TODO: How could we optimise it? Is it such a big deal? - let from_style = compute_style_for_animation_step::<E>( - context, - last_keyframe, - style, - &self.cascade_style, - font_metrics_provider, - ); - // NB: The spec says that the timing function can be overwritten // from the keyframe style. - let timing_function = if last_keyframe.declared_timing_function { + let timing_function = if prev_keyframe.declared_timing_function { // NB: animation_timing_function can never be empty, always has // at least the default value (`ease`). - from_style.get_box().animation_timing_function_at(0) + prev_keyframe_style + .get_box() + .animation_timing_function_at(0) } else { // TODO(mrobinson): It isn't optimal to have to walk this list every // time. Perhaps this should be stored in the animation. @@ -477,18 +517,10 @@ impl Animation { style.get_box().animation_timing_function_mod(index) }; - let target_style = compute_style_for_animation_step::<E>( - context, - target_keyframe, - &from_style, - &self.cascade_style, - font_metrics_provider, - ); - - let mut new_style = (*style).clone(); + let mut new_style = (**style).clone(); let mut update_style_for_longhand = |longhand| { - let from = AnimationValue::from_computed_values(longhand, &from_style)?; - let to = AnimationValue::from_computed_values(longhand, &target_style)?; + let from = AnimationValue::from_computed_values(longhand, &prev_keyframe_style)?; + let to = AnimationValue::from_computed_values(longhand, &next_keyframe_style)?; PropertyAnimation { from, to, @@ -503,7 +535,7 @@ impl Animation { update_style_for_longhand(property); } - *style = new_style; + *Arc::make_mut(style) = new_style; } } @@ -560,7 +592,7 @@ impl Transition { } /// Update a style to the value specified by this `Transition` given a `SharedStyleContext`. - fn update_style(&self, context: &SharedStyleContext, style: &mut ComputedValues) { + fn update_style(&self, context: &SharedStyleContext, style: &mut Arc<ComputedValues>) { // Never apply canceled transitions to a style. if self.state == AnimationState::Canceled { return; @@ -568,7 +600,8 @@ impl Transition { let progress = self.progress(context.current_time_for_animations); if progress >= 0.0 { - self.property_animation.update(style, progress); + self.property_animation + .update(Arc::make_mut(style), progress); } } } @@ -603,12 +636,6 @@ impl ElementAnimationSet { ) where E: TElement, { - // Return early to avoid potentially copying the style. - if self.animations.is_empty() && self.transitions.is_empty() { - return; - } - - let style = Arc::make_mut(style); for animation in &self.animations { animation.update_style::<E>(context, style, font_metrics); } @@ -618,15 +645,6 @@ impl ElementAnimationSet { } } - pub(crate) fn clear_finished_animations(&mut self) { - // TODO(mrobinson): This should probably not clear finished animations - // because of `animation-fill-mode`. - self.animations - .retain(|animation| animation.state != AnimationState::Finished); - self.transitions - .retain(|animation| animation.state != AnimationState::Finished); - } - /// Clear all canceled animations and transitions from this `ElementAnimationSet`. pub fn clear_canceled_animations(&mut self) { self.animations @@ -933,6 +951,7 @@ pub fn maybe_start_animations<E>( keyframes_animation: anim.clone(), started_at: animation_start, duration: duration as f64, + fill_mode: box_style.animation_fill_mode_mod(i), delay: delay as f64, iteration_state, state, @@ -944,7 +963,7 @@ pub fn maybe_start_animations<E>( // If the animation was already present in the list for the node, just update its state. for existing_animation in animation_state.animations.iter_mut() { - if existing_animation.state != AnimationState::Running { + if existing_animation.state == AnimationState::Canceled { continue; } diff --git a/components/style/matching.rs b/components/style/matching.rs index e01adda303e..677f024acfa 100644 --- a/components/style/matching.rs +++ b/components/style/matching.rs @@ -7,6 +7,7 @@ #![allow(unsafe_code)] #![deny(missing_docs)] +use crate::animation::AnimationState; use crate::computed_value_flags::ComputedValueFlags; use crate::context::{ElementCascadeInputs, QuirksMode, SelectorFlagsMap}; use crate::context::{SharedStyleContext, StyleContext}; @@ -458,9 +459,14 @@ trait PrivateMatchMethods: TElement { &context.thread_local.font_metrics_provider, ); + // We clear away any finished transitions, but retain animations, because they + // might still be used for proper calculation of `animation-fill-mode`. + animation_state + .transitions + .retain(|transition| transition.state != AnimationState::Finished); + // If the ElementAnimationSet is empty, and don't store it in order to // save memory and to avoid extra processing later. - animation_state.clear_finished_animations(); if !animation_state.is_empty() { animation_states.insert(this_opaque, animation_state); } diff --git a/components/style/properties/longhands/box.mako.rs b/components/style/properties/longhands/box.mako.rs index a4f2cc670f5..243acb76ea3 100644 --- a/components/style/properties/longhands/box.mako.rs +++ b/components/style/properties/longhands/box.mako.rs @@ -312,7 +312,7 @@ ${helpers.single_keyword( ${helpers.single_keyword( "animation-fill-mode", "none forwards backwards both", - engines="gecko servo-2013", + engines="gecko servo-2013 servo-2020", need_index=True, animation_value_type="none", vector=True, diff --git a/tests/wpt/metadata/css/css-animations/animation-delay-010.html.ini b/tests/wpt/metadata/css/css-animations/animation-delay-010.html.ini index 8f275c978f4..284bbbac898 100644 --- a/tests/wpt/metadata/css/css-animations/animation-delay-010.html.ini +++ b/tests/wpt/metadata/css/css-animations/animation-delay-010.html.ini @@ -1,3 +1,3 @@ [animation-delay-010.html] bug: https://github.com/servo/servo/issues/17335 - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini index 454e1d2dec0..e61911d3325 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini @@ -1,5 +1,2 @@ [variable-animation-substitute-into-keyframe-shorthand.html] bug: https://github.com/servo/servo/issues/21564 - [Verify border-bottom-color before animation] - expected: FAIL - diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini index f4720d8cf96..ccac15c6181 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini @@ -1,5 +1,2 @@ [variable-animation-substitute-into-keyframe.html] bug: https://github.com/servo/servo/issues/21564 - [Verify color before animation] - expected: FAIL - diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini index a0f8055196a..8fe87ce4aa4 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini @@ -1,8 +1,5 @@ [variable-animation-substitute-within-keyframe-fallback.html] bug: https://github.com/servo/servo/issues/21564 - [Verify color before animation] - expected: FAIL - [Verify color after animation] expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini index baa8d60195e..7046292ccd7 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini @@ -1,8 +1,5 @@ [variable-animation-substitute-within-keyframe-multiple.html] bug: https://github.com/servo/servo/issues/21564 - [Verify color before animation] - expected: FAIL - [Verify color after animation] expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini index e1e48b487e4..705cf7ed37a 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini @@ -1,8 +1,5 @@ [variable-animation-substitute-within-keyframe.html] bug: https://github.com/servo/servo/issues/21564 - [Verify color before animation] - expected: FAIL - [Verify color after animation] expected: FAIL diff --git a/tests/wpt/metadata/css/cssom/getComputedStyle-animations-replaced-into-ib-split.html.ini b/tests/wpt/metadata/css/cssom/getComputedStyle-animations-replaced-into-ib-split.html.ini deleted file mode 100644 index ed664c1c84d..00000000000 --- a/tests/wpt/metadata/css/cssom/getComputedStyle-animations-replaced-into-ib-split.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[getComputedStyle-animations-replaced-into-ib-split.html] - [getComputedStyle() should return animation styles for nodes just inserted into the document, even if they're in an IB-split] - expected: FAIL - diff --git a/tests/wpt/mozilla/meta-layout-2020/css/animations/__dir__.ini b/tests/wpt/mozilla/meta-layout-2020/css/animations/__dir__.ini new file mode 100644 index 00000000000..696581b4a4a --- /dev/null +++ b/tests/wpt/mozilla/meta-layout-2020/css/animations/__dir__.ini @@ -0,0 +1,2 @@ +prefs: ["layout.animations.test.enabled:true", + "dom.testbinding.enabled:true"] diff --git a/tests/wpt/mozilla/meta-layout-2020/css/animations/animation-fill-mode.html.ini b/tests/wpt/mozilla/meta-layout-2020/css/animations/animation-fill-mode.html.ini new file mode 100644 index 00000000000..9d52b8c51db --- /dev/null +++ b/tests/wpt/mozilla/meta-layout-2020/css/animations/animation-fill-mode.html.ini @@ -0,0 +1,16 @@ +[animation-fill-mode.html] + [animation-fill-mode: both should function correctly] + expected: FAIL + + [animation-fill-mode: both on animation with multiple iterations] + expected: FAIL + + [animation-fill-mode: forwards should function correctly] + expected: FAIL + + [animation-fill-mode: both on reversed animation] + expected: FAIL + + [animation-fill-mode: backwards should function correctly] + expected: FAIL + diff --git a/tests/wpt/mozilla/meta-layout-2020/css/animations/basic-transition.html.ini b/tests/wpt/mozilla/meta-layout-2020/css/animations/basic-transition.html.ini deleted file mode 100644 index aad401f6822..00000000000 --- a/tests/wpt/mozilla/meta-layout-2020/css/animations/basic-transition.html.ini +++ /dev/null @@ -1,5 +0,0 @@ -[basic-transition.html] - expected: ERROR - [Transition test] - expected: TIMEOUT - diff --git a/tests/wpt/mozilla/meta-layout-2020/css/animations/transition-raf.html.ini b/tests/wpt/mozilla/meta-layout-2020/css/animations/transition-raf.html.ini index 9c4652f5544..cd03f205cae 100644 --- a/tests/wpt/mozilla/meta-layout-2020/css/animations/transition-raf.html.ini +++ b/tests/wpt/mozilla/meta-layout-2020/css/animations/transition-raf.html.ini @@ -1,2 +1,5 @@ [transition-raf.html] - expected: ERROR + expected: TIMEOUT + [Transitions should work during RAF loop] + expected: TIMEOUT + diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 8de2df10dec..fcd1e3da6e3 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -12849,6 +12849,13 @@ }, "css": { "animations": { + "animation-fill-mode.html": [ + "4cfaab9fbce0adccd83f592935e63fa8ff58a1cf", + [ + null, + {} + ] + ], "basic-linear-width.html": [ "634b09dca5924b8bea58ac8532d9d46c20d8a0ad", [ diff --git a/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html b/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html new file mode 100644 index 00000000000..4cfaab9fbce --- /dev/null +++ b/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html @@ -0,0 +1,116 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Animation test: Automated test for animation-fill-mode.</title> +<style> + .target { + width: 50px; + height: 50px; + background: red; + } + + @keyframes width-animation { + from { width: 0px; } + to { width: 500px; } + } + +</style> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body></body> + +<script> +function setAndTriggerAnimationOnElement(fillMode, direction = "normal", iterationCount = "1") { + let element = document.createElement("div"); + element.className = "target"; + + element.style.animationDelay = "1s"; + element.style.animationDirection = direction; + element.style.animationDuration = "1s"; + element.style.animationFillMode = fillMode; + element.style.animationIterationCount = iterationCount; + element.style.animationName = "width-animation"; + element.style.animationTimingFunction = "linear"; + + document.body.appendChild(element); + return element; +} + +function runThroughAnimation(testBinding, element) { + testBinding.advanceClock(1000); + assert_equals(getComputedStyle(element).getPropertyValue("width"), "0px"); + + testBinding.advanceClock(500); + assert_equals(getComputedStyle(element).getPropertyValue("width"), "250px"); + + testBinding.advanceClock(500); + assert_equals(getComputedStyle(element).getPropertyValue("width"), "500px"); +} + +test(function() { + let testBinding = new window.TestBinding(); + let div = setAndTriggerAnimationOnElement("both"); + + // The style should reflect the first and last keyframe of the animation + // before and after the animation runs. + assert_equals(getComputedStyle(div).getPropertyValue("width"), "0px"); + runThroughAnimation(testBinding, div); + testBinding.advanceClock(2000); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "500px"); +}, "animation-fill-mode: both should function correctly"); + +test(function() { + let testBinding = new window.TestBinding(); + let div = setAndTriggerAnimationOnElement("forwards"); + + // The style should reflect the last keyframe of the animation after the animation runs. + assert_equals(getComputedStyle(div).getPropertyValue("width"), "50px"); + runThroughAnimation(testBinding, div); + testBinding.advanceClock(2000); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "500px"); +}, "animation-fill-mode: forwards should function correctly"); + +test(function() { + let testBinding = new window.TestBinding(); + let div = setAndTriggerAnimationOnElement("backwards"); + + // The style should reflect the first keyframe of the animation before the animation runs. + assert_equals(getComputedStyle(div).getPropertyValue("width"), "0px"); + runThroughAnimation(testBinding, div); + testBinding.advanceClock(2000); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "50px"); +}, "animation-fill-mode: backwards should function correctly"); + +test(function() { + let testBinding = new window.TestBinding(); + let div = setAndTriggerAnimationOnElement("both", "reverse"); + + // The style should reflect the first keyframe of the animation before the animation runs. + assert_equals(getComputedStyle(div).getPropertyValue("width"), "500px"); + + testBinding.advanceClock(1000); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "500px"); + + testBinding.advanceClock(500); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "250px"); + + testBinding.advanceClock(500); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "0px"); + + testBinding.advanceClock(2000); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "0px"); +}, "animation-fill-mode: both on reversed animation"); + +test(function() { + let testBinding = new window.TestBinding(); + let div = setAndTriggerAnimationOnElement("both", "normal", "3"); + + assert_equals(getComputedStyle(div).getPropertyValue("width"), "0px"); + runThroughAnimation(testBinding, div); + runThroughAnimation(testBinding, div); + runThroughAnimation(testBinding, div); + testBinding.advanceClock(1000); + assert_equals(getComputedStyle(div).getPropertyValue("width"), "500px"); +}, "animation-fill-mode: both on animation with multiple iterations"); + +</script> |