diff --git a/atest/acceptance/keywords/textfields.robot b/atest/acceptance/keywords/textfields.robot index b8f225e1a..b957b1f97 100644 --- a/atest/acceptance/keywords/textfields.robot +++ b/atest/acceptance/keywords/textfields.robot @@ -77,6 +77,42 @@ Press Key Attempt Clear Element Text On Non-Editable Field Run Keyword And Expect Error * Clear Element Text can_send_email +Input Password Accepts Secret Type + [Tags] require-rf-7.4 + [Setup] Go To Page "forms/login.html" + Set Environment Variable TEST_PASSWORD s3cret-pass + VAR ${pw: Secret} %{TEST_PASSWORD} + Input Text username_field my_username + Input Password password_field ${pw} + ${value}= Get Value password_field + Should Be Equal ${value} s3cret-pass + +Input Text Accepts Secret Type + [Tags] require-rf-7.4 + [Setup] Go To Page "forms/login.html" + Set Environment Variable TEST_USERNAME my_username + VAR ${user: Secret} %{TEST_USERNAME} + Input Text username_field ${user} + ${value}= Get Value username_field + Should Be Equal ${value} my_username + +Input Password With Plain String Still Works + [Setup] Go To Page "forms/login.html" + [Documentation] Backwards compatibility — plain str must still be accepted. + Input Text username_field my_username + Input Password password_field plain-pass + ${value}= Get Value password_field + Should Be Equal ${value} plain-pass + +Input Password Does Not Log Secret Value + [Tags] require-rf-7.4 NoGrid + [Setup] Go To Page "forms/login.html" + [Documentation] + ... LOG 3:1 INFO Typing password into text field 'password_field'. + Set Environment Variable TEST_PASSWORD must-not-leak + VAR ${pw: Secret} %{TEST_PASSWORD} + Input Password password_field ${pw} + *** Keywords *** Open Browser To Start Page Disabling Chrome Leaked Password Detection @@ -87,4 +123,4 @@ Open Browser To Start Page Disabling Chrome Leaked Password Detection ... options=add_experimental_option("prefs", {"profile.password_manager_leak_detection": False}) alias=${alias} ELSE Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} alias=${alias} - END \ No newline at end of file + END diff --git a/atest/run.py b/atest/run.py index 637e86547..b0e2de2ba 100755 --- a/atest/run.py +++ b/atest/run.py @@ -50,6 +50,7 @@ import subprocess import tempfile +from packaging.version import Version from robot import rebot_cli from robot import __version__ as robot_version from selenium import __version__ as selenium_version @@ -223,6 +224,8 @@ def execute_tests(interpreter, browser, rf_options, grid, event_firing, port): "--exclude", "triage", ] + if Version(robot_version) < Version("7.4"): + options.extend(["--exclude", "require-rf-7.4"]) command = runner if grid: command += [ diff --git a/src/SeleniumLibrary/keywords/formelement.py b/src/SeleniumLibrary/keywords/formelement.py index 7bfb04a90..2706a164d 100644 --- a/src/SeleniumLibrary/keywords/formelement.py +++ b/src/SeleniumLibrary/keywords/formelement.py @@ -20,7 +20,7 @@ from SeleniumLibrary.base import LibraryComponent, keyword from SeleniumLibrary.errors import ElementNotFound -from SeleniumLibrary.utils.types import Locator +from SeleniumLibrary.utils.types import Locator, Secret class FormElementKeywords(LibraryComponent): @@ -238,7 +238,9 @@ def choose_file(self, locator: Locator, file_path: str): self.ctx._running_keyword = None @keyword - def input_password(self, locator: Locator, password: str, clear: bool = True): + def input_password( + self, locator: Locator, password: str | Secret, clear: bool = True + ): """Types the given password into the text field identified by ``locator``. See the `Locating elements` section for details about the locator @@ -256,8 +258,15 @@ def input_password(self, locator: Locator, password: str, clear: bool = True): | Input Password | password_field | ${PASSWORD} | Please notice that Robot Framework logs all arguments using - the TRACE level and tests must not be executed using level below - DEBUG if the password should not be logged in any format. + the TRACE level. When not using the ``Secret`` type, tests must + not be executed using level below DEBUG if the password should + not be logged in any format.`` + + This keyword supports Robot Framework 7.4 + [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#secret-variables|Secret] + variable type. When a ``Secret`` is passed, the value is protected + from Robot Framework logs and Selenium internal logging is also + suppressed during typing. The `clear` argument is new in SeleniumLibrary 4.0. Hiding password logging from Selenium logs is new in SeleniumLibrary 4.2. @@ -266,7 +275,7 @@ def input_password(self, locator: Locator, password: str, clear: bool = True): self._input_text_into_text_field(locator, password, clear, disable_log=True) @keyword - def input_text(self, locator: Locator, text: str, clear: bool = True): + def input_text(self, locator: Locator, text: str | Secret, clear: bool = True): """Types the given ``text`` into the text field identified by ``locator``. When ``clear`` is true, the input element is cleared before @@ -274,6 +283,12 @@ def input_text(self, locator: Locator, text: str, clear: bool = True): is not cleared from the element. Use `Input Password` if you do not want the given ``text`` to be logged. + This keyword supports Robot Framework 7.4 + [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#secret-variables|Secret] + variable type. When a ``Secret`` is passed, the value is masked in + Robot Framework logs. Note that unlike `Input Password`, Selenium's + internal logs are not suppressed during typing. + If [https://github.com/SeleniumHQ/selenium/wiki/Grid2|Selenium Grid] is used and the ``text`` argument points to a file in the file system, then this keyword prevents the Selenium to transfer the file to the @@ -502,7 +517,7 @@ def _input_text_into_text_field(self, locator, text, clear=True, disable_log=Fal self.info("Temporally setting log level to: NONE") previous_level = BuiltIn().set_log_level("NONE") try: - element.send_keys(text) + element.send_keys(text.value if isinstance(text, Secret) else text) finally: if disable_log: BuiltIn().set_log_level(previous_level) diff --git a/src/SeleniumLibrary/utils/types.py b/src/SeleniumLibrary/utils/types.py index 694a7b4f2..c2d9edc82 100644 --- a/src/SeleniumLibrary/utils/types.py +++ b/src/SeleniumLibrary/utils/types.py @@ -19,6 +19,29 @@ from robot.utils import is_falsy, is_truthy, timestr_to_secs # noqa from selenium.webdriver.remote.webelement import WebElement +try: + from robot.api.types import Secret +except ImportError: + # Secret was introduced in Robot Framework 7.4. On older versions we + # provide a minimal stand-in so that the type hint ``str | Secret`` and + # ``isinstance`` checks work without requiring an upgrade. + class Secret: # type: ignore[no-redef] + """Stand-in for ``robot.api.types.Secret`` on Robot Framework < 7.4. + + Exposes the same ``.value`` attribute and masked string representation + as the real class, so keyword code can treat both identically. + """ + + def __init__(self, value: str): + self.value = value + + def __str__(self) -> str: + return "" + + def __repr__(self) -> str: + return f"{type(self).__name__}(value=)" + + Locator: TypeAlias = WebElement | str | list["Locator"]