Skip to content

Commit 8621c15

Browse files
authored
Merge branch 'trunk' into java-bc-event-tests
2 parents 1e0724e + 775cfb3 commit 8621c15

File tree

13 files changed

+229
-159
lines changed

13 files changed

+229
-159
lines changed

java/src/org/openqa/selenium/support/ui/Select.java

Lines changed: 69 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -122,55 +122,7 @@ public WebElement getFirstSelectedOption() {
122122
*/
123123
@Override
124124
public void selectByVisibleText(String text) {
125-
assertSelectIsEnabled();
126-
assertSelectIsVisible();
127-
128-
// try to find the option via XPATH ...
129-
List<WebElement> options =
130-
element.findElements(
131-
By.xpath(".//option[normalize-space(.) = " + Quotes.escape(text) + "]"));
132-
133-
for (WebElement option : options) {
134-
if (!hasCssPropertyAndVisible(option))
135-
throw new NoSuchElementException("Invisible option with text: " + text);
136-
setSelected(option, true);
137-
if (!isMultiple()) {
138-
return;
139-
}
140-
}
141-
142-
boolean matched = !options.isEmpty();
143-
if (!matched && text.contains(" ")) {
144-
String subStringWithoutSpace = getLongestSubstringWithoutSpace(text);
145-
List<WebElement> candidates;
146-
if ("".equals(subStringWithoutSpace)) {
147-
// hmm, text is either empty or contains only spaces - get all options ...
148-
candidates = element.findElements(By.tagName("option"));
149-
} else {
150-
// get candidates via XPATH ...
151-
candidates =
152-
element.findElements(
153-
By.xpath(".//option[contains(., " + Quotes.escape(subStringWithoutSpace) + ")]"));
154-
}
155-
156-
String trimmed = text.trim();
157-
158-
for (WebElement option : candidates) {
159-
if (trimmed.equals(option.getText().trim())) {
160-
if (!hasCssPropertyAndVisible(option))
161-
throw new NoSuchElementException("Invisible option with text: " + text);
162-
setSelected(option, true);
163-
if (!isMultiple()) {
164-
return;
165-
}
166-
matched = true;
167-
}
168-
}
169-
}
170-
171-
if (!matched) {
172-
throw new NoSuchElementException("Cannot locate option with text: " + text);
173-
}
125+
selectByVisibleText(text, false);
174126
}
175127

176128
/**
@@ -193,53 +145,7 @@ public void selectByVisibleText(String text) {
193145
*/
194146
@Override
195147
public void selectByContainsVisibleText(String text) {
196-
assertSelectIsEnabled();
197-
assertSelectIsVisible();
198-
199-
// try to find the option via XPATH ...
200-
List<WebElement> options =
201-
element.findElements(
202-
By.xpath(".//option[normalize-space(.) = " + Quotes.escape(text) + "]"));
203-
204-
for (WebElement option : options) {
205-
if (!hasCssPropertyAndVisible(option))
206-
throw new NoSuchElementException("Invisible option with text: " + text);
207-
setSelected(option, true);
208-
if (!isMultiple()) {
209-
return;
210-
}
211-
}
212-
213-
boolean matched = !options.isEmpty();
214-
if (!matched) {
215-
String searchText = text.contains(" ") ? getLongestSubstringWithoutSpace(text) : text;
216-
217-
List<WebElement> candidates;
218-
if (searchText.isEmpty()) {
219-
candidates = element.findElements(By.tagName("option"));
220-
} else {
221-
candidates =
222-
element.findElements(
223-
By.xpath(".//option[contains(., " + Quotes.escape(searchText) + ")]"));
224-
}
225-
226-
String trimmed = text.trim();
227-
for (WebElement option : candidates) {
228-
if (option.getText().contains(trimmed)) {
229-
if (!hasCssPropertyAndVisible(option))
230-
throw new NoSuchElementException("Invisible option with text: " + text);
231-
setSelected(option, true);
232-
if (!isMultiple()) {
233-
return;
234-
}
235-
matched = true;
236-
}
237-
}
238-
}
239-
240-
if (!matched) {
241-
throw new NoSuchElementException("Cannot locate option with text: " + text);
242-
}
148+
selectByVisibleText(text, true);
243149
}
244150

245151
private String getLongestSubstringWithoutSpace(String s) {
@@ -425,6 +331,73 @@ private void setSelected(WebElement option, boolean select) {
425331
}
426332
}
427333

334+
/**
335+
* @param text The visible text to match against. It can be a partial match of the option if
336+
* isPartialMatch = true
337+
* @param isPartialMatch If true a partial match on the Options list will be performed, otherwise
338+
* exact match
339+
* @throws NoSuchElementException If no matching option elements are found or matching options are
340+
* hidden
341+
*/
342+
private void selectByVisibleText(String text, boolean isPartialMatch) {
343+
assertSelectIsEnabled();
344+
assertSelectIsVisible();
345+
346+
// try to find the option via XPATH ...
347+
List<WebElement> options =
348+
element.findElements(
349+
By.xpath(".//option[normalize-space(.) = " + Quotes.escape(text) + "]"));
350+
351+
for (WebElement option : options) {
352+
if (!hasCssPropertyAndVisible(option))
353+
throw new NoSuchElementException("Invisible option with text: " + text);
354+
355+
setSelected(option, true);
356+
357+
if (!isMultiple()) {
358+
return;
359+
}
360+
}
361+
362+
boolean matched = !options.isEmpty();
363+
if (!matched) {
364+
String searchText = text.contains(" ") ? getLongestSubstringWithoutSpace(text) : text;
365+
366+
List<WebElement> candidates;
367+
if (searchText.isEmpty()) {
368+
candidates = element.findElements(By.tagName("option"));
369+
} else {
370+
candidates =
371+
element.findElements(
372+
By.xpath(".//option[contains(., " + Quotes.escape(searchText) + ")]"));
373+
}
374+
375+
String trimmed = text.trim();
376+
for (WebElement option : candidates) {
377+
boolean isMatchedOptionFound =
378+
isPartialMatch
379+
? option.getText().contains(trimmed)
380+
: option.getText().trim().equals(trimmed);
381+
382+
if (isMatchedOptionFound) {
383+
if (!hasCssPropertyAndVisible(option))
384+
throw new NoSuchElementException("Invisible option with text: " + text);
385+
386+
setSelected(option, true);
387+
388+
if (!isMultiple()) {
389+
return;
390+
}
391+
matched = true;
392+
}
393+
}
394+
}
395+
396+
if (!matched) {
397+
throw new NoSuchElementException("Cannot locate option with text: " + text);
398+
}
399+
}
400+
428401
private void assertOptionIsEnabled(WebElement option, boolean select) {
429402
if (select && !option.isEnabled()) {
430403
throw new UnsupportedOperationException("You may not select a disabled option");

javascript/atoms/dom.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,11 +1173,18 @@ bot.dom.appendVisibleTextLinesFromTextNode_ = function (textNode, lines,
11731173
}
11741174

11751175
if (textTransform == 'capitalize') {
1176-
// the unicode regex ending with /gu does not work in IE
1177-
var re = goog.userAgent.IE ? /(^|\s|\b)(\S)/g : /(^|\s|\b)(\S)/gu;
1176+
// 1) don't treat '_' as a separator (protects snake_case)
1177+
var re = /(^|[^'_0-9A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF])([A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9])/g;
11781178
text = text.replace(re, function () {
11791179
return arguments[1] + arguments[2].toUpperCase();
11801180
});
1181+
1182+
// 2) capitalize after opening "_" or "*"
1183+
// Preceded by start or a non-word (so it won't fire for snake_case)
1184+
re = /(^|[^'_0-9A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24B6-\u24E9])([_*])([A-Za-z\u00C0-\u02AF\u1E00-\u1EFF\u24D0-\u24E9])/g;
1185+
text = text.replace(re, function () {
1186+
return arguments[1] + arguments[2] + arguments[3].toUpperCase();
1187+
});
11811188
} else if (textTransform == 'uppercase') {
11821189
text = text.toUpperCase();
11831190
} else if (textTransform == 'lowercase') {

javascript/atoms/test/text_test.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,13 @@
235235
function testShouldRetainTheFormattingOfTextWithinAPreElement() {
236236
var text = getVisibleTextByElementId("preformatted");
237237

238-
assertEquals(" This section has a preformatted\n text block \n split in four lines\n ", text);
238+
assertEquals(" This section has a preformatted\n text block\n split in four lines\n ", text);
239239
}
240240

241241
function testShouldRetainTheFormattingOfTextWithinAPreElementThatIsWithinARegularBlock() {
242242
var text = getVisibleTextByElementId("div-with-pre");
243243

244-
assertEquals("before pre\n This section has a preformatted\n text block \n split in four lines\n \nafter pre", text);
244+
assertEquals("before pre\n This section has a preformatted\n text block\n split in four lines\n \nafter pre", text);
245245
}
246246

247247
function testGetVisibleTextShouldHandleCssContentReplacement() {
@@ -264,6 +264,11 @@
264264
text = getVisibleTextByElementId("uppercased");
265265
assertEquals("HELLO, WORLD! BLA-BLA-BLA", text);
266266

267+
text = getVisibleTextByElementId("capitalized_accented_character");
268+
assertEquals("Fecha De Expiración", text);
269+
text = getVisibleTextByElementId("capitalized_enye");
270+
assertEquals("Mañana", text);
271+
267272
text = getVisibleTextByElementId("capitalized-1");
268273
assertEquals("Äåìî", text);
269274
text = getVisibleTextByElementId("capitalized-2");
@@ -423,6 +428,11 @@
423428
<a id="uppercased" style="text-transform: uppercase">hello, world! bla-bla-BLA</a><br/>
424429
</div>
425430

431+
<div>
432+
<a id="capitalized_accented_character" style="text-transform: capitalize">Fecha de expiración</a><br/>
433+
<a id="capitalized_enye" style="text-transform: capitalize">mañana</a><br/>
434+
</div>
435+
426436
<div>
427437
<a lang="ru" id="capitalized-1" style="text-transform: capitalize">äåìî</a><br/>
428438
<a id="capitalized-2" style="text-transform: capitalize">Manipulowanie przepływem</a><br/>

py/BUILD.bazel

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,11 +328,11 @@ py_wheel(
328328
python_tag = "py3",
329329
requires = [
330330
"urllib3[socks]>=2.5.0,<3.0",
331-
"trio~=0.30.0",
332-
"trio-websocket~=0.12.2",
331+
"trio>=0.30.0,<1.0",
332+
"trio-websocket>=0.12.2,<1.0",
333333
"certifi>=2025.6.15",
334-
"typing_extensions~=4.14.0",
335-
"websocket-client~=1.8.0",
334+
"typing_extensions>=4.14.0,<5.0",
335+
"websocket-client>=1.8.0,<2.0",
336336
],
337337
strip_path_prefixes = [
338338
"py/",

py/conftest.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# under the License.
1717

1818
import os
19-
import platform
19+
import sys
2020
from dataclasses import dataclass
2121
from pathlib import Path
2222

@@ -182,7 +182,14 @@ def driver_class(self, cls_name):
182182

183183
@property
184184
def exe_platform(self):
185-
return platform.system()
185+
if sys.platform == "win32":
186+
return "Windows"
187+
elif sys.platform == "darwin":
188+
return "Darwin"
189+
elif sys.platform == "linux":
190+
return "Linux"
191+
else:
192+
return sys.platform.title()
186193

187194
@property
188195
def browser_path(self):
@@ -397,7 +404,7 @@ def server(request):
397404
)
398405

399406
remote_env = os.environ.copy()
400-
if platform.system() == "Linux":
407+
if sys.platform == "linux":
401408
# There are issues with window size/position when running Firefox
402409
# under Wayland, so we use XWayland instead.
403410
remote_env["MOZ_ENABLE_WAYLAND"] = "0"

py/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ classifiers = [
2727
]
2828
dependencies = [
2929
"urllib3[socks]>=2.5.0,<3.0",
30-
"trio~=0.30.0",
31-
"trio-websocket~=0.12.2",
30+
"trio>=0.30.0,<1.0",
31+
"trio-websocket>=0.12.2,<1.0",
3232
"certifi>=2025.6.15",
33-
"typing_extensions~=4.14.0",
34-
"websocket-client~=1.8.0",
33+
"typing_extensions>=4.14.0,<5.0",
34+
"websocket-client>=1.8.0,<2.0",
3535
]
3636

3737
[project.urls]

py/selenium/webdriver/common/actions/action_builder.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
from typing import Optional, Union
18+
19+
from typing import Any, Optional, Union
1920

2021
from selenium.webdriver.remote.command import Command
2122

@@ -40,7 +41,7 @@ def __init__(
4041
mouse = mouse or PointerInput(interaction.POINTER_MOUSE, "mouse")
4142
keyboard = keyboard or KeyInput(interaction.KEY)
4243
wheel = wheel or WheelInput(interaction.WHEEL)
43-
self.devices = [mouse, keyboard, wheel]
44+
self.devices: list[Union[PointerInput, KeyInput, WheelInput]] = [mouse, keyboard, wheel]
4445
self._key_action = KeyActions(keyboard)
4546
self._pointer_action = PointerActions(mouse, duration=duration)
4647
self._wheel_action = WheelActions(wheel)
@@ -62,11 +63,11 @@ def get_device_with(self, name: str) -> Optional[Union["WheelInput", "PointerInp
6263

6364
@property
6465
def pointer_inputs(self) -> list[PointerInput]:
65-
return [device for device in self.devices if device.type == interaction.POINTER]
66+
return [device for device in self.devices if isinstance(device, PointerInput)]
6667

6768
@property
6869
def key_inputs(self) -> list[KeyInput]:
69-
return [device for device in self.devices if device.type == interaction.KEY]
70+
return [device for device in self.devices if isinstance(device, KeyInput)]
7071

7172
@property
7273
def key_action(self) -> KeyActions:
@@ -159,7 +160,7 @@ def perform(self) -> None:
159160
>>> el = driver.find_element(id: "some_id")
160161
>>> action_builder.click(el).pause(keyboard).pause(keyboard).pause(keyboard).send_keys("keys").perform()
161162
"""
162-
enc = {"actions": []}
163+
enc: dict[str, list[Any]] = {"actions": []}
163164
for device in self.devices:
164165
encoded = device.encode()
165166
if encoded["actions"]:

0 commit comments

Comments
 (0)