Skip to content

Commit 2be6e97

Browse files
[py] Add Mutation Logging support
1 parent 7661e5e commit 2be6e97

File tree

4 files changed

+69
-6
lines changed

4 files changed

+69
-6
lines changed

py/BUILD.bazel

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ copy_file(
2929
out = "selenium/webdriver/remote/findElements.js",
3030
)
3131

32+
copy_file(
33+
name = "mutation-listener",
34+
src = "//javascript/cdp-support:closure",
35+
out = "selenium/webdriver/remote/mutation-listener.js"
36+
)
37+
3238
copy_file(
3339
name = "firefox-driver-prefs",
3440
src = "//third_party/js/selenium:webdriver_json",
@@ -46,6 +52,7 @@ py_library(
4652
":firefox-driver-prefs",
4753
":get-attribute",
4854
":is-displayed",
55+
":mutation-listener",
4956
] + [":create-cdp-srcs-" + n for n in BROWSER_VERSIONS],
5057
imports = ["."],
5158
visibility = ["//visibility:public"],

py/selenium/webdriver/remote/script_key.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
class ScriptKey:
2121

22-
def __init__(self):
23-
self._id = uuid.uuid4()
22+
def __init__(self, id=None):
23+
self._id = id or uuid.uuid4()
2424

2525
@property
2626
def id(self):

py/selenium/webdriver/remote/webdriver.py

+44-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import copy
2323
from contextlib import (contextmanager, asynccontextmanager)
2424
from importlib import import_module
25+
import json
2526
import pkgutil
2627
import warnings
2728
import sys
@@ -710,13 +711,16 @@ def find_elements_by_css_selector(self, css_selector):
710711
warnings.warn("find_elements_by_* commands are deprecated. Please use find_elements() instead")
711712
return self.find_elements(by=By.CSS_SELECTOR, value=css_selector)
712713

713-
def pin_script(self, script):
714+
def pin_script(self, script, script_key=None):
714715
"""
715716
716717
"""
717-
script_key = ScriptKey()
718-
self.pinned_scripts[script_key.id] = script
719-
return script_key
718+
if not script_key:
719+
_script_key = ScriptKey()
720+
else:
721+
_script_key = ScriptKey(script_key)
722+
self.pinned_scripts[_script_key.id] = script
723+
return _script_key
720724

721725
def unpin(self, script_key):
722726
"""
@@ -1483,6 +1487,42 @@ def get_log(self, log_type):
14831487
"""
14841488
return self.execute(Command.GET_LOG, {'type': log_type})['value']
14851489

1490+
@asynccontextmanager
1491+
async def log_mutation_events(self):
1492+
"""
1493+
Listens for mutation events and emits them as it finds them
1494+
1495+
:Usage:
1496+
::
1497+
1498+
"""
1499+
_pkg = '.'.join(__name__.split('.')[:-1])
1500+
mutation_listener_js = pkgutil.get_data(_pkg, 'mutation-listener.js').decode('utf8').strip()
1501+
1502+
assert sys.version_info >= (3, 7)
1503+
global cdp
1504+
async with self._get_bidi_connection():
1505+
global devtools
1506+
page = cdp.get_session_context('page.enable')
1507+
await page.execute(devtools.page.enable())
1508+
runtime = cdp.get_session_context('runtime.enable')
1509+
await runtime.execute(devtools.runtime.enable())
1510+
await runtime.execute(devtools.runtime.add_binding("__webdriver_attribute"))
1511+
self.pin_script(mutation_listener_js)
1512+
script_key = await page.execute(devtools.page.add_script_to_evaluate_on_new_document(mutation_listener_js))
1513+
self.pin_script(mutation_listener_js, script_key)
1514+
self.execute_script(f"return {mutation_listener_js}")
1515+
event = {}
1516+
async with runtime.wait_for(devtools.runtime.BindingCalled) as evnt:
1517+
yield event
1518+
1519+
payload = json.loads(evnt.value.payload)
1520+
elements = self.find_elements(By.CSS_SELECTOR, "*[data-__webdriver_id={}".format(payload['target']))
1521+
# event["element"] = elements[0]
1522+
event["attribute_name"] = payload['name']
1523+
event["current_value"] = payload['value']
1524+
event["old_value"] = payload['oldValue']
1525+
14861526
@asynccontextmanager
14871527
async def add_js_error_listener(self):
14881528
"""

py/test/selenium/webdriver/common/bidi_tests.py

+16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
from selenium.webdriver.common.by import By
18+
from selenium.webdriver.support import expected_conditions as EC
19+
from selenium.webdriver.support.ui import WebDriverWait
20+
1821
import pytest
1922

2023

@@ -45,3 +48,16 @@ async def test_collect_js_exceptions(driver, pages):
4548
driver.find_element(By.ID, "throwing-mouseover").click()
4649
assert exceptions is not None
4750
assert exceptions.exception_details.stack_trace.call_frames[0].function_name == "onmouseover"
51+
52+
53+
@pytest.mark.xfail_firefox
54+
@pytest.mark.xfail_safari
55+
async def test_collect_log_mutations(driver, pages):
56+
async with driver.log_mutation_events() as event:
57+
pages.load("dynamic.html")
58+
driver.find_element(By.ID, "reveal").click()
59+
WebDriverWait(driver, 5).until(EC.visibility_of(driver.find_element(By.ID, "revealed")))
60+
61+
assert event["attribute_name"] == "style"
62+
assert event["current_value"] == ""
63+
assert event["old_value"] == "display:none;"

0 commit comments

Comments
 (0)