robot-appium-vision 0.1.2__py3-none-any.whl
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.
- appium_vision/__init__.py +3 -0
- appium_vision/cli.py +55 -0
- appium_vision/keywords.py +520 -0
- robot_appium_vision-0.1.2.dist-info/METADATA +75 -0
- robot_appium_vision-0.1.2.dist-info/RECORD +9 -0
- robot_appium_vision-0.1.2.dist-info/WHEEL +5 -0
- robot_appium_vision-0.1.2.dist-info/entry_points.txt +2 -0
- robot_appium_vision-0.1.2.dist-info/licenses/LICENSE +21 -0
- robot_appium_vision-0.1.2.dist-info/top_level.txt +1 -0
appium_vision/cli.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import venv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _venv_python(venv_dir):
|
|
8
|
+
"""Return python executable path inside venv."""
|
|
9
|
+
if os.name == "nt":
|
|
10
|
+
return os.path.join(venv_dir, "Scripts", "python.exe")
|
|
11
|
+
return os.path.join(venv_dir, "bin", "python")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def setup():
|
|
15
|
+
"""
|
|
16
|
+
Creates a virtual environment and installs robot-appium-vision into it.
|
|
17
|
+
"""
|
|
18
|
+
venv_dir = ".venv"
|
|
19
|
+
|
|
20
|
+
if not os.path.exists(venv_dir):
|
|
21
|
+
print("📦 Creating virtual environment...")
|
|
22
|
+
venv.create(venv_dir, with_pip=True)
|
|
23
|
+
else:
|
|
24
|
+
print("ℹ️ Virtual environment already exists")
|
|
25
|
+
|
|
26
|
+
python = _venv_python(venv_dir)
|
|
27
|
+
|
|
28
|
+
print("📥 Installing robot-appium-vision into virtual environment...")
|
|
29
|
+
subprocess.check_call([
|
|
30
|
+
python, "-m", "pip", "install", "--upgrade", "pip"
|
|
31
|
+
])
|
|
32
|
+
subprocess.check_call([
|
|
33
|
+
python, "-m", "pip", "install", "robot-appium-vision"
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
print("\n✅ Setup complete!")
|
|
37
|
+
print("\nNext steps:")
|
|
38
|
+
if os.name == "nt":
|
|
39
|
+
print(r" Activate venv: .\.venv\Scripts\Activate.ps1")
|
|
40
|
+
else:
|
|
41
|
+
print(" Activate venv: source .venv/bin/activate")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
if len(sys.argv) < 2:
|
|
46
|
+
print("Usage: appium-vision setup")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
command = sys.argv[1].lower()
|
|
50
|
+
|
|
51
|
+
if command == "setup":
|
|
52
|
+
setup()
|
|
53
|
+
else:
|
|
54
|
+
print(f"Unknown command: {command}")
|
|
55
|
+
print("Available commands: setup")
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
from appium import webdriver
|
|
2
|
+
from robot.api.deco import keyword
|
|
3
|
+
from appium.options.android import UiAutomator2Options
|
|
4
|
+
from robot.api import logger
|
|
5
|
+
from robot.libraries.BuiltIn import BuiltIn
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import subprocess
|
|
8
|
+
import configparser
|
|
9
|
+
import time
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import cv2
|
|
13
|
+
import shutil
|
|
14
|
+
import importlib.util
|
|
15
|
+
import pytesseract
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AppiumKeywords:
|
|
19
|
+
"""
|
|
20
|
+
Robot Framework keyword library for Android automation using Appium.
|
|
21
|
+
|
|
22
|
+
Capabilities:
|
|
23
|
+
- Multi-DUT Appium session management
|
|
24
|
+
- OCR-based text verification and tapping
|
|
25
|
+
- Image-based verification and clicking using OpenCV
|
|
26
|
+
- Coordinate-based tap actions
|
|
27
|
+
- Android shell command execution
|
|
28
|
+
- Safe swipe and scroll gestures
|
|
29
|
+
- Screen recording with embedded video reporting
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
"""
|
|
34
|
+
Initializes keyword library.
|
|
35
|
+
- Loads DUT configuration from configurations.ini
|
|
36
|
+
- Initializes driver dictionary
|
|
37
|
+
"""
|
|
38
|
+
self.drivers = {}
|
|
39
|
+
|
|
40
|
+
base_path = os.path.dirname(os.path.abspath(__file__))
|
|
41
|
+
ini_path = os.path.join(base_path, "..", "Configurations", "configurations.ini")
|
|
42
|
+
|
|
43
|
+
self.config = configparser.ConfigParser()
|
|
44
|
+
self.config.read(ini_path)
|
|
45
|
+
|
|
46
|
+
# Optional Tesseract override (recommended for PyPI users)
|
|
47
|
+
tesseract_cmd = os.getenv("TESSERACT_CMD")
|
|
48
|
+
if tesseract_cmd:
|
|
49
|
+
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
|
|
50
|
+
|
|
51
|
+
# Runtime dependency validation
|
|
52
|
+
try:
|
|
53
|
+
self._check_runtime_dependencies()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.warn(
|
|
56
|
+
f"⚠️ Dependency check skipped during initialization:\n{e}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------
|
|
60
|
+
@keyword
|
|
61
|
+
def get_device_id(self, dut_name):
|
|
62
|
+
"""
|
|
63
|
+
Returns DUT configuration section for the given DUT name.
|
|
64
|
+
|
|
65
|
+
Arguments:
|
|
66
|
+
- dut_name (str): Logical DUT name (Phone / Main / Cluster)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
- Config section containing device capabilities
|
|
70
|
+
|
|
71
|
+
Fails If:
|
|
72
|
+
- DUT section is not found
|
|
73
|
+
"""
|
|
74
|
+
section = f"DUT.{dut_name}"
|
|
75
|
+
if section not in self.config:
|
|
76
|
+
raise Exception(f"DUT section '{section}' not found")
|
|
77
|
+
return self.config[section]
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------
|
|
80
|
+
@keyword
|
|
81
|
+
def start_appium_session(self, dut_name):
|
|
82
|
+
"""
|
|
83
|
+
Starts or reuses an Appium session for the given DUT.
|
|
84
|
+
|
|
85
|
+
Maintains one Appium driver per DUT.
|
|
86
|
+
|
|
87
|
+
Arguments:
|
|
88
|
+
- dut_name (str): Logical DUT name
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
- Appium WebDriver instance
|
|
92
|
+
"""
|
|
93
|
+
if dut_name in self.drivers:
|
|
94
|
+
return self.drivers[dut_name]
|
|
95
|
+
|
|
96
|
+
caps = self.get_device_id(dut_name)
|
|
97
|
+
options = UiAutomator2Options().load_capabilities(caps)
|
|
98
|
+
|
|
99
|
+
driver = webdriver.Remote(
|
|
100
|
+
command_executor="http://127.0.0.1:4723",
|
|
101
|
+
options=options
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.drivers[dut_name] = driver
|
|
105
|
+
return driver
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------
|
|
108
|
+
@keyword
|
|
109
|
+
def stop_appium_session(self):
|
|
110
|
+
"""
|
|
111
|
+
Stops all active Appium sessions.
|
|
112
|
+
"""
|
|
113
|
+
for driver in self.drivers.values():
|
|
114
|
+
driver.quit()
|
|
115
|
+
self.drivers.clear()
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------
|
|
118
|
+
@keyword
|
|
119
|
+
def verify_text_appium_full(self, expected_text, dut_name):
|
|
120
|
+
"""
|
|
121
|
+
Verifies that exact visible text is present on screen using Appium.
|
|
122
|
+
|
|
123
|
+
Arguments:
|
|
124
|
+
- expected_text (str): Exact text to verify
|
|
125
|
+
- dut_name (str): Logical DUT name
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
- True if text is found
|
|
129
|
+
|
|
130
|
+
Fails If:
|
|
131
|
+
- Text is not present
|
|
132
|
+
"""
|
|
133
|
+
driver = self.start_appium_session(dut_name)
|
|
134
|
+
|
|
135
|
+
elements = driver.find_elements(
|
|
136
|
+
by="xpath",
|
|
137
|
+
value="//*[normalize-space(@text) != '']"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
visible_texts = [el.text.strip() for el in elements if el.text.strip()]
|
|
141
|
+
|
|
142
|
+
if expected_text in visible_texts:
|
|
143
|
+
logger.info(f"<b style='color:green'>Text verified:</b> {expected_text}", html=True)
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
raise AssertionError(f"Exact text '{expected_text}' not found")
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------
|
|
149
|
+
@keyword
|
|
150
|
+
def tap_by_coordinates(self, json_name, key_name, dut_name):
|
|
151
|
+
"""
|
|
152
|
+
Taps on screen using X,Y coordinates from a JSON file.
|
|
153
|
+
|
|
154
|
+
Arguments:
|
|
155
|
+
- json_name (str): JSON file name
|
|
156
|
+
- key_name (str): Key containing x,y values
|
|
157
|
+
- dut_name (str): Logical DUT name
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
- Success message
|
|
161
|
+
"""
|
|
162
|
+
driver = self.start_appium_session(dut_name)
|
|
163
|
+
project_root = BuiltIn().get_variable_value("${EXECDIR}")
|
|
164
|
+
|
|
165
|
+
json_file = os.path.join(project_root, "Resources", "Coordinates", json_name)
|
|
166
|
+
if not os.path.isfile(json_file):
|
|
167
|
+
raise AssertionError(f"JSON file not found: {json_file}")
|
|
168
|
+
|
|
169
|
+
with open(json_file) as f:
|
|
170
|
+
data = json.load(f)
|
|
171
|
+
|
|
172
|
+
if key_name not in data:
|
|
173
|
+
raise AssertionError(f"Key '{key_name}' not found")
|
|
174
|
+
|
|
175
|
+
x = int(data[key_name]["x"])
|
|
176
|
+
y = int(data[key_name]["y"])
|
|
177
|
+
|
|
178
|
+
driver.execute_script("mobile: clickGesture", {"x": x, "y": y})
|
|
179
|
+
return f"Tapped at ({x},{y}) on {dut_name}"
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------
|
|
182
|
+
@keyword
|
|
183
|
+
def tap_by_text(self, expected_text, dut_name):
|
|
184
|
+
"""
|
|
185
|
+
Taps on visible text using OCR instead of UI hierarchy.
|
|
186
|
+
|
|
187
|
+
Arguments:
|
|
188
|
+
- expected_text (str): Text to tap
|
|
189
|
+
- dut_name (str): Logical DUT name
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
- True if text is tapped
|
|
193
|
+
|
|
194
|
+
Fails If:
|
|
195
|
+
- Text not found via OCR
|
|
196
|
+
"""
|
|
197
|
+
driver = self.start_appium_session(dut_name)
|
|
198
|
+
output_dir = BuiltIn().get_variable_value("${OUTPUTDIR}")
|
|
199
|
+
screenshot_path = os.path.join(output_dir, "ocr_screen.png")
|
|
200
|
+
|
|
201
|
+
driver.save_screenshot(screenshot_path)
|
|
202
|
+
img = cv2.imread(screenshot_path)
|
|
203
|
+
|
|
204
|
+
ocr_data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)
|
|
205
|
+
|
|
206
|
+
for i, text in enumerate(ocr_data["text"]):
|
|
207
|
+
if text.strip() == expected_text:
|
|
208
|
+
x = ocr_data["left"][i]
|
|
209
|
+
y = ocr_data["top"][i]
|
|
210
|
+
w = ocr_data["width"][i]
|
|
211
|
+
h = ocr_data["height"][i]
|
|
212
|
+
|
|
213
|
+
driver.execute_script(
|
|
214
|
+
"mobile: clickGesture",
|
|
215
|
+
{"x": int(x + w / 2), "y": int(y + h / 2)}
|
|
216
|
+
)
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
raise AssertionError(f"Text '{expected_text}' not found via OCR")
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------
|
|
222
|
+
@keyword
|
|
223
|
+
def verify_image_element(self, image_name, dut_name, threshold=0.9):
|
|
224
|
+
"""
|
|
225
|
+
Verifies an image on screen using OpenCV template matching.
|
|
226
|
+
|
|
227
|
+
Arguments:
|
|
228
|
+
- image_name (str): Reference image
|
|
229
|
+
- dut_name (str): Logical DUT name
|
|
230
|
+
- threshold (float): Similarity threshold
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
- True if image is matched
|
|
234
|
+
"""
|
|
235
|
+
driver = self.start_appium_session(dut_name)
|
|
236
|
+
project_root = BuiltIn().get_variable_value("${EXECDIR}")
|
|
237
|
+
output_dir = BuiltIn().get_variable_value("${OUTPUTDIR}")
|
|
238
|
+
|
|
239
|
+
ref_img = cv2.imread(os.path.join(project_root, "Resources", "images", image_name))
|
|
240
|
+
screenshot_path = os.path.join(output_dir, f"verify_{time.time()}.png")
|
|
241
|
+
driver.save_screenshot(screenshot_path)
|
|
242
|
+
|
|
243
|
+
screen = cv2.imread(screenshot_path)
|
|
244
|
+
res = cv2.matchTemplate(
|
|
245
|
+
cv2.cvtColor(screen, cv2.COLOR_BGR2GRAY),
|
|
246
|
+
cv2.cvtColor(ref_img, cv2.COLOR_BGR2GRAY),
|
|
247
|
+
cv2.TM_CCOEFF_NORMED
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
_, max_val, _, _ = cv2.minMaxLoc(res)
|
|
251
|
+
if max_val >= threshold:
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
raise AssertionError(f"Image match failed: score={max_val:.3f}")
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------------
|
|
257
|
+
@keyword
|
|
258
|
+
def click_by_image(self, image_name, dut_name, threshold=0.8):
|
|
259
|
+
"""
|
|
260
|
+
Clicks on UI element using image recognition.
|
|
261
|
+
|
|
262
|
+
Arguments:
|
|
263
|
+
- image_name (str): Reference image
|
|
264
|
+
- dut_name (str): Logical DUT name
|
|
265
|
+
- threshold (float): Confidence threshold
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
- Click success message
|
|
269
|
+
"""
|
|
270
|
+
driver = self.start_appium_session(dut_name)
|
|
271
|
+
project_root = BuiltIn().get_variable_value("${EXECDIR}")
|
|
272
|
+
output_dir = BuiltIn().get_variable_value("${OUTPUTDIR}")
|
|
273
|
+
|
|
274
|
+
ref = cv2.imread(os.path.join(project_root, "Resources", "images", image_name))
|
|
275
|
+
screenshot = os.path.join(output_dir, f"click_{time.time()}.png")
|
|
276
|
+
driver.save_screenshot(screenshot)
|
|
277
|
+
|
|
278
|
+
screen = cv2.imread(screenshot)
|
|
279
|
+
res = cv2.matchTemplate(
|
|
280
|
+
cv2.cvtColor(screen, cv2.COLOR_BGR2GRAY),
|
|
281
|
+
cv2.cvtColor(ref, cv2.COLOR_BGR2GRAY),
|
|
282
|
+
cv2.TM_CCOEFF_NORMED
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
|
286
|
+
if max_val < threshold:
|
|
287
|
+
raise AssertionError("Image not found")
|
|
288
|
+
|
|
289
|
+
h, w = ref.shape[:2]
|
|
290
|
+
x = max_loc[0] + w // 2
|
|
291
|
+
y = max_loc[1] + h // 2
|
|
292
|
+
|
|
293
|
+
driver.execute_script("mobile: clickGesture", {"x": x, "y": y})
|
|
294
|
+
return f"Clicked image at ({x},{y})"
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------------
|
|
297
|
+
@keyword
|
|
298
|
+
def run_command(self, command, dut_name, timeout_ms=5000):
|
|
299
|
+
"""
|
|
300
|
+
Executes Android shell command using Appium.
|
|
301
|
+
|
|
302
|
+
Arguments:
|
|
303
|
+
- command (str): Shell command
|
|
304
|
+
- dut_name (str): Logical DUT name
|
|
305
|
+
- timeout_ms (int): Timeout
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
- Command output
|
|
309
|
+
"""
|
|
310
|
+
driver = self.start_appium_session(dut_name)
|
|
311
|
+
parts = command.split()
|
|
312
|
+
|
|
313
|
+
result = driver.execute_script(
|
|
314
|
+
"mobile: shell",
|
|
315
|
+
{"command": parts[0], "args": parts[1:], "timeout": timeout_ms}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if isinstance(result, dict):
|
|
319
|
+
return result.get("stdout", "").strip()
|
|
320
|
+
return result.strip()
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------
|
|
323
|
+
@keyword
|
|
324
|
+
def press_key(self, keycode, dut_name):
|
|
325
|
+
"""
|
|
326
|
+
Presses Android hardware/system key.
|
|
327
|
+
|
|
328
|
+
Arguments:
|
|
329
|
+
- keycode (int): Android keycode
|
|
330
|
+
- dut_name (str): Logical DUT name
|
|
331
|
+
"""
|
|
332
|
+
driver = self.start_appium_session(dut_name)
|
|
333
|
+
driver.execute_script(
|
|
334
|
+
"mobile: shell",
|
|
335
|
+
{"command": "input", "args": ["keyevent", str(keycode)]}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------
|
|
339
|
+
@keyword
|
|
340
|
+
def swipe_left_right(self, dut_name, direction="left", percent=0.9):
|
|
341
|
+
"""
|
|
342
|
+
Performs safe horizontal swipe.
|
|
343
|
+
|
|
344
|
+
Arguments:
|
|
345
|
+
- dut_name (str): Logical DUT name
|
|
346
|
+
- direction (str): left / right
|
|
347
|
+
- percent (float): Swipe distance
|
|
348
|
+
"""
|
|
349
|
+
driver = self.start_appium_session(dut_name)
|
|
350
|
+
size = driver.get_window_size()
|
|
351
|
+
|
|
352
|
+
driver.execute_script(
|
|
353
|
+
"mobile: scrollGesture",
|
|
354
|
+
{
|
|
355
|
+
"direction": direction,
|
|
356
|
+
"percent": percent,
|
|
357
|
+
"left": int(size["width"] * 0.1),
|
|
358
|
+
"top": int(size["height"] * 0.35),
|
|
359
|
+
"width": int(size["width"] * 0.8),
|
|
360
|
+
"height": int(size["height"] * 0.3),
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# ---------------------------------------------------------------------
|
|
365
|
+
@keyword
|
|
366
|
+
def scroll_top_bottom(self, dut_name, direction="down", percent=0.9):
|
|
367
|
+
"""
|
|
368
|
+
Performs safe vertical scroll.
|
|
369
|
+
|
|
370
|
+
Arguments:
|
|
371
|
+
- dut_name (str): Logical DUT name
|
|
372
|
+
- direction (str): up / down
|
|
373
|
+
- percent (float): Scroll distance
|
|
374
|
+
"""
|
|
375
|
+
driver = self.start_appium_session(dut_name)
|
|
376
|
+
size = driver.get_window_size()
|
|
377
|
+
|
|
378
|
+
driver.execute_script(
|
|
379
|
+
"mobile: scrollGesture",
|
|
380
|
+
{
|
|
381
|
+
"direction": direction,
|
|
382
|
+
"percent": percent,
|
|
383
|
+
"left": int(size["width"] * 0.1),
|
|
384
|
+
"top": int(size["height"] * 0.15),
|
|
385
|
+
"width": int(size["width"] * 0.8),
|
|
386
|
+
"height": int(size["height"] * 0.7),
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------
|
|
391
|
+
@keyword
|
|
392
|
+
def start_screen_recording(self, dut_name, test_name):
|
|
393
|
+
"""
|
|
394
|
+
Starts Android screen recording using adb.
|
|
395
|
+
|
|
396
|
+
Arguments:
|
|
397
|
+
- dut_name (str): Logical DUT name
|
|
398
|
+
- test_name (str): Test case name
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
- Device video path
|
|
402
|
+
"""
|
|
403
|
+
device_id = self.get_device_id(dut_name).get("device_id")
|
|
404
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
405
|
+
path = f"/sdcard/{device_id}_{timestamp}_{test_name}.mp4"
|
|
406
|
+
|
|
407
|
+
self._screen_proc = subprocess.Popen(
|
|
408
|
+
["adb", "-s", device_id, "shell", "screenrecord", path],
|
|
409
|
+
stdout=subprocess.DEVNULL,
|
|
410
|
+
stderr=subprocess.DEVNULL
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
self._device_video_path = path
|
|
414
|
+
self._device_id = device_id
|
|
415
|
+
return path
|
|
416
|
+
|
|
417
|
+
# ---------------------------------------------------------------------
|
|
418
|
+
@keyword
|
|
419
|
+
def stop_screen_recording(self, dut_name, local_video_path):
|
|
420
|
+
"""
|
|
421
|
+
Stops screen recording and pulls video to local system.
|
|
422
|
+
|
|
423
|
+
Arguments:
|
|
424
|
+
- dut_name (str): Logical DUT name
|
|
425
|
+
- local_video_path (str): Local save path
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
- Local video path
|
|
429
|
+
"""
|
|
430
|
+
if self._screen_proc:
|
|
431
|
+
self._screen_proc.terminate()
|
|
432
|
+
self._screen_proc.wait()
|
|
433
|
+
|
|
434
|
+
subprocess.run(
|
|
435
|
+
["adb", "-s", self._device_id, "pull", self._device_video_path, local_video_path],
|
|
436
|
+
check=True
|
|
437
|
+
)
|
|
438
|
+
return local_video_path
|
|
439
|
+
|
|
440
|
+
# ---------------------------------------------------------------------
|
|
441
|
+
@keyword
|
|
442
|
+
def Test_Video(self, video_path, width=480, title="Screen Recording"):
|
|
443
|
+
"""
|
|
444
|
+
Embeds video in Robot Framework HTML report.
|
|
445
|
+
|
|
446
|
+
Arguments:
|
|
447
|
+
- video_path (str): MP4 file path
|
|
448
|
+
- width (int): Video width
|
|
449
|
+
- title (str): Video title
|
|
450
|
+
"""
|
|
451
|
+
if not os.path.exists(video_path):
|
|
452
|
+
logger.warn(f"Video not found: {video_path}")
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
html = f"""
|
|
456
|
+
<b>{title}</b><br>
|
|
457
|
+
<video width="{width}" controls>
|
|
458
|
+
<source src="{video_path}" type="video/mp4">
|
|
459
|
+
</video>
|
|
460
|
+
"""
|
|
461
|
+
logger.info(html, html=True)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _check_runtime_dependencies(self):
|
|
465
|
+
"""
|
|
466
|
+
Performs runtime dependency checks.
|
|
467
|
+
|
|
468
|
+
- Verifies required Python packages
|
|
469
|
+
- Verifies optional system tools (adb, tesseract)
|
|
470
|
+
- Logs warnings instead of crashing where possible
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
# -------------------------------
|
|
474
|
+
# Required Python packages
|
|
475
|
+
# -------------------------------
|
|
476
|
+
required_packages = {
|
|
477
|
+
"robotframework": "robot",
|
|
478
|
+
"appium-python-client": "appium",
|
|
479
|
+
"selenium": "selenium",
|
|
480
|
+
"opencv-python": "cv2",
|
|
481
|
+
"pytesseract": "pytesseract",
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
for pkg_name, import_name in required_packages.items():
|
|
485
|
+
if importlib.util.find_spec(import_name) is None:
|
|
486
|
+
raise RuntimeError(
|
|
487
|
+
f"❌ Required dependency '{pkg_name}' is not installed.\n"
|
|
488
|
+
f"Install it using: pip install {pkg_name}"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
logger.info("✅ All required Python dependencies are installed")
|
|
492
|
+
|
|
493
|
+
# -------------------------------
|
|
494
|
+
# adb check (required for shell & recording)
|
|
495
|
+
# -------------------------------
|
|
496
|
+
if shutil.which("adb") is None:
|
|
497
|
+
logger.warn(
|
|
498
|
+
"⚠️ adb not found in PATH.\n"
|
|
499
|
+
"Keywords using shell commands and screen recording may fail."
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
logger.info("✅ adb detected")
|
|
503
|
+
|
|
504
|
+
# -------------------------------
|
|
505
|
+
# Tesseract OCR check (optional)
|
|
506
|
+
# -------------------------------
|
|
507
|
+
tesseract_path = (
|
|
508
|
+
os.getenv("TESSERACT_CMD")
|
|
509
|
+
or shutil.which("tesseract")
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
if not tesseract_path:
|
|
513
|
+
logger.warn(
|
|
514
|
+
"⚠️ Tesseract OCR not found.\n"
|
|
515
|
+
"OCR-based keywords (Tap By Text) will NOT work.\n"
|
|
516
|
+
"Install Tesseract and set TESSERACT_CMD environment variable."
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
pytesseract.pytesseract.tesseract_cmd = tesseract_path
|
|
520
|
+
logger.info(f"✅ Tesseract OCR detected at: {tesseract_path}")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robot-appium-vision
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Robot Framework Appium keyword library with OCR and image-based actions
|
|
5
|
+
Author-email: Khajavali <dudekulakhaja786@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Khajavali
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/khaja786431/robot-appium-vision
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Framework :: Robot Framework
|
|
31
|
+
Classifier: Topic :: Software Development :: Testing
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: robotframework
|
|
37
|
+
Requires-Dist: appium-python-client
|
|
38
|
+
Requires-Dist: selenium
|
|
39
|
+
Requires-Dist: opencv-python
|
|
40
|
+
Requires-Dist: pytesseract
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Robot Appium Vision Library
|
|
44
|
+
|
|
45
|
+
Advanced Robot Framework keyword library for Appium automation with:
|
|
46
|
+
- OCR-based text detection
|
|
47
|
+
- Image-based verification and clicking
|
|
48
|
+
- Coordinate tapping
|
|
49
|
+
- Safe scroll & swipe
|
|
50
|
+
- Android shell commands
|
|
51
|
+
- Screen recording with video embedding
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
pip install robot-appium-vision
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
*** Settings ***
|
|
61
|
+
Library AppiumKeywords
|
|
62
|
+
|
|
63
|
+
*** Test Cases ***
|
|
64
|
+
Verify Text
|
|
65
|
+
Verify Text Appium Full Settings Phone
|
|
66
|
+
|
|
67
|
+
## OCR Setup (Windows)
|
|
68
|
+
set TESSERACT_CMD=C:\Program Files\Tesseract-OCR\tesseract.exe
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Dependencies
|
|
72
|
+
- Appium Server
|
|
73
|
+
- Android device / emulator
|
|
74
|
+
- OpenCV
|
|
75
|
+
- Tesseract OCR
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
appium_vision/__init__.py,sha256=wpxfNgRSeFJ5QJ2q7XypR88onVWUD3URV7lcInZI9Ug,70
|
|
2
|
+
appium_vision/cli.py,sha256=Pzt3WAkoXBVv_FuxM6CeCqoighTrKBgBtPpkgw0dLOg,1471
|
|
3
|
+
appium_vision/keywords.py,sha256=itPnbee0fbizDFQiK-g3tw-edOtWUgF0GDwNCZT4kI8,17173
|
|
4
|
+
robot_appium_vision-0.1.2.dist-info/licenses/LICENSE,sha256=kFvwPgAMa_78-0tCdfeYtYnMzE-sHKzFZWOqwfn7d7s,1087
|
|
5
|
+
robot_appium_vision-0.1.2.dist-info/METADATA,sha256=IwZ3LPgkOrHQjvQbelqojyLwhVJbRQQN8zLzRWr0YUA,2680
|
|
6
|
+
robot_appium_vision-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
robot_appium_vision-0.1.2.dist-info/entry_points.txt,sha256=AbE8XLiapFO_BDhAexO9FTWCGY4T0qNAWdnnQNyUYOk,57
|
|
8
|
+
robot_appium_vision-0.1.2.dist-info/top_level.txt,sha256=7-hJE9FsccdYlLxmVdXwwC_MjKHRCvswV7NsJyiXjRc,14
|
|
9
|
+
robot_appium_vision-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khajavali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
appium_vision
|