1# Copyright 2013 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.
4import time
5
6from telemetry.util import image_util
7
8
9class InspectorPage(object):
10  """Class that controls a page connected by an inspector_websocket.
11
12  This class provides utility methods for controlling a page connected by an
13  inspector_websocket. It does not perform any exception handling. All
14  inspector_websocket exceptions must be handled by the caller.
15  """
16  def __init__(self, inspector_websocket, timeout=60):
17    self._inspector_websocket = inspector_websocket
18    self._inspector_websocket.RegisterDomain('Page', self._OnNotification)
19
20    self._navigation_pending = False
21    self._navigation_url = ''  # Support for legacy backends.
22    self._navigation_frame_id = ''
23    self._navigated_frame_ids = None  # Holds frame ids while navigating.
24    self._script_to_evaluate_on_commit = None
25    # Turn on notifications. We need them to get the Page.frameNavigated event.
26    self._EnablePageNotifications(timeout=timeout)
27
28  def _OnNotification(self, msg):
29    if msg['method'] == 'Page.frameNavigated':
30      url = msg['params']['frame']['url']
31      if not self._navigated_frame_ids == None:
32        frame_id = msg['params']['frame']['id']
33        if self._navigation_frame_id == frame_id:
34          self._navigation_frame_id = ''
35          self._navigated_frame_ids = None
36          self._navigation_pending = False
37        else:
38          self._navigated_frame_ids.add(frame_id)
39      elif self._navigation_url == url:
40        # TODO(tonyg): Remove this when Chrome 38 goes stable.
41        self._navigation_url = ''
42        self._navigation_pending = False
43      elif (not url == 'chrome://newtab/' and not url == 'about:blank'
44        and not 'parentId' in msg['params']['frame']):
45        # Marks the navigation as complete and unblocks the
46        # WaitForNavigate call.
47        self._navigation_pending = False
48
49  def _SetScriptToEvaluateOnCommit(self, source):
50    existing_source = (self._script_to_evaluate_on_commit and
51                       self._script_to_evaluate_on_commit['source'])
52    if source == existing_source:
53      return
54    if existing_source:
55      request = {
56          'method': 'Page.removeScriptToEvaluateOnLoad',
57          'params': {
58              'identifier': self._script_to_evaluate_on_commit['id'],
59              }
60          }
61      self._inspector_websocket.SyncRequest(request)
62      self._script_to_evaluate_on_commit = None
63    if source:
64      request = {
65          'method': 'Page.addScriptToEvaluateOnLoad',
66          'params': {
67              'scriptSource': source,
68              }
69          }
70      res = self._inspector_websocket.SyncRequest(request)
71      self._script_to_evaluate_on_commit = {
72          'id': res['result']['identifier'],
73          'source': source
74          }
75
76  def _EnablePageNotifications(self, timeout=60):
77    request = {
78        'method': 'Page.enable'
79        }
80    res = self._inspector_websocket.SyncRequest(request, timeout)
81    assert len(res['result'].keys()) == 0
82
83  def WaitForNavigate(self, timeout=60):
84    """Waits for the navigation to complete.
85
86    The current page is expect to be in a navigation. This function returns
87    when the navigation is complete or when the timeout has been exceeded.
88    """
89    start_time = time.time()
90    remaining_time = timeout
91    self._navigation_pending = True
92    while self._navigation_pending and remaining_time > 0:
93      remaining_time = max(timeout - (time.time() - start_time), 0.0)
94      self._inspector_websocket.DispatchNotifications(remaining_time)
95
96  def Navigate(self, url, script_to_evaluate_on_commit=None, timeout=60):
97    """Navigates to |url|.
98
99    If |script_to_evaluate_on_commit| is given, the script source string will be
100    evaluated when the navigation is committed. This is after the context of
101    the page exists, but before any script on the page itself has executed.
102    """
103
104    self._SetScriptToEvaluateOnCommit(script_to_evaluate_on_commit)
105    request = {
106        'method': 'Page.navigate',
107        'params': {
108            'url': url,
109            }
110        }
111    self._navigated_frame_ids = set()
112    res = self._inspector_websocket.SyncRequest(request, timeout)
113    if 'frameId' in res['result']:
114      # Modern backends are returning frameId from Page.navigate.
115      # Use it here to unblock upon precise navigation.
116      frame_id = res['result']['frameId']
117      if self._navigated_frame_ids and frame_id in self._navigated_frame_ids:
118        self._navigated_frame_ids = None
119        return
120      self._navigation_frame_id = frame_id
121    else:
122      # TODO(tonyg): Remove this when Chrome 38 goes stable.
123      self._navigated_frame_ids = None
124      self._navigation_url = url
125    self.WaitForNavigate(timeout)
126
127  def GetCookieByName(self, name, timeout=60):
128    """Returns the value of the cookie by the given |name|."""
129    request = {
130        'method': 'Page.getCookies'
131        }
132    res = self._inspector_websocket.SyncRequest(request, timeout)
133    cookies = res['result']['cookies']
134    for cookie in cookies:
135      if cookie['name'] == name:
136        return cookie['value']
137    return None
138
139  def CaptureScreenshot(self, timeout=60):
140    request = {
141        'method': 'Page.captureScreenshot'
142        }
143    # "Google API are missing..." infobar might cause a viewport resize
144    # which invalidates screenshot request. See crbug.com/459820.
145    for _ in range(2):
146      res = self._inspector_websocket.SyncRequest(request, timeout)
147      if res and ('result' in res) and ('data' in res['result']):
148        return image_util.FromBase64Png(res['result']['data'])
149    return None
150
151  def CollectGarbage(self, timeout=60):
152    request = {
153        'method': 'HeapProfiler.collectGarbage'
154        }
155    res = self._inspector_websocket.SyncRequest(request, timeout)
156    assert 'result' in res
157