|  | <!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), | 
|  | }); | 
|  | 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; | 
|  | 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 = ""; | 
|  | // Wait for new animation frame  which allows the timeline to compute new | 
|  | // current time. | 
|  | await waitForNextFrame(); | 
|  |  | 
|  | 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" | 
|  | await waitForNextFrame(); | 
|  |  | 
|  | 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; | 
|  | // 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> |