Skip to content

Commit 2e7e8bc

Browse files
authored
Merge pull request #159 from seleniumbase/improve-javascript-actions
Improve JavaScript actions
2 parents aa741cf + b0b3ac2 commit 2e7e8bc

File tree

5 files changed

+161
-26
lines changed

5 files changed

+161
-26
lines changed

help_docs/method_summary.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ self.scroll_click(selector, by=By.CSS_SELECTOR) # DEPRECATED
100100

101101
self.click_xpath(xpath)
102102

103+
self.js_click(selector, by=By.CSS_SELECTOR)
104+
103105
self.jquery_click(selector, by=By.CSS_SELECTOR)
104106

105107
self.hide_element(selector, by=By.CSS_SELECTOR)
@@ -124,13 +126,13 @@ self.download_file(file_url, destination_folder=None)
124126

125127
self.save_file_as(file_url, new_file_name, destination_folder=None)
126128

127-
self.get_downloads_folder(file):
129+
self.get_downloads_folder(file)
128130

129-
self.get_path_of_downloaded_file(file):
131+
self.get_path_of_downloaded_file(file)
130132

131-
self.is_downloaded_file_present(file):
133+
self.is_downloaded_file_present(file)
132134

133-
self.assert_downloaded_file(file):
135+
self.assert_downloaded_file(file)
134136

135137
self.convert_xpath_to_css(xpath)
136138

@@ -139,6 +141,9 @@ self.convert_to_css_selector(selector, by)
139141
self.set_value(selector, new_value, by=By.CSS_SELECTOR,
140142
timeout=settings.SMALL_TIMEOUT)
141143

144+
self.js_update_text(selector, new_value, by=By.CSS_SELECTOR,
145+
timeout=settings.LARGE_TIMEOUT)
146+
142147
self.jquery_update_text_value(selector, new_value, by=By.CSS_SELECTOR,
143148
timeout=settings.SMALL_TIMEOUT)
144149

integrations/selenium_ide/convert_ide.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def main():
131131
if data:
132132
whitespace = data.group(1)
133133
css_selector = '#%s' % data.group(2)
134+
css_selector = css_selector.replace('[', '\\[').replace(']', '\\]')
134135
command = '''%sself.click('%s')''' % (whitespace, css_selector)
135136
seleniumbase_lines.append(command)
136137
continue
@@ -142,6 +143,7 @@ def main():
142143
if data:
143144
whitespace = data.group(1)
144145
css_selector = '#%s' % data.group(2)
146+
css_selector = css_selector.replace('[', '\\[').replace(']', '\\]')
145147
text = data.group(3)
146148
command = '''%sself.update_text('%s', '%s')''' % (
147149
whitespace, css_selector, text)
@@ -156,6 +158,7 @@ def main():
156158
uses_keys = True
157159
whitespace = data.group(1)
158160
css_selector = '#%s' % data.group(2)
161+
css_selector = css_selector.replace('[', '\\[').replace(']', '\\]')
159162
key = 'Keys.%s' % data.group(3)
160163
command = '''%sself.send_keys('%s', %s)''' % (
161164
whitespace, css_selector, key)

seleniumbase/fixtures/base_case.py

Lines changed: 120 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ def test_anything(self):
2222
"""
2323

2424
import getpass
25-
import json
2625
import logging
2726
import math
2827
import os
@@ -122,6 +121,17 @@ def click(self, selector, by=By.CSS_SELECTOR,
122121
element = page_actions.wait_for_element_visible(
123122
self.driver, selector, by, timeout=timeout)
124123
element.click()
124+
except WebDriverException:
125+
self.wait_for_ready_state_complete()
126+
if not by == By.LINK_TEXT:
127+
# Only use a JavaScript click if not clicking by Link Text
128+
self.__js_click(selector, by=by)
129+
else:
130+
# One more attempt to click on the element
131+
element = page_actions.wait_for_element_visible(
132+
self.driver, selector, by, timeout=timeout)
133+
element.click()
134+
125135
if settings.WAIT_FOR_RSC_ON_CLICKS:
126136
self.wait_for_ready_state_complete()
127137
if self.demo_mode:
@@ -489,6 +499,20 @@ def add_text(self, selector, new_value, by=By.CSS_SELECTOR,
489499
element.send_keys(Keys.RETURN)
490500
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
491501
self.wait_for_ready_state_complete()
502+
except Exception:
503+
exc_message = self._get_exception_message()
504+
update = ("Your version of ChromeDriver may be out-of-date! "
505+
"Please go to "
506+
"https://sites.google.com/a/chromium.org/chromedriver/ "
507+
"and download the latest version to your system PATH! "
508+
"Original Exception Message: %s" % exc_message)
509+
using_old_chromedriver = False
510+
if "unknown error: call function result missing" in exc_message:
511+
using_old_chromedriver = True
512+
if self.browser == 'chrome' and using_old_chromedriver:
513+
raise Exception(update)
514+
else:
515+
raise Exception(exc_message)
492516
if self.demo_mode:
493517
if self.driver.current_url != pre_action_url:
494518
self._demo_mode_pause_if_active()
@@ -512,7 +536,7 @@ def update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
512536
new_value - the new value for setting the text field
513537
by - the type of selector to search by (Default: CSS)
514538
timeout - how long to wait for the selector to be visible
515-
retry - if True, use jquery if the selenium text update fails
539+
retry - if True, use JS if the selenium text update fails
516540
"""
517541
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
518542
timeout = self._get_new_timeout(timeout)
@@ -531,6 +555,8 @@ def update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
531555
element = self.wait_for_element_visible(
532556
selector, by=by, timeout=timeout)
533557
element.clear()
558+
except Exception:
559+
pass # Clearing the text field first isn't critical
534560
self._demo_mode_pause_if_active(tiny=True)
535561
pre_action_url = self.driver.current_url
536562
try:
@@ -556,10 +582,23 @@ def update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
556582
element.send_keys(Keys.RETURN)
557583
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
558584
self.wait_for_ready_state_complete()
585+
except Exception:
586+
exc_message = self._get_exception_message()
587+
update = ("Your version of ChromeDriver may be out-of-date! "
588+
"Please go to "
589+
"https://sites.google.com/a/chromium.org/chromedriver/ "
590+
"and download the latest version to your system PATH! "
591+
"Original Exception Message: %s" % exc_message)
592+
using_old_chromedriver = False
593+
if "unknown error: call function result missing" in exc_message:
594+
using_old_chromedriver = True
595+
if self.browser == 'chrome' and using_old_chromedriver:
596+
raise Exception(update)
597+
else:
598+
raise Exception(exc_message)
559599
if (retry and element.get_attribute('value') != new_value and (
560600
not new_value.endswith('\n'))):
561-
logging.debug('update_text_value is falling back to jQuery!')
562-
selector = re.escape(selector)
601+
logging.debug('update_text() is falling back to JavaScript!')
563602
self.set_value(selector, new_value, by=by)
564603
if self.demo_mode:
565604
if self.driver.current_url != pre_action_url:
@@ -772,7 +811,7 @@ def bring_to_front(self, selector, by=By.CSS_SELECTOR):
772811

773812
def highlight(self, selector, by=By.CSS_SELECTOR,
774813
loops=settings.HIGHLIGHTS, scroll=True):
775-
""" This method uses fancy javascript to highlight an element.
814+
""" This method uses fancy JavaScript to highlight an element.
776815
Used during demo_mode.
777816
@Params
778817
selector - the selector of the element to find
@@ -930,13 +969,37 @@ def scroll_click(self, selector, by=By.CSS_SELECTOR):
930969
def click_xpath(self, xpath):
931970
self.click(xpath, by=By.XPATH)
932971

972+
def js_click(self, selector, by=By.CSS_SELECTOR):
973+
""" Clicks an element using pure JS. Does not use jQuery. """
974+
selector, by = self._recalculate_selector(selector, by)
975+
if by == By.LINK_TEXT:
976+
message = (
977+
"Pure JavaScript doesn't support clicking by Link Text. "
978+
"You may want to use self.jquery_click() instead, which "
979+
"allows this with :contains(), assuming jQuery isn't blocked. "
980+
"For now, self.js_click() will use a regular WebDriver click.")
981+
logging.debug(message)
982+
self.click(selector, by=by)
983+
return
984+
element = self.wait_for_element_present(
985+
selector, by=by, timeout=settings.SMALL_TIMEOUT)
986+
if self.is_element_visible(selector, by=by):
987+
self._demo_mode_highlight_if_active(selector, by)
988+
if not self.demo_mode:
989+
self._scroll_to_element(element)
990+
css_selector = self.convert_to_css_selector(selector, by=by)
991+
css_selector = re.escape(css_selector)
992+
self.__js_click(selector, by=by) # The real "magic" happens here
993+
self._demo_mode_pause_if_active()
994+
933995
def jquery_click(self, selector, by=By.CSS_SELECTOR):
996+
""" Clicks an element using jQuery. Different from using pure JS. """
934997
selector, by = self._recalculate_selector(selector, by)
935-
selector = self.convert_to_css_selector(selector, by=by)
936998
self.wait_for_element_present(
937999
selector, by=by, timeout=settings.SMALL_TIMEOUT)
9381000
if self.is_element_visible(selector, by=by):
9391001
self._demo_mode_highlight_if_active(selector, by)
1002+
selector = self.convert_to_css_selector(selector, by=by)
9401003
selector = self._make_css_match_first_element_only(selector)
9411004
click_script = """jQuery('%s')[0].click()""" % selector
9421005
self.safe_execute_script(click_script)
@@ -1056,7 +1119,8 @@ def convert_xpath_to_css(self, xpath):
10561119
def convert_to_css_selector(self, selector, by):
10571120
""" This method converts a selector to a CSS_SELECTOR.
10581121
jQuery commands require a CSS_SELECTOR for finding elements.
1059-
This method should only be used for jQuery actions. """
1122+
This method should only be used for jQuery/JavaScript actions.
1123+
Pure JavaScript doesn't support using a:contains("LINK_TEXT"). """
10601124
if by == By.CSS_SELECTOR:
10611125
return selector
10621126
elif by == By.ID:
@@ -1080,21 +1144,21 @@ def convert_to_css_selector(self, selector, by):
10801144

10811145
def set_value(self, selector, new_value, by=By.CSS_SELECTOR,
10821146
timeout=settings.LARGE_TIMEOUT):
1083-
""" This method uses jQuery to update a text field.
1084-
Similar to jquery_update_text_value(), but the element
1085-
doesn't need to be officially visible to work. """
1147+
""" This method uses JavaScript to update a text field. """
10861148
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
10871149
timeout = self._get_new_timeout(timeout)
10881150
if page_utils.is_xpath_selector(selector):
10891151
by = By.XPATH
10901152
orginal_selector = selector
1091-
selector = self.convert_to_css_selector(selector, by=by)
1092-
self._demo_mode_highlight_if_active(selector, by)
1093-
self.scroll_to(selector, by=by, timeout=timeout)
1094-
value = json.dumps(new_value)
1095-
selector = self._make_css_match_first_element_only(selector)
1096-
set_value_script = """jQuery('%s').val(%s)""" % (selector, value)
1097-
self.safe_execute_script(set_value_script)
1153+
css_selector = self.convert_to_css_selector(selector, by=by)
1154+
self._demo_mode_highlight_if_active(orginal_selector, by)
1155+
if not self.demo_mode:
1156+
self.scroll_to(orginal_selector, by=by, timeout=timeout)
1157+
value = re.escape(new_value)
1158+
css_selector = re.escape(css_selector)
1159+
script = ("""document.querySelector('%s').value='%s';"""
1160+
% (css_selector, value))
1161+
self.execute_script(script)
10981162
if new_value.endswith('\n'):
10991163
element = self.wait_for_element_present(
11001164
orginal_selector, by=by, timeout=timeout)
@@ -1103,12 +1167,20 @@ def set_value(self, selector, new_value, by=By.CSS_SELECTOR,
11031167
self.wait_for_ready_state_complete()
11041168
self._demo_mode_pause_if_active()
11051169

1170+
def js_update_text(self, selector, new_value, by=By.CSS_SELECTOR,
1171+
timeout=settings.LARGE_TIMEOUT):
1172+
""" Same as self.set_value() """
1173+
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
1174+
timeout = self._get_new_timeout(timeout)
1175+
self.set_value(
1176+
selector, new_value, by=by, timeout=timeout)
1177+
11061178
def jquery_update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
11071179
timeout=settings.LARGE_TIMEOUT):
11081180
""" This method uses jQuery to update a text field.
11091181
If the new_value string ends with the newline character,
11101182
WebDriver will finish the call, which simulates pressing
1111-
{Enter/Return} after the text is entered. """
1183+
{Enter/Return} after the text is entered. """
11121184
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
11131185
timeout = self._get_new_timeout(timeout)
11141186
if page_utils.is_xpath_selector(selector):
@@ -1128,7 +1200,7 @@ def jquery_update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
11281200

11291201
def jquery_update_text(self, selector, new_value, by=By.CSS_SELECTOR,
11301202
timeout=settings.LARGE_TIMEOUT):
1131-
""" The shorter version of jquery_update_text_value()
1203+
""" The shorter version of self.jquery_update_text_value()
11321204
(The longer version remains for backwards compatibility.) """
11331205
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
11341206
timeout = self._get_new_timeout(timeout)
@@ -1138,6 +1210,9 @@ def jquery_update_text(self, selector, new_value, by=By.CSS_SELECTOR,
11381210
def hover_on_element(self, selector, by=By.CSS_SELECTOR):
11391211
if page_utils.is_xpath_selector(selector):
11401212
by = By.XPATH
1213+
if page_utils.is_link_text_selector(selector):
1214+
selector = page_utils.get_link_text_from_selector(selector)
1215+
by = By.LINK_TEXT
11411216
self.wait_for_element_visible(
11421217
selector, by=by, timeout=settings.SMALL_TIMEOUT)
11431218
self._demo_mode_highlight_if_active(selector, by)
@@ -1154,6 +1229,14 @@ def hover_and_click(self, hover_selector, click_selector,
11541229
hover_by = By.XPATH
11551230
if page_utils.is_xpath_selector(click_selector):
11561231
click_by = By.XPATH
1232+
if page_utils.is_link_text_selector(hover_selector):
1233+
hover_selector = page_utils.get_link_text_from_selector(
1234+
hover_selector)
1235+
hover_by = By.LINK_TEXT
1236+
if page_utils.is_link_text_selector(click_selector):
1237+
click_selector = page_utils.get_link_text_from_selector(
1238+
click_selector)
1239+
click_by = By.LINK_TEXT
11571240
self.wait_for_element_visible(
11581241
hover_selector, by=hover_by, timeout=timeout)
11591242
self._demo_mode_highlight_if_active(hover_selector, hover_by)
@@ -1740,6 +1823,24 @@ def process_checks(self, print_only=False):
17401823

17411824
############
17421825

1826+
def __js_click(self, selector, by=By.CSS_SELECTOR):
1827+
""" Clicks an element using pure JS. Does not use jQuery. """
1828+
selector, by = self._recalculate_selector(selector, by)
1829+
css_selector = self.convert_to_css_selector(selector, by=by)
1830+
css_selector = re.escape(css_selector)
1831+
script = ("""var simulateClick = function (elem) {
1832+
var evt = new MouseEvent('click', {
1833+
bubbles: true,
1834+
cancelable: true,
1835+
view: window
1836+
});
1837+
var canceled = !elem.dispatchEvent(evt);
1838+
};
1839+
var someLink = document.querySelector('%s');
1840+
simulateClick(someLink);"""
1841+
% css_selector)
1842+
self.execute_script(script)
1843+
17431844
def _get_href_from_link_text(self, link_text, hard_fail=True):
17441845
href = self.get_link_attribute(link_text, "href", hard_fail)
17451846
if not href:
@@ -1815,8 +1916,6 @@ def _pick_select_option(self, dropdown_selector, option,
18151916
else:
18161917
self._demo_mode_pause_if_active(tiny=True)
18171918

1818-
############
1819-
18201919
def _recalculate_selector(self, selector, by):
18211920
# Try to determine the type of selector automatically
18221921
if page_utils.is_xpath_selector(selector):

seleniumbase/fixtures/xpath_to_css.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ class XpathException(Exception):
3535
pass
3636

3737

38+
def _handle_brackets_in_strings(xpath):
39+
# Edge Case: Brackets in strings.
40+
# Example from GitHub.com -
41+
# '<input type="text" id="user[login]">' => '//*[@id="user[login]"]'
42+
# Need to tell apart string-brackets from regular brackets
43+
new_xpath = ""
44+
chunks = xpath.split('"')
45+
len_chunks = len(chunks)
46+
for chunk_num in range(len_chunks):
47+
if chunk_num % 2 != 0:
48+
chunks[chunk_num] = chunks[chunk_num].replace(
49+
'[', '_STR_L_bracket_')
50+
chunks[chunk_num] = chunks[chunk_num].replace(
51+
']', '_STR_R_bracket_')
52+
new_xpath += chunks[chunk_num]
53+
if chunk_num != len_chunks - 1:
54+
new_xpath += '"'
55+
xpath = new_xpath
56+
return xpath
57+
58+
3859
def _filter_xpath_grouping(xpath):
3960
"""
4061
This method removes the outer parentheses for xpath grouping.
@@ -108,6 +129,9 @@ def _get_raw_css_from_xpath(xpath):
108129

109130

110131
def convert_xpath_to_css(xpath):
132+
if xpath[0] != '"' and xpath[-1] != '"' and xpath.count('"') % 2 == 0:
133+
xpath = _handle_brackets_in_strings(xpath)
134+
111135
if xpath.startswith('('):
112136
xpath = _filter_xpath_grouping(xpath)
113137

@@ -124,4 +148,8 @@ def convert_xpath_to_css(xpath):
124148
new_attr_def = attr_def[:q1] + "'" + attr_def[q1:q2] + "']"
125149
css = css.replace(attr_def, new_attr_def)
126150

151+
# Replace the string-brackets with escaped ones
152+
css = css.replace('_STR_L_bracket_', '\\[')
153+
css = css.replace('_STR_R_bracket_', '\\]')
154+
127155
return css

setup.py

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

88
setup(
99
name='seleniumbase',
10-
version='1.8.5',
10+
version='1.8.6',
1111
description='Web Automation & Testing Framework - http://seleniumbase.com',
1212
long_description='Web Automation and Testing Framework - seleniumbase.com',
1313
platforms='Mac * Windows * Linux * Docker',

0 commit comments

Comments
 (0)