|  | <!DOCTYPE html> | 
|  | <meta charset=utf-8> | 
|  | <title>Test clamping logic of element-based scroll offset for scroll timeline.</title> | 
|  | <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: scroll; | 
|  | height: 500px; | 
|  | width: 500px; | 
|  | will-change: transform; | 
|  | } | 
|  |  | 
|  | /* Disable scrollbars to simplify the calculations in the test. */ | 
|  | .scroller { | 
|  | scrollbar-width: 0; | 
|  | } | 
|  | /* | 
|  | Chrome does not support scrollbar-width so we use this non-standard property | 
|  | until it does. | 
|  | */ | 
|  | .scroller::-webkit-scrollbar { | 
|  | display: none; | 
|  | } | 
|  |  | 
|  | .contents { | 
|  | height: 1200px; | 
|  | width: 1200px; | 
|  | position: relative; | 
|  | } | 
|  |  | 
|  | .vertical #target { | 
|  | background: blue; | 
|  | border-top: 0px solid pink; | 
|  | border-bottom: 0px solid pink; | 
|  | box-sizing: border-box; | 
|  | position: absolute; | 
|  | width: 100%; | 
|  | top: var(--start-position); | 
|  | height: calc(var(--end-position) - var(--start-position)); | 
|  | } | 
|  |  | 
|  | .horizontal #target { | 
|  | background: blue; | 
|  | border-left: 0px solid pink; | 
|  | border-right: 0px solid pink; | 
|  | box-sizing: border-box; | 
|  | position: absolute; | 
|  | height: 100%; | 
|  | left: var(--start-position); | 
|  | width: calc(var(--end-position) - var(--start-position)); | 
|  | } | 
|  | </style> | 
|  | <div id="log"></div> | 
|  | <script> | 
|  | 'use strict'; | 
|  |  | 
|  | function createScrollerWithTarget(test, config) { | 
|  | const orientationClass = config.orientation; | 
|  | const positions = ` | 
|  | --start-position: ${config.startElementPosition}; | 
|  | --end-position: ${config.endElementPosition};` | 
|  |  | 
|  | var scroller = createDiv(test); | 
|  | scroller.innerHTML = | 
|  | `<div class='contents' style="${positions}"> | 
|  | <div id='target'></div> | 
|  | </div>`; | 
|  | scroller.classList.add('scroller'); | 
|  | scroller.classList.add(orientationClass); | 
|  |  | 
|  | return scroller; | 
|  | } | 
|  |  | 
|  | async function createScrollAnimationTest(description, config) { | 
|  | promise_test(async t => { | 
|  | const scroller = createScrollerWithTarget(t, config); | 
|  | t.add_cleanup(() => scroller.remove()); | 
|  |  | 
|  | const target = scroller.querySelector("#target"); | 
|  |  | 
|  | const timeline = createScrollTimeline(t, { | 
|  | scrollSource: scroller, | 
|  | orientation: config.orientation, | 
|  | timeRange: 1000, | 
|  | fill: 'both', | 
|  | startScrollOffset: {target: target, edge: 'end', ...config.start}, | 
|  | endScrollOffset: {target: target, edge:'start', ...config.end } | 
|  | }); | 
|  |  | 
|  | // Wait for new animation frame which allows the timeline to compute new | 
|  | // current time. | 
|  | await waitForNextFrame(); | 
|  |  | 
|  | const animation = createScrollLinkedAnimation(t, timeline); | 
|  | const timeRange = animation.timeline.timeRange; | 
|  |  | 
|  | // Verify initial start and current times in Idle state. | 
|  | assert_equals(animation.currentTime, null, | 
|  | "The current time is null in Idle state."); | 
|  | assert_equals(animation.startTime, null, | 
|  | "The start time is null in Idle state."); | 
|  |  | 
|  | animation.play(); | 
|  | assert_true(animation.pending, "Animation is in pending state."); | 
|  | // Verify initial start and current times in Pending state. | 
|  | assert_times_equal(animation.currentTime, 0, | 
|  | "The current time is zero in Pending state."); | 
|  | assert_equals(animation.startTime, 0, | 
|  | "The start time is zero in Pending state."); | 
|  |  | 
|  | await animation.ready; | 
|  | // Verify initial start and current times in Playing state. | 
|  | assert_times_equal(animation.currentTime, 0, | 
|  | "The current time is zero in Playing state."); | 
|  | assert_times_equal(animation.startTime, 0, | 
|  | "The start time is zero in Playing state."); | 
|  |  | 
|  | // Now do some scrolling and make sure that the Animation current time is | 
|  | // correct. | 
|  | if (config.orientation == 'vertical') { | 
|  | scroller.scrollTo({top: config.scrollTo}); | 
|  | assert_equals(scroller.scrollTop, config.scrollTo); | 
|  | } else { | 
|  | scroller.scrollTo({left: config.scrollTo}); | 
|  | assert_equals(scroller.scrollLeft, config.scrollTo); | 
|  | } | 
|  |  | 
|  | await waitForNextFrame(); | 
|  |  | 
|  | assert_times_equal(animation.timeline.currentTime, config.expectedCurrentTime, | 
|  | "The timeline current time corresponds to the scroll position of the scroller."); | 
|  | assert_times_equal(animation.currentTime, config.expectedCurrentTime, | 
|  | "The animation current time corresponds to the scroll position of the scroller."); | 
|  | assert_times_equal( | 
|  | animation.effect.getComputedTiming().localTime, | 
|  | config.expectedCurrentTime, | 
|  | 'Effect local time corresponds to the scroll position of the scroller.'); | 
|  | }, description); | 
|  | } | 
|  |  | 
|  | // We have no scrollbar and the scroller is symmetric on x & y axis so this | 
|  | // static value is axis and platform agnostic. | 
|  | const scroll_max = 700; | 
|  |  | 
|  | // For this test we setup a single target, and scroll timeline in a way that | 
|  | // our animation runs from when target enters the scroll port until it fully | 
|  | // exits it. Then we create various edgecase scenarios to see the clamping | 
|  | // logic. | 
|  | // | 
|  | // Scroller has 500px heights with 1200px content which translates to | 
|  | // 0 < scroll < 700px | 
|  | // | 
|  | // +----------+ ^ | 
|  | // | | | | 
|  | // | Scroller | | | 
|  | // | | | scrollRange | 
|  | // | | | | 
|  | // +----------+ | +--+ | 
|  | // |TT| | |TT| | 
|  | // +--+ v +----------+ | 
|  | // | | | 
|  | // | Scroller | | 
|  | // | | | 
|  | // | | | 
|  | // +----------+ | 
|  | // | 
|  | // For each test the expected timeline start/end is in the comment to help | 
|  | // with the verification. | 
|  | const tests = { | 
|  | // offsets: [0, 600] | 
|  | "no clamping is expected": { | 
|  | startElementPosition: '500px', | 
|  | endElementPosition: '600px', | 
|  | scrollTo: 300, | 
|  | expectedCurrentTime: 500, | 
|  | }, | 
|  | // offsets: [0, 600] | 
|  | "start is visible at zero offset and should get clamped": { | 
|  | startElementPosition: '400px', | 
|  | endElementPosition: '600px', | 
|  | scrollTo: 300, | 
|  | expectedCurrentTime: 500, | 
|  | }, | 
|  |  | 
|  | // offsets: [0, scroll_max] | 
|  | "end is not reachable and should be clamped": { | 
|  | startElementPosition: '500px', | 
|  | endElementPosition: '800px', | 
|  | scrollTo: scroll_max / 2, | 
|  | expectedCurrentTime: 500, | 
|  | }, | 
|  |  | 
|  | // offsets: [0, scroll_max] | 
|  | "both start and end are clamped": { | 
|  | startElementPosition: '400px', | 
|  | endElementPosition: '800px', | 
|  | scrollTo: scroll_max / 2, | 
|  | expectedCurrentTime: 500, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | for (let orientation of ['vertical', 'horizontal']) { | 
|  | for (let testName in tests) { | 
|  | const description = `Animation start and current times are correct given | 
|  | element-based offsets for orienation ${orientation} and ${testName}.`; | 
|  | const config = tests[testName]; | 
|  | config.orientation = orientation; | 
|  | createScrollAnimationTest(description, config); | 
|  | } | 
|  | } | 
|  | </script> |