Skip to main content
Glama
recorder.py8.34 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) -> str: """Export recorded actions as executable Python test script.""" 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) # Generate Python script content 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(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 _generate_action_code(self, action: TestAction, action_num: int) -> str: """Generate Python code for a single action.""" 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 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