Skip to content

Commit 17e7099

Browse files
authored
Merge branch 'trunk' into network_implementation_python
2 parents 01f6aaf + c145a83 commit 17e7099

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
@@ -148,6 +148,14 @@ class RemoteConnection:
148148
)
149149
_ca_certs = os.getenv("REQUESTS_CA_BUNDLE") if "REQUESTS_CA_BUNDLE" in os.environ else certifi.where()
150150

151+
system = platform.system().lower()
152+
if system == "darwin":
153+
system = "mac"
154+
155+
# Class variables for headers
156+
extra_headers = None
157+
user_agent = f"selenium/{__version__} (python {system})"
158+
151159
@classmethod
152160
def get_timeout(cls):
153161
""":Returns:
@@ -201,14 +209,10 @@ def get_remote_connection_headers(cls, parsed_url, keep_alive=False):
201209
- keep_alive (Boolean) - Is this a keep-alive connection (default: False)
202210
"""
203211

204-
system = platform.system().lower()
205-
if system == "darwin":
206-
system = "mac"
207-
208212
headers = {
209213
"Accept": "application/json",
210214
"Content-Type": "application/json;charset=UTF-8",
211-
"User-Agent": f"selenium/{__version__} (python {system})",
215+
"User-Agent": cls.user_agent,
212216
}
213217

214218
if parsed_url.username:
@@ -218,6 +222,9 @@ def get_remote_connection_headers(cls, parsed_url, keep_alive=False):
218222
if keep_alive:
219223
headers.update({"Connection": "keep-alive"})
220224

225+
if cls.extra_headers:
226+
headers.update(cls.extra_headers)
227+
221228
return headers
222229

223230
def _get_proxy_url(self):
@@ -241,7 +248,12 @@ def _separate_http_proxy_auth(self):
241248

242249
def _get_connection_manager(self):
243250
pool_manager_init_args = {"timeout": self.get_timeout()}
244-
if self._ca_certs:
251+
pool_manager_init_args.update(self._init_args_for_pool_manager.get("init_args_for_pool_manager", {}))
252+
253+
if self._ignore_certificates:
254+
pool_manager_init_args["cert_reqs"] = "CERT_NONE"
255+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
256+
elif self._ca_certs:
245257
pool_manager_init_args["cert_reqs"] = "CERT_REQUIRED"
246258
pool_manager_init_args["ca_certs"] = self._ca_certs
247259

@@ -257,9 +269,18 @@ def _get_connection_manager(self):
257269

258270
return urllib3.PoolManager(**pool_manager_init_args)
259271

260-
def __init__(self, remote_server_addr: str, keep_alive: bool = False, ignore_proxy: bool = False):
272+
def __init__(
273+
self,
274+
remote_server_addr: str,
275+
keep_alive: bool = False,
276+
ignore_proxy: bool = False,
277+
ignore_certificates: bool = False,
278+
init_args_for_pool_manager: dict = None,
279+
):
261280
self.keep_alive = keep_alive
262281
self._url = remote_server_addr
282+
self._ignore_certificates = ignore_certificates
283+
self._init_args_for_pool_manager = init_args_for_pool_manager or {}
263284

264285
# Env var NO_PROXY will override this part of the code
265286
_no_proxy = os.environ.get("no_proxy", os.environ.get("NO_PROXY"))
@@ -285,6 +306,16 @@ def __init__(self, remote_server_addr: str, keep_alive: bool = False, ignore_pro
285306
self._conn = self._get_connection_manager()
286307
self._commands = remote_commands
287308

309+
extra_commands = {}
310+
311+
def add_command(self, name, method, url):
312+
"""Register a new command."""
313+
self._commands[name] = (method, url)
314+
315+
def get_command(self, name: str):
316+
"""Retrieve a command if it exists."""
317+
return self._commands.get(name)
318+
288319
def execute(self, command, params):
289320
"""Send a command to the remote server.
290321
@@ -296,7 +327,7 @@ def execute(self, command, params):
296327
- params - A dictionary of named parameters to send with the command as
297328
its JSON payload.
298329
"""
299-
command_info = self._commands[command]
330+
command_info = self._commands.get(command) or self.extra_commands.get(command)
300331
assert command_info is not None, f"Unrecognised command {command}"
301332
path_string = command_info[1]
302333
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
@@ -59,6 +59,7 @@
5959
from .errorhandler import ErrorHandler
6060
from .file_detector import FileDetector
6161
from .file_detector import LocalFileDetector
62+
from .locator_converter import LocatorConverter
6263
from .mobile import Mobile
6364
from .remote_connection import RemoteConnection
6465
from .script_key import ScriptKey
@@ -172,6 +173,8 @@ def __init__(
172173
keep_alive: bool = True,
173174
file_detector: Optional[FileDetector] = None,
174175
options: Optional[Union[BaseOptions, List[BaseOptions]]] = None,
176+
locator_converter: Optional[LocatorConverter] = None,
177+
web_element_cls: Optional[type] = None,
175178
) -> None:
176179
"""Create a new driver that will issue commands using the wire
177180
protocol.
@@ -184,6 +187,8 @@ def __init__(
184187
- file_detector - Pass custom file detector object during instantiation. If None,
185188
then default LocalFileDetector() will be used.
186189
- options - instance of a driver options.Options class
190+
- locator_converter - Custom locator converter to use. Defaults to None.
191+
- web_element_cls - Custom class to use for web elements. Defaults to WebElement.
187192
"""
188193

189194
if isinstance(options, list):
@@ -208,6 +213,8 @@ def __init__(
208213
self._switch_to = SwitchTo(self)
209214
self._mobile = Mobile(self)
210215
self.file_detector = file_detector or LocalFileDetector()
216+
self.locator_converter = locator_converter or LocatorConverter()
217+
self._web_element_cls = web_element_cls or self._web_element_cls
211218
self._authenticator_id = None
212219
self.start_client()
213220
self.start_session(capabilities)
@@ -730,22 +737,14 @@ def find_element(self, by=By.ID, value: Optional[str] = None) -> WebElement:
730737
731738
:rtype: WebElement
732739
"""
740+
by, value = self.locator_converter.convert(by, value)
741+
733742
if isinstance(by, RelativeBy):
734743
elements = self.find_elements(by=by, value=value)
735744
if not elements:
736745
raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
737746
return elements[0]
738747

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

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

767-
if by == By.ID:
768-
by = By.CSS_SELECTOR
769-
value = f'[id="{value}"]'
770-
elif by == By.CLASS_NAME:
771-
by = By.CSS_SELECTOR
772-
value = f".{value}"
773-
elif by == By.NAME:
774-
by = By.CSS_SELECTOR
775-
value = f'[name="{value}"]'
776-
777768
# Return empty list if driver returns null
778769
# See https://github.com/SeleniumHQ/selenium/issues/4555
779770
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)