1# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from telemetry.internal.actions import page_action
6from telemetry.internal.actions.scroll import ScrollAction
7from telemetry.util import js_template
8
9
10class ScrollToElementAction(page_action.PageAction):
11
12
13  def __init__(self, selector=None, element_function=None,
14               container_selector=None, container_element_function=None,
15               speed_in_pixels_per_second=800):
16    """Perform scroll gesture on container until an element is in view.
17
18    Both the element and the container can be specified by a CSS selector
19    xor a JavaScript function, provided as a string, which returns an element.
20    The element is required so exactly one of selector and element_function
21    must be provided. The container is optional so at most one of
22    container_selector and container_element_function can be provided.
23    The container defaults to document.scrollingElement or document.body if
24    scrollingElement is not set.
25
26    Args:
27      selector: A CSS selector describing the element.
28      element_function: A JavaScript function (as string) that is used
29          to retrieve the element. For example:
30          'function() { return foo.element; }'.
31      container_selector: A CSS selector describing the container element.
32      container_element_function: A JavaScript function (as a string) that is
33          used to retrieve the container element.
34      speed_in_pixels_per_second: Speed to scroll.
35    """
36    super(ScrollToElementAction, self).__init__()
37    self._selector = selector
38    self._element_function = element_function
39    self._container_selector = container_selector
40    self._container_element_function = container_element_function
41    self._speed = speed_in_pixels_per_second
42    self._distance = None
43    self._direction = None
44    self._scroller = None
45    assert (self._selector or self._element_function), (
46        'Must have either selector or element function')
47
48  def WillRunAction(self, tab):
49    if self._selector:
50      element = js_template.Render(
51          'document.querySelector({{ selector }})', selector=self._selector)
52    else:
53      element = self._element_function
54
55    self._distance = tab.EvaluateJavaScript('''
56        (function(elem){
57          var rect = elem.getBoundingClientRect();
58          if (rect.bottom < 0) {
59            // The bottom of the element is above the viewport.
60            // Scroll up until the top of the element is on screen.
61            return rect.top - (window.innerHeight / 2);
62          }
63          if (rect.top - window.innerHeight >= 0) {
64            // rect.top provides the pixel offset of the element from the
65            // top of the page. Because that exceeds the viewport's height,
66            // we know that the element is below the viewport.
67            return rect.top - (window.innerHeight / 2);
68          }
69          return 0;
70        })({{ @element }});
71        ''', element=element)
72    self._direction = 'down' if self._distance > 0 else 'up'
73    self._distance = abs(self._distance)
74    self._scroller = ScrollAction(
75        direction=self._direction,
76        selector=self._container_selector,
77        element_function=self._container_element_function,
78        distance=self._distance,
79        speed_in_pixels_per_second=self._speed)
80
81  def RunAction(self, tab):
82    if self._distance == 0:  # Element is already in view.
83      return
84    self._scroller.WillRunAction(tab)
85    self._scroller.RunAction(tab)
86