tab.py revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
1# Copyright (c) 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
5from telemetry.core import bitmap
6from telemetry.core import web_contents
7
8DEFAULT_TAB_TIMEOUT = 60
9
10
11class BoundingBoxNotFoundException(Exception):
12  pass
13
14
15class Tab(web_contents.WebContents):
16  """Represents a tab in the browser
17
18  The important parts of the Tab object are in the runtime and page objects.
19  E.g.:
20      # Navigates the tab to a given url.
21      tab.Navigate('http://www.google.com/')
22
23      # Evaluates 1+1 in the tab's JavaScript context.
24      tab.Evaluate('1+1')
25  """
26  def __init__(self, inspector_backend):
27    super(Tab, self).__init__(inspector_backend)
28    self._previous_tab_contents_bounding_box = None
29
30  def __del__(self):
31    super(Tab, self).__del__()
32
33  @property
34  def browser(self):
35    """The browser in which this tab resides."""
36    return self._inspector_backend.browser
37
38  @property
39  def url(self):
40    return self._inspector_backend.url
41
42  @property
43  def dom_stats(self):
44    """A dictionary populated with measured DOM statistics.
45
46    Currently this dictionary contains:
47    {
48      'document_count': integer,
49      'node_count': integer,
50      'event_listener_count': integer
51    }
52    """
53    dom_counters = self._inspector_backend.GetDOMStats(
54        timeout=DEFAULT_TAB_TIMEOUT)
55    assert (len(dom_counters) == 3 and
56            all([x in dom_counters for x in ['document_count', 'node_count',
57                                             'event_listener_count']]))
58    return dom_counters
59
60
61  def Activate(self):
62    """Brings this tab to the foreground asynchronously.
63
64    Not all browsers or browser versions support this method.
65    Be sure to check browser.supports_tab_control.
66
67    Please note: this is asynchronous. There is a delay between this call
68    and the page's documentVisibilityState becoming 'visible', and yet more
69    delay until the actual tab is visible to the user. None of these delays
70    are included in this call."""
71    self._inspector_backend.Activate()
72
73  @property
74  def screenshot_supported(self):
75    """True if the browser instance is capable of capturing screenshots."""
76    return self._inspector_backend.screenshot_supported
77
78  def Screenshot(self, timeout=DEFAULT_TAB_TIMEOUT):
79    """Capture a screenshot of the tab's contents.
80
81    Returns:
82      A telemetry.core.Bitmap.
83    """
84    return self._inspector_backend.Screenshot(timeout)
85
86  @property
87  def video_capture_supported(self):
88    """True if the browser instance is capable of capturing video."""
89    return self.browser.platform.CanCaptureVideo()
90
91  def Highlight(self, color):
92    """Synchronously highlights entire tab contents with the given RgbaColor.
93
94    TODO(tonyg): It is possible that the z-index hack here might not work for
95    all pages. If this happens, DevTools also provides a method for this.
96    """
97    self.ExecuteJavaScript("""
98      (function() {
99        var screen = document.createElement('div');
100        screen.style.background = 'rgba(%d, %d, %d, %d)';
101        screen.style.position = 'fixed';
102        screen.style.top = '0';
103        screen.style.left = '0';
104        screen.style.width = '100%%';
105        screen.style.height = '100%%';
106        screen.style.zIndex = '2147483638';
107        document.body.appendChild(screen);
108        requestAnimationFrame(function() {
109          window.__telemetry_screen_%d = screen;
110        });
111      })();
112    """ % (color.r, color.g, color.b, color.a, int(color)))
113    self.WaitForJavaScriptExpression(
114        '!!window.__telemetry_screen_%d' % int(color), 5)
115
116  def ClearHighlight(self, color):
117    """Clears a highlight of the given bitmap.RgbaColor."""
118    self.ExecuteJavaScript("""
119      (function() {
120        document.body.removeChild(window.__telemetry_screen_%d);
121        requestAnimationFrame(function() {
122          window.__telemetry_screen_%d = null;
123        });
124      })();
125    """ % (int(color), int(color)))
126    self.WaitForJavaScriptExpression(
127        '!window.__telemetry_screen_%d' % int(color), 5)
128
129  def StartVideoCapture(self, min_bitrate_mbps):
130    """Starts capturing video of the tab's contents.
131
132    This works by flashing the entire tab contents to a arbitrary color and then
133    starting video recording. When the frames are processed, we can look for
134    that flash as the content bounds.
135
136    Args:
137      min_bitrate_mbps: The minimum caputre bitrate in MegaBits Per Second.
138          The platform is free to deliver a higher bitrate if it can do so
139          without increasing overhead.
140    """
141    self.Highlight(bitmap.WEB_PAGE_TEST_ORANGE)
142    self.browser.platform.StartVideoCapture(min_bitrate_mbps)
143    self.ClearHighlight(bitmap.WEB_PAGE_TEST_ORANGE)
144
145  def _FindHighlightBoundingBox(self, bmp, color, bounds_tolerance=4,
146      color_tolerance=8):
147    """Returns the bounding box of the content highlight of the given color.
148
149    Raises:
150      BoundingBoxNotFoundException if the hightlight could not be found.
151    """
152    content_box, pixel_count = bmp.GetBoundingBox(color,
153        tolerance=color_tolerance)
154
155    if not content_box:
156      return None
157
158    # We assume arbitrarily that tabs are all larger than 200x200. If this
159    # fails it either means that assumption has changed or something is
160    # awry with our bounding box calculation.
161    if content_box[2] < 200 or content_box[3] < 200:
162      raise BoundingBoxNotFoundException('Unexpectedly small tab contents.')
163
164    # TODO(tonyg): Can this threshold be increased?
165    if pixel_count < 0.9 * content_box[2] * content_box[3]:
166      raise BoundingBoxNotFoundException(
167          'Low count of pixels in tab contents matching expected color.')
168
169    # Since Telemetry doesn't know how to resize the window, we assume
170    # that we should always get the same content box for a tab. If this
171    # fails, it means either that assumption has changed or something is
172    # awry with our bounding box calculation. If this assumption changes,
173    # this can be removed.
174    #
175    # TODO(tonyg): This assert doesn't seem to work.
176    if (self._previous_tab_contents_bounding_box and
177        self._previous_tab_contents_bounding_box != content_box):
178      # Check if there's just a minor variation on the bounding box. If it's
179      # just a few pixels, we can assume it's probably due to
180      # compression artifacts.
181      for i in xrange(len(content_box)):
182        bounds_difference = abs(content_box[i] -
183            self._previous_tab_contents_bounding_box[i])
184        if bounds_difference > bounds_tolerance:
185          raise BoundingBoxNotFoundException(
186              'Unexpected change in tab contents box.')
187    self._previous_tab_contents_bounding_box = content_box
188
189    return content_box
190
191  def StopVideoCapture(self):
192    """Stops recording video of the tab's contents.
193
194    This looks for the initial color flash in the first frame to establish the
195    tab content boundaries and then omits all frames displaying the flash.
196
197    Yields:
198      (time_ms, bitmap) tuples representing each video keyframe. Only the first
199      frame in a run of sequential duplicate bitmaps is typically included.
200        time_ms is milliseconds since navigationStart.
201        bitmap is a telemetry.core.Bitmap.
202    """
203    frame_generator = self.browser.platform.StopVideoCapture()
204
205    # Flip through frames until we find the initial tab contents flash.
206    content_box = None
207    for _, bmp in frame_generator:
208      content_box = self._FindHighlightBoundingBox(
209          bmp, bitmap.WEB_PAGE_TEST_ORANGE)
210      if content_box:
211        break
212
213    if not content_box:
214      raise BoundingBoxNotFoundException(
215          'Failed to identify tab contents in video capture.')
216
217    # Flip through frames until the flash goes away and emit that as frame 0.
218    timestamp = 0
219    for timestamp, bmp in frame_generator:
220      if not self._FindHighlightBoundingBox(bmp, bitmap.WEB_PAGE_TEST_ORANGE):
221        yield 0, bmp.Crop(*content_box)
222        break
223
224    start_time = timestamp
225    for timestamp, bmp in frame_generator:
226      yield timestamp - start_time, bmp.Crop(*content_box)
227
228  def PerformActionAndWaitForNavigate(
229      self, action_function, timeout=DEFAULT_TAB_TIMEOUT):
230    """Executes action_function, and waits for the navigation to complete.
231
232    action_function must be a Python function that results in a navigation.
233    This function returns when the navigation is complete or when
234    the timeout has been exceeded.
235    """
236    self._inspector_backend.PerformActionAndWaitForNavigate(
237        action_function, timeout)
238
239  def Navigate(self, url, script_to_evaluate_on_commit=None,
240               timeout=DEFAULT_TAB_TIMEOUT):
241    """Navigates to url.
242
243    If |script_to_evaluate_on_commit| is given, the script source string will be
244    evaluated when the navigation is committed. This is after the context of
245    the page exists, but before any script on the page itself has executed.
246    """
247    self._inspector_backend.Navigate(url, script_to_evaluate_on_commit, timeout)
248
249  def GetCookieByName(self, name, timeout=DEFAULT_TAB_TIMEOUT):
250    """Returns the value of the cookie by the given |name|."""
251    return self._inspector_backend.GetCookieByName(name, timeout)
252
253  def CollectGarbage(self):
254    self._inspector_backend.CollectGarbage()
255
256  def ClearCache(self, force):
257    """Clears the browser's networking related disk, memory and other caches.
258
259    Args:
260      force: Iff true, navigates to about:blank which destroys the previous
261          renderer, ensuring that even "live" resources in the memory cache are
262          cleared.
263    """
264    self.ExecuteJavaScript("""
265        if (window.chrome && chrome.benchmarking &&
266            chrome.benchmarking.clearCache) {
267          chrome.benchmarking.clearCache();
268          chrome.benchmarking.clearPredictorCache();
269          chrome.benchmarking.clearHostResolverCache();
270        }
271    """)
272    if force:
273      self.Navigate('about:blank')
274