Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow WebDriverElement to be used with Browser().execute_script() #1303

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
42 changes: 39 additions & 3 deletions splinter/driver/webdriver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down 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
Loading