diff --git a/docs/javascript.rst b/docs/javascript.rst
index 33b074f1e..2f896aab2 100644
--- a/docs/javascript.rst
+++ b/docs/javascript.rst
@@ -3,47 +3,106 @@
license that can be found in the LICENSE file.
.. meta::
- :description: Executing javascript
+ :description: Execute JavaScript In The Browser
:keywords: splinter, python, tutorial, javascript
++++++++++++++++++
Execute JavaScript
++++++++++++++++++
-You can easily execute JavaScript, in drivers which support it:
+When using WebDriver-based drivers, you can run JavaScript inside the web
+browser.
+
+Execute
+=======
+
+The `execute_script()` method takes a string containing JavaScript code and
+executes it.
+
+JSON-serializable objects and WebElements can be sent to the browser and used
+by the JavaScript.
+
+Examples
+--------
+
+Change the Background Color of an Element
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. highlight:: python
::
- browser.execute_script("$('body').empty()")
+ browser = Browser()
+
+ browser.execute_script(
+ "document.querySelector('body').setAttribute('style', 'background-color: red')",
+ )
-You can return the result of the script:
+Sending a WebElement to the browser
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. highlight:: python
::
- browser.evaluate_script("4+4") == 8
+ browser = Browser()
+
+ elem = browser.find_by_tag('body').first
+ browser.execute_script(
+ "arguments[0].setAttribute('style', 'background-color: red')",
+ elem,
+ )
+
+
+
+Evaluate
+========
+
+The `evaluate_script()` method takes a string containing a JavaScript
+expression and runs it, then returns the result.
+
+JSON-serializable objects and WebElements can be sent to the browser and used
+by the JavaScript.
+
+Examples
+--------
+
+Get the href from the browser
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. highlight:: python
+
+::
+
+ browser = Browser()
+
+ href = browser.evaluate_script("document.location.href")
+
+Cookbook
+========
-Example: Manipulate text fields with JavaScript
-+++++++++++++++++++++++++++++++++++++++++++++++++
+Manipulate text fields with JavaScript
+--------------------------------------
-Some text input actions cannot be "typed" thru ``browser.fill()``, like new lines and tab characters. Below is en example how to work around this using ``browser.execute_script()``. This is also much faster than ``browser.fill()`` as there is no simulated key typing delay, making it suitable for longer texts.
+Some text input actions cannot be "typed" thru ``browser.fill()``, like new lines and tab characters.
+Below is en example how to work around this using ``browser.execute_script()``.
+This is also much faster than ``browser.fill()`` as there is no simulated key typing delay, making it suitable for longer texts.
::
- def fast_fill_by_javascript(browser: DriverAPI, elem_id: str, text: str):
+ def fast_fill(browser, query: str, text: str):
"""Fill text field with copy-paste, not by typing key by key.
Otherwise you cannot type enter or tab.
- :param id: CSS id of the textarea element to fill
+ Arguments:
+ query: CSS id of the textarea element to fill
"""
text = text.replace("\t", "\\t")
text = text.replace("\n", "\\n")
- # Construct a JavaScript snippet that is executed on the browser sdie
- snippet = f"""document.querySelector("#{elem_id}").value = "{text}";"""
- browser.execute_script(snippet)
+ elem = browser.find_by_css(query).first
+ # Construct a JavaScript snippet that is executed on the browser side
+ script = f"arguments[0].value = "{text}";"
+ browser.execute_script(script, elem)
diff --git a/splinter/driver/__init__.py b/splinter/driver/__init__.py
index a57ea263f..0a7c9c5a7 100644
--- a/splinter/driver/__init__.py
+++ b/splinter/driver/__init__.py
@@ -107,35 +107,47 @@ def get_iframe(self, name: Any) -> Any:
"""
raise NotImplementedError("%s doesn't support frames." % self.driver_name)
- def execute_script(self, script: str, *args: str) -> Any:
- """Execute a piece of JavaScript in the browser.
+ def execute_script(self, script: str, *args: Any) -> Any:
+ """Execute JavaScript in the current browser window.
+
+ The code is assumed to be synchronous.
Arguments:
- script (str): The piece of JavaScript to execute.
+ script (str): The JavaScript code to execute.
+ args: Any of:
+ - JSON-serializable objects.
+ - WebElement.
+ These will be available to the JavaScript as the `arguments` object.
Example:
- >>> browser.execute_script('document.getElementById("body").innerHTML = "
Hello world!
"')
+ >>> Browser().execute_script('document.querySelector("body").innerHTML = "Hello world!
"')
"""
raise NotImplementedError(
- "%s doesn't support execution of arbitrary JavaScript." % self.driver_name,
+ f"{self.driver_name} doesn't support execution of arbitrary JavaScript.",
)
- def evaluate_script(self, script: str, *args: str) -> Any:
- """
- Similar to :meth:`execute_script ` method.
+ def evaluate_script(self, script: str, *args: Any) -> Any:
+ """Evaluate JavaScript in the current browser window and return the completion value.
- Execute javascript in the browser and return the value of the expression.
+ The code is assumed to be synchronous.
Arguments:
- script (str): The piece of JavaScript to execute.
+ script (str): The JavaScript code to execute.
+ args: Any of:
+ - JSON-serializable objects.
+ - WebElement.
+ These will be available to the JavaScript as the `arguments` object.
+
+ Returns:
+ The result of the code's execution.
Example:
- >>> assert 4 == browser.evaluate_script('2 + 2')
+ >>> assert 4 == Browser().evaluate_script('2 + 2')
"""
raise NotImplementedError(
- "%s doesn't support evaluation of arbitrary JavaScript." % self.driver_name,
+ f"{self.driver_name} doesn't support evaluation of arbitrary JavaScript.",
)
def find_by_css(self, css_selector: str) -> ElementList:
diff --git a/splinter/driver/webdriver/__init__.py b/splinter/driver/webdriver/__init__.py
index cd8049f9c..bfdfa12a4 100644
--- a/splinter/driver/webdriver/__init__.py
+++ b/splinter/driver/webdriver/__init__.py
@@ -7,7 +7,7 @@
import time
import warnings
from contextlib import contextmanager
-from typing import Optional
+from typing import Dict, Optional
from selenium.common.exceptions import ElementClickInterceptedException
from selenium.common.exceptions import MoveTargetOutOfBoundsException
@@ -316,11 +316,29 @@ def forward(self):
def reload(self):
self.driver.refresh()
+ def _script_prepare_args(self, args) -> list:
+ """Modify user arguments sent to execute_script() and evaluate_script().
+
+ If a WebDriverElement or ShadowRootElement is given,
+ replace it with their Element ID.
+ """
+ result = []
+
+ for item in args:
+ if isinstance(item, (WebDriverElement, ShadowRootElement)):
+ result.append(item._as_id_dict())
+ else:
+ result.append(item)
+
+ return result
+
def execute_script(self, script, *args):
- return self.driver.execute_script(script, *args)
+ converted_args = self._script_prepare_args(args)
+ return self.driver.execute_script(script, *converted_args)
def evaluate_script(self, script, *args):
- return self.driver.execute_script("return %s" % script, *args)
+ converted_args = self._script_prepare_args(args)
+ return self.driver.execute_script(f"return {script}", *converted_args)
def is_element_present(self, finder, selector, wait_time=None):
wait_time = wait_time or self.wait_time
@@ -694,6 +712,15 @@ def __init__(self, element, parent):
self.wait_time = self.parent.wait_time
self.element_class = self.parent.element_class
+ def _as_id_dict(self) -> Dict[str, str]:
+ """Get the canonical object to identify an element by it's ID.
+
+ When sent to the browser, it will be used to build an Element object.
+
+ Not to be confused with the 'id' tag on an element.
+ """
+ return {"shadow-6066-11e4-a52e-4f735466cecf": self._element._id}
+
def _find(self, by: By, selector, wait_time=None):
return self.find_by(
self._element.find_elements,
@@ -743,6 +770,15 @@ def _set_value(self, value):
def __getitem__(self, attr):
return self._element.get_attribute(attr)
+ def _as_id_dict(self) -> Dict[str, str]:
+ """Get the canonical object to identify an element by it's ID.
+
+ When sent to the browser, it will be used to build an Element object.
+
+ Not to be confused with the 'id' tag on an element.
+ """
+ return {"element-6066-11e4-a52e-4f735466cecf": self._element._id}
+
@property
def text(self):
return self._element.text
diff --git a/tests/tests_webdriver/test_javascript.py b/tests/tests_webdriver/test_javascript.py
deleted file mode 100644
index 9d086bfc3..000000000
--- a/tests/tests_webdriver/test_javascript.py
+++ /dev/null
@@ -1,14 +0,0 @@
-def test_can_execute_javascript(browser, app_url):
- "should be able to execute javascript"
- browser.visit(app_url)
- browser.execute_script("$('body').empty()")
- assert "" == browser.find_by_tag("body").value
-
-
-def test_can_evaluate_script(browser):
- "should evaluate script"
- assert 8 == browser.evaluate_script("4+4")
-
-
-def test_execute_script_returns_result_if_present(browser):
- assert browser.execute_script("return 42") == 42
diff --git a/tests/tests_webdriver/test_javascript/test_evaluate_script.py b/tests/tests_webdriver/test_javascript/test_evaluate_script.py
new file mode 100644
index 000000000..3a91a0714
--- /dev/null
+++ b/tests/tests_webdriver/test_javascript/test_evaluate_script.py
@@ -0,0 +1,76 @@
+import pytest
+
+from selenium.common.exceptions import JavascriptException
+
+
+def test_evaluate_script_valid(browser, app_url):
+ """Scenario: Evaluating JavaScript Returns The Code's Result
+
+ When I evaluate JavaScript code
+ Then the result of the evaluation is returned
+ """
+ browser.visit(app_url)
+
+ document_href = browser.evaluate_script("document.location.href")
+ assert app_url == document_href
+
+
+def test_evaluate_script_valid_args(browser, app_url):
+ """Scenario: Execute Valid JavaScript With Arguments
+
+ When I execute valid JavaScript code which modifies the DOM
+ And I send arguments to the web browser
+ Then the arguments are available for use
+ """
+ browser.visit(app_url)
+
+ browser.evaluate_script(
+ "document.querySelector('body').innerHTML = arguments[0] + arguments[1]",
+ "A String And ",
+ "Another String",
+ )
+
+ elem = browser.find_by_tag("body").first
+ assert elem.value == "A String And Another String"
+
+
+def test_evaluate_script_valid_args_element(browser, app_url):
+ """Scenario: Execute Valid JavaScript
+
+ When I execute valid JavaScript code
+ And I send an Element to the browser as an argument
+ Then the modifications are seen in the document
+ """
+ browser.visit(app_url)
+
+ elem = browser.find_by_id("firstheader").first
+ elem_text = browser.evaluate_script("arguments[0].innerHTML", elem)
+ assert elem_text == "Example Header"
+
+
+def test_evaluate_script_invalid(browser, app_url):
+ """Scenario: Evaluate Invalid JavaScript.
+
+ When I evaluate invalid JavaScript code
+ Then an error is raised
+ """
+ browser.visit(app_url)
+
+ with pytest.raises(JavascriptException):
+ browser.evaluate_script("invalid.thisIsNotGood()")
+
+
+def test_evaluate_script_invalid_args(browser, app_url):
+ """Scenario: Execute Valid JavaScript
+
+ When I execute valid JavaScript code which modifies the DOM
+ And I send an object to the browser which is not JSON serializable
+ Then an error is raised
+ """
+ browser.visit(app_url)
+
+ def unserializable():
+ "You can't JSON serialize a function."
+
+ with pytest.raises(TypeError):
+ browser.evaluate_script("arguments[0]", unserializable)
diff --git a/tests/tests_webdriver/test_javascript/test_execute_script.py b/tests/tests_webdriver/test_javascript/test_execute_script.py
new file mode 100644
index 000000000..add022794
--- /dev/null
+++ b/tests/tests_webdriver/test_javascript/test_execute_script.py
@@ -0,0 +1,105 @@
+import pytest
+
+from selenium.common.exceptions import JavascriptException
+
+
+def test_execute_script_valid(browser, app_url):
+ """Scenario: Execute Valid JavaScript
+
+ When I execute valid JavaScript code which modifies the DOM
+ Then the modifications are seen in the document
+ """
+ browser.visit(app_url)
+
+ browser.execute_script("document.querySelector('body').innerHTML = ''")
+
+ elem = browser.find_by_tag("body").first
+ assert elem.value == ""
+
+
+def test_execute_script_return_value(browser, app_url):
+ """Scenario: Execute Valid JavaScript With No Return Value
+
+ When I execute valid JavaScript code
+ And the code does not return a value
+ Then no value is returned to the driver
+ """
+ browser.visit(app_url)
+
+ result = browser.execute_script("document.querySelector('body').innerHTML")
+ assert result is None
+
+
+def test_execute_script_return_value_if_explicit(browser):
+ """Scenario: Execute Valid JavaScript With A Return Value
+
+ When I execute JavaScript code
+ And the code returns a value
+ Then the value is returned from the web browser
+ """
+ result = browser.execute_script("return 42")
+ assert result == 42
+
+
+def test_execute_script_valid_args(browser, app_url):
+ """Scenario: Execute Valid JavaScript With Arguments
+
+ When I execute valid JavaScript code which modifies the DOM
+ And I send arguments to the web browser
+ Then the arguments are available for use
+ """
+ browser.visit(app_url)
+
+ browser.execute_script(
+ "document.querySelector('body').innerHTML = arguments[0] + arguments[1]",
+ "A String And ",
+ "Another String",
+ )
+
+ elem = browser.find_by_tag("body").first
+ assert elem.value == "A String And Another String"
+
+
+def test_execute_script_valid_args_element(browser, app_url):
+ """Scenario: Execute Valid JavaScript With Arguments - Send Element
+
+ When I execute valid JavaScript code
+ And I send an Element to the web browser as an argument
+ Then the argument is available for use
+ """
+ browser.visit(app_url)
+
+ elem = browser.find_by_id("firstheader").first
+ assert elem.value == "Example Header"
+ browser.execute_script("arguments[0].innerHTML = 'A New Header'", elem)
+
+ elem = browser.find_by_id("firstheader").first
+ assert elem.value == "A New Header"
+
+
+def test_execute_script_invalid(browser, app_url):
+ """Scenario: Evaluate Invalid JavaScript
+
+ When I execute invalid JavaScript code
+ Then an error is raised
+ """
+ browser.visit(app_url)
+
+ with pytest.raises(JavascriptException):
+ browser.execute_script("invalid.thisIsNotGood()")
+
+
+def test_execute_script_invalid_args(browser, app_url):
+ """Scenario: Execute Valid JavaScript With Invalid Arguments
+
+ When I execute valid JavaScript code which modifies the DOM
+ And I send an object to the browser which is not JSON serializable
+ Then an error is raised
+ """
+ browser.visit(app_url)
+
+ def unserializable():
+ "You can't JSON serialize a function."
+
+ with pytest.raises(TypeError):
+ browser.execute_script("arguments[0]", unserializable)