cesium-ios 0.1.0__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.
- cesium_ios-0.1.0/PKG-INFO +100 -0
- cesium_ios-0.1.0/README.md +76 -0
- cesium_ios-0.1.0/cesium_ios/__init__.py +25 -0
- cesium_ios-0.1.0/cesium_ios/client.py +81 -0
- cesium_ios-0.1.0/cesium_ios/device.py +238 -0
- cesium_ios-0.1.0/cesium_ios/exceptions.py +22 -0
- cesium_ios-0.1.0/cesium_ios.egg-info/PKG-INFO +100 -0
- cesium_ios-0.1.0/cesium_ios.egg-info/SOURCES.txt +12 -0
- cesium_ios-0.1.0/cesium_ios.egg-info/dependency_links.txt +1 -0
- cesium_ios-0.1.0/cesium_ios.egg-info/requires.txt +1 -0
- cesium_ios-0.1.0/cesium_ios.egg-info/top_level.txt +1 -0
- cesium_ios-0.1.0/pyproject.toml +36 -0
- cesium_ios-0.1.0/setup.cfg +4 -0
- cesium_ios-0.1.0/setup.py +22 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cesium-ios
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Driverless, WDA-Free iOS Test Automation & Telemetry SDK for Linux and Windows
|
|
5
|
+
Author: Cesium Team
|
|
6
|
+
Author-email: Cesium Team <info@cesium-ios.org>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/cesium-ios/cesium-ios
|
|
9
|
+
Project-URL: Repository, https://github.com/cesium-ios/cesium-ios
|
|
10
|
+
Keywords: ios,automation,coredevice,driverless,testing,wda-free,telemetry
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: pymobiledevice3>=4.0.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
|
|
25
|
+
# cesium-ios Python SDK
|
|
26
|
+
|
|
27
|
+
**Driverless, 100% WDA-Free iOS Test Automation and Telemetry for Linux & Windows.**
|
|
28
|
+
|
|
29
|
+
Eliminates WebDriverAgent (WDA), macOS requirements, and Apple Developer accounts. Directly communicates with physical iPhones running iOS 17+ via CoreDevice, `universalhidservice`, and `axAuditDaemon`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📦 Installation
|
|
34
|
+
|
|
35
|
+
### From Local Source (Editable Mode):
|
|
36
|
+
```bash
|
|
37
|
+
cd cesium_sdk
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### From PyPI (When Published):
|
|
42
|
+
```bash
|
|
43
|
+
pip install cesium-ios
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🚀 Quickstart Example
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import time
|
|
52
|
+
from cesium_ios import IOSDevice
|
|
53
|
+
|
|
54
|
+
# Automatically connects to the attached device or reads os.environ["UDID"]
|
|
55
|
+
device = IOSDevice()
|
|
56
|
+
|
|
57
|
+
# 1. Clean slate: Return to Home
|
|
58
|
+
device.home()
|
|
59
|
+
|
|
60
|
+
# 2. Cold-launch an app or deep-link to a URL in < 25ms
|
|
61
|
+
device.launch_app("com.apple.Preferences")
|
|
62
|
+
time.sleep(2.0)
|
|
63
|
+
|
|
64
|
+
# 3. Tap on Wi-Fi (using coordinates or by element name)
|
|
65
|
+
device.tap(fx=0.50, fy=0.23)
|
|
66
|
+
|
|
67
|
+
# 4. Scroll Down & Up
|
|
68
|
+
device.scroll_down()
|
|
69
|
+
time.sleep(1.0)
|
|
70
|
+
device.scroll_up()
|
|
71
|
+
|
|
72
|
+
# 5. Terminate the app
|
|
73
|
+
device.terminate_app("Preferences")
|
|
74
|
+
device.home()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🔍 Smart Element Clicking (Zero Guesswork)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Automatically finds "Wi-Fi" on screen and taps its center
|
|
83
|
+
device.tap_element("Wi-Fi")
|
|
84
|
+
|
|
85
|
+
# Dump all elements on screen in ~78ms
|
|
86
|
+
elements = device.get_accessibility_elements()
|
|
87
|
+
for el in elements:
|
|
88
|
+
print(el.caption, el.bounds)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🛠️ Publishing to PyPI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install build twine
|
|
97
|
+
python -m build
|
|
98
|
+
twine check dist/*
|
|
99
|
+
twine upload dist/*
|
|
100
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# cesium-ios Python SDK
|
|
2
|
+
|
|
3
|
+
**Driverless, 100% WDA-Free iOS Test Automation and Telemetry for Linux & Windows.**
|
|
4
|
+
|
|
5
|
+
Eliminates WebDriverAgent (WDA), macOS requirements, and Apple Developer accounts. Directly communicates with physical iPhones running iOS 17+ via CoreDevice, `universalhidservice`, and `axAuditDaemon`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Installation
|
|
10
|
+
|
|
11
|
+
### From Local Source (Editable Mode):
|
|
12
|
+
```bash
|
|
13
|
+
cd cesium_sdk
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### From PyPI (When Published):
|
|
18
|
+
```bash
|
|
19
|
+
pip install cesium-ios
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🚀 Quickstart Example
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import time
|
|
28
|
+
from cesium_ios import IOSDevice
|
|
29
|
+
|
|
30
|
+
# Automatically connects to the attached device or reads os.environ["UDID"]
|
|
31
|
+
device = IOSDevice()
|
|
32
|
+
|
|
33
|
+
# 1. Clean slate: Return to Home
|
|
34
|
+
device.home()
|
|
35
|
+
|
|
36
|
+
# 2. Cold-launch an app or deep-link to a URL in < 25ms
|
|
37
|
+
device.launch_app("com.apple.Preferences")
|
|
38
|
+
time.sleep(2.0)
|
|
39
|
+
|
|
40
|
+
# 3. Tap on Wi-Fi (using coordinates or by element name)
|
|
41
|
+
device.tap(fx=0.50, fy=0.23)
|
|
42
|
+
|
|
43
|
+
# 4. Scroll Down & Up
|
|
44
|
+
device.scroll_down()
|
|
45
|
+
time.sleep(1.0)
|
|
46
|
+
device.scroll_up()
|
|
47
|
+
|
|
48
|
+
# 5. Terminate the app
|
|
49
|
+
device.terminate_app("Preferences")
|
|
50
|
+
device.home()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🔍 Smart Element Clicking (Zero Guesswork)
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Automatically finds "Wi-Fi" on screen and taps its center
|
|
59
|
+
device.tap_element("Wi-Fi")
|
|
60
|
+
|
|
61
|
+
# Dump all elements on screen in ~78ms
|
|
62
|
+
elements = device.get_accessibility_elements()
|
|
63
|
+
for el in elements:
|
|
64
|
+
print(el.caption, el.bounds)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 🛠️ Publishing to PyPI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install build twine
|
|
73
|
+
python -m build
|
|
74
|
+
twine check dist/*
|
|
75
|
+
twine upload dist/*
|
|
76
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cesium iOS Python SDK
|
|
3
|
+
=====================
|
|
4
|
+
Driverless, 100% WDA-Free iOS Test Automation and Telemetry for Linux and Windows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import EngineClient
|
|
8
|
+
from .device import AccessibilityElement, IOSDevice
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
CesiumConnectionError,
|
|
11
|
+
CesiumError,
|
|
12
|
+
DeviceNotFoundError,
|
|
13
|
+
ElementNotFoundError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"IOSDevice",
|
|
19
|
+
"EngineClient",
|
|
20
|
+
"AccessibilityElement",
|
|
21
|
+
"CesiumError",
|
|
22
|
+
"CesiumConnectionError",
|
|
23
|
+
"DeviceNotFoundError",
|
|
24
|
+
"ElementNotFoundError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level JSON-RPC client for the Cesium Native CoreDevice Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import socket
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from .exceptions import CesiumConnectionError, CesiumError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("cesium_ios.client")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EngineClient:
|
|
17
|
+
"""
|
|
18
|
+
Direct low-latency JSON-RPC 2.0 client talking to the C cesium-engine daemon (port 9876).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, host: str = "127.0.0.1", port: int = 9876, timeout: float = 6.0):
|
|
22
|
+
self.host = host
|
|
23
|
+
self.port = port
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
self._seq = 0
|
|
26
|
+
|
|
27
|
+
def call(self, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Executes an RPC method synchronously over a short-lived TCP socket.
|
|
30
|
+
"""
|
|
31
|
+
self._seq += 1
|
|
32
|
+
req_id = self._seq
|
|
33
|
+
payload = {
|
|
34
|
+
"jsonrpc": "2.0",
|
|
35
|
+
"method": method,
|
|
36
|
+
"params": params or {},
|
|
37
|
+
"id": req_id,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
41
|
+
s.settimeout(self.timeout)
|
|
42
|
+
try:
|
|
43
|
+
s.connect((self.host, self.port))
|
|
44
|
+
data = json.dumps(payload).encode("utf-8") + b"\n"
|
|
45
|
+
s.sendall(data)
|
|
46
|
+
|
|
47
|
+
raw_resp = b""
|
|
48
|
+
while True:
|
|
49
|
+
chunk = s.recv(4096)
|
|
50
|
+
if not chunk:
|
|
51
|
+
break
|
|
52
|
+
raw_resp += chunk
|
|
53
|
+
if b"\n" in chunk:
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if not raw_resp:
|
|
57
|
+
raise CesiumConnectionError(f"Empty response from engine at {self.host}:{self.port}")
|
|
58
|
+
|
|
59
|
+
parsed = json.loads(raw_resp.decode("utf-8"))
|
|
60
|
+
if not parsed.get("ok", True) or "error" in parsed:
|
|
61
|
+
err_msg = parsed.get("error", "Unknown engine error")
|
|
62
|
+
raise CesiumError(f"Engine method '{method}' failed: {err_msg}")
|
|
63
|
+
|
|
64
|
+
return parsed.get("result", {})
|
|
65
|
+
|
|
66
|
+
except socket.timeout:
|
|
67
|
+
raise CesiumConnectionError(f"Socket timed out while calling '{method}' on {self.host}:{self.port}")
|
|
68
|
+
except ConnectionRefusedError:
|
|
69
|
+
raise CesiumConnectionError(
|
|
70
|
+
f"Could not connect to cesium-engine at {self.host}:{self.port}. "
|
|
71
|
+
"Ensure cesium-engine is running."
|
|
72
|
+
)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
if isinstance(e, CesiumError):
|
|
75
|
+
raise
|
|
76
|
+
raise CesiumConnectionError(f"Network error communicating with cesium-engine: {e}") from e
|
|
77
|
+
finally:
|
|
78
|
+
try:
|
|
79
|
+
s.close()
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level Pythonic interface for physical iOS device automation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
10
|
+
|
|
11
|
+
from .client import EngineClient
|
|
12
|
+
from .exceptions import DeviceNotFoundError, ElementNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AccessibilityElement:
|
|
16
|
+
"""Represents an on-screen UIKit element returned by axAuditDaemon."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
caption: str = "",
|
|
21
|
+
label: str = "",
|
|
22
|
+
identifier: str = "",
|
|
23
|
+
element_type: str = "",
|
|
24
|
+
bounds: Optional[Dict[str, int]] = None,
|
|
25
|
+
screen_w: int = 390,
|
|
26
|
+
screen_h: int = 844,
|
|
27
|
+
):
|
|
28
|
+
self.caption = caption
|
|
29
|
+
self.label = label
|
|
30
|
+
self.identifier = identifier
|
|
31
|
+
self.element_type = element_type
|
|
32
|
+
self.bounds = bounds or {"x": 0, "y": 0, "width": 0, "height": 0}
|
|
33
|
+
self.screen_w = screen_w
|
|
34
|
+
self.screen_h = screen_h
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def center_fraction(self) -> tuple[float, float]:
|
|
38
|
+
"""Calculates normalized (fx, fy) center fractions for tap injection."""
|
|
39
|
+
x = self.bounds.get("x", 0)
|
|
40
|
+
y = self.bounds.get("y", 0)
|
|
41
|
+
w = self.bounds.get("width", 0)
|
|
42
|
+
h = self.bounds.get("height", 0)
|
|
43
|
+
sw = max(self.screen_w, 1)
|
|
44
|
+
sh = max(self.screen_h, 1)
|
|
45
|
+
fx = (x + w / 2.0) / float(sw)
|
|
46
|
+
fy = (y + h / 2.0) / float(sh)
|
|
47
|
+
return (max(0.0, min(1.0, fx)), max(0.0, min(1.0, fy)))
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return f"<AccessibilityElement type='{self.element_type}' caption='{self.caption}' bounds={self.bounds}>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IOSDevice:
|
|
54
|
+
"""
|
|
55
|
+
Primary interface for automating physical iOS devices (iOS 17+) driverless.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
udid: Optional[str] = None,
|
|
61
|
+
host: str = "127.0.0.1",
|
|
62
|
+
port: int = 9876,
|
|
63
|
+
):
|
|
64
|
+
self.host = host
|
|
65
|
+
self.port = port
|
|
66
|
+
self.udid = udid or os.environ.get("UDID") or self._auto_detect_udid()
|
|
67
|
+
self.client = EngineClient(host=host, port=port)
|
|
68
|
+
self.screen_width = 390
|
|
69
|
+
self.screen_height = 844
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _auto_detect_udid() -> str:
|
|
73
|
+
"""Attempts to auto-detect the connected device UDID via usbmuxd."""
|
|
74
|
+
try:
|
|
75
|
+
from pymobiledevice3.usbmux import list_devices
|
|
76
|
+
devs = asyncio.run(list_devices())
|
|
77
|
+
if devs:
|
|
78
|
+
return devs[0].serial
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return "default"
|
|
82
|
+
|
|
83
|
+
# =========================================================================
|
|
84
|
+
# APP LIFECYCLE & PROCESS MANAGEMENT
|
|
85
|
+
# =========================================================================
|
|
86
|
+
def launch_app(self, bundle_id: str, url: Optional[str] = None) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Launches an application cold via CoreDevice.
|
|
89
|
+
If `url` is provided, opens deep link directly in under 25ms.
|
|
90
|
+
"""
|
|
91
|
+
params = {"bundle_id": bundle_id}
|
|
92
|
+
if url:
|
|
93
|
+
params["url"] = url
|
|
94
|
+
return self.client.call("launch_app", params)
|
|
95
|
+
|
|
96
|
+
def list_processes(self) -> List[Dict[str, Any]]:
|
|
97
|
+
"""Returns all running processes on the device with PIDs and executable paths."""
|
|
98
|
+
res = self.client.call("list_processes")
|
|
99
|
+
return res.get("processTokens", [])
|
|
100
|
+
|
|
101
|
+
def terminate_process(self, pid: int, signal: int = 9) -> Dict[str, Any]:
|
|
102
|
+
"""Terminates a process by its PID."""
|
|
103
|
+
return self.client.call("terminate_app", {"pid": pid, "signal": signal})
|
|
104
|
+
|
|
105
|
+
def terminate_app(self, name_or_bundle_id: str) -> int:
|
|
106
|
+
"""
|
|
107
|
+
Finds any running processes matching the name or bundle ID and terminates them.
|
|
108
|
+
Returns the number of terminated processes.
|
|
109
|
+
"""
|
|
110
|
+
target = name_or_bundle_id.lower()
|
|
111
|
+
tokens = self.list_processes()
|
|
112
|
+
killed = 0
|
|
113
|
+
for tok in tokens:
|
|
114
|
+
exe = tok.get("executableURL", {}).get("relative", "")
|
|
115
|
+
if target in exe.lower():
|
|
116
|
+
pid = tok.get("processIdentifier")
|
|
117
|
+
if pid:
|
|
118
|
+
self.terminate_process(pid)
|
|
119
|
+
killed += 1
|
|
120
|
+
return killed
|
|
121
|
+
|
|
122
|
+
# =========================================================================
|
|
123
|
+
# HARDWARE GESTURES & BUTTONS
|
|
124
|
+
# =========================================================================
|
|
125
|
+
def home(self) -> Dict[str, Any]:
|
|
126
|
+
"""Simulates native Home gesture (returns to SpringBoard)."""
|
|
127
|
+
return self.client.call("home")
|
|
128
|
+
|
|
129
|
+
def app_switcher(self) -> Dict[str, Any]:
|
|
130
|
+
"""Simulates native App Switcher gesture."""
|
|
131
|
+
return self.client.call("app_switcher")
|
|
132
|
+
|
|
133
|
+
# =========================================================================
|
|
134
|
+
# TOUCH & GESTURE CONTROL (HARDWARE DIGITIZER)
|
|
135
|
+
# =========================================================================
|
|
136
|
+
def tap(self, fx: float, fy: float, dwell_ms: int = 80) -> Dict[str, Any]:
|
|
137
|
+
"""
|
|
138
|
+
Injects a physical hardware digitizer tap at normalized fractions (0.0 to 1.0).
|
|
139
|
+
"""
|
|
140
|
+
return self.client.call("swipe", {
|
|
141
|
+
"fx0": fx, "fy0": fy,
|
|
142
|
+
"fx1": fx, "fy1": fy,
|
|
143
|
+
"duration_ms": dwell_ms
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
def swipe(
|
|
147
|
+
self,
|
|
148
|
+
fx0: float,
|
|
149
|
+
fy0: float,
|
|
150
|
+
fx1: float,
|
|
151
|
+
fy1: float,
|
|
152
|
+
duration_ms: int = 300,
|
|
153
|
+
) -> Dict[str, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Injects a physical hardware digitizer swipe between two normalized points.
|
|
156
|
+
"""
|
|
157
|
+
return self.client.call("swipe", {
|
|
158
|
+
"x0": fx0, "y0": fy0,
|
|
159
|
+
"x1": fx1, "y1": fy1,
|
|
160
|
+
"duration_ms": duration_ms
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
def scroll_down(self, duration_ms: int = 300) -> Dict[str, Any]:
|
|
164
|
+
"""Performs a standard vertical scroll down gesture."""
|
|
165
|
+
return self.swipe(0.50, 0.70, 0.50, 0.30, duration_ms=duration_ms)
|
|
166
|
+
|
|
167
|
+
def scroll_up(self, duration_ms: int = 300) -> Dict[str, Any]:
|
|
168
|
+
"""Performs a standard vertical scroll up gesture."""
|
|
169
|
+
return self.swipe(0.50, 0.30, 0.50, 0.70, duration_ms=duration_ms)
|
|
170
|
+
|
|
171
|
+
# =========================================================================
|
|
172
|
+
# ACCESSIBILITY INSPECTION & SMART CLICKING
|
|
173
|
+
# =========================================================================
|
|
174
|
+
def get_accessibility_elements(self) -> List[AccessibilityElement]:
|
|
175
|
+
"""
|
|
176
|
+
Queries Apple's native axAuditDaemon to dump all current on-screen UIKit elements.
|
|
177
|
+
"""
|
|
178
|
+
elements = []
|
|
179
|
+
try:
|
|
180
|
+
from pymobiledevice3.lockdown import create_using_usbmux
|
|
181
|
+
from pymobiledevice3.services.accessibilityaudit import AccessibilityAudit
|
|
182
|
+
|
|
183
|
+
async def _dump():
|
|
184
|
+
nonlocal elements
|
|
185
|
+
lockdown = await create_using_usbmux(self.udid)
|
|
186
|
+
audit = AccessibilityAudit(lockdown)
|
|
187
|
+
try:
|
|
188
|
+
first_pid = None
|
|
189
|
+
async for el in audit.iter_elements():
|
|
190
|
+
pid = getattr(el, 'platform_identifier', '')
|
|
191
|
+
if first_pid is None:
|
|
192
|
+
first_pid = pid
|
|
193
|
+
elif pid == first_pid:
|
|
194
|
+
break
|
|
195
|
+
caption = el.caption or ""
|
|
196
|
+
elements.append(AccessibilityElement(
|
|
197
|
+
caption=caption,
|
|
198
|
+
label=getattr(el, 'label', caption),
|
|
199
|
+
identifier=getattr(el, 'identifier', ''),
|
|
200
|
+
element_type=getattr(el, 'type', 'Element'),
|
|
201
|
+
bounds=getattr(el, 'bounds', None),
|
|
202
|
+
screen_w=self.screen_width,
|
|
203
|
+
screen_h=self.screen_height,
|
|
204
|
+
))
|
|
205
|
+
finally:
|
|
206
|
+
await audit.close()
|
|
207
|
+
|
|
208
|
+
asyncio.run(_dump())
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
return elements
|
|
212
|
+
|
|
213
|
+
def find_element(self, text_or_predicate: Union[str, Callable[[AccessibilityElement], bool]]) -> AccessibilityElement:
|
|
214
|
+
"""
|
|
215
|
+
Finds the first on-screen element matching the text caption or custom predicate.
|
|
216
|
+
"""
|
|
217
|
+
elements = self.get_accessibility_elements()
|
|
218
|
+
if isinstance(text_or_predicate, str):
|
|
219
|
+
needle = text_or_predicate.lower()
|
|
220
|
+
predicate = lambda el: needle in el.caption.lower() or needle in el.label.lower() or needle in el.identifier.lower()
|
|
221
|
+
else:
|
|
222
|
+
predicate = text_or_predicate
|
|
223
|
+
|
|
224
|
+
for el in elements:
|
|
225
|
+
if predicate(el):
|
|
226
|
+
return el
|
|
227
|
+
raise ElementNotFoundError(f"No accessibility element matched '{text_or_predicate}'")
|
|
228
|
+
|
|
229
|
+
def tap_element(self, text_or_element: Union[str, AccessibilityElement]) -> Dict[str, Any]:
|
|
230
|
+
"""
|
|
231
|
+
Finds an element by text/caption and taps its geometric center automatically!
|
|
232
|
+
"""
|
|
233
|
+
if isinstance(text_or_element, str):
|
|
234
|
+
el = self.find_element(text_or_element)
|
|
235
|
+
else:
|
|
236
|
+
el = text_or_element
|
|
237
|
+
fx, fy = el.center_fraction
|
|
238
|
+
return self.tap(fx, fy)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions for the Cesium iOS SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class CesiumError(Exception):
|
|
6
|
+
"""Base exception for all Cesium SDK errors."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CesiumConnectionError(CesiumError):
|
|
11
|
+
"""Raised when communication with cesium-engine fails or times out."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeviceNotFoundError(CesiumError):
|
|
16
|
+
"""Raised when a requested device UDID cannot be found or attached."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElementNotFoundError(CesiumError):
|
|
21
|
+
"""Raised when an accessibility element matching the query cannot be located."""
|
|
22
|
+
pass
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cesium-ios
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Driverless, WDA-Free iOS Test Automation & Telemetry SDK for Linux and Windows
|
|
5
|
+
Author: Cesium Team
|
|
6
|
+
Author-email: Cesium Team <info@cesium-ios.org>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/cesium-ios/cesium-ios
|
|
9
|
+
Project-URL: Repository, https://github.com/cesium-ios/cesium-ios
|
|
10
|
+
Keywords: ios,automation,coredevice,driverless,testing,wda-free,telemetry
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: pymobiledevice3>=4.0.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
|
|
25
|
+
# cesium-ios Python SDK
|
|
26
|
+
|
|
27
|
+
**Driverless, 100% WDA-Free iOS Test Automation and Telemetry for Linux & Windows.**
|
|
28
|
+
|
|
29
|
+
Eliminates WebDriverAgent (WDA), macOS requirements, and Apple Developer accounts. Directly communicates with physical iPhones running iOS 17+ via CoreDevice, `universalhidservice`, and `axAuditDaemon`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📦 Installation
|
|
34
|
+
|
|
35
|
+
### From Local Source (Editable Mode):
|
|
36
|
+
```bash
|
|
37
|
+
cd cesium_sdk
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### From PyPI (When Published):
|
|
42
|
+
```bash
|
|
43
|
+
pip install cesium-ios
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🚀 Quickstart Example
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import time
|
|
52
|
+
from cesium_ios import IOSDevice
|
|
53
|
+
|
|
54
|
+
# Automatically connects to the attached device or reads os.environ["UDID"]
|
|
55
|
+
device = IOSDevice()
|
|
56
|
+
|
|
57
|
+
# 1. Clean slate: Return to Home
|
|
58
|
+
device.home()
|
|
59
|
+
|
|
60
|
+
# 2. Cold-launch an app or deep-link to a URL in < 25ms
|
|
61
|
+
device.launch_app("com.apple.Preferences")
|
|
62
|
+
time.sleep(2.0)
|
|
63
|
+
|
|
64
|
+
# 3. Tap on Wi-Fi (using coordinates or by element name)
|
|
65
|
+
device.tap(fx=0.50, fy=0.23)
|
|
66
|
+
|
|
67
|
+
# 4. Scroll Down & Up
|
|
68
|
+
device.scroll_down()
|
|
69
|
+
time.sleep(1.0)
|
|
70
|
+
device.scroll_up()
|
|
71
|
+
|
|
72
|
+
# 5. Terminate the app
|
|
73
|
+
device.terminate_app("Preferences")
|
|
74
|
+
device.home()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🔍 Smart Element Clicking (Zero Guesswork)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Automatically finds "Wi-Fi" on screen and taps its center
|
|
83
|
+
device.tap_element("Wi-Fi")
|
|
84
|
+
|
|
85
|
+
# Dump all elements on screen in ~78ms
|
|
86
|
+
elements = device.get_accessibility_elements()
|
|
87
|
+
for el in elements:
|
|
88
|
+
print(el.caption, el.bounds)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🛠️ Publishing to PyPI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install build twine
|
|
97
|
+
python -m build
|
|
98
|
+
twine check dist/*
|
|
99
|
+
twine upload dist/*
|
|
100
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
cesium_ios/__init__.py
|
|
5
|
+
cesium_ios/client.py
|
|
6
|
+
cesium_ios/device.py
|
|
7
|
+
cesium_ios/exceptions.py
|
|
8
|
+
cesium_ios.egg-info/PKG-INFO
|
|
9
|
+
cesium_ios.egg-info/SOURCES.txt
|
|
10
|
+
cesium_ios.egg-info/dependency_links.txt
|
|
11
|
+
cesium_ios.egg-info/requires.txt
|
|
12
|
+
cesium_ios.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymobiledevice3>=4.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cesium_ios
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cesium-ios"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Driverless, WDA-Free iOS Test Automation & Telemetry SDK for Linux and Windows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Cesium Team", email = "info@cesium-ios.org" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pymobiledevice3>=4.0.0",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["ios", "automation", "coredevice", "driverless", "testing", "wda-free", "telemetry"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Topic :: Software Development :: Testing",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/cesium-ios/cesium-ios"
|
|
32
|
+
Repository = "https://github.com/cesium-ios/cesium-ios"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["."]
|
|
36
|
+
include = ["cesium_ios*"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="cesium-ios",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Driverless, WDA-Free iOS Test Automation & Telemetry SDK",
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Cesium Team",
|
|
10
|
+
packages=find_packages(),
|
|
11
|
+
python_requires=">=3.9",
|
|
12
|
+
install_requires=[
|
|
13
|
+
"pymobiledevice3>=4.0.0",
|
|
14
|
+
],
|
|
15
|
+
classifiers=[
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Software Development :: Testing",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
],
|
|
22
|
+
)
|