| <!DOCTYPE html> |
| <meta charset=utf-8> |
| <title>Correctness of worklet animation state when timeline becomes newly |
| active or inactive.</title> |
| <link rel="help" href="https://drafts.css-houdini.org/css-animationworklet/"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/web-animations/testcommon.js"></script> |
| <script src="common.js"></script> |
| <style> |
| .scroller { |
| overflow: auto; |
| height: 100px; |
| width: 100px; |
| } |
| .contents { |
| height: 1000px; |
| width: 100%; |
| } |
| </style> |
| <body> |
| <div id="log"></div> |
| <script> |
| 'use strict'; |
| |
| function createScroller(test) { |
| var scroller = createDiv(test); |
| scroller.innerHTML = "<div class='contents'></div>"; |
| scroller.classList.add('scroller'); |
| return scroller; |
| } |
| |
| function createScrollLinkedWorkletAnimation(test) { |
| const timeline = new ScrollTimeline({ |
| scrollSource: createScroller(test), |
| timeRange: 1000 |
| }); |
| const DURATION = 1000; // ms |
| const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] }; |
| return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test), |
| KEYFRAMES, DURATION), timeline); |
| } |
| |
| setup(setupAndRegisterTests, {explicit_done: true}); |
| |
| function setupAndRegisterTests() { |
| registerPassthroughAnimator().then(() => { |
| |
| promise_test(async t => { |
| const animation = createScrollLinkedWorkletAnimation(t); |
| const scroller = animation.timeline.scrollSource; |
| const target = animation.effect.target; |
| |
| // There is no direct way to control when local times of composited |
| // animations are synced to the main thread. This test uses another |
| // composited worklet animation with an always active timeline as an |
| // indicator of when the sync is ready. The sync is done when animation |
| // effect's output has changed as a result of advancing the timeline. |
| const animationRef = createScrollLinkedWorkletAnimation(t); |
| const scrollerRef = animationRef.timeline.scrollSource; |
| const targetRef = animationRef.effect.target; |
| |
| const maxScroll = scroller.scrollHeight - scroller.clientHeight; |
| const timeRange = animation.timeline.timeRange; |
| scroller.scrollTop = 0.2 * maxScroll; |
| |
| // Make the timeline inactive. |
| scroller.style.display = "none" |
| // Force relayout. |
| scroller.scrollTop; |
| |
| animation.play(); |
| animationRef.play(); |
| assert_equals(animation.currentTime, null, |
| 'Initial current time must be unresolved in idle state.'); |
| assert_equals(animation.startTime, null, |
| 'Initial start time must be unresolved in idle state.'); |
| waitForAnimationFrameWithCondition(_=> { |
| return animation.playState == "running" |
| }); |
| assert_equals(animation.currentTime, null, |
| 'Initial current time must be unresolved in playing state.'); |
| assert_equals(animation.startTime, null, |
| 'Initial start time must be unresolved in playing state.'); |
| |
| scrollerRef.scrollTop = 0.2 * maxScroll; |
| |
| // Wait until local times are synced back to the main thread. |
| await waitForAnimationFrameWithCondition(_ => { |
| return animationRef.effect.getComputedTiming().localTime == 200; |
| }); |
| |
| assert_equals(animation.effect.getComputedTiming().localTime, null, |
| 'The underlying effect local time must be undefined while the ' + |
| 'timeline is inactive.'); |
| |
| // Make the timeline active. |
| scroller.style.display = "" |
| scroller.scrollTop; |
| |
| assert_times_equal(animation.currentTime, 200, |
| 'Current time must be initialized.'); |
| assert_times_equal(animation.startTime, 0, |
| 'Start time must be initialized.'); |
| |
| scrollerRef.scrollTop = 0.4 * maxScroll; |
| // Wait until local times are synced back to the main thread. |
| await waitForAnimationFrameWithCondition(_ => { |
| return animationRef.effect.getComputedTiming().localTime == 400; |
| }); |
| assert_times_equal(animation.effect.getComputedTiming().localTime, 200, |
| 'When the timeline becomes newly active, the underlying effect\'s ' + |
| 'timing should be properly updated.'); |
| |
| // Make the timeline inactive again. |
| scroller.style.display = "none" |
| scroller.scrollTop; |
| |
| assert_times_equal(animation.currentTime, 200, |
| 'Current time must be the previous current time.'); |
| assert_equals(animation.startTime, null, |
| 'Initial start time must be unresolved.'); |
| |
| scrollerRef.scrollTop = 0.6 * maxScroll; |
| scrollerRef.scrollTop; |
| // Wait until local times are synced back to the main thread. |
| await waitForAnimationFrameWithCondition(_ => { |
| return animationRef.effect.getComputedTiming().localTime == 600; |
| }); |
| |
| assert_times_equal(animation.effect.getComputedTiming().localTime, 200, |
| 'When the timeline becomes newly inactive, the underlying effect\'s ' + |
| 'timing should stay unchanged.'); |
| }, 'When timeline time becomes inactive previous current time must be ' + |
| 'the current time and start time unresolved'); |
| done(); |
| }); |
| } |
| </script> |
| </body> |