Skip to content

Commit

Permalink
Allow WebDriverElement to be used with Browser().execute_script()
Browse files Browse the repository at this point in the history
  • Loading branch information
jsfehler committed Jun 24, 2024
1 parent 931da93 commit c65ef6c
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 41 deletions.
85 changes: 72 additions & 13 deletions docs/javascript.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
36 changes: 24 additions & 12 deletions splinter/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<p>Hello world!</p>"')
>>> Browser().execute_script('document.querySelector("body").innerHTML = "<p>Hello world!</p>"')
"""
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 <DriverAPI.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:
Expand Down
40 changes: 38 additions & 2 deletions splinter/driver/webdriver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
14 changes: 0 additions & 14 deletions tests/tests_webdriver/test_javascript.py

This file was deleted.

76 changes: 76 additions & 0 deletions tests/tests_webdriver/test_javascript/test_evaluate_script.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit c65ef6c

Please sign in to comment.