<!DOCTYPE html> <meta charset=utf-8> <title>Verify timeline time, animation time, effect time, and effect progress for 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: hidden; height: 200px; width: 200px; } .contents { /* Make scroll range 1000 to simplify the math and avoid rounding errors */ height: 1200px; width: 100%; } </style> <div id="log"></div> <script> 'use strict'; // Note: effects are scaled to fill the timeline. // Each entry is [[test input], [test expectations]] // test input = ["description", delay, end_delay, scroll percent] // test expectations = [timeline time, animation current time, // effect local time, effect progress, effect phase, // opacity] /* All interesting transitions: at timeline start before effect delay at effect start in active range at effect end after effect end at timeline end */ const test_cases = [ // Case 1: No delays. // Boundary at end of active phase is inclusive. [ ["at start", 0, 0, 0], [0, 0, 0, 0, "active", 0.3] ], [ ["in active range", 0, 0, 0.50], [50, 50, 50, 0.5, "active", 0.5] ], [ ["at effect end time", 0, 0, 1.0], [100, 100, 100, 1.0, "active", 0.7] ], // Case 2: Positive start delay and no end delay. // Boundary at end of active phase is inclusive. [ ["at timeline start", 500, 0, 0], [0, 0, 0, null, "before", 1] ], [ ["before start delay", 500, 0, 0.25], [25, 25, 25, null, "before", 1] ], [ ["at start delay", 500, 0, 0.5], [50, 50, 50, 0, "active", 0.3] ], [ ["in active range", 500, 0, 0.75], [75, 75, 75, 0.5, "active", 0.5] ], [ ["at effect end time", 500, 0, 1.0], [100, 100, 100, 1.0, "active", 0.7] ], // case 3: No start delay, Positive end delay. // Boundary at end of active phase is exclusive. [ ["at timeline start", 0, 500, 0], [0, 0, 0, 0, "active", 0.3] ], [ ["in active range", 0, 500, 0.25], [25, 25, 25, 0.5, "active", 0.5] ], [ ["at effect end time", 0, 500, 0.5], [50, 50, 50, null, "after", 1.0] ], [ ["after effect end time", 0, 500, 0.75], [75, 75, 75, null, "after", 1.0] ], [ ["at timeline boundary", 0, 500, 1.0], [100, 100, 100, null, "after", 1.0] ], // case 4: Positive start and end delays. // Boundary at end of active phase is exclusive. [ ["at timeline start", 250, 250, 0], [0, 0, 0, null, "before", 1] ], [ ["before start delay", 250, 250, 0.1], [10, 10, 10, null, "before", 1] ], [ ["at start delay", 250, 250, 0.25], [25, 25, 25, 0, "active", 0.3] ], [ ["in active range", 250, 250, 0.5], [50, 50, 50, 0.5, "active", 0.5] ], [ ["at effect end time", 250, 250, 0.75], [75, 75, 75, null, "after", 1.0] ], [ ["after effect end time", 250, 250, 0.9], [90, 90, 90, null, "after", 1.0] ], [ ["at timeline boundary", 250, 250, 1.0], [100, 100, 100, null, "after", 1.0] ], // Case 5: Negative start and end delays. // Effect boundaries are not reachable. [ ["at timeline start", -125, -125, 0], [0, 0, 0, 0.25, "active", 0.4] ], [ ["in active range", -125, -125, 0.5], [50, 50, 50, 0.5, "active", 0.5] ], [ ["at timeline end", -125, -125, 1.0], [100, 100, 100, 0.75, "active", 0.6] ] ]; for (const test_case of test_cases) { const [inputs, expected] = test_case; const [test_name, delay, end_delay, scroll_percentage] = inputs; const description = `Current times and effect phase ${test_name} when` + ` delay = ${delay} and endDelay = ${end_delay} |`; promise_test( create_scroll_timeline_delay_test( delay, end_delay, scroll_percentage, expected), description); } function create_scroll_timeline_delay_test( delay, end_delay, scroll_percentage, expected){ return async t => { const target = createDiv(t); const timeline = createScrollTimeline(t); const effect = new KeyframeEffect( target, { opacity: [0.3, 0.7] }, { duration: 500, delay: delay, endDelay: end_delay } ); const animation = new Animation(effect, timeline); t.add_cleanup(() => { animation.cancel(); }); const scroller = timeline.source; 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_opacity] = expected; assert_percents_equal( animation.timeline.currentTime, expected_timeline_current_time, "timeline current time"); assert_percents_equal( animation.currentTime, expected_animation_current_time, "animation current time"); assert_percents_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); assert_approx_equals( parseFloat(getComputedStyle(target).opacity), expected_opacity, 0.001, 'target opacity'); } } function createKeyframeEffectOpacity(test){ return new KeyframeEffect( createDiv(test), { opacity: [0.3, 0.7] }, { duration: 1000 } ); } function verifyEffectBeforePhase(animation) { // If currentTime is null, we are either idle, or running with an // inactive timeline. Either way, the animation is not in effect and cannot // be in the before phase. assert_true(animation.currentTime != null, 'Animation is not in effect'); const fillMode = animation.effect.getTiming().fill; animation.effect.updateTiming({ fill: 'none' }); // progress == null AND opacity == 1 implies we are in the effect before // or after phase. assert_equals(animation.effect.getComputedTiming().progress, null); assert_equals( window.getComputedStyle(animation.effect.target) .getPropertyValue("opacity"), "1"); // If the progress is no longer null after adding fill: backwards, then we // are in the before phase. animation.effect.updateTiming({ fill: 'backwards' }); assert_true(animation.effect.getComputedTiming().progress != null); assert_equals( window.getComputedStyle(animation.effect.target) .getPropertyValue("opacity"), "0.3"); // Reset fill mode to avoid side-effects. animation.effect.updateTiming({ fill: fillMode }); } function createScrollLinkedOpacityAnimationWithDelays(t) { const animation = new Animation( createKeyframeEffectOpacity(t), createScrollTimeline(t) ); t.add_cleanup(() => { animation.cancel(); }); animation.effect.updateTiming({ duration: 1000, delay: 500, endDelay: 500 }); return animation; } promise_test(async t => { const animation = createScrollLinkedOpacityAnimationWithDelays(t); const scroller = animation.timeline.source; const maxScroll = scroller.scrollHeight - scroller.clientHeight; animation.play(); await animation.ready; verifyEffectBeforePhase(animation); animation.pause(); await waitForNextFrame(); verifyEffectBeforePhase(animation); animation.play(); await waitForNextFrame(); verifyEffectBeforePhase(animation); }, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.'); promise_test(async t => { const animation = createScrollLinkedOpacityAnimationWithDelays(t); const scroller = animation.timeline.source; const maxScroll = scroller.scrollHeight - scroller.clientHeight; animation.play(); await animation.ready; verifyEffectBeforePhase(animation); animation.pause(); await waitForNextFrame(); verifyEffectBeforePhase(animation); // Scrolling should not cause the animation effect to change. scroller.scrollTop = 0.5 * maxScroll; await waitForNextFrame(); // Check timeline phase assert_percents_equal(animation.timeline.currentTime, 50); assert_percents_equal(animation.currentTime, 0); assert_percents_equal(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 = createScrollLinkedOpacityAnimationWithDelays(t); const scroller = animation.timeline.source; const maxScroll = scroller.scrollHeight - scroller.clientHeight; animation.play(); await animation.ready; verifyEffectBeforePhase(animation); animation.pause(); await waitForNextFrame(); verifyEffectBeforePhase(animation); // Setting the current time should force the animation into effect. const expected_time = 50; animation.currentTime = CSS.percent(expected_time); await waitForNextFrame(); assert_percents_equal(animation.timeline.currentTime, 0); assert_percents_equal(animation.currentTime, expected_time, 'Current time matches set value'); assert_percents_equal( animation.effect.getComputedTiming().localTime, expected_time, "Effect local time after setting animation.currentTime"); assert_equals(animation.effect.getComputedTiming().progress, 0.5, "Progress after setting animation.currentTime"); assert_equals( window.getComputedStyle(animation.effect.target) .getPropertyValue("opacity"), "0.5", "Opacity after setting animation.currentTime"); // Scrolling should not cause the animation effect to change since // paused. scroller.scrollTop = 0.75 * maxScroll; // scroll so that timeline is 75% await waitForNextFrame(); assert_percents_equal(animation.timeline.currentTime, 75); // animation and effect timings are unchanged. assert_percents_equal(animation.currentTime, expected_time, "Current time after scrolling while paused"); assert_percents_equal( animation.effect.getComputedTiming().localTime, expected_time, "Effect local time after scrolling while paused"); assert_equals(animation.effect.getComputedTiming().progress, 0.5, "Progress after scrolling while paused"); assert_equals( window.getComputedStyle(animation.effect.target) .getPropertyValue("opacity"), "0.5", "Opacity after scrolling while paused"); }, 'Pause in before phase, set animation current time to be in active ' + 'range, animation should become active. Scrolling should have no effect.'); promise_test(async t => { const animation = createScrollLinkedOpacityAnimationWithDelays(t); const scroller = animation.timeline.source; 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(); // Verify that he timeline is inactive assert_equals(animation.timeline.currentTime, null, "Timeline is inactive"); assert_equals( animation.currentTime, null, "Current time for running animation with an inactive timeline"); assert_equals(animation.effect.getComputedTiming().localTime, null, "effect local time with inactive timeline"); // Setting the current time while timeline is inactive should pause the // animation at the specified time. animation.currentTime = CSS.percent(50); await waitForNextFrame(); await waitForNextFrame(); // Verify that animation currentTime is properly set despite the inactive // timeline. assert_equals(animation.timeline.currentTime, null); assert_percents_equal(animation.currentTime, 50); assert_percents_equal(animation.effect.getComputedTiming().localTime, 50, "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 = createScrollLinkedOpacityAnimationWithDelays(t); const scroller = animation.timeline.source; const maxScroll = scroller.scrollHeight - scroller.clientHeight; scroller.scrollTop = 0.5 * maxScroll; // Update timeline.currentTime. 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.'); promise_test(async t => { const animation = createScrollLinkedOpacityAnimationWithDelays(t); const scroller = animation.timeline.source; const maxScroll = scroller.scrollHeight - scroller.clientHeight; animation.play(); await animation.ready; // cause the timeline to become inactive scroller.style.overflow = 'visible'; scroller.scrollTop; animation.pause(); }, 'Make timeline inactive, force style update then pause the animation. ' + 'No crashing indicates test success.'); </script>