Skip to content

Commit 287f1f6

Browse files
committed
Merge remote-tracking branch 'upstream/trunk' into dotnet-aot-classic
2 parents 800a8dd + 029d263 commit 287f1f6

File tree

13 files changed

+330
-54
lines changed

13 files changed

+330
-54
lines changed

.github/workflows/ci-dotnet.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ jobs:
2323
java-version: 17
2424
os: windows
2525
run: |
26+
fsutil 8dot3name set 0
2627
bazel test //dotnet/test/common:ElementFindingTest-firefox //dotnet/test/common:ElementFindingTest-chrome --pin_browsers=true

.github/workflows/ci-java.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
# https://github.com/bazelbuild/rules_jvm_external/issues/1046
2323
java-version: 17
2424
run: |
25+
fsutil 8dot3name set 0
2526
bazel test --flaky_test_attempts 3 //java/test/org/openqa/selenium/chrome:ChromeDriverFunctionalTest `
2627
//java/test/org/openqa/selenium/federatedcredentialmanagement:FederatedCredentialManagementTest `
2728
//java/test/org/openqa/selenium/firefox:FirefoxDriverBuilderTest `

py/selenium/webdriver/common/by.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
# under the License.
1717
"""The By implementation."""
1818

19+
from typing import Dict
1920
from typing import Literal
21+
from typing import Optional
2022

2123

2224
class By:
@@ -31,5 +33,19 @@ class By:
3133
CLASS_NAME = "class name"
3234
CSS_SELECTOR = "css selector"
3335

36+
_custom_finders: Dict[str, str] = {}
37+
38+
@classmethod
39+
def register_custom_finder(cls, name: str, strategy: str) -> None:
40+
cls._custom_finders[name] = strategy
41+
42+
@classmethod
43+
def get_finder(cls, name: str) -> Optional[str]:
44+
return cls._custom_finders.get(name) or getattr(cls, name.upper(), None)
45+
46+
@classmethod
47+
def clear_custom_finders(cls) -> None:
48+
cls._custom_finders.clear()
49+
3450

3551
ByType = Literal["id", "xpath", "link text", "partial link text", "name", "tag name", "class name", "css selector"]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
class LocatorConverter:
20+
def convert(self, by, value):
21+
# Default conversion logic
22+
if by == "id":
23+
return "css selector", f'[id="{value}"]'
24+
elif by == "class name":
25+
return "css selector", f".{value}"
26+
elif by == "name":
27+
return "css selector", f'[name="{value}"]'
28+
return by, value

py/selenium/webdriver/remote/remote_connection.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ class RemoteConnection:
143143
)
144144
_ca_certs = os.getenv("REQUESTS_CA_BUNDLE") if "REQUESTS_CA_BUNDLE" in os.environ else certifi.where()
145145

146+
system = platform.system().lower()
147+
if system == "darwin":
148+
system = "mac"
149+
150+
# Class variables for headers
151+
extra_headers = None
152+
user_agent = f"selenium/{__version__} (python {system})"
153+
146154
@classmethod
147155
def get_timeout(cls):
148156
""":Returns:
@@ -196,14 +204,10 @@ def get_remote_connection_headers(cls, parsed_url, keep_alive=False):
196204
- keep_alive (Boolean) - Is this a keep-alive connection (default: False)
197205
"""
198206

199-
system = platform.system().lower()
200-
if system == "darwin":
201-
system = "mac"
202-
203207
headers = {
204208
"Accept": "application/json",
205209
"Content-Type": "application/json;charset=UTF-8",
206-
"User-Agent": f"selenium/{__version__} (python {system})",
210+
"User-Agent": cls.user_agent,
207211
}
208212

209213
if parsed_url.username:
@@ -213,6 +217,9 @@ def get_remote_connection_headers(cls, parsed_url, keep_alive=False):
213217
if keep_alive:
214218
headers.update({"Connection": "keep-alive"})
215219

220+
if cls.extra_headers:
221+
headers.update(cls.extra_headers)
222+
216223
return headers
217224

218225
def _get_proxy_url(self):
@@ -236,7 +243,12 @@ def _separate_http_proxy_auth(self):
236243

237244
def _get_connection_manager(self):
238245
pool_manager_init_args = {"timeout": self.get_timeout()}
239-
if self._ca_certs:
246+
pool_manager_init_args.update(self._init_args_for_pool_manager.get("init_args_for_pool_manager", {}))
247+
248+
if self._ignore_certificates:
249+
pool_manager_init_args["cert_reqs"] = "CERT_NONE"
250+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
251+
elif self._ca_certs:
240252
pool_manager_init_args["cert_reqs"] = "CERT_REQUIRED"
241253
pool_manager_init_args["ca_certs"] = self._ca_certs
242254

@@ -252,9 +264,18 @@ def _get_connection_manager(self):
252264

253265
return urllib3.PoolManager(**pool_manager_init_args)
254266

255-
def __init__(self, remote_server_addr: str, keep_alive: bool = False, ignore_proxy: bool = False):
267+
def __init__(
268+
self,
269+
remote_server_addr: str,
270+
keep_alive: bool = False,
271+
ignore_proxy: bool = False,
272+
ignore_certificates: bool = False,
273+
init_args_for_pool_manager: dict = None,
274+
):
256275
self.keep_alive = keep_alive
257276
self._url = remote_server_addr
277+
self._ignore_certificates = ignore_certificates
278+
self._init_args_for_pool_manager = init_args_for_pool_manager or {}
258279

259280
# Env var NO_PROXY will override this part of the code
260281
_no_proxy = os.environ.get("no_proxy", os.environ.get("NO_PROXY"))
@@ -280,6 +301,16 @@ def __init__(self, remote_server_addr: str, keep_alive: bool = False, ignore_pro
280301
self._conn = self._get_connection_manager()
281302
self._commands = remote_commands
282303

304+
extra_commands = {}
305+
306+
def add_command(self, name, method, url):
307+
"""Register a new command."""
308+
self._commands[name] = (method, url)
309+
310+
def get_command(self, name: str):
311+
"""Retrieve a command if it exists."""
312+
return self._commands.get(name)
313+
283314
def execute(self, command, params):
284315
"""Send a command to the remote server.
285316
@@ -291,7 +322,7 @@ def execute(self, command, params):
291322
- params - A dictionary of named parameters to send with the command as
292323
its JSON payload.
293324
"""
294-
command_info = self._commands[command]
325+
command_info = self._commands.get(command) or self.extra_commands.get(command)
295326
assert command_info is not None, f"Unrecognised command {command}"
296327
path_string = command_info[1]
297328
path = string.Template(path_string).substitute(params)

py/selenium/webdriver/remote/webdriver.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from .errorhandler import ErrorHandler
5959
from .file_detector import FileDetector
6060
from .file_detector import LocalFileDetector
61+
from .locator_converter import LocatorConverter
6162
from .mobile import Mobile
6263
from .remote_connection import RemoteConnection
6364
from .script_key import ScriptKey
@@ -171,6 +172,8 @@ def __init__(
171172
keep_alive: bool = True,
172173
file_detector: Optional[FileDetector] = None,
173174
options: Optional[Union[BaseOptions, List[BaseOptions]]] = None,
175+
locator_converter: Optional[LocatorConverter] = None,
176+
web_element_cls: Optional[type] = None,
174177
) -> None:
175178
"""Create a new driver that will issue commands using the wire
176179
protocol.
@@ -183,6 +186,8 @@ def __init__(
183186
- file_detector - Pass custom file detector object during instantiation. If None,
184187
then default LocalFileDetector() will be used.
185188
- options - instance of a driver options.Options class
189+
- locator_converter - Custom locator converter to use. Defaults to None.
190+
- web_element_cls - Custom class to use for web elements. Defaults to WebElement.
186191
"""
187192

188193
if isinstance(options, list):
@@ -207,6 +212,8 @@ def __init__(
207212
self._switch_to = SwitchTo(self)
208213
self._mobile = Mobile(self)
209214
self.file_detector = file_detector or LocalFileDetector()
215+
self.locator_converter = locator_converter or LocatorConverter()
216+
self._web_element_cls = web_element_cls or self._web_element_cls
210217
self._authenticator_id = None
211218
self.start_client()
212219
self.start_session(capabilities)
@@ -729,22 +736,14 @@ def find_element(self, by=By.ID, value: Optional[str] = None) -> WebElement:
729736
730737
:rtype: WebElement
731738
"""
739+
by, value = self.locator_converter.convert(by, value)
740+
732741
if isinstance(by, RelativeBy):
733742
elements = self.find_elements(by=by, value=value)
734743
if not elements:
735744
raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
736745
return elements[0]
737746

738-
if by == By.ID:
739-
by = By.CSS_SELECTOR
740-
value = f'[id="{value}"]'
741-
elif by == By.CLASS_NAME:
742-
by = By.CSS_SELECTOR
743-
value = f".{value}"
744-
elif by == By.NAME:
745-
by = By.CSS_SELECTOR
746-
value = f'[name="{value}"]'
747-
748747
return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
749748

750749
def find_elements(self, by=By.ID, value: Optional[str] = None) -> List[WebElement]:
@@ -757,22 +756,14 @@ def find_elements(self, by=By.ID, value: Optional[str] = None) -> List[WebElemen
757756
758757
:rtype: list of WebElement
759758
"""
759+
by, value = self.locator_converter.convert(by, value)
760+
760761
if isinstance(by, RelativeBy):
761762
_pkg = ".".join(__name__.split(".")[:-1])
762763
raw_function = pkgutil.get_data(_pkg, "findElements.js").decode("utf8")
763764
find_element_js = f"/* findElements */return ({raw_function}).apply(null, arguments);"
764765
return self.execute_script(find_element_js, by.to_dict())
765766

766-
if by == By.ID:
767-
by = By.CSS_SELECTOR
768-
value = f'[id="{value}"]'
769-
elif by == By.CLASS_NAME:
770-
by = By.CSS_SELECTOR
771-
value = f".{value}"
772-
elif by == By.NAME:
773-
by = By.CSS_SELECTOR
774-
value = f'[name="{value}"]'
775-
776767
# Return empty list if driver returns null
777768
# See https://github.com/SeleniumHQ/selenium/issues/4555
778769
return self.execute(Command.FIND_ELEMENTS, {"using": by, "value": value})["value"] or []

py/selenium/webdriver/remote/webelement.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -404,16 +404,7 @@ def find_element(self, by=By.ID, value=None) -> WebElement:
404404
405405
:rtype: WebElement
406406
"""
407-
if by == By.ID:
408-
by = By.CSS_SELECTOR
409-
value = f'[id="{value}"]'
410-
elif by == By.CLASS_NAME:
411-
by = By.CSS_SELECTOR
412-
value = f".{value}"
413-
elif by == By.NAME:
414-
by = By.CSS_SELECTOR
415-
value = f'[name="{value}"]'
416-
407+
by, value = self._parent.locator_converter.convert(by, value)
417408
return self._execute(Command.FIND_CHILD_ELEMENT, {"using": by, "value": value})["value"]
418409

419410
def find_elements(self, by=By.ID, value=None) -> List[WebElement]:
@@ -426,16 +417,7 @@ def find_elements(self, by=By.ID, value=None) -> List[WebElement]:
426417
427418
:rtype: list of WebElement
428419
"""
429-
if by == By.ID:
430-
by = By.CSS_SELECTOR
431-
value = f'[id="{value}"]'
432-
elif by == By.CLASS_NAME:
433-
by = By.CSS_SELECTOR
434-
value = f".{value}"
435-
elif by == By.NAME:
436-
by = By.CSS_SELECTOR
437-
value = f'[name="{value}"]'
438-
420+
by, value = self._parent.locator_converter.convert(by, value)
439421
return self._execute(Command.FIND_CHILD_ELEMENTS, {"using": by, "value": value})["value"]
440422

441423
def __hash__(self) -> int:

py/test/selenium/webdriver/common/driver_element_finding_tests.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,3 +715,21 @@ def test_should_not_be_able_to_find_an_element_on_a_blank_page(driver, pages):
715715
driver.get("about:blank")
716716
with pytest.raises(NoSuchElementException):
717717
driver.find_element(By.TAG_NAME, "a")
718+
719+
720+
# custom finders tests
721+
722+
723+
def test_register_and_get_custom_finder():
724+
By.register_custom_finder("custom", "custom strategy")
725+
assert By.get_finder("custom") == "custom strategy"
726+
727+
728+
def test_get_nonexistent_finder():
729+
assert By.get_finder("nonexistent") is None
730+
731+
732+
def test_clear_custom_finders():
733+
By.register_custom_finder("custom", "custom strategy")
734+
By.clear_custom_finders()
735+
assert By.get_finder("custom") is None
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
from selenium.webdriver.common.by import By
18+
from selenium.webdriver.remote.webelement import WebElement
19+
20+
21+
# Custom element class
22+
class MyCustomElement(WebElement):
23+
def custom_method(self):
24+
return "Custom element method"
25+
26+
27+
def test_find_element_with_custom_class(driver, pages):
28+
"""Test to ensure custom element class is used for a single element."""
29+
driver._web_element_cls = MyCustomElement
30+
pages.load("simpleTest.html")
31+
element = driver.find_element(By.TAG_NAME, "body")
32+
assert isinstance(element, MyCustomElement)
33+
assert element.custom_method() == "Custom element method"
34+
35+
36+
def test_find_elements_with_custom_class(driver, pages):
37+
"""Test to ensure custom element class is used for multiple elements."""
38+
driver._web_element_cls = MyCustomElement
39+
pages.load("simpleTest.html")
40+
elements = driver.find_elements(By.TAG_NAME, "div")
41+
assert all(isinstance(el, MyCustomElement) for el in elements)
42+
assert all(el.custom_method() == "Custom element method" for el in elements)
43+
44+
45+
def test_default_element_class(driver, pages):
46+
"""Test to ensure default WebElement class is used."""
47+
pages.load("simpleTest.html")
48+
element = driver.find_element(By.TAG_NAME, "body")
49+
assert isinstance(element, WebElement)
50+
assert not hasattr(element, "custom_method")

0 commit comments

Comments
 (0)