tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

helpers.py (10025B)


      1 import base64
      2 import collections
      3 import math
      4 import sys
      5 import os
      6 from urllib.parse import urlparse
      7 
      8 import webdriver
      9 
     10 from tests.support import defaults
     11 from tests.support.sync import Poll
     12 
     13 
     14 def ignore_exceptions(f):
     15    def inner(session, *args, **kwargs):
     16        # Do not try to clean up already ended session.
     17        if session.session_id is None:
     18            return
     19        try:
     20            return f(session, *args, **kwargs)
     21        except webdriver.error.WebDriverException as e:
     22            print("Ignored exception %s" % e, file=sys.stderr)
     23    inner.__name__ = f.__name__
     24    return inner
     25 
     26 
     27 def cleanup_session(session):
     28    """Clean-up the current session for a clean state."""
     29    @ignore_exceptions
     30    def _dismiss_user_prompts(session):
     31        """Dismiss any open user prompts in windows."""
     32        current_window = session.window_handle
     33 
     34        for window in _windows(session):
     35            session.window_handle = window
     36            try:
     37                session.alert.dismiss()
     38            except webdriver.NoSuchAlertException:
     39                pass
     40 
     41        session.window_handle = current_window
     42 
     43    @ignore_exceptions
     44    def _ensure_valid_window(session):
     45        """If current window was closed, ensure to have a valid one selected."""
     46        try:
     47            session.window_handle
     48        except webdriver.NoSuchWindowException:
     49            handles = session.handles
     50            if handles:
     51                # Update only when there is at least one valid window left.
     52                session.window_handle = handles[0]
     53 
     54    @ignore_exceptions
     55    def _restore_timeouts(session):
     56        """Restore modified timeouts to their default values."""
     57        session.timeouts.implicit = defaults.IMPLICIT_WAIT_TIMEOUT
     58        session.timeouts.page_load = defaults.PAGE_LOAD_TIMEOUT
     59        session.timeouts.script = defaults.SCRIPT_TIMEOUT
     60 
     61    @ignore_exceptions
     62    def _restore_window_state(session):
     63        """Reset window to an acceptable size.
     64 
     65        This also includes bringing it out of maximized, minimized,
     66        or fullscreen state.
     67        """
     68        if session.capabilities.get("setWindowRect"):
     69            session.window.size = defaults.WINDOW_SIZE
     70 
     71    @ignore_exceptions
     72    def _restore_windows(session):
     73        """Close superfluous windows opened by the test.
     74 
     75        It will not end the session implicitly by closing the last window.
     76        """
     77        current_window = session.window_handle
     78 
     79        for window in _windows(session, exclude=[current_window]):
     80            session.window_handle = window
     81            if len(session.handles) > 1:
     82                session.window.close()
     83 
     84        session.window_handle = current_window
     85 
     86    _restore_timeouts(session)
     87    _ensure_valid_window(session)
     88    _dismiss_user_prompts(session)
     89    _restore_windows(session)
     90    _restore_window_state(session)
     91    _switch_to_top_level_browsing_context(session)
     92 
     93 
     94 @ignore_exceptions
     95 def _switch_to_top_level_browsing_context(session):
     96    """If the current browsing context selected by WebDriver is a
     97    `<frame>` or an `<iframe>`, switch it back to the top-level
     98    browsing context.
     99    """
    100    session.switch_to_frame(None)
    101 
    102 
    103 def _windows(session, exclude=None):
    104    """Set of window handles, filtered by an `exclude` list if
    105    provided.
    106    """
    107    if exclude is None:
    108        exclude = []
    109    wins = [w for w in session.handles if w not in exclude]
    110    return set(wins)
    111 
    112 
    113 
    114 
    115 def deep_update(source, overrides):
    116    """
    117    Update a nested dictionary or similar mapping.
    118    Modify ``source`` in place.
    119    """
    120    for key, value in overrides.items():
    121        if isinstance(value, collections.abc.Mapping) and value:
    122            source[key] = deep_update(source.get(key, {}), value)
    123        elif isinstance(value, list) and isinstance(source.get(key), list) and value:
    124            # Concatenate lists, ensuring all elements are kept without duplicates
    125            source[key] = list(dict.fromkeys(source[key] + value))
    126        else:
    127            source[key] = value
    128 
    129    return source
    130 
    131 
    132 def document_dimensions(session):
    133    return tuple(session.execute_script("""
    134        const {devicePixelRatio} = window;
    135        const {width, height} = document.documentElement.getBoundingClientRect();
    136        return [width * devicePixelRatio, height * devicePixelRatio];
    137        """))
    138 
    139 
    140 def center_point(element):
    141    """Calculates the in-view center point of a web element."""
    142    inner_width, inner_height = element.session.execute_script(
    143        "return [window.innerWidth, window.innerHeight]")
    144    rect = element.rect
    145 
    146    # calculate the intersection of the rect that is inside the viewport
    147    visible = {
    148        "left": max(0, min(rect["x"], rect["x"] + rect["width"])),
    149        "right": min(inner_width, max(rect["x"], rect["x"] + rect["width"])),
    150        "top": max(0, min(rect["y"], rect["y"] + rect["height"])),
    151        "bottom": min(inner_height, max(rect["y"], rect["y"] + rect["height"])),
    152    }
    153 
    154    # arrive at the centre point of the visible rectangle
    155    x = (visible["left"] + visible["right"]) / 2.0
    156    y = (visible["top"] + visible["bottom"]) / 2.0
    157 
    158    # convert to CSS pixels, as centre point can be float
    159    return (math.floor(x), math.floor(y))
    160 
    161 
    162 def document_hidden(session):
    163    return session.execute_script("return document.hidden")
    164 
    165 
    166 def document_location(session):
    167    """
    168    Unlike ``webdriver.Session#url``, which always returns
    169    the top-level browsing context's URL, this returns
    170    the current browsing context's active document's URL.
    171    """
    172    return session.execute_script("return document.location.href")
    173 
    174 
    175 def element_rect(session, element):
    176    return session.execute_script("""
    177        let element = arguments[0];
    178        let rect = element.getBoundingClientRect();
    179 
    180        return {
    181            x: rect.left + window.pageXOffset,
    182            y: rect.top + window.pageYOffset,
    183            width: rect.width,
    184            height: rect.height,
    185        };
    186        """, args=(element,))
    187 
    188 
    189 def is_element_in_viewport(session, element):
    190    """Check if element is outside of the viewport"""
    191    return session.execute_script("""
    192        let el = arguments[0];
    193 
    194        let rect = el.getBoundingClientRect();
    195        let viewport = {
    196          height: window.innerHeight || document.documentElement.clientHeight,
    197          width: window.innerWidth || document.documentElement.clientWidth,
    198        };
    199 
    200        return !(rect.right < 0 || rect.bottom < 0 ||
    201            rect.left > viewport.width || rect.top > viewport.height)
    202    """, args=(element,))
    203 
    204 
    205 def is_fullscreen(session):
    206    # At the time of writing, WebKit does not conform to the
    207    # Fullscreen API specification.
    208    #
    209    # Remove the prefixed fallback when
    210    # https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
    211    return session.execute_script("""
    212        return !!(window.fullScreen || document.webkitIsFullScreen)
    213        """)
    214 
    215 
    216 def _get_maximized_state(session):
    217    dimensions = session.execute_script("""
    218        return {
    219            availWidth: screen.availWidth,
    220            availHeight: screen.availHeight,
    221            windowWidth: window.outerWidth,
    222            windowHeight: window.outerHeight,
    223        }
    224        """)
    225 
    226    # The maximized window can still have a border attached which would
    227    # cause its dimensions to exceed the whole available screen.
    228    return (dimensions["windowWidth"] >= dimensions["availWidth"] and
    229        dimensions["windowHeight"] >= dimensions["availHeight"] and
    230        # Only return true if the window is not in fullscreen mode
    231        not is_fullscreen(session)
    232    )
    233 
    234 
    235 def is_maximized(session, original_rect):
    236    if _get_maximized_state(session):
    237        return True
    238 
    239    # Wayland doesn't guarantee that the window will get maximized
    240    # to the screen, so check if the dimensions got larger.
    241    elif is_wayland():
    242        dimensions = session.execute_script("""
    243            return {
    244                windowWidth: window.outerWidth,
    245                windowHeight: window.outerHeight,
    246            }
    247            """)
    248        return (
    249            dimensions["windowWidth"] > original_rect["width"] and
    250            dimensions["windowHeight"] > original_rect["height"] and
    251            # Only return true if the window is not in fullscreen mode
    252            not is_fullscreen(session)
    253        )
    254    else:
    255        return False
    256 
    257 
    258 def is_not_maximized(session):
    259    return not _get_maximized_state(session)
    260 
    261 
    262 def is_wayland():
    263    # We don't use mozinfo.display here to make sure it also
    264    # works upstream in wpt Github repo.
    265    return os.environ.get("WAYLAND_DISPLAY", "") != ""
    266 
    267 
    268 def filter_dict(source, d):
    269    """Filter `source` dict to only contain same keys as `d` dict.
    270 
    271    :param source: dictionary to filter.
    272    :param d: dictionary whose keys determine the filtering.
    273    """
    274    return {k: source[k] for k in d.keys()}
    275 
    276 
    277 def filter_supported_key_events(all_events, expected):
    278    events = [filter_dict(e, expected[0]) for e in all_events]
    279    if len(events) > 0 and events[0]["code"] is None:
    280        # Remove 'code' entry if browser doesn't support it
    281        expected = [filter_dict(e, {"key": "", "type": ""}) for e in expected]
    282        events = [filter_dict(e, expected[0]) for e in events]
    283 
    284    return (events, expected)
    285 
    286 
    287 def get_origin_from_url(url):
    288    parsed_uri = urlparse(url)
    289    return '{uri.scheme}://{uri.netloc}'.format(uri=parsed_uri)
    290 
    291 
    292 def wait_for_new_handle(session, handles_before):
    293    def find_new_handle(session):
    294        new_handles = list(set(session.handles) - set(handles_before))
    295        assert len(new_handles) == 1, "No new window was opened"
    296        return new_handles[0]
    297 
    298    wait = Poll(session, timeout=5)
    299    return wait.until(find_new_handle)
    300 
    301 
    302 def get_extension_path(filename):
    303    return os.path.join(
    304        os.path.abspath(os.path.dirname(__file__)), "webextensions", filename
    305    )
    306 
    307 
    308 def get_base64_for_extension_file(filename):
    309    with open(
    310        get_extension_path(filename),
    311        "rb",
    312    ) as file:
    313        return base64.b64encode(file.read()).decode("utf-8")