From eac6dd1de1ed9a3c493b3f0c2efee42434786b15 Mon Sep 17 00:00:00 2001 From: vamsi Date: Sat, 23 May 2026 10:51:04 -0400 Subject: [PATCH 1/3] Add new keyword to get Lightning picklist values --- AUTHORS.rst | 1 + cumulusci/robotframework/Salesforce.py | 66 +++++++++++++++++++ .../tests/salesforce/picklist.robot | 63 ++++++++++++++++++ .../robotframework/tests/test_salesforce.py | 66 +++++++++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 cumulusci/robotframework/tests/salesforce/picklist.robot diff --git a/AUTHORS.rst b/AUTHORS.rst index 5f3a5f3253..91bdec75af 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -40,3 +40,4 @@ For example: * Ben French (BenjaminFrench) * Rupert Barrow (rupertbarrow) * Rupesh J (rupeshjSFDC) +* Bhimeswara Vamsi Punnam (b-vamsipunnam) diff --git a/cumulusci/robotframework/Salesforce.py b/cumulusci/robotframework/Salesforce.py index 99eebdb0d0..925995c588 100644 --- a/cumulusci/robotframework/Salesforce.py +++ b/cumulusci/robotframework/Salesforce.py @@ -934,3 +934,69 @@ def select_window(self, locator="MAIN", timeout=None): "'Select Window' is deprecated; use 'Switch Window' instead", "WARN" ) self.selenium.switch_window(locator=locator, timeout=timeout) + + @capture_screenshot_on_error + def get_all_picklist_values(self, picklist, timeout="10s"): + """Return all available values from a Salesforce Lightning picklist. + + Opens the picklist identified by its visible field label, collects + all visible non-empty option values, removes duplicates, and returns + the values as a sorted list. + + Required field labels are supported. For example, both ``Status`` + and ``Status *`` can be matched. + + Examples: + | ${values}= | Get All Picklist Values | Status | + | ${values}= | Get All Picklist Values | Lead Status | timeout=15s | + """ + label_xpath = ( + f"//label[" + f"normalize-space(.)='{picklist}' " + f"or normalize-space(.)='{picklist} *' " + f"or normalize-space(.)='*{picklist}'" + f"]" + ) + + option_xpath = ( + f"{label_xpath}" + "/ancestor::*[contains(@class,'slds-form-element')]" + "//lightning-base-combobox-item" + "//span[contains(@class,'slds-media__body') or @slot='label']" + ) + + try: + self.selenium.set_focus_to_element(label_xpath) + self.scroll_element_into_view(label_xpath) + self.selenium.click_element(label_xpath) + self.selenium.wait_until_element_is_visible(option_xpath, timeout=timeout) + + values = [] + for element in self.selenium.get_webelements(option_xpath): + try: + text = element.text.strip() + if text: + values.append(text) + except (StaleElementReferenceException, WebDriverException): + continue + + values = sorted(set(values)) + self.builtin.log( + f"Retrieved {len(values)} values from picklist '{picklist}'.", + "INFO", + ) + return values + + except ElementNotFound: + raise AssertionError( + f"Picklist '{picklist}' was not found on the page." + ) from None + + finally: + try: + self.selenium.press_keys(None, "ESC") + except Exception as err: + self.builtin.log( + f"Failed to close picklist dropdown: {err}", + "DEBUG", + ) diff --git a/cumulusci/robotframework/tests/salesforce/picklist.robot b/cumulusci/robotframework/tests/salesforce/picklist.robot new file mode 100644 index 0000000000..84a681d29c --- /dev/null +++ b/cumulusci/robotframework/tests/salesforce/picklist.robot @@ -0,0 +1,63 @@ +*** Settings *** +Documentation +... This suite tests the `Get All Picklist Values` keyword. +... Specifically, it verifies that the keyword can open a Salesforce +... Lightning picklist and return the available option values. + +Library String +Library DateTime +Library Collections +Resource cumulusci/robotframework/Salesforce.robot + +Suite Setup run keywords +... Open test browser +... AND Create opportunity +Suite Teardown Delete Records and Close Browser + + +*** Keywords *** +Create opportunity + [Documentation] + ... Creates a test opportunity with a random name and a close + ... date 30 days in the future. A reference to the created + ... opportunity will be stored in the suite variable ${opportunity}. + + ${30 days from now}= Get Current Date increment=30 days result_format=%Y-%m-%d + ${random}= Generate random string 8 [NUMBERS] + ${opportunity_id}= Salesforce Insert Opportunity + ... Name=Picklist Test ${random} + ... CloseDate=${30 days from now} + ... Amount=100000 + ... Probability=42 + ... StageName=Prospecting + ... Description=Picklist keyword test + &{opportunity}= Salesforce Get Opportunity ${opportunity_id} + set suite variable ${opportunity} + [Return] ${opportunity} + + +*** Test Cases *** +Test Get All Picklist Values + [Documentation] + ... Verify that Get All Picklist Values returns available values + ... from a Lightning picklist field. + + [Setup] run keywords + ... go to object home Opportunity + ... AND click link ${opportunity['Name']} + ... AND click object button Edit + ... AND wait until modal is open + + ${values}= Get All Picklist Values Stage + Log ${values} + + Run keyword and continue on failure + ... Should Not Be Empty + ... ${values} + ... Expected Stage picklist values to be returned but received an empty list + + Run keyword and continue on failure + ... List Should Contain Value + ... ${values} + ... Prospecting + ... Expected Stage picklist to contain 'Prospecting' but values were '${values}' \ No newline at end of file diff --git a/cumulusci/robotframework/tests/test_salesforce.py b/cumulusci/robotframework/tests/test_salesforce.py index d18514c85e..67010c4fbb 100644 --- a/cumulusci/robotframework/tests/test_salesforce.py +++ b/cumulusci/robotframework/tests/test_salesforce.py @@ -4,6 +4,7 @@ from SeleniumLibrary.errors import ElementNotFound from cumulusci.robotframework.Salesforce import Salesforce +from selenium.common.exceptions import StaleElementReferenceException # _init_locators has a special code block @@ -77,3 +78,68 @@ def setup_class(cls): def test_breakpoint(self, mock_robot_context): """Verify that the keyword doesn't raise an exception""" assert self.sflib.breakpoint() is None + + +@mock.patch("robot.libraries.BuiltIn.BuiltIn._get_context") +class TestKeywordGetAllPicklistValues: + @classmethod + def setup_class(cls): + cls.sflib = Salesforce(locators={"body": "//whatever"}) + + def test_returns_sorted_unique_non_empty_values(self, mock_robot_context): + option_1 = mock.Mock(text="Warm") + option_2 = mock.Mock(text="Cold") + option_3 = mock.Mock(text="Warm") + option_4 = mock.Mock(text=" ") + + with mock.patch.object(self.sflib, "scroll_element_into_view"): + self.sflib.selenium.get_webelements.return_value = [ + option_1, + option_2, + option_3, + option_4, + ] + + values = self.sflib.get_all_picklist_values("Status") + + assert values == ["Cold", "Warm"] + self.sflib.selenium.wait_until_element_is_visible.assert_called_once() + self.sflib.selenium.press_keys.assert_any_call(None, "ESC") + + def test_ignores_stale_options(self, mock_robot_context): + good_option = mock.Mock(text="Active") + + stale_option = mock.Mock() + type(stale_option).text = mock.PropertyMock( + side_effect=StaleElementReferenceException() + ) + + with mock.patch.object(self.sflib, "scroll_element_into_view"): + self.sflib.selenium.get_webelements.return_value = [ + stale_option, + good_option, + ] + + values = self.sflib.get_all_picklist_values("Status") + + assert values == ["Active"] + + def test_raises_assertion_when_picklist_not_found(self, mock_robot_context): + self.sflib.selenium.set_focus_to_element.side_effect = ElementNotFound() + + with pytest.raises( + AssertionError, + match="Picklist 'Status' was not found on the page.", + ): + self.sflib.get_all_picklist_values("Status") + + def test_uses_custom_timeout(self, mock_robot_context): + option = mock.Mock(text="Active") + + with mock.patch.object(self.sflib, "scroll_element_into_view"): + self.sflib.selenium.get_webelements.return_value = [option] + + self.sflib.get_all_picklist_values("Status", timeout="15s") + + _, kwargs = self.sflib.selenium.wait_until_element_is_visible.call_args + assert kwargs["timeout"] == "15s" From 3826649c97d8167b4e44cda56c3fc2ea98487420 Mon Sep 17 00:00:00 2001 From: vamsi Date: Mon, 25 May 2026 11:08:34 -0400 Subject: [PATCH 2/3] Update picklist keyword behavior to return rendered values --- cumulusci/robotframework/Salesforce.py | 14 +++++--------- cumulusci/robotframework/tests/test_salesforce.py | 6 +++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/cumulusci/robotframework/Salesforce.py b/cumulusci/robotframework/Salesforce.py index 925995c588..0c410d701d 100644 --- a/cumulusci/robotframework/Salesforce.py +++ b/cumulusci/robotframework/Salesforce.py @@ -939,12 +939,11 @@ def select_window(self, locator="MAIN", timeout=None): def get_all_picklist_values(self, picklist, timeout="10s"): """Return all available values from a Salesforce Lightning picklist. - Opens the picklist identified by its visible field label, collects - all visible non-empty option values, removes duplicates, and returns - the values as a sorted list. + Opens the picklist identified by its visible field label and returns + the option values currently rendered in the Lightning picklist. - Required field labels are supported. For example, both ``Status`` - and ``Status *`` can be matched. + Required field labels are supported. For example, ``Status``, + ``Status *``, and ``*Status`` can be matched. Examples: | ${values}= | Get All Picklist Values | Status | @@ -974,13 +973,10 @@ def get_all_picklist_values(self, picklist, timeout="10s"): values = [] for element in self.selenium.get_webelements(option_xpath): try: - text = element.text.strip() - if text: - values.append(text) + values.append(element.text.strip()) except (StaleElementReferenceException, WebDriverException): continue - values = sorted(set(values)) self.builtin.log( f"Retrieved {len(values)} values from picklist '{picklist}'.", "INFO", diff --git a/cumulusci/robotframework/tests/test_salesforce.py b/cumulusci/robotframework/tests/test_salesforce.py index 67010c4fbb..7282d05a0d 100644 --- a/cumulusci/robotframework/tests/test_salesforce.py +++ b/cumulusci/robotframework/tests/test_salesforce.py @@ -86,7 +86,7 @@ class TestKeywordGetAllPicklistValues: def setup_class(cls): cls.sflib = Salesforce(locators={"body": "//whatever"}) - def test_returns_sorted_unique_non_empty_values(self, mock_robot_context): + def test_returns_rendered_picklist_values(self, mock_robot_context): option_1 = mock.Mock(text="Warm") option_2 = mock.Mock(text="Cold") option_3 = mock.Mock(text="Warm") @@ -102,7 +102,7 @@ def test_returns_sorted_unique_non_empty_values(self, mock_robot_context): values = self.sflib.get_all_picklist_values("Status") - assert values == ["Cold", "Warm"] + assert values == ["Warm", "Cold", "Warm", ""] self.sflib.selenium.wait_until_element_is_visible.assert_called_once() self.sflib.selenium.press_keys.assert_any_call(None, "ESC") @@ -142,4 +142,4 @@ def test_uses_custom_timeout(self, mock_robot_context): self.sflib.get_all_picklist_values("Status", timeout="15s") _, kwargs = self.sflib.selenium.wait_until_element_is_visible.call_args - assert kwargs["timeout"] == "15s" + assert kwargs["timeout"] == "15s" \ No newline at end of file From ecbcd611c4edd057ff8fb4c70149e08c7361efe6 Mon Sep 17 00:00:00 2001 From: vamsi Date: Wed, 27 May 2026 19:05:43 -0400 Subject: [PATCH 3/3] Address review feedback for the picklist keyword --- cumulusci/robotframework/Salesforce.py | 48 +++++++++---------- .../robotframework/tests/test_salesforce.py | 24 ++++++---- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/cumulusci/robotframework/Salesforce.py b/cumulusci/robotframework/Salesforce.py index 0c410d701d..0f0ae2c3b5 100644 --- a/cumulusci/robotframework/Salesforce.py +++ b/cumulusci/robotframework/Salesforce.py @@ -936,56 +936,52 @@ def select_window(self, locator="MAIN", timeout=None): self.selenium.switch_window(locator=locator, timeout=timeout) @capture_screenshot_on_error - def get_all_picklist_values(self, picklist, timeout="10s"): - """Return all available values from a Salesforce Lightning picklist. + def get_all_picklist_values(self, name, timeout="10s"): + """Return available non-empty values from a Salesforce Lightning picklist. Opens the picklist identified by its visible field label and returns - the option values currently rendered in the Lightning picklist. - - Required field labels are supported. For example, ``Status``, - ``Status *``, and ``*Status`` can be matched. + the non-empty option values currently rendered in the Lightning picklist. Examples: - | ${values}= | Get All Picklist Values | Status | + | ${values}= | Get All Picklist Values | Stage | | ${values}= | Get All Picklist Values | Lead Status | timeout=15s | """ - label_xpath = ( - f"//label[" - f"normalize-space(.)='{picklist}' " - f"or normalize-space(.)='{picklist} *' " - f"or normalize-space(.)='*{picklist}'" - f"]" - ) - + picklist_locator = f"label:{name}" option_xpath = ( - f"{label_xpath}" - "/ancestor::*[contains(@class,'slds-form-element')]" - "//lightning-base-combobox-item" + "xpath://lightning-base-combobox-item" "//span[contains(@class,'slds-media__body') or @slot='label']" ) try: - self.selenium.set_focus_to_element(label_xpath) - self.scroll_element_into_view(label_xpath) - self.selenium.click_element(label_xpath) - self.selenium.wait_until_element_is_visible(option_xpath, timeout=timeout) + self.selenium.set_focus_to_element(picklist_locator) + self.scroll_element_into_view(picklist_locator) + self.selenium.click_element(picklist_locator) + self.selenium.wait_until_element_is_visible( + option_xpath, + timeout=timeout, + ) values = [] for element in self.selenium.get_webelements(option_xpath): try: - values.append(element.text.strip()) - except (StaleElementReferenceException, WebDriverException): + text = element.text.strip() + if text: + values.append(text) + except ( + StaleElementReferenceException, + WebDriverException, + ): continue self.builtin.log( - f"Retrieved {len(values)} values from picklist '{picklist}'.", + f"Retrieved {len(values)} values from picklist '{name}'.", "INFO", ) return values except ElementNotFound: raise AssertionError( - f"Picklist '{picklist}' was not found on the page." + f"Picklist '{name}' was not found on the page." ) from None finally: diff --git a/cumulusci/robotframework/tests/test_salesforce.py b/cumulusci/robotframework/tests/test_salesforce.py index 7282d05a0d..fb9b53897e 100644 --- a/cumulusci/robotframework/tests/test_salesforce.py +++ b/cumulusci/robotframework/tests/test_salesforce.py @@ -86,7 +86,7 @@ class TestKeywordGetAllPicklistValues: def setup_class(cls): cls.sflib = Salesforce(locators={"body": "//whatever"}) - def test_returns_rendered_picklist_values(self, mock_robot_context): + def test_returns_rendered_non_empty_picklist_values(self, mock_robot_context): option_1 = mock.Mock(text="Warm") option_2 = mock.Mock(text="Cold") option_3 = mock.Mock(text="Warm") @@ -102,7 +102,10 @@ def test_returns_rendered_picklist_values(self, mock_robot_context): values = self.sflib.get_all_picklist_values("Status") - assert values == ["Warm", "Cold", "Warm", ""] + assert values == ["Warm", "Cold", "Warm"] + + self.sflib.selenium.set_focus_to_element.assert_called_once_with("label:Status") + self.sflib.selenium.click_element.assert_called_once_with("label:Status") self.sflib.selenium.wait_until_element_is_visible.assert_called_once() self.sflib.selenium.press_keys.assert_any_call(None, "ESC") @@ -125,13 +128,16 @@ def test_ignores_stale_options(self, mock_robot_context): assert values == ["Active"] def test_raises_assertion_when_picklist_not_found(self, mock_robot_context): - self.sflib.selenium.set_focus_to_element.side_effect = ElementNotFound() - - with pytest.raises( - AssertionError, - match="Picklist 'Status' was not found on the page.", + with mock.patch.object( + self.sflib.selenium, + "set_focus_to_element", + side_effect=ElementNotFound(), ): - self.sflib.get_all_picklist_values("Status") + with pytest.raises( + AssertionError, + match="Picklist 'Status' was not found on the page.", + ): + self.sflib.get_all_picklist_values("Status") def test_uses_custom_timeout(self, mock_robot_context): option = mock.Mock(text="Active") @@ -142,4 +148,4 @@ def test_uses_custom_timeout(self, mock_robot_context): self.sflib.get_all_picklist_values("Status", timeout="15s") _, kwargs = self.sflib.selenium.wait_until_element_is_visible.call_args - assert kwargs["timeout"] == "15s" \ No newline at end of file + assert kwargs["timeout"] == "15s"