| <!DOCTYPE html> |
| <meta charset=utf-8> |
| <title>Verify timeline time, animation time, effect time and effect value for all fill modes in all timeline states: before start, at start, in range, at end, after end while using various effect delay values</title> |
| <meta name="timeout" content="long"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/web-animations/testcommon.js"></script> |
| <script src="testcommon.js"></script> |
| <style> |
| .scroller { |
| overflow: auto; |
| height: 100px; |
| width: 100px; |
| } |
| .contents { |
| height: 1000px; |
| width: 100%; |
| } |
| </style> |
| <div id="log"></div> |
| <script> |
| 'use strict'; |
| |
| // Test cases are included where effect delay causes the effect iteration to |
| // overlap with the timeline start time and also the timeline end time. |
| // Timeline |
| // BEFORE +-----------------+ AFTER |
| // time: 0 timeRange |
| // 1) +--------+ |
| // 2) +-------+ |
| // 3) +------+ |
| // 4) +------+ |
| |
| // Each entry is [[test input], [test expectations]] |
| // test input = ["description", delay, scroll percent] |
| // test expectations = [timeline time, animation current time, |
| // effect local time, effect progress, effect phase] |
| const test_cases = [ |
| // Case 1: No delay. Effect starts at the same time as the timeline. |
| [["before timeline start", 0, 0.1 ], [0, 0, 0, null, "before"]], |
| [["at timeline start", 0, 0.2 ], [0, 0, 0, 0, "active"]], |
| [["in timeline range", 0, 0.5 ], [500, 500, 500, 0.833, "active"]], |
| |
| // Case 2: Positive delay. |
| [["before timeline start", 100, 0.1 ], [0, 0, 0, null, "before"]], |
| [["at timeline start", 100, 0.2 ], [0, 0, 0, null, "before"]], |
| [["before effect delay", 100, 0.25], [83.333, 83.333, 83.333, null, "before"]], |
| [["at effect start", 100, 0.26], [100, 100, 100, 0, "active"]], |
| [["in timeline range", 100, 0.5 ], [500, 500, 500, 0.666, "active"]], |
| [["at effect end", 100, 0.62], [700, 700, 700, null, "after"]], |
| [["after effect end", 100, 0.65], [750, 700, 700, null, "after"]], |
| [["at timeline end", 100, 0.8 ], [1000, 700, 700, null, "after"]], |
| [["after timeline end", 100, 0.9 ], [1000, 700, 700, null, "after"]], |
| |
| // Case 3: Negative delay. |
| // Can't test values for "before effect delay" and "at effect start" because |
| // they occur before the timeline start and are therefore unreachable. |
| [["before timeline start", -100, 0.1 ], [0, 0, 0, null, "before"]], |
| [["at timeline start", -100, 0.2 ], [0, 0, 0, 0.166, "active"]], |
| [["in timeline range", -100, 0.3 ], [166.666, 166.666, 166.666, 0.444, "active"]], |
| [["at effect end", -100, 0.5 ], [500, 500, 500, null, "after"]], |
| [["after effect end", -100, 0.51], [516.666, 500, 500, null, "after"]], |
| [["at timeline end", -100, 0.8 ], [1000, 500, 500, null, "after"]], |
| [["after timeline end", -100, 0.9 ], [1000, 500, 500, null, "after"]], |
| |
| // Case 4: Effect delay is large enough to cause the effect to not finish |
| // before the timeline. |
| [["before timeline start", 500, 0.1 ], [0, 0, 0, null, "before"]], |
| [["at timeline start", 500, 0.2 ], [0, 0, 0, null, "before"]], |
| [["before effect delay", 500, 0.4 ], [333.333, 333.333, 333.333, null, "before"]], |
| [["at effect start", 500, 0.5 ], [500, 500, 500, 0, "active"]], |
| [["in timeline range", 500, 0.65], [750, 750, 750, 0.416, "active"]], |
| [["at timeline end", 500, 0.8 ], [1000, 1000, 1000, 0.833, "active"]], |
| [["after timeline end", 500, 0.9 ], [1000, 1000, 1000, 0.833, "active"]], |
| // Can't scroll past the end of the scroller and therefore cannot reach the |
| // effect end, so "at effect end" and "after effect end" states are not |
| // included. |
| ]; |
| |
| for (const test_case of test_cases) { |
| const [inputs, expected] = test_case; |
| const [test_name, delay, scroll_percentage] = inputs; |
| |
| const description = `Current times and effect phase ${test_name} when` + |
| ` delay = ${delay} |`; |
| |
| promise_test( |
| create_scroll_timeline_fill_test(delay, scroll_percentage, expected), |
| description); |
| } |
| |
| function create_scroll_timeline_fill_test(delay, scroll_percentage, expected){ |
| return async t => { |
| const target = createDiv(t); |
| const timeline = createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80)); |
| const effect = new KeyframeEffect( |
| target, |
| { |
| opacity: [0.3, 0.7] |
| }, |
| { |
| duration: 600, |
| delay: delay |
| } |
| ); |
| const animation = new Animation(effect, timeline); |
| const scroller = timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| |
| await animation.ready; |
| |
| scroller.scrollTop = scroll_percentage * maxScroll; |
| |
| // Wait for new animation frame which allows the timeline to compute |
| // new current time. |
| await waitForNextFrame(); |
| |
| const [expected_timeline_current_time, |
| expected_animation_current_time, |
| expected_effect_local_time, |
| expected_effect_progress, |
| expected_effect_phase] = expected; |
| |
| assert_times_equal( |
| animation.timeline.currentTime, |
| expected_timeline_current_time, |
| "timeline current time" |
| ); |
| assert_times_equal( |
| animation.currentTime, |
| expected_animation_current_time, |
| "animation current time" |
| ); |
| assert_times_equal( |
| animation.effect.getComputedTiming().localTime, |
| expected_effect_local_time, |
| "animation effect local time" |
| ); |
| assert_approx_equals_or_null( |
| animation.effect.getComputedTiming().progress, |
| expected_effect_progress, |
| 0.001, |
| "animation effect progress" |
| ); |
| assert_phase_at_time( |
| animation, |
| expected_effect_phase, |
| animation.currentTime |
| ); |
| } |
| } |
| |
| function createKeyframeEffectOpacity(test){ |
| return new KeyframeEffect( |
| createDiv(test), |
| { |
| opacity: [0.3, 0.7] |
| }, |
| { |
| duration: 1000 |
| } |
| ); |
| } |
| |
| function verifyTimelineBeforePhase(animation){ |
| assert_equals(animation.timeline.phase, "before"); |
| assert_equals(animation.timeline.currentTime, 0); |
| assert_equals(animation.currentTime, 0); |
| assert_equals( |
| animation.effect.getComputedTiming().localTime, |
| 0, |
| "effect local time in timeline before phase"); |
| } |
| |
| function verifyEffectBeforePhase(animation){ |
| // progress == null AND opacity == 1 implies we are in the effect before |
| // phase |
| assert_equals( |
| animation.effect.getComputedTiming().progress, |
| null |
| ); |
| assert_equals( |
| window.getComputedStyle(animation.effect.target).getPropertyValue("opacity"), |
| "1" |
| ); |
| } |
| |
| promise_test(async t => { |
| const animation = new Animation( |
| createKeyframeEffectOpacity(t), |
| createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80)) |
| ); |
| |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| await animation.ready; |
| |
| verifyTimelineBeforePhase(animation); |
| verifyEffectBeforePhase(animation); |
| |
| animation.pause(); |
| await waitForNextFrame(); |
| |
| verifyTimelineBeforePhase(animation); |
| verifyEffectBeforePhase(animation); |
| |
| animation.play(); |
| await waitForNextFrame(); |
| |
| verifyTimelineBeforePhase(animation); |
| verifyEffectBeforePhase(animation); |
| }, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.'); |
| |
| promise_test(async t => { |
| const animation = new Animation( |
| createKeyframeEffectOpacity(t), |
| createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80)) |
| ); |
| |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| await animation.ready; |
| |
| verifyTimelineBeforePhase(animation); |
| verifyEffectBeforePhase(animation); |
| |
| animation.pause(); |
| await waitForNextFrame(); |
| |
| verifyTimelineBeforePhase(animation); |
| verifyEffectBeforePhase(animation); |
| |
| // Scrolling should not cause the animation effect to change. |
| scroller.scrollTop = 0.5 * maxScroll; |
| await waitForNextFrame(); |
| |
| // Check timeline phase |
| assert_equals(animation.timeline.phase, "active"); |
| assert_equals(animation.timeline.currentTime, 500); |
| assert_equals(animation.currentTime, 0); |
| assert_equals( |
| animation.effect.getComputedTiming().localTime, |
| 0, |
| "effect local time" |
| ); |
| |
| // Make sure the effect is still in the before phase even though the |
| // timeline is not. |
| verifyEffectBeforePhase(animation); |
| }, 'Pause in before phase, scroll timeline into active phase, animation ' + |
| 'should remain in the before phase'); |
| |
| promise_test(async t => { |
| const animation = new Animation( |
| createKeyframeEffectOpacity(t), |
| createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80)) |
| ); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| |
| animation.play(); |
| await animation.ready; |
| |
| // Causes the timeline to be inactive |
| scroller.style.overflow = "visible"; |
| await waitForNextFrame(); |
| await waitForNextFrame(); |
| |
| // Check timeline phase |
| assert_equals(animation.timeline.phase, "inactive"); |
| assert_equals(animation.timeline.currentTime, null); |
| assert_equals(animation.currentTime, null); |
| assert_equals( |
| animation.effect.getComputedTiming().localTime, |
| null, |
| "effect local time with inactive timeline" |
| ); |
| |
| verifyEffectBeforePhase(animation); |
| |
| // Setting the current time while timeline is inactive should cause hold phase |
| // and hold time to be populated |
| animation.currentTime = 500; |
| await waitForNextFrame(); |
| await waitForNextFrame(); |
| |
| // Check timeline phase |
| assert_equals(animation.timeline.phase, "inactive"); |
| assert_equals(animation.timeline.currentTime, null); |
| assert_equals(animation.currentTime, 500); |
| assert_equals( |
| animation.effect.getComputedTiming().localTime, |
| 500, |
| "effect local time after setting animation current time" |
| ); |
| |
| // Check effect phase |
| // progress == 0.5 AND opacity == 0.5 shows we are in the effect active phase |
| assert_equals( |
| animation.effect.getComputedTiming().progress, |
| 0.5, |
| "effect progress" |
| ); |
| assert_equals( |
| window.getComputedStyle(animation.effect.target).getPropertyValue("opacity"), |
| "0.5", |
| "effect opacity after setting animation current time" |
| ); |
| }, 'Make scroller inactive, then set current time to an in range time'); |
| |
| promise_test(async t => { |
| const animation = new Animation( |
| createKeyframeEffectOpacity(t), |
| createScrollTimelineWithOffsets(t, CSS.percent(20), CSS.percent(80)) |
| ); |
| const scroller = animation.timeline.scrollSource; |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| scroller.scrollTop = 0.5 * maxScroll; |
| // allow the scroll to finish. |
| await waitForNextFrame(); |
| |
| animation.pause(); |
| await animation.ready; |
| // verify effect is applied. |
| const expected_progress = 0.5; |
| assert_equals( |
| animation.effect.getComputedTiming().progress, |
| expected_progress, |
| "Verify effect progress after pausing." |
| ); |
| |
| // cause the timeline to become inactive |
| scroller.style.overflow = 'visible'; |
| await waitForAnimationFrames(2); |
| assert_equals(animation.timeline.currentTime, null, |
| 'Sanity check the timeline is inactive.'); |
| assert_equals( |
| animation.effect.getComputedTiming().progress, |
| expected_progress, |
| "Verify effect progress after the timeline goes inactive." |
| ); |
| }, 'Animation effect is still applied after pausing and making timeline ' + |
| 'inactive.'); |
| |
| </script> |