applib-ui 0.0.1__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.
- applib_ui/__init__.py +13 -0
- applib_ui/case.py +79 -0
- applib_ui/cli.py +75 -0
- applib_ui/core/__init__.py +0 -0
- applib_ui/core/android/__init__.py +16 -0
- applib_ui/core/android/case.py +8 -0
- applib_ui/core/android/driver.py +191 -0
- applib_ui/core/android/element.py +291 -0
- applib_ui/core/android/page.py +6 -0
- applib_ui/core/android/recorder.py +171 -0
- applib_ui/core/android/remote_adb.py +69 -0
- applib_ui/core/android/remote_driver.py +176 -0
- applib_ui/core/img/__init__.py +4 -0
- applib_ui/core/img/driver.py +43 -0
- applib_ui/core/img/element.py +73 -0
- applib_ui/core/ios/__init__.py +16 -0
- applib_ui/core/ios/case.py +8 -0
- applib_ui/core/ios/driver.py +290 -0
- applib_ui/core/ios/elem.py +222 -0
- applib_ui/core/ios/local_wda.py +576 -0
- applib_ui/core/ios/page.py +6 -0
- applib_ui/core/ios/recorder.py +202 -0
- applib_ui/core/ios/remote_driver.py +220 -0
- applib_ui/core/ios/remote_wda.py +54 -0
- applib_ui/core/ocr/__init__.py +5 -0
- applib_ui/core/ocr/driver.py +185 -0
- applib_ui/core/ocr/element.py +65 -0
- applib_ui/device.py +53 -0
- applib_ui/element.py +210 -0
- applib_ui/page.py +6 -0
- applib_ui/pytest_plugin.py +57 -0
- applib_ui/running/__init__.py +0 -0
- applib_ui/running/conf.py +14 -0
- applib_ui/running/conf.yml +19 -0
- applib_ui/running/runner.py +33 -0
- applib_ui/utils/__init__.py +1 -0
- applib_ui/utils/aes_util.py +47 -0
- applib_ui/utils/allure_util.py +213 -0
- applib_ui/utils/base64_util.py +21 -0
- applib_ui/utils/common.py +84 -0
- applib_ui/utils/config.py +135 -0
- applib_ui/utils/cv_util.py +110 -0
- applib_ui/utils/dingtalk.py +56 -0
- applib_ui/utils/excel_util.py +101 -0
- applib_ui/utils/log.py +12 -0
- applib_ui/utils/mail.py +47 -0
- applib_ui/utils/mysql_util.py +74 -0
- applib_ui/utils/nacos_util.py +52 -0
- applib_ui/utils/pytest_util.py +89 -0
- applib_ui/utils/swagger_util.py +195 -0
- applib_ui-0.0.1.dist-info/METADATA +170 -0
- applib_ui-0.0.1.dist-info/RECORD +55 -0
- applib_ui-0.0.1.dist-info/WHEEL +5 -0
- applib_ui-0.0.1.dist-info/entry_points.txt +5 -0
- applib_ui-0.0.1.dist-info/top_level.txt +1 -0
applib_ui/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .running.runner import main
|
|
2
|
+
from .running.conf import App
|
|
3
|
+
from .utils.config import FileConfig
|
|
4
|
+
from .utils.pytest_util import *
|
|
5
|
+
from .utils.allure_util import *
|
|
6
|
+
from .utils.log import logger
|
|
7
|
+
from .page import Page
|
|
8
|
+
from .element import Elem
|
|
9
|
+
from .case import TestCase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__version__ = "0.0.1"
|
|
13
|
+
__description__ = "Android / iOS 通用 UI 自动化测试框架"
|
applib_ui/case.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from .running.conf import App
|
|
6
|
+
from .utils.log import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCase:
|
|
10
|
+
"""pytest 测试基类;也可以直接在测试函数中使用 app_driver fixture。"""
|
|
11
|
+
|
|
12
|
+
dr = None
|
|
13
|
+
|
|
14
|
+
def start_class(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def end_class(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def setup_class(cls):
|
|
22
|
+
cls().start_class()
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def teardown_class(cls):
|
|
26
|
+
cls().end_class()
|
|
27
|
+
|
|
28
|
+
def start(self):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def end(self):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(autouse=True)
|
|
35
|
+
def _app_lifecycle(self, app_driver):
|
|
36
|
+
self.dr = app_driver
|
|
37
|
+
try:
|
|
38
|
+
if getattr(self, '_platform', None) is not None:
|
|
39
|
+
self.dr.unlock()
|
|
40
|
+
self.start()
|
|
41
|
+
yield
|
|
42
|
+
finally:
|
|
43
|
+
self.end()
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def sleep(n: float):
|
|
47
|
+
logger.info(f"暂停: {n}s")
|
|
48
|
+
time.sleep(n)
|
|
49
|
+
|
|
50
|
+
def shot(self, name: str, delay=0):
|
|
51
|
+
if delay:
|
|
52
|
+
self.sleep(delay)
|
|
53
|
+
return self.dr.shot(name)
|
|
54
|
+
|
|
55
|
+
def start_record(self, video_name='record'):
|
|
56
|
+
"""开始录屏"""
|
|
57
|
+
return self.dr.start_record(video_name)
|
|
58
|
+
|
|
59
|
+
def stop_record(self):
|
|
60
|
+
"""结束录屏并上传 allure"""
|
|
61
|
+
return self.dr.stop_record()
|
|
62
|
+
|
|
63
|
+
def start_app(self, force=True, activity=None, use_monkey=False):
|
|
64
|
+
if force:
|
|
65
|
+
self.stop_app()
|
|
66
|
+
if App.platform == 'ios':
|
|
67
|
+
self.dr.start_app()
|
|
68
|
+
elif activity is not None:
|
|
69
|
+
self.dr.start_app(activity=activity)
|
|
70
|
+
elif use_monkey:
|
|
71
|
+
self.dr.start_app(use_monkey=True)
|
|
72
|
+
else:
|
|
73
|
+
self.dr.start_app()
|
|
74
|
+
|
|
75
|
+
def stop_app(self):
|
|
76
|
+
self.dr.stop_app()
|
|
77
|
+
|
|
78
|
+
def back(self):
|
|
79
|
+
self.dr.back()
|
applib_ui/cli.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from . import __version__
|
|
3
|
+
|
|
4
|
+
@click.group()
|
|
5
|
+
@click.version_option(version=__version__, help="Show version.")
|
|
6
|
+
def main():
|
|
7
|
+
"""applib-ui 移动端自动化工具。"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@main.command(context_settings={'ignore_unknown_options': True})
|
|
11
|
+
@click.argument('pytest_args', nargs=-1, type=click.UNPROCESSED)
|
|
12
|
+
def run(pytest_args):
|
|
13
|
+
"""运行 pytest,透传路径、筛选条件和报告等参数。"""
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
raise SystemExit(pytest.main(list(pytest_args)))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@main.command()
|
|
20
|
+
@click.option('-u', '--udid', help="Specify the udid.")
|
|
21
|
+
def restart_wda(udid):
|
|
22
|
+
"""
|
|
23
|
+
重启wda
|
|
24
|
+
@param udid: IOS设备udid
|
|
25
|
+
@return:
|
|
26
|
+
"""
|
|
27
|
+
from .core.ios.local_wda import LocalWdaInit
|
|
28
|
+
|
|
29
|
+
LocalWdaInit(udid=udid).restart_wda()
|
|
30
|
+
|
|
31
|
+
@main.command()
|
|
32
|
+
@click.option('-u', '--udid', help="Specify the udid.")
|
|
33
|
+
def start_wda(udid):
|
|
34
|
+
"""
|
|
35
|
+
启动wda
|
|
36
|
+
@param udid: IOS设备udid
|
|
37
|
+
@return:
|
|
38
|
+
"""
|
|
39
|
+
from .core.ios.local_wda import LocalWdaInit
|
|
40
|
+
|
|
41
|
+
LocalWdaInit(udid=udid).run()
|
|
42
|
+
|
|
43
|
+
@main.command()
|
|
44
|
+
@click.option('-u', '--udid', help="Specify the udid.")
|
|
45
|
+
def stop_wda(udid):
|
|
46
|
+
"""
|
|
47
|
+
杀掉wda
|
|
48
|
+
@param udid: IOS设备udid
|
|
49
|
+
@return:
|
|
50
|
+
"""
|
|
51
|
+
from .core.ios.local_wda import LocalWdaInit
|
|
52
|
+
|
|
53
|
+
LocalWdaInit(udid=udid).kill_wda_server()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# @main.command()
|
|
57
|
+
# @click.option('-p', '--platform', help="Specify the platform.")
|
|
58
|
+
# @click.option('-u', '--url', help="Specify the url.")
|
|
59
|
+
# def inspector(platform, url):
|
|
60
|
+
# """
|
|
61
|
+
# 获取元素定位信息
|
|
62
|
+
# @param platform: 平台,android、ios、web
|
|
63
|
+
# @param url:针对web,需要录制的页面的url
|
|
64
|
+
# @return:
|
|
65
|
+
# """
|
|
66
|
+
# import os
|
|
67
|
+
# if platform in ['android', 'ios', 'web']:
|
|
68
|
+
# os.system('weditor')
|
|
69
|
+
# elif platform == 'web':
|
|
70
|
+
# if not url:
|
|
71
|
+
# raise KeyError('url不能为空')
|
|
72
|
+
# os.system(f'playwright codegen {url}')
|
|
73
|
+
# else:
|
|
74
|
+
# raise KeyError('只支持android、ios、web')
|
|
75
|
+
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""android 平台接口,按需加载驱动依赖。"""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
__all__ = ['Elem', 'Page', 'Driver', 'RemoteDriver', 'TestCase']
|
|
6
|
+
|
|
7
|
+
_MODULES = {'Elem': 'element', 'Page': 'page', 'Driver': 'driver',
|
|
8
|
+
'RemoteDriver': 'remote_driver', 'TestCase': 'case'}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name):
|
|
12
|
+
if name not in _MODULES:
|
|
13
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
14
|
+
value = getattr(import_module(f'.{_MODULES[name]}', __name__), name)
|
|
15
|
+
globals()[name] = value
|
|
16
|
+
return value
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import allure
|
|
2
|
+
import uiautomator2 as u2
|
|
3
|
+
|
|
4
|
+
from applib_ui.utils.log import logger
|
|
5
|
+
from applib_ui.utils.common import general_file_path, general_video_path
|
|
6
|
+
from .recorder import ScrcpyRecorder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Driver:
|
|
10
|
+
"""
|
|
11
|
+
u2库代码地址:https://github.com/openatx/uiautomator2
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, serial: str, package: str = None):
|
|
15
|
+
"""
|
|
16
|
+
@param serial: 设备序列号,通过adb devices获取,或者通过关于手机-序列号获取
|
|
17
|
+
@param package: 应用包名,通过adb shell pm list packages获取
|
|
18
|
+
"""
|
|
19
|
+
logger.info(f"初始化本地设备连接: {serial}")
|
|
20
|
+
self.serial = serial
|
|
21
|
+
self.package = package
|
|
22
|
+
self.d = u2.connect(serial)
|
|
23
|
+
self._recorder = None
|
|
24
|
+
|
|
25
|
+
def uninstall_app(self, package: str = None):
|
|
26
|
+
"""
|
|
27
|
+
卸载应用
|
|
28
|
+
@param package: 应用包名
|
|
29
|
+
@return:
|
|
30
|
+
"""
|
|
31
|
+
if self.package is None:
|
|
32
|
+
if package is None:
|
|
33
|
+
raise KeyError('package不能为空')
|
|
34
|
+
else:
|
|
35
|
+
self.package = package
|
|
36
|
+
logger.info(f"卸载应用: {self.package}")
|
|
37
|
+
self.d.app_uninstall(self.package)
|
|
38
|
+
|
|
39
|
+
def install_app(self, apk_url, package: str = None):
|
|
40
|
+
"""
|
|
41
|
+
安装应用
|
|
42
|
+
@param apk_url: apk链接
|
|
43
|
+
@param package: 应用包名
|
|
44
|
+
@return:
|
|
45
|
+
"""
|
|
46
|
+
if self.package is None:
|
|
47
|
+
if package is None:
|
|
48
|
+
raise KeyError('package不能为空')
|
|
49
|
+
else:
|
|
50
|
+
self.package = package
|
|
51
|
+
self.d.app_uninstall(self.package)
|
|
52
|
+
logger.info(f"安装应用: {apk_url}")
|
|
53
|
+
self.d.app_install(apk_url)
|
|
54
|
+
|
|
55
|
+
def start_app(self, package: str = None, activity: str = None, use_monkey: bool = False):
|
|
56
|
+
"""
|
|
57
|
+
启动应用
|
|
58
|
+
@param package: 应用包名
|
|
59
|
+
@return:
|
|
60
|
+
"""
|
|
61
|
+
if self.package is None:
|
|
62
|
+
if package is None:
|
|
63
|
+
raise KeyError('package不能为空')
|
|
64
|
+
else:
|
|
65
|
+
self.package = package
|
|
66
|
+
logger.info(f"启动应用: {self.package}")
|
|
67
|
+
if activity is not None:
|
|
68
|
+
self.d.app_start(package_name=self.package, activity=activity)
|
|
69
|
+
else:
|
|
70
|
+
self.d.app_start(package_name=self.package, use_monkey=use_monkey)
|
|
71
|
+
|
|
72
|
+
def stop_app(self, package: str = None):
|
|
73
|
+
"""
|
|
74
|
+
停止应用
|
|
75
|
+
@param package: 应用包名
|
|
76
|
+
@return:
|
|
77
|
+
"""
|
|
78
|
+
if self.package is None:
|
|
79
|
+
if package is None:
|
|
80
|
+
raise KeyError('package不能为空')
|
|
81
|
+
else:
|
|
82
|
+
self.package = package
|
|
83
|
+
logger.info(f"停止应用: {self.package}")
|
|
84
|
+
self.d.app_stop(self.package)
|
|
85
|
+
|
|
86
|
+
def shot(self, file_name: str = None):
|
|
87
|
+
"""
|
|
88
|
+
截图
|
|
89
|
+
@param file_name: 截图保存后的文件名
|
|
90
|
+
@return:
|
|
91
|
+
"""
|
|
92
|
+
file_path = general_file_path(file_name, self.serial)
|
|
93
|
+
logger.info(f"截图保存至: {file_path}")
|
|
94
|
+
self.d.screenshot(file_path)
|
|
95
|
+
|
|
96
|
+
logger.info("截图上传allure报告")
|
|
97
|
+
allure.attach.file(
|
|
98
|
+
file_path,
|
|
99
|
+
attachment_type=allure.attachment_type.PNG,
|
|
100
|
+
name=f"{file_path}",
|
|
101
|
+
)
|
|
102
|
+
return file_path
|
|
103
|
+
|
|
104
|
+
def click(self, x, y):
|
|
105
|
+
"""
|
|
106
|
+
点击坐标
|
|
107
|
+
@param x: 横坐标
|
|
108
|
+
@param y: 纵坐标
|
|
109
|
+
@return:
|
|
110
|
+
"""
|
|
111
|
+
self.d.click(x, y)
|
|
112
|
+
|
|
113
|
+
def input(self, *args, **kwargs):
|
|
114
|
+
"""
|
|
115
|
+
输入
|
|
116
|
+
@param args:
|
|
117
|
+
@param kwargs:
|
|
118
|
+
@return:
|
|
119
|
+
"""
|
|
120
|
+
self.d.send_keys(*args, **kwargs)
|
|
121
|
+
|
|
122
|
+
def unlock(self):
|
|
123
|
+
"""
|
|
124
|
+
设备解锁
|
|
125
|
+
@return:
|
|
126
|
+
"""
|
|
127
|
+
self.d.unlock()
|
|
128
|
+
|
|
129
|
+
def shell(self, *args, **kwargs):
|
|
130
|
+
"""
|
|
131
|
+
执行adb shell
|
|
132
|
+
@param args:
|
|
133
|
+
@param kwargs:
|
|
134
|
+
@return:
|
|
135
|
+
"""
|
|
136
|
+
self.d.shell(*args, **kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def back(self):
|
|
140
|
+
logger.info("返回上一页")
|
|
141
|
+
self.d.press('back')
|
|
142
|
+
|
|
143
|
+
def start_record(self, file_name: str = "record"):
|
|
144
|
+
"""
|
|
145
|
+
开始录屏
|
|
146
|
+
@param file_name: 录屏文件名
|
|
147
|
+
@return: 录像路径;录制没起来返回 None(最终产物以 stop_record 的返回值为准)
|
|
148
|
+
"""
|
|
149
|
+
if self._recorder is not None:
|
|
150
|
+
self.stop_record()
|
|
151
|
+
file_path = general_video_path(file_name, self.serial)
|
|
152
|
+
self._recorder = ScrcpyRecorder(self.serial)
|
|
153
|
+
if not self._recorder.start(file_path):
|
|
154
|
+
# 没起来:原因已由 recorder 打成 ERROR,这里返回 None,别让调用方以为正在录
|
|
155
|
+
self._recorder = None
|
|
156
|
+
return None
|
|
157
|
+
return file_path
|
|
158
|
+
|
|
159
|
+
def stop_record(self):
|
|
160
|
+
"""
|
|
161
|
+
停止录屏并上传allure
|
|
162
|
+
@return:
|
|
163
|
+
"""
|
|
164
|
+
if self._recorder is None:
|
|
165
|
+
return None
|
|
166
|
+
file_path = self._recorder.stop()
|
|
167
|
+
self._recorder = None
|
|
168
|
+
return file_path
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from .driver import Driver
|
|
3
|
+
|
|
4
|
+
from applib_ui.utils.log import logger
|
|
5
|
+
from uiautomator2 import UiObject
|
|
6
|
+
from uiautomator2.xpath import XPathSelector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# 链式调用临时存储定位方式
|
|
10
|
+
# class BaseLocator:
|
|
11
|
+
#
|
|
12
|
+
# def __init__(self, **kwargs):
|
|
13
|
+
# self.rid = kwargs.get('rid', None)
|
|
14
|
+
# self.text = kwargs.get('text', None)
|
|
15
|
+
# self.xpath = kwargs.get('xpath', None)
|
|
16
|
+
# self.type = 'current'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChildLocator:
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
self.args = args
|
|
23
|
+
self.kwargs = kwargs
|
|
24
|
+
self.type = 'child'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SiblingLocator:
|
|
28
|
+
|
|
29
|
+
def __init__(self, *args, **kwargs):
|
|
30
|
+
self.args = args
|
|
31
|
+
self.kwargs = kwargs
|
|
32
|
+
self.type = 'sibling'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LeftLocator:
|
|
36
|
+
|
|
37
|
+
def __init__(self, *args, **kwargs):
|
|
38
|
+
self.args = args
|
|
39
|
+
self.kwargs = kwargs
|
|
40
|
+
self.type = 'left'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RightLocator:
|
|
44
|
+
|
|
45
|
+
def __init__(self, *args, **kwargs):
|
|
46
|
+
self.args = args
|
|
47
|
+
self.kwargs = kwargs
|
|
48
|
+
self.type = 'right'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UpLocator:
|
|
52
|
+
|
|
53
|
+
def __init__(self, *args, **kwargs):
|
|
54
|
+
self.args = args
|
|
55
|
+
self.kwargs = kwargs
|
|
56
|
+
self.type = 'up'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DownLocator:
|
|
60
|
+
|
|
61
|
+
def __init__(self, *args, **kwargs):
|
|
62
|
+
self.args = args
|
|
63
|
+
self.kwargs = kwargs
|
|
64
|
+
self.type = 'down'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Elem(object):
|
|
68
|
+
"""
|
|
69
|
+
安卓控件定义
|
|
70
|
+
https://github.com/openatx/uiautomator2
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self,
|
|
74
|
+
tag=None,
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
@param tag: 元素名称,方面后续维护
|
|
78
|
+
"""
|
|
79
|
+
self.tag = tag
|
|
80
|
+
# self._kwargs = kwargs
|
|
81
|
+
self._driver: Driver = None
|
|
82
|
+
# self._xpath = kwargs.get('xpath', None)
|
|
83
|
+
# self._watch = watch
|
|
84
|
+
self._first_locator = None
|
|
85
|
+
self._locators = []
|
|
86
|
+
# if 'rid' in self._kwargs:
|
|
87
|
+
# self._kwargs['resourceId'] = self._kwargs.pop('rid')
|
|
88
|
+
# self._pkg = App.pkg
|
|
89
|
+
# if 'resourceId' in self._kwargs:
|
|
90
|
+
# if not self._kwargs['resourceId'].startswith(self._pkg):
|
|
91
|
+
# if not self._pkg:
|
|
92
|
+
# raise KeyError('应用包名不能为空')
|
|
93
|
+
# else:
|
|
94
|
+
# self._kwargs['resourceId'] = self._pkg + ":" + self._kwargs['resourceId']
|
|
95
|
+
|
|
96
|
+
def __get__(self, instance, owner):
|
|
97
|
+
"""po模式中element初始化不需要带driver的关键"""
|
|
98
|
+
if instance is None:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
self._driver = instance.driver
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
# 链式调用方法
|
|
105
|
+
# def get_by_rid(self, rid):
|
|
106
|
+
# self.locators.append(BaseLocator(rid=rid))
|
|
107
|
+
# return self
|
|
108
|
+
#
|
|
109
|
+
# def get_by_text(self, text):
|
|
110
|
+
# self.locators.append(BaseLocator(text=text))
|
|
111
|
+
# return self
|
|
112
|
+
#
|
|
113
|
+
# def get_by_xpath(self, xpath):
|
|
114
|
+
# self.locators.append(BaseLocator(xpath=xpath))
|
|
115
|
+
# return self
|
|
116
|
+
|
|
117
|
+
def loc(self, **kwargs):
|
|
118
|
+
if not kwargs:
|
|
119
|
+
raise KeyError('定位方式不能为空')
|
|
120
|
+
if 'xpath' in kwargs:
|
|
121
|
+
self._first_locator = kwargs.pop('xpath')
|
|
122
|
+
else:
|
|
123
|
+
self._first_locator = kwargs
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def child(self, *args, **kwargs):
|
|
127
|
+
self._locators.append(ChildLocator(*args, **kwargs))
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def sibling(self, *args, **kwargs):
|
|
131
|
+
self._locators.append(SiblingLocator(*args, **kwargs))
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def left(self, *args, **kwargs):
|
|
135
|
+
self._locators.append(LeftLocator(*args, **kwargs))
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def right(self, *args, **kwargs):
|
|
139
|
+
self._locators.append(RightLocator(*args, **kwargs))
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def up(self, *args, **kwargs):
|
|
143
|
+
self._locators.append(UpLocator(*args, **kwargs))
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def down(self, *args, **kwargs):
|
|
147
|
+
self._locators.append(DownLocator(*args, **kwargs))
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
# # 公共方法
|
|
151
|
+
# def watch_handler(self):
|
|
152
|
+
# """
|
|
153
|
+
# 异常弹窗处理
|
|
154
|
+
# @return:
|
|
155
|
+
# """
|
|
156
|
+
# logger.info(f"开始弹窗检测: {self._watch}")
|
|
157
|
+
# ctx = self._driver.d.watch_context()
|
|
158
|
+
# for loc in self._watch:
|
|
159
|
+
# ctx.when(loc).click()
|
|
160
|
+
# ctx.wait_stable()
|
|
161
|
+
# ctx.close()
|
|
162
|
+
# logger.info("检测结束")
|
|
163
|
+
|
|
164
|
+
def find(self, timeout=5):
|
|
165
|
+
"""
|
|
166
|
+
增加截图的方法
|
|
167
|
+
@param timeout: 每次查找时间
|
|
168
|
+
@return:
|
|
169
|
+
"""
|
|
170
|
+
logger.info(f"查找: {self.tag}")
|
|
171
|
+
# 第一个定位
|
|
172
|
+
if isinstance(self._first_locator, dict):
|
|
173
|
+
_element = self._driver.d(**self._first_locator)
|
|
174
|
+
else:
|
|
175
|
+
_element = self._driver.d.xpath(self._first_locator)
|
|
176
|
+
|
|
177
|
+
# 后面的定位
|
|
178
|
+
if self._locators:
|
|
179
|
+
for loc_obj in self._locators:
|
|
180
|
+
if loc_obj.type == "child":
|
|
181
|
+
_element = _element.child(*loc_obj.args, **loc_obj.kwargs)
|
|
182
|
+
elif loc_obj.type == 'sibling':
|
|
183
|
+
_element = _element.sibling(*loc_obj.args, **loc_obj.kwargs)
|
|
184
|
+
elif loc_obj.type == 'left':
|
|
185
|
+
_element = _element.left(*loc_obj.args, **loc_obj.kwargs)
|
|
186
|
+
elif loc_obj.type == 'right':
|
|
187
|
+
_element = _element.right(*loc_obj.args, **loc_obj.kwargs)
|
|
188
|
+
elif loc_obj.type == 'up':
|
|
189
|
+
_element = _element.up(*loc_obj.args, **loc_obj.kwargs)
|
|
190
|
+
elif loc_obj.type == 'down':
|
|
191
|
+
_element = _element.down(*loc_obj.args, **loc_obj.kwargs)
|
|
192
|
+
|
|
193
|
+
if _element.wait(timeout=timeout):
|
|
194
|
+
logger.info(f"查找成功")
|
|
195
|
+
return _element
|
|
196
|
+
else:
|
|
197
|
+
logger.info("查找失败")
|
|
198
|
+
self._driver.shot(f"{self.tag}_查找失败")
|
|
199
|
+
raise Exception(f"{self.tag}, 查找失败")
|
|
200
|
+
|
|
201
|
+
# 属性获取
|
|
202
|
+
def get_text(self, timeout=5):
|
|
203
|
+
logger.info(f"获取{self.tag}的文本")
|
|
204
|
+
_elem = self.find(timeout=timeout)
|
|
205
|
+
text = _elem.get_text()
|
|
206
|
+
logger.info(f"获取成功: {text}")
|
|
207
|
+
return text
|
|
208
|
+
|
|
209
|
+
def get_texts(self, timeout=5):
|
|
210
|
+
logger.info(f"获取{self.tag}的文本")
|
|
211
|
+
_elem = self.find(timeout=timeout)
|
|
212
|
+
texts = []
|
|
213
|
+
if isinstance(_elem, XPathSelector):
|
|
214
|
+
elems = _elem.all()
|
|
215
|
+
for elem in elems:
|
|
216
|
+
texts.append(elem.text)
|
|
217
|
+
else:
|
|
218
|
+
elems = list(_elem)
|
|
219
|
+
for elem in elems:
|
|
220
|
+
texts.append(elem.get_text())
|
|
221
|
+
logger.info(f"获取成功: {texts}")
|
|
222
|
+
return texts
|
|
223
|
+
|
|
224
|
+
def exists(self, timeout=5):
|
|
225
|
+
logger.info(f"判断{self.tag}是否存在")
|
|
226
|
+
result = False
|
|
227
|
+
try:
|
|
228
|
+
_element = self.find(timeout=timeout)
|
|
229
|
+
result = True
|
|
230
|
+
except:
|
|
231
|
+
result = False
|
|
232
|
+
finally:
|
|
233
|
+
logger.info(result)
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
def center(self, timeout=5, *args, **kwargs):
|
|
237
|
+
return self.find(timeout=timeout).center(*args, **kwargs)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _adapt_center(e: typing.Union[UiObject, XPathSelector],
|
|
241
|
+
offset=(0.5, 0.5)):
|
|
242
|
+
"""
|
|
243
|
+
修正控件中心坐标
|
|
244
|
+
"""
|
|
245
|
+
if isinstance(e, UiObject):
|
|
246
|
+
return e.center(offset=offset)
|
|
247
|
+
else:
|
|
248
|
+
return e.offset(offset[0], offset[1])
|
|
249
|
+
|
|
250
|
+
# 操作
|
|
251
|
+
def click_exists(self, timeout=5):
|
|
252
|
+
logger.info(f"{self.tag}存在才点击")
|
|
253
|
+
try:
|
|
254
|
+
element = self.find(timeout=timeout)
|
|
255
|
+
x, y = self._adapt_center(element)
|
|
256
|
+
self._driver.click(x, y)
|
|
257
|
+
logger.info("点击完成")
|
|
258
|
+
except:
|
|
259
|
+
logger.info("不存在,不进行操作")
|
|
260
|
+
|
|
261
|
+
def click(self, timeout=5):
|
|
262
|
+
logger.info(f"{self.tag}点击")
|
|
263
|
+
element = self.find(timeout=timeout)
|
|
264
|
+
x, y = self._adapt_center(element)
|
|
265
|
+
self._driver.click(x, y)
|
|
266
|
+
logger.info("点击完成")
|
|
267
|
+
|
|
268
|
+
def input(self, text, timeout=5, clear=False, pwd_check=False):
|
|
269
|
+
logger.info(f"{self.tag}输入: {text}")
|
|
270
|
+
element = self.find(timeout=timeout)
|
|
271
|
+
# 清空输入框
|
|
272
|
+
if clear is True:
|
|
273
|
+
element.clear_text()
|
|
274
|
+
# 点击输入框
|
|
275
|
+
x, y = self._adapt_center(element)
|
|
276
|
+
self._driver.click(x, y)
|
|
277
|
+
# 输入
|
|
278
|
+
if pwd_check is True:
|
|
279
|
+
self._driver.d(focused=True).set_text(text)
|
|
280
|
+
else:
|
|
281
|
+
self._driver.input(text)
|
|
282
|
+
logger.info("输入完成")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|