-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add Appium automation skill for cross-platform UI testing #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add .github/skills/appium-automation/ with Python-based Appium agent - Support iOS Simulator, Android Emulator, and Mac Catalyst - Features: tap, type, swipe, scroll, slider, assertions, screenshots - Session caching for 10x faster subsequent calls - Document AutomationId requirements for reliable element targeting Co-authored-by: GitHub Copilot <copilot@github.com>
d0cbe8d to
79dd9dd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a comprehensive Appium automation skill for cross-platform UI testing of .NET MAUI applications. The implementation provides a Python-based automation agent that supports iOS Simulator, Android Emulator, and Mac Catalyst platforms with rich interaction capabilities.
Changes:
- New Python automation agent with 40+ UI actions (tap, type, swipe, scroll, assertions)
- Session caching mechanism for 10x performance improvement on subsequent calls
- Element discovery tools (list-elements, list-buttons, fuzzy matching)
- Comprehensive CLI interface and Python API for programmatic automation
- Complete documentation with setup instructions and troubleshooting guide
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 49 comments.
| File | Description |
|---|---|
.github/skills/appium-automation/scripts/automate.py |
Main automation agent implementing cross-platform Appium wrapper with session management, gesture support, and CLI interface |
.github/skills/appium-automation/scripts/__init__.py |
Empty init file for Python package structure |
.github/skills/appium-automation/pyproject.toml |
Python project configuration with dependencies and entry point definition |
.github/skills/appium-automation/SKILL.md |
Comprehensive documentation covering setup, usage examples, troubleshooting, and platform compatibility |
| ## Python API | ||
|
|
||
| ```python | ||
| from appium_agent import AppiumAgent |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation references "appium_agent.py" and "from appium_agent import AppiumAgent" but the actual file is named "automate.py" and the module path would be different. All examples in the documentation should be updated to use the correct file/module name for consistency.
| from appium_agent import AppiumAgent | |
| from automate import AppiumAgent |
| self.driver.execute_script('mobile: dragFromToForDuration', { | ||
| 'fromX': from_x, | ||
| 'fromY': from_y, | ||
| 'toX': to_x, | ||
| 'toY': to_y, | ||
| 'duration': duration | ||
| }) |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The drag method uses 'mobile: dragFromToForDuration' which is iOS-specific and will fail on Android or Mac Catalyst. The method should either document this iOS-only limitation or implement platform-specific drag operations using ActionChains for cross-platform support.
| self.driver.execute_script('mobile: dragFromToForDuration', { | |
| 'fromX': from_x, | |
| 'fromY': from_y, | |
| 'toX': to_x, | |
| 'toY': to_y, | |
| 'duration': duration | |
| }) | |
| # Use cross-platform gesture helper instead of iOS-specific script | |
| self._perform_gesture( | |
| from_x, | |
| from_y, | |
| to_x, | |
| to_y, | |
| duration_ms=int(duration * 1000), | |
| ) |
| try: | ||
| data = json.loads(cache_file.read_text()) | ||
| # Session expires after 5 minutes of inactivity | ||
| if time.time() - data.get("timestamp", 0) > 300: |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Magic number 300 for session timeout (5 minutes) is hardcoded without explanation. Consider defining this as a named constant at module level (e.g., SESSION_TIMEOUT_SECONDS = 300) for better maintainability and to allow future configuration.
| def _try_reuse_session(self) -> bool: | ||
| """Try to reuse a cached session. Returns True if successful.""" | ||
| cached = self._load_cached_session() | ||
| if not cached: | ||
| return False | ||
|
|
||
| try: | ||
| import urllib.request | ||
| from appium.webdriver.errorhandler import MobileErrorHandler | ||
| from selenium.webdriver.remote.remote_connection import RemoteConnection | ||
| from selenium.webdriver.remote.file_detector import LocalFileDetector | ||
| from selenium.webdriver.remote.locator_converter import LocatorConverter | ||
| from selenium.webdriver.remote.mobile import Mobile | ||
| from selenium.webdriver.remote.shadowroot import ShadowRoot | ||
| from selenium.webdriver.remote.switch_to import SwitchTo | ||
|
|
||
| # Test if session is still valid | ||
| req = urllib.request.Request( | ||
| f"{self.appium_url}/session/{cached['session_id']}" | ||
| ) | ||
| try: | ||
| with urllib.request.urlopen(req, timeout=5) as resp: | ||
| if resp.status != 200: | ||
| self._clear_session_cache() | ||
| return False | ||
| caps_data = json.loads(resp.read().decode()) | ||
| except: | ||
| self._clear_session_cache() | ||
| return False | ||
|
|
||
| # Create driver attached to existing session (no new session) | ||
| from selenium.webdriver.remote.client_config import ClientConfig | ||
| from appium.webdriver.appium_connection import AppiumConnection | ||
| from appium.webdriver.mobilecommand import MobileCommand | ||
| config = ClientConfig(remote_server_addr=self.appium_url) | ||
| executor = AppiumConnection(client_config=config) | ||
|
|
||
| # Add Appium-specific commands that are normally added in webdriver.__init__ | ||
| executor.add_command(MobileCommand.CLEAR, 'POST', '/session/$sessionId/element/$id/clear') | ||
|
|
||
| # Construct driver with all required attributes | ||
| self.driver = object.__new__(webdriver.Remote) | ||
| self.driver.command_executor = executor | ||
| self.driver.session_id = cached["session_id"] | ||
| self.driver._is_remote = True | ||
| self.driver.caps = caps_data.get("value", {}).get("capabilities", {}) | ||
| self.driver.pinned_scripts = {} | ||
| self.driver.error_handler = MobileErrorHandler() | ||
| self.driver._file_detector = LocalFileDetector() | ||
| self.driver.locator_converter = LocatorConverter() | ||
| self.driver._switch_to = SwitchTo(self.driver) | ||
| self.driver._mobile = Mobile(self.driver) | ||
| self.driver._bidi_connection = None | ||
| self.driver._fedcm = None | ||
| self.driver._script = None | ||
| self.driver._absent_extensions = set() # Required for activate_app/terminate_app | ||
|
|
||
| self._session_reused = True | ||
| self._save_session() # Update timestamp | ||
| return True | ||
|
|
||
| except Exception as e: | ||
| self._clear_session_cache() | ||
| return False |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bare except clauses throughout this method can hide critical errors. Multiple instances at lines 258, 293 should catch specific exception types for better error diagnosis and to avoid catching SystemExit or KeyboardInterrupt.
| element = self.find_element(identifier, timeout=5) | ||
| if element: | ||
| # Platform-specific attribute order | ||
| if self.platform in ['ios', 'maccatalyst']: |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Platform value is compared as string 'ios' and 'maccatalyst' instead of using the Platform enum (Platform.IOS, Platform.MACCATALYST). This is inconsistent with how platform is checked elsewhere in the codebase and could lead to bugs if the Platform enum values change.
| if self.platform in ['ios', 'maccatalyst']: | |
| if self.platform in {Platform.IOS, Platform.MACCATALYST}: |
| try: | ||
| done_btn = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, 'Done') | ||
| done_btn.click() | ||
| except: |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except: | ||
| pass |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except: | |
| pass | |
| except Exception: | |
| # Some elements may become stale or inaccessible during attribute retrieval; safely skip them. | |
| continue |
| info[attr] = val | ||
| try: | ||
| info["text"] = e.text | ||
| except: |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except: | ||
| pass | ||
| result.append(info) | ||
| except: |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| "text": e.text or e.get_attribute("label") or e.get_attribute("text"), | ||
| "accessibility_id": e.get_attribute("accessibility-id") or e.get_attribute("name"), | ||
| }) | ||
| except: |
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
Summary
Adds a new Copilot skill for automating .NET MAUI apps using Appium across iOS, Android, and Mac Catalyst.
Features
--expectfor UI testing with pass/fail exit codes--reuse-sessionChanges
.github/skills/appium-automation/- New skill with Python automation agent10.0/Apps/TipCalc/TipCalcPage.xaml- Added AutomationIds for testabilityUsage
See
SKILL.mdfor full documentation.