Skip to content

Conversation

@StephaneDelcroix
Copy link

Summary

Adds a new Copilot skill for automating .NET MAUI apps using Appium across iOS, Android, and Mac Catalyst.

Features

  • Cross-platform support: iOS Simulator, Android Emulator, Mac Catalyst
  • Rich actions: tap, type, swipe, scroll, drag, slider manipulation
  • Assertions: --expect for UI testing with pass/fail exit codes
  • Session caching: 10x faster subsequent calls with --reuse-session
  • Element discovery: list-elements, list-buttons, find-text, fuzzy matching

Changes

  • .github/skills/appium-automation/ - New skill with Python automation agent
  • 10.0/Apps/TipCalc/TipCalcPage.xaml - Added AutomationIds for testability

Usage

# Install dependencies
pip install Appium-Python-Client selenium
appium driver install xcuitest

# Run automation
python .github/skills/appium-automation/scripts/automate.py \
  --platform ios --app-id com.companyname.tipcalc \
  --type SubtotalEntry 100 --expect TipAmountValue "€ 15"

See SKILL.md for full documentation.

- 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>
@StephaneDelcroix StephaneDelcroix force-pushed the feature/appium-automation-skill branch from d0cbe8d to 79dd9dd Compare January 19, 2026 08:08
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this?

Copy link

Copilot AI left a 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
Copy link

Copilot AI Jan 20, 2026

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.

Suggested change
from appium_agent import AppiumAgent
from automate import AppiumAgent

Copilot uses AI. Check for mistakes.
Comment on lines +978 to +984
self.driver.execute_script('mobile: dragFromToForDuration', {
'fromX': from_x,
'fromY': from_y,
'toX': to_x,
'toY': to_y,
'duration': duration
})
Copy link

Copilot AI Jan 20, 2026

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.

Suggested change
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),
)

Copilot uses AI. Check for mistakes.
try:
data = json.loads(cache_file.read_text())
# Session expires after 5 minutes of inactivity
if time.time() - data.get("timestamp", 0) > 300:
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +295
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
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
element = self.find_element(identifier, timeout=5)
if element:
# Platform-specific attribute order
if self.platform in ['ios', 'maccatalyst']:
Copy link

Copilot AI Jan 20, 2026

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.

Suggested change
if self.platform in ['ios', 'maccatalyst']:
if self.platform in {Platform.IOS, Platform.MACCATALYST}:

Copilot uses AI. Check for mistakes.
try:
done_btn = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, 'Done')
done_btn.click()
except:
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1241 to +1242
except:
pass
Copy link

Copilot AI Jan 20, 2026

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.

Suggested change
except:
pass
except Exception:
# Some elements may become stale or inaccessible during attribute retrieval; safely skip them.
continue

Copilot uses AI. Check for mistakes.
info[attr] = val
try:
info["text"] = e.text
except:
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
except:
pass
result.append(info)
except:
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
"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:
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants