Skip to main content
Glama
recorder.py16.2 kB
"""Test action recorder for capturing and exporting test scripts.""" from dataclasses import dataclass, asdict from typing import List, Any from datetime import datetime import json from pathlib import Path @dataclass class TestAction: """Represents a single user action during testing.""" action: str # click, swipe, type, press, etc. timestamp: float parameters: dict result: str = "" description: str = "" def to_dict(self): return asdict(self) class TestRecorder: """Records user actions during testing for export as executable test scripts.""" def __init__(self): self.actions: List[TestAction] = [] self.start_time = datetime.now() self.is_recording = False def start(self): """Start recording actions.""" self.actions = [] self.start_time = datetime.now() self.is_recording = True def stop(self): """Stop recording actions.""" self.is_recording = False def record_action(self, action: str, parameters: dict, result: str = "", description: str = ""): """Record a single action.""" if not self.is_recording: return timestamp = (datetime.now() - self.start_time).total_seconds() test_action = TestAction( action=action, timestamp=timestamp, parameters=parameters, result=result, description=description ) self.actions.append(test_action) def get_actions(self) -> List[TestAction]: """Get all recorded actions.""" return self.actions def clear(self): """Clear all recorded actions.""" self.actions = [] def export_as_json(self, filename: str = None) -> str: """Export recorded actions as JSON.""" if not filename: timestamp = self.start_time.strftime("%Y%m%d_%H%M%S") filename = f"test_script_{timestamp}.json" filepath = Path(filename) data = { "test_name": filename.replace(".json", ""), "start_time": self.start_time.isoformat(), "duration_seconds": sum( action.timestamp for action in self.actions ) if self.actions else 0, "total_actions": len(self.actions), "actions": [action.to_dict() for action in self.actions] } with open(filepath, 'w') as f: json.dump(data, f, indent=2) return str(filepath) def export_as_python(self, filename: str = None, test_name: str = None, use_adb: bool = True) -> str: """Export recorded actions as executable Python test script. Args: filename: Output filename test_name: Name of the test use_adb: If True, generate ADB-based script (independent). If False, use uiautomator2. """ if not filename: timestamp = self.start_time.strftime("%Y%m%d_%H%M%S") filename = f"test_{timestamp}.py" if not test_name: test_name = filename.replace(".py", "") filepath = Path(filename) if use_adb: return self._export_as_adb_python(filepath, test_name) else: return self._export_as_uiautomator_python(filepath, test_name) def _export_as_uiautomator_python(self, filepath: Path, test_name: str) -> str: """Export as uiautomator2-based script (original behavior).""" script_lines = [ '"""', f'Auto-generated test script: {test_name}', f'Generated: {self.start_time.isoformat()}', f'Total actions: {len(self.actions)}', '"""', '', 'import uiautomator2 as u2', 'import time', '', 'def run_test(device=None):', ' """Run the recorded test sequence."""', ' if device is None:', ' device = u2.connect()', ' ', ] for i, action in enumerate(self.actions, 1): wait_before = "" if i > 1: prev_action = self.actions[i-2] time_diff = action.timestamp - prev_action.timestamp if time_diff > 0.5: # Add delay if there was a significant gap wait_before = f" time.sleep({time_diff:.1f})\n" script_lines.append(wait_before) script_lines.append(self._generate_action_code_uiautomator(action, i)) script_lines.extend([ '', 'if __name__ == "__main__":', ' run_test()', ]) script_content = '\n'.join(script_lines) with open(filepath, 'w') as f: f.write(script_content) return str(filepath) def _export_as_adb_python(self, filepath: Path, test_name: str) -> str: """Export as ADB-based script (fully independent, no dependencies).""" script_lines = [ '"""', f'Auto-generated test script: {test_name}', f'Generated: {self.start_time.isoformat()}', f'Total actions: {len(self.actions)}', '', 'This script uses direct ADB commands and is fully independent.', 'No external dependencies required beyond ADB.', '"""', '', 'import subprocess', 'import time', 'import sys', '', '', 'class DeviceController:', ' """Direct ADB device controller - no external dependencies."""', ' ', ' def __init__(self, device_id="emulator-5554"):', ' self.device_id = device_id', ' self._verify_device()', ' ', ' def _verify_device(self):', ' """Verify device is connected."""', ' result = subprocess.run(["adb", "devices"], capture_output=True, text=True)', ' if self.device_id not in result.stdout:', ' raise Exception(f"Device {self.device_id} not found. Available devices:\\n{result.stdout}")', ' ', ' def click(self, x, y):', ' """Click at coordinates."""', ' cmd = f"adb -s {self.device_id} shell input tap {x} {y}"', ' subprocess.run(cmd, shell=True)', ' ', ' def long_click(self, x, y, duration=1000):', ' """Long click at coordinates."""', ' cmd = f"adb -s {self.device_id} shell input touchscreen swipe {x} {y} {x} {y} {duration}"', ' subprocess.run(cmd, shell=True)', ' ', ' def swipe(self, x1, y1, x2, y2, duration=300):', ' """Swipe from one point to another."""', ' cmd = f"adb -s {self.device_id} shell input touchscreen swipe {x1} {y1} {x2} {y2} {duration}"', ' subprocess.run(cmd, shell=True)', ' ', ' def drag(self, x1, y1, x2, y2, duration=500):', ' """Drag from one point to another."""', ' cmd = f"adb -s {self.device_id} shell input touchscreen swipe {x1} {y1} {x2} {y2} {duration}"', ' subprocess.run(cmd, shell=True)', ' ', ' def type_text(self, text):', ' """Type text on device."""', ' cmd = f"adb -s {self.device_id} shell input text \\"{text}\\""', ' subprocess.run(cmd, shell=True)', ' ', ' def press_key(self, key_code):', ' """Press a key code."""', ' key_map = {', ' "ENTER": "66",', ' "BACK": "4",', ' "HOME": "3",', ' "MENU": "1",', ' "POWER": "26",', ' "VOLUME_UP": "24",', ' "VOLUME_DOWN": "25",', ' }', ' code = key_map.get(key_code.upper(), key_code)', ' cmd = f"adb -s {self.device_id} shell input keyevent {code}"', ' subprocess.run(cmd, shell=True)', ' ', ' def open_notification(self):', ' """Open notification bar."""', ' cmd = f"adb -s {self.device_id} shell cmd statusbar expand-notifications"', ' subprocess.run(cmd, shell=True)', ' ', '', 'def run_test(device_id="emulator-5554"):', ' """Run the recorded test sequence."""', ' print(f"Connecting to device: {device_id}")', ' device = DeviceController(device_id)', ' print("Device connected. Starting test...")', ' ', ] for i, action in enumerate(self.actions, 1): wait_before = "" if i > 1: prev_action = self.actions[i-2] time_diff = action.timestamp - prev_action.timestamp if time_diff > 0.5: # Add delay if there was a significant gap wait_before = f" time.sleep({time_diff:.1f})\n" script_lines.append(wait_before) script_lines.append(self._generate_action_code_adb(action, i)) script_lines.extend([ ' ', ' print("Test completed successfully!")', '', '', 'if __name__ == "__main__":', ' device_id = sys.argv[1] if len(sys.argv) > 1 else "emulator-5554"', ' try:', ' run_test(device_id)', ' except KeyboardInterrupt:', ' print("\\nTest interrupted by user")', ' except Exception as e:', ' print(f"Error: {e}")', ' sys.exit(1)', ]) script_content = '\n'.join(script_lines) with open(filepath, 'w') as f: f.write(script_content) return str(filepath) def _generate_action_code_uiautomator(self, action: TestAction, action_num: int) -> str: """Generate Python code for a single action using uiautomator2.""" params = action.parameters indent = " " if action.action == "click": return f'{indent}# Action {action_num}: Click at ({params["x"]}, {params["y"]})\n{indent}device.click({params["x"]}, {params["y"]})' elif action.action == "long_click": return f'{indent}# Action {action_num}: Long click at ({params["x"]}, {params["y"]})\n{indent}device.long_click({params["x"]}, {params["y"]})' elif action.action == "swipe": return f'{indent}# Action {action_num}: Swipe from ({params["x1"]}, {params["y1"]}) to ({params["x2"]}, {params["y2"]})\n{indent}device.swipe({params["x1"]}, {params["y1"]}, {params["x2"]}, {params["y2"]})' elif action.action == "type": text = params["text"].replace('"', '\\"') return f'{indent}# Action {action_num}: Type "{text}"\n{indent}device.send_keys("{text}")' elif action.action == "drag": return f'{indent}# Action {action_num}: Drag from ({params["x1"]}, {params["y1"]}) to ({params["x2"]}, {params["y2"]})\n{indent}device.drag({params["x1"]}, {params["y1"]}, {params["x2"]}, {params["y2"]})' elif action.action == "press": button = params["button"] return f'{indent}# Action {action_num}: Press {button} button\n{indent}device.press("{button}")' elif action.action == "notification": return f'{indent}# Action {action_num}: Open notification bar\n{indent}device.open_notification()' elif action.action == "wait": duration = params["duration"] return f'{indent}# Action {action_num}: Wait {duration} seconds\n{indent}time.sleep({duration})' else: return f'{indent}# Action {action_num}: {action.action} {params}' def _generate_action_code_adb(self, action: TestAction, action_num: int) -> str: """Generate Python code for a single action using direct ADB commands.""" params = action.parameters indent = " " if action.action == "click": return f'{indent}# Action {action_num}: Click at ({params["x"]}, {params["y"]})\n{indent}device.click({params["x"]}, {params["y"]})' elif action.action == "long_click": return f'{indent}# Action {action_num}: Long click at ({params["x"]}, {params["y"]})\n{indent}device.long_click({params["x"]}, {params["y"]})' elif action.action == "swipe": return f'{indent}# Action {action_num}: Swipe from ({params["x1"]}, {params["y1"]}) to ({params["x2"]}, {params["y2"]})\n{indent}device.swipe({params["x1"]}, {params["y1"]}, {params["x2"]}, {params["y2"]})' elif action.action == "type": text = params["text"].replace('"', '\\"') return f'{indent}# Action {action_num}: Type "{text}"\n{indent}device.type_text("{text}")' elif action.action == "drag": return f'{indent}# Action {action_num}: Drag from ({params["x1"]}, {params["y1"]}) to ({params["x2"]}, {params["y2"]})\n{indent}device.drag({params["x1"]}, {params["y1"]}, {params["x2"]}, {params["y2"]})' elif action.action == "press": button = params["button"] return f'{indent}# Action {action_num}: Press {button} button\n{indent}device.press_key("{button}")' elif action.action == "notification": return f'{indent}# Action {action_num}: Open notification bar\n{indent}device.open_notification()' elif action.action == "wait": duration = params["duration"] return f'{indent}# Action {action_num}: Wait {duration} seconds\n{indent}time.sleep({duration})' else: return f'{indent}# Action {action_num}: {action.action} {params}' def export_as_readable(self, filename: str = None) -> str: """Export recorded actions as human-readable test steps.""" if not filename: timestamp = self.start_time.strftime("%Y%m%d_%H%M%S") filename = f"test_steps_{timestamp}.txt" filepath = Path(filename) lines = [ f"Test Script: {filename.replace('.txt', '')}", f"Generated: {self.start_time.isoformat()}", f"Total Actions: {len(self.actions)}", "", "=" * 60, "TEST STEPS:", "=" * 60, "", ] for i, action in enumerate(self.actions, 1): description = self._get_action_description(action, i) lines.append(f"{i}. {description}") if action.description: lines.append(f" Note: {action.description}") lines.append("") with open(filepath, 'w') as f: f.write('\n'.join(lines)) return str(filepath) def _get_action_description(self, action: TestAction, action_num: int) -> str: """Generate a human-readable description of an action.""" params = action.parameters if action.action == "click": return f"Click at coordinates ({params['x']}, {params['y']})" elif action.action == "long_click": return f"Long click at coordinates ({params['x']}, {params['y']})" elif action.action == "swipe": return f"Swipe from ({params['x1']}, {params['y1']}) to ({params['x2']}, {params['y2']})" elif action.action == "type": return f"Type text: \"{params['text']}\"" elif action.action == "drag": return f"Drag from ({params['x1']}, {params['y1']}) to ({params['x2']}, {params['y2']})" elif action.action == "press": return f"Press {params['button']} button" elif action.action == "notification": return "Open notification bar" elif action.action == "wait": return f"Wait for {params['duration']} seconds" else: return f"{action.action.capitalize()}: {params}"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HadyAhmed00/Android-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server