Skip to content

Commit

Permalink
Merge pull request #12 from RedHatQE/elements-parent-inject
Browse files Browse the repository at this point in the history
Automatic parent element injection for widgets that define __locator__
  • Loading branch information
Milan Falešník committed Jan 12, 2017
2 parents c622829 + 82fd1b1 commit 5b56e27
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Features
can be worked around by using ``View.nested`` decorator on the nested View.
- Includes a wrapper around selenium functionality that tries to make the experience as hassle-free
as possible including customizable hooks and built-in "JavaScript wait" code.
- Views can define their root locators and those are automatically honoured in the element lookup
in the child Widgets.
- Supports `Parametrized views`_.
- Supports `Version picking`_.

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'jsmin',
'selenium',
'selenium-smart-locator',
'six',
'wait_for',
],
setup_requires=[
Expand Down
59 changes: 59 additions & 0 deletions src/widgetastic/browser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import inspect
import six
import time

Expand Down Expand Up @@ -583,3 +584,61 @@ def handle_alert(self, cancel=False, wait=30.0, squash=False, prompt=None, check
return False
else:
raise


class BrowserParentWrapper(object):
"""A wrapper/proxy class that ensures passing of correct parent locator on elements lookup.
Required for the proper operation of nesting.
Assumes the object passed has a ``browser`` attribute.
Args:
o: Object which should be considered as a parent element for lookups. Must have ``.browser``
defined.
"""
def __init__(self, o, browser):
self._o = o
self._browser = browser

def __eq__(self, other):
if not isinstance(other, BrowserParentWrapper):
return False
return self._o == other._o and self._browser == other._browser

def elements(
self, locator, parent=None, check_visibility=False, check_safe=True,
force_check_safe=False):
if parent is None:
parent = self._o
return self._browser.elements(
locator,
parent=parent,
check_visibility=check_visibility,
check_safe=check_safe,
force_check_safe=force_check_safe)

def __getattr__(self, attr):
"""Route all other attribute requests into the parent object's browser. Black magic included
Here is the explanation:
If you call ``.elements`` on this object directly, it will correctly inject the parent
locator. But if you call eg. ``element``, what will happen is that it will invoke the
original method from underlying browser and that method's ``self`` is the underlying browser
and not this wrapper. Therefore ``element`` would call the original ``elements`` without
injecting the parent.
What this getter does is that if you pull out a method, it detects that, unbinds the
pure function and rebinds it to this wrapper. The method that came from the browser object
is now executed not against the browser, but against this wrapper, enabling us to intercept
every single ``elements`` call.
"""
value = getattr(self._browser, attr)
if inspect.ismethod(value):
function = six.get_method_function(value)
# Bind the function like it was defined on this class
value = function.__get__(self, BrowserParentWrapper)
return value

def __repr__(self):
return '<{} for {!r}>'.format(type(self).__name__, self._o)
28 changes: 26 additions & 2 deletions src/widgetastic/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from smartloc import Locator
from wait_for import wait_for

from .browser import Browser
from .browser import Browser, BrowserParentWrapper
from .exceptions import (
NoSuchElementException, LocatorNotImplemented, WidgetOperationFailed, DoNotReadThisWidget)
from .log import PrependParentsAdapter, create_widget_logger, logged
Expand Down Expand Up @@ -387,6 +387,30 @@ def flush_widget_cache(self):
view._widget_cache.clear()
self._widget_cache.clear()

@property
def browser(self):
"""Returns the instance of parent browser.
If the view defines ``__locator__`` or ``ROOT`` then a new wrapper is created that injects
the ``parent=``
Returns:
:py:class:`widgetastic.browser.Browser` instance
Raises:
:py:class:`ValueError` when the browser is not defined, which is an error.
"""
try:
super_browser = super(View, self).browser
if hasattr(self, '__locator__'):
# Wrap it so we have automatic parent injection
return BrowserParentWrapper(self, super_browser)
else:
# This view has no locator, therefore just use the parent browser
return super_browser
except AttributeError:
raise ValueError('Unknown value {!r} specified as parent.'.format(self.parent))

@staticmethod
def nested(view_class):
"""Shortcut for :py:class:`WidgetDescriptor`
Expand Down Expand Up @@ -449,7 +473,7 @@ def is_displayed(self):
:py:class:`bool`
"""
try:
return super(View, self).is_displayed
return self.parent.browser.is_displayed(self)
except LocatorNotImplemented:
return True

Expand Down
64 changes: 64 additions & 0 deletions testing/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from __future__ import unicode_literals
import pytest

from widgetastic.browser import BrowserParentWrapper
from widgetastic.exceptions import NoSuchElementException, LocatorNotImplemented
from widgetastic.widget import View, Text


def test_is_displayed(browser):
Expand Down Expand Up @@ -119,3 +121,65 @@ def test_simple_input_send_keys_clear(browser):
assert browser.get_attribute('value', '#input') == 'test!'
browser.clear('#input')
assert browser.get_attribute('value', '#input') == ''


def test_nested_views_parent_injection(browser):
class MyView(View):
ROOT = '#proper'

class c1(View): # noqa
ROOT = '.c1'

w = Text('.lookmeup')

class c2(View): # noqa
ROOT = '.c2'

w = Text('.lookmeup')

class c3(View): # noqa
ROOT = '.c3'

w = Text('.lookmeup')

class without(View): # noqa
# This one receives the parent browser wrapper
class nested(View): # noqa
# and it should work in multiple levels
pass

view = MyView(browser)
assert isinstance(view.browser, BrowserParentWrapper)
assert view.browser == view.without.browser
assert view.browser == view.without.nested.browser
assert len(view.c1.browser.elements('.lookmeup')) == 1
assert view.c1.w.text == 'C1'
assert view.c1.browser.text('.lookmeup') == 'C1'
assert len(view.c2.browser.elements('.lookmeup')) == 1
assert view.c2.w.text == 'C2'
assert view.c2.browser.text('.lookmeup') == 'C2'
assert len(view.c3.browser.elements('.lookmeup')) == 1
assert view.c3.w.text == 'C3'
assert view.c3.browser.text('.lookmeup') == 'C3'

assert len(view.browser.elements('.lookmeup')) == 3
assert view.c3.browser.text('.lookmeup') == 'C3'


def test_element_force_visibility_check_by_locator(browser):
class MyLocator(object):
CHECK_VISIBILITY = True # Always check visibility no matter what

def __locator__(self):
return '#invisible'

loc = MyLocator()
with pytest.raises(NoSuchElementException):
browser.element(loc)

with pytest.raises(NoSuchElementException):
browser.element(loc, check_visibility=False)

loc.CHECK_VISIBILITY = False # Never check visibility no matter what
browser.element(loc)
browser.element(loc, check_visibility=True)
14 changes: 14 additions & 0 deletions testing/testing_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,19 @@ <h3>test test</h3>
<option value="bar"> Bar </option>
<option value="baz">Baz</option>
</select>
<div id="bogus">
<span class="lookmeup">BAD</span>
</div>
<div id="proper">
<div class="c1">
<span class="lookmeup">C1</span>
</div>
<div class="c2">
<span class="lookmeup">C2</span>
</div>
<div class="c3">
<span class="lookmeup">C3</span>
</div>
</div>
</body>
</html>

0 comments on commit 5b56e27

Please sign in to comment.