Skip to content

Commit 42e0569

Browse files
authored
Chore: e2e mobile - add browserstack tests (#19111)
* refactor(e2e_appium): locators and app lifecycle management - wallet locators - app lifecycle management - toast message handling (visibility checks and logging). - saved addresses page * refactor(e2e_appium): migrate to cloud reporting and enhance test assertions - Replaced instances of LambdaTest reporting with Cloud reporting - Assertion messages for clarity - Updated test methods for toast handling
1 parent c016a5d commit 42e0569

14 files changed

+416
-223
lines changed

test/e2e_appium/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ report/
4646
reports/
4747
screenshots/
4848
logs/
49+
log/
4950
*.log
5051
result.xml
5152
junit.xml

test/e2e_appium/locators/onboarding/wallet/wallet_locators.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
from ...base_locators import BaseLocators
22

3-
class WalletLocators(BaseLocators):
43

4+
class WalletLocators(BaseLocators):
55
WALLET_HEADER = BaseLocators.content_desc_contains("walletHeader")
66
WALLET_FOOTER_SEND_BUTTON = BaseLocators.xpath(
77
"//*[contains(@resource-id, 'walletFooterSendButton')]"
88
)
99
ASSETS_TAB = BaseLocators.text_contains("Assets")
1010
ACTIVITY_TAB = BaseLocators.text_contains("Activity")
11-
11+
1212
ACCOUNT_NAME_ANY = BaseLocators.xpath(
1313
"//*[contains(@resource-id, 'Account') or contains(@text, 'Account')]"
1414
)
1515
BALANCE_ANY = BaseLocators.xpath(
1616
"//*[contains(@text, 'ETH') or contains(@text, 'USD')]"
1717
)
1818

19-
SAVED_ADDRESSES_BUTTON = BaseLocators.xpath(
20-
"//*[contains(@resource-id, 'savedAddressesBtn') or @content-desc='Saved addresses']"
19+
SAVED_ADDRESSES_BUTTON = BaseLocators.content_desc_contains(
20+
"[tid:savedAddressesBtn]"
2121
)
2222
ADD_NEW_ADDRESS_BUTTON = BaseLocators.xpath(
2323
"//*[contains(@resource-id, 'walletHeaderButton') or @content-desc='Add new address']"
2424
)
25-
WALLET_HEADER_ADDRESS = BaseLocators.content_desc_contains("[tid:walletHeaderButton]")
25+
WALLET_HEADER_ADDRESS = BaseLocators.content_desc_contains(
26+
"[tid:walletHeaderButton]"
27+
)
2628

2729
# Account selection
2830
ACCOUNT_1_BY_TEXT = BaseLocators.xpath(

test/e2e_appium/locators/wallet/saved_addresses_locators.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,36 @@
22

33

44
class SavedAddressesLocators(BaseLocators):
5-
WALLET_SAVED_ADDRESSES_BUTTON = BaseLocators.xpath("//android.view.View.VirtualChild[@content-desc=\"Saved addresses [tid:savedAddressesBtn]\"]")
5+
WALLET_SAVED_ADDRESSES_BUTTON = BaseLocators.content_desc_contains(
6+
"[tid:savedAddressesBtn]"
7+
)
68
SETTINGS_WALLET_MENU_ITEM = BaseLocators.content_desc_contains("[tid:5-MenuItem]")
79
SAVED_ADDRESSES_ITEM = BaseLocators.xpath(
810
"//*[contains(@resource-id, 'savedAddressesItem') or contains(@content-desc, 'Saved Addresses')]"
911
)
1012
ADD_NEW_SAVED_ADDRESS_BUTTON_SETTINGS = BaseLocators.xpath(
11-
"//android.view.View.VirtualChild[@content-desc=\"Add new address [tid:addNewSavedAddressButton]\"]"
13+
'//android.view.View.VirtualChild[@content-desc="Add new address [tid:addNewSavedAddressButton]"]'
1214
)
1315
ADD_NEW_SAVED_ADDRESS_BUTTON_WALLET = BaseLocators.xpath(
14-
"//android.view.View.VirtualChild[@content-desc=\"Add new address [tid:walletHeaderButton]\"]"
16+
'//android.view.View.VirtualChild[@content-desc="Add new address [tid:walletHeaderButton]"]'
1517
)
1618
SAVED_ADDRESS_ITEM_ANY = BaseLocators.xpath(
17-
"//android.view.View.VirtualChild[@resource-id=\"savedAddressDelegate\"]"
19+
'//android.view.View.VirtualChild[@resource-id="savedAddressDelegate"]'
1820
)
1921
SAVED_ADDRESS_DETAILS_POPUP = BaseLocators.xpath(
2022
"//*[contains(@resource-id, 'SavedAddressActivityPopup')]"
2123
)
2224
POPUP_MENU_BUTTON_GENERIC = BaseLocators.xpath(
2325
"//*[contains(@resource-id,'SavedAddressActivityPopup')]//*[contains(@resource-id, 'savedAddressView_Delegate_menuButton_')]"
2426
)
25-
POPUP_MENU_BUTTON_TID = BaseLocators.content_desc_contains("tid:savedAddressMenuButton")
27+
POPUP_MENU_BUTTON_TID = BaseLocators.content_desc_contains(
28+
"tid:savedAddressMenuButton"
29+
)
30+
2631
@staticmethod
2732
def row_by_name(name: str) -> tuple:
2833
return BaseLocators.xpath(
29-
"//android.view.View.VirtualChild[@resource-id=\"savedAddressDelegate\"]"
34+
'//android.view.View.VirtualChild[@resource-id="savedAddressDelegate"]'
3035
+ f"//*[contains(@resource-id, 'savedAddressView_Delegate_{name}')]"
3136
)
3237

@@ -47,21 +52,19 @@ def popup_menu_by_name(name: str) -> tuple:
4752
+ f"contains(@resource-id, 'savedAddressView_Delegate_menuButton_{name}')"
4853
+ "]"
4954
)
55+
5056
NAME_INPUT = BaseLocators.xpath(
51-
"//android.view.View.VirtualChild[@content-desc=\"Address name [tid:statusBaseInput]\"]"
57+
'//android.view.View.VirtualChild[@content-desc="Address name [tid:statusBaseInput]"]'
5258
)
5359
ADDRESS_INPUT = BaseLocators.xpath(
54-
"//android.view.View.VirtualChild[@content-desc=\"Ethereum address [tid:statusBaseInput]\"]"
60+
'//android.view.View.VirtualChild[@content-desc="Ethereum address [tid:statusBaseInput]"]'
5561
)
5662
SAVE_BUTTON = BaseLocators.xpath(
57-
"//android.view.View.VirtualChild[@content-desc=\"Add address [tid:addSavedAddress]\"]"
63+
'//android.view.View.VirtualChild[@content-desc="Add address [tid:addSavedAddress]"]'
5864
)
5965
DELETE_SAVED_ADDRESS_ACTION = BaseLocators.xpath(
6066
"//*[@resource-id and contains(@resource-id, 'deleteSavedAddress') or @content-desc='Remove saved address']"
6167
)
6268
CONFIRM_DELETE_BUTTON = BaseLocators.xpath(
6369
"//*[@resource-id and contains(@resource-id, 'RemoveSavedAddressPopup-ConfirmButton')]"
6470
)
65-
66-
67-

test/e2e_appium/pages/app.py

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99

1010
class App(BasePage):
11-
1211
def __init__(self, driver):
1312
super().__init__(driver)
1413
self.locators = AppLocators()
@@ -98,34 +97,68 @@ def click_settings(self) -> bool:
9897
return self.navigate_to("settings", timeout=4, max_attempts=2)
9998

10099
def click_settings_left_nav(self) -> bool:
101-
return self.safe_click(self.locators.LEFT_NAV_SETTINGS, timeout=4, max_attempts=2)
100+
return self.safe_click(
101+
self.locators.LEFT_NAV_SETTINGS, timeout=4, max_attempts=2
102+
)
103+
104+
def wait_for_toast(
105+
self,
106+
expected_substring: Optional[str] = None,
107+
timeout: float = 6.0,
108+
poll_interval: float = 0.2,
109+
stability: float = 0.0,
110+
) -> Optional[str]:
111+
"""Poll for a toast message and optionally match its content."""
112+
113+
deadline = time.time() + (timeout or 0)
114+
last_seen: Optional[str] = None
115+
116+
while time.time() < deadline:
117+
remaining = max(deadline - time.time(), 0.3)
118+
desc = self.get_toast_content_desc(timeout=remaining)
119+
if desc:
120+
last_seen = desc
121+
matches = (
122+
not expected_substring or expected_substring.lower() in desc.lower()
123+
)
124+
if matches:
125+
if stability > 0:
126+
stable_until = time.time() + stability
127+
while time.time() < stable_until:
128+
if not self.is_element_visible(
129+
self.locators.ANY_TOAST, timeout=0.1
130+
):
131+
break
132+
time.sleep(0.05)
133+
else:
134+
self.logger.info(f"Toast detected text='{desc}'")
135+
try:
136+
save_page_source(
137+
self.driver, self._screenshots_dir, "toast"
138+
)
139+
except Exception as e:
140+
self.logger.debug(f"Toast page source save failed: {e}")
141+
return desc
142+
else:
143+
self.logger.info(f"Toast detected text='{desc}'")
144+
try:
145+
save_page_source(
146+
self.driver, self._screenshots_dir, "toast"
147+
)
148+
except Exception as e:
149+
self.logger.debug(f"Toast page source save failed: {e}")
150+
return desc
151+
152+
time.sleep(min(poll_interval, max(deadline - time.time(), 0.1)))
153+
154+
if last_seen:
155+
self.logger.debug(
156+
"Toast detected but did not match expectation: '%s'", last_seen
157+
)
158+
return None
102159

103160
def is_toast_present(self, timeout: Optional[int] = 3) -> bool:
104-
present = self.is_element_visible(self.locators.ANY_TOAST, timeout=timeout)
105-
if not present:
106-
return False
107-
108-
try:
109-
el = self.find_element_safe(self.locators.ANY_TOAST, timeout=1)
110-
if el is not None:
111-
text_value = ElementStateChecker.get_text_content(el)
112-
try:
113-
desc_value = el.get_attribute("content-desc") or ""
114-
except Exception:
115-
desc_value = ""
116-
if text_value or desc_value:
117-
self.logger.info(
118-
f"Toast detected text='{text_value}' content-desc='{desc_value}'"
119-
)
120-
except Exception as e:
121-
self.logger.debug(f"Toast attribute read failed: {e}")
122-
123-
try:
124-
_ = save_page_source(self.driver, self._screenshots_dir, "toast")
125-
except Exception as e:
126-
self.logger.debug(f"Toast page source save failed: {e}")
127-
128-
return True
161+
return self.wait_for_toast(timeout=timeout or 3.0) is not None
129162

130163
def get_toast_content_desc(self, timeout: Optional[int] = 3) -> Optional[str]:
131164
"""Return toast's content-desc, polling until non-empty or timeout."""

test/e2e_appium/pages/base_page.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def __init__(self, driver):
2323
self.gestures = Gestures(driver)
2424
self.app_lifecycle = AppLifecycleManager(driver)
2525
self.keyboard = KeyboardManager(driver)
26-
env_name = os.getenv("CURRENT_TEST_ENVIRONMENT", "lambdatest")
26+
env_name = os.getenv("CURRENT_TEST_ENVIRONMENT", "browserstack")
2727

2828
try:
2929
switcher = EnvironmentSwitcher()
@@ -146,13 +146,20 @@ def safe_click(
146146
attempts = 0
147147
while attempts < max_attempts:
148148
attempts += 1
149+
element = None
149150
try:
150151
wait = self._create_wait(timeout, "element_click")
151152
element = wait.until(EC.element_to_be_clickable(loc))
152153
element.click()
153154
log_element_action("click_element", f"{loc[0]}: {loc[1]}", True, 0)
154155
return True
155156
except Exception as e:
157+
if element is not None and self._gesture_tap_fallback(element, loc):
158+
log_element_action(
159+
"click_element", f"{loc[0]}: {loc[1]}", True, 0
160+
)
161+
return True
162+
156163
self.logger.debug(f"Click attempt {attempts} failed for {loc}: {e}")
157164
if attempts >= max_attempts:
158165
break
@@ -359,13 +366,33 @@ def tap_coordinate_relative(self, element, x_offset: int, y_offset: int) -> bool
359366
self.logger.debug(f"Coordinate tap failed: {e}")
360367
return False
361368

362-
def restart_app(self, app_package: str = "im.status.app") -> bool:
369+
def _gesture_tap_fallback(self, element, locator) -> bool:
370+
"""Fallback tap using Appium gestures when native click fails."""
371+
try:
372+
if self.gestures.element_tap(element):
373+
self.logger.debug(f"Gesture tap fallback succeeded for {locator}")
374+
return True
375+
except Exception as err:
376+
self.logger.debug(f"Gesture tap fallback error for {locator}: {err}")
377+
378+
try:
379+
rect = element.rect
380+
center_x = int(rect["x"] + rect["width"] / 2)
381+
center_y = int(rect["y"] + rect["height"] / 2)
382+
if self.gestures.tap(center_x, center_y):
383+
self.logger.debug(
384+
f"Coordinate tap fallback succeeded for {locator} at ({center_x}, {center_y})"
385+
)
386+
return True
387+
except Exception as err:
388+
self.logger.debug(f"Coordinate fallback error for {locator}: {err}")
389+
return False
390+
391+
def restart_app(self, app_package: Optional[str] = None) -> bool:
363392
"""Restart the app within the current session."""
364393
return self.app_lifecycle.restart_app(app_package)
365394

366-
def restart_app_with_data_cleared(
367-
self, app_package: str = "im.status.app"
368-
) -> bool:
395+
def restart_app_with_data_cleared(self, app_package: Optional[str] = None) -> bool:
369396
"""Restart the app with all app data cleared (fresh app state)."""
370397
return self.app_lifecycle.restart_app_with_data_cleared(app_package)
371398

@@ -385,8 +412,8 @@ def wait_for_condition(
385412
return False
386413

387414
def _wait_between_attempts(self, base_delay: float = 0.5) -> None:
388-
env_name = os.getenv("CURRENT_TEST_ENVIRONMENT", "lambdatest").lower()
389-
if env_name in ("lt", "lambdatest"):
415+
env_name = os.getenv("CURRENT_TEST_ENVIRONMENT", "browserstack").lower()
416+
if env_name in ("browserstack",):
390417
time.sleep(base_delay * 1.5)
391418
else:
392419
time.sleep(base_delay * 0.5)

test/e2e_appium/pages/settings/change_password_modal.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ def complete_reencrypt_and_restart(self, timeout: int = 90) -> bool:
3434
restart_confirmed = False
3535

3636
while time.time() < deadline:
37-
modal_present = self.find_element_safe(self.locators.MODAL_CONTAINER, timeout=1)
37+
modal_present = self.find_element_safe(
38+
self.locators.MODAL_CONTAINER, timeout=1
39+
)
3840
if not modal_present:
3941
restart_confirmed = True
4042
break
@@ -67,8 +69,16 @@ def complete_reencrypt_and_restart(self, timeout: int = 90) -> bool:
6769

6870
try:
6971
self.app_lifecycle.activate_app()
70-
except Exception:
71-
pass
72+
except Exception as err:
73+
self.logger.debug("App activation after password change failed: %s", err)
74+
75+
try:
76+
from services.app_state_manager import AppStateManager
77+
78+
if not AppStateManager(self.driver).wait_for_app_ready(timeout=45):
79+
self.logger.debug("App state manager did not confirm readiness in time")
80+
except Exception as err:
81+
self.logger.debug("App readiness wait failed: %s", err)
7282
return True
7383

7484
def _wait_for_primary_button_enabled(self, timeout: int = 10) -> bool:

0 commit comments

Comments
 (0)