Skip to content

Commit

Permalink
Merge pull request #2243 from SFDO-Tooling/feature/robot_spring-21
Browse files Browse the repository at this point in the history
Feature/robot spring 21
  • Loading branch information
David Glick authored Dec 10, 2020
2 parents 269886b + 872d0ee commit 3d47754
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 270 deletions.
100 changes: 50 additions & 50 deletions .github/workflows/feature_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,56 +240,56 @@ jobs:
name: robot
path: robot/CumulusCI/results

# robot_ui_prerelease:
# name: "Robot: Winter '21"
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Set up Python 3.8
# uses: actions/setup-python@v2
# with:
# python-version: 3.8
# - name: pip cache
# uses: actions/cache@v1
# with:
# path: ~\AppData\Local\pip\Cache
# key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
# restore-keys: |
# ${{ runner.os }}-pip-
# - name: Install Python dependencies
# run: |
# python -m pip install -U pip
# pip install -r requirements_dev.txt
# - name: Install sfdx
# run: |
# mkdir sfdx
# wget -qO- https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz | tar xJ -C sfdx --strip-components 1
# ./sfdx/install
# echo $SFDX_HUB_KEY_BASE64 | base64 --decode > sfdx.key
# sfdx force:auth:jwt:grant --clientid $SFDX_CLIENT_ID --jwtkeyfile sfdx.key --username $SFDX_HUB_USERNAME --setdefaultdevhubusername -a hub
# env:
# SFDX_HUB_KEY_BASE64: ${{ secrets.SFDX_HUB_KEY_BASE64 }}
# SFDX_CLIENT_ID: ${{ secrets.SFDX_CLIENT_ID }}
# SFDX_HUB_USERNAME: ${{ secrets.SFDX_HUB_USERNAME }}
# - name: Run robot tests
# run: |
# coverage run --append $(which cci) task run robot \
# --org prerelease \
# -o suites cumulusci/robotframework/tests/salesforce \
# -o exclude no-browser \
# -o vars BROWSER:headlesschrome
# - name: Delete scratch org
# if: always()
# run: |
# cci org scratch_delete prerelease
# - name: Report coverage
# run: coveralls
# - name: Store robot results
# if: failure()
# uses: actions/upload-artifact@v1
# with:
# name: robot
# path: robot/CumulusCI/results
robot_ui_prerelease:
name: "Robot: Spring '21"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: pip cache
uses: actions/cache@v1
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install Python dependencies
run: |
python -m pip install -U pip
pip install -r requirements_dev.txt
- name: Install sfdx
run: |
mkdir sfdx
wget -qO- https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz | tar xJ -C sfdx --strip-components 1
./sfdx/install
echo $SFDX_HUB_KEY_BASE64 | base64 --decode > sfdx.key
sfdx force:auth:jwt:grant --clientid $SFDX_CLIENT_ID --jwtkeyfile sfdx.key --username $SFDX_HUB_USERNAME --setdefaultdevhubusername -a hub
env:
SFDX_HUB_KEY_BASE64: ${{ secrets.SFDX_HUB_KEY_BASE64 }}
SFDX_CLIENT_ID: ${{ secrets.SFDX_CLIENT_ID }}
SFDX_HUB_USERNAME: ${{ secrets.SFDX_HUB_USERNAME }}
- name: Run robot tests
run: |
coverage run --append $(which cci) task run robot \
--org prerelease \
-o suites cumulusci/robotframework/tests/salesforce \
-o exclude no-browser \
-o vars BROWSER:headlesschrome
- name: Delete scratch org
if: always()
run: |
cci org scratch_delete prerelease
- name: Report coverage
run: coveralls
- name: Store robot results
if: failure()
uses: actions/upload-artifact@v1
with:
name: robot
path: robot/CumulusCI/results

coveralls_done:
name: Finalize coveralls
Expand Down
28 changes: 17 additions & 11 deletions cumulusci/core/tests/test_salesforce_locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from robot.libraries.BuiltIn import RobotNotRunningError
from cumulusci.robotframework.Salesforce import Salesforce
from unittest import mock
from pathlib import Path


# FIXME: we shouldn't have to tweak these tests for every
Expand Down Expand Up @@ -36,33 +37,38 @@ def test_locators_outside_robot_context(self, builtin_mock):
# however, because we've mocked get_library_instance to throw an error,
# we expect the library to still be instantiated, but with the latest
# version of the locators.

sf = Salesforce()
expected = "cumulusci.robotframework.locators_50"

locator_folder = Path("./cumulusci/robotframework")
locator_modules = sorted(locator_folder.glob("locators_[0-9][0-9].py"))
expected = f"cumulusci.robotframework.{locator_modules[-1].stem}"

actual = sf.locators_module.__name__
message = "expected to load '{}', actually loaded '{}'".format(expected, actual)
self.assertEqual(expected, actual, message)

def test_locators_50(self):
"""Verify that locators_50 is a superset of the locators_49
def test_locators_51(self):
"""Verify that locators_51 is a superset of the locators_50
This test is far from perfect, but it should at least flag a
catastrophic error in how locators for a version that augments
the locators from previous versions.
Note: this test assumes that locators_49 doesn't delete any of the
keys from 49.
Note: this test assumes that locators_51 doesn't delete any of the
keys from 50.
"""
import cumulusci.robotframework.locators_49 as locators_49
import cumulusci.robotframework.locators_50 as locators_50
import cumulusci.robotframework.locators_51 as locators_51

keys_49 = set(locators_49.lex_locators)
keys_50 = set(locators_50.lex_locators)
keys_51 = set(locators_51.lex_locators)

self.assertNotEqual(
id(locators_49.lex_locators),
id(locators_50.lex_locators),
"locators_49.lex_locators and locators_50.lex_locators are the same object",
id(locators_51.lex_locators),
"locators_50.lex_locators and locators_51.lex_locators are the same object",
)
self.assertTrue(len(keys_49) > 0)
self.assertTrue(keys_49.issubset(keys_50))
self.assertTrue(len(keys_50) > 0)
self.assertTrue(keys_50.issubset(keys_51))
30 changes: 25 additions & 5 deletions cumulusci/robotframework/Salesforce.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def _init_locators(self):
"""
try:
version = int(float(self.get_latest_api_version()))
self.builtin.set_suite_metadata("Salesforce API Version", version)
except RobotNotRunningError:
# Likely this means we are running in the context of
# documentation generation. Setting the version to
Expand Down Expand Up @@ -377,12 +376,32 @@ def get_current_record_id(self):
return oid_match.group(2)
raise AssertionError("Could not parse record id from url: {}".format(url))

def field_value_should_be(self, label, expected_value):
"""Verify that the form field for the given label is the expected value
Example:
| Field value should be Account Name ACME Labs
"""
value = self.get_field_value(label)
self.builtin.should_be_equal(value, expected_value)

def get_field_value(self, label):
"""Return the current value of a form field based on the field label"""
input_element_id = self.selenium.get_element_attribute(
"xpath://label[contains(., '{}')]".format(label), "for"
)
value = self.selenium.get_value(input_element_id)
api_version = int(float(self.get_latest_api_version()))

locator = self._get_input_field_locator(label)
if api_version >= 51:
# this works for both First Name (input) and Account Name (picklist)
value = self.selenium.get_value(locator)
else:
# older releases it's a bit more complex
element = self.selenium.get_webelement(locator)
if element.get_attribute("role") == "combobox":
value = self.selenium.get_text(f"sf:object.field_lookup_value:{label}")
else:
value = self.selenium.get_value(f"sf:object.field:{label}")

return value

def get_locator(self, path, *args, **kwargs):
Expand Down Expand Up @@ -508,6 +527,7 @@ def open_app_launcher(self, retry=True):
"""

self._jsclick("sf:app_launcher.button")
self.selenium.wait_until_element_is_visible("sf:app_launcher.view_all")
self._jsclick("sf:app_launcher.view_all")
self.wait_until_modal_is_open()
try:
Expand Down
19 changes: 19 additions & 0 deletions cumulusci/robotframework/locators_51.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from cumulusci.robotframework import locators_50
import copy

lex_locators = copy.deepcopy(locators_50.lex_locators)

lex_locators["modal"][
"button"
] = "//div[contains(@class,'uiModal')]//force-form-footer//button[.='{}']"

lex_locators["modal"]["has_error"] = "css: div.forceFormPageError"

lex_locators["modal"][
"review_alert"
] = '//a[@force-recordediterror_recordediterror and text()="{}"]'

lex_locators["modal"]["field_alert"] = "//div[contains(@class, 'forceFormPageError')]"

# I like the new markup I'm seeing in Spring '21!
lex_locators["object"]["field"] = "//lightning-input[.//label[text()='{}']]//input"
78 changes: 57 additions & 21 deletions cumulusci/robotframework/pageobjects/BasePageObjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,7 @@ def close_the_modal(self):
def click_modal_button(self, button_label):
"""Click the named modal button (Save, Save & New, Cancel, etc)"""
# stolen from Salesforce.py:click_modal_button
locator = (
"//div[contains(@class,'uiModal')]"
"//div[contains(@class,'modal-footer') or contains(@class, 'actionsContainer')]"
"//button[.//span[text()='{}']]"
).format(button_label)

locator = f"sf:modal.button:{button_label}"
self.selenium.wait_until_page_contains_element(locator)
self.selenium.wait_until_element_is_enabled(locator)
self.salesforce._jsclick(locator)
Expand All @@ -103,6 +98,23 @@ def modal_should_contain_errors(self, *messages):
'The page did not contain an error with the text "{}"'.format(message),
)

@capture_screenshot_on_error
def modal_should_show_edit_error_for_fields(self, *field_names):
"""Verify that a dialog is showing requiring a review of the given fields
This works by looking for a the "we hit a snag" popup, and
then looking for an anchor tag with the attribute
force-recordediterror_recordediterror, along with the given
text.
"""
self.selenium.wait_until_page_contains_element("sf:modal.field_alert")
for field_name in field_names:
locator = f"sf:modal.review_alert:{field_name}"
self.selenium.page_should_contain_element(
locator, f"Unable to find alert with field '{field_name}'"
)

@capture_screenshot_on_error
def populate_field(self, name, value):
"""Populate a field on the modal form
Expand Down Expand Up @@ -157,22 +169,34 @@ def select_dropdown_value(self, label, value):
self.selenium.driver.execute_script(
"arguments[0].scrollIntoView()", input_element
)
trigger = input_element.find_element_by_class_name("uiPopupTrigger")
trigger.click()

try:
popup_locator = (
"//div[contains(@class, 'uiPopupTarget')][contains(@class, 'visible')]"
)
popup = self.selenium.get_webelement(popup_locator)
except ElementNotFound:
raise ElementNotFound("Timed out waiting for the dropdown menu")

try:
value_element = popup.find_element_by_link_text(value)
value_element.click()
except NoSuchElementException:
raise Exception(f"Dropdown value '{value}' not found")
if input_element.get_attribute("role") == "combobox":
# new lightning combobox. So much easier!
self.builtin.log(f"New style combo for {label}", "DEBUG")
input_element.click()
try:
item = input_element.find_element_by_xpath(
f'//lightning-base-combobox-item[.="{value}"]'
)
item.click()
except NoSuchElementException:
raise Exception(f"Dropdown value '{value}' not found")
else:
self.builtin.log(f"Old style combo for {label}", "DEBUG")
trigger = input_element.find_element_by_class_name("uiPopupTrigger")
trigger.click()

try:
popup_locator = "//div[contains(@class, 'uiPopupTarget')][contains(@class, 'visible')]"
popup = self.selenium.get_webelement(popup_locator)
except ElementNotFound:
raise ElementNotFound("Timed out waiting for the dropdown menu")

try:
value_element = popup.find_element_by_link_text(value)
value_element.click()
except NoSuchElementException:
raise Exception(f"Dropdown value '{value}' not found")

@capture_screenshot_on_error
def wait_until_modal_is_closed(self, timeout=None):
Expand Down Expand Up @@ -207,6 +231,18 @@ def _get_form_element_for_label(self, label):
if modals:
root = modals[0]
try:
# As salesforce evolves, more and more fields use <label for='some-id'>
# where "for" points to the associated input field. w00t! If we can,
# use that. If not, fall back to an older strategy
try:
label_element = root.find_element_by_xpath(f'//label[text()="{label}"]')
input_id = label_element.get_attribute("for")
element = root.find_element_by_id(input_id)
return element

except NoSuchElementException:
pass

# gnarly, but effective.
# this finds an element with the class slds-form-element which
# has a child element with the class 'form-element__label' and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,21 @@ NewModal - click modal button
Click modal button Cancel
Wait until modal is closed

NewModal - Modal should contain errors
NewModal - Modal errors
[Documentation]
... Verify that we can use the NewModal 'modal should contain errors' keyword
... Verify that we can detect errors in the model
... with 'modal should contain errors' keyword (API < 51)
... or 'Modal should show edit error for fields' (API >= 51)
[Setup] Run keywords
... Go to page Home Contact
... AND Click object button New
... AND Wait for modal New Contact

Click modal button Save
Modal should contain errors
... These required fields must be completed: Last Name
capture page screenshot
${api}= Get latest API version
Run keyword if int(float($api)) >= 51
... Modal should show edit error for fields Name
... ELSE
... Modal should contain errors These required fields must be completed: Last Name
Loading

0 comments on commit 3d47754

Please sign in to comment.