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.
4import inspect
5import logging
6import os
7import urlparse
8
9from catapult_base import cloud_storage  # pylint: disable=import-error
10
11from telemetry import story
12from telemetry.page import shared_page_state
13from telemetry.page import action_runner as action_runner_module
14
15
16class Page(story.Story):
17
18  def __init__(self, url, page_set=None, base_dir=None, name='',
19               credentials_path=None,
20               credentials_bucket=cloud_storage.PUBLIC_BUCKET, labels=None,
21               startup_url='', make_javascript_deterministic=True,
22               shared_page_state_class=shared_page_state.SharedPageState):
23    self._url = url
24
25    super(Page, self).__init__(
26        shared_page_state_class, name=name, labels=labels,
27        is_local=self._scheme in ['file', 'chrome', 'about'],
28        make_javascript_deterministic=make_javascript_deterministic)
29
30    self._page_set = page_set
31    # Default value of base_dir is the directory of the file that defines the
32    # class of this page instance.
33    if base_dir is None:
34      base_dir = os.path.dirname(inspect.getfile(self.__class__))
35    self._base_dir = base_dir
36    self._name = name
37    if credentials_path:
38      credentials_path = os.path.join(self._base_dir, credentials_path)
39      cloud_storage.GetIfChanged(credentials_path, credentials_bucket)
40      if not os.path.exists(credentials_path):
41        logging.error('Invalid credentials path: %s' % credentials_path)
42        credentials_path = None
43    self._credentials_path = credentials_path
44
45    # Whether to collect garbage on the page before navigating & performing
46    # page actions.
47    self._collect_garbage_before_run = True
48
49    # These attributes can be set dynamically by the page.
50    self.synthetic_delays = dict()
51    self._startup_url = startup_url
52    self.credentials = None
53    self.skip_waits = False
54    self.script_to_evaluate_on_commit = None
55    self._SchemeErrorCheck()
56
57  @property
58  def credentials_path(self):
59    return self._credentials_path
60
61  @property
62  def startup_url(self):
63    return self._startup_url
64
65  def _SchemeErrorCheck(self):
66    if not self._scheme:
67      raise ValueError('Must prepend the URL with scheme (e.g. file://)')
68
69    if self.startup_url:
70      startup_url_scheme = urlparse.urlparse(self.startup_url).scheme
71      if not startup_url_scheme:
72        raise ValueError('Must prepend the URL with scheme (e.g. http://)')
73      if startup_url_scheme == 'file':
74        raise ValueError('startup_url with local file scheme is not supported')
75
76  def Run(self, shared_state):
77    current_tab = shared_state.current_tab
78    # Collect garbage from previous run several times to make the results more
79    # stable if needed.
80    if self._collect_garbage_before_run:
81      for _ in xrange(0, 5):
82        current_tab.CollectGarbage()
83    shared_state.page_test.WillNavigateToPage(self, current_tab)
84    shared_state.page_test.RunNavigateSteps(self, current_tab)
85    shared_state.page_test.DidNavigateToPage(self, current_tab)
86    action_runner = action_runner_module.ActionRunner(
87        current_tab, skip_waits=self.skip_waits)
88    self.RunPageInteractions(action_runner)
89
90  def RunNavigateSteps(self, action_runner):
91    url = self.file_path_url_with_scheme if self.is_file else self.url
92    action_runner.Navigate(
93        url, script_to_evaluate_on_commit=self.script_to_evaluate_on_commit)
94
95  def RunPageInteractions(self, action_runner):
96    """Override this to define custom interactions with the page.
97    e.g:
98      def RunPageInteractions(self, action_runner):
99        action_runner.ScrollPage()
100        action_runner.TapElement(text='Next')
101    """
102    pass
103
104  def AsDict(self):
105    """Converts a page object to a dict suitable for JSON output."""
106    d = {
107        'id': self._id,
108        'url': self._url,
109    }
110    if self._name:
111      d['name'] = self._name
112    return d
113
114  @property
115  def story_set(self):
116    return self._page_set
117
118  # TODO(nednguyen, aiolos): deprecate this property.
119  @property
120  def page_set(self):
121    return self._page_set
122
123  @property
124  def url(self):
125    return self._url
126
127  def GetSyntheticDelayCategories(self):
128    result = []
129    for delay, options in self.synthetic_delays.items():
130      options = '%f;%s' % (options.get('target_duration', 0),
131                           options.get('mode', 'static'))
132      result.append('DELAY(%s;%s)' % (delay, options))
133    return result
134
135  def __lt__(self, other):
136    return self.url < other.url
137
138  def __cmp__(self, other):
139    x = cmp(self.name, other.name)
140    if x != 0:
141      return x
142    return cmp(self.url, other.url)
143
144  def __str__(self):
145    return self.url
146
147  def AddCustomizeBrowserOptions(self, options):
148    """ Inherit page overrides this to add customized browser options."""
149    pass
150
151  @property
152  def _scheme(self):
153    return urlparse.urlparse(self.url).scheme
154
155  @property
156  def is_file(self):
157    """Returns True iff this URL points to a file."""
158    return self._scheme == 'file'
159
160  @property
161  def file_path(self):
162    """Returns the path of the file, stripping the scheme and query string."""
163    assert self.is_file
164    # Because ? is a valid character in a filename,
165    # we have to treat the URL as a non-file by removing the scheme.
166    parsed_url = urlparse.urlparse(self.url[7:])
167    return os.path.normpath(os.path.join(
168        self._base_dir, parsed_url.netloc + parsed_url.path))
169
170  @property
171  def base_dir(self):
172    return self._base_dir
173
174  @property
175  def file_path_url(self):
176    """Returns the file path, including the params, query, and fragment."""
177    assert self.is_file
178    file_path_url = os.path.normpath(
179        os.path.join(self._base_dir, self.url[7:]))
180    # Preserve trailing slash or backslash.
181    # It doesn't matter in a file path, but it does matter in a URL.
182    if self.url.endswith('/'):
183      file_path_url += os.sep
184    return file_path_url
185
186  @property
187  def file_path_url_with_scheme(self):
188    return 'file://' + self.file_path_url
189
190  @property
191  def serving_dir(self):
192    if not self.is_file:
193      return None
194    file_path = os.path.realpath(self.file_path)
195    if os.path.isdir(file_path):
196      return file_path
197    else:
198      return os.path.dirname(file_path)
199
200  @property
201  def display_name(self):
202    if self.name:
203      return self.name
204    if not self.is_file:
205      return self.url
206    all_urls = [p.url.rstrip('/') for p in self.page_set if p.is_file]
207    common_prefix = os.path.dirname(os.path.commonprefix(all_urls))
208    return self.url[len(common_prefix):].strip('/')
209