Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions test/e2e_appium/config/environments/local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,31 @@ metadata:
device:
name: "sdk_gphone64_arm64"
platform_version: "15"
platform_name: "android"

devices:
- name: "sdk_gphone64_arm64"
platform_name: "android"
platform_version: "12L"
tags: ["emulator", "phone", "android"]
- name: "sdk_google_atv64_arm64"
platform_name: "android"
platform_version: "14"
tags: ["emulator", "android", "tv"]

appium:
server_url: "http://localhost:4723"

app:
# Prefer attaching to already-installed package when path not provided
source_type: "local_file"
path_template: "${LOCAL_APP_PATH}"
path_template: "${LOCAL_APP_PATH:-}"

timeouts:
default: 60
element_wait: 45
element_wait: 30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Use consistent timeout keys

Suggested change
element_wait: 30
element_wait_timeout: 30

element_find: 10
element_click: 10

logging:
level: "DEBUG"
Expand All @@ -32,4 +46,8 @@ capabilities:
platformName: "android"
automationName: "UiAutomator2"
newCommandTimeout: 300
noReset: false
noReset: false
appPackage: "im.status.tablet"
appActivity: "org.qtproject.qt.android.bindings.QtActivity"
unicodeKeyboard: true
resetKeyboard: true
46 changes: 46 additions & 0 deletions test/e2e_appium/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .config.logging_config import get_logger
from .core import EnvironmentSwitcher
from .utils.lambdatest_reporter import LambdaTestReporter
from .utils.screenshot import save_screenshot, save_page_source


# Expose fixture modules without star imports
Expand Down Expand Up @@ -161,6 +162,51 @@ def pytest_runtest_makereport(item, call):
logger = get_logger("session")
logger.error(f"Failed to report test result to LambdaTest: {e}")

# Get screenshot and page source artifacts
try:
if getattr(rep, "failed", False) and not getattr(item, "_failure_artifacts_saved", False):
driver = None
try:
if hasattr(item, "instance") and hasattr(item.instance, "driver"):
driver = item.instance.driver
except Exception:
driver = None

if driver:
# Resolve screenshots directory from environment config; fallback to 'screenshots'
try:
env_name = os.getenv("CURRENT_TEST_ENVIRONMENT", "lambdatest")
switcher = EnvironmentSwitcher()
env_config = switcher.switch_to(env_name)
screenshots_dir = env_config.directories.get("screenshots", "screenshots")
except Exception:
screenshots_dir = "screenshots"

test_id = getattr(item, "name", "test") + (f"__{rep.when}" if getattr(rep, "when", None) else "")

s_path = None
x_path = None
try:
s_path = save_screenshot(driver, str(screenshots_dir), f"FAILED_{test_id}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exceptions could be logged!

except Exception:
pass
try:
x_path = save_page_source(driver, str(screenshots_dir), f"FAILED_{test_id}")
except Exception:
pass

log = get_logger("conftest")
if s_path:
log.info(f"Saved failure screenshot: {s_path}")
if x_path:
log.info(f"Saved failure page source: {x_path}")

setattr(item, "_failure_artifacts_saved", True)
except Exception as e:
log = get_logger("conftest")
log.warning(f"Artifact capture failed: {e}")


def pytest_terminal_summary(terminalreporter, exitstatus, config):
if not _logging_setup:
return
Expand Down
14 changes: 10 additions & 4 deletions test/e2e_appium/core/environment.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import re
from dataclasses import dataclass
from typing import Dict, Any
from typing import Dict, Any, List, Optional
from pathlib import Path


Expand All @@ -22,6 +22,7 @@ class EnvironmentConfig:
directories: Dict[str, str]
logging_config: Dict[str, Any]
lambdatest_config: Dict[str, Any] = None
available_devices: Optional[List[Dict[str, Any]]] = None

def validate(self) -> None:
if self.environment == "local":
Expand All @@ -33,8 +34,10 @@ def _validate_local_config(self):
app_path = self.app_source.get("path_template", "")
resolved_path = self._resolve_template(app_path)

if resolved_path and not Path(resolved_path).exists():
raise ConfigurationError(f"Local app not found: {resolved_path}")
# Only enforce path existence if one is provided; otherwise assume appPackage/appActivity launch
if resolved_path:
if not Path(resolved_path).exists():
raise ConfigurationError(f"Local app not found: {resolved_path}")

try:
import requests
Expand Down Expand Up @@ -127,7 +130,10 @@ def get_device_capabilities(self) -> Dict[str, Any]:
}

if self.environment == "local":
base_caps["app"] = self.get_resolved_app_path()
app_path = self.get_resolved_app_path()
if app_path:
base_caps["app"] = app_path # Use APK if provided
# If no app path resolved, rely on provided appPackage/appActivity

base_caps.update(self.capabilities)
return base_caps
50 changes: 50 additions & 0 deletions test/e2e_appium/core/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from dataclasses import dataclass, field
from typing import Dict, Any, Optional


@dataclass
class TestUser:
display_name: str
password: str = "StatusPassword123!"
seed_phrase: Optional[str] = None
source: str = "created"

def to_dict(self) -> Dict[str, Any]:
return {
"display_name": self.display_name,
"password": self.password,
"seed_phrase": self.seed_phrase,
"source": self.source,
}


@dataclass
class TestAppState:
is_home_loaded: bool = False
current_screen: str = "unknown"
requires_authentication: bool = False
has_existing_profiles: bool = False


@dataclass
class TestConfiguration:
environment: str = "lambdatest"
profile_method: str = "password"
display_name: str = "TestUser"
validate_steps: bool = True
take_screenshots: bool = False
custom_config: Dict[str, Any] = field(default_factory=dict)
device_override: Optional[Dict[str, Any]] = None

@classmethod
def from_pytest_marker(cls, request, marker_name: str) -> "TestConfiguration":
config = cls()
for marker in request.node.iter_markers():
if marker.name == marker_name:
for key, value in marker.kwargs.items():
if hasattr(config, key):
setattr(config, key, value)
else:
config.custom_config[key] = value
break
return config
31 changes: 28 additions & 3 deletions test/e2e_appium/core/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from selenium.webdriver.remote.client_config import ClientConfig

try:
from config import get_config, TestConfig, get_logger, log_session_info
from config import get_logger, log_session_info
from core import EnvironmentSwitcher, ConfigurationError
except ImportError:
from config import get_logger, log_session_info
Expand All @@ -16,10 +16,11 @@
class SessionManager:
"""Manages Appium driver sessions and environment configuration"""

def __init__(self, environment="lambdatest"):
def __init__(self, environment="lambdatest", device_override=None):
self.environment = environment
self.driver = None
self.logger = get_logger("session")
self._device_override = device_override or None

# Load YAML-based configuration (simplified)
try:
Expand All @@ -38,10 +39,34 @@ def __init__(self, environment="lambdatest"):
f" Timeouts: default={timeouts.get('default')}s, wait={timeouts.get('element_wait')}s"
)

# Apply device override if provided
if self._device_override:
self._apply_device_override(self._device_override)

except ConfigurationError as e:
self.logger.error(f"❌ Configuration error: {e}")
self.logger.error("💡 Ensure YAML configuration files are properly set up")
raise # Don't fall back to legacy, force proper config
raise

def _apply_device_override(self, override: dict) -> None:
"""Override device fields from a device entry (name, platform_name, platform_version, tags)."""
try:
name = override.get("name")
platform_name = override.get("platform_name", self.env_config.platform_name)
platform_version = override.get(
"platform_version", self.env_config.platform_version
)
if name:
self.env_config.device_name = name
if platform_name:
self.env_config.platform_name = platform_name
if platform_version:
self.env_config.platform_version = platform_version
self.logger.info(
f"🔧 Device override applied → {self.env_config.device_name} ({self.env_config.platform_name} {self.env_config.platform_version})"
)
except Exception as e:
self.logger.warning(f"Failed to apply device override: {e}")

def _get_lambdatest_naming(self) -> dict:
"""Generate LambdaTest build and test names from YAML config."""
Expand Down
Loading