From 6da9cd0357779d76b5e640380193e5a8d7bd1393 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 1 Nov 2024 23:37:49 +0530 Subject: [PATCH 01/15] FedCM python implementation --- py/selenium/webdriver/chrome/webdriver.py | 3 +- .../driver_extensions/has_fedcm_dialog.py | 62 ++++++++ py/selenium/webdriver/common/fedcm/account.py | 71 +++++++++ py/selenium/webdriver/common/fedcm/dialog.py | 67 ++++++++ py/selenium/webdriver/remote/command.py | 10 ++ .../webdriver/remote/remote_connection.py | 9 ++ py/selenium/webdriver/remote/webdriver.py | 49 ++++++ .../selenium/webdriver/common/fedcm_tests.py | 146 ++++++++++++++++++ .../webdriver/common/fedcm/account_tests.py | 44 ++++++ .../webdriver/common/fedcm/dialog_tests.py | 74 +++++++++ 10 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py create mode 100644 py/selenium/webdriver/common/fedcm/account.py create mode 100644 py/selenium/webdriver/common/fedcm/dialog.py create mode 100644 py/test/selenium/webdriver/common/fedcm_tests.py create mode 100644 py/test/unit/selenium/webdriver/common/fedcm/account_tests.py create mode 100644 py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py diff --git a/py/selenium/webdriver/chrome/webdriver.py b/py/selenium/webdriver/chrome/webdriver.py index 5fb9583b6817c..53c2ba9a7c545 100644 --- a/py/selenium/webdriver/chrome/webdriver.py +++ b/py/selenium/webdriver/chrome/webdriver.py @@ -17,12 +17,13 @@ from selenium.webdriver.chromium.webdriver import ChromiumDriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.driver_extensions.has_fedcm_dialog import HasFedCmDialog from .options import Options from .service import Service -class WebDriver(ChromiumDriver): +class WebDriver(ChromiumDriver, HasFedCmDialog): """Controls the ChromeDriver and allows you to drive the browser.""" def __init__( diff --git a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py new file mode 100644 index 0000000000000..c31282e404474 --- /dev/null +++ b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py @@ -0,0 +1,62 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from selenium.webdriver.common.fedcm.dialog import Dialog +from selenium.webdriver.remote.webdriver import WebDriver + + +class HasFedCmDialog(WebDriver): + """Mixin that provides FedCM-specific functionality.""" + + @property + def fedcm_dialog(self): + """Returns the FedCM dialog object for interaction.""" + return Dialog(self) + + def enable_fedcm_delay(self, enable): + """Disables the promise rejection delay for FedCm. + + FedCm by default delays promise resolution in failure cases for + privacy reasons. This method allows turning it off to let tests + run faster where this is not relevant. + """ + self.execute("setFedCmDelay", {"enabled": enable}) + + def fedcm_cooldown(self): + """Resets the FedCm dialog cooldown. + + If a user agent triggers a cooldown when the account chooser is + dismissed, this method resets that cooldown so that the dialog + can be triggered again immediately. + """ + self.reset_fedcm_cooldown() + + def wait_for_fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None): + """Waits for the FedCM dialog to appear.""" + from selenium.common.exceptions import NoAlertPresentException + from selenium.webdriver.support.wait import WebDriverWait + + if ignored_exceptions is None: + ignored_exceptions = (NoAlertPresentException,) + + def check_fedcm(): + try: + return self.fedcm_dialog if self.fedcm_dialog.type else None + except NoAlertPresentException: + return None + + wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions) + return wait.until(lambda _: check_fedcm()) diff --git a/py/selenium/webdriver/common/fedcm/account.py b/py/selenium/webdriver/common/fedcm/account.py new file mode 100644 index 0000000000000..6b8c20b12c781 --- /dev/null +++ b/py/selenium/webdriver/common/fedcm/account.py @@ -0,0 +1,71 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from enum import Enum +from typing import Optional + + +class LoginState(Enum): + SIGN_IN = "SignIn" + SIGN_UP = "SignUp" + + +class Account: + """Represents an account displayed in a FedCM account list. + + See: https://w3c-fedid.github.io/FedCM/#dictdef-identityprovideraccount + https://w3c-fedid.github.io/FedCM/#webdriver-accountlist + """ + + def __init__(self, account_data): + self._account_data = account_data + + @property + def account_id(self) -> Optional[str]: + return self._account_data.get("accountId") + + @property + def email(self) -> Optional[str]: + return self._account_data.get("email") + + @property + def name(self) -> Optional[str]: + return self._account_data.get("name") + + @property + def given_name(self) -> Optional[str]: + return self._account_data.get("givenName") + + @property + def picture_url(self) -> Optional[str]: + return self._account_data.get("pictureUrl") + + @property + def idp_config_url(self) -> Optional[str]: + return self._account_data.get("idpConfigUrl") + + @property + def terms_of_service_url(self) -> Optional[str]: + return self._account_data.get("termsOfServiceUrl") + + @property + def privacy_policy_url(self) -> Optional[str]: + return self._account_data.get("privacyPolicyUrl") + + @property + def login_state(self) -> Optional[str]: + return self._account_data.get("loginState") diff --git a/py/selenium/webdriver/common/fedcm/dialog.py b/py/selenium/webdriver/common/fedcm/dialog.py new file mode 100644 index 0000000000000..d7f4c4d0aabbe --- /dev/null +++ b/py/selenium/webdriver/common/fedcm/dialog.py @@ -0,0 +1,67 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import List +from typing import Optional + +from .account import Account + + +class Dialog: + """Represents a FedCM dialog that can be interacted with.""" + + DIALOG_TYPE_ACCOUNT_LIST = "AccountChooser" + DIALOG_TYPE_AUTO_REAUTH = "AutoReauthn" + + def __init__(self, driver) -> None: + self._driver = driver + + @property + def type(self) -> Optional[str]: + """Gets the type of the dialog currently being shown.""" + result = self._driver.execute("getFedCmDialogType") + return result["value"] if result else None + + @property + def title(self) -> str: + """Gets the title of the dialog.""" + # return self._driver.execute("getFedCmTitle").get("title") + return self._driver.get_fedcm_title() + + @property + def subtitle(self) -> Optional[str]: + """Gets the subtitle of the dialog.""" + result = self._driver.execute("getFedCmTitle") + return result.get("subtitle") if result else None + + @property + def get_accounts(self) -> List[Account]: + """Gets the list of accounts shown in the dialog.""" + accounts = self._driver.execute("getFedCmAccountList") + return [Account(account) for account in accounts] + + def select_account(self, index: int) -> None: + """Selects an account from the dialog by index.""" + self._driver.execute("selectFedCmAccount", {"accountIndex": index}) + + def click_continue(self) -> None: + """Clicks the continue button in the dialog.""" + self._driver.execute("clickFedCmDialogButton", {"dialogButton": "ConfirmIdpLoginContinue"}) + + def cancel(self) -> None: + """Cancels/dismisses the dialog.""" + self._driver.execute("cancelFedCmDialog") diff --git a/py/selenium/webdriver/remote/command.py b/py/selenium/webdriver/remote/command.py index 0c104c2a46ab2..ccc8259a9b887 100644 --- a/py/selenium/webdriver/remote/command.py +++ b/py/selenium/webdriver/remote/command.py @@ -122,3 +122,13 @@ class Command: GET_DOWNLOADABLE_FILES: str = "getDownloadableFiles" DOWNLOAD_FILE: str = "downloadFile" DELETE_DOWNLOADABLE_FILES: str = "deleteDownloadableFiles" + + # Federated Credential Management (FedCM) + GET_FEDCM_TITLE: str = "getFedcmTitle" + GET_FEDCM_DIALOG_TYPE: str = "getFedcmDialogType" + GET_FEDCM_ACCOUNT_LIST: str = "getFedcmAccountList" + CLICK_FEDCM_DIALOG_BUTTON: str = "clickFedcmDialogButton" + CANCEL_FEDCM_DIALOG: str = "cancelFedcmDialog" + SELECT_FEDCM_ACCOUNT: str = "selectFedcmAccount" + SET_FEDCM_DELAY: str = "setFedcmDelay" + RESET_FEDCM_COOLDOWN: str = "resetFedcmCooldown" diff --git a/py/selenium/webdriver/remote/remote_connection.py b/py/selenium/webdriver/remote/remote_connection.py index 04786c39c2673..4de4c95425d1a 100644 --- a/py/selenium/webdriver/remote/remote_connection.py +++ b/py/selenium/webdriver/remote/remote_connection.py @@ -125,6 +125,15 @@ Command.GET_DOWNLOADABLE_FILES: ("GET", "/session/$sessionId/se/files"), Command.DOWNLOAD_FILE: ("POST", "/session/$sessionId/se/files"), Command.DELETE_DOWNLOADABLE_FILES: ("DELETE", "/session/$sessionId/se/files"), + # Federated Credential Management (FedCM) + Command.GET_FEDCM_TITLE: ("GET", "/session/$sessionId/fedcm/gettitle"), + Command.GET_FEDCM_DIALOG_TYPE: ("GET", "/session/$sessionId/fedcm/getdialogtype"), + Command.GET_FEDCM_ACCOUNT_LIST: ("GET", "/session/$sessionId/fedcm/accountlist"), + Command.CLICK_FEDCM_DIALOG_BUTTON: ("POST", "/session/$sessionId/fedcm/clickdialogbutton"), + Command.CANCEL_FEDCM_DIALOG: ("POST", "/session/$sessionId/fedcm/canceldialog"), + Command.SELECT_FEDCM_ACCOUNT: ("POST", "/session/$sessionId/fedcm/selectaccount"), + Command.SET_FEDCM_DELAY: ("POST", "/session/$sessionId/fedcm/setdelayenabled"), + Command.RESET_FEDCM_COOLDOWN: ("POST", "/session/$sessionId/fedcm/resetcooldown"), } diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index bae7f4e8d28c1..d33348c3939b3 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1222,3 +1222,52 @@ def delete_downloadable_files(self) -> None: raise WebDriverException("You must enable downloads in order to work with downloadable files.") self.execute(Command.DELETE_DOWNLOADABLE_FILES) + + # Federated Credential Management (FedCM) + def cancel_fedcm_dialog(self) -> None: + """Cancels/dismisses the FedCM dialog.""" + self.execute(Command.CANCEL_FEDCM_DIALOG) + + def select_fedcm_account(self, index: int) -> None: + """Selects an account from the dialog by index.""" + self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) + + def get_fedcm_dialog_type(self) -> str: + """Gets the type of the dialog currently being shown.""" + return self.execute(Command.GET_FEDCM_DIALOG_TYPE)["value"] + + def get_fedcm_title(self) -> str: + """Gets the title of the dialog.""" + return self.execute(Command.GET_FEDCM_TITLE).get("title") + + def get_fedcm_subtitle(self) -> Optional[str]: + """Gets the subtitle of the dialog.""" + return self.execute(Command.GET_FEDCM_TITLE).get("subtitle") + + def get_fedcm_account_list(self) -> list: + """Gets the list of accounts shown in the dialog.""" + return self.execute(Command.GET_FEDCM_ACCOUNT_LIST)["value"] + + def set_fedcm_delay(self, enabled: bool) -> None: + """Disables the promise rejection delay for FedCM. + + FedCM by default delays promise resolution in failure cases for privacy reasons. + This method allows turning it off to let tests run faster where this is not relevant. + + Args: + enabled: True to enable the delay, False to disable it + """ + self.execute(Command.SET_FEDCM_DELAY, {"enabled": enabled}) + + def reset_fedcm_cooldown(self) -> None: + """Resets the FedCM dialog cooldown. + + If a user agent triggers a cooldown when the account chooser is + dismissed, this method resets that cooldown so that the dialog + can be triggered again immediately. + """ + self.execute(Command.RESET_FEDCM_COOLDOWN) + + def click_fedcm_dialog_button(self) -> None: + """Clicks the continue button in the dialog.""" + self.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) diff --git a/py/test/selenium/webdriver/common/fedcm_tests.py b/py/test/selenium/webdriver/common/fedcm_tests.py new file mode 100644 index 0000000000000..f7722ed778dce --- /dev/null +++ b/py/test/selenium/webdriver/common/fedcm_tests.py @@ -0,0 +1,146 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os + +import pytest + +from selenium.common.exceptions import NoAlertPresentException + + +@pytest.mark.xfail_safari(reason="FedCM not supported") +@pytest.mark.xfail_firefox(reason="FedCM not supported") +@pytest.mark.xfail_ie(reason="FedCM not supported") +@pytest.mark.xfail_edge(reason="FedCM not supported") +class TestFedCM: + @pytest.fixture(autouse=True) + def setup(self, driver): + html_file_path = os.path.abspath( + "/Users/navinchandra/Documents/Projects/selenium/common/src/web/fedcm/fedcm.html" + ) + print("File path is: ", html_file_path) + + driver.get(f"file://{html_file_path}") + # driver.get('fedcm/fedcm.html') + self.dialog = driver.fedcm_dialog + + def test_no_dialog_present(self, driver): + with pytest.raises(NoAlertPresentException): + self.dialog.title + with pytest.raises(NoAlertPresentException): + self.dialog.subtitle + with pytest.raises(NoAlertPresentException): + self.dialog.type + with pytest.raises(NoAlertPresentException): + self.dialog.get_accounts() + with pytest.raises(NoAlertPresentException): + self.dialog.select_account(1) + with pytest.raises(NoAlertPresentException): + self.dialog.cancel() + + def test_dialog_present(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.wait_for_fedcm_dialog() + + assert dialog.title == "Sign in to localhost with localhost" + assert dialog.subtitle is None + assert dialog.type == "AccountChooser" + + accounts = dialog.get_accounts() + assert accounts[0].name == "John Doe" + + dialog.select_account(1) + dialog.cancel() + + with pytest.raises(NoAlertPresentException): + dialog.title + + def test_fedcm_delay(self, driver): + driver.set_fedcm_delay(True) + + def test_reset_cooldown(self, driver): + driver.fedcm_cooldown() + + def test_fedcm_commands(self, driver): + # Test get_fedcm_dialog_type + with pytest.raises(NoAlertPresentException): + driver.get_fedcm_dialog_type() + + # Test get_fedcm_title + with pytest.raises(NoAlertPresentException): + driver.get_fedcm_title() + + # Test get_fedcm_subtitle + with pytest.raises(NoAlertPresentException): + driver.get_fedcm_subtitle() + + # Test get_fedcm_account_list + with pytest.raises(NoAlertPresentException): + driver.get_fedcm_account_list() + + # Test select_fedcm_account + with pytest.raises(NoAlertPresentException): + driver.select_fedcm_account(1) + + # Test cancel_fedcm_dialog + with pytest.raises(NoAlertPresentException): + driver.cancel_fedcm_dialog() + + # Test click_fedcm_dialog_button + with pytest.raises(NoAlertPresentException): + driver.click_fedcm_dialog_button() + + def test_fedcm_dialog_flow(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.wait_for_fedcm_dialog() + + # Test dialog properties + assert dialog.type == "AccountChooser" + assert dialog.title == "Sign in to localhost with localhost" + assert dialog.subtitle is None + + # Test account list + accounts = dialog.get_accounts() + assert len(accounts) > 0 + assert accounts[0].name == "John Doe" + + # Test account selection + dialog.select_account(1) + + # Test dialog button click + dialog.click_continue() + + # Test dialog cancellation + dialog.cancel() + with pytest.raises(NoAlertPresentException): + dialog.title + + def test_fedcm_delay_settings(self, driver): + # Test enabling delay + driver.set_fedcm_delay(True) + + # Test disabling delay + driver.set_fedcm_delay(False) + + def test_fedcm_cooldown_management(self, driver): + # Test cooldown reset + driver.fedcm_cooldown() + + # Verify dialog can be triggered after reset + driver.execute_script("triggerFedCm();") + dialog = driver.wait_for_fedcm_dialog() + assert dialog.type == "AccountChooser" diff --git a/py/test/unit/selenium/webdriver/common/fedcm/account_tests.py b/py/test/unit/selenium/webdriver/common/fedcm/account_tests.py new file mode 100644 index 0000000000000..419a44afad949 --- /dev/null +++ b/py/test/unit/selenium/webdriver/common/fedcm/account_tests.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.fedcm.account import Account + + +def test_account_properties(): + account_data = { + "accountId": "12341234", + "email": "test@email.com", + "name": "Real Name", + "givenName": "Test Name", + "pictureUrl": "picture-url", + "idpConfigUrl": "idp-config-url", + "loginState": "login-state", + "termsOfServiceUrl": "terms-of-service-url", + "privacyPolicyUrl": "privacy-policy-url", + } + + account = Account(account_data) + + assert account.account_id == "12341234" + assert account.email == "test@email.com" + assert account.name == "Real Name" + assert account.given_name == "Test Name" + assert account.picture_url == "picture-url" + assert account.idp_config_url == "idp-config-url" + assert account.login_state == "login-state" + assert account.terms_of_service_url == "terms-of-service-url" + assert account.privacy_policy_url == "privacy-policy-url" diff --git a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py new file mode 100644 index 0000000000000..7f80cf708e629 --- /dev/null +++ b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py @@ -0,0 +1,74 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from unittest.mock import Mock + +import pytest + +from selenium.webdriver.common.fedcm.dialog import Dialog + + +@pytest.fixture +def mock_driver(): + return Mock() + + +@pytest.fixture +def dialog(mock_driver): + return Dialog(mock_driver) + + +def test_click_continue(dialog, mock_driver): + dialog.click_continue() + mock_driver.execute.assert_called_with("clickFedCmDialogButton", {"dialogButton": "ConfirmIdpLoginContinue"}) + + +def test_cancel(dialog, mock_driver): + dialog.cancel() + mock_driver.execute.assert_called_with("cancelFedCmDialog") + + +def test_select_account(dialog, mock_driver): + dialog.select_account(1) + mock_driver.execute.assert_called_with("selectFedCmAccount", {"accountIndex": 1}) + + +def test_type(dialog, mock_driver): + mock_driver.execute.return_value = {"value": "AccountChooser"} + assert dialog.type == "AccountChooser" + + +def test_title(dialog, mock_driver): + mock_driver.execute.return_value = {"title": "Sign in"} + assert dialog.title == "Sign in" + + +def test_subtitle(dialog, mock_driver): + mock_driver.execute.return_value = {"subtitle": "Choose an account"} + assert dialog.subtitle == "Choose an account" + + +def test_get_accounts(dialog, mock_driver): + accounts_data = [ + {"name": "Account1", "email": "account1@example.com"}, + {"name": "Account2", "email": "account2@example.com"}, + ] + mock_driver.execute.return_value = accounts_data + accounts = dialog.get_accounts() + assert len(accounts) == 2 + assert accounts[0].name == "Account1" + assert accounts[0].email == "account1@example.com" From 729f2bd7280662d270521604e07b35e1001f6215 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 2 Nov 2024 00:33:15 +0530 Subject: [PATCH 02/15] set dialog.py methods to use webdriver methods --- py/selenium/webdriver/common/fedcm/dialog.py | 14 ++++++-------- py/selenium/webdriver/remote/webdriver.py | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/py/selenium/webdriver/common/fedcm/dialog.py b/py/selenium/webdriver/common/fedcm/dialog.py index d7f4c4d0aabbe..838a5e4c6653d 100644 --- a/py/selenium/webdriver/common/fedcm/dialog.py +++ b/py/selenium/webdriver/common/fedcm/dialog.py @@ -33,35 +33,33 @@ def __init__(self, driver) -> None: @property def type(self) -> Optional[str]: """Gets the type of the dialog currently being shown.""" - result = self._driver.execute("getFedCmDialogType") - return result["value"] if result else None + return self._driver.get_fedcm_dialog_type() @property def title(self) -> str: """Gets the title of the dialog.""" - # return self._driver.execute("getFedCmTitle").get("title") return self._driver.get_fedcm_title() @property def subtitle(self) -> Optional[str]: """Gets the subtitle of the dialog.""" - result = self._driver.execute("getFedCmTitle") + result = self._driver.get_fedcm_subtitle() return result.get("subtitle") if result else None @property def get_accounts(self) -> List[Account]: """Gets the list of accounts shown in the dialog.""" - accounts = self._driver.execute("getFedCmAccountList") + accounts = self._driver.get_fedcm_account_list() return [Account(account) for account in accounts] def select_account(self, index: int) -> None: """Selects an account from the dialog by index.""" - self._driver.execute("selectFedCmAccount", {"accountIndex": index}) + self._driver.select_fedcm_account(index) def click_continue(self) -> None: """Clicks the continue button in the dialog.""" - self._driver.execute("clickFedCmDialogButton", {"dialogButton": "ConfirmIdpLoginContinue"}) + self._driver.click_fedcm_dialog_button() def cancel(self) -> None: """Cancels/dismisses the dialog.""" - self._driver.execute("cancelFedCmDialog") + self._driver.cancel_fedcm_dialog() diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index d33348c3939b3..d806081951087 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1232,9 +1232,9 @@ def select_fedcm_account(self, index: int) -> None: """Selects an account from the dialog by index.""" self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) - def get_fedcm_dialog_type(self) -> str: + def get_fedcm_dialog_type(self): """Gets the type of the dialog currently being shown.""" - return self.execute(Command.GET_FEDCM_DIALOG_TYPE)["value"] + return self.execute(Command.GET_FEDCM_DIALOG_TYPE) def get_fedcm_title(self) -> str: """Gets the title of the dialog.""" @@ -1244,9 +1244,9 @@ def get_fedcm_subtitle(self) -> Optional[str]: """Gets the subtitle of the dialog.""" return self.execute(Command.GET_FEDCM_TITLE).get("subtitle") - def get_fedcm_account_list(self) -> list: + def get_fedcm_account_list(self): """Gets the list of accounts shown in the dialog.""" - return self.execute(Command.GET_FEDCM_ACCOUNT_LIST)["value"] + return self.execute(Command.GET_FEDCM_ACCOUNT_LIST) def set_fedcm_delay(self, enabled: bool) -> None: """Disables the promise rejection delay for FedCM. From eca9892bf2f93b0a612fb24ac49dcd370298a74f Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 2 Nov 2024 13:59:51 +0530 Subject: [PATCH 03/15] fix implementation and unit tests --- .../driver_extensions/has_fedcm_dialog.py | 2 +- py/selenium/webdriver/common/fedcm/dialog.py | 1 - py/selenium/webdriver/remote/command.py | 2 +- py/selenium/webdriver/remote/webdriver.py | 38 +++++++++---------- .../webdriver/common/fedcm/dialog_tests.py | 16 ++++---- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py index c31282e404474..745ad22be51bf 100644 --- a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py +++ b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py @@ -33,7 +33,7 @@ def enable_fedcm_delay(self, enable): privacy reasons. This method allows turning it off to let tests run faster where this is not relevant. """ - self.execute("setFedCmDelay", {"enabled": enable}) + self.set_fedcm_delay(enable) def fedcm_cooldown(self): """Resets the FedCm dialog cooldown. diff --git a/py/selenium/webdriver/common/fedcm/dialog.py b/py/selenium/webdriver/common/fedcm/dialog.py index 838a5e4c6653d..b94a57f3e0ed6 100644 --- a/py/selenium/webdriver/common/fedcm/dialog.py +++ b/py/selenium/webdriver/common/fedcm/dialog.py @@ -46,7 +46,6 @@ def subtitle(self) -> Optional[str]: result = self._driver.get_fedcm_subtitle() return result.get("subtitle") if result else None - @property def get_accounts(self) -> List[Account]: """Gets the list of accounts shown in the dialog.""" accounts = self._driver.get_fedcm_account_list() diff --git a/py/selenium/webdriver/remote/command.py b/py/selenium/webdriver/remote/command.py index ccc8259a9b887..b079ed5406f53 100644 --- a/py/selenium/webdriver/remote/command.py +++ b/py/selenium/webdriver/remote/command.py @@ -127,8 +127,8 @@ class Command: GET_FEDCM_TITLE: str = "getFedcmTitle" GET_FEDCM_DIALOG_TYPE: str = "getFedcmDialogType" GET_FEDCM_ACCOUNT_LIST: str = "getFedcmAccountList" + SELECT_FEDCM_ACCOUNT: str = "selectFedcmAccount" CLICK_FEDCM_DIALOG_BUTTON: str = "clickFedcmDialogButton" CANCEL_FEDCM_DIALOG: str = "cancelFedcmDialog" - SELECT_FEDCM_ACCOUNT: str = "selectFedcmAccount" SET_FEDCM_DELAY: str = "setFedcmDelay" RESET_FEDCM_COOLDOWN: str = "resetFedcmCooldown" diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index d806081951087..230e4fdb09d7a 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1224,29 +1224,33 @@ def delete_downloadable_files(self) -> None: self.execute(Command.DELETE_DOWNLOADABLE_FILES) # Federated Credential Management (FedCM) - def cancel_fedcm_dialog(self) -> None: - """Cancels/dismisses the FedCM dialog.""" - self.execute(Command.CANCEL_FEDCM_DIALOG) - - def select_fedcm_account(self, index: int) -> None: - """Selects an account from the dialog by index.""" - self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) - - def get_fedcm_dialog_type(self): - """Gets the type of the dialog currently being shown.""" - return self.execute(Command.GET_FEDCM_DIALOG_TYPE) - def get_fedcm_title(self) -> str: """Gets the title of the dialog.""" - return self.execute(Command.GET_FEDCM_TITLE).get("title") + return self.execute(Command.GET_FEDCM_TITLE)["value"].get("title") def get_fedcm_subtitle(self) -> Optional[str]: """Gets the subtitle of the dialog.""" - return self.execute(Command.GET_FEDCM_TITLE).get("subtitle") + return self.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle") + + def get_fedcm_dialog_type(self): + """Gets the type of the dialog currently being shown.""" + return self.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value") def get_fedcm_account_list(self): """Gets the list of accounts shown in the dialog.""" - return self.execute(Command.GET_FEDCM_ACCOUNT_LIST) + return self.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value") + + def select_fedcm_account(self, index: int) -> None: + """Selects an account from the dialog by index.""" + self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) + + def click_fedcm_dialog_button(self) -> None: + """Clicks the continue button in the dialog.""" + self.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) + + def cancel_fedcm_dialog(self) -> None: + """Cancels/dismisses the FedCM dialog.""" + self.execute(Command.CANCEL_FEDCM_DIALOG) def set_fedcm_delay(self, enabled: bool) -> None: """Disables the promise rejection delay for FedCM. @@ -1267,7 +1271,3 @@ def reset_fedcm_cooldown(self) -> None: can be triggered again immediately. """ self.execute(Command.RESET_FEDCM_COOLDOWN) - - def click_fedcm_dialog_button(self) -> None: - """Clicks the continue button in the dialog.""" - self.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) diff --git a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py index 7f80cf708e629..a35b689673690 100644 --- a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py +++ b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py @@ -34,31 +34,31 @@ def dialog(mock_driver): def test_click_continue(dialog, mock_driver): dialog.click_continue() - mock_driver.execute.assert_called_with("clickFedCmDialogButton", {"dialogButton": "ConfirmIdpLoginContinue"}) + mock_driver.click_fedcm_dialog_button.assert_called_once() def test_cancel(dialog, mock_driver): dialog.cancel() - mock_driver.execute.assert_called_with("cancelFedCmDialog") + mock_driver.cancel_fedcm_dialog.assert_called_once() def test_select_account(dialog, mock_driver): dialog.select_account(1) - mock_driver.execute.assert_called_with("selectFedCmAccount", {"accountIndex": 1}) + mock_driver.select_fedcm_account.assert_called_once_with(1) def test_type(dialog, mock_driver): - mock_driver.execute.return_value = {"value": "AccountChooser"} + mock_driver.get_fedcm_dialog_type.return_value = "AccountChooser" assert dialog.type == "AccountChooser" def test_title(dialog, mock_driver): - mock_driver.execute.return_value = {"title": "Sign in"} + mock_driver.get_fedcm_title.return_value = "Sign in" assert dialog.title == "Sign in" def test_subtitle(dialog, mock_driver): - mock_driver.execute.return_value = {"subtitle": "Choose an account"} + mock_driver.get_fedcm_subtitle.return_value = {"subtitle": "Choose an account"} assert dialog.subtitle == "Choose an account" @@ -67,8 +67,10 @@ def test_get_accounts(dialog, mock_driver): {"name": "Account1", "email": "account1@example.com"}, {"name": "Account2", "email": "account2@example.com"}, ] - mock_driver.execute.return_value = accounts_data + mock_driver.get_fedcm_account_list.return_value = accounts_data accounts = dialog.get_accounts() assert len(accounts) == 2 assert accounts[0].name == "Account1" assert accounts[0].email == "account1@example.com" + assert accounts[1].name == "Account2" + assert accounts[1].email == "account2@example.com" From bd2b3f6a1a1d41438aa0af7c9bd0c743056c4043 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 2 Nov 2024 19:53:46 +0530 Subject: [PATCH 04/15] use webserver in tests --- .../{common => chrome}/fedcm_tests.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) rename py/test/selenium/webdriver/{common => chrome}/fedcm_tests.py (86%) diff --git a/py/test/selenium/webdriver/common/fedcm_tests.py b/py/test/selenium/webdriver/chrome/fedcm_tests.py similarity index 86% rename from py/test/selenium/webdriver/common/fedcm_tests.py rename to py/test/selenium/webdriver/chrome/fedcm_tests.py index f7722ed778dce..6ba5a84846973 100644 --- a/py/test/selenium/webdriver/common/fedcm_tests.py +++ b/py/test/selenium/webdriver/chrome/fedcm_tests.py @@ -15,27 +15,15 @@ # specific language governing permissions and limitations # under the License. -import os - import pytest from selenium.common.exceptions import NoAlertPresentException -@pytest.mark.xfail_safari(reason="FedCM not supported") -@pytest.mark.xfail_firefox(reason="FedCM not supported") -@pytest.mark.xfail_ie(reason="FedCM not supported") -@pytest.mark.xfail_edge(reason="FedCM not supported") class TestFedCM: @pytest.fixture(autouse=True) - def setup(self, driver): - html_file_path = os.path.abspath( - "/Users/navinchandra/Documents/Projects/selenium/common/src/web/fedcm/fedcm.html" - ) - print("File path is: ", html_file_path) - - driver.get(f"file://{html_file_path}") - # driver.get('fedcm/fedcm.html') + def setup(self, driver, webserver): + driver.get(webserver.where_is("fedcm/fedcm.html", localhost=True)) self.dialog = driver.fedcm_dialog def test_no_dialog_present(self, driver): @@ -52,7 +40,8 @@ def test_no_dialog_present(self, driver): with pytest.raises(NoAlertPresentException): self.dialog.cancel() - def test_dialog_present(self, driver): + def test_dialog_present(self, driver, webserver): + driver.execute_script("triggerFedCm();") dialog = driver.wait_for_fedcm_dialog() @@ -64,6 +53,8 @@ def test_dialog_present(self, driver): assert accounts[0].name == "John Doe" dialog.select_account(1) + # wait for the dialog to become interactable after selecting the account + driver.wait_for_fedcm_dialog() dialog.cancel() with pytest.raises(NoAlertPresentException): @@ -121,8 +112,12 @@ def test_fedcm_dialog_flow(self, driver): # Test account selection dialog.select_account(1) - # Test dialog button click - dialog.click_continue() + # Wait for the dialog to become interactable + driver.wait_for_fedcm_dialog() + + # # Test dialog button click + # dialog.click_continue() + # driver.wait_for_fedcm_dialog() # Test dialog cancellation dialog.cancel() From 8ff195d4608224e6e4ae3c468505efc4938de52d Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 2 Nov 2024 23:34:27 +0530 Subject: [PATCH 05/15] use a custom fedcm_webserver fixture to use the ExtendedHandler --- py/conftest.py | 11 +++++++++++ py/test/selenium/webdriver/chrome/fedcm_tests.py | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index d5bf9aca8c1ea..936c89c349ace 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -21,6 +21,7 @@ import subprocess import time from test.selenium.webdriver.common.network import get_lan_ip +from test.selenium.webdriver.common.webserver import ExtendedHandler from test.selenium.webdriver.common.webserver import SimpleWebServer from urllib.request import urlopen @@ -329,6 +330,16 @@ def webserver(request): webserver.stop() +@pytest.fixture(scope="session") +def fedcm_webserver(request): + """Webserver fixture specifically for FedCM tests using ExtendedHandler.""" + host = get_lan_ip() if request.config.getoption("use_lan_ip") else None + webserver = SimpleWebServer(host=host, handler_class=ExtendedHandler) + webserver.start() + yield webserver + webserver.stop() + + @pytest.fixture def edge_service(): from selenium.webdriver.edge.service import Service as EdgeService diff --git a/py/test/selenium/webdriver/chrome/fedcm_tests.py b/py/test/selenium/webdriver/chrome/fedcm_tests.py index 6ba5a84846973..71437668c491f 100644 --- a/py/test/selenium/webdriver/chrome/fedcm_tests.py +++ b/py/test/selenium/webdriver/chrome/fedcm_tests.py @@ -22,8 +22,8 @@ class TestFedCM: @pytest.fixture(autouse=True) - def setup(self, driver, webserver): - driver.get(webserver.where_is("fedcm/fedcm.html", localhost=True)) + def setup(self, driver, fedcm_webserver): + driver.get(fedcm_webserver.where_is("fedcm/fedcm.html", localhost=True)) self.dialog = driver.fedcm_dialog def test_no_dialog_present(self, driver): @@ -40,7 +40,7 @@ def test_no_dialog_present(self, driver): with pytest.raises(NoAlertPresentException): self.dialog.cancel() - def test_dialog_present(self, driver, webserver): + def test_dialog_present(self, driver): driver.execute_script("triggerFedCm();") dialog = driver.wait_for_fedcm_dialog() From 10aa166980bd194c9551f508d7b45d7292d28470 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sun, 3 Nov 2024 00:28:03 +0530 Subject: [PATCH 06/15] rename to `extended_webserver` --- py/conftest.py | 4 ++-- py/test/selenium/webdriver/chrome/fedcm_tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 936c89c349ace..2273099ba5b61 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -330,8 +330,8 @@ def webserver(request): webserver.stop() -@pytest.fixture(scope="session") -def fedcm_webserver(request): +@pytest.fixture(autouse=True, scope="session") +def extended_webserver(request): """Webserver fixture specifically for FedCM tests using ExtendedHandler.""" host = get_lan_ip() if request.config.getoption("use_lan_ip") else None webserver = SimpleWebServer(host=host, handler_class=ExtendedHandler) diff --git a/py/test/selenium/webdriver/chrome/fedcm_tests.py b/py/test/selenium/webdriver/chrome/fedcm_tests.py index 71437668c491f..0a6a2c8e84612 100644 --- a/py/test/selenium/webdriver/chrome/fedcm_tests.py +++ b/py/test/selenium/webdriver/chrome/fedcm_tests.py @@ -22,8 +22,8 @@ class TestFedCM: @pytest.fixture(autouse=True) - def setup(self, driver, fedcm_webserver): - driver.get(fedcm_webserver.where_is("fedcm/fedcm.html", localhost=True)) + def setup(self, driver, extended_webserver): + driver.get(extended_webserver.where_is("fedcm/fedcm.html", localhost=True)) self.dialog = driver.fedcm_dialog def test_no_dialog_present(self, driver): From 484b621313ff4e996c9f0971583f6369bcfc9265 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Mon, 4 Nov 2024 12:30:33 +0530 Subject: [PATCH 07/15] use `webserver` --- py/conftest.py | 11 ----------- py/test/selenium/webdriver/chrome/fedcm_tests.py | 4 ++-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/py/conftest.py b/py/conftest.py index 2273099ba5b61..d5bf9aca8c1ea 100644 --- a/py/conftest.py +++ b/py/conftest.py @@ -21,7 +21,6 @@ import subprocess import time from test.selenium.webdriver.common.network import get_lan_ip -from test.selenium.webdriver.common.webserver import ExtendedHandler from test.selenium.webdriver.common.webserver import SimpleWebServer from urllib.request import urlopen @@ -330,16 +329,6 @@ def webserver(request): webserver.stop() -@pytest.fixture(autouse=True, scope="session") -def extended_webserver(request): - """Webserver fixture specifically for FedCM tests using ExtendedHandler.""" - host = get_lan_ip() if request.config.getoption("use_lan_ip") else None - webserver = SimpleWebServer(host=host, handler_class=ExtendedHandler) - webserver.start() - yield webserver - webserver.stop() - - @pytest.fixture def edge_service(): from selenium.webdriver.edge.service import Service as EdgeService diff --git a/py/test/selenium/webdriver/chrome/fedcm_tests.py b/py/test/selenium/webdriver/chrome/fedcm_tests.py index 0a6a2c8e84612..b315d6fe49633 100644 --- a/py/test/selenium/webdriver/chrome/fedcm_tests.py +++ b/py/test/selenium/webdriver/chrome/fedcm_tests.py @@ -22,8 +22,8 @@ class TestFedCM: @pytest.fixture(autouse=True) - def setup(self, driver, extended_webserver): - driver.get(extended_webserver.where_is("fedcm/fedcm.html", localhost=True)) + def setup(self, driver, webserver): + driver.get(webserver.where_is("fedcm/fedcm.html", localhost=True)) self.dialog = driver.fedcm_dialog def test_no_dialog_present(self, driver): From c2f5115e80f26327d9a1308b2ea03458f3316b98 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 6 Nov 2024 15:58:50 +0530 Subject: [PATCH 08/15] move fedcm commands to a new `fedcm` namespace --- .../driver_extensions/has_fedcm_dialog.py | 22 +++--- py/selenium/webdriver/common/fedcm/dialog.py | 14 ++-- py/selenium/webdriver/remote/fedcm.py | 70 +++++++++++++++++++ 3 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 py/selenium/webdriver/remote/fedcm.py diff --git a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py index 745ad22be51bf..88c978b28d98e 100644 --- a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py +++ b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py @@ -22,18 +22,22 @@ class HasFedCmDialog(WebDriver): """Mixin that provides FedCM-specific functionality.""" @property - def fedcm_dialog(self): + def dialog(self): """Returns the FedCM dialog object for interaction.""" return Dialog(self) - def enable_fedcm_delay(self, enable): - """Disables the promise rejection delay for FedCm. + def enable_fedcm_delay(self): + """Re-enables the promise rejection delay for FedCM.""" + self.fedcm.enable_delay() + + def disable_fedcm_delay(self): + """Disables the promise rejection delay for FedCM. FedCm by default delays promise resolution in failure cases for privacy reasons. This method allows turning it off to let tests run faster where this is not relevant. """ - self.set_fedcm_delay(enable) + self.fedcm.disable_delay() def fedcm_cooldown(self): """Resets the FedCm dialog cooldown. @@ -42,9 +46,9 @@ def fedcm_cooldown(self): dismissed, this method resets that cooldown so that the dialog can be triggered again immediately. """ - self.reset_fedcm_cooldown() + self.fedcm.reset_cooldown() - def wait_for_fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None): + def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None): """Waits for the FedCM dialog to appear.""" from selenium.common.exceptions import NoAlertPresentException from selenium.webdriver.support.wait import WebDriverWait @@ -52,11 +56,11 @@ def wait_for_fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exception if ignored_exceptions is None: ignored_exceptions = (NoAlertPresentException,) - def check_fedcm(): + def _check_fedcm(): try: - return self.fedcm_dialog if self.fedcm_dialog.type else None + return self.dialog if self.dialog.type else None except NoAlertPresentException: return None wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions) - return wait.until(lambda _: check_fedcm()) + return wait.until(lambda _: _check_fedcm()) diff --git a/py/selenium/webdriver/common/fedcm/dialog.py b/py/selenium/webdriver/common/fedcm/dialog.py index b94a57f3e0ed6..6da9596f541cd 100644 --- a/py/selenium/webdriver/common/fedcm/dialog.py +++ b/py/selenium/webdriver/common/fedcm/dialog.py @@ -33,32 +33,32 @@ def __init__(self, driver) -> None: @property def type(self) -> Optional[str]: """Gets the type of the dialog currently being shown.""" - return self._driver.get_fedcm_dialog_type() + return self._driver.fedcm.dialog_type @property def title(self) -> str: """Gets the title of the dialog.""" - return self._driver.get_fedcm_title() + return self._driver.fedcm.title @property def subtitle(self) -> Optional[str]: """Gets the subtitle of the dialog.""" - result = self._driver.get_fedcm_subtitle() + result = self._driver.fedcm.subtitle return result.get("subtitle") if result else None def get_accounts(self) -> List[Account]: """Gets the list of accounts shown in the dialog.""" - accounts = self._driver.get_fedcm_account_list() + accounts = self._driver.fedcm.account_list return [Account(account) for account in accounts] def select_account(self, index: int) -> None: """Selects an account from the dialog by index.""" - self._driver.select_fedcm_account(index) + self._driver.fedcm.select_account(index) def click_continue(self) -> None: """Clicks the continue button in the dialog.""" - self._driver.click_fedcm_dialog_button() + self._driver.fedcm.click_continue() def cancel(self) -> None: """Cancels/dismisses the dialog.""" - self._driver.cancel_fedcm_dialog() + self._driver.fedcm.cancel_dialog() diff --git a/py/selenium/webdriver/remote/fedcm.py b/py/selenium/webdriver/remote/fedcm.py new file mode 100644 index 0000000000000..635fb8d77abd5 --- /dev/null +++ b/py/selenium/webdriver/remote/fedcm.py @@ -0,0 +1,70 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import List +from typing import Optional + +from .command import Command + + +class FedCM: + def __init__(self, driver) -> None: + self._driver = driver + + @property + def title(self) -> str: + """Gets the title of the dialog.""" + return self._driver.execute(Command.GET_FEDCM_TITLE)["value"].get("title") + + @property + def subtitle(self) -> Optional[str]: + """Gets the subtitle of the dialog.""" + return self._driver.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle") + + @property + def dialog_type(self) -> str: + """Gets the type of the dialog currently being shown.""" + return self._driver.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value") + + @property + def account_list(self) -> List[dict]: + """Gets the list of accounts shown in the dialog.""" + return self._driver.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value") + + def select_account(self, index: int) -> None: + """Selects an account from the dialog by index.""" + self._driver.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) + + def click_continue(self) -> None: + """Clicks the continue button in the dialog.""" + self._driver.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) + + def cancel_dialog(self) -> None: + """Cancels/dismisses the FedCM dialog.""" + self._driver.execute(Command.CANCEL_FEDCM_DIALOG) + + def enable_delay(self) -> None: + """Re-enables the promise rejection delay for FedCM.""" + self._driver.execute(Command.SET_FEDCM_DELAY, {"enabled": True}) + + def disable_delay(self) -> None: + """Disables the promise rejection delay for FedCM.""" + self._driver.execute(Command.SET_FEDCM_DELAY, {"enabled": False}) + + def reset_cooldown(self) -> None: + """Resets the FedCM dialog cooldown, allowing immediate retriggers.""" + self._driver.execute(Command.RESET_FEDCM_COOLDOWN) From f5d47fa3a1b80a9cd08a99d787f6acc1af8a55e0 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 6 Nov 2024 16:00:32 +0530 Subject: [PATCH 09/15] support both edge and chrome browser for fedcm --- py/selenium/webdriver/chrome/webdriver.py | 3 +- py/selenium/webdriver/chromium/webdriver.py | 3 +- py/selenium/webdriver/remote/webdriver.py | 111 ++++++++++++-------- 3 files changed, 70 insertions(+), 47 deletions(-) diff --git a/py/selenium/webdriver/chrome/webdriver.py b/py/selenium/webdriver/chrome/webdriver.py index 53c2ba9a7c545..5fb9583b6817c 100644 --- a/py/selenium/webdriver/chrome/webdriver.py +++ b/py/selenium/webdriver/chrome/webdriver.py @@ -17,13 +17,12 @@ from selenium.webdriver.chromium.webdriver import ChromiumDriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities -from selenium.webdriver.common.driver_extensions.has_fedcm_dialog import HasFedCmDialog from .options import Options from .service import Service -class WebDriver(ChromiumDriver, HasFedCmDialog): +class WebDriver(ChromiumDriver): """Controls the ChromeDriver and allows you to drive the browser.""" def __init__( diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index d7bf5c706a453..18f76ac34b19a 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -16,13 +16,14 @@ # under the License. from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection +from selenium.webdriver.common.driver_extensions.has_fedcm_dialog import HasFedCmDialog from selenium.webdriver.common.driver_finder import DriverFinder from selenium.webdriver.common.options import ArgOptions from selenium.webdriver.common.service import Service from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver -class ChromiumDriver(RemoteWebDriver): +class ChromiumDriver(HasFedCmDialog, RemoteWebDriver): """Controls the WebDriver instance of ChromiumDriver and allows you to drive the browser.""" diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 230e4fdb09d7a..cfe5a998f7d4a 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -57,6 +57,7 @@ from .client_config import ClientConfig from .command import Command from .errorhandler import ErrorHandler +from .fedcm import FedCM from .file_detector import FileDetector from .file_detector import LocalFileDetector from .locator_converter import LocatorConverter @@ -236,6 +237,7 @@ def __init__( self._authenticator_id = None self.start_client() self.start_session(capabilities) + self._fedcm = FedCM(self) self._websocket_connection = None self._script = None @@ -1223,51 +1225,72 @@ def delete_downloadable_files(self) -> None: self.execute(Command.DELETE_DOWNLOADABLE_FILES) - # Federated Credential Management (FedCM) - def get_fedcm_title(self) -> str: - """Gets the title of the dialog.""" - return self.execute(Command.GET_FEDCM_TITLE)["value"].get("title") - - def get_fedcm_subtitle(self) -> Optional[str]: - """Gets the subtitle of the dialog.""" - return self.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle") - - def get_fedcm_dialog_type(self): - """Gets the type of the dialog currently being shown.""" - return self.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value") - - def get_fedcm_account_list(self): - """Gets the list of accounts shown in the dialog.""" - return self.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value") - - def select_fedcm_account(self, index: int) -> None: - """Selects an account from the dialog by index.""" - self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) - - def click_fedcm_dialog_button(self) -> None: - """Clicks the continue button in the dialog.""" - self.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) - - def cancel_fedcm_dialog(self) -> None: - """Cancels/dismisses the FedCM dialog.""" - self.execute(Command.CANCEL_FEDCM_DIALOG) - - def set_fedcm_delay(self, enabled: bool) -> None: - """Disables the promise rejection delay for FedCM. - - FedCM by default delays promise resolution in failure cases for privacy reasons. - This method allows turning it off to let tests run faster where this is not relevant. - - Args: - enabled: True to enable the delay, False to disable it + @property + def fedcm(self) -> FedCM: """ - self.execute(Command.SET_FEDCM_DELAY, {"enabled": enabled}) + :Returns: + - FedCM: an object providing access to all Federated Credential Management (FedCM) dialog commands. - def reset_fedcm_cooldown(self) -> None: - """Resets the FedCM dialog cooldown. + :Usage: + :: - If a user agent triggers a cooldown when the account chooser is - dismissed, this method resets that cooldown so that the dialog - can be triggered again immediately. + title = driver.fedcm.title + subtitle = driver.fedcm.subtitle + dialog_type = driver.fedcm.dialog_type + accounts = driver.fedcm.account_list + driver.fedcm.select_account(0) + driver.fedcm.click_dialog_button() + driver.fedcm.cancel_dialog() + driver.fedcm.set_delay(False) + driver.fedcm.reset_cooldown() """ - self.execute(Command.RESET_FEDCM_COOLDOWN) + return self._fedcm + + # # Federated Credential Management (FedCM) + # def get_fedcm_title(self) -> str: + # """Gets the title of the dialog.""" + # return self.execute(Command.GET_FEDCM_TITLE)["value"].get("title") + # + # def get_fedcm_subtitle(self) -> Optional[str]: + # """Gets the subtitle of the dialog.""" + # return self.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle") + # + # def get_fedcm_dialog_type(self): + # """Gets the type of the dialog currently being shown.""" + # return self.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value") + # + # def get_fedcm_account_list(self): + # """Gets the list of accounts shown in the dialog.""" + # return self.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value") + # + # def select_fedcm_account(self, index: int) -> None: + # """Selects an account from the dialog by index.""" + # self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) + # + # def click_fedcm_dialog_button(self) -> None: + # """Clicks the continue button in the dialog.""" + # self.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) + # + # def cancel_fedcm_dialog(self) -> None: + # """Cancels/dismisses the FedCM dialog.""" + # self.execute(Command.CANCEL_FEDCM_DIALOG) + # + # def set_fedcm_delay(self, enabled: bool) -> None: + # """Disables the promise rejection delay for FedCM. + # + # FedCM by default delays promise resolution in failure cases for privacy reasons. + # This method allows turning it off to let tests run faster where this is not relevant. + # + # Args: + # enabled: True to enable the delay, False to disable it + # """ + # self.execute(Command.SET_FEDCM_DELAY, {"enabled": enabled}) + # + # def reset_fedcm_cooldown(self) -> None: + # """Resets the FedCM dialog cooldown. + # + # If a user agent triggers a cooldown when the account chooser is + # dismissed, this method resets that cooldown so that the dialog + # can be triggered again immediately. + # """ + # self.execute(Command.RESET_FEDCM_COOLDOWN) From 25d3431cf837d755f67a8fa526e11f04eff823b0 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 6 Nov 2024 16:01:26 +0530 Subject: [PATCH 10/15] fix tests as per new implementation --- .../selenium/webdriver/chrome/fedcm_tests.py | 141 ------------------ .../selenium/webdriver/common/fedcm_tests.py | 137 +++++++++++++++++ .../webdriver/common/fedcm/dialog_tests.py | 44 ++++-- 3 files changed, 165 insertions(+), 157 deletions(-) delete mode 100644 py/test/selenium/webdriver/chrome/fedcm_tests.py create mode 100644 py/test/selenium/webdriver/common/fedcm_tests.py diff --git a/py/test/selenium/webdriver/chrome/fedcm_tests.py b/py/test/selenium/webdriver/chrome/fedcm_tests.py deleted file mode 100644 index b315d6fe49633..0000000000000 --- a/py/test/selenium/webdriver/chrome/fedcm_tests.py +++ /dev/null @@ -1,141 +0,0 @@ -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -import pytest - -from selenium.common.exceptions import NoAlertPresentException - - -class TestFedCM: - @pytest.fixture(autouse=True) - def setup(self, driver, webserver): - driver.get(webserver.where_is("fedcm/fedcm.html", localhost=True)) - self.dialog = driver.fedcm_dialog - - def test_no_dialog_present(self, driver): - with pytest.raises(NoAlertPresentException): - self.dialog.title - with pytest.raises(NoAlertPresentException): - self.dialog.subtitle - with pytest.raises(NoAlertPresentException): - self.dialog.type - with pytest.raises(NoAlertPresentException): - self.dialog.get_accounts() - with pytest.raises(NoAlertPresentException): - self.dialog.select_account(1) - with pytest.raises(NoAlertPresentException): - self.dialog.cancel() - - def test_dialog_present(self, driver): - - driver.execute_script("triggerFedCm();") - dialog = driver.wait_for_fedcm_dialog() - - assert dialog.title == "Sign in to localhost with localhost" - assert dialog.subtitle is None - assert dialog.type == "AccountChooser" - - accounts = dialog.get_accounts() - assert accounts[0].name == "John Doe" - - dialog.select_account(1) - # wait for the dialog to become interactable after selecting the account - driver.wait_for_fedcm_dialog() - dialog.cancel() - - with pytest.raises(NoAlertPresentException): - dialog.title - - def test_fedcm_delay(self, driver): - driver.set_fedcm_delay(True) - - def test_reset_cooldown(self, driver): - driver.fedcm_cooldown() - - def test_fedcm_commands(self, driver): - # Test get_fedcm_dialog_type - with pytest.raises(NoAlertPresentException): - driver.get_fedcm_dialog_type() - - # Test get_fedcm_title - with pytest.raises(NoAlertPresentException): - driver.get_fedcm_title() - - # Test get_fedcm_subtitle - with pytest.raises(NoAlertPresentException): - driver.get_fedcm_subtitle() - - # Test get_fedcm_account_list - with pytest.raises(NoAlertPresentException): - driver.get_fedcm_account_list() - - # Test select_fedcm_account - with pytest.raises(NoAlertPresentException): - driver.select_fedcm_account(1) - - # Test cancel_fedcm_dialog - with pytest.raises(NoAlertPresentException): - driver.cancel_fedcm_dialog() - - # Test click_fedcm_dialog_button - with pytest.raises(NoAlertPresentException): - driver.click_fedcm_dialog_button() - - def test_fedcm_dialog_flow(self, driver): - driver.execute_script("triggerFedCm();") - dialog = driver.wait_for_fedcm_dialog() - - # Test dialog properties - assert dialog.type == "AccountChooser" - assert dialog.title == "Sign in to localhost with localhost" - assert dialog.subtitle is None - - # Test account list - accounts = dialog.get_accounts() - assert len(accounts) > 0 - assert accounts[0].name == "John Doe" - - # Test account selection - dialog.select_account(1) - - # Wait for the dialog to become interactable - driver.wait_for_fedcm_dialog() - - # # Test dialog button click - # dialog.click_continue() - # driver.wait_for_fedcm_dialog() - - # Test dialog cancellation - dialog.cancel() - with pytest.raises(NoAlertPresentException): - dialog.title - - def test_fedcm_delay_settings(self, driver): - # Test enabling delay - driver.set_fedcm_delay(True) - - # Test disabling delay - driver.set_fedcm_delay(False) - - def test_fedcm_cooldown_management(self, driver): - # Test cooldown reset - driver.fedcm_cooldown() - - # Verify dialog can be triggered after reset - driver.execute_script("triggerFedCm();") - dialog = driver.wait_for_fedcm_dialog() - assert dialog.type == "AccountChooser" diff --git a/py/test/selenium/webdriver/common/fedcm_tests.py b/py/test/selenium/webdriver/common/fedcm_tests.py new file mode 100644 index 0000000000000..accde0c96c1d3 --- /dev/null +++ b/py/test/selenium/webdriver/common/fedcm_tests.py @@ -0,0 +1,137 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import NoAlertPresentException + + +@pytest.mark.xfail_safari(reason="FedCM not supported") +@pytest.mark.xfail_firefox(reason="FedCM not supported") +@pytest.mark.xfail_ie(reason="FedCM not supported") +class TestFedCM: + @pytest.fixture(autouse=True) + def setup(self, driver, webserver): + driver.get(webserver.where_is("fedcm/fedcm.html", localhost=True)) + self.dialog = driver.dialog + + def test_no_dialog_title(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.title + + def test_no_dialog_subtitle(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.subtitle + + def test_no_dialog_type(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.type + + def test_no_dialog_get_accounts(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.get_accounts() + + def test_no_dialog_select_account(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.select_account(1) + + def test_no_dialog_cancel(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.cancel() + + def test_no_dialog_click_continue(driver): + with pytest.raises(NoAlertPresentException): + driver.dialog.click_continue() + + def test_trigger_and_verify_dialog_title(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + assert dialog.title == "Sign in to localhost with localhost" + + def test_trigger_and_verify_dialog_subtitle(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + assert dialog.subtitle is None + + def test_trigger_and_verify_dialog_type(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + assert dialog.type == "AccountChooser" + + def test_trigger_and_verify_account_list(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + accounts = dialog.get_accounts() + assert len(accounts) > 0 + assert accounts[0].name == "John Doe" + + def test_select_account(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + dialog.select_account(1) + driver.fedcm_dialog() # Wait for dialog to become interactable + # dialog.click_continue() + + def test_dialog_cancel(self, driver): + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + dialog.cancel() + with pytest.raises(NoAlertPresentException): + dialog.title + + def test_enable_fedcm_delay(self, driver): + driver.fedcm.enable_delay() + + def test_disable_fedcm_delay(self, driver): + driver.fedcm.disable_delay() + + def test_fedcm_cooldown_reset(self, driver): + driver.fedcm_cooldown() + + def test_fedcm_no_dialog_type_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.dialog_type + + def test_fedcm_no_title_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.title + + def test_fedcm_no_subtitle_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.subtitle + + def test_fedcm_no_account_list_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.account_list() + + def test_fedcm_no_select_account_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.select_account(1) + + def test_fedcm_no_cancel_dialog_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.cancel_dialog() + + def test_fedcm_no_click_continue_present(self, driver): + with pytest.raises(NoAlertPresentException): + driver.fedcm.click_continue() + + def test_verify_dialog_type_after_cooldown_reset(self, driver): + driver.fedcm_cooldown() + driver.execute_script("triggerFedCm();") + dialog = driver.fedcm_dialog() + assert dialog.type == "AccountChooser" diff --git a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py index a35b689673690..4f18cebc7fe1f 100644 --- a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py +++ b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py @@ -16,6 +16,7 @@ # under the License. from unittest.mock import Mock +from unittest.mock import patch import pytest @@ -28,47 +29,58 @@ def mock_driver(): @pytest.fixture -def dialog(mock_driver): +def fedcm(mock_driver): + fedcm = Mock() + mock_driver.fedcm = fedcm + return fedcm + + +@pytest.fixture +def dialog(mock_driver, fedcm): return Dialog(mock_driver) -def test_click_continue(dialog, mock_driver): +def test_click_continue(dialog, fedcm): dialog.click_continue() - mock_driver.click_fedcm_dialog_button.assert_called_once() + fedcm.click_continue.assert_called_once() -def test_cancel(dialog, mock_driver): +def test_cancel(dialog, fedcm): dialog.cancel() - mock_driver.cancel_fedcm_dialog.assert_called_once() + fedcm.cancel_dialog.assert_called_once() -def test_select_account(dialog, mock_driver): +def test_select_account(dialog, fedcm): dialog.select_account(1) - mock_driver.select_fedcm_account.assert_called_once_with(1) + fedcm.select_account.assert_called_once_with(1) -def test_type(dialog, mock_driver): - mock_driver.get_fedcm_dialog_type.return_value = "AccountChooser" +def test_type(dialog, fedcm): + fedcm.dialog_type = "AccountChooser" assert dialog.type == "AccountChooser" -def test_title(dialog, mock_driver): - mock_driver.get_fedcm_title.return_value = "Sign in" +def test_title(dialog, fedcm): + fedcm.title = "Sign in" assert dialog.title == "Sign in" -def test_subtitle(dialog, mock_driver): - mock_driver.get_fedcm_subtitle.return_value = {"subtitle": "Choose an account"} +def test_subtitle(dialog, fedcm): + fedcm.subtitle = {"subtitle": "Choose an account"} assert dialog.subtitle == "Choose an account" -def test_get_accounts(dialog, mock_driver): +def test_get_accounts(dialog, fedcm): accounts_data = [ {"name": "Account1", "email": "account1@example.com"}, {"name": "Account2", "email": "account2@example.com"}, ] - mock_driver.get_fedcm_account_list.return_value = accounts_data - accounts = dialog.get_accounts() + fedcm.account_list = accounts_data + + with patch("selenium.webdriver.common.fedcm.account.Account") as MockAccount: + MockAccount.return_value = Mock() # Mock the Account instance + accounts = dialog.get_accounts() + assert len(accounts) == 2 assert accounts[0].name == "Account1" assert accounts[0].email == "account1@example.com" From bfb5a428dd6d683ba0a7d86e0a9d986022fe77c0 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Wed, 6 Nov 2024 16:05:55 +0530 Subject: [PATCH 11/15] remove old commented code --- py/selenium/webdriver/remote/webdriver.py | 49 ----------------------- 1 file changed, 49 deletions(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index cfe5a998f7d4a..c300fa3c85bdd 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1245,52 +1245,3 @@ def fedcm(self) -> FedCM: driver.fedcm.reset_cooldown() """ return self._fedcm - - # # Federated Credential Management (FedCM) - # def get_fedcm_title(self) -> str: - # """Gets the title of the dialog.""" - # return self.execute(Command.GET_FEDCM_TITLE)["value"].get("title") - # - # def get_fedcm_subtitle(self) -> Optional[str]: - # """Gets the subtitle of the dialog.""" - # return self.execute(Command.GET_FEDCM_TITLE)["value"].get("subtitle") - # - # def get_fedcm_dialog_type(self): - # """Gets the type of the dialog currently being shown.""" - # return self.execute(Command.GET_FEDCM_DIALOG_TYPE).get("value") - # - # def get_fedcm_account_list(self): - # """Gets the list of accounts shown in the dialog.""" - # return self.execute(Command.GET_FEDCM_ACCOUNT_LIST).get("value") - # - # def select_fedcm_account(self, index: int) -> None: - # """Selects an account from the dialog by index.""" - # self.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) - # - # def click_fedcm_dialog_button(self) -> None: - # """Clicks the continue button in the dialog.""" - # self.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) - # - # def cancel_fedcm_dialog(self) -> None: - # """Cancels/dismisses the FedCM dialog.""" - # self.execute(Command.CANCEL_FEDCM_DIALOG) - # - # def set_fedcm_delay(self, enabled: bool) -> None: - # """Disables the promise rejection delay for FedCM. - # - # FedCM by default delays promise resolution in failure cases for privacy reasons. - # This method allows turning it off to let tests run faster where this is not relevant. - # - # Args: - # enabled: True to enable the delay, False to disable it - # """ - # self.execute(Command.SET_FEDCM_DELAY, {"enabled": enabled}) - # - # def reset_fedcm_cooldown(self) -> None: - # """Resets the FedCM dialog cooldown. - # - # If a user agent triggers a cooldown when the account chooser is - # dismissed, this method resets that cooldown so that the dialog - # can be triggered again immediately. - # """ - # self.execute(Command.RESET_FEDCM_COOLDOWN) From 9e52be8e06727a3993427372119e0f2dfb1d4ec8 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 7 Nov 2024 00:32:38 +0530 Subject: [PATCH 12/15] remove mixin and implement in remote webdriver --- py/selenium/webdriver/chromium/webdriver.py | 3 +- .../driver_extensions/has_fedcm_dialog.py | 66 -------------- py/selenium/webdriver/common/options.py | 2 + py/selenium/webdriver/remote/webdriver.py | 89 +++++++++++++++++++ .../selenium/webdriver/common/fedcm_tests.py | 4 +- 5 files changed, 94 insertions(+), 70 deletions(-) delete mode 100644 py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index 18f76ac34b19a..d7bf5c706a453 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -16,14 +16,13 @@ # under the License. from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection -from selenium.webdriver.common.driver_extensions.has_fedcm_dialog import HasFedCmDialog from selenium.webdriver.common.driver_finder import DriverFinder from selenium.webdriver.common.options import ArgOptions from selenium.webdriver.common.service import Service from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver -class ChromiumDriver(HasFedCmDialog, RemoteWebDriver): +class ChromiumDriver(RemoteWebDriver): """Controls the WebDriver instance of ChromiumDriver and allows you to drive the browser.""" diff --git a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py b/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py deleted file mode 100644 index 88c978b28d98e..0000000000000 --- a/py/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.py +++ /dev/null @@ -1,66 +0,0 @@ -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from selenium.webdriver.common.fedcm.dialog import Dialog -from selenium.webdriver.remote.webdriver import WebDriver - - -class HasFedCmDialog(WebDriver): - """Mixin that provides FedCM-specific functionality.""" - - @property - def dialog(self): - """Returns the FedCM dialog object for interaction.""" - return Dialog(self) - - def enable_fedcm_delay(self): - """Re-enables the promise rejection delay for FedCM.""" - self.fedcm.enable_delay() - - def disable_fedcm_delay(self): - """Disables the promise rejection delay for FedCM. - - FedCm by default delays promise resolution in failure cases for - privacy reasons. This method allows turning it off to let tests - run faster where this is not relevant. - """ - self.fedcm.disable_delay() - - def fedcm_cooldown(self): - """Resets the FedCm dialog cooldown. - - If a user agent triggers a cooldown when the account chooser is - dismissed, this method resets that cooldown so that the dialog - can be triggered again immediately. - """ - self.fedcm.reset_cooldown() - - def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None): - """Waits for the FedCM dialog to appear.""" - from selenium.common.exceptions import NoAlertPresentException - from selenium.webdriver.support.wait import WebDriverWait - - if ignored_exceptions is None: - ignored_exceptions = (NoAlertPresentException,) - - def _check_fedcm(): - try: - return self.dialog if self.dialog.type else None - except NoAlertPresentException: - return None - - wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions) - return wait.until(lambda _: _check_fedcm()) diff --git a/py/selenium/webdriver/common/options.py b/py/selenium/webdriver/common/options.py index 3e754d2537ba6..b31fcc348ce18 100644 --- a/py/selenium/webdriver/common/options.py +++ b/py/selenium/webdriver/common/options.py @@ -491,6 +491,8 @@ def ignore_local_proxy_environment_variables(self) -> None: class ArgOptions(BaseOptions): BINARY_LOCATION_ERROR = "Binary Location Must be a String" + # FedCM capability key + FEDCM_CAPABILITY = "fedcm:accounts" def __init__(self) -> None: super().__init__() diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index c300fa3c85bdd..b671300fb9138 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -43,6 +43,7 @@ from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.bidi.script import Script from selenium.webdriver.common.by import By +from selenium.webdriver.common.options import ArgOptions from selenium.webdriver.common.options import BaseOptions from selenium.webdriver.common.print_page_options import PrintOptions from selenium.webdriver.common.timeouts import Timeouts @@ -53,6 +54,7 @@ ) from selenium.webdriver.support.relative_locator import RelativeBy +from ..common.fedcm.dialog import Dialog from .bidi_connection import BidiConnection from .client_config import ClientConfig from .command import Command @@ -1245,3 +1247,90 @@ def fedcm(self) -> FedCM: driver.fedcm.reset_cooldown() """ return self._fedcm + + @property + def supports_fedcm(self) -> bool: + """Returns whether the browser supports FedCM capabilities.""" + return self.capabilities.get(ArgOptions.FEDCM_CAPABILITY, False) + + def _require_fedcm_support(self): + """Raises an exception if FedCM is not supported.""" + if not self.supports_fedcm: + raise WebDriverException( + "This browser does not support Federated Credential Management. " + "Please ensure you're using a supported browser." + ) + + @property + def dialog(self): + """Returns the FedCM dialog object for interaction.""" + self._require_fedcm_support() + return Dialog(self) + + def enable_fedcm_delay(self): + """Enables the promise rejection delay for FedCM. + + Raises: + WebDriverException if FedCM not supported + """ + self._require_fedcm_support() + self.fedcm.enable_delay() + + def disable_fedcm_delay(self): + """Disables the promise rejection delay for FedCM. + + FedCM by default delays promise resolution in failure cases for + privacy reasons. This method allows turning it off to let tests + run faster where this is not relevant. + + Raises: + WebDriverException if FedCM not supported + """ + self._require_fedcm_support() + self.fedcm.disable_delay() + + def reset_fedcm_cooldown(self): + """Resets the FedCM dialog cooldown. + + If a user agent triggers a cooldown when the account chooser is + dismissed, this method resets that cooldown so that the dialog + can be triggered again immediately. + + Raises: + WebDriverException if FedCM not supported + """ + self._require_fedcm_support() + self.fedcm.reset_cooldown() + + def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None): + """Waits for and returns the FedCM dialog. + + Args: + timeout: How long to wait for the dialog + poll_frequency: How frequently to poll + ignored_exceptions: Exceptions to ignore while waiting + + Returns: + The FedCM dialog object if found + + Raises: + TimeoutException if dialog doesn't appear + WebDriverException if FedCM not supported + """ + from selenium.common.exceptions import NoAlertPresentException + from selenium.webdriver.support.wait import WebDriverWait + + self._require_fedcm_support() + + if ignored_exceptions is None: + ignored_exceptions = (NoAlertPresentException,) + + def _check_fedcm(): + try: + dialog = Dialog(self) + return dialog if dialog.type else None + except NoAlertPresentException: + return None + + wait = WebDriverWait(self, timeout, poll_frequency=poll_frequency, ignored_exceptions=ignored_exceptions) + return wait.until(lambda _: _check_fedcm()) diff --git a/py/test/selenium/webdriver/common/fedcm_tests.py b/py/test/selenium/webdriver/common/fedcm_tests.py index accde0c96c1d3..20500541a9b66 100644 --- a/py/test/selenium/webdriver/common/fedcm_tests.py +++ b/py/test/selenium/webdriver/common/fedcm_tests.py @@ -100,7 +100,7 @@ def test_disable_fedcm_delay(self, driver): driver.fedcm.disable_delay() def test_fedcm_cooldown_reset(self, driver): - driver.fedcm_cooldown() + driver.reset_fedcm_cooldown() def test_fedcm_no_dialog_type_present(self, driver): with pytest.raises(NoAlertPresentException): @@ -131,7 +131,7 @@ def test_fedcm_no_click_continue_present(self, driver): driver.fedcm.click_continue() def test_verify_dialog_type_after_cooldown_reset(self, driver): - driver.fedcm_cooldown() + driver.reset_fedcm_cooldown() driver.execute_script("triggerFedCm();") dialog = driver.fedcm_dialog() assert dialog.type == "AccountChooser" From b9255139ac2cf72cd444989be9fc09804d87a01a Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 7 Nov 2024 09:03:20 +0530 Subject: [PATCH 13/15] fix python usage in docstrings --- py/selenium/webdriver/remote/webdriver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index b671300fb9138..791affacfc806 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1241,9 +1241,10 @@ def fedcm(self) -> FedCM: dialog_type = driver.fedcm.dialog_type accounts = driver.fedcm.account_list driver.fedcm.select_account(0) - driver.fedcm.click_dialog_button() + driver.fedcm.click_continue() driver.fedcm.cancel_dialog() - driver.fedcm.set_delay(False) + driver.fedcm.enable_delay() + driver.fedcm.disable_delay() driver.fedcm.reset_cooldown() """ return self._fedcm From c2c4faffa3c5afc313eced2d4dd609017d5eec18 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 7 Nov 2024 17:19:31 +0530 Subject: [PATCH 14/15] move to `accept` and `dismiss` for dialog --- py/selenium/webdriver/common/fedcm/dialog.py | 8 ++++---- py/selenium/webdriver/remote/fedcm.py | 4 ++-- py/test/selenium/webdriver/common/fedcm_tests.py | 11 ++++++----- .../selenium/webdriver/common/fedcm/dialog_tests.py | 8 ++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/py/selenium/webdriver/common/fedcm/dialog.py b/py/selenium/webdriver/common/fedcm/dialog.py index 6da9596f541cd..fce069d00d767 100644 --- a/py/selenium/webdriver/common/fedcm/dialog.py +++ b/py/selenium/webdriver/common/fedcm/dialog.py @@ -55,10 +55,10 @@ def select_account(self, index: int) -> None: """Selects an account from the dialog by index.""" self._driver.fedcm.select_account(index) - def click_continue(self) -> None: + def accept(self) -> None: """Clicks the continue button in the dialog.""" - self._driver.fedcm.click_continue() + self._driver.fedcm.accept() - def cancel(self) -> None: + def dismiss(self) -> None: """Cancels/dismisses the dialog.""" - self._driver.fedcm.cancel_dialog() + self._driver.fedcm.dismiss() diff --git a/py/selenium/webdriver/remote/fedcm.py b/py/selenium/webdriver/remote/fedcm.py index 635fb8d77abd5..eb2331923c2d5 100644 --- a/py/selenium/webdriver/remote/fedcm.py +++ b/py/selenium/webdriver/remote/fedcm.py @@ -49,11 +49,11 @@ def select_account(self, index: int) -> None: """Selects an account from the dialog by index.""" self._driver.execute(Command.SELECT_FEDCM_ACCOUNT, {"accountIndex": index}) - def click_continue(self) -> None: + def accept(self) -> None: """Clicks the continue button in the dialog.""" self._driver.execute(Command.CLICK_FEDCM_DIALOG_BUTTON, {"dialogButton": "ConfirmIdpLoginContinue"}) - def cancel_dialog(self) -> None: + def dismiss(self) -> None: """Cancels/dismisses the FedCM dialog.""" self._driver.execute(Command.CANCEL_FEDCM_DIALOG) diff --git a/py/test/selenium/webdriver/common/fedcm_tests.py b/py/test/selenium/webdriver/common/fedcm_tests.py index 20500541a9b66..53c3d3c7f5d6c 100644 --- a/py/test/selenium/webdriver/common/fedcm_tests.py +++ b/py/test/selenium/webdriver/common/fedcm_tests.py @@ -23,6 +23,7 @@ @pytest.mark.xfail_safari(reason="FedCM not supported") @pytest.mark.xfail_firefox(reason="FedCM not supported") @pytest.mark.xfail_ie(reason="FedCM not supported") +@pytest.mark.xfail_remote(reason="FedCM not supported, since remote uses Firefox") class TestFedCM: @pytest.fixture(autouse=True) def setup(self, driver, webserver): @@ -51,11 +52,11 @@ def test_no_dialog_select_account(driver): def test_no_dialog_cancel(driver): with pytest.raises(NoAlertPresentException): - driver.dialog.cancel() + driver.dialog.dismiss() def test_no_dialog_click_continue(driver): with pytest.raises(NoAlertPresentException): - driver.dialog.click_continue() + driver.dialog.accept() def test_trigger_and_verify_dialog_title(self, driver): driver.execute_script("triggerFedCm();") @@ -89,7 +90,7 @@ def test_select_account(self, driver): def test_dialog_cancel(self, driver): driver.execute_script("triggerFedCm();") dialog = driver.fedcm_dialog() - dialog.cancel() + dialog.dismiss() with pytest.raises(NoAlertPresentException): dialog.title @@ -124,11 +125,11 @@ def test_fedcm_no_select_account_present(self, driver): def test_fedcm_no_cancel_dialog_present(self, driver): with pytest.raises(NoAlertPresentException): - driver.fedcm.cancel_dialog() + driver.fedcm.dismiss() def test_fedcm_no_click_continue_present(self, driver): with pytest.raises(NoAlertPresentException): - driver.fedcm.click_continue() + driver.fedcm.accept() def test_verify_dialog_type_after_cooldown_reset(self, driver): driver.reset_fedcm_cooldown() diff --git a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py index 4f18cebc7fe1f..30224b723a83a 100644 --- a/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py +++ b/py/test/unit/selenium/webdriver/common/fedcm/dialog_tests.py @@ -41,13 +41,13 @@ def dialog(mock_driver, fedcm): def test_click_continue(dialog, fedcm): - dialog.click_continue() - fedcm.click_continue.assert_called_once() + dialog.accept() + fedcm.accept.assert_called_once() def test_cancel(dialog, fedcm): - dialog.cancel() - fedcm.cancel_dialog.assert_called_once() + dialog.dismiss() + fedcm.dismiss.assert_called_once() def test_select_account(dialog, fedcm): From 46302f3ace0e97fcb63bab7ea59bae8f140fbe78 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 9 Nov 2024 22:41:48 +0530 Subject: [PATCH 15/15] update docs and remove duplicate methods --- py/selenium/webdriver/remote/webdriver.py | 39 +------------------ .../selenium/webdriver/common/fedcm_tests.py | 4 +- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 791affacfc806..c2dc89551d6ba 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -1241,8 +1241,8 @@ def fedcm(self) -> FedCM: dialog_type = driver.fedcm.dialog_type accounts = driver.fedcm.account_list driver.fedcm.select_account(0) - driver.fedcm.click_continue() - driver.fedcm.cancel_dialog() + driver.fedcm.accept() + driver.fedcm.dismiss() driver.fedcm.enable_delay() driver.fedcm.disable_delay() driver.fedcm.reset_cooldown() @@ -1268,41 +1268,6 @@ def dialog(self): self._require_fedcm_support() return Dialog(self) - def enable_fedcm_delay(self): - """Enables the promise rejection delay for FedCM. - - Raises: - WebDriverException if FedCM not supported - """ - self._require_fedcm_support() - self.fedcm.enable_delay() - - def disable_fedcm_delay(self): - """Disables the promise rejection delay for FedCM. - - FedCM by default delays promise resolution in failure cases for - privacy reasons. This method allows turning it off to let tests - run faster where this is not relevant. - - Raises: - WebDriverException if FedCM not supported - """ - self._require_fedcm_support() - self.fedcm.disable_delay() - - def reset_fedcm_cooldown(self): - """Resets the FedCM dialog cooldown. - - If a user agent triggers a cooldown when the account chooser is - dismissed, this method resets that cooldown so that the dialog - can be triggered again immediately. - - Raises: - WebDriverException if FedCM not supported - """ - self._require_fedcm_support() - self.fedcm.reset_cooldown() - def fedcm_dialog(self, timeout=5, poll_frequency=0.5, ignored_exceptions=None): """Waits for and returns the FedCM dialog. diff --git a/py/test/selenium/webdriver/common/fedcm_tests.py b/py/test/selenium/webdriver/common/fedcm_tests.py index 53c3d3c7f5d6c..868fdd991e27e 100644 --- a/py/test/selenium/webdriver/common/fedcm_tests.py +++ b/py/test/selenium/webdriver/common/fedcm_tests.py @@ -101,7 +101,7 @@ def test_disable_fedcm_delay(self, driver): driver.fedcm.disable_delay() def test_fedcm_cooldown_reset(self, driver): - driver.reset_fedcm_cooldown() + driver.fedcm.reset_cooldown() def test_fedcm_no_dialog_type_present(self, driver): with pytest.raises(NoAlertPresentException): @@ -132,7 +132,7 @@ def test_fedcm_no_click_continue_present(self, driver): driver.fedcm.accept() def test_verify_dialog_type_after_cooldown_reset(self, driver): - driver.reset_fedcm_cooldown() + driver.fedcm.reset_cooldown() driver.execute_script("triggerFedCm();") dialog = driver.fedcm_dialog() assert dialog.type == "AccountChooser"