From 77362cada4b6b6fa991f263acf0b41e997cbf22c Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 29 Aug 2025 15:53:38 +0530 Subject: [PATCH 1/8] add emulation command - `set_locale_override` --- .../webdriver/common/bidi/emulation.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/py/selenium/webdriver/common/bidi/emulation.py b/py/selenium/webdriver/common/bidi/emulation.py index 3c8b609235f37..3386ee0419cd1 100644 --- a/py/selenium/webdriver/common/bidi/emulation.py +++ b/py/selenium/webdriver/common/bidi/emulation.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. +import re from typing import Any, Optional, Union from selenium.webdriver.common.bidi.common import command_builder @@ -215,3 +216,67 @@ def set_geolocation_override( params["userContexts"] = user_contexts self.conn.execute(command_builder("emulation.setGeolocationOverride", params)) + + def set_locale_override( + self, + locale: Optional[str] = None, + contexts: Optional[list[str]] = None, + user_contexts: Optional[list[str]] = None, + ) -> None: + """Set locale override for the given contexts or user contexts. + + Parameters: + ----------- + locale: Locale string (language tag) to emulate, or None to clear override. + contexts: List of browsing context IDs to apply the override to. + user_contexts: List of user context IDs to apply the override to. + + Raises: + ------ + ValueError: If both contexts and user_contexts are provided, or if neither + contexts nor user_contexts are provided, or if locale is invalid. + """ + if contexts is not None and user_contexts is not None: + raise ValueError("Cannot specify both contexts and userContexts") + + if contexts is None and user_contexts is None: + raise ValueError("Must specify either contexts or userContexts") + + if locale is not None and not self._is_valid_language_tag(locale): + raise ValueError(f"Invalid language tag: {locale}") + + params: dict[str, Any] = {"locale": locale} + + if contexts is not None: + params["contexts"] = contexts + elif user_contexts is not None: + params["userContexts"] = user_contexts + + self.conn.execute(command_builder("emulation.setLocaleOverride", params)) + + def _is_valid_language_tag(self, tag: str) -> bool: + """Check if a language tag is structurally valid according to BCP 47. + + This is a simplified validation that covers the most common cases. + Full BCP 47 validation would be more complex. + + Parameters: + ----------- + tag: The language tag to validate. + + Returns: + -------- + True if the tag is structurally valid, False otherwise. + """ + if not tag or not isinstance(tag, str): + return False + + # Basic BCP 47 language tag pattern + # Format: language[-script][-region][-variant][-extension][-privateuse] + # language: 2-3 lowercase letters + # script: 4 letters with first uppercase + # region: 2 uppercase letters or 3 digits + # variant: 5-8 alphanumeric characters or 4 characters starting with digit + pattern = r"^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-[A-Z]{2}|[0-9]{3})?(?:-[a-zA-Z0-9]{5,8}|[0-9][a-zA-Z0-9]{3})*(?:-[a-wy-zA-WY-Z0-9](?:-[a-zA-Z0-9]{2,8})+)*(?:-x(?:-[a-zA-Z0-9]{1,8})+)?$" # noqa: E501 + + return bool(re.match(pattern, tag)) From eb8289351b5f57597e7d7dfae0d4887fd3f0e0b4 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 29 Aug 2025 15:53:50 +0530 Subject: [PATCH 2/8] add test for `set_locale_override` --- .../webdriver/common/bidi_emulation_tests.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/py/test/selenium/webdriver/common/bidi_emulation_tests.py b/py/test/selenium/webdriver/common/bidi_emulation_tests.py index 8778088615fba..47d2904a8a4bf 100644 --- a/py/test/selenium/webdriver/common/bidi_emulation_tests.py +++ b/py/test/selenium/webdriver/common/bidi_emulation_tests.py @@ -48,6 +48,10 @@ def get_browser_geolocation(driver, user_context=None): """) +def get_browser_locale(driver): + return driver.execute_script("return navigator.languages || [navigator.language];") + + def test_emulation_initialized(driver): """Test that the emulation module is initialized properly.""" assert driver.emulation is not None @@ -214,3 +218,44 @@ def test_set_geolocation_override_with_error(driver, pages): result = get_browser_geolocation(driver) assert "error" in result, f"Expected geolocation error, got: {result}" + + +def test_set_locale_override_with_contexts(driver, pages): + """Test setting locale override with browsing contexts.""" + context_id = driver.current_window_handle + driver.browsing_context.navigate(context_id, pages.url("formPage.html")) + + original_locale = get_browser_locale(driver) + print("Original locale:", original_locale) + + # Set locale override to French + test_locale = "fr-FR" + driver.emulation.set_locale_override(locale=test_locale, contexts=[context_id]) + driver.browsing_context.reload(context_id, wait="complete") + + current_locale = get_browser_locale(driver) + assert current_locale == test_locale, f"Expected locale {test_locale}, got {current_locale}" + + +def test_set_locale_override_with_user_contexts(driver, pages): + """Test setting locale override with user contexts.""" + # Create a user context + user_context = driver.browser.create_user_context() + + context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context) + + driver.switch_to.window(context_id) + driver.browsing_context.navigate(context_id, pages.url("formPage.html")) + + original_locale = get_browser_locale(driver) + print("Original locale:", original_locale) + + # Set locale override to Spanish + test_locale = "es-ES" + driver.emulation.set_locale_override(locale=test_locale, user_contexts=[user_context]) + + current_locale = get_browser_locale(driver) + assert current_locale == test_locale, f"Expected locale {test_locale}, got {current_locale}" + + driver.browsing_context.close(context_id) + driver.browser.remove_user_context(user_context) From 9dab7b63b978efb924e8d6360544a6bdca695f71 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 24 Oct 2025 17:18:49 +0530 Subject: [PATCH 3/8] fix tests --- .../webdriver/common/bidi_emulation_tests.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_emulation_tests.py b/py/test/selenium/webdriver/common/bidi_emulation_tests.py index 47d2904a8a4bf..6c9eeb836c514 100644 --- a/py/test/selenium/webdriver/common/bidi_emulation_tests.py +++ b/py/test/selenium/webdriver/common/bidi_emulation_tests.py @@ -49,7 +49,12 @@ def get_browser_geolocation(driver, user_context=None): def get_browser_locale(driver): - return driver.execute_script("return navigator.languages || [navigator.language];") + result = driver.script._evaluate( + "Intl.DateTimeFormat().resolvedOptions().locale", + {"context": driver.current_window_handle}, + await_promise=False, + ) + return result.result["value"] def test_emulation_initialized(driver): @@ -223,15 +228,12 @@ def test_set_geolocation_override_with_error(driver, pages): def test_set_locale_override_with_contexts(driver, pages): """Test setting locale override with browsing contexts.""" context_id = driver.current_window_handle - driver.browsing_context.navigate(context_id, pages.url("formPage.html")) - - original_locale = get_browser_locale(driver) - print("Original locale:", original_locale) # Set locale override to French test_locale = "fr-FR" driver.emulation.set_locale_override(locale=test_locale, contexts=[context_id]) - driver.browsing_context.reload(context_id, wait="complete") + + driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete") current_locale = get_browser_locale(driver) assert current_locale == test_locale, f"Expected locale {test_locale}, got {current_locale}" @@ -239,21 +241,19 @@ def test_set_locale_override_with_contexts(driver, pages): def test_set_locale_override_with_user_contexts(driver, pages): """Test setting locale override with user contexts.""" - # Create a user context user_context = driver.browser.create_user_context() context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context) driver.switch_to.window(context_id) - driver.browsing_context.navigate(context_id, pages.url("formPage.html")) - - original_locale = get_browser_locale(driver) - print("Original locale:", original_locale) # Set locale override to Spanish test_locale = "es-ES" driver.emulation.set_locale_override(locale=test_locale, user_contexts=[user_context]) + url = pages.url("formPage.html") + driver.browsing_context.navigate(context_id, url, wait="complete") + current_locale = get_browser_locale(driver) assert current_locale == test_locale, f"Expected locale {test_locale}, got {current_locale}" From b1b3465271d2bcb491b95ef2d3bec282c8910f61 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 24 Oct 2025 18:39:24 +0530 Subject: [PATCH 4/8] use langcodes for locale validation --- py/requirements.txt | 1 + py/requirements_lock.txt | 80 ++++++++++++++++++- .../webdriver/common/bidi/emulation.py | 48 +++++------ 3 files changed, 95 insertions(+), 34 deletions(-) diff --git a/py/requirements.txt b/py/requirements.txt index 46960fac7d730..3810a2f101bce 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -22,6 +22,7 @@ jaraco.context==6.0.1 jaraco.functools==4.3.0 jeepney==0.9.0 keyring==25.6.0 +langcodes==3.5.0 markdown-it-py==4.0.0 mdurl==0.1.2 more-itertools==10.8.0 diff --git a/py/requirements_lock.txt b/py/requirements_lock.txt index 3fb0d9d063105..e0939608e297a 100644 --- a/py/requirements_lock.txt +++ b/py/requirements_lock.txt @@ -388,7 +388,6 @@ jeepney==0.9.0 \ --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 # via # -r py/requirements.txt - # keyring # secretstorage keyring==25.6.0 \ --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ @@ -396,6 +395,81 @@ keyring==25.6.0 \ # via # -r py/requirements.txt # twine +langcodes==3.5.0 \ + --hash=sha256:1eef8168d07e51e131a2497ffecad4b663f6208e7c3ae3b8dc15c51734a6f801 \ + --hash=sha256:853c69d1a35e0e13da2f427bb68fb2fa4a8f4fb899e0c62ad8df8d073dcfed33 + # via -r py/requirements.txt +language-data==1.3.0 \ + --hash=sha256:7600ef8aa39555145d06c89f0c324bf7dab834ea0b0a439d8243762e3ebad7ec \ + --hash=sha256:e2ee943551b5ae5f89cd0e801d1fc3835bb0ef5b7e9c3a4e8e17b2b214548fbf + # via langcodes +marisa-trie==1.3.1 \ + --hash=sha256:076731f79f8603cb3216cb6e5bbbc56536c89f63f175ad47014219ecb01e5996 \ + --hash=sha256:0b9816ab993001a7854b02a7daec228892f35bd5ab0ac493bacbd1b80baec9f1 \ + --hash=sha256:0c2bc6bee737f4d47fce48c5b03a7bd3214ef2d83eb5c9f84210091370a5f195 \ + --hash=sha256:0dcd42774e367ceb423c211a4fc8e7ce586acfaf0929c9c06d98002112075239 \ + --hash=sha256:0e6f3b45def6ff23e254eeaa9079267004f0069d0a34eba30a620780caa4f2cb \ + --hash=sha256:137010598d8cebc53dbfb7caf59bde96c33a6af555e3e1bdbf30269b6a157e1e \ + --hash=sha256:2f7c10f69cbc3e6c7d715ec9cb0c270182ea2496063bebeda873f4aa83fd9910 \ + --hash=sha256:3715d779561699471edde70975e07b1de7dddb2816735d40ed16be4b32054188 \ + --hash=sha256:3834304fdeaa1c9b73596ad5a6c01a44fc19c13c115194704b85f7fbdf0a7b8e \ + --hash=sha256:389721481c14a92fa042e4b91ae065bff13e2bc567c85a10aa9d9de80aaa8622 \ + --hash=sha256:3a96ef3e461ecc85ec7d2233ddc449ff5a3fbdc520caea752bc5bc8faa975231 \ + --hash=sha256:3e2a0e1be95237981bd375a388f44b33d69ea5669a2f79fea038e45fff326595 \ + --hash=sha256:3e431f9c80ee1850b2a406770acf52c058b97a27968a0ed6aca45c2614d64c9f \ + --hash=sha256:47631614c5243ed7d15ae0af8245fcc0599f5b7921fae2a4ae992afb27c9afbb \ + --hash=sha256:52d1764906befef91886e3bff374d8090c9716822bd56b70e07aa697188090b7 \ + --hash=sha256:5370f9ef6c008e502537cc1ff518c80ddf749367ce90179efa0e7f6275903a76 \ + --hash=sha256:56043cf908ddf3d7364498085dbc2855d4ea8969aff3bf2439a79482a79e68e2 \ + --hash=sha256:5a6abc9573a6a45d09548fde136dbcd4260b8c56f8dff443eaa565352d7cca59 \ + --hash=sha256:5b7c1e7fa6c3b855e8cfbabf38454d7decbaba1c567d0cd58880d033c6b363bd \ + --hash=sha256:5ef045f694ef66079b4e00c4c9063a00183d6af7d1ff643de6ea5c3b0d9af01b \ + --hash=sha256:68678816818efcd4a1787b557af81f215b989ec88680a86c85c34c914d413690 \ + --hash=sha256:6cac19952e0e258ded765737d1fb11704fe81bf4f27526638a5d44496f329235 \ + --hash=sha256:70b4c96f9119cfeb4dc6a0cf4afc9f92f0b002cde225bcd910915d976c78e66a \ + --hash=sha256:7e957aa4251a8e70b9fe02a16b2d190f18787902da563cb7ba865508b8e8fb04 \ + --hash=sha256:82de2de90488d0fbbf74cf9f20e1afd62e320693b88f5e9565fc80b28f5bbad3 \ + --hash=sha256:83a3748088d117a9b15d8981c947df9e4f56eb2e4b5456ae34fe1f83666c9185 \ + --hash=sha256:83efc045fc58ca04c91a96c9b894d8a19ac6553677a76f96df01ff9f0405f53d \ + --hash=sha256:8c8b2386d2d22c57880ed20a913ceca86363765623175671137484a7d223f07a \ + --hash=sha256:8f81344d212cb41992340b0b8a67e375f44da90590b884204fd3fa5e02107df2 \ + --hash=sha256:954fef9185f8a79441b4e433695116636bf66402945cfee404f8983bafa59788 \ + --hash=sha256:9651daa1fdc471df5a5fa6a4833d3b01e76ac512eea141a5995681aebac5555f \ + --hash=sha256:9688c7b45f744366a4ef661e399f24636ebe440d315ab35d768676c59c613186 \ + --hash=sha256:97107fd12f30e4f8fea97790343a2d2d9a79d93697fe14e1b6f6363c984ff85b \ + --hash=sha256:9868b7a8e0f648d09ffe25ac29511e6e208cc5fb0d156c295385f9d5dc2a138e \ + --hash=sha256:986eaf35a7f63c878280609ecd37edf8a074f7601c199acfec81d03f1ee9a39a \ + --hash=sha256:99a00cab4cf9643a87977c87a5c8961aa44fff8d5dd46e00250135f686e7dedf \ + --hash=sha256:9c56001badaf1779afae5c24b7ab85938644ab8ef3c5fd438ab5d49621b84482 \ + --hash=sha256:9dc61fb8f8993589544f6df268229c6cf0a56ad4ed3e8585a9cd23c5ad79527b \ + --hash=sha256:9de573d933db4753a50af891bcb3ffbfe14e200406214c223aa5dfe2163f316d \ + --hash=sha256:9e467e13971c64db6aed8afe4c2a131c3f73f048bec3f788a6141216acda598d \ + --hash=sha256:9e6496bbad3068e3bbbb934b1e1307bf1a9cb4609f9ec47b57e8ea37f1b5ee40 \ + --hash=sha256:9f92d3577c72d5a97af5c8e3d98247b79c8ccfb64ebf611311dcf631b11e5604 \ + --hash=sha256:a1c6990961d1177f6d8fdf7b610fa2e7c0c02743a090d173f6dfa9dc9231c73c \ + --hash=sha256:a5a0a58ffe2a7eb3f870214c6df8f9a43ce768bd8fed883e6ba8c77645666b63 \ + --hash=sha256:a7416f1a084eb889c5792c57317875aeaa86abfe0bdc6f167712cebcec1d36ee \ + --hash=sha256:a83f5f7ae3494e0cc25211296252b1b86901c788ed82c83adda19d0c98f828d6 \ + --hash=sha256:a850b151bd1e3a5d9afef113adc22727d696603659d575d7e84f994bd8d04bf1 \ + --hash=sha256:ad82ab8a58562cf69e6b786debcc7638b28df12f9f1c7bcffb07efb5c1f09cbd \ + --hash=sha256:b173ec46d521308f7c97d96d6e05cf2088e0548f82544ec9a8656af65593304d \ + --hash=sha256:bf9f2b97fcfd5e2dbb0090d0664023872dcde990df0b545eca8d0ce95795a409 \ + --hash=sha256:c12b44c190deb0d67655021da1f2d0a7d61a257bf844101cf982e68ed344f28d \ + --hash=sha256:c6571462417cda2239b1ade86ceaf3852da9b52c6286046e87d404afc6da20a7 \ + --hash=sha256:c785fd6dae9daa6825734b7b494cdac972f958be1f9cb3fb1f32be8598d2b936 \ + --hash=sha256:c7a33506d0451112911c69f38d55da3e0e050f2be0ea4e5176865cf03baf26a9 \ + --hash=sha256:c89df75aefe1ad7e613340790130f1badc5926bcfa66a6b3c9471071002956a5 \ + --hash=sha256:ca644534f15f85bba14c412afc17de07531e79a766ce85b8dbf3f8b6e7758f20 \ + --hash=sha256:cbd28f95d5f30d9a7af6130869568e75bfd7ef2e0adfb1480f1f44480f5d3603 \ + --hash=sha256:d0f87bdf660f01e88ab3a507955697b2e3284065afa0b94fc9e77d6ad153ed5e \ + --hash=sha256:d4bd41a6e73c0d0adafe4de449b6d35530a4ce6a836a6ee839baf117785ecfd7 \ + --hash=sha256:d8d5e686db0ae758837ed29b3b742afb994d1a01ce10977eabd3490f16b5c9f9 \ + --hash=sha256:e5888b269e790356ce4525f3e8df1fe866d1497b7d7fb7548cfec883cb985288 \ + --hash=sha256:ec633e108f277f2b7f4671d933a909f39bba549910bf103e2940b87a14da2783 \ + --hash=sha256:ecdb19d33b26738a32602ef432b06cc6deeca4b498ce67ba8e5e39c8a7c19745 \ + --hash=sha256:ee428575377e29c636f2b4b3b0488875dcea310c6c5b3412ec4ef997f7bb37cc \ + --hash=sha256:f4bae4f920f2a1082eaf766c1883df7da84abdf333bafa15b8717c10416a615e + # via language-data markdown-it-py==4.0.0 \ --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 @@ -678,9 +752,7 @@ rich==14.1.0 \ secretstorage==3.4.0 \ --hash=sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e \ --hash=sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c - # via - # -r py/requirements.txt - # keyring + # via -r py/requirements.txt sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc diff --git a/py/selenium/webdriver/common/bidi/emulation.py b/py/selenium/webdriver/common/bidi/emulation.py index 3386ee0419cd1..77c0c24c00d7e 100644 --- a/py/selenium/webdriver/common/bidi/emulation.py +++ b/py/selenium/webdriver/common/bidi/emulation.py @@ -15,9 +15,10 @@ # specific language governing permissions and limitations # under the License. -import re from typing import Any, Optional, Union +from langcodes import standardize_tag, tag_is_valid + from selenium.webdriver.common.bidi.common import command_builder @@ -164,6 +165,20 @@ def to_dict(self) -> dict[str, str]: return {"type": self.type} +def _is_valid_language_tag(locale: str) -> str | None: + """Validate and normalize a BCP 47 language tag.""" + + if locale is None: + return None + + if not tag_is_valid(locale): + raise ValueError(f"Invalid locale: {locale}") + + # Canonicalization / normalization + normalized = standardize_tag(locale) + return normalized + + class Emulation: """ BiDi implementation of the emulation module. @@ -227,7 +242,7 @@ def set_locale_override( Parameters: ----------- - locale: Locale string (language tag) to emulate, or None to clear override. + locale: Locale string as per BCP 47, or None to clear override. contexts: List of browsing context IDs to apply the override to. user_contexts: List of user context IDs to apply the override to. @@ -242,7 +257,7 @@ def set_locale_override( if contexts is None and user_contexts is None: raise ValueError("Must specify either contexts or userContexts") - if locale is not None and not self._is_valid_language_tag(locale): + if locale is not None and not _is_valid_language_tag(locale): raise ValueError(f"Invalid language tag: {locale}") params: dict[str, Any] = {"locale": locale} @@ -253,30 +268,3 @@ def set_locale_override( params["userContexts"] = user_contexts self.conn.execute(command_builder("emulation.setLocaleOverride", params)) - - def _is_valid_language_tag(self, tag: str) -> bool: - """Check if a language tag is structurally valid according to BCP 47. - - This is a simplified validation that covers the most common cases. - Full BCP 47 validation would be more complex. - - Parameters: - ----------- - tag: The language tag to validate. - - Returns: - -------- - True if the tag is structurally valid, False otherwise. - """ - if not tag or not isinstance(tag, str): - return False - - # Basic BCP 47 language tag pattern - # Format: language[-script][-region][-variant][-extension][-privateuse] - # language: 2-3 lowercase letters - # script: 4 letters with first uppercase - # region: 2 uppercase letters or 3 digits - # variant: 5-8 alphanumeric characters or 4 characters starting with digit - pattern = r"^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-[A-Z]{2}|[0-9]{3})?(?:-[a-zA-Z0-9]{5,8}|[0-9][a-zA-Z0-9]{3})*(?:-[a-wy-zA-WY-Z0-9](?:-[a-zA-Z0-9]{2,8})+)*(?:-x(?:-[a-zA-Z0-9]{1,8})+)?$" # noqa: E501 - - return bool(re.match(pattern, tag)) From db35b59d85426caa206e2c6f0a80ddc981b3297b Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 24 Oct 2025 18:49:52 +0530 Subject: [PATCH 5/8] parametrize test for multiple locales --- .../webdriver/common/bidi_emulation_tests.py | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_emulation_tests.py b/py/test/selenium/webdriver/common/bidi_emulation_tests.py index 6c9eeb836c514..0c95b20b79a1c 100644 --- a/py/test/selenium/webdriver/common/bidi_emulation_tests.py +++ b/py/test/selenium/webdriver/common/bidi_emulation_tests.py @@ -225,21 +225,47 @@ def test_set_geolocation_override_with_error(driver, pages): assert "error" in result, f"Expected geolocation error, got: {result}" -def test_set_locale_override_with_contexts(driver, pages): +@pytest.mark.parametrize( + "locale,expected_locale", + [ + # Locale with Unicode extension keyword for collation. + ("de-DE-u-co-phonebk", "de-DE"), + # Lowercase language and region. + ("fr-ca", "fr-CA"), + # Uppercase language and region (should be normalized by Intl.Locale). + ("FR-CA", "fr-CA"), + # Mixed case language and region (should be normalized by Intl.Locale). + ("fR-cA", "fr-CA"), + # Locale with transform extension (simple case). + ("en-t-zh", "en"), + ], +) +def test_set_locale_override_with_contexts(driver, pages, locale, expected_locale): """Test setting locale override with browsing contexts.""" context_id = driver.current_window_handle - # Set locale override to French - test_locale = "fr-FR" - driver.emulation.set_locale_override(locale=test_locale, contexts=[context_id]) + driver.emulation.set_locale_override(locale=locale, contexts=[context_id]) driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete") current_locale = get_browser_locale(driver) - assert current_locale == test_locale, f"Expected locale {test_locale}, got {current_locale}" - - -def test_set_locale_override_with_user_contexts(driver, pages): + assert current_locale == expected_locale, f"Expected locale {expected_locale}, got {current_locale}" + + +@pytest.mark.parametrize( + "value", + [ + # Simple language code (2-letter). + "en", + # Language and region (both 2-letter). + "en-US", + # Language and script (4-letter). + "sr-Latn", + # Language, script, and region. + "zh-Hans-CN", + ], +) +def test_set_locale_override_with_user_contexts(driver, pages, value): """Test setting locale override with user contexts.""" user_context = driver.browser.create_user_context() @@ -247,15 +273,12 @@ def test_set_locale_override_with_user_contexts(driver, pages): driver.switch_to.window(context_id) - # Set locale override to Spanish - test_locale = "es-ES" - driver.emulation.set_locale_override(locale=test_locale, user_contexts=[user_context]) + driver.emulation.set_locale_override(locale=value, user_contexts=[user_context]) - url = pages.url("formPage.html") - driver.browsing_context.navigate(context_id, url, wait="complete") + driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete") current_locale = get_browser_locale(driver) - assert current_locale == test_locale, f"Expected locale {test_locale}, got {current_locale}" + assert current_locale == value, f"Expected locale {value}, got {current_locale}" driver.browsing_context.close(context_id) driver.browser.remove_user_context(user_context) From 923a640039d363ee0300802abe85bdf8da61f904 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 24 Oct 2025 19:14:50 +0530 Subject: [PATCH 6/8] remove validation validation is done on the remote end, and error on incorrect locale will propagate as WebdriverException --- py/requirements.txt | 1 - py/requirements_lock.txt | 75 ------------------- .../webdriver/common/bidi/emulation.py | 19 ----- 3 files changed, 95 deletions(-) diff --git a/py/requirements.txt b/py/requirements.txt index 3810a2f101bce..46960fac7d730 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -22,7 +22,6 @@ jaraco.context==6.0.1 jaraco.functools==4.3.0 jeepney==0.9.0 keyring==25.6.0 -langcodes==3.5.0 markdown-it-py==4.0.0 mdurl==0.1.2 more-itertools==10.8.0 diff --git a/py/requirements_lock.txt b/py/requirements_lock.txt index e0939608e297a..1d3ebfd73c84c 100644 --- a/py/requirements_lock.txt +++ b/py/requirements_lock.txt @@ -395,81 +395,6 @@ keyring==25.6.0 \ # via # -r py/requirements.txt # twine -langcodes==3.5.0 \ - --hash=sha256:1eef8168d07e51e131a2497ffecad4b663f6208e7c3ae3b8dc15c51734a6f801 \ - --hash=sha256:853c69d1a35e0e13da2f427bb68fb2fa4a8f4fb899e0c62ad8df8d073dcfed33 - # via -r py/requirements.txt -language-data==1.3.0 \ - --hash=sha256:7600ef8aa39555145d06c89f0c324bf7dab834ea0b0a439d8243762e3ebad7ec \ - --hash=sha256:e2ee943551b5ae5f89cd0e801d1fc3835bb0ef5b7e9c3a4e8e17b2b214548fbf - # via langcodes -marisa-trie==1.3.1 \ - --hash=sha256:076731f79f8603cb3216cb6e5bbbc56536c89f63f175ad47014219ecb01e5996 \ - --hash=sha256:0b9816ab993001a7854b02a7daec228892f35bd5ab0ac493bacbd1b80baec9f1 \ - --hash=sha256:0c2bc6bee737f4d47fce48c5b03a7bd3214ef2d83eb5c9f84210091370a5f195 \ - --hash=sha256:0dcd42774e367ceb423c211a4fc8e7ce586acfaf0929c9c06d98002112075239 \ - --hash=sha256:0e6f3b45def6ff23e254eeaa9079267004f0069d0a34eba30a620780caa4f2cb \ - --hash=sha256:137010598d8cebc53dbfb7caf59bde96c33a6af555e3e1bdbf30269b6a157e1e \ - --hash=sha256:2f7c10f69cbc3e6c7d715ec9cb0c270182ea2496063bebeda873f4aa83fd9910 \ - --hash=sha256:3715d779561699471edde70975e07b1de7dddb2816735d40ed16be4b32054188 \ - --hash=sha256:3834304fdeaa1c9b73596ad5a6c01a44fc19c13c115194704b85f7fbdf0a7b8e \ - --hash=sha256:389721481c14a92fa042e4b91ae065bff13e2bc567c85a10aa9d9de80aaa8622 \ - --hash=sha256:3a96ef3e461ecc85ec7d2233ddc449ff5a3fbdc520caea752bc5bc8faa975231 \ - --hash=sha256:3e2a0e1be95237981bd375a388f44b33d69ea5669a2f79fea038e45fff326595 \ - --hash=sha256:3e431f9c80ee1850b2a406770acf52c058b97a27968a0ed6aca45c2614d64c9f \ - --hash=sha256:47631614c5243ed7d15ae0af8245fcc0599f5b7921fae2a4ae992afb27c9afbb \ - --hash=sha256:52d1764906befef91886e3bff374d8090c9716822bd56b70e07aa697188090b7 \ - --hash=sha256:5370f9ef6c008e502537cc1ff518c80ddf749367ce90179efa0e7f6275903a76 \ - --hash=sha256:56043cf908ddf3d7364498085dbc2855d4ea8969aff3bf2439a79482a79e68e2 \ - --hash=sha256:5a6abc9573a6a45d09548fde136dbcd4260b8c56f8dff443eaa565352d7cca59 \ - --hash=sha256:5b7c1e7fa6c3b855e8cfbabf38454d7decbaba1c567d0cd58880d033c6b363bd \ - --hash=sha256:5ef045f694ef66079b4e00c4c9063a00183d6af7d1ff643de6ea5c3b0d9af01b \ - --hash=sha256:68678816818efcd4a1787b557af81f215b989ec88680a86c85c34c914d413690 \ - --hash=sha256:6cac19952e0e258ded765737d1fb11704fe81bf4f27526638a5d44496f329235 \ - --hash=sha256:70b4c96f9119cfeb4dc6a0cf4afc9f92f0b002cde225bcd910915d976c78e66a \ - --hash=sha256:7e957aa4251a8e70b9fe02a16b2d190f18787902da563cb7ba865508b8e8fb04 \ - --hash=sha256:82de2de90488d0fbbf74cf9f20e1afd62e320693b88f5e9565fc80b28f5bbad3 \ - --hash=sha256:83a3748088d117a9b15d8981c947df9e4f56eb2e4b5456ae34fe1f83666c9185 \ - --hash=sha256:83efc045fc58ca04c91a96c9b894d8a19ac6553677a76f96df01ff9f0405f53d \ - --hash=sha256:8c8b2386d2d22c57880ed20a913ceca86363765623175671137484a7d223f07a \ - --hash=sha256:8f81344d212cb41992340b0b8a67e375f44da90590b884204fd3fa5e02107df2 \ - --hash=sha256:954fef9185f8a79441b4e433695116636bf66402945cfee404f8983bafa59788 \ - --hash=sha256:9651daa1fdc471df5a5fa6a4833d3b01e76ac512eea141a5995681aebac5555f \ - --hash=sha256:9688c7b45f744366a4ef661e399f24636ebe440d315ab35d768676c59c613186 \ - --hash=sha256:97107fd12f30e4f8fea97790343a2d2d9a79d93697fe14e1b6f6363c984ff85b \ - --hash=sha256:9868b7a8e0f648d09ffe25ac29511e6e208cc5fb0d156c295385f9d5dc2a138e \ - --hash=sha256:986eaf35a7f63c878280609ecd37edf8a074f7601c199acfec81d03f1ee9a39a \ - --hash=sha256:99a00cab4cf9643a87977c87a5c8961aa44fff8d5dd46e00250135f686e7dedf \ - --hash=sha256:9c56001badaf1779afae5c24b7ab85938644ab8ef3c5fd438ab5d49621b84482 \ - --hash=sha256:9dc61fb8f8993589544f6df268229c6cf0a56ad4ed3e8585a9cd23c5ad79527b \ - --hash=sha256:9de573d933db4753a50af891bcb3ffbfe14e200406214c223aa5dfe2163f316d \ - --hash=sha256:9e467e13971c64db6aed8afe4c2a131c3f73f048bec3f788a6141216acda598d \ - --hash=sha256:9e6496bbad3068e3bbbb934b1e1307bf1a9cb4609f9ec47b57e8ea37f1b5ee40 \ - --hash=sha256:9f92d3577c72d5a97af5c8e3d98247b79c8ccfb64ebf611311dcf631b11e5604 \ - --hash=sha256:a1c6990961d1177f6d8fdf7b610fa2e7c0c02743a090d173f6dfa9dc9231c73c \ - --hash=sha256:a5a0a58ffe2a7eb3f870214c6df8f9a43ce768bd8fed883e6ba8c77645666b63 \ - --hash=sha256:a7416f1a084eb889c5792c57317875aeaa86abfe0bdc6f167712cebcec1d36ee \ - --hash=sha256:a83f5f7ae3494e0cc25211296252b1b86901c788ed82c83adda19d0c98f828d6 \ - --hash=sha256:a850b151bd1e3a5d9afef113adc22727d696603659d575d7e84f994bd8d04bf1 \ - --hash=sha256:ad82ab8a58562cf69e6b786debcc7638b28df12f9f1c7bcffb07efb5c1f09cbd \ - --hash=sha256:b173ec46d521308f7c97d96d6e05cf2088e0548f82544ec9a8656af65593304d \ - --hash=sha256:bf9f2b97fcfd5e2dbb0090d0664023872dcde990df0b545eca8d0ce95795a409 \ - --hash=sha256:c12b44c190deb0d67655021da1f2d0a7d61a257bf844101cf982e68ed344f28d \ - --hash=sha256:c6571462417cda2239b1ade86ceaf3852da9b52c6286046e87d404afc6da20a7 \ - --hash=sha256:c785fd6dae9daa6825734b7b494cdac972f958be1f9cb3fb1f32be8598d2b936 \ - --hash=sha256:c7a33506d0451112911c69f38d55da3e0e050f2be0ea4e5176865cf03baf26a9 \ - --hash=sha256:c89df75aefe1ad7e613340790130f1badc5926bcfa66a6b3c9471071002956a5 \ - --hash=sha256:ca644534f15f85bba14c412afc17de07531e79a766ce85b8dbf3f8b6e7758f20 \ - --hash=sha256:cbd28f95d5f30d9a7af6130869568e75bfd7ef2e0adfb1480f1f44480f5d3603 \ - --hash=sha256:d0f87bdf660f01e88ab3a507955697b2e3284065afa0b94fc9e77d6ad153ed5e \ - --hash=sha256:d4bd41a6e73c0d0adafe4de449b6d35530a4ce6a836a6ee839baf117785ecfd7 \ - --hash=sha256:d8d5e686db0ae758837ed29b3b742afb994d1a01ce10977eabd3490f16b5c9f9 \ - --hash=sha256:e5888b269e790356ce4525f3e8df1fe866d1497b7d7fb7548cfec883cb985288 \ - --hash=sha256:ec633e108f277f2b7f4671d933a909f39bba549910bf103e2940b87a14da2783 \ - --hash=sha256:ecdb19d33b26738a32602ef432b06cc6deeca4b498ce67ba8e5e39c8a7c19745 \ - --hash=sha256:ee428575377e29c636f2b4b3b0488875dcea310c6c5b3412ec4ef997f7bb37cc \ - --hash=sha256:f4bae4f920f2a1082eaf766c1883df7da84abdf333bafa15b8717c10416a615e - # via language-data markdown-it-py==4.0.0 \ --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 diff --git a/py/selenium/webdriver/common/bidi/emulation.py b/py/selenium/webdriver/common/bidi/emulation.py index 77c0c24c00d7e..678940ac36002 100644 --- a/py/selenium/webdriver/common/bidi/emulation.py +++ b/py/selenium/webdriver/common/bidi/emulation.py @@ -17,8 +17,6 @@ from typing import Any, Optional, Union -from langcodes import standardize_tag, tag_is_valid - from selenium.webdriver.common.bidi.common import command_builder @@ -165,20 +163,6 @@ def to_dict(self) -> dict[str, str]: return {"type": self.type} -def _is_valid_language_tag(locale: str) -> str | None: - """Validate and normalize a BCP 47 language tag.""" - - if locale is None: - return None - - if not tag_is_valid(locale): - raise ValueError(f"Invalid locale: {locale}") - - # Canonicalization / normalization - normalized = standardize_tag(locale) - return normalized - - class Emulation: """ BiDi implementation of the emulation module. @@ -257,9 +241,6 @@ def set_locale_override( if contexts is None and user_contexts is None: raise ValueError("Must specify either contexts or userContexts") - if locale is not None and not _is_valid_language_tag(locale): - raise ValueError(f"Invalid language tag: {locale}") - params: dict[str, Any] = {"locale": locale} if contexts is not None: From a72c4c85add1076f0fb145d403965ea675a90cf2 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 24 Oct 2025 19:24:51 +0530 Subject: [PATCH 7/8] revert lock file --- py/requirements_lock.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/py/requirements_lock.txt b/py/requirements_lock.txt index 1d3ebfd73c84c..3fb0d9d063105 100644 --- a/py/requirements_lock.txt +++ b/py/requirements_lock.txt @@ -388,6 +388,7 @@ jeepney==0.9.0 \ --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 # via # -r py/requirements.txt + # keyring # secretstorage keyring==25.6.0 \ --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ @@ -677,7 +678,9 @@ rich==14.1.0 \ secretstorage==3.4.0 \ --hash=sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e \ --hash=sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c - # via -r py/requirements.txt + # via + # -r py/requirements.txt + # keyring sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc From f3cacebfcf031c43639d9002a61ed574ffcb6946 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 24 Oct 2025 20:06:03 +0530 Subject: [PATCH 8/8] better cleanup --- .../webdriver/common/bidi_emulation_tests.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_emulation_tests.py b/py/test/selenium/webdriver/common/bidi_emulation_tests.py index f12bb0eafa991..01705b0ae3c6f 100644 --- a/py/test/selenium/webdriver/common/bidi_emulation_tests.py +++ b/py/test/selenium/webdriver/common/bidi_emulation_tests.py @@ -347,17 +347,18 @@ def test_set_locale_override_with_contexts(driver, pages, locale, expected_local def test_set_locale_override_with_user_contexts(driver, pages, value): """Test setting locale override with user contexts.""" user_context = driver.browser.create_user_context() + try: + context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context) + try: + driver.switch_to.window(context_id) - context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context) - - driver.switch_to.window(context_id) + driver.emulation.set_locale_override(locale=value, user_contexts=[user_context]) - driver.emulation.set_locale_override(locale=value, user_contexts=[user_context]) - - driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete") + driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete") - current_locale = get_browser_locale(driver) - assert current_locale == value, f"Expected locale {value}, got {current_locale}" - - driver.browsing_context.close(context_id) - driver.browser.remove_user_context(user_context) + current_locale = get_browser_locale(driver) + assert current_locale == value, f"Expected locale {value}, got {current_locale}" + finally: + driver.browsing_context.close(context_id) + finally: + driver.browser.remove_user_context(user_context)