uiautodev 0.3.3__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.
Potentially problematic release.
This version of uiautodev might be problematic. Click here for more details.
- uiautodev/__init__.py +12 -0
- uiautodev/__main__.py +10 -0
- uiautodev/app.py +92 -0
- uiautodev/appium_proxy.py +53 -0
- uiautodev/case.py +137 -0
- uiautodev/cli.py +171 -0
- uiautodev/command_proxy.py +154 -0
- uiautodev/command_types.py +89 -0
- uiautodev/driver/android.py +228 -0
- uiautodev/driver/appium.py +136 -0
- uiautodev/driver/base_driver.py +76 -0
- uiautodev/driver/ios.py +114 -0
- uiautodev/driver/mock.py +74 -0
- uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- uiautodev/driver/udt/udt.py +259 -0
- uiautodev/exceptions.py +32 -0
- uiautodev/model.py +37 -0
- uiautodev/provider.py +76 -0
- uiautodev/router/device.py +104 -0
- uiautodev/router/xml.py +28 -0
- uiautodev/static/demo.html +34 -0
- uiautodev/utils/common.py +166 -0
- uiautodev/utils/exceptions.py +43 -0
- uiautodev/utils/usbmux.py +485 -0
- uiautodev-0.3.3.dist-info/LICENSE +21 -0
- uiautodev-0.3.3.dist-info/METADATA +56 -0
- uiautodev-0.3.3.dist-info/RECORD +29 -0
- uiautodev-0.3.3.dist-info/WHEEL +4 -0
- uiautodev-0.3.3.dist-info/entry_points.txt +4 -0
uiautodev/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Mon Mar 04 2024 14:28:53 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("uiautodev")
|
|
11
|
+
except PackageNotFoundError:
|
|
12
|
+
__version__ = "0.0.0"
|
uiautodev/__main__.py
ADDED
uiautodev/app.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Sun Feb 18 2024 13:48:55 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import signal
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
from fastapi.responses import FileResponse, RedirectResponse
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from uiautodev import __version__
|
|
20
|
+
from uiautodev.provider import AndroidProvider, IOSProvider, MockProvider
|
|
21
|
+
from uiautodev.router.device import make_router
|
|
22
|
+
from uiautodev.router.xml import router as xml_router
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
app = FastAPI()
|
|
27
|
+
|
|
28
|
+
app.add_middleware(
|
|
29
|
+
CORSMiddleware,
|
|
30
|
+
allow_origins=["*"],
|
|
31
|
+
allow_credentials=True,
|
|
32
|
+
allow_methods=["GET", "POST"],
|
|
33
|
+
allow_headers=["*"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
android_router = make_router(AndroidProvider())
|
|
37
|
+
ios_router = make_router(IOSProvider())
|
|
38
|
+
mock_router = make_router(MockProvider())
|
|
39
|
+
|
|
40
|
+
app.include_router(mock_router, prefix="/api/mock", tags=["mock"])
|
|
41
|
+
|
|
42
|
+
if os.environ.get("UIAUTODEV_MOCK"):
|
|
43
|
+
app.include_router(mock_router, prefix="/api/android", tags=["mock"])
|
|
44
|
+
app.include_router(mock_router, prefix="/api/ios", tags=["mock"])
|
|
45
|
+
else:
|
|
46
|
+
app.include_router(android_router, prefix="/api/android", tags=["android"])
|
|
47
|
+
app.include_router(ios_router, prefix="/api/ios", tags=["ios"])
|
|
48
|
+
|
|
49
|
+
app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InfoResponse(BaseModel):
|
|
53
|
+
version: str
|
|
54
|
+
description: str
|
|
55
|
+
platform: str
|
|
56
|
+
code_language: str
|
|
57
|
+
cwd: str
|
|
58
|
+
drivers: List[str]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.get("/api/info")
|
|
62
|
+
def info() -> InfoResponse:
|
|
63
|
+
"""Information about the application"""
|
|
64
|
+
return InfoResponse(
|
|
65
|
+
version=__version__,
|
|
66
|
+
description="client for https://uiauto.dev",
|
|
67
|
+
platform=platform.system(), # Linux | Darwin | Windows
|
|
68
|
+
code_language="Python",
|
|
69
|
+
cwd=os.getcwd(),
|
|
70
|
+
drivers=["android"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.get("/shutdown")
|
|
75
|
+
def shutdown() -> str:
|
|
76
|
+
"""Shutdown the server"""
|
|
77
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
78
|
+
return "Server shutting down..."
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.get("/demo")
|
|
82
|
+
def demo() -> str:
|
|
83
|
+
"""Demo endpoint"""
|
|
84
|
+
static_dir = Path(__file__).parent / "static"
|
|
85
|
+
print(static_dir / "demo.html")
|
|
86
|
+
return FileResponse(static_dir / "demo.html")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.get("/")
|
|
90
|
+
def index_redirect():
|
|
91
|
+
""" redirect to official homepage """
|
|
92
|
+
return RedirectResponse("https://uiauto.dev")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Tue Mar 19 2024 22:23:37 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from fastapi import FastAPI, Request, Response
|
|
11
|
+
|
|
12
|
+
app = FastAPI()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Retrieve the target URL from the command line arguments
|
|
16
|
+
try:
|
|
17
|
+
TARGET_URL = sys.argv[1]
|
|
18
|
+
except IndexError:
|
|
19
|
+
print("Usage: python proxy_server.py <target_url>")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"])
|
|
23
|
+
async def proxy(request: Request, path: str):
|
|
24
|
+
# Construct the full URL to forward the request to
|
|
25
|
+
if path.endswith('/execute/sync'):
|
|
26
|
+
# 旧版appium处理不好这个请求,直接返回404, unknown command
|
|
27
|
+
# 目前browserstack也不支持这个请求
|
|
28
|
+
return Response(content=b'{"value": {"error": "unknown command", "message": "unknown command", "stacktrace": "UnknownCommandError"}}', status_code=404)
|
|
29
|
+
full_url = f"{TARGET_URL}/{path}"
|
|
30
|
+
body = await request.body()
|
|
31
|
+
print("Forwarding to", request.method, full_url)
|
|
32
|
+
print("==> BODY <==")
|
|
33
|
+
print(body)
|
|
34
|
+
# Include original headers in the request
|
|
35
|
+
headers = {k: v for k, v in request.headers.items() if k != 'host'}
|
|
36
|
+
|
|
37
|
+
# Forward the request to the target server
|
|
38
|
+
async with httpx.AsyncClient(timeout=120) as client:
|
|
39
|
+
resp = await client.request(
|
|
40
|
+
method=request.method,
|
|
41
|
+
url=full_url,
|
|
42
|
+
headers=headers,
|
|
43
|
+
data=body,
|
|
44
|
+
follow_redirects=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Return the response received from the target server
|
|
48
|
+
return Response(content=resp.content, status_code=resp.status_code, headers=dict(resp.headers))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
import uvicorn
|
|
53
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uiautodev/case.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Sat Apr 13 2024 22:35:03 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import enum
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Union
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from uiautodev import command_proxy
|
|
14
|
+
from uiautodev.command_types import Command
|
|
15
|
+
from uiautodev.driver.base_driver import BaseDriver
|
|
16
|
+
from uiautodev.provider import AndroidProvider
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
class CommandStep(BaseModel):
|
|
21
|
+
method: Union[str, Command]
|
|
22
|
+
params: Dict[str, str]
|
|
23
|
+
skip: bool = False
|
|
24
|
+
ignore_error: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CompareEnum(str, enum.Enum):
|
|
28
|
+
EQUAL = "equal"
|
|
29
|
+
CONTAINS = "contains"
|
|
30
|
+
NOT_EQUAL = "not_equal"
|
|
31
|
+
NOT_CONTAINS = "not_contains"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CompareCheckStep(BaseModel):
|
|
35
|
+
method: CompareEnum
|
|
36
|
+
value_a: str
|
|
37
|
+
value_b: str
|
|
38
|
+
skip: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_driver_command(driver: BaseDriver, command: Command, params: dict):
|
|
42
|
+
model = command_proxy.get_command_params_type(command)
|
|
43
|
+
params_obj = model.model_validate(params) if params else None
|
|
44
|
+
# print("Params:", params, params_obj)
|
|
45
|
+
result = command_proxy.send_command(driver, command, params_obj)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
def run():
|
|
49
|
+
# all params key and value should be string
|
|
50
|
+
# Step中
|
|
51
|
+
# 入参类型在为前端保存一份,后端需要同步兼容
|
|
52
|
+
# params所有的key和value都是string类型
|
|
53
|
+
# 出参类型支持重命名 result
|
|
54
|
+
# - key: string, old_key: string, desc: string
|
|
55
|
+
# eg. "WIDTH", "width", "屏幕宽度"
|
|
56
|
+
steps = [
|
|
57
|
+
CommandStep(
|
|
58
|
+
method=Command.APP_LAUNCH,
|
|
59
|
+
params= {
|
|
60
|
+
"package": "com.saucelabs.mydemoapp.android",
|
|
61
|
+
"stop": "true" # bool
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
CommandStep(
|
|
65
|
+
method=Command.GET_WINDOW_SIZE,
|
|
66
|
+
result_trans=[
|
|
67
|
+
dict(key="WIDTH", result_key="width", desc="屏幕宽度"),
|
|
68
|
+
]
|
|
69
|
+
),
|
|
70
|
+
CommandStep(
|
|
71
|
+
method=Command.ECHO,
|
|
72
|
+
params={
|
|
73
|
+
"message": "WindowWidth is {{WIDTH}}",
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
CommandStep(
|
|
77
|
+
method=Command.CLICK_ELEMENT,
|
|
78
|
+
params={
|
|
79
|
+
"by": "id",
|
|
80
|
+
"value": "com.saucelabs.mydemoapp.android:id/productIV",
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
CommandStep(
|
|
84
|
+
method=Command.CLICK_ELEMENT,
|
|
85
|
+
params={
|
|
86
|
+
"by": "id",
|
|
87
|
+
"value": "com.saucelabs.mydemoapp.android:id/plusIV",
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
CommandStep(
|
|
91
|
+
method=Command.CLICK_ELEMENT,
|
|
92
|
+
params={
|
|
93
|
+
"by": "id",
|
|
94
|
+
"value": "com.saucelabs.mydemoapp.android:id/cartBt",
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
CommandStep(
|
|
98
|
+
method=Command.CLICK_ELEMENT,
|
|
99
|
+
params={
|
|
100
|
+
"by": "id",
|
|
101
|
+
"value": "com.saucelabs.mydemoapp.android:id/cartIV",
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
CommandStep(
|
|
105
|
+
method=Command.FIND_ELEMENT,
|
|
106
|
+
params={
|
|
107
|
+
"by": "text",
|
|
108
|
+
"value": "Proceed To Checkout",
|
|
109
|
+
},
|
|
110
|
+
skip=True,
|
|
111
|
+
),
|
|
112
|
+
CompareCheckStep(
|
|
113
|
+
method=CompareEnum.EQUAL,
|
|
114
|
+
value_a="$.name",
|
|
115
|
+
value_b="com.saucelabs.mydemoapp.android:id/cartIV",
|
|
116
|
+
),
|
|
117
|
+
CommandStep(
|
|
118
|
+
method=Command.CLICK_ELEMENT,
|
|
119
|
+
params={
|
|
120
|
+
"by": "text",
|
|
121
|
+
"value": "Proceed To Checkout",
|
|
122
|
+
}
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
provider = AndroidProvider()
|
|
126
|
+
driver = provider.get_single_device_driver()
|
|
127
|
+
local_vars: Dict[str, str] = {}
|
|
128
|
+
for step in steps:
|
|
129
|
+
if not isinstance(step, CommandStep):
|
|
130
|
+
continue
|
|
131
|
+
command = Command(step.method)
|
|
132
|
+
params = step.params
|
|
133
|
+
print(step.method, params)
|
|
134
|
+
if step.skip:
|
|
135
|
+
logger.debug("Skip step: %s", step.method)
|
|
136
|
+
continue
|
|
137
|
+
run_driver_command(driver, command, params)
|
uiautodev/cli.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Tue Mar 19 2024 10:53:03 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import platform
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pprint import pprint
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import httpx
|
|
18
|
+
import pydantic
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from uiautodev import __version__, command_proxy
|
|
22
|
+
from uiautodev.command_types import Command
|
|
23
|
+
from uiautodev.provider import AndroidProvider, BaseProvider, IOSProvider
|
|
24
|
+
from uiautodev.utils.common import convert_params_to_model, print_json
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
@click.option("--verbose", "-v", is_flag=True, default=False, help="verbose mode")
|
|
31
|
+
def cli(verbose: bool):
|
|
32
|
+
if verbose:
|
|
33
|
+
root_logger = logging.getLogger(__name__.split(".")[0])
|
|
34
|
+
root_logger.setLevel(logging.DEBUG)
|
|
35
|
+
|
|
36
|
+
console_handler = logging.StreamHandler()
|
|
37
|
+
console_handler.setLevel(logging.DEBUG)
|
|
38
|
+
|
|
39
|
+
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
|
|
40
|
+
console_handler.setFormatter(formatter)
|
|
41
|
+
|
|
42
|
+
root_logger.addHandler(console_handler)
|
|
43
|
+
logger.debug("Verbose mode enabled")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_driver_command(provider: BaseProvider, command: Command, params: list[str] = None):
|
|
47
|
+
if command == Command.LIST:
|
|
48
|
+
devices = provider.list_devices()
|
|
49
|
+
print("==> Devices <==")
|
|
50
|
+
pprint(devices)
|
|
51
|
+
return
|
|
52
|
+
driver = provider.get_single_device_driver()
|
|
53
|
+
params_obj = None
|
|
54
|
+
model = command_proxy.get_command_params_type(command)
|
|
55
|
+
if model:
|
|
56
|
+
if not params:
|
|
57
|
+
print(f"params is required for {command}")
|
|
58
|
+
pprint(model.model_json_schema())
|
|
59
|
+
return
|
|
60
|
+
params_obj = convert_params_to_model(params, model)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
print("Command:", command.value)
|
|
64
|
+
print("Params ↓")
|
|
65
|
+
print_json(params_obj)
|
|
66
|
+
result = command_proxy.send_command(driver, command, params_obj)
|
|
67
|
+
print("Result ↓")
|
|
68
|
+
print_json(result)
|
|
69
|
+
except pydantic.ValidationError as e:
|
|
70
|
+
print(f"params error: {e}")
|
|
71
|
+
print(f"\n--- params should be match schema ---")
|
|
72
|
+
pprint(model.model_json_schema()["properties"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@cli.command(help="COMMAND: " + ", ".join(c.value for c in Command))
|
|
76
|
+
@click.argument("command", type=Command, required=True)
|
|
77
|
+
@click.argument("params", required=False, nargs=-1)
|
|
78
|
+
def android(command: Command, params: list[str] = None):
|
|
79
|
+
provider = AndroidProvider()
|
|
80
|
+
run_driver_command(provider, command, params)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.command(help="COMMAND: " + ", ".join(c.value for c in Command))
|
|
84
|
+
@click.argument("command", type=Command, required=True)
|
|
85
|
+
@click.argument("params", required=False, nargs=-1)
|
|
86
|
+
def ios(command: Command, params: list[str] = None):
|
|
87
|
+
provider = IOSProvider()
|
|
88
|
+
run_driver_command(provider, command, params)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@cli.command(help="run case (beta)")
|
|
92
|
+
def case():
|
|
93
|
+
from uiautodev.case import run
|
|
94
|
+
run()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@cli.command(help="COMMAND: " + ", ".join(c.value for c in Command))
|
|
98
|
+
@click.argument("command", type=Command, required=True)
|
|
99
|
+
@click.argument("params", required=False, nargs=-1)
|
|
100
|
+
def appium(command: Command, params: list[str] = None):
|
|
101
|
+
from uiautodev.driver.appium import AppiumProvider
|
|
102
|
+
from uiautodev.exceptions import AppiumDriverException
|
|
103
|
+
|
|
104
|
+
provider = AppiumProvider()
|
|
105
|
+
try:
|
|
106
|
+
run_driver_command(provider, command, params)
|
|
107
|
+
except AppiumDriverException as e:
|
|
108
|
+
print(f"Error: {e}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@cli.command('version')
|
|
112
|
+
def print_version():
|
|
113
|
+
print(__version__)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@cli.command(help="start uiauto.dev local server [default]")
|
|
117
|
+
@click.option("--port", default=20242, help="port number", show_default=True)
|
|
118
|
+
@click.option("--host", default="127.0.0.1", help="host", show_default=True)
|
|
119
|
+
@click.option("--reload", is_flag=True, default=False, help="auto reload, dev only")
|
|
120
|
+
@click.option("-f", "--force", is_flag=True, default=False, help="shutdown alrealy runningserver")
|
|
121
|
+
@click.option("--no-browser", is_flag=True, default=False, help="do not open browser")
|
|
122
|
+
def server(port: int, host: str, reload: bool, force: bool, no_browser: bool):
|
|
123
|
+
logger.info("version: %s", __version__)
|
|
124
|
+
if force:
|
|
125
|
+
try:
|
|
126
|
+
httpx.get(f"http://{host}:{port}/shutdown", timeout=3)
|
|
127
|
+
except httpx.HTTPError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
use_color = True
|
|
131
|
+
if platform.system() == 'Windows':
|
|
132
|
+
use_color = False
|
|
133
|
+
|
|
134
|
+
if not no_browser:
|
|
135
|
+
th = threading.Thread(target=open_browser_when_server_start, args=(f"http://{host}:{port}",))
|
|
136
|
+
th.daemon = True
|
|
137
|
+
th.start()
|
|
138
|
+
uvicorn.run("uiautodev.app:app", host=host, port=port, reload=reload, use_colors=use_color)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def open_browser_when_server_start(server_url: str):
|
|
142
|
+
deadline = time.time() + 10
|
|
143
|
+
while time.time() < deadline:
|
|
144
|
+
try:
|
|
145
|
+
httpx.get(f"{server_url}/api/info", timeout=1)
|
|
146
|
+
break
|
|
147
|
+
except Exception as e:
|
|
148
|
+
time.sleep(0.5)
|
|
149
|
+
import webbrowser
|
|
150
|
+
web_url = "https://uiauto.dev"
|
|
151
|
+
logger.info("open browser: %s", web_url)
|
|
152
|
+
webbrowser.open(web_url)
|
|
153
|
+
|
|
154
|
+
def main():
|
|
155
|
+
# set logger level to INFO
|
|
156
|
+
# logging.basicConfig(level=logging.INFO)
|
|
157
|
+
logger.setLevel(logging.INFO)
|
|
158
|
+
|
|
159
|
+
has_command = False
|
|
160
|
+
for name in sys.argv[1:]:
|
|
161
|
+
if not name.startswith("-"):
|
|
162
|
+
has_command = True
|
|
163
|
+
|
|
164
|
+
if not has_command:
|
|
165
|
+
cli.main(args=sys.argv[1:] + ["server"], prog_name="uiauto.dev")
|
|
166
|
+
else:
|
|
167
|
+
cli()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Tue Mar 19 2024 10:43:51 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import typing
|
|
11
|
+
from typing import Callable, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from uiautodev.command_types import AppLaunchRequest, AppTerminateRequest, By, Command, CurrentAppResponse, \
|
|
16
|
+
DumpResponse, FindElementRequest, FindElementResponse, InstallAppRequest, InstallAppResponse, TapRequest, \
|
|
17
|
+
WindowSizeResponse
|
|
18
|
+
from uiautodev.driver.base_driver import BaseDriver
|
|
19
|
+
from uiautodev.exceptions import ElementNotFoundError
|
|
20
|
+
from uiautodev.model import Node
|
|
21
|
+
from uiautodev.utils.common import node_travel
|
|
22
|
+
|
|
23
|
+
COMMANDS: Dict[Command, Callable] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def register(command: Command):
|
|
27
|
+
def wrapper(func):
|
|
28
|
+
COMMANDS[command] = func
|
|
29
|
+
return func
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_command_params_type(command: Command) -> Optional[BaseModel]:
|
|
35
|
+
func = COMMANDS.get(command)
|
|
36
|
+
if func is None:
|
|
37
|
+
return None
|
|
38
|
+
type_hints = typing.get_type_hints(func)
|
|
39
|
+
return type_hints.get("params")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def send_command(driver: BaseDriver, command: Command, params=None):
|
|
43
|
+
if command not in COMMANDS:
|
|
44
|
+
raise NotImplementedError(f"command {command} not implemented")
|
|
45
|
+
func = COMMANDS[command]
|
|
46
|
+
type_hints = typing.get_type_hints(func)
|
|
47
|
+
if type_hints.get("params"):
|
|
48
|
+
if params is None:
|
|
49
|
+
raise ValueError(f"params is required for {command}")
|
|
50
|
+
if not isinstance(params, type_hints["params"]):
|
|
51
|
+
raise TypeError(f"params should be {type_hints['params']}")
|
|
52
|
+
if params is None:
|
|
53
|
+
return func(driver)
|
|
54
|
+
return func(driver, params)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@register(Command.TAP)
|
|
58
|
+
def tap(driver: BaseDriver, params: TapRequest):
|
|
59
|
+
"""Tap on the screen
|
|
60
|
+
:param x: x coordinate
|
|
61
|
+
:param y: y coordinate
|
|
62
|
+
"""
|
|
63
|
+
x = params.x
|
|
64
|
+
y = params.y
|
|
65
|
+
if params.isPercent:
|
|
66
|
+
wsize = driver.window_size()
|
|
67
|
+
x = int(wsize[0] * params.x)
|
|
68
|
+
y = int(wsize[1] * params.y)
|
|
69
|
+
driver.tap(x, y)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@register(Command.APP_INSTALL)
|
|
73
|
+
def app_install(driver: BaseDriver, params: InstallAppRequest):
|
|
74
|
+
"""install app"""
|
|
75
|
+
driver.app_install(params.url)
|
|
76
|
+
return InstallAppResponse(success=True, id=None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@register(Command.APP_CURRENT)
|
|
80
|
+
def app_current(driver: BaseDriver) -> CurrentAppResponse:
|
|
81
|
+
"""get current app"""
|
|
82
|
+
return driver.app_current()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@register(Command.APP_LAUNCH)
|
|
86
|
+
def app_launch(driver: BaseDriver, params: AppLaunchRequest):
|
|
87
|
+
if params.stop:
|
|
88
|
+
driver.app_terminate(params.package)
|
|
89
|
+
driver.app_launch(params.package)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@register(Command.APP_TERMINATE)
|
|
93
|
+
def app_terminate(driver: BaseDriver, params: AppTerminateRequest):
|
|
94
|
+
driver.app_terminate(params.package)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@register(Command.GET_WINDOW_SIZE)
|
|
98
|
+
def window_size(driver: BaseDriver) -> WindowSizeResponse:
|
|
99
|
+
wsize = driver.window_size()
|
|
100
|
+
return WindowSizeResponse(width=wsize[0], height=wsize[1])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@register(Command.HOME)
|
|
104
|
+
def home(driver: BaseDriver):
|
|
105
|
+
driver.home()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@register(Command.DUMP)
|
|
109
|
+
def dump(driver: BaseDriver) -> DumpResponse:
|
|
110
|
+
source, _ = driver.dump_hierarchy()
|
|
111
|
+
return DumpResponse(value=source)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@register(Command.WAKE_UP)
|
|
115
|
+
def wake_up(driver: BaseDriver):
|
|
116
|
+
driver.wake_up()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def node_match(node: Node, by: By, value: str) -> bool:
|
|
120
|
+
if by == By.ID:
|
|
121
|
+
return node.properties.get("resource-id") == value
|
|
122
|
+
if by == By.TEXT:
|
|
123
|
+
return node.properties.get("text") == value
|
|
124
|
+
if by == By.CLASS_NAME:
|
|
125
|
+
return node.name == value
|
|
126
|
+
raise ValueError(f"not support by {by!r}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@register(Command.FIND_ELEMENTS)
|
|
130
|
+
def find_elements(driver: BaseDriver, params: FindElementRequest) -> FindElementResponse:
|
|
131
|
+
_, root_node = driver.dump_hierarchy()
|
|
132
|
+
# TODO: support By.XPATH
|
|
133
|
+
nodes = []
|
|
134
|
+
for node in node_travel(root_node):
|
|
135
|
+
if node_match(node, params.by, params.value):
|
|
136
|
+
nodes.append(node)
|
|
137
|
+
return FindElementResponse(count=len(nodes), value=nodes)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@register(Command.CLICK_ELEMENT)
|
|
141
|
+
def click_element(driver: BaseDriver, params: FindElementRequest):
|
|
142
|
+
node = None
|
|
143
|
+
deadline = time.time() + params.timeout
|
|
144
|
+
while time.time() < deadline:
|
|
145
|
+
result = find_elements(driver, params)
|
|
146
|
+
if result.value:
|
|
147
|
+
node = result.value[0]
|
|
148
|
+
break
|
|
149
|
+
time.sleep(.5) # interval
|
|
150
|
+
if not node:
|
|
151
|
+
raise ElementNotFoundError(f"element not found by {params.by}={params.value}")
|
|
152
|
+
center_x = (node.bounds[0] + node.bounds[2]) / 2
|
|
153
|
+
center_y = (node.bounds[1] + node.bounds[3]) / 2
|
|
154
|
+
tap(driver, TapRequest(x=center_x, y=center_y, isPercent=True))
|