Skip to content

Commit

Permalink
Merge pull request #51 from duncanjbrown/follow-link-with-text
Browse files Browse the repository at this point in the history
Support a `text=` argument for `follow_link`
  • Loading branch information
spookylukey committed Jan 10, 2024
2 parents 488143f + 19aa9f9 commit 5918c79
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 18 deletions.
6 changes: 3 additions & 3 deletions docs/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ followed the same pattern.

The current full URL

.. method:: follow_link(css_selector)
.. method:: follow_link(css_selector=None, text=None)

Follows the link specified in the CSS selector.
Follows the link specified in the CSS selector or the specified text.

You will get an exception if no links match
You will get an exception if no links match.

For :class:`django_functest.FuncWebTestMixin`, you will get an exception if multiple
links match and they don't have the same href.
Expand Down
4 changes: 2 additions & 2 deletions src/django_functest/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ def current_url(self):
"""
raise NotImplementedError()

def follow_link(self, css_selector):
def follow_link(self, css_selector=None, text=None):
"""
Follows the link specified in the CSS selector.
Follows the link specified by CSS in css_selector= or matching the text in text=
"""
raise NotImplementedError()

Expand Down
27 changes: 22 additions & 5 deletions src/django_functest/funcselenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,18 @@ def current_url(self):
"""
return self._driver.current_url

def follow_link(self, css_selector):
def follow_link(self, css_selector=None, text=None):
"""
Follows the link specified in the CSS selector.
Follows the link specified by CSS in css_selector= or matching the text in text=
"""
return self.click(css_selector, wait_for_reload=True)
if css_selector is not None and text is not None:
raise ValueError("pass only one of text= or css_selector= to follow_link")
elif css_selector is not None:
return self.click(css_selector=css_selector, wait_for_reload=True)
elif text is not None:
return self.click(link_text=text, wait_for_reload=True)
else:
raise ValueError("follow_link requires either a text= or css_selector= argument")

def fill(self, fields, scroll=NotPassed):
"""
Expand Down Expand Up @@ -295,6 +302,7 @@ def click(
xpath=None,
text=None,
text_parent_id=None,
link_text=None,
wait_for_reload=False,
wait_timeout=None,
double=False,
Expand All @@ -319,6 +327,7 @@ def click(
xpath=xpath,
text=text,
text_parent_id=text_parent_id,
link_text=link_text,
timeout=wait_timeout,
)
if _expect_form and elem.tag_name == "form":
Expand Down Expand Up @@ -496,6 +505,7 @@ def wait_until_loaded(
xpath=None,
text=None,
text_parent_id=None,
link_text=None,
timeout=None,
):
"""
Expand All @@ -508,6 +518,7 @@ def wait_until_loaded(
xpath=xpath,
text=text,
text_parent_id=text_parent_id,
link_text=link_text,
),
timeout=timeout,
)
Expand Down Expand Up @@ -573,7 +584,7 @@ def _driver(self):
else:
return self._cls_driver

def _get_finder(self, css_selector=None, xpath=None, text=None, text_parent_id=None):
def _get_finder(self, css_selector=None, xpath=None, text=None, text_parent_id=None, link_text=None):
def _find_by_css(driver):
css_selectors = [css_selector] if isinstance(css_selector, str) else list(css_selector)
first_selector, *remaining_selectors = css_selectors
Expand All @@ -593,6 +604,8 @@ def _find_by_css(driver):
prefix = ""
_xpath = prefix + f'//*[contains(text(), "{text}")]'
return lambda driver: driver.find_element(By.XPATH, _xpath)
if link_text is not None:
return lambda driver: driver.find_element(By.PARTIAL_LINK_TEXT, link_text)
raise AssertionError("No selector passed in")

def _get_url_raw(self, url):
Expand Down Expand Up @@ -681,11 +694,12 @@ def _fill_input_by_text(self, elem, val, scroll=True):
else:
raise SeleniumCantUseElement(f"Can't do 'fill_by_text' on elements of type {elem.tag_name}")

def _find(self, css_selector=None, xpath=None, text=None, text_parent_id=None):
def _find(self, css_selector=None, xpath=None, text=None, link_text=None, text_parent_id=None):
return self._get_finder(
css_selector=css_selector,
xpath=xpath,
text=text,
link_text=link_text,
text_parent_id=text_parent_id,
)(self._driver)

Expand All @@ -695,6 +709,7 @@ def _find_with_timeout(
xpath=None,
text=None,
text_parent_id=None,
link_text=None,
timeout=None,
):
if timeout != 0:
Expand All @@ -703,12 +718,14 @@ def _find_with_timeout(
xpath=xpath,
text=text,
text_parent_id=text_parent_id,
link_text=link_text,
timeout=timeout,
)
return self._find(
css_selector=css_selector,
xpath=xpath,
text=text,
link_text=link_text,
text_parent_id=text_parent_id,
)

Expand Down
29 changes: 22 additions & 7 deletions src/django_functest/funcwebtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,28 @@ def current_url(self):
"""
return self.last_response.request.url

def follow_link(self, css_selector):
"""
Follows the link specified in the CSS selector.
"""
elems = self._make_pq(self.last_response).find(css_selector)
if len(elems) == 0:
raise WebTestNoSuchElementException(f"Can't find element matching '{css_selector}'")
def follow_link(self, css_selector=None, text=None):
"""
Follows the link specified by CSS in css_selector= or matching the text in text=
"""
if css_selector is not None and text is not None:
raise ValueError("pass only one of text= or css_selector= to follow_link")
elif css_selector is not None:
elems = self._make_pq(self.last_response).find(css_selector)
if len(elems) == 0:
raise WebTestNoSuchElementException(f"Can't find element matching '{css_selector}'")
elif text is not None:
# cssselect (via PyQuery) handles the implementation of :contains()
# and doesn't do any escaping, so we escape here
escaped_text = text.replace('"', '\\"')
css_expr = f'a:contains("{escaped_text}")'

elems = self._make_pq(self.last_response).find(css_expr)

if len(elems) == 0:
raise WebTestNoSuchElementException(f"Can't find a link with the text '{text}'")
else:
raise ValueError("follow_link requires either a text= or css_selector= argument")

hrefs = []
for e in elems:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{% for thing in things %}
<p><input type="checkbox" name="select_thing" value="{{ thing.id }}" {% if thing in selected_things %}checked{% endif %}></p>
<p><a class="edit" href="{% url "edit_thing" thing_id=thing.id %}">Edit {{ thing.name }}</a></p>
<p><a class="edit" href="{% url "edit_thing" thing_id=thing.id %}">Edit {{ thing.name }} "with quotes"</a></p>
{% endfor %}
</div>
<input type="submit" name="select" value="select">
Expand Down
35 changes: 34 additions & 1 deletion tests/django_functest_tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,44 @@ def test_follow_link(self):
self.follow_link("a.edit")
self.assertUrlsEqual(reverse("edit_thing", kwargs={"thing_id": self.thing.id}))

def test_follow_link_not_found(self):
# this has been the default behaviour for the first positional argument but
# now that it's optional as we have text=, write the explicit css_selector
# version into the contract expressed by this test
def test_follow_link_css_selector(self):
self.get_url("list_things")
self.follow_link(css_selector="a.edit")
self.assertUrlsEqual(reverse("edit_thing", kwargs={"thing_id": self.thing.id}))

def test_follow_link_text(self):
self.get_url("list_things")
self.follow_link(text="Edit Rock")
self.assertUrlsEqual(reverse("edit_thing", kwargs={"thing_id": self.thing.id}))

def test_follow_link_text_with_quotes(self):
self.get_url("list_things")
self.follow_link(text='Edit Rock "with quotes"')
self.assertUrlsEqual(reverse("edit_thing", kwargs={"thing_id": self.thing.id}))

def test_follow_link_css_not_found(self):
self.get_url("list_things")
with pytest.raises(self.ElementNotFoundException):
self.follow_link("a.foobar")

def test_follow_link_text_not_found(self):
self.get_url("list_things")
with pytest.raises(self.ElementNotFoundException):
self.follow_link(text="this text is not on the page")

def test_follow_link_unspecified(self):
self.get_url("list_things")
with pytest.raises(ValueError, match="either a text= or css_selector="):
self.follow_link(None)

def test_follow_link_both_specified(self):
self.get_url("list_things")
with pytest.raises(ValueError, match="only one of"):
self.follow_link(css_selector="x", text="y")

def test_follow_link_path_relative(self):
self.get_url("test_misc")
self.follow_link('a[href="."]')
Expand Down

0 comments on commit 5918c79

Please sign in to comment.