1# Copyright 2014 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
5import time
6
7from telemetry.page.actions.javascript_click import ClickElementAction
8from telemetry.page.actions.loop import LoopAction
9from telemetry.page.actions.navigate import NavigateAction
10from telemetry.page.actions.pinch import PinchAction
11from telemetry.page.actions.play import PlayAction
12from telemetry.page.actions.repaint_continuously import (
13    RepaintContinuouslyAction)
14from telemetry.page.actions.scroll import ScrollAction
15from telemetry.page.actions.scroll_bounce import ScrollBounceAction
16from telemetry.page.actions.seek import SeekAction
17from telemetry.page.actions.swipe import SwipeAction
18from telemetry.page.actions.tap import TapAction
19from telemetry.page.actions.wait import WaitForElementAction
20from telemetry.web_perf import timeline_interaction_record
21
22
23class ActionRunner(object):
24
25  def __init__(self, tab, skip_waits=False):
26    self._tab = tab
27    self._skip_waits = skip_waits
28
29  def _RunAction(self, action):
30    action.WillRunAction(self._tab)
31    action.RunAction(self._tab)
32
33  def BeginInteraction(self, label, is_fast=False, is_smooth=False,
34                       is_responsive=False, repeatable=False):
35    """Marks the beginning of an interaction record.
36
37    An interaction record is a labeled time period containing
38    interaction that developers care about. Each set of metrics
39    specified in flags will be calculated for this time period.. The
40    End() method in the returned object must be called once to mark
41    the end of the timeline.
42
43    Args:
44      label: A label for this particular interaction. This can be any
45          user-defined string, but must not contain '/'.
46      is_fast: Whether to measure how fast the browser completes necessary work
47          for this interaction record. See fast_metric.py for details.
48      is_smooth: Whether to check for smoothness metrics for this interaction.
49      is_responsive: Whether to check for responsiveness metrics for
50          this interaction.
51      repeatable: Whether other interactions may use the same logical name
52          as this interaction. All interactions with the same logical name must
53          have the same flags.
54    """
55    flags = []
56    if is_fast:
57      flags.append(timeline_interaction_record.IS_FAST)
58    if is_smooth:
59      flags.append(timeline_interaction_record.IS_SMOOTH)
60    if is_responsive:
61      flags.append(timeline_interaction_record.IS_RESPONSIVE)
62    if repeatable:
63      flags.append(timeline_interaction_record.REPEATABLE)
64
65    interaction = Interaction(self._tab, label, flags)
66    interaction.Begin()
67    return interaction
68
69  def BeginGestureInteraction(self, label, is_fast=False, is_smooth=False,
70                              is_responsive=False, repeatable=False):
71    """Marks the beginning of a gesture-based interaction record.
72
73    This is similar to normal interaction record, but it will
74    auto-narrow the interaction time period to only include the
75    synthetic gesture event output by Chrome. This is typically use to
76    reduce noise in gesture-based analysis (e.g., analysis for a
77    swipe/scroll).
78
79    The interaction record label will be prepended with 'Gesture_'.
80
81    Args:
82      label: A label for this particular interaction. This can be any
83          user-defined string, but must not contain '/'.
84      is_fast: Whether to measure how fast the browser completes necessary work
85          for this interaction record. See fast_metric.py for details.
86      is_smooth: Whether to check for smoothness metrics for this interaction.
87      is_responsive: Whether to check for responsiveness metrics for
88          this interaction.
89      repeatable: Whether other interactions may use the same logical name
90          as this interaction. All interactions with the same logical name must
91          have the same flags.
92    """
93    return self.BeginInteraction('Gesture_' + label, is_fast, is_smooth,
94                                 is_responsive, repeatable)
95
96  def NavigateToPage(self, page, timeout_in_seconds=60):
97    """Navigate to the given page.
98
99    Args:
100      page: page is an instance of page.Page
101      timeout_in_seconds: The timeout in seconds (default to 60).
102    """
103    if page.is_file:
104      target_side_url = self._tab.browser.http_server.UrlOf(page.file_path_url)
105    else:
106      target_side_url = page.url
107    self._RunAction(NavigateAction(
108        url=target_side_url,
109        script_to_evaluate_on_commit=page.script_to_evaluate_on_commit,
110        timeout_in_seconds=timeout_in_seconds))
111
112  def WaitForNavigate(self, timeout_in_seconds_seconds=60):
113    self._tab.WaitForNavigate(timeout_in_seconds_seconds)
114    self._tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
115
116  def ReloadPage(self):
117    """Reloads the page."""
118    self._tab.ExecuteJavaScript('window.location.reload()')
119    self._tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
120
121  def ExecuteJavaScript(self, statement):
122    """Executes a given JavaScript expression. Does not return the result.
123
124    Example: runner.ExecuteJavaScript('var foo = 1;');
125
126    Args:
127      statement: The statement to execute (provided as string).
128
129    Raises:
130      EvaluationException: The statement failed to execute.
131    """
132    self._tab.ExecuteJavaScript(statement)
133
134  def EvaluateJavaScript(self, expression):
135    """Returns the evaluation result of the given JavaScript expression.
136
137    The evaluation results must be convertible to JSON. If the result
138    is not needed, use ExecuteJavaScript instead.
139
140    Example: num = runner.EvaluateJavaScript('document.location.href')
141
142    Args:
143      expression: The expression to evaluate (provided as string).
144
145    Raises:
146      EvaluationException: The statement expression failed to execute
147          or the evaluation result can not be JSON-ized.
148    """
149    return self._tab.EvaluateJavaScript(expression)
150
151  def Wait(self, seconds):
152    """Wait for the number of seconds specified.
153
154    Args:
155      seconds: The number of seconds to wait.
156    """
157    if not self._skip_waits:
158      time.sleep(seconds)
159
160  def WaitForJavaScriptCondition(self, condition, timeout_in_seconds=60):
161    """Wait for a JavaScript condition to become true.
162
163    Example: runner.WaitForJavaScriptCondition('window.foo == 10');
164
165    Args:
166      condition: The JavaScript condition (as string).
167      timeout_in_seconds: The timeout in seconds (default to 60).
168    """
169    self._tab.WaitForJavaScriptExpression(condition, timeout_in_seconds)
170
171  def WaitForElement(self, selector=None, text=None, element_function=None,
172                     timeout_in_seconds=60):
173    """Wait for an element to appear in the document.
174
175    The element may be selected via selector, text, or element_function.
176    Only one of these arguments must be specified.
177
178    Args:
179      selector: A CSS selector describing the element.
180      text: The element must contains this exact text.
181      element_function: A JavaScript function (as string) that is used
182          to retrieve the element. For example:
183          '(function() { return foo.element; })()'.
184      timeout_in_seconds: The timeout in seconds (default to 60).
185    """
186    self._RunAction(WaitForElementAction(
187        selector=selector, text=text, element_function=element_function,
188        timeout_in_seconds=timeout_in_seconds))
189
190  def TapElement(self, selector=None, text=None, element_function=None):
191    """Tap an element.
192
193    The element may be selected via selector, text, or element_function.
194    Only one of these arguments must be specified.
195
196    Args:
197      selector: A CSS selector describing the element.
198      text: The element must contains this exact text.
199      element_function: A JavaScript function (as string) that is used
200          to retrieve the element. For example:
201          '(function() { return foo.element; })()'.
202    """
203    self._RunAction(TapAction(
204        selector=selector, text=text, element_function=element_function))
205
206  def ClickElement(self, selector=None, text=None, element_function=None):
207    """Click an element.
208
209    The element may be selected via selector, text, or element_function.
210    Only one of these arguments must be specified.
211
212    Args:
213      selector: A CSS selector describing the element.
214      text: The element must contains this exact text.
215      element_function: A JavaScript function (as string) that is used
216          to retrieve the element. For example:
217          '(function() { return foo.element; })()'.
218    """
219    self._RunAction(ClickElementAction(
220        selector=selector, text=text, element_function=element_function))
221
222  def PinchPage(self, left_anchor_ratio=0.5, top_anchor_ratio=0.5,
223                scale_factor=None, speed_in_pixels_per_second=800):
224    """Perform the pinch gesture on the page.
225
226    It computes the pinch gesture automatically based on the anchor
227    coordinate and the scale factor. The scale factor is the ratio of
228    of the final span and the initial span of the gesture.
229
230    Args:
231      left_anchor_ratio: The horizontal pinch anchor coordinate of the
232          gesture, as a ratio of the visible bounding rectangle for
233          document.body.
234      top_anchor_ratio: The vertical pinch anchor coordinate of the
235          gesture, as a ratio of the visible bounding rectangle for
236          document.body.
237      scale_factor: The ratio of the final span to the initial span.
238          The default scale factor is
239          3.0 / (window.outerWidth/window.innerWidth).
240      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
241    """
242    self._RunAction(PinchAction(
243        left_anchor_ratio=left_anchor_ratio, top_anchor_ratio=top_anchor_ratio,
244        scale_factor=scale_factor,
245        speed_in_pixels_per_second=speed_in_pixels_per_second))
246
247  def PinchElement(self, selector=None, text=None, element_function=None,
248                   left_anchor_ratio=0.5, top_anchor_ratio=0.5,
249                   scale_factor=None, speed_in_pixels_per_second=800):
250    """Perform the pinch gesture on an element.
251
252    It computes the pinch gesture automatically based on the anchor
253    coordinate and the scale factor. The scale factor is the ratio of
254    of the final span and the initial span of the gesture.
255
256    Args:
257      selector: A CSS selector describing the element.
258      text: The element must contains this exact text.
259      element_function: A JavaScript function (as string) that is used
260          to retrieve the element. For example:
261          'function() { return foo.element; }'.
262      left_anchor_ratio: The horizontal pinch anchor coordinate of the
263          gesture, as a ratio of the visible bounding rectangle for
264          the element.
265      top_anchor_ratio: The vertical pinch anchor coordinate of the
266          gesture, as a ratio of the visible bounding rectangle for
267          the element.
268      scale_factor: The ratio of the final span to the initial span.
269          The default scale factor is
270          3.0 / (window.outerWidth/window.innerWidth).
271      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
272    """
273    self._RunAction(PinchAction(
274        selector=selector, text=text, element_function=element_function,
275        left_anchor_ratio=left_anchor_ratio, top_anchor_ratio=top_anchor_ratio,
276        scale_factor=scale_factor,
277        speed_in_pixels_per_second=speed_in_pixels_per_second))
278
279  def ScrollPage(self, left_start_ratio=0.5, top_start_ratio=0.5,
280                 direction='down', distance=None, distance_expr=None,
281                 speed_in_pixels_per_second=800, use_touch=False):
282    """Perform scroll gesture on the page.
283
284    You may specify distance or distance_expr, but not both. If
285    neither is specified, the default scroll distance is variable
286    depending on direction (see scroll.js for full implementation).
287
288    Args:
289      left_start_ratio: The horizontal starting coordinate of the
290          gesture, as a ratio of the visible bounding rectangle for
291          document.body.
292      top_start_ratio: The vertical starting coordinate of the
293          gesture, as a ratio of the visible bounding rectangle for
294          document.body.
295      direction: The direction of scroll, either 'left', 'right',
296          'up', or 'down'
297      distance: The distance to scroll (in pixel).
298      distance_expr: A JavaScript expression (as string) that can be
299          evaluated to compute scroll distance. Example:
300          'window.scrollTop' or '(function() { return crazyMath(); })()'.
301      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
302      use_touch: Whether scrolling should be done with touch input.
303    """
304    self._RunAction(ScrollAction(
305        left_start_ratio=left_start_ratio, top_start_ratio=top_start_ratio,
306        direction=direction, distance=distance, distance_expr=distance_expr,
307        speed_in_pixels_per_second=speed_in_pixels_per_second,
308        use_touch=use_touch))
309
310  def ScrollElement(self, selector=None, text=None, element_function=None,
311                    left_start_ratio=0.5, top_start_ratio=0.5,
312                    direction='down', distance=None, distance_expr=None,
313                    speed_in_pixels_per_second=800, use_touch=False):
314    """Perform scroll gesture on the element.
315
316    The element may be selected via selector, text, or element_function.
317    Only one of these arguments must be specified.
318
319    You may specify distance or distance_expr, but not both. If
320    neither is specified, the default scroll distance is variable
321    depending on direction (see scroll.js for full implementation).
322
323    Args:
324      selector: A CSS selector describing the element.
325      text: The element must contains this exact text.
326      element_function: A JavaScript function (as string) that is used
327          to retrieve the element. For example:
328          'function() { return foo.element; }'.
329      left_start_ratio: The horizontal starting coordinate of the
330          gesture, as a ratio of the visible bounding rectangle for
331          the element.
332      top_start_ratio: The vertical starting coordinate of the
333          gesture, as a ratio of the visible bounding rectangle for
334          the element.
335      direction: The direction of scroll, either 'left', 'right',
336          'up', or 'down'
337      distance: The distance to scroll (in pixel).
338      distance_expr: A JavaScript expression (as string) that can be
339          evaluated to compute scroll distance. Example:
340          'window.scrollTop' or '(function() { return crazyMath(); })()'.
341      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
342      use_touch: Whether scrolling should be done with touch input.
343    """
344    self._RunAction(ScrollAction(
345        selector=selector, text=text, element_function=element_function,
346        left_start_ratio=left_start_ratio, top_start_ratio=top_start_ratio,
347        direction=direction, distance=distance, distance_expr=distance_expr,
348        speed_in_pixels_per_second=speed_in_pixels_per_second,
349        use_touch=use_touch))
350
351  def ScrollBouncePage(self, left_start_ratio=0.5, top_start_ratio=0.5,
352                       direction='down', distance=100,
353                       overscroll=10, repeat_count=10,
354                       speed_in_pixels_per_second=400):
355    """Perform scroll bounce gesture on the page.
356
357    This gesture scrolls the page by the number of pixels specified in
358    distance, in the given direction, followed by a scroll by
359    (distance + overscroll) pixels in the opposite direction.
360    The above gesture is repeated repeat_count times.
361
362    Args:
363      left_start_ratio: The horizontal starting coordinate of the
364          gesture, as a ratio of the visible bounding rectangle for
365          document.body.
366      top_start_ratio: The vertical starting coordinate of the
367          gesture, as a ratio of the visible bounding rectangle for
368          document.body.
369      direction: The direction of scroll, either 'left', 'right',
370          'up', or 'down'
371      distance: The distance to scroll (in pixel).
372      overscroll: The number of additional pixels to scroll back, in
373          addition to the givendistance.
374      repeat_count: How often we want to repeat the full gesture.
375      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
376    """
377    self._RunAction(ScrollBounceAction(
378        left_start_ratio=left_start_ratio, top_start_ratio=top_start_ratio,
379        direction=direction, distance=distance,
380        overscroll=overscroll, repeat_count=repeat_count,
381        speed_in_pixels_per_second=speed_in_pixels_per_second))
382
383  def ScrollBounceElement(self, selector=None, text=None, element_function=None,
384                          left_start_ratio=0.5, top_start_ratio=0.5,
385                          direction='down', distance=100,
386                          overscroll=10, repeat_count=10,
387                          speed_in_pixels_per_second=400):
388    """Perform scroll bounce gesture on the element.
389
390    This gesture scrolls on the element by the number of pixels specified in
391    distance, in the given direction, followed by a scroll by
392    (distance + overscroll) pixels in the opposite direction.
393    The above gesture is repeated repeat_count times.
394
395    Args:
396      selector: A CSS selector describing the element.
397      text: The element must contains this exact text.
398      element_function: A JavaScript function (as string) that is used
399          to retrieve the element. For example:
400          'function() { return foo.element; }'.
401      left_start_ratio: The horizontal starting coordinate of the
402          gesture, as a ratio of the visible bounding rectangle for
403          document.body.
404      top_start_ratio: The vertical starting coordinate of the
405          gesture, as a ratio of the visible bounding rectangle for
406          document.body.
407      direction: The direction of scroll, either 'left', 'right',
408          'up', or 'down'
409      distance: The distance to scroll (in pixel).
410      overscroll: The number of additional pixels to scroll back, in
411          addition to the givendistance.
412      repeat_count: How often we want to repeat the full gesture.
413      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
414    """
415    self._RunAction(ScrollBounceAction(
416        selector=selector, text=text, element_function=element_function,
417        left_start_ratio=left_start_ratio, top_start_ratio=top_start_ratio,
418        direction=direction, distance=distance,
419        overscroll=overscroll, repeat_count=repeat_count,
420        speed_in_pixels_per_second=speed_in_pixels_per_second))
421
422  def SwipePage(self, left_start_ratio=0.5, top_start_ratio=0.5,
423                direction='left', distance=100, speed_in_pixels_per_second=800):
424    """Perform swipe gesture on the page.
425
426    Args:
427      left_start_ratio: The horizontal starting coordinate of the
428          gesture, as a ratio of the visible bounding rectangle for
429          document.body.
430      top_start_ratio: The vertical starting coordinate of the
431          gesture, as a ratio of the visible bounding rectangle for
432          document.body.
433      direction: The direction of swipe, either 'left', 'right',
434          'up', or 'down'
435      distance: The distance to swipe (in pixel).
436      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
437    """
438    self._RunAction(SwipeAction(
439        left_start_ratio=left_start_ratio, top_start_ratio=top_start_ratio,
440        direction=direction, distance=distance,
441        speed_in_pixels_per_second=speed_in_pixels_per_second))
442
443  def SwipeElement(self, selector=None, text=None, element_function=None,
444                   left_start_ratio=0.5, top_start_ratio=0.5,
445                   direction='left', distance=100,
446                   speed_in_pixels_per_second=800):
447    """Perform swipe gesture on the element.
448
449    The element may be selected via selector, text, or element_function.
450    Only one of these arguments must be specified.
451
452    Args:
453      selector: A CSS selector describing the element.
454      text: The element must contains this exact text.
455      element_function: A JavaScript function (as string) that is used
456          to retrieve the element. For example:
457          'function() { return foo.element; }'.
458      left_start_ratio: The horizontal starting coordinate of the
459          gesture, as a ratio of the visible bounding rectangle for
460          the element.
461      top_start_ratio: The vertical starting coordinate of the
462          gesture, as a ratio of the visible bounding rectangle for
463          the element.
464      direction: The direction of swipe, either 'left', 'right',
465          'up', or 'down'
466      distance: The distance to swipe (in pixel).
467      speed_in_pixels_per_second: The speed of the gesture (in pixels/s).
468    """
469    self._RunAction(SwipeAction(
470        selector=selector, text=text, element_function=element_function,
471        left_start_ratio=left_start_ratio, top_start_ratio=top_start_ratio,
472        direction=direction, distance=distance,
473        speed_in_pixels_per_second=speed_in_pixels_per_second))
474
475  def PlayMedia(self, selector=None,
476                playing_event_timeout_in_seconds=0,
477                ended_event_timeout_in_seconds=0):
478    """Invokes the "play" action on media elements (such as video).
479
480    Args:
481      selector: A CSS selector describing the element. If none is
482          specified, play the first media element on the page. If the
483          selector matches more than 1 media element, all of them will
484          be played.
485      playing_event_timeout_in_seconds: Maximum waiting time for the "playing"
486          event (dispatched when the media begins to play) to be fired.
487          0 means do not wait.
488      ended_event_timeout_in_seconds: Maximum waiting time for the "ended"
489          event (dispatched when playback completes) to be fired.
490          0 means do not wait.
491
492    Raises:
493      TimeoutException: If the maximum waiting time is exceeded.
494    """
495    self._RunAction(PlayAction(
496        selector=selector,
497        playing_event_timeout_in_seconds=playing_event_timeout_in_seconds,
498        ended_event_timeout_in_seconds=ended_event_timeout_in_seconds))
499
500  def SeekMedia(self, seconds, selector=None, timeout_in_seconds=0,
501                log_time=True, label=''):
502    """Performs a seek action on media elements (such as video).
503
504    Args:
505      seconds: The media time to seek to.
506      selector: A CSS selector describing the element. If none is
507          specified, seek the first media element on the page. If the
508          selector matches more than 1 media element, all of them will
509          be seeked.
510      timeout_in_seconds: Maximum waiting time for the "seeked" event
511          (dispatched when the seeked operation completes) to be
512          fired.  0 means do not wait.
513      log_time: Whether to log the seek time for the perf
514          measurement. Useful when performing multiple seek.
515      label: A suffix string to name the seek perf measurement.
516
517    Raises:
518      TimeoutException: If the maximum waiting time is exceeded.
519    """
520    self._RunAction(SeekAction(
521        seconds=seconds, selector=selector,
522        timeout_in_seconds=timeout_in_seconds,
523        log_time=log_time, label=label))
524
525  def LoopMedia(self, loop_count, selector=None, timeout_in_seconds=None):
526    """Loops a media playback.
527
528    Args:
529      loop_count: The number of times to loop the playback.
530      selector: A CSS selector describing the element. If none is
531          specified, loop the first media element on the page. If the
532          selector matches more than 1 media element, all of them will
533          be looped.
534      timeout_in_seconds: Maximum waiting time for the looped playback to
535          complete. 0 means do not wait. None (the default) means to
536          wait loop_count * 60 seconds.
537
538    Raises:
539      TimeoutException: If the maximum waiting time is exceeded.
540    """
541    self._RunAction(LoopAction(
542        loop_count=loop_count, selector=selector,
543        timeout_in_seconds=timeout_in_seconds))
544
545  def ForceGarbageCollection(self):
546    """Forces JavaScript garbage collection on the page."""
547    self._tab.CollectGarbage()
548
549  def PauseInteractive(self):
550    """Pause the page execution and wait for terminal interaction.
551
552    This is typically used for debugging. You can use this to pause
553    the page execution and inspect the browser state before
554    continuing.
555    """
556    raw_input("Interacting... Press Enter to continue.")
557
558  def RepaintContinuously(self, seconds):
559    """Continuously repaints the visible content.
560
561    It does this by requesting animation frames until the given number
562    of seconds have elapsed AND at least three RAFs have been
563    fired. Times out after max(60, self.seconds), if less than three
564    RAFs were fired."""
565    self._RunAction(RepaintContinuouslyAction(
566        seconds=0 if self._skip_waits else seconds))
567
568class Interaction(object):
569
570  def __init__(self, action_runner, label, flags):
571    assert action_runner
572    assert label
573    assert isinstance(flags, list)
574
575    self._action_runner = action_runner
576    self._label = label
577    self._flags = flags
578    self._started = False
579
580  def Begin(self):
581    assert not self._started
582    self._started = True
583    self._action_runner.ExecuteJavaScript('console.time("%s");' %
584        timeline_interaction_record.GetJavaScriptMarker(
585            self._label, self._flags))
586
587  def End(self):
588    assert self._started
589    self._started = False
590    self._action_runner.ExecuteJavaScript('console.timeEnd("%s");' %
591        timeline_interaction_record.GetJavaScriptMarker(
592            self._label, self._flags))
593