Skip to content

Commit c69505f

Browse files
authored
Merge branch 'trunk' into issue-13793
2 parents d965ca5 + c145a83 commit c69505f

File tree

15 files changed

+376
-65
lines changed

15 files changed

+376
-65
lines changed

.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 `
89 Bytes
Binary file not shown.

common/extensions/webextensions-selenium-example/manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
]
1515
}
1616
],
17+
"permissions": [
18+
"storage",
19+
"scripting"
20+
],
21+
"host_permissions": [
22+
"https://*/*",
23+
"http://*/*"
24+
],
1725
"browser_specific_settings": {
1826
"gecko": {
1927

javascript/grid-ui/src/screens/Overview/Overview.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,39 @@ function Overview (): JSX.Element {
4848
fetchPolicy: 'network-only'
4949
})
5050

51-
const [sortOption, setSortOption] = useState('osInfo.name')
51+
function compareSlotStereotypes(a: NodeInfo, b: NodeInfo, attribute: string): number {
52+
const joinA = a.slotStereotypes.length === 1
53+
? a.slotStereotypes[0][attribute]
54+
: a.slotStereotypes.slice().map(st => st[attribute]).reverse().join(',')
55+
const joinB = b.slotStereotypes.length === 1
56+
? b.slotStereotypes[0][attribute]
57+
: b.slotStereotypes.slice().map(st => st[attribute]).reverse().join(',')
58+
return joinA.localeCompare(joinB)
59+
}
60+
61+
const sortProperties = {
62+
'platformName': (a, b) => compareSlotStereotypes(a, b, 'platformName'),
63+
'status': (a, b) => a.status.localeCompare(b.status),
64+
'browserName': (a, b) => compareSlotStereotypes(a, b, 'browserName'),
65+
'browserVersion': (a, b) => compareSlotStereotypes(a, b, 'browserVersion'),
66+
'slotCount': (a, b) => {
67+
const valueA = a.slotStereotypes.reduce((sum, st) => sum + st.slotCount, 0)
68+
const valueB = b.slotStereotypes.reduce((sum, st) => sum + st.slotCount, 0)
69+
return valueA < valueB ? -1 : 1
70+
},
71+
'id': (a, b) => (a.id < b.id ? -1 : 1)
72+
}
73+
74+
const sortPropertiesLabel = {
75+
'platformName': 'Platform Name',
76+
'status': 'Status',
77+
'browserName': 'Browser Name',
78+
'browserVersion': 'Browser Version',
79+
'slotCount': 'Slot Count',
80+
'id': 'ID'
81+
}
82+
83+
const [sortOption, setSortOption] = useState(Object.keys(sortProperties)[0])
5284
const [sortOrder, setSortOrder] = useState(1)
5385
const [sortedNodes, setSortedNodes] = useState<NodeInfo[]>([])
5486
const [isDescending, setIsDescending] = useState(false)
@@ -62,12 +94,6 @@ function Overview (): JSX.Element {
6294
setSortOrder(event.target.checked ? -1 : 1)
6395
}
6496

65-
const sortProperties = {
66-
'osInfo.name': (a, b) => a.osInfo.name.localeCompare(b.osInfo.name),
67-
'status': (a, b) => a.status.localeCompare(b.status),
68-
'id': (a, b) => (a.id < b.id ? -1 : 1)
69-
}
70-
7197
const sortNodes = useMemo(() => {
7298
return (nodes: NodeInfo[], option: string, order: number) => {
7399
const sortFn = sortProperties[option] || (() => 0)
@@ -156,10 +182,12 @@ function Overview (): JSX.Element {
156182
<InputLabel>Sort By</InputLabel>
157183
<Box display="flex" alignItems="center">
158184
<Select value={sortOption} onChange={handleSortChange}
159-
label="Sort By" style={{ minWidth: '150px' }}>
160-
<MenuItem value="osInfo.name">Platform</MenuItem>
161-
<MenuItem value="status">Status</MenuItem>
162-
<MenuItem value="id">ID</MenuItem>
185+
label="Sort By" style={{ minWidth: '170px' }}>
186+
{Object.keys(sortProperties).map((key) => (
187+
<MenuItem value={key}>
188+
{sortPropertiesLabel[key]}
189+
</MenuItem>
190+
))}
163191
</Select>
164192
<FormControlLabel
165193
control={<Checkbox checked={isDescending}

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

0 commit comments

Comments
 (0)