Skip to content

Commit 418e35c

Browse files
authored
Merge pull request #368 from seleniumbase/partial-link-text-updates
Add additional methods for handling partial_link_text
2 parents c8d7c5c + 9f8c0b6 commit 418e35c

File tree

5 files changed

+195
-34
lines changed

5 files changed

+195
-34
lines changed

help_docs/method_summary.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@ self.click_chain(selectors_list, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIME
2323

2424
self.is_link_text_present(link_text)
2525

26+
self.is_partial_link_text_present(link_text)
27+
2628
self.get_link_attribute(link_text, attribute, hard_fail)
2729

30+
self.get_partial_link_attribute(link_text, attribute, hard_fail)
31+
2832
self.wait_for_link_text_present(link_text, timeout=settings.SMALL_TIMEOUT)
2933

34+
self.wait_for_partial_link_text_present(link_text, timeout=settings.SMALL_TIMEOUT)
35+
3036
self.click_link_text(link_text, timeout=settings.SMALL_TIMEOUT)
3137

3238
self.click_link(link_text, timeout=settings.SMALL_TIMEOUT)

seleniumbase/console_scripts/run.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def show_basic_usage():
4444
print(" inject-objects [SELENIUMBASE_PYTHON_FILE] [OPTIONS]")
4545
print(" objectify [SELENIUMBASE_PYTHON_FILE] [OPTIONS]")
4646
print(" revert-objects [SELENIUMBASE_PYTHON_FILE]")
47-
print(" download [ITEM]")
47+
print(" download server")
4848
print(" grid-hub [start|stop|restart] [OPTIONS]")
4949
print(" grid-node [start|stop|restart] --hub=[HUB_IP] [OPTIONS]")
5050
print(' * (EXAMPLE: "seleniumbase install chromedriver") *')
@@ -172,13 +172,10 @@ def show_download_usage():
172172
print(" ** download **")
173173
print("")
174174
print(" Usage:")
175-
print(" seleniumbase download [ITEM]")
176-
print(" (Choices: server)")
177-
print(" Example:")
178175
print(" seleniumbase download server")
179176
print(" Output:")
180-
print(" Downloads the specified item.")
181-
print(" (server is required for using your own Selenium Grid)")
177+
print(" Downloads the Selenium Standalone Server.")
178+
print(" (Server is required for using your own Selenium Grid.)")
182179
print("")
183180

184181

seleniumbase/fixtures/base_case.py

Lines changed: 177 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,13 @@ def click(self, selector, by=By.CSS_SELECTOR,
109109
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
110110
timeout = self.__get_new_timeout(timeout)
111111
selector, by = self.__recalculate_selector(selector, by)
112-
if page_utils.is_link_text_selector(selector):
112+
if page_utils.is_link_text_selector(selector) or by == By.LINK_TEXT:
113113
if not self.is_link_text_visible(selector):
114114
# Handle a special case of links hidden in dropdowns
115115
self.click_link_text(selector, timeout=timeout)
116116
return
117-
if page_utils.is_partial_link_text_selector(selector):
117+
if page_utils.is_partial_link_text_selector(selector) or (
118+
by == By.PARTIAL_LINK_TEXT):
118119
if not self.is_partial_link_text_visible(selector):
119120
# Handle a special case of partial links hidden in dropdowns
120121
self.click_partial_link_text(selector, timeout=timeout)
@@ -232,6 +233,17 @@ def is_link_text_present(self, link_text):
232233
return True
233234
return False
234235

236+
def is_partial_link_text_present(self, link_text):
237+
""" Returns True if the partial link appears in the HTML of the page.
238+
The element doesn't need to be visible,
239+
such as elements hidden inside a dropdown selection. """
240+
soup = self.get_beautiful_soup()
241+
html_links = soup.find_all('a')
242+
for html_link in html_links:
243+
if link_text.strip() in html_link.text.strip():
244+
return True
245+
return False
246+
235247
def get_link_attribute(self, link_text, attribute, hard_fail=True):
236248
""" Finds a link by link text and then returns the attribute's value.
237249
If the link text or attribute cannot be found, an exception will
@@ -254,6 +266,31 @@ def get_link_attribute(self, link_text, attribute, hard_fail=True):
254266
else:
255267
return None
256268

269+
def get_partial_link_attribute(self, link_text, attribute, hard_fail=True):
270+
""" Finds a link by partial link text and then returns the attribute's
271+
value. If the partial link text or attribute cannot be found, an
272+
exception will get raised if hard_fail is True (otherwise None
273+
is returned). """
274+
soup = self.get_beautiful_soup()
275+
html_links = soup.find_all('a')
276+
for html_link in html_links:
277+
if link_text.strip() in html_link.text.strip():
278+
if html_link.has_attr(attribute):
279+
attribute_value = html_link.get(attribute)
280+
return attribute_value
281+
if hard_fail:
282+
raise Exception(
283+
'Unable to find attribute {%s} from '
284+
'partial link text {%s}!'
285+
% (attribute, link_text))
286+
else:
287+
return None
288+
if hard_fail:
289+
raise Exception(
290+
"Partial Link text {%s} was not found!" % link_text)
291+
else:
292+
return None
293+
257294
def wait_for_link_text_present(self, link_text,
258295
timeout=settings.SMALL_TIMEOUT):
259296
start_ms = time.time() * 1000.0
@@ -273,6 +310,25 @@ def wait_for_link_text_present(self, link_text,
273310
"Link text {%s} was not present after %s seconds!" % (
274311
link_text, timeout))
275312

313+
def wait_for_partial_link_text_present(self, link_text,
314+
timeout=settings.SMALL_TIMEOUT):
315+
start_ms = time.time() * 1000.0
316+
stop_ms = start_ms + (timeout * 1000.0)
317+
for x in range(int(timeout * 5)):
318+
try:
319+
if not self.is_partial_link_text_present(link_text):
320+
raise Exception(
321+
"Partial Link text {%s} was not found!" % link_text)
322+
return
323+
except Exception:
324+
now_ms = time.time() * 1000.0
325+
if now_ms >= stop_ms:
326+
break
327+
time.sleep(0.2)
328+
raise Exception(
329+
"Partial Link text {%s} was not present after %s seconds!" % (
330+
link_text, timeout))
331+
276332
def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT):
277333
""" This method clicks link text on a page """
278334
# If using phantomjs, might need to extract and open the link directly
@@ -387,20 +443,68 @@ def click_partial_link_text(self, partial_link_text,
387443
'{%s}' % partial_link_text)
388444
raise Exception(
389445
"Partial link text {%s} was not found!" % partial_link_text)
390-
# Not using phantomjs
391-
element = self.wait_for_partial_link_text(
392-
partial_link_text, timeout=timeout)
393-
self.__demo_mode_highlight_if_active(
394-
partial_link_text, by=By.PARTIAL_LINK_TEXT)
395-
pre_action_url = self.driver.current_url
446+
if not self.is_partial_link_text_present(partial_link_text):
447+
self.wait_for_partial_link_text_present(
448+
partial_link_text, timeout=timeout)
449+
pre_action_url = self.get_current_url()
396450
try:
397-
element.click()
398-
except (StaleElementReferenceException, ENI_Exception):
399-
self.wait_for_ready_state_complete()
400-
time.sleep(0.05)
401451
element = self.wait_for_partial_link_text(
402-
partial_link_text, timeout=timeout)
403-
element.click()
452+
partial_link_text, timeout=0.2)
453+
self.__demo_mode_highlight_if_active(
454+
partial_link_text, by=By.LINK_TEXT)
455+
try:
456+
element.click()
457+
except (StaleElementReferenceException, ENI_Exception):
458+
self.wait_for_ready_state_complete()
459+
time.sleep(0.05)
460+
element = self.wait_for_partial_link_text(
461+
partial_link_text, timeout=timeout)
462+
element.click()
463+
except Exception:
464+
found_css = False
465+
text_id = self.get_partial_link_attribute(
466+
partial_link_text, "id", False)
467+
if text_id:
468+
link_css = '[id="%s"]' % partial_link_text
469+
found_css = True
470+
471+
if not found_css:
472+
href = self.__get_href_from_partial_link_text(
473+
partial_link_text, False)
474+
if href:
475+
if href.startswith('/') or page_utils.is_valid_url(href):
476+
link_css = '[href="%s"]' % href
477+
found_css = True
478+
479+
if not found_css:
480+
ngclick = self.get_partial_link_attribute(
481+
partial_link_text, "ng-click", False)
482+
if ngclick:
483+
link_css = '[ng-click="%s"]' % ngclick
484+
found_css = True
485+
486+
if not found_css:
487+
onclick = self.get_partial_link_attribute(
488+
partial_link_text, "onclick", False)
489+
if onclick:
490+
link_css = '[onclick="%s"]' % onclick
491+
found_css = True
492+
493+
success = False
494+
if found_css:
495+
if self.is_element_visible(link_css):
496+
self.click(link_css)
497+
success = True
498+
else:
499+
# The link text might be hidden under a dropdown menu
500+
success = self.__click_dropdown_partial_link_text(
501+
partial_link_text, link_css)
502+
503+
if not success:
504+
element = self.wait_for_link_text_visible(
505+
partial_link_text, timeout=settings.MINI_TIMEOUT)
506+
element.click()
507+
404508
if settings.WAIT_FOR_RSC_ON_CLICKS:
405509
self.wait_for_ready_state_complete()
406510
if self.demo_mode:
@@ -3064,29 +3168,75 @@ def __get_href_from_link_text(self, link_text, hard_fail=True):
30643168
def __click_dropdown_link_text(self, link_text, link_css):
30653169
""" When a link may be hidden under a dropdown menu, use this. """
30663170
soup = self.get_beautiful_soup()
3067-
drop_down_list = soup.select('[class*=dropdown]')
3068-
for item in soup.select('[class*=HeaderMenu]'):
3069-
drop_down_list.append(item)
3070-
for item in soup.select('[class*=menu-item]'):
3171+
drop_down_list = []
3172+
for item in soup.select('li[class]'):
30713173
drop_down_list.append(item)
3072-
for item in soup.select('[class*=chevron]'):
3174+
csstype = link_css.split('[')[1].split('=')[0]
3175+
for item in drop_down_list:
3176+
item_text_list = item.text.split('\n')
3177+
if link_text in item_text_list and csstype in item.decode():
3178+
dropdown_css = ""
3179+
try:
3180+
for css_class in item['class']:
3181+
dropdown_css += '.'
3182+
dropdown_css += css_class
3183+
except Exception:
3184+
continue
3185+
dropdown_css = item.name + dropdown_css
3186+
matching_dropdowns = self.find_visible_elements(dropdown_css)
3187+
for dropdown in matching_dropdowns:
3188+
# The same class names might be used for multiple dropdowns
3189+
try:
3190+
if dropdown.is_displayed():
3191+
page_actions.hover_element_and_click(
3192+
self.driver, dropdown, link_text,
3193+
click_by=By.LINK_TEXT, timeout=0.12)
3194+
return True
3195+
except Exception:
3196+
pass
3197+
return False
3198+
3199+
def __get_href_from_partial_link_text(self, link_text, hard_fail=True):
3200+
href = self.get_partial_link_attribute(link_text, "href", hard_fail)
3201+
if not href:
3202+
return None
3203+
if href.startswith('//'):
3204+
link = "http:" + href
3205+
elif href.startswith('/'):
3206+
url = self.driver.current_url
3207+
domain_url = self.get_domain_url(url)
3208+
link = domain_url + href
3209+
else:
3210+
link = href
3211+
return link
3212+
3213+
def __click_dropdown_partial_link_text(self, link_text, link_css):
3214+
""" When a partial link may be hidden under a dropdown, use this. """
3215+
soup = self.get_beautiful_soup()
3216+
drop_down_list = []
3217+
for item in soup.select('li[class]'):
30733218
drop_down_list.append(item)
30743219
csstype = link_css.split('[')[1].split('=')[0]
30753220
for item in drop_down_list:
3076-
if link_text in item.text.split('\n') and csstype in item.decode():
3221+
item_text_list = item.text.split('\n')
3222+
if link_text in item_text_list and csstype in item.decode():
30773223
dropdown_css = ""
3078-
for css_class in item['class']:
3079-
dropdown_css += '.'
3080-
dropdown_css += css_class
3224+
try:
3225+
for css_class in item['class']:
3226+
dropdown_css += '.'
3227+
dropdown_css += css_class
3228+
except Exception:
3229+
continue
30813230
dropdown_css = item.name + dropdown_css
30823231
matching_dropdowns = self.find_visible_elements(dropdown_css)
30833232
for dropdown in matching_dropdowns:
30843233
# The same class names might be used for multiple dropdowns
30853234
try:
3086-
page_actions.hover_element_and_click(
3087-
self.driver, dropdown, link_text,
3088-
click_by=By.LINK_TEXT, timeout=0.2)
3089-
return True
3235+
if dropdown.is_displayed():
3236+
page_actions.hover_element_and_click(
3237+
self.driver, dropdown, link_text,
3238+
click_by=By.PARTIAL_LINK_TEXT, timeout=0.12)
3239+
return True
30903240
except Exception:
30913241
pass
30923242
return False

seleniumbase/utilities/selenium_ide/convert_ide.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,14 @@ def main():
427427
if data:
428428
whitespace = data.group(1)
429429
xpath = '%s' % data.group(2)
430+
if './/*[normalize-space(text())' in xpath and (
431+
"normalize-space(.)='" in xpath):
432+
x_match = re.match(
433+
r'''^[\S\s]+normalize-'''
434+
r'''space\(\.\)=\'([\S\s]+)\'\]\)[\S\s]+''', xpath)
435+
if x_match:
436+
partial_link_text = x_match.group(1)
437+
xpath = "partial_link=%s" % partial_link_text
430438
uni = ""
431439
if '(u"' in line:
432440
uni = "u"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
setup(
1919
name='seleniumbase',
20-
version='1.31.3',
20+
version='1.31.4',
2121
description='Fast, Easy, and Reliable Browser Automation & Testing.',
2222
long_description=long_description,
2323
long_description_content_type='text/markdown',

0 commit comments

Comments
 (0)