Skip to content

Commit 51db9d0

Browse files
committed
[py] Implement script module for BiDi
The commit also generates Bazel test targets for BiDi-backed implementation
1 parent e4501f4 commit 51db9d0

File tree

10 files changed

+391
-21
lines changed

10 files changed

+391
-21
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ py/selenium/webdriver/remote/isDisplayed.js
7676
py/docs/build/
7777
py/build/
7878
py/LICENSE
79+
py/pytestdebug.log
7980
selenium.egg-info/
8081
third_party/java/jetty/jetty-repacked.jar
8182
*.user

Diff for: py/BUILD.bazel

+39-7
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ py_package(
192192
"py.selenium.webdriver.chrome",
193193
"py.selenium.webdriver.chromium",
194194
"py.selenium.webdriver.common",
195+
"py.selenium.webdriver.common.bidi",
195196
"py.selenium.webdriver.common.devtools",
196197
"py.selenium.webdriver.edge",
197198
"py.selenium.webdriver.firefox",
@@ -358,10 +359,39 @@ py_library(
358359
deps = [],
359360
)
360361

362+
BIDI_TESTS = glob(["test/selenium/webdriver/common/**/*bidi*_tests.py"])
363+
361364
[
362365
py_test_suite(
363366
name = "common-%s" % browser,
364367
size = "large",
368+
srcs = glob(
369+
[
370+
"test/selenium/webdriver/common/**/*.py",
371+
"test/selenium/webdriver/support/**/*.py",
372+
],
373+
exclude = BIDI_TESTS + ["test/selenium/webdriver/common/print_pdf_tests.py"],
374+
),
375+
args = [
376+
"--instafail",
377+
"--bidi=false",
378+
] + BROWSERS[browser]["args"],
379+
data = BROWSERS[browser]["data"],
380+
env_inherit = ["DISPLAY"],
381+
tags = ["no-sandbox"] + BROWSERS[browser]["tags"],
382+
deps = [
383+
":init-tree",
384+
":selenium",
385+
":webserver",
386+
] + TEST_DEPS,
387+
)
388+
for browser in BROWSERS.keys()
389+
]
390+
391+
[
392+
py_test_suite(
393+
name = "common-%s-bidi" % browser,
394+
size = "large",
365395
srcs = glob(
366396
[
367397
"test/selenium/webdriver/common/**/*.py",
@@ -371,12 +401,11 @@ py_library(
371401
),
372402
args = [
373403
"--instafail",
404+
"--bidi=true",
374405
] + BROWSERS[browser]["args"],
375406
data = BROWSERS[browser]["data"],
376407
env_inherit = ["DISPLAY"],
377-
tags = [
378-
"no-sandbox",
379-
] + BROWSERS[browser]["tags"],
408+
tags = ["no-sandbox"] + BROWSERS[browser]["tags"],
380409
deps = [
381410
":init-tree",
382411
":selenium",
@@ -482,10 +511,13 @@ py_test_suite(
482511
py_test_suite(
483512
name = "test-remote",
484513
size = "large",
485-
srcs = glob([
486-
"test/selenium/webdriver/common/**/*.py",
487-
"test/selenium/webdriver/support/**/*.py",
488-
]),
514+
srcs = glob(
515+
[
516+
"test/selenium/webdriver/common/**/*.py",
517+
"test/selenium/webdriver/support/**/*.py",
518+
],
519+
exclude = BIDI_TESTS,
520+
),
489521
args = [
490522
"--instafail",
491523
"--driver=remote",

Diff for: py/conftest.py

+15
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ def pytest_addoption(parser):
7979
dest="use_lan_ip",
8080
help="Whether to start test server with lan ip instead of localhost",
8181
)
82+
parser.addoption(
83+
"--bidi",
84+
action="store",
85+
dest="bidi",
86+
metavar="BIDI",
87+
help="Whether to enable BiDi support",
88+
)
8289

8390

8491
def pytest_ignore_collect(path, config):
@@ -166,6 +173,7 @@ def get_options(driver_class, config):
166173
browser_path = config.option.binary
167174
browser_args = config.option.args
168175
headless = bool(config.option.headless)
176+
bidi = bool(config.option.bidi)
169177
options = None
170178

171179
if browser_path or browser_args:
@@ -187,6 +195,13 @@ def get_options(driver_class, config):
187195
options.add_argument("--headless=new")
188196
if driver_class == "Firefox":
189197
options.add_argument("-headless")
198+
199+
if bidi:
200+
if not options:
201+
options = getattr(webdriver, f"{driver_class}Options")()
202+
203+
options.web_socket_url = True
204+
190205
return options
191206

192207

Diff for: py/selenium/webdriver/common/bidi/script.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import typing
19+
from dataclasses import dataclass
20+
21+
from .session import session_subscribe
22+
from .session import session_unsubscribe
23+
24+
25+
class Script:
26+
def __init__(self, conn):
27+
self.conn = conn
28+
self.log_entry_subscribed = False
29+
30+
def add_console_message_handler(self, handler):
31+
self._subscribe_to_log_entries()
32+
return self.conn.add_callback(LogEntryAdded, self._handle_log_entry("console", handler))
33+
34+
def add_javascript_error_handler(self, handler):
35+
self._subscribe_to_log_entries()
36+
return self.conn.add_callback(LogEntryAdded, self._handle_log_entry("javascript", handler))
37+
38+
def remove_console_message_handler(self, id):
39+
self.conn.remove_callback(LogEntryAdded, id)
40+
self._unsubscribe_from_log_entries()
41+
42+
remove_javascript_error_handler = remove_console_message_handler
43+
44+
def _subscribe_to_log_entries(self):
45+
if not self.log_entry_subscribed:
46+
self.conn.execute(session_subscribe(LogEntryAdded.event_class))
47+
self.log_entry_subscribed = True
48+
49+
def _unsubscribe_from_log_entries(self):
50+
if self.log_entry_subscribed and LogEntryAdded.event_class not in self.conn.callbacks:
51+
self.conn.execute(session_unsubscribe(LogEntryAdded.event_class))
52+
self.log_entry_subscribed = False
53+
54+
def _handle_log_entry(self, type, handler):
55+
def _handle_log_entry(log_entry):
56+
if log_entry.type_ == type:
57+
handler(log_entry)
58+
59+
return _handle_log_entry
60+
61+
62+
class LogEntryAdded:
63+
event_class = "log.entryAdded"
64+
65+
@classmethod
66+
def from_json(cls, json):
67+
print(json)
68+
if json["type"] == "console":
69+
return ConsoleLogEntry.from_json(json)
70+
elif json["type"] == "javascript":
71+
return JavaScriptLogEntry.from_json(json)
72+
73+
74+
@dataclass
75+
class ConsoleLogEntry:
76+
level: str
77+
text: str
78+
timestamp: str
79+
method: str
80+
args: typing.List[dict]
81+
type_: str
82+
83+
@classmethod
84+
def from_json(cls, json):
85+
return cls(
86+
level=json["level"],
87+
text=json["text"],
88+
timestamp=json["timestamp"],
89+
method=json["method"],
90+
args=json["args"],
91+
type_=json["type"],
92+
)
93+
94+
95+
@dataclass
96+
class JavaScriptLogEntry:
97+
level: str
98+
text: str
99+
timestamp: str
100+
stacktrace: dict
101+
type_: str
102+
103+
@classmethod
104+
def from_json(cls, json):
105+
return cls(
106+
level=json["level"],
107+
text=json["text"],
108+
timestamp=json["timestamp"],
109+
stacktrace=json["stackTrace"],
110+
type_=json["type"],
111+
)

Diff for: py/selenium/webdriver/common/bidi/session.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
19+
def session_subscribe(*events, browsing_contexts=[]):
20+
cmd_dict = {
21+
"method": "session.subscribe",
22+
"params": {
23+
"events": events,
24+
},
25+
}
26+
if browsing_contexts:
27+
cmd_dict["params"]["browsingContexts"] = browsing_contexts
28+
_ = yield cmd_dict
29+
return None
30+
31+
32+
def session_unsubscribe(*events, browsing_contexts=[]):
33+
cmd_dict = {
34+
"method": "session.unsubscribe",
35+
"params": {
36+
"events": events,
37+
},
38+
}
39+
if browsing_contexts:
40+
cmd_dict["params"]["browsingContexts"] = browsing_contexts
41+
_ = yield cmd_dict
42+
return None

Diff for: py/selenium/webdriver/common/options.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def __init__(self, name):
4444
self.name = name
4545

4646
def __get__(self, obj, cls):
47-
if self.name in ("acceptInsecureCerts", "strictFileInteractability", "setWindowRect", "se:downloadsEnabled"):
47+
if self.name in (
48+
"acceptInsecureCerts",
49+
"strictFileInteractability",
50+
"setWindowRect",
51+
"se:downloadsEnabled",
52+
"webSocketUrl",
53+
):
4854
return obj._caps.get(self.name, False)
4955
return obj._caps.get(self.name)
5056

@@ -361,6 +367,28 @@ class BaseOptions(metaclass=ABCMeta):
361367
- `None`
362368
"""
363369

370+
web_socket_url = _BaseOptionsDescriptor("webSocketUrl")
371+
"""Gets and Sets WebSocket URL.
372+
373+
Usage
374+
-----
375+
- Get
376+
- `self.web_socket_url`
377+
- Set
378+
- `self.web_socket_url` = `value`
379+
380+
Parameters
381+
----------
382+
`value`: `bool`
383+
384+
Returns
385+
-------
386+
- Get
387+
- `bool`
388+
- Set
389+
- `None`
390+
"""
391+
364392
def __init__(self) -> None:
365393
super().__init__()
366394
self._caps = self.default_capabilities

Diff for: py/selenium/webdriver/remote/webdriver.py

+21
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from selenium.common.exceptions import NoSuchCookieException
4242
from selenium.common.exceptions import NoSuchElementException
4343
from selenium.common.exceptions import WebDriverException
44+
from selenium.webdriver.common.bidi.script import Script
4445
from selenium.webdriver.common.by import By
4546
from selenium.webdriver.common.options import BaseOptions
4647
from selenium.webdriver.common.print_page_options import PrintOptions
@@ -209,7 +210,9 @@ def __init__(
209210
self._authenticator_id = None
210211
self.start_client()
211212
self.start_session(capabilities)
213+
212214
self._websocket_connection = None
215+
self._script = None
213216

214217
def __repr__(self):
215218
return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'
@@ -1067,6 +1070,24 @@ async def bidi_connection(self):
10671070
async with conn.open_session(target_id) as session:
10681071
yield BidiConnection(session, cdp, devtools)
10691072

1073+
@property
1074+
def script(self):
1075+
if not self._websocket_connection:
1076+
self._start_bidi()
1077+
1078+
if not self._script:
1079+
self._script = Script(self._websocket_connection)
1080+
1081+
return self._script
1082+
1083+
def _start_bidi(self):
1084+
if self.caps.get("webSocketUrl"):
1085+
ws_url = self.caps.get("webSocketUrl")
1086+
else:
1087+
raise WebDriverException("Unable to find url to connect to from capabilities")
1088+
1089+
self._websocket_connection = WebSocketConnection(ws_url)
1090+
10701091
def _get_cdp_details(self):
10711092
import json
10721093

0 commit comments

Comments
 (0)