1# Copyright 2012 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 os
6
7from telemetry.core import exceptions
8from telemetry.core import util
9
10DEFAULT_WEB_CONTENTS_TIMEOUT = 90
11
12# TODO(achuith, dtu, nduca): Add unit tests specifically for WebContents,
13# independent of Tab.
14class WebContents(object):
15  """Represents web contents in the browser"""
16  def __init__(self, inspector_backend):
17    self._inspector_backend = inspector_backend
18
19    with open(os.path.join(os.path.dirname(__file__),
20        'network_quiescence.js')) as f:
21      self._quiescence_js = f.read()
22
23  @property
24  def id(self):
25    """Return the unique id string for this tab object."""
26    return self._inspector_backend.id
27
28  def GetUrl(self):
29    """Returns the URL to which the WebContents is connected.
30
31    Raises:
32      exceptions.Error: If there is an error in inspector backend connection.
33    """
34    return self._inspector_backend.url
35
36  def GetWebviewContexts(self):
37    """Returns a list of webview contexts within the current inspector backend.
38
39    Returns:
40      A list of WebContents objects representing the webview contexts.
41
42    Raises:
43      exceptions.Error: If there is an error in inspector backend connection.
44    """
45    webviews = []
46    inspector_backends = self._inspector_backend.GetWebviewInspectorBackends()
47    for inspector_backend in inspector_backends:
48      webviews.append(WebContents(inspector_backend))
49    return webviews
50
51  def WaitForDocumentReadyStateToBeComplete(self,
52      timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
53    """Waits for the document to finish loading.
54
55    Raises:
56      exceptions.Error: See WaitForJavaScriptExpression() for a detailed list
57      of possible exceptions.
58    """
59
60    self.WaitForJavaScriptExpression(
61        'document.readyState == "complete"', timeout)
62
63  def WaitForDocumentReadyStateToBeInteractiveOrBetter(self,
64      timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
65    """Waits for the document to be interactive.
66
67    Raises:
68      exceptions.Error: See WaitForJavaScriptExpression() for a detailed list
69      of possible exceptions.
70    """
71    self.WaitForJavaScriptExpression(
72        'document.readyState == "interactive" || '
73        'document.readyState == "complete"', timeout)
74
75  def WaitForJavaScriptExpression(self, expr, timeout):
76    """Waits for the given JavaScript expression to be True.
77
78    This method is robust against any given Evaluation timing out.
79
80    Args:
81      expr: The expression to evaluate.
82      timeout: The number of seconds to wait for the expression to be True.
83
84    Raises:
85      exceptions.TimeoutException: On a timeout.
86      exceptions.Error: See EvaluateJavaScript() for a detailed list of
87      possible exceptions.
88    """
89    def IsJavaScriptExpressionTrue():
90      try:
91        return bool(self.EvaluateJavaScript(expr))
92      except exceptions.TimeoutException:
93        # If the main thread is busy for longer than Evaluate's timeout, we
94        # may time out here early. Instead, we want to wait for the full
95        # timeout of this method.
96        return False
97    try:
98      util.WaitFor(IsJavaScriptExpressionTrue, timeout)
99    except exceptions.TimeoutException as e:
100      # Try to make timeouts a little more actionable by dumping console output.
101      debug_message = None
102      try:
103        debug_message = (
104            'Console output:\n%s' %
105            self._inspector_backend.GetCurrentConsoleOutputBuffer())
106      except Exception as e:
107        debug_message = (
108            'Exception thrown when trying to capture console output: %s' %
109            repr(e))
110      raise exceptions.TimeoutException(
111          e.message + '\n' + debug_message)
112
113  def HasReachedQuiescence(self):
114    """Determine whether the page has reached quiescence after loading.
115
116    Returns:
117      True if 2 seconds have passed since last resource received, false
118      otherwise.
119    Raises:
120      exceptions.Error: See EvaluateJavaScript() for a detailed list of
121      possible exceptions.
122    """
123
124    # Inclusion of the script that provides
125    # window.__telemetry_testHasReachedNetworkQuiescence()
126    # is idempotent, it's run on every call because WebContents doesn't track
127    # page loads and we need to execute anew for every newly loaded page.
128    has_reached_quiescence = (
129        self.EvaluateJavaScript(self._quiescence_js +
130            "window.__telemetry_testHasReachedNetworkQuiescence()"))
131    return has_reached_quiescence
132
133  def ExecuteJavaScript(self, statement, timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
134    """Executes statement in JavaScript. Does not return the result.
135
136    If the statement failed to evaluate, EvaluateException will be raised.
137
138    Raises:
139      exceptions.Error: See ExecuteJavaScriptInContext() for a detailed list of
140      possible exceptions.
141    """
142    return self.ExecuteJavaScriptInContext(
143        statement, context_id=None, timeout=timeout)
144
145  def EvaluateJavaScript(self, expr, timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
146    """Evalutes expr in JavaScript and returns the JSONized result.
147
148    Consider using ExecuteJavaScript for cases where the result of the
149    expression is not needed.
150
151    If evaluation throws in JavaScript, a Python EvaluateException will
152    be raised.
153
154    If the result of the evaluation cannot be JSONized, then an
155    EvaluationException will be raised.
156
157    Raises:
158      exceptions.Error: See EvaluateJavaScriptInContext() for a detailed list
159      of possible exceptions.
160    """
161    return self.EvaluateJavaScriptInContext(
162        expr, context_id=None, timeout=timeout)
163
164  def ExecuteJavaScriptInContext(self, expr, context_id,
165                                 timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
166    """Similar to ExecuteJavaScript, except context_id can refer to an iframe.
167    The main page has context_id=1, the first iframe context_id=2, etc.
168
169    Raises:
170      exceptions.EvaluateException
171      exceptions.WebSocketDisconnected
172      exceptions.TimeoutException
173      exceptions.DevtoolsTargetCrashException
174    """
175    return self._inspector_backend.ExecuteJavaScript(
176        expr, context_id=context_id, timeout=timeout)
177
178  def EvaluateJavaScriptInContext(self, expr, context_id,
179                                  timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
180    """Similar to ExecuteJavaScript, except context_id can refer to an iframe.
181    The main page has context_id=1, the first iframe context_id=2, etc.
182
183    Raises:
184      exceptions.EvaluateException
185      exceptions.WebSocketDisconnected
186      exceptions.TimeoutException
187      exceptions.DevtoolsTargetCrashException
188    """
189    return self._inspector_backend.EvaluateJavaScript(
190        expr, context_id=context_id, timeout=timeout)
191
192  def EnableAllContexts(self):
193    """Enable all contexts in a page. Returns the number of available contexts.
194
195    Raises:
196      exceptions.WebSocketDisconnected
197      exceptions.TimeoutException
198      exceptions.DevtoolsTargetCrashException
199    """
200    return self._inspector_backend.EnableAllContexts()
201
202  def WaitForNavigate(self, timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
203    """Waits for the navigation to complete.
204
205    The current page is expect to be in a navigation.
206    This function returns when the navigation is complete or when
207    the timeout has been exceeded.
208
209    Raises:
210      exceptions.TimeoutException
211      exceptions.DevtoolsTargetCrashException
212    """
213    self._inspector_backend.WaitForNavigate(timeout)
214
215  def Navigate(self, url, script_to_evaluate_on_commit=None,
216               timeout=DEFAULT_WEB_CONTENTS_TIMEOUT):
217    """Navigates to url.
218
219    If |script_to_evaluate_on_commit| is given, the script source string will be
220    evaluated when the navigation is committed. This is after the context of
221    the page exists, but before any script on the page itself has executed.
222
223    Raises:
224      exceptions.TimeoutException
225      exceptions.DevtoolsTargetCrashException
226    """
227    self._inspector_backend.Navigate(url, script_to_evaluate_on_commit, timeout)
228
229  def IsAlive(self):
230    """Whether the WebContents is still operating normally.
231
232    Since WebContents function asynchronously, this method does not guarantee
233    that the WebContents will still be alive at any point in the future.
234
235    Returns:
236      A boolean indicating whether the WebContents is opearting normally.
237    """
238    return self._inspector_backend.IsInspectable()
239
240  def CloseConnections(self):
241    """Closes all TCP sockets held open by the browser.
242
243    Raises:
244      exceptions.DevtoolsTargetCrashException if the tab is not alive.
245    """
246    if not self.IsAlive():
247      raise exceptions.DevtoolsTargetCrashException
248    self.ExecuteJavaScript('window.chrome && chrome.benchmarking &&'
249                           'chrome.benchmarking.closeConnections()')
250
251  def SynthesizeScrollGesture(self, x=100, y=800, xDistance=0, yDistance=-500,
252                              xOverscroll=None, yOverscroll=None,
253                              preventFling=True, speed=None,
254                              gestureSourceType=None, repeatCount=None,
255                              repeatDelayMs=None, interactionMarkerName=None,
256                              timeout=60):
257    """Runs an inspector command that causes a repeatable browser driven scroll.
258
259    Args:
260      x: X coordinate of the start of the gesture in CSS pixels.
261      y: Y coordinate of the start of the gesture in CSS pixels.
262      xDistance: Distance to scroll along the X axis (positive to scroll left).
263      yDistance: Ddistance to scroll along the Y axis (positive to scroll up).
264      xOverscroll: Number of additional pixels to scroll back along the X axis.
265      xOverscroll: Number of additional pixels to scroll back along the Y axis.
266      preventFling: Prevents a fling gesture.
267      speed: Swipe speed in pixels per second.
268      gestureSourceType: Which type of input events to be generated.
269      repeatCount: Number of additional repeats beyond the first scroll.
270      repeatDelayMs: Number of milliseconds delay between each repeat.
271      interactionMarkerName: The name of the interaction markers to generate.
272
273    Raises:
274      exceptions.TimeoutException
275      exceptions.DevtoolsTargetCrashException
276    """
277    return self._inspector_backend.SynthesizeScrollGesture(
278        x=x, y=y, xDistance=xDistance, yDistance=yDistance,
279        xOverscroll=xOverscroll, yOverscroll=yOverscroll,
280        preventFling=preventFling, speed=speed,
281        gestureSourceType=gestureSourceType, repeatCount=repeatCount,
282        repeatDelayMs=repeatDelayMs,
283        interactionMarkerName=interactionMarkerName,
284        timeout=timeout)
285