flowpilot-mobile 1.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowpilot_mobile-1.1.8/PKG-INFO +153 -0
- flowpilot_mobile-1.1.8/README.md +127 -0
- flowpilot_mobile-1.1.8/flowpilot/__init__.py +7 -0
- flowpilot_mobile-1.1.8/flowpilot/api/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/api/mobile.py +284 -0
- flowpilot_mobile-1.1.8/flowpilot/cli/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/cli/interactive.py +76 -0
- flowpilot_mobile-1.1.8/flowpilot/cli/main.py +89 -0
- flowpilot_mobile-1.1.8/flowpilot/commands/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/commands/core.py +135 -0
- flowpilot_mobile-1.1.8/flowpilot/config/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/config/models.py +52 -0
- flowpilot_mobile-1.1.8/flowpilot/devices/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/devices/manager.py +26 -0
- flowpilot_mobile-1.1.8/flowpilot/elements/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/exceptions/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/exceptions/core.py +15 -0
- flowpilot_mobile-1.1.8/flowpilot/fixtures/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/reporting/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/reporting/engine.py +48 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/adapter.py +38 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/adb_engine.py +339 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/executor.py +183 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/guard.py +214 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/queue.py +16 -0
- flowpilot_mobile-1.1.8/flowpilot/runtime/translator.py +34 -0
- flowpilot_mobile-1.1.8/flowpilot/smart/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/smart/engine.py +74 -0
- flowpilot_mobile-1.1.8/flowpilot/utils/__init__.py +0 -0
- flowpilot_mobile-1.1.8/flowpilot/utils/helpers.py +24 -0
- flowpilot_mobile-1.1.8/flowpilot/utils/setup.py +221 -0
- flowpilot_mobile-1.1.8/flowpilot_mobile.egg-info/PKG-INFO +153 -0
- flowpilot_mobile-1.1.8/flowpilot_mobile.egg-info/SOURCES.txt +51 -0
- flowpilot_mobile-1.1.8/flowpilot_mobile.egg-info/dependency_links.txt +1 -0
- flowpilot_mobile-1.1.8/flowpilot_mobile.egg-info/entry_points.txt +5 -0
- flowpilot_mobile-1.1.8/flowpilot_mobile.egg-info/requires.txt +8 -0
- flowpilot_mobile-1.1.8/flowpilot_mobile.egg-info/top_level.txt +2 -0
- flowpilot_mobile-1.1.8/pyproject.toml +47 -0
- flowpilot_mobile-1.1.8/pytest_flowpilot/__init__.py +0 -0
- flowpilot_mobile-1.1.8/pytest_flowpilot/plugin.py +28 -0
- flowpilot_mobile-1.1.8/setup.cfg +4 -0
- flowpilot_mobile-1.1.8/tests/test_chrome_automation.py +46 -0
- flowpilot_mobile-1.1.8/tests/test_chrome_login.py +45 -0
- flowpilot_mobile-1.1.8/tests/test_double_swipe.py +27 -0
- flowpilot_mobile-1.1.8/tests/test_gestures.py +31 -0
- flowpilot_mobile-1.1.8/tests/test_google_search_simple.py +31 -0
- flowpilot_mobile-1.1.8/tests/test_physical_device.py +34 -0
- flowpilot_mobile-1.1.8/tests/test_scroll_find.py +19 -0
- flowpilot_mobile-1.1.8/tests/test_settings_google.py +30 -0
- flowpilot_mobile-1.1.8/tests/test_text_discovery.py +31 -0
- flowpilot_mobile-1.1.8/tests/test_visible_phone.py +24 -0
- flowpilot_mobile-1.1.8/tests/test_widget_swipe.py +26 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowpilot-mobile
|
|
3
|
+
Version: 1.1.8
|
|
4
|
+
Summary: Modern, Intelligent Mobile Automation Framework
|
|
5
|
+
Author-email: Niranjan Kumar <niranjan4@outlook.in>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/niranjangs4/flowpilot-mobile
|
|
8
|
+
Project-URL: Documentation, https://github.com/niranjangs4/flowpilot-mobile#readme
|
|
9
|
+
Keywords: mobile,automation,android,smart,testing,pytest
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: pytest>=8.0.0
|
|
19
|
+
Requires-Dist: pytest-xdist>=3.5.0
|
|
20
|
+
Requires-Dist: rich>=13.0.0
|
|
21
|
+
Requires-Dist: loguru>=0.7.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
24
|
+
Requires-Dist: jinja2>=3.1.0
|
|
25
|
+
Requires-Dist: cryptography>=41.0.0
|
|
26
|
+
|
|
27
|
+
# FlowPilot Mobile
|
|
28
|
+
|
|
29
|
+
Modern, Python-first mobile automation framework.
|
|
30
|
+
Provides clean Playwright-style APIs for smart mobile automation with AI-assisted test generation.
|
|
31
|
+
|
|
32
|
+
## 🚀 Quick Start
|
|
33
|
+
|
|
34
|
+
### 1. Install via pip
|
|
35
|
+
```bash
|
|
36
|
+
pip install flowpilot-mobile
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Initialize your Project
|
|
40
|
+
Initialize a new project folder with all necessary configurations:
|
|
41
|
+
```bash
|
|
42
|
+
flowpilot setup --project_folder my_tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Configure Tool Paths
|
|
46
|
+
Open `my_tests/flowpilot.yaml` and specify your manual paths for ADB and Scrcpy if they are not in your system PATH:
|
|
47
|
+
```yaml
|
|
48
|
+
tools:
|
|
49
|
+
adb_path: "C:\\path\\to\\adb.exe"
|
|
50
|
+
scrcpy_path: "C:\\path\\to\\scrcpy.exe"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🌟 The FlowPilot "Smart" Advantage
|
|
56
|
+
|
|
57
|
+
FlowPilot includes an **Intelligent Layer** that handles the "messy" parts of mobile testing automatically:
|
|
58
|
+
|
|
59
|
+
- **Auto-Scroll Search**: Automatically swipes and retries if an element is off-screen.
|
|
60
|
+
- **Smart Locator Scoring**: Distinguishes between actual inputs and "distractor" elements (like Search Labs icons).
|
|
61
|
+
- **Nearby Label Matching**: Fills input fields based on the text labels next to them.
|
|
62
|
+
- **Self-Healing**: Uses fuzzy matching for minor UI text changes (e.g., "Log In" vs "Login").
|
|
63
|
+
- **Automatic Device Reset**: Automatically presses the **Home button** before and after every test to ensure a clean baseline.
|
|
64
|
+
- **Zero-Sleep Synchronization**: Built-in polling ensures elements are ready before interaction.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🤖 AI-Assisted Automation (TEST_GEN_PROMPT)
|
|
69
|
+
|
|
70
|
+
FlowPilot now supports **Instant Test Generation** via AI. Every project initialized with `flowpilot setup` includes a `TEST_GEN_PROMPT.md` file.
|
|
71
|
+
|
|
72
|
+
1. Copy the content of `TEST_GEN_PROMPT.md`.
|
|
73
|
+
2. Paste it into **Gemini** or **ChatGPT**.
|
|
74
|
+
3. Provide your manual test steps or describe a screen recording.
|
|
75
|
+
4. **Result:** The AI will generate a complete FlowPilot Python test script for you.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🖥️ Live Monitoring & Evidence
|
|
80
|
+
|
|
81
|
+
### Live Desktop Mirroring
|
|
82
|
+
See your phone's screen in a window on your desktop (Mac/Windows) during execution.
|
|
83
|
+
- **Enable:** Set `mirror_screen: true` in your `flowpilot.yaml`.
|
|
84
|
+
- **Manual Path:** Configure `scrcpy_path` in `flowpilot.yaml` if needed.
|
|
85
|
+
|
|
86
|
+
### Automated Evidence
|
|
87
|
+
- **Screen Recording (MP4)**: Every test is recorded with hardware encoding (zero performance drop). Includes professional 1s padding.
|
|
88
|
+
- **Retention Policy**: Automatically purges recordings older than **5 days** (configurable) to save disk space.
|
|
89
|
+
- **Smart Screenshots**: Captures specific UI states into `.flowpilot/screenshots/`.
|
|
90
|
+
- **Selective Cleanup**: Wipes temporary flows and recent screenshots before each run while **preserving** your video history.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ⚙️ Configuration (`flowpilot.yaml`)
|
|
95
|
+
```yaml
|
|
96
|
+
project_name: "My Custom App"
|
|
97
|
+
|
|
98
|
+
# Manual Tool Paths
|
|
99
|
+
tools:
|
|
100
|
+
adb_path: ""
|
|
101
|
+
scrcpy_path: ""
|
|
102
|
+
|
|
103
|
+
# Evidence & Storage
|
|
104
|
+
output_dir: ".flowpilot"
|
|
105
|
+
recordings_dir: ".flowpilot/recordings"
|
|
106
|
+
screenshots_dir: ".flowpilot/screenshots"
|
|
107
|
+
|
|
108
|
+
# Features
|
|
109
|
+
record_video: true
|
|
110
|
+
mirror_screen: true
|
|
111
|
+
|
|
112
|
+
# Smart Engine Settings
|
|
113
|
+
auto_scroll: true
|
|
114
|
+
timeout: 30
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 📖 Public API Reference
|
|
120
|
+
|
|
121
|
+
| Category | Method | Description | Example |
|
|
122
|
+
| :--- | :--- | :--- | :--- |
|
|
123
|
+
| **Lifecycle** | `launch_app(pkg)` | Opens a specific app (defaults to Chrome). | `mobile.launch_app()` |
|
|
124
|
+
| | `home()` | Returns to the device Home Screen. | `mobile.home()` |
|
|
125
|
+
| | `back()` | Simulates the hardware Back button. | `mobile.back()` |
|
|
126
|
+
| | `navigate(url)` | Opens a specific URL in the browser. | `mobile.navigate("https://google.com")` |
|
|
127
|
+
| **Interactions** | `tap(target)` | **Smart Tap**: Auto-scrolls and waits for visibility. | `mobile.tap("Settings")` |
|
|
128
|
+
| | `fill(target, val)` | **Smart Fill**: Finds nearest input to a label. | `mobile.fill("Username", "admin")` |
|
|
129
|
+
| | `fill_focused(val)` | **Active Field**: Types into currently focused input. | `mobile.fill_focused("secret")` |
|
|
130
|
+
| | `press_key(key)` | Presses hardware keys (Enter, Home, Back). | `mobile.press_key("Enter")` |
|
|
131
|
+
| **Gestures** | `open_notifications()`| Pulls down the notification shade (Left). | `mobile.open_notifications()` |
|
|
132
|
+
| | `open_quick_settings()`| Pulls down full Quick Settings (Center). | `mobile.open_quick_settings()` |
|
|
133
|
+
| | `scroll_down()` | Performs a smart downward scroll. | `mobile.scroll_down()` |
|
|
134
|
+
| **Media** | `screenshot(name)` | Saves a screenshot to `.flowpilot/screenshots/`. | `mobile.screenshot("step_1")` |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🤖 CLI Commands
|
|
139
|
+
|
|
140
|
+
| Command | Description |
|
|
141
|
+
| :--- | :--- |
|
|
142
|
+
| `flowpilot setup` | Initializes a new project folder and generates configurations. |
|
|
143
|
+
| `flowpilot interactive`| Start a live step-by-step automation session. |
|
|
144
|
+
| `flowpilot run` | Executes tests with smart logs. |
|
|
145
|
+
| `flowpilot devices` | Lists all connected physical Android devices. |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🛡 Engineering Principles
|
|
150
|
+
1. **Hide Complexity**: Zero YAML or runtime engine logic exposed.
|
|
151
|
+
2. **Text-First**: Locators prioritize display text over brittle XPaths.
|
|
152
|
+
3. **Hardware-Accelerated**: Native MP4 capture and Scrcpy mirroring with zero host lag.
|
|
153
|
+
4. **Resilient Reset**: Automated pre/post Home screen logic for repeatability.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# FlowPilot Mobile
|
|
2
|
+
|
|
3
|
+
Modern, Python-first mobile automation framework.
|
|
4
|
+
Provides clean Playwright-style APIs for smart mobile automation with AI-assisted test generation.
|
|
5
|
+
|
|
6
|
+
## 🚀 Quick Start
|
|
7
|
+
|
|
8
|
+
### 1. Install via pip
|
|
9
|
+
```bash
|
|
10
|
+
pip install flowpilot-mobile
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Initialize your Project
|
|
14
|
+
Initialize a new project folder with all necessary configurations:
|
|
15
|
+
```bash
|
|
16
|
+
flowpilot setup --project_folder my_tests
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 3. Configure Tool Paths
|
|
20
|
+
Open `my_tests/flowpilot.yaml` and specify your manual paths for ADB and Scrcpy if they are not in your system PATH:
|
|
21
|
+
```yaml
|
|
22
|
+
tools:
|
|
23
|
+
adb_path: "C:\\path\\to\\adb.exe"
|
|
24
|
+
scrcpy_path: "C:\\path\\to\\scrcpy.exe"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🌟 The FlowPilot "Smart" Advantage
|
|
30
|
+
|
|
31
|
+
FlowPilot includes an **Intelligent Layer** that handles the "messy" parts of mobile testing automatically:
|
|
32
|
+
|
|
33
|
+
- **Auto-Scroll Search**: Automatically swipes and retries if an element is off-screen.
|
|
34
|
+
- **Smart Locator Scoring**: Distinguishes between actual inputs and "distractor" elements (like Search Labs icons).
|
|
35
|
+
- **Nearby Label Matching**: Fills input fields based on the text labels next to them.
|
|
36
|
+
- **Self-Healing**: Uses fuzzy matching for minor UI text changes (e.g., "Log In" vs "Login").
|
|
37
|
+
- **Automatic Device Reset**: Automatically presses the **Home button** before and after every test to ensure a clean baseline.
|
|
38
|
+
- **Zero-Sleep Synchronization**: Built-in polling ensures elements are ready before interaction.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 🤖 AI-Assisted Automation (TEST_GEN_PROMPT)
|
|
43
|
+
|
|
44
|
+
FlowPilot now supports **Instant Test Generation** via AI. Every project initialized with `flowpilot setup` includes a `TEST_GEN_PROMPT.md` file.
|
|
45
|
+
|
|
46
|
+
1. Copy the content of `TEST_GEN_PROMPT.md`.
|
|
47
|
+
2. Paste it into **Gemini** or **ChatGPT**.
|
|
48
|
+
3. Provide your manual test steps or describe a screen recording.
|
|
49
|
+
4. **Result:** The AI will generate a complete FlowPilot Python test script for you.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🖥️ Live Monitoring & Evidence
|
|
54
|
+
|
|
55
|
+
### Live Desktop Mirroring
|
|
56
|
+
See your phone's screen in a window on your desktop (Mac/Windows) during execution.
|
|
57
|
+
- **Enable:** Set `mirror_screen: true` in your `flowpilot.yaml`.
|
|
58
|
+
- **Manual Path:** Configure `scrcpy_path` in `flowpilot.yaml` if needed.
|
|
59
|
+
|
|
60
|
+
### Automated Evidence
|
|
61
|
+
- **Screen Recording (MP4)**: Every test is recorded with hardware encoding (zero performance drop). Includes professional 1s padding.
|
|
62
|
+
- **Retention Policy**: Automatically purges recordings older than **5 days** (configurable) to save disk space.
|
|
63
|
+
- **Smart Screenshots**: Captures specific UI states into `.flowpilot/screenshots/`.
|
|
64
|
+
- **Selective Cleanup**: Wipes temporary flows and recent screenshots before each run while **preserving** your video history.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## ⚙️ Configuration (`flowpilot.yaml`)
|
|
69
|
+
```yaml
|
|
70
|
+
project_name: "My Custom App"
|
|
71
|
+
|
|
72
|
+
# Manual Tool Paths
|
|
73
|
+
tools:
|
|
74
|
+
adb_path: ""
|
|
75
|
+
scrcpy_path: ""
|
|
76
|
+
|
|
77
|
+
# Evidence & Storage
|
|
78
|
+
output_dir: ".flowpilot"
|
|
79
|
+
recordings_dir: ".flowpilot/recordings"
|
|
80
|
+
screenshots_dir: ".flowpilot/screenshots"
|
|
81
|
+
|
|
82
|
+
# Features
|
|
83
|
+
record_video: true
|
|
84
|
+
mirror_screen: true
|
|
85
|
+
|
|
86
|
+
# Smart Engine Settings
|
|
87
|
+
auto_scroll: true
|
|
88
|
+
timeout: 30
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 📖 Public API Reference
|
|
94
|
+
|
|
95
|
+
| Category | Method | Description | Example |
|
|
96
|
+
| :--- | :--- | :--- | :--- |
|
|
97
|
+
| **Lifecycle** | `launch_app(pkg)` | Opens a specific app (defaults to Chrome). | `mobile.launch_app()` |
|
|
98
|
+
| | `home()` | Returns to the device Home Screen. | `mobile.home()` |
|
|
99
|
+
| | `back()` | Simulates the hardware Back button. | `mobile.back()` |
|
|
100
|
+
| | `navigate(url)` | Opens a specific URL in the browser. | `mobile.navigate("https://google.com")` |
|
|
101
|
+
| **Interactions** | `tap(target)` | **Smart Tap**: Auto-scrolls and waits for visibility. | `mobile.tap("Settings")` |
|
|
102
|
+
| | `fill(target, val)` | **Smart Fill**: Finds nearest input to a label. | `mobile.fill("Username", "admin")` |
|
|
103
|
+
| | `fill_focused(val)` | **Active Field**: Types into currently focused input. | `mobile.fill_focused("secret")` |
|
|
104
|
+
| | `press_key(key)` | Presses hardware keys (Enter, Home, Back). | `mobile.press_key("Enter")` |
|
|
105
|
+
| **Gestures** | `open_notifications()`| Pulls down the notification shade (Left). | `mobile.open_notifications()` |
|
|
106
|
+
| | `open_quick_settings()`| Pulls down full Quick Settings (Center). | `mobile.open_quick_settings()` |
|
|
107
|
+
| | `scroll_down()` | Performs a smart downward scroll. | `mobile.scroll_down()` |
|
|
108
|
+
| **Media** | `screenshot(name)` | Saves a screenshot to `.flowpilot/screenshots/`. | `mobile.screenshot("step_1")` |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🤖 CLI Commands
|
|
113
|
+
|
|
114
|
+
| Command | Description |
|
|
115
|
+
| :--- | :--- |
|
|
116
|
+
| `flowpilot setup` | Initializes a new project folder and generates configurations. |
|
|
117
|
+
| `flowpilot interactive`| Start a live step-by-step automation session. |
|
|
118
|
+
| `flowpilot run` | Executes tests with smart logs. |
|
|
119
|
+
| `flowpilot devices` | Lists all connected physical Android devices. |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🛡 Engineering Principles
|
|
124
|
+
1. **Hide Complexity**: Zero YAML or runtime engine logic exposed.
|
|
125
|
+
2. **Text-First**: Locators prioritize display text over brittle XPaths.
|
|
126
|
+
3. **Hardware-Accelerated**: Native MP4 capture and Scrcpy mirroring with zero host lag.
|
|
127
|
+
4. **Resilient Reset**: Automated pre/post Home screen logic for repeatability.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from flowpilot.api.mobile import Mobile
|
|
2
|
+
from flowpilot.runtime.adb_engine import ADBRuntime
|
|
3
|
+
from flowpilot.runtime.executor import RuntimeExecutor
|
|
4
|
+
from flowpilot.utils.setup import SetupManager
|
|
5
|
+
|
|
6
|
+
__version__ = "1.1.9"
|
|
7
|
+
__all__ = ["Mobile", "ADBRuntime", "RuntimeExecutor", "SetupManager"]
|
|
File without changes
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
from flowpilot.commands.core import *
|
|
3
|
+
from flowpilot.runtime.queue import FlowContext
|
|
4
|
+
from flowpilot.runtime.executor import RuntimeExecutor
|
|
5
|
+
|
|
6
|
+
class SignatureAPI:
|
|
7
|
+
def __init__(self, context: FlowContext):
|
|
8
|
+
self._context = context
|
|
9
|
+
|
|
10
|
+
def draw(self):
|
|
11
|
+
# Internally use coordinate swipe actions
|
|
12
|
+
self._context.add_command(Command()) # Placeholder for DrawSignatureCommand
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
class RelationalAPI:
|
|
16
|
+
def __init__(self, anchor: str, relation: str, context: FlowContext, mobile):
|
|
17
|
+
self.anchor = anchor
|
|
18
|
+
self.relation = relation
|
|
19
|
+
self.target_text = None
|
|
20
|
+
self.target_type = None
|
|
21
|
+
self._context = context
|
|
22
|
+
self._mobile = mobile
|
|
23
|
+
|
|
24
|
+
def text(self, text: str):
|
|
25
|
+
"""Filters by specific text."""
|
|
26
|
+
self.target_text = text
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def type(self, type_name: str):
|
|
30
|
+
"""Filters by class name (e.g., 'EditText', 'Button')."""
|
|
31
|
+
self.target_type = type_name
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def input(self):
|
|
35
|
+
"""Shortcut to filter for EditText elements."""
|
|
36
|
+
self.target_type = "EditText"
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def tap(self):
|
|
40
|
+
self._context.add_command(RelationalCommand(
|
|
41
|
+
anchor=self.anchor,
|
|
42
|
+
relation=self.relation,
|
|
43
|
+
target_text=self.target_text,
|
|
44
|
+
target_type=self.target_type,
|
|
45
|
+
action="tap"
|
|
46
|
+
))
|
|
47
|
+
return self._mobile
|
|
48
|
+
|
|
49
|
+
def fill(self, value: str):
|
|
50
|
+
self._context.add_command(RelationalCommand(
|
|
51
|
+
anchor=self.anchor,
|
|
52
|
+
relation=self.relation,
|
|
53
|
+
target_text=self.target_text,
|
|
54
|
+
target_type=self.target_type,
|
|
55
|
+
action="fill",
|
|
56
|
+
value=value
|
|
57
|
+
))
|
|
58
|
+
return self._mobile
|
|
59
|
+
|
|
60
|
+
class ElementAPI:
|
|
61
|
+
def __init__(self, target: str, context: FlowContext, mobile):
|
|
62
|
+
self.target = target
|
|
63
|
+
self._context = context
|
|
64
|
+
self._mobile = mobile
|
|
65
|
+
|
|
66
|
+
def tap(self):
|
|
67
|
+
self._context.add_command(TapCommand(self.target))
|
|
68
|
+
return self._mobile
|
|
69
|
+
|
|
70
|
+
def fill(self, value: str):
|
|
71
|
+
self._context.add_command(FillCommand(self.target, value))
|
|
72
|
+
return self._mobile
|
|
73
|
+
|
|
74
|
+
# Relational entry points
|
|
75
|
+
def below(self, anchor: str):
|
|
76
|
+
return RelationalAPI(anchor, "below", self._context, self._mobile)
|
|
77
|
+
|
|
78
|
+
def above(self, anchor: str):
|
|
79
|
+
return RelationalAPI(anchor, "above", self._context, self._mobile)
|
|
80
|
+
|
|
81
|
+
def left_of(self, anchor: str):
|
|
82
|
+
return RelationalAPI(anchor, "left_of", self._context, self._mobile)
|
|
83
|
+
|
|
84
|
+
def right_of(self, anchor: str):
|
|
85
|
+
return RelationalAPI(anchor, "right_of", self._context, self._mobile)
|
|
86
|
+
|
|
87
|
+
class Mobile:
|
|
88
|
+
"""Core Mobile API for the FlowPilot framework."""
|
|
89
|
+
|
|
90
|
+
def __init__(self):
|
|
91
|
+
self._context = FlowContext()
|
|
92
|
+
self._executor = RuntimeExecutor()
|
|
93
|
+
self.signature = SignatureAPI(self._context)
|
|
94
|
+
|
|
95
|
+
# App Lifecycle
|
|
96
|
+
def launch_app(self, package: str = None):
|
|
97
|
+
self._context.add_command(LaunchAppCommand(package=package))
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def close_app(self):
|
|
101
|
+
self._context.add_command(CloseAppCommand())
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def restart_app(self):
|
|
105
|
+
self._context.add_command(RestartAppCommand())
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
# Relational Selectors (Maestro-style)
|
|
109
|
+
def below(self, anchor: str):
|
|
110
|
+
return RelationalAPI(anchor, "below", self._context, self)
|
|
111
|
+
|
|
112
|
+
def above(self, anchor: str):
|
|
113
|
+
return RelationalAPI(anchor, "above", self._context, self)
|
|
114
|
+
|
|
115
|
+
def left_of(self, anchor: str):
|
|
116
|
+
return RelationalAPI(anchor, "left_of", self._context, self)
|
|
117
|
+
|
|
118
|
+
def right_of(self, anchor: str):
|
|
119
|
+
return RelationalAPI(anchor, "right_of", self._context, self)
|
|
120
|
+
|
|
121
|
+
def navigate(self, url: str):
|
|
122
|
+
self._context.add_command(NavigateCommand(url))
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
# Core Interactions
|
|
126
|
+
def tap(self, target: str):
|
|
127
|
+
"""
|
|
128
|
+
Smart Tap:
|
|
129
|
+
1. Checks visibility.
|
|
130
|
+
2. Auto-scrolls if not found.
|
|
131
|
+
3. Retries until timeout.
|
|
132
|
+
"""
|
|
133
|
+
self._context.add_command(TapCommand(target))
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def fill(self, target: str, value: str):
|
|
137
|
+
"""
|
|
138
|
+
Smart Fill:
|
|
139
|
+
1. Finds label.
|
|
140
|
+
2. Calculates nearby input field.
|
|
141
|
+
3. Clears and types safely.
|
|
142
|
+
"""
|
|
143
|
+
self._context.add_command(FillCommand(target, value))
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def fill_focused(self, value: str):
|
|
147
|
+
"""Types text into the currently focused/active input field."""
|
|
148
|
+
self._context.add_command(FillFocusedCommand(value))
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def back(self):
|
|
152
|
+
self._context.add_command(BackCommand())
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def home(self):
|
|
156
|
+
"""Simulates the Home button press."""
|
|
157
|
+
self._context.add_command(HomeCommand())
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def hide_keyboard(self):
|
|
161
|
+
self._context.add_command(HideKeyboardCommand())
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def press_key(self, key: str):
|
|
165
|
+
self._context.add_command(PressKeyCommand(key))
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def clear(self, target: str):
|
|
169
|
+
self._context.add_command(ClearCommand(target))
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
# Assertions / State Check
|
|
173
|
+
def visible(self, target: str):
|
|
174
|
+
self._context.add_command(VisibleCommand(target))
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def not_visible(self, target: str):
|
|
178
|
+
self._context.add_command(NotVisibleCommand(target))
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def contains_text(self, text: str):
|
|
182
|
+
self._context.add_command(ContainsTextCommand(text))
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
# Gestures
|
|
186
|
+
def swipe(self, x1, y1, x2, y2, duration: int = 300):
|
|
187
|
+
self._context.add_command(SwipeCommand(x1, y1, x2, y2, duration))
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def open_notifications(self):
|
|
191
|
+
"""Swipes top-to-bottom from the left/center to open notifications."""
|
|
192
|
+
self.swipe(200, 10, 200, 1000)
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def open_widgets(self):
|
|
196
|
+
"""Swipes top-to-bottom from the right side to open widgets/control center."""
|
|
197
|
+
self.swipe(800, 10, 800, 1000)
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def open_quick_settings(self):
|
|
201
|
+
"""Swipes top-to-bottom from the center to open quick settings/notifications."""
|
|
202
|
+
self.swipe(540, 10, 540, 1000)
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
def swipe_right(self):
|
|
206
|
+
"""Performs a left-to-right horizontal swipe."""
|
|
207
|
+
self.swipe(100, 1000, 900, 1000)
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def swipe_left(self):
|
|
211
|
+
"""Performs a right-to-left horizontal swipe."""
|
|
212
|
+
self.swipe(900, 1000, 100, 1000)
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
# Discovery
|
|
216
|
+
def get_screen_text(self) -> list[str]:
|
|
217
|
+
"""
|
|
218
|
+
Retrieves all visible text strings from the current screen.
|
|
219
|
+
Forces execution of queued commands first.
|
|
220
|
+
"""
|
|
221
|
+
self.execute()
|
|
222
|
+
return self._executor.adb.get_all_text()
|
|
223
|
+
|
|
224
|
+
def find_text(self, text: str) -> bool:
|
|
225
|
+
"""Checks if a specific text is currently visible on screen."""
|
|
226
|
+
all_text = self.get_screen_text()
|
|
227
|
+
return any(text.lower() in t.lower() for t in all_text)
|
|
228
|
+
|
|
229
|
+
def list_elements(self, type: str = "button"):
|
|
230
|
+
"""
|
|
231
|
+
Returns a list of visible elements of a certain type.
|
|
232
|
+
Used for discovery and smart assertions.
|
|
233
|
+
"""
|
|
234
|
+
self.execute()
|
|
235
|
+
return self._executor.adb.get_all_text()
|
|
236
|
+
|
|
237
|
+
# Scrolling
|
|
238
|
+
def scroll_down(self):
|
|
239
|
+
self._context.add_command(ScrollCommand("down"))
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def scroll_up(self):
|
|
243
|
+
self._context.add_command(ScrollCommand("up"))
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
# Waits
|
|
247
|
+
def wait_for(self, condition: str, timeout: int = 30):
|
|
248
|
+
self._context.add_command(WaitCommand(condition, timeout))
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
# Media / Sensors
|
|
252
|
+
def capture_photo(self):
|
|
253
|
+
self._context.add_command(CapturePhotoCommand())
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
def screenshot(self, name: str = None):
|
|
257
|
+
self._context.add_command(ScreenshotCommand(name))
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
def visual_assert(self, baseline_name: str):
|
|
261
|
+
"""
|
|
262
|
+
Smart Visual Assertion:
|
|
263
|
+
Compares current screen with a previously saved baseline.
|
|
264
|
+
"""
|
|
265
|
+
logger.info(f"Visual Assertion requested: {baseline_name}")
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
# Fluent Smart Element APIs
|
|
269
|
+
def button(self, name: str) -> ElementAPI:
|
|
270
|
+
return ElementAPI(name, self._context, self)
|
|
271
|
+
|
|
272
|
+
def input(self, name: str) -> ElementAPI:
|
|
273
|
+
return ElementAPI(name, self._context, self)
|
|
274
|
+
|
|
275
|
+
def checkbox(self, name: str) -> ElementAPI:
|
|
276
|
+
return ElementAPI(name, self._context, self)
|
|
277
|
+
|
|
278
|
+
def dropdown(self, name: str) -> ElementAPI:
|
|
279
|
+
return ElementAPI(name, self._context, self)
|
|
280
|
+
|
|
281
|
+
# Execution (Internal or Implicit)
|
|
282
|
+
def execute(self):
|
|
283
|
+
"""Forces the execution of the command queue."""
|
|
284
|
+
self._executor.execute(self._context)
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.prompt import Prompt
|
|
4
|
+
from flowpilot.api.mobile import Mobile
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
class InteractiveSession:
|
|
10
|
+
"""
|
|
11
|
+
A Live REPL for FlowPilot.
|
|
12
|
+
Executes commands on the mobile device line-by-line.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.mobile = Mobile()
|
|
17
|
+
self.history = []
|
|
18
|
+
|
|
19
|
+
def start(self):
|
|
20
|
+
# Disable recording for interactive mode (Step Padding / User Request)
|
|
21
|
+
self.mobile._executor.config.record_video = False
|
|
22
|
+
|
|
23
|
+
console.print("[bold magenta]📱 FlowPilot Interactive Session Started[/bold magenta]")
|
|
24
|
+
console.print("Type your commands (e.g., [cyan]tap(\"Settings\")[/cyan]) and press Enter.")
|
|
25
|
+
console.print("Type [bold red]exit[/bold red] to quit.\n")
|
|
26
|
+
|
|
27
|
+
# Start a persistent session (one recording, one initial reset)
|
|
28
|
+
self.mobile._executor.start_session()
|
|
29
|
+
|
|
30
|
+
while True:
|
|
31
|
+
try:
|
|
32
|
+
user_input = Prompt.ask("[bold green]flowpilot[/bold green]")
|
|
33
|
+
|
|
34
|
+
if user_input.lower() in ["exit", "quit", "q"]:
|
|
35
|
+
console.print("[yellow]Closing interactive session...[/yellow]")
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
if not user_input.strip():
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
# Prepare the command execution
|
|
42
|
+
# We check if the method exists on mobile
|
|
43
|
+
method_name = user_input.split("(")[0].strip()
|
|
44
|
+
if not hasattr(self.mobile, method_name):
|
|
45
|
+
console.print(f"[bold red]Error:[/bold red] '{method_name}' is not a valid FlowPilot command.")
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
console.print(f"[dim]Executing: {user_input}...[/dim]")
|
|
49
|
+
|
|
50
|
+
# Execute the command
|
|
51
|
+
# We handle both fluent (returns Mobile) and non-fluent (returns value) calls
|
|
52
|
+
result = eval(f"self.mobile.{user_input}")
|
|
53
|
+
|
|
54
|
+
# If it's a fluent method, we need to call .execute() manually
|
|
55
|
+
# because the REPL is line-by-line.
|
|
56
|
+
from flowpilot.api.mobile import Mobile as MobileClass
|
|
57
|
+
if isinstance(result, MobileClass):
|
|
58
|
+
result.execute()
|
|
59
|
+
else:
|
|
60
|
+
# It was a discovery method like get_screen_text()
|
|
61
|
+
console.print(f"[bold blue]Result:[/bold blue] {result}")
|
|
62
|
+
|
|
63
|
+
self.history.append(user_input)
|
|
64
|
+
console.print("[bold green]✓ Done[/bold green]")
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
console.print(f"[bold red]Execution Error:[/bold red] {e}")
|
|
68
|
+
|
|
69
|
+
# Final cleanup (stop recording, final home reset)
|
|
70
|
+
self.mobile._executor.stop_session()
|
|
71
|
+
console.print("[bold magenta]Session Ended.[/bold magenta]")
|
|
72
|
+
|
|
73
|
+
if self.history:
|
|
74
|
+
console.print(f"\n[bold blue]Summary of your session:[/bold blue]")
|
|
75
|
+
for i, cmd in enumerate(self.history, 1):
|
|
76
|
+
console.print(f"{i}. {cmd}")
|