[py] Raise InvalidSelectorException for compound class names (#16291)

NOKEYCHECK=True
GitOrigin-RevId: 0ff768a2229f23fcb9fec84978a146bf2acb07b9
diff --git a/selenium/webdriver/remote/locator_converter.py b/selenium/webdriver/remote/locator_converter.py
index e924d7f..ceb05c4 100644
--- a/selenium/webdriver/remote/locator_converter.py
+++ b/selenium/webdriver/remote/locator_converter.py
@@ -14,6 +14,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+from selenium.common.exceptions import InvalidSelectorException
 from selenium.webdriver.common.by import By
 
 
@@ -23,6 +25,8 @@
         if by == By.ID:
             return By.CSS_SELECTOR, f'[id="{value}"]'
         elif by == By.CLASS_NAME:
+            if value and any(char.isspace() for char in value.strip()):
+                raise InvalidSelectorException("Compound class names are not allowed.")
             return By.CSS_SELECTOR, f".{value}"
         elif by == By.NAME:
             return By.CSS_SELECTOR, f'[name="{value}"]'
diff --git a/selenium/webdriver/remote/shadowroot.py b/selenium/webdriver/remote/shadowroot.py
index 9603770..f23f5c4 100644
--- a/selenium/webdriver/remote/shadowroot.py
+++ b/selenium/webdriver/remote/shadowroot.py
@@ -17,6 +17,7 @@
 
 from hashlib import md5 as md5_hash
 
+from selenium.common.exceptions import InvalidSelectorException
 from selenium.webdriver.common.by import By
 from selenium.webdriver.remote.command import Command
 
@@ -74,6 +75,8 @@
             by = By.CSS_SELECTOR
             value = f'[id="{value}"]'
         elif by == By.CLASS_NAME:
+            if value and any(char.isspace() for char in value.strip()):
+                raise InvalidSelectorException("Compound class names are not allowed.")
             by = By.CSS_SELECTOR
             value = f".{value}"
         elif by == By.NAME:
@@ -112,6 +115,8 @@
             by = By.CSS_SELECTOR
             value = f'[id="{value}"]'
         elif by == By.CLASS_NAME:
+            if value and any(char.isspace() for char in value.strip()):
+                raise InvalidSelectorException("Compound class names are not allowed.")
             by = By.CSS_SELECTOR
             value = f".{value}"
         elif by == By.NAME:
diff --git a/selenium/webdriver/remote/webdriver.py b/selenium/webdriver/remote/webdriver.py
index 84cc7c5..cb8d08b 100644
--- a/selenium/webdriver/remote/webdriver.py
+++ b/selenium/webdriver/remote/webdriver.py
@@ -14,6 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
 """The WebDriver implementation."""
 
 import base64
diff --git a/test/selenium/webdriver/common/driver_element_finding_tests.py b/test/selenium/webdriver/common/driver_element_finding_tests.py
index cfdca06..42df9ff 100644
--- a/test/selenium/webdriver/common/driver_element_finding_tests.py
+++ b/test/selenium/webdriver/common/driver_element_finding_tests.py
@@ -20,7 +20,7 @@
 from selenium.common.exceptions import InvalidSelectorException, NoSuchElementException
 from selenium.webdriver.common.by import By
 
-# By.id positive
+# By.ID positive
 
 
 def test_should_be_able_to_find_asingle_element_by_id(driver, pages):
@@ -53,7 +53,7 @@
     assert len(elements) == 8
 
 
-# By.id negative
+# By.ID negative
 
 
 def test_should_not_be_able_to_locate_by_id_asingle_element_that_does_not_exist(driver, pages):
@@ -99,7 +99,7 @@
         driver.find_element(By.ID, "non_Existent_Button")
 
 
-# By.name positive
+# By.NAME positive
 
 
 def test_should_be_able_to_find_asingle_element_by_name(driver, pages):
@@ -120,7 +120,7 @@
     assert element.get_attribute("name") == "div1"
 
 
-# By.name negative
+# By.NAME negative
 
 
 def test_should_not_be_able_to_locate_by_name_asingle_element_that_does_not_exist(driver, pages):
@@ -159,7 +159,7 @@
     assert len(elements) == 0
 
 
-# By.tag_Name positive
+# By.TAG_NAME positive
 
 
 def test_should_be_able_to_find_asingle_element_by_tag_name(driver, pages):
@@ -174,7 +174,7 @@
     assert len(elements) > 1
 
 
-# By.tag_Name negative
+# By.TAG_NAME negative
 
 
 def test_should_not_be_able_to_locate_by_tag_name_asingle_element_that_does_not_exist(driver, pages):
@@ -189,22 +189,18 @@
     assert len(elements) == 0
 
 
-@pytest.mark.xfail_firefox(reason="https://github.com/mozilla/geckodriver/issues/2007")
-@pytest.mark.xfail_remote(reason="https://github.com/mozilla/geckodriver/issues/2007")
+@pytest.mark.xfail_firefox(reason="unlike chrome, firefox raises NoSuchElementException")
+@pytest.mark.xfail_remote(reason="unlike chrome, firefox raises NoSuchElementException")
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises NoSuchElementException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_asingle_element_by_empty_tag_name_should_throw(driver, pages):
     pages.load("formPage.html")
     with pytest.raises(InvalidSelectorException):
         driver.find_element(By.TAG_NAME, "")
 
 
-@pytest.mark.xfail_firefox(reason="https://github.com/mozilla/geckodriver/issues/2007")
-@pytest.mark.xfail_remote(reason="https://github.com/mozilla/geckodriver/issues/2007")
+@pytest.mark.xfail_firefox(reason="unlike chrome, firefox returns an empty list")
+@pytest.mark.xfail_remote(reason="unlike chrome, firefox returns an empty list")
 @pytest.mark.xfail_safari(reason="unlike chrome, safari returns an empty list")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_multiple_elements_by_empty_tag_name_should_throw(driver, pages):
     pages.load("formPage.html")
     with pytest.raises(InvalidSelectorException):
@@ -223,7 +219,7 @@
     assert len(elements) == 0
 
 
-# By.class_Name positive
+# By.CLASS_NAME positive
 
 
 def test_should_be_able_to_find_asingle_element_by_class(driver, pages):
@@ -269,7 +265,7 @@
     assert elements[0].text == "Spaced out"
 
 
-# By.class_Name negative
+# By.CLASS_NAME negative
 
 
 def test_should_not_find_element_by_class_when_the_name_queried_is_shorter_than_candidate_name(driver, pages):
@@ -279,8 +275,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_asingle_element_by_empty_class_name_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     msg = r"\/errors#invalidselectorexception"
@@ -289,8 +283,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_multiple_elements_by_empty_class_name_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
@@ -299,13 +291,11 @@
 
 def test_finding_asingle_element_by_compound_class_name_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
-    with pytest.raises(NoSuchElementException):
+    with pytest.raises(InvalidSelectorException):
         driver.find_element(By.CLASS_NAME, "a b")
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_asingle_element_by_invalid_class_name_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
@@ -313,15 +303,13 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_multiple_elements_by_invalid_class_name_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
         driver.find_elements(By.CLASS_NAME, "!@#$%^&*")
 
 
-# By.xpath positive
+# By.XPATH positive
 
 
 def test_should_be_able_to_find_asingle_element_by_xpath(driver, pages):
@@ -388,7 +376,7 @@
     assert "baz" in element.text
 
 
-# By.xpath negative
+# By.XPATH negative
 
 
 def test_should_throw_an_exception_when_there_is_no_link_to_click(driver, pages):
@@ -398,8 +386,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_is_syntactically_invalid_in_driver_find_element(
     driver, pages
 ):
@@ -409,8 +395,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_is_syntactically_invalid_in_driver_find_elements(
     driver, pages
 ):
@@ -420,8 +404,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_is_syntactically_invalid_in_element_find_element(
     driver, pages
 ):
@@ -432,8 +414,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_is_syntactically_invalid_in_element_find_elements(
     driver, pages
 ):
@@ -444,8 +424,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_returns_wrong_type_in_driver_find_element(driver, pages):
     pages.load("formPage.html")
     with pytest.raises(InvalidSelectorException):
@@ -453,8 +431,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_returns_wrong_type_in_driver_find_elements(driver, pages):
     pages.load("formPage.html")
     with pytest.raises(InvalidSelectorException):
@@ -462,8 +438,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_returns_wrong_type_in_element_find_element(driver, pages):
     pages.load("formPage.html")
     body = driver.find_element(By.TAG_NAME, "body")
@@ -472,8 +446,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_should_throw_invalid_selector_exception_when_xpath_returns_wrong_type_in_element_find_elements(driver, pages):
     pages.load("formPage.html")
     body = driver.find_element(By.TAG_NAME, "body")
@@ -481,7 +453,7 @@
         body.find_elements(By.XPATH, "count(//input)")
 
 
-# By.css_Selector positive
+# By.CSS_SELECTOR positive
 
 
 def test_should_be_able_to_find_asingle_element_by_css_selector(driver, pages):
@@ -530,7 +502,7 @@
     assert element.get_attribute("value") == "two"
 
 
-# By.css_Selector negative
+# By.CSS_SELECTOR negative
 
 
 def test_should_not_find_element_by_css_selector_when_there_is_no_such_element(driver, pages):
@@ -546,8 +518,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_asingle_element_by_empty_css_selector_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
@@ -555,8 +525,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_multiple_elements_by_empty_css_selector_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
@@ -564,8 +532,6 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_asingle_element_by_invalid_css_selector_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
@@ -573,15 +539,13 @@
 
 
 @pytest.mark.xfail_safari(reason="unlike chrome, safari raises TimeoutException")
-@pytest.mark.xfail_chrome(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
-@pytest.mark.xfail_edge(reason="https://bugs.chromium.org/p/chromedriver/issues/detail?id=4743")
 def test_finding_multiple_elements_by_invalid_css_selector_should_throw(driver, pages):
     pages.load("xhtmlTest.html")
     with pytest.raises(InvalidSelectorException):
         driver.find_elements(By.CSS_SELECTOR, "//a/b/c[@id='1']")
 
 
-# By.link_Text positive
+# By.LINK_TEXT positive
 
 
 def test_should_be_able_to_find_alink_by_text(driver, pages):
@@ -632,7 +596,7 @@
     assert link.text == "link with trailing space"
 
 
-# By.link_Text negative
+# By.LINK_TEXT negative
 
 
 def test_should_not_be_able_to_locate_by_link_text_asingle_element_that_does_not_exist(driver, pages):
@@ -647,7 +611,7 @@
     assert len(elements) == 0
 
 
-# By.partial_Link_Text positive
+# By.PARTIAL_LINK_TEXT positive
 
 
 def test_should_be_able_to_find_multiple_elements_by_partial_link_text(driver, pages):