├── README.md └── sweet ├── lib ├── __init__.py ├── c.py ├── http_handle.py └── u.py └── modules ├── __init__.py ├── db.py ├── file.py ├── http.py ├── mobile ├── __init__.py ├── app.py ├── config.py ├── locator.py └── window.py └── web ├── __init__.py ├── app.py ├── config.py ├── locator.py └── window.py /README.md: -------------------------------------------------------------------------------- 1 | ![sweetest](https://sweeter.io/docs/_media/sweeter.png) 2 | 3 | 4 | Sweetest 已全面升级为 Sweet,请访问: 5 | 6 | 官网:https://sweeter.io 7 | 8 | GitHub:https://github.com/sweeterio 9 | -------------------------------------------------------------------------------- /sweet/lib/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['u', 'c', 'http_handle'] -------------------------------------------------------------------------------- /sweet/lib/c.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | # write your function this file 5 | def today(): 6 | now = datetime.datetime.now() 7 | return now.strftime('%Y%m%d') 8 | -------------------------------------------------------------------------------- /sweet/lib/http_handle.py: -------------------------------------------------------------------------------- 1 | # 本文件为 Http 测试调用,请勿修改名称。 2 | 3 | def before_send(method, data, kwargs): 4 | ''' 5 | method: str, 请求类型 6 | 值可以为:post, get, put, patch, delete, options 7 | 8 | data: dict,格式如下: 9 | {'headers':{}, 10 | 'params': {}, 11 | 'data': {}, 12 | 'json': {}, 13 | 'files': 'file path' 14 | } 15 | 16 | keargs: dict,其他参数 17 | ''' 18 | # handle the request here 19 | 20 | return data, kwargs 21 | 22 | def after_receive(response): 23 | ''' 24 | response: dict, 格式如下: 25 | {'status_code': 200, 26 | 'headers': {}, 27 | '_cookies': '', # 原始内容 28 | 'cookies': {}, # dict 格式 29 | 'content': b'', 30 | 'text': '', 31 | 'json': {} 32 | } 33 | ''' 34 | # handle the response here 35 | 36 | return response 37 | -------------------------------------------------------------------------------- /sweet/lib/u.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def today(): 5 | now = datetime.datetime.now() 6 | return now.strftime('%Y%m%d') 7 | 8 | 9 | days = ['20180422', '20180423', '20180424','20180425', '20180426', '20180427', '20180428'] 10 | 11 | def td(t=0): 12 | day = today() 13 | td = '19700101' 14 | 15 | for i,d in enumerate(days): 16 | if d >= day: 17 | td = days[i+t] 18 | break 19 | return td 20 | 21 | def test_trade_day(): 22 | assert '20180422' == td(-3) 23 | assert '20180426' == td(1) 24 | -------------------------------------------------------------------------------- /sweet/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | path = Path(__file__).resolve().parents[0] 5 | 6 | __all__ = [] 7 | for p in path.iterdir(): 8 | __all__.append(p.stem) -------------------------------------------------------------------------------- /sweet/modules/db.py: -------------------------------------------------------------------------------- 1 | from injson import check 2 | from sweet import log, vars 3 | from sweet.utility import compare, json2dict 4 | 5 | 6 | keywords = { 7 | 'SQL': 'SQL' 8 | } 9 | 10 | 11 | class App: 12 | 13 | keywords = keywords 14 | 15 | def __init__(self, setting): 16 | # 获取连接参数 17 | self.db = DB(setting) 18 | 19 | def _close(self): 20 | pass 21 | 22 | def _call(self, step): 23 | # 根据关键字调用关键字实现 24 | getattr(self, step['keyword'].lower())(step) 25 | 26 | def sql(self, step): 27 | 28 | response = {} 29 | 30 | _sql = step['element'] 31 | 32 | log.debug(f'SQL: {repr(_sql)}') 33 | 34 | row = {} 35 | if _sql.lower().startswith('select'): 36 | row = self.db.fetchone(_sql) 37 | log.debug(f'SQL response: {repr(row)}') 38 | if not row: 39 | raise Exception('*** Fetch None ***') 40 | 41 | elif _sql.lower().startswith('db.'): 42 | _sql_ = _sql.split('.', 2) 43 | collection = _sql_[1] 44 | sql = _sql_[2] 45 | response = self.db.mongo(collection, sql) 46 | if response: 47 | log.debug(f'find result: {repr(response)}') 48 | else: 49 | self.db.execute(_sql) 50 | 51 | if _sql.lower().startswith('select'): 52 | text = _sql[6:].split('FROM')[0].split('from')[0].strip() 53 | keys = dedup(text).split(',') 54 | for i, k in enumerate(keys): 55 | keys[i] = k.split(' ')[-1] 56 | response = dict(zip(keys, row)) 57 | log.debug(f'select result: {repr(response)}') 58 | 59 | expected = step['data'] 60 | if not expected: 61 | expected = step['expected'] 62 | if 'json' in expected: 63 | expected['json'] = json2dict(expected.get('json', '{}')) 64 | result = check(expected.pop('json'), response['json']) 65 | log.debug(f'json check result: {result}') 66 | if result['code'] != 0: 67 | raise Exception( 68 | f'json | EXPECTED:{repr(expected["json"])}, REAL:{repr(response["json"])}, RESULT: {result}') 69 | elif result['var']: 70 | vars.put(result['var']) 71 | log.debug(f'json var: {repr(result["var"])}') 72 | 73 | if expected: 74 | for key in expected: 75 | sv, pv = expected[key], response[key] 76 | log.debug(f'key: {repr(key)}, expect: {repr(sv)}, real: { repr(pv)}') 77 | 78 | compare(sv, pv) 79 | 80 | output = step['output'] 81 | if output: 82 | 83 | for k, v in output.items(): 84 | if k == 'json': 85 | sub = json2dict(output.get('json', '{}')) 86 | result = check(sub, response['json']) 87 | vars.put(result['var']) 88 | log.debug(f'json var: {repr(result["var"])}') 89 | else: 90 | vars.put({k: response[v]}) 91 | log.debug(f'output: {vars.output()}') 92 | 93 | 94 | def dedup(text): 95 | ''' 96 | 去掉 text 中括号及其包含的字符 97 | ''' 98 | _text = '' 99 | n = 0 100 | 101 | for s in text: 102 | if s not in ('(', ')'): 103 | if n <= 0: 104 | _text += s 105 | elif s == '(': 106 | n += 1 107 | elif s == ')': 108 | n -= 1 109 | return _text 110 | 111 | 112 | class DB: 113 | 114 | def __init__(self, arg): 115 | self.connect = '' 116 | self.cursor = '' 117 | self.db = '' 118 | 119 | try: 120 | if arg['type'].lower() == 'mongodb': 121 | import pymongo 122 | host = arg.pop('host') if arg.get( 123 | 'host') else 'localhost:27017' 124 | host = host.split(',') if ',' in host else host 125 | port = int(arg.pop('port')) if arg.get('port') else 27017 126 | if arg.get('user'): 127 | arg['username'] = arg.pop('user') 128 | # username = arg['user'] if arg.get('user') else '' 129 | # password = arg['password'] if arg.get('password') else '' 130 | # self.connect = pymongo.MongoClient('mongodb://' + username + password + arg['host'] + ':' + arg['port'] + '/') 131 | self.connect = pymongo.MongoClient(host=host, port=port, **arg) 132 | self.connect.server_info() 133 | self.db = self.connect[arg['dbname']] 134 | 135 | return 136 | 137 | sql = '' 138 | if arg['type'].lower() == 'mysql': 139 | import pymysql as mysql 140 | self.connect = mysql.connect( 141 | host=arg['host'], port=int(arg['port']), user=arg['user'], password=arg['password'], database=arg['dbname'], charset=arg.get('charset', 'utf8')) 142 | self.cursor = self.connect.cursor() 143 | sql = 'select version()' 144 | 145 | elif arg['type'].lower() == 'oracle': 146 | import os 147 | import cx_Oracle as oracle 148 | # Oracle查询出的数据,中文输出问题解决 149 | os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8' 150 | self.connect = oracle.connect( 151 | arg['user'] + '/' + arg['password'] + '@' + arg['host'] + '/' + arg['sid']) 152 | self.cursor = self.connect.cursor() 153 | sql = 'select * from v$version' 154 | elif arg['type'].lower() == 'sqlserver': 155 | import pymssql as sqlserver 156 | self.connect = sqlserver.connect( 157 | host=arg['host'], port=arg['port'], user=arg['user'], password=arg['password'], database=arg['dbname'], charset=arg.get('charset', 'utf8')) 158 | self.cursor = self.connect.cursor() 159 | sql = 'select @@version' 160 | 161 | self.cursor.execute(sql) 162 | self.cursor.fetchone() 163 | 164 | except: 165 | log.exception(f'*** {arg["type"]} connect is failure ***') 166 | raise 167 | 168 | def fetchone(self, sql): 169 | try: 170 | self.cursor.execute(sql) 171 | data = self.cursor.fetchone() 172 | self.connect.commit() 173 | return data 174 | except: 175 | log.exception('*** fetchone failure ***') 176 | raise 177 | 178 | def fetchall(self, sql): 179 | try: 180 | self.cursor.execute(sql) 181 | data = self.cursor.fetchall() 182 | self.connect.commit() 183 | return data 184 | except: 185 | log.exception('*** Fetchall failure ***') 186 | raise 187 | 188 | def execute(self, sql): 189 | try: 190 | self.cursor.execute(sql) 191 | self.connect.commit() 192 | except: 193 | log.exception('*** execute failure ***') 194 | raise 195 | 196 | def mongo(self, collection, sql): 197 | try: 198 | cmd = 'self.db[\'' + collection + '\'].' + sql 199 | result = eval(cmd) 200 | if sql.startswith('find_one'): 201 | return result 202 | elif sql.startswith('find'): 203 | for d in result: 204 | return d 205 | elif 'count' in sql: 206 | return {'count': result} 207 | else: 208 | return {} 209 | except: 210 | log.exception('*** execute failure ***') 211 | raise 212 | 213 | def __del__(self): 214 | self.connect.close() 215 | -------------------------------------------------------------------------------- /sweet/modules/file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | keywords = { 5 | '复制': 'COPY', 6 | 'COPY': 'COPY', 7 | '移动': 'MOVE', 8 | 'MOVE': 'MOVE', 9 | '删除文件': 'REMOVE', 10 | 'REMOVE': 'REMOVE', 11 | '删除目录': 'RMDIR', 12 | 'RMDIR': 'RMDIR', 13 | '创建目录': 'MKDIR', 14 | 'MKDIR': 'MKDIR', 15 | '路径存在': 'EXISTS', 16 | 'EXISTS': 'EXISTS', 17 | '路径不存在': 'NOT_EXISTS', 18 | 'NOT_EXISTS': 'NOT_EXISTS', 19 | '是文件': 'IS_FILE', 20 | 'IS_FILE': 'IS_FILE', 21 | '是目录': 'IS_DIR', 22 | 'IS_DIR': 'IS_DIR', 23 | '不是文件': 'NOT_FILE', 24 | 'NOT_FILE': 'NOT_FILE', 25 | '不是目录': 'NOT_DIR', 26 | 'NOT_DIR': 'NOT_DIR' 27 | } 28 | 29 | 30 | class App: 31 | 32 | keywords = keywords 33 | 34 | def __init__(self, setting): 35 | self.dir = '.' 36 | if 'dir' in setting: 37 | self.dir = setting['dir'] 38 | os.chdir(self.dir) 39 | 40 | def _close(self): 41 | pass 42 | 43 | def _call(self, step): 44 | # 根据关键字调用关键字实现 45 | getattr(self, step['keyword'].lower())(step) 46 | 47 | 48 | def copy(self, step): 49 | source = step['element'] 50 | data = step['data'] 51 | destination = data['text'] 52 | 53 | if 'dir' in data: 54 | os.chdir(data['dir']) 55 | else: 56 | os.chdir(self.dir) 57 | 58 | code = 0 59 | if os.name == 'nt': 60 | code = os.system(f'COPY /Y {source} {destination}') 61 | if os.name == 'posix': 62 | code = os.system(f'cp -f -R {source} {destination}') 63 | 64 | if code != 0: 65 | raise Exception( 66 | f'COPY {source} {destination} is failure, code: {code}') 67 | 68 | def move(self, step): 69 | source = step['element'] 70 | data = step['data'] 71 | destination = data['text'] 72 | 73 | if 'dir' in data: 74 | os.chdir(data['dir']) 75 | else: 76 | os.chdir(self.dir) 77 | 78 | code = 0 79 | if os.name == 'nt': 80 | code = os.system(f'MOVE /Y {source} {destination}') 81 | if os.name == 'posix': 82 | code = os.system(f'mv -f {source} {destination}') 83 | 84 | if code != 0: 85 | raise Exception( 86 | f'MOVE {source} {destination} is failure, code: {code}') 87 | 88 | def remove(self, step): 89 | path = step['element'] 90 | data = step['data'] 91 | 92 | if 'dir' in data: 93 | os.chdir(data['dir']) 94 | else: 95 | os.chdir(self.dir) 96 | 97 | code = 0 98 | if os.name == 'nt': 99 | code = os.system(f'del /S /Q {path}') 100 | if os.name == 'posix': 101 | code = os.system(f'rm -f {path}') 102 | 103 | if code != 0: 104 | raise Exception(f'REMOVE {path} is failure, code: {code}') 105 | 106 | def rmdir(self, step): 107 | path = step['element'] 108 | data = step['data'] 109 | 110 | if 'dir' in data: 111 | os.chdir(data['dir']) 112 | else: 113 | os.chdir(self.dir) 114 | 115 | code = 0 116 | if os.name == 'nt': 117 | code = os.system(f'rd /S /Q {path}') 118 | if os.name == 'posix': 119 | code = os.system(f'rm -rf {path}') 120 | 121 | if code != 0: 122 | raise Exception(f'RERMDIR {path} is failure, code: {code}') 123 | 124 | def mkdir(self, step): 125 | path = step['element'] 126 | data = step['data'] 127 | 128 | if 'dir' in data: 129 | os.chdir(data['dir']) 130 | else: 131 | os.chdir(self.dir) 132 | 133 | code = 0 134 | if os.name == 'nt': 135 | code = os.system(f'mkdir {path}') 136 | if os.name == 'posix': 137 | code = os.system(f'mkdir -p {path}') 138 | 139 | if code != 0: 140 | raise Exception(f'MKDIR {path} is failure, code: {code}') 141 | 142 | def exists(self, step): 143 | path = step['element'] 144 | result = os.path.exists(path) 145 | 146 | if not result: 147 | raise Exception(f'{path} is not exists') 148 | 149 | def not_exists(self, step): 150 | try: 151 | self.exists(step) 152 | except: 153 | pass 154 | else: 155 | path = step['element'] 156 | raise Exception(f'{path} is a exists') 157 | 158 | def is_file(self, step): 159 | path = step['element'] 160 | 161 | result = os.path.isfile(path) 162 | 163 | if not result: 164 | raise Exception(f'{path} is not file') 165 | 166 | def not_file(self, step): 167 | try: 168 | self.is_file(step) 169 | except: 170 | pass 171 | else: 172 | path = step['element'] 173 | raise Exception(f'{path} is a file') 174 | 175 | def is_dir(self, step): 176 | path = step['element'] 177 | 178 | result = os.path.isdir(path) 179 | 180 | if not result: 181 | raise Exception(f'{path} is not dir') 182 | 183 | def not_dir(self, step): 184 | try: 185 | self.is_dir(step) 186 | except: 187 | pass 188 | else: 189 | path = step['element'] 190 | raise Exception(f'{path} is a dir') 191 | -------------------------------------------------------------------------------- /sweet/modules/http.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from pathlib import Path 3 | from injson import check 4 | 5 | from sweet import log, vars 6 | from sweet.utility import json2dict 7 | 8 | 9 | path = Path('lib') / 'http_handle.py' 10 | if path.is_file(): 11 | from lib import http_handle 12 | else: 13 | from sweet.lib import http_handle 14 | 15 | 16 | keywords = { 17 | 'GET': 'GET', 18 | 'POST': 'POST', 19 | 'PUT': 'PUT', 20 | 'PATCH': 'PATCH', 21 | 'DELETE': 'DELETE', 22 | 'OPTIONS': 'OPTIONS' 23 | } 24 | 25 | 26 | class App: 27 | 28 | keywords = keywords 29 | 30 | def __init__(self, setting): 31 | # 获取 path 32 | self.path = setting.get('path', '') 33 | if self.path: 34 | if not self.path.endswith('/'): 35 | self.path += '/' 36 | 37 | self.r = requests.Session() 38 | 39 | # 获取 headers 40 | self.headers = {} 41 | for key in keywords: 42 | if setting.get(key.lower()): 43 | self.headers[key.upper()] = setting.get(key.lower()) 44 | 45 | 46 | def _close(self): 47 | self.r.close() 48 | 49 | def _call(self, step): 50 | # 根据关键字调用关键字实现 51 | getattr(self, step['keyword'].lower())(step) 52 | 53 | 54 | def get(self, step): 55 | self.request('get', step) 56 | 57 | def post(self, step): 58 | self.request('post', step) 59 | 60 | def put(self, step): 61 | self.request('put', step) 62 | 63 | def patch(self, step): 64 | self.request('patch', step) 65 | 66 | def delete(self, step): 67 | self.request('delete', step) 68 | 69 | def options(self, step): 70 | self.request('options', step) 71 | 72 | 73 | def request(self, kw, step): 74 | url = step['element'] 75 | if url.startswith('/'): 76 | url = url[1:] 77 | 78 | data = step['data'] 79 | # 测试数据解析时,会默认添加一个 text 键,需要删除 80 | if 'text' in data and not data['text']: 81 | data.pop('text') 82 | 83 | _data = {} 84 | _data['headers'] = json2dict(data.pop('headers', '{}')) 85 | if data.get('cookies'): 86 | data['cookies'] = json2dict(data['cookies']) 87 | if kw == 'get': 88 | _data['params'] = json2dict( 89 | data.pop('params', '{}')) or json2dict(data.pop('data', '{}')) 90 | elif kw == 'post': 91 | if data.get('text'): 92 | _data['data'] = data.pop('text').encode('utf-8') 93 | else: 94 | _data['data'] = json2dict(data.pop('data', '{}')) 95 | _data['json'] = json2dict(data.pop('json', '{}')) 96 | _data['files'] = eval(data.pop('files', 'None')) 97 | elif kw in ('put', 'patch'): 98 | _data['data'] = json2dict(data.pop('data', '{}')) 99 | 100 | for k in data: 101 | for s in ('{', '[', 'False', 'True'): 102 | if s in data[k]: 103 | try: 104 | data[k] = eval(data[k]) 105 | except: 106 | log.warning(f'try eval data failure: {data[k]}') 107 | break 108 | expected = step['expected'] 109 | expected['status_code'] = expected.get('status_code', None) 110 | expected['text'] = expected.get('text', '').encode('utf-8') 111 | expected['json'] = json2dict(expected.get('json', '{}')) 112 | expected['cookies'] = json2dict(expected.get('cookies', '{}')) 113 | expected['headers'] = json2dict(expected.get('headers', '{}')) 114 | timeout = float(expected.get('timeout', 10)) 115 | expected['time'] = float(expected.get('time', 0)) 116 | 117 | for key in keywords: 118 | if kw.upper() == key.upper(): 119 | self.r.headers.update(self.headers[key.upper()]) 120 | 121 | log.debug(f'URL: {self.path + url}') 122 | 123 | # 处理 before_send 124 | before_send = data.pop('before_send', '') 125 | if before_send: 126 | _data, data = getattr(http_handle, before_send)(kw, _data, data) 127 | else: 128 | _data, data = getattr(http_handle, 'before_send')(kw, _data, data) 129 | 130 | if _data['headers']: 131 | for k in [x for x in _data['headers']]: 132 | if not _data['headers'][k]: 133 | del self.r.headers[k] 134 | del _data['headers'][k] 135 | self.r.headers.update(_data['headers']) 136 | 137 | r = '' 138 | if kw == 'get': 139 | r = getattr(self.r, kw)(self.path + url, 140 | params=_data['params'], timeout=timeout, **data) 141 | if _data['params']: 142 | log.debug(f'PARAMS: {_data["params"]}') 143 | 144 | elif kw == 'post': 145 | r = getattr(self.r, kw)(self.path + url, 146 | data=_data['data'], json=_data['json'], files=_data['files'], timeout=timeout, **data) 147 | log.debug(f'BODY: {r.request.body}') 148 | 149 | elif kw in ('put', 'patch'): 150 | r = getattr(self.r, kw)(self.path + url, 151 | data=_data['data'], timeout=timeout, **data) 152 | log.debug(f'BODY: {r.request.body}') 153 | 154 | elif kw in ('delete', 'options'): 155 | r = getattr(self.r, kw)(self.path + url, timeout=timeout, **data) 156 | 157 | log.debug(f'status_code: {repr(r.status_code)}') 158 | try: # json 响应 159 | log.debug(f'response json: {repr(r.json())}') 160 | except: # 其他响应 161 | log.debug(f'response text: {repr(r.text)}') 162 | 163 | response = {'status_code': r.status_code, 'headers': r.headers, 164 | '_cookies': r.cookies, 'content': r.content, 'text': r.text} 165 | 166 | try: 167 | response['cookies'] = requests.utils.dict_from_cookiejar(r.cookies) 168 | except: 169 | response['cookies'] = r.cookies 170 | 171 | try: 172 | j = r.json() 173 | response['json'] = j 174 | except: 175 | response['json'] = {} 176 | 177 | # 处理 after_receive 178 | after_receive = expected.pop('after_receive', '') 179 | if after_receive: 180 | response = getattr(http_handle, after_receive)(response) 181 | else: 182 | response = getattr(http_handle, 'after_receive')(response) 183 | 184 | if expected['status_code']: 185 | if str(expected['status_code']) != str(response['status_code']): 186 | raise Exception( 187 | f'status_code | EXPECTED:{repr(expected["status_code"])}, REAL:{repr(response["status_code"])}') 188 | 189 | if expected['text']: 190 | if expected['text'].startswith('*'): 191 | if expected['text'][1:] not in response['text']: 192 | raise Exception( 193 | f'text | EXPECTED:{repr(expected["text"])}, REAL:{repr(response["text"])}') 194 | else: 195 | if expected['text'] == response['text']: 196 | raise Exception( 197 | f'text | EXPECTED:{repr(expected["text"])}, REAL:{repr(response["text"])}') 198 | 199 | if expected['headers']: 200 | result = check(expected['headers'], response['headers']) 201 | log.debug(f'headers check result: {result}') 202 | if result['code'] != 0: 203 | raise Exception( 204 | f'headers | EXPECTED:{repr(expected["headers"])}, REAL:{repr(response["headers"])}, RESULT: {result}') 205 | elif result['var']: 206 | # var.update(result['var']) 207 | vars.put(result['var']) 208 | log.debug(f'headers var: {repr(result["var"])}') 209 | 210 | if expected['cookies']: 211 | log.debug(f'response cookies: {response["cookies"]}') 212 | result = check(expected['cookies'], response['cookies']) 213 | log.debug(f'cookies check result: {result}') 214 | if result['code'] != 0: 215 | raise Exception( 216 | f'cookies | EXPECTED:{repr(expected["cookies"])}, REAL:{repr(response["cookies"])}, RESULT: {result}') 217 | elif result['var']: 218 | # var.update(result['var']) 219 | vars.put(result['var']) 220 | log.debug(f'cookies var: {repr(result["var"])}') 221 | 222 | if expected['json']: 223 | result = check(expected['json'], response['json']) 224 | log.debug(f'json check result: {result}') 225 | if result['code'] != 0: 226 | raise Exception( 227 | f'json | EXPECTED:{repr(expected["json"])}, REAL:{repr(response["json"])}, RESULT: {result}') 228 | elif result['var']: 229 | # var.update(result['var']) 230 | vars.put(result['var']) 231 | log.debug(f'json var: {repr(result["var"])}') 232 | 233 | if expected['time']: 234 | if expected['time'] < r.elapsed.total_seconds(): 235 | raise Exception( 236 | f'time | EXPECTED:{repr(expected["time"])}, REAL:{repr(r.elapsed.total_seconds())}') 237 | 238 | output = step['output'] 239 | # if output: 240 | # log.debug('output: %s' % repr(output)) 241 | 242 | for k, v in output.items(): 243 | if v == 'status_code': 244 | status_code = response['status_code'] 245 | vars.put({k: status_code}) 246 | log.debug(f'{k}: {status_code}') 247 | elif v == 'text': 248 | text = response['text'] 249 | vars.put({k: text}) 250 | log.debug(f'{k}: {text}') 251 | elif k == 'json': 252 | sub = json2dict(output.get('json', '{}')) 253 | result = check(sub, response['json']) 254 | # var.update(result['var']) 255 | vars.put(result['var']) 256 | log.debug(f'json var: {repr(result["var"])}') 257 | elif k == 'cookies': 258 | sub = json2dict(output.get('cookies', '{}')) 259 | result = check(sub, response['cookies']) 260 | vars.put(result['var']) 261 | log.debug(f'cookies var: {repr(result["var"])}') 262 | -------------------------------------------------------------------------------- /sweet/modules/mobile/__init__.py: -------------------------------------------------------------------------------- 1 | from sweet.modules.mobile.app import App 2 | 3 | -------------------------------------------------------------------------------- /sweet/modules/mobile/app.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.common.exceptions import ElementClickInterceptedException 3 | from appium.webdriver.common.touch_action import TouchAction 4 | from time import sleep 5 | import re 6 | 7 | from sweet import log, vars 8 | from sweet.utility import compare, replace, json2dict 9 | 10 | from sweet.modules.mobile.window import Windows 11 | from sweet.modules.web.locator import locating 12 | from sweet.modules.web.config import * 13 | 14 | 15 | class App: 16 | 17 | keywords = keywords 18 | 19 | def __init__(self, setting): 20 | self.action = {} 21 | platform = setting.get('platformName', '') 22 | # snapshot = setting.pop('snapshot', False) 23 | 24 | if platform.lower() == 'ios': 25 | from appium import webdriver as appdriver 26 | self.driver = appdriver.Remote(self.server_url, self.desired_caps) 27 | 28 | elif platform.lower() == 'android': 29 | from appium import webdriver as appdriver 30 | self.driver = appdriver.Remote(self.server_url, self.desired_caps) 31 | 32 | # 等待元素超时时间 33 | self.driver.implicitly_wait(element_wait_timeout) # seconds 34 | # 页面刷新超时时间 35 | self.driver.set_page_load_timeout(page_flash_timeout) # seconds 36 | self.w = Windows() 37 | self.w.driver = self.driver 38 | 39 | def _close(self): 40 | pass 41 | 42 | def _call(self, step): 43 | # 处理截图数据 44 | # snap = Snapshot() 45 | # snap.pre(step) 46 | 47 | context = replace(step.get('frame', '')).strip() 48 | self.w.switch_context(context) 49 | 50 | if self.w.current_context.startswith('WEBVIEW'): 51 | # 切换标签页 52 | tab = step['data'].get('#tab') 53 | if tab: 54 | del step['data']['#tab'] 55 | self.driver.switch_to_window(self.w.windows[tab]) 56 | log.debug(f'current context: {repr(self.w.current_context)}') 57 | 58 | # 根据关键字调用关键字实现 59 | element = getattr(self, step['keyword'].lower())(step) 60 | # snap.web_shot(step, element) 61 | 62 | 63 | def title(self, data, output): 64 | log.debug(f'DATA:{repr(data["text"])}') 65 | log.debug(f'REAL:{repr(self.driver.title)}') 66 | 67 | if data['text'].startswith('*'): 68 | assert data['text'][1:] in self.driver.title 69 | else: 70 | assert data['text'] == self.driver.title 71 | # 只能获取到元素标题 72 | for key in output: 73 | vars.put({key: self.driver.title}) 74 | 75 | 76 | def current_url(self, data, output): 77 | log.debug(f'DATA:{repr(data["text"])}') 78 | log.debug(f'REAL:{repr(self.driver.current_url)}') 79 | try: 80 | if data['text'].startswith('*'): 81 | assert data['text'][1:] in self.driver.current_url 82 | else: 83 | assert data['text'] == self.driver.current_url 84 | except: 85 | raise Exception( 86 | f'check failure, DATA:{data["text"]}, REAL:{self.driver.current_url}') 87 | # 只能获取到元素 url 88 | for key in output: 89 | vars.put({key: self.driver.current_url}) 90 | return self.driver.current_url 91 | 92 | def locat(self, element, action=''): 93 | if not isinstance(element, dict): 94 | raise Exception(f'no this element:{element}') 95 | 96 | 97 | def open(self, step): 98 | url = step['element']['value'] 99 | 100 | if step['data'].get('#clear', ''): 101 | self.driver.delete_all_cookies() 102 | 103 | self.driver.get(url) 104 | 105 | cookie = step['data'].get('cookie', '') 106 | if cookie: 107 | self.driver.add_cookie(json2dict(cookie)) 108 | co = self.driver.get_cookie(json2dict(cookie).get('name', '')) 109 | log.debug(f'cookie is add: {co}') 110 | sleep(0.5) 111 | 112 | 113 | def check(self, step): 114 | data = step['data'] 115 | if not data: 116 | data = step['expected'] 117 | 118 | element = step['element'] 119 | by = element['by'] 120 | output = step['output'] 121 | 122 | if by in ('title', 'current_url'): 123 | getattr(self, by)(data, output) 124 | else: 125 | location = self.locat(element) 126 | for key in data: 127 | # 预期结果 128 | expected = data[key] 129 | # 切片操作处理 130 | s = re.findall(r'\[.*?\]', key) 131 | if s: 132 | s = s[0] 133 | key = key.replace(s, '') 134 | 135 | if key == 'text': 136 | real = location.text 137 | else: 138 | real = location.get_attribute(key) 139 | if s: 140 | real = eval('real' + s) 141 | 142 | log.debug(f'DATA:{repr(expected)}') 143 | log.debug(f'REAL:{repr(real)}') 144 | try: 145 | compare(expected, real) 146 | except: 147 | raise Exception( 148 | f'check failure, DATA:{repr(expected)}, REAL:{repr(real)}') 149 | 150 | # 获取元素其他属性 151 | for key in output: 152 | if output[key] == 'text': 153 | v = location.text 154 | vars.put({key: v}) 155 | elif output[key] in ('text…', 'text...'): 156 | if location.text.endswith('...'): 157 | v = location.text[:-3] 158 | vars.put({key: v}) 159 | else: 160 | v = location.text 161 | vars.put({key: v}) 162 | else: 163 | v = location.get_attribute(output[key]) 164 | vars.put({key: v}) 165 | 166 | 167 | def notcheck(self, step): 168 | try: 169 | self.check(step) 170 | raise Exception('check is success') 171 | except: 172 | pass 173 | 174 | def input(self, step): 175 | data = step['data'] 176 | location = self.locat(step['element']) 177 | 178 | if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': 179 | pass 180 | else: 181 | location.clear() 182 | 183 | for key in data: 184 | if key.startswith('text'): 185 | if isinstance(data[key], tuple): 186 | location.send_keys(*data[key]) 187 | elif location: 188 | location.send_keys(data[key]) 189 | sleep(0.5) 190 | if key == 'word': # 逐字输入 191 | for d in data[key]: 192 | location.send_keys(d) 193 | sleep(0.3) 194 | 195 | def set_value(self, step): 196 | data = step['data'] 197 | location = self.locat(step['element']) 198 | if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': 199 | pass 200 | else: 201 | location.clear() 202 | 203 | for key in data: 204 | if key.startswith('text'): 205 | if isinstance(data[key], tuple): 206 | location.set_value(*data[key]) 207 | elif location: 208 | location.set_value(data[key]) 209 | sleep(0.5) 210 | if key == 'word': # 逐字输入 211 | for d in data[key]: 212 | location.set_value(d) 213 | sleep(0.3) 214 | 215 | def click(self, step): 216 | elements = step['elements'] # click 支持多个元素连续操作,需要转换为 list 217 | # data = step['data'] 218 | 219 | location = '' 220 | for element in elements: 221 | location = self.locat(element, 'CLICK') 222 | sleep(0.5) 223 | try: 224 | location.click() 225 | except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 226 | sleep(1) 227 | location.click() 228 | sleep(0.5) 229 | 230 | # 获取元素其他属性 231 | output = step['output'] 232 | for key in output: 233 | if output[key] == 'text': 234 | vars.put({key: location.text}) 235 | elif output[key] == 'tag_name': 236 | vars.put({key: location.tag_name}) 237 | elif output[key] in ('text…', 'text...'): 238 | if location.text.endswith('...'): 239 | vars.put({key: location.text[:-3]}) 240 | else: 241 | vars.put({key: location.text}) 242 | else: 243 | vars.put({key: location.get_attribute(output[key])}) 244 | 245 | def tap(self, step): 246 | action = TouchAction(self.driver) 247 | 248 | elements = step['elements'] # click 支持多个元素连续操作,需要转换为 list 249 | # data = step['data'] 250 | 251 | location = '' 252 | 253 | for element in elements: 254 | if ',' in element: 255 | position = element.split(',') 256 | x = int(position[0]) 257 | y = int(position[1]) 258 | position = (x, y) 259 | self.driver.tap([position]) 260 | sleep(0.5) 261 | else: 262 | location = self.locat(element, 'CLICK') 263 | action.tap(location).perform() 264 | sleep(0.5) 265 | 266 | # 获取元素其他属性 267 | output = step['output'] 268 | for key in output: 269 | if output[key] == 'text': 270 | vars.put({key: location.text}) 271 | elif output[key] == 'tag_name': 272 | vars.put({key: location.tag_name}) 273 | elif output[key] in ('text…', 'text...'): 274 | if location.text.endswith('...'): 275 | vars.put({key: location.text[:-3]}) 276 | else: 277 | vars.put({key: location.text}) 278 | else: 279 | vars.put({key: location.get_attribute(output[key])}) 280 | 281 | def press_keycode(self, step): 282 | element = step['element'] 283 | self.driver.press_keycode(int(element)) 284 | 285 | def swipe(self, step): 286 | elements = step['elements'] 287 | duration = step['data'].get('持续时间', 0.3) 288 | assert isinstance(elements, list) and len( 289 | elements) == 2, '坐标格式或数量不对,正确格式如:100,200|300,400' 290 | 291 | start = elements[0].replace(',', ',').split(',') 292 | start_x = int(start[0]) 293 | start_y = int(start[1]) 294 | 295 | end = elements[1].replace(',', ',').split(',') 296 | end_x = int(end[0]) 297 | end_y = int(end[1]) 298 | 299 | if duration: 300 | self.driver.swipe(start_x, start_y, end_x, 301 | end_y, sleep(float(duration))) 302 | else: 303 | self.driver.swipe(start_x, start_y, end_x, end_y) 304 | 305 | def line(self, step): 306 | elements = step['elements'] 307 | duration = float(step['data'].get('持续时间', 0.3)) 308 | assert isinstance(elements, list) and len( 309 | elements) > 1, '坐标格式或数量不对,正确格式如:258,756|540,1032' 310 | postions = [] 311 | for element in elements: 312 | element = element.replace(',', ',') 313 | p = element.split(',') 314 | postions.append(p) 315 | 316 | action = TouchAction(self.driver) 317 | action = action.press( 318 | x=postions[0][0], y=postions[0][1]).wait(duration * 1000) 319 | for i in range(1, len(postions)): 320 | action.move_to(x=postions[i][0], y=postions[i] 321 | [1]).wait(duration * 1000) 322 | action.release().perform() 323 | 324 | def line_unlock(self, step): 325 | elements = step['elements'] 326 | duration = float(step['data'].get('持续时间', 0.3)) 327 | assert isinstance(elements, list) and len( 328 | elements) > 2, '坐标格式或数量不对,正确格式如:lock_pattern|1|4|7|8|9' 329 | location = self.locat(elements[0]) 330 | rect = location.rect 331 | w = rect['width'] / 6 332 | h = rect['height'] / 6 333 | 334 | key = {} 335 | key['1'] = (rect['x'] + 1 * w, rect['y'] + 1 * h) 336 | key['2'] = (rect['x'] + 3 * w, rect['y'] + 1 * h) 337 | key['3'] = (rect['x'] + 5 * w, rect['y'] + 1 * h) 338 | key['4'] = (rect['x'] + 1 * w, rect['y'] + 3 * h) 339 | key['5'] = (rect['x'] + 3 * w, rect['y'] + 3 * h) 340 | key['6'] = (rect['x'] + 5 * w, rect['y'] + 3 * h) 341 | key['7'] = (rect['x'] + 1 * w, rect['y'] + 5 * h) 342 | key['8'] = (rect['x'] + 3 * w, rect['y'] + 5 * h) 343 | key['9'] = (rect['x'] + 5 * w, rect['y'] + 5 * h) 344 | 345 | action = TouchAction(self.driver) 346 | for i in range(1, len(elements)): 347 | k = elements[i] 348 | if i == 1: 349 | action = action.press( 350 | x=key[k][0], y=key[k][1]).wait(duration * 1000) 351 | action.move_to(x=key[k][0], y=key[k][1]).wait(duration * 1000) 352 | action.release().perform() 353 | 354 | def rocker(self, step): 355 | elements = step['elements'] 356 | duration = float(step['data'].get('持续时间', 0.3)) 357 | rocker_name = step['data'].get('摇杆', 'rocker') 358 | release = step['data'].get('释放', False) 359 | 360 | # if isinstance(element, str): 361 | # if element: 362 | # element = [element] 363 | # else: 364 | # element = [] 365 | 366 | postions = [] 367 | for element in elements: 368 | element = element.replace(',', ',') 369 | p = element.split(',') 370 | postions.append(p) 371 | 372 | # 如果 action 中么有此摇杆名,则是新的遥感 373 | if not self.action.get(rocker_name): 374 | self.action[rocker_name] = TouchAction(self.driver) 375 | self.action[rocker_name].press( 376 | x=postions[0][0], y=postions[0][1]).wait(duration * 1000) 377 | # 新摇杆的第一个点已操作,需要删除 378 | postions.pop(0) 379 | # 依次操作 380 | for i in range(len(postions)): 381 | self.action[rocker_name].move_to( 382 | x=postions[i][0], y=postions[i][1]).wait(duration * 1000) 383 | 384 | if release: 385 | # 释放摇杆,并删除摇杆 386 | self.action[rocker_name].release().perform() 387 | del self.action[rocker_name] 388 | else: 389 | self.action[rocker_name].perform() 390 | 391 | def scroll(self, step): 392 | elements = step['elements'] 393 | assert isinstance(elements, list) and len( 394 | elements) == 2, '元素格式或数量不对,正确格式如:origin_el|destination_el' 395 | origin = self.locat(elements[0]) 396 | destination = self.locat(elements[1]) 397 | self.driver.scroll(origin, destination) 398 | 399 | def flick_element(self, step): 400 | elements = step['elements'] 401 | speed = step['data'].get('持续时间', 10) 402 | assert isinstance(elements, list) and len( 403 | elements) == 2, '坐标格式或数量不对,正确格式如:elment|200,300' 404 | location = self.locat(elements[0]) 405 | 406 | end = elements[1].replace(',', ',').split(',') 407 | end_x = int(end[0]) 408 | end_y = int(end[1]) 409 | 410 | if speed: 411 | self.driver.flick_element(location, end_x, end_y, int(speed)) 412 | 413 | def flick(self, step): 414 | elements = step['elements'] 415 | assert isinstance(elements, list) and len( 416 | elements) == 2, '坐标格式或数量不对,正确格式如:100,200|300,400' 417 | 418 | start = elements[0].replace(',', ',').split(',') 419 | start_x = int(start[0]) 420 | start_y = int(start[1]) 421 | 422 | end = elements[1].replace(',', ',').split(',') 423 | end_x = int(end[0]) 424 | end_y = int(end[1]) 425 | 426 | self.driver.flick(start_x, start_y, end_x, end_y) 427 | 428 | def drag_and_drop(self, step): 429 | elements = step['elements'] 430 | assert isinstance(elements, list) and len( 431 | elements) == 2, '元素格式或数量不对,正确格式如:origin_el|destination_el' 432 | origin = self.locat(elements[0]) 433 | destination = self.locat(elements[1]) 434 | self.driver.drag_and_drop(origin, destination) 435 | 436 | def long_press(self, step): 437 | action = TouchAction(self.driver) 438 | 439 | element = step['element'] 440 | duration = step['data'].get('持续时间', 1000) 441 | if ',' in element or ',' in element: 442 | position = element.replace(',', ',').split(',') 443 | x = int(position[0]) 444 | y = int(position[1]) 445 | action.long_press(x=x, y=y, duration=duration).perform() 446 | else: 447 | location = self.locat(element) 448 | action.long_press(location, duration=duration).perform() 449 | sleep(0.5) 450 | 451 | def pinch(self, step): 452 | element = step['element'] 453 | location = self.locat(element) 454 | percent = step['data'].get('百分比', 200) 455 | steps = step['data'].get('步长', 50) 456 | self.driver.pinch(location, percent, steps) 457 | 458 | def zoom(self, step): 459 | element = step['element'] 460 | location = self.locat(element) 461 | percent = step['data'].get('百分比', 200) 462 | steps = step['data'].get('步长', 50) 463 | self.driver.zoom(location, percent, steps) 464 | 465 | def hide_keyboard(self, step): 466 | self.driver.hide_keyboard() 467 | 468 | def shake(self, step): 469 | self.driver.shake() 470 | 471 | def launch_app(self, step): 472 | self.driver.launch_app() 473 | 474 | def is_locked(self, step): 475 | status = self.driver.is_locked() 476 | assert status, "it's not locked" 477 | 478 | def lock(self, step): 479 | self.driver.lock() 480 | 481 | def unlock(self, step): 482 | self.driver.unlock() -------------------------------------------------------------------------------- /sweet/modules/mobile/config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | element_wait_timeout = 10 # 等待元素出现超时时间,单位:秒 4 | page_flash_timeout = 90 # 页面刷新超时时间,单位:秒 5 | 6 | 7 | keywords = { 8 | '检查': 'CHECK', 9 | 'CHECK': 'CHECK', 10 | '#检查': 'NOTCHECK', 11 | '#CHECK': 'NOTCHECK', 12 | '输入': 'INPUT', 13 | 'INPUT': 'INPUT', 14 | '填写': 'SET_VALUE', 15 | 'SET_VALUE': 'SET_VALUE', 16 | '点击': 'CLICK', 17 | 'CLICK': 'CLICK', 18 | '轻点': 'TAP', 19 | 'TAP': 'TAP', 20 | '按键码': 'PRESS_KEYCODE', # Android 特有,常见代码 HOME:3, 菜单键:82,返回键:4 21 | 'PRESS_KEYCODE': 'PRESS_KEYCODE', 22 | '滑动': 'SWIPE', 23 | 'SWIPE': 'SWIPE', 24 | '划线': 'LINE', 25 | 'LINE': 'LINE', 26 | '划线解锁': 'LINE_UNLOCK', 27 | 'LINE_UNLOCK': 'LINE_UNLOCK', 28 | '摇杆': 'ROCKER', 29 | 'ROCKER': 'ROCKER', 30 | '滚动': 'SCROLL', # iOS 专用 31 | 'SCROLL': 'SCROLL', 32 | '拖拽': 'DRAG_AND_DROP', 33 | 'DRAG_AND_DROP': 'DRAG_AND_DROP', 34 | '摇晃': 'SHAKE', # 貌似 Android 上不可用 35 | 'SHAKE': 'SHAKE', 36 | '快速滑动': 'FLICK', 37 | 'FLICK': 'FLICK', 38 | '滑动元素': 'FLICK_ELEMENT', 39 | 'FLICK_ELEMENT': 'FLICK_ELEMENT', 40 | '长按': 'LONG_PRESS', 41 | 'LONG_PRESS': 'LONG_PRESS', 42 | '缩小': 'PINCH', 43 | 'PINCH': 'PINCH', 44 | '放大': 'ZOOM', 45 | 'ZOOM': 'ZOOM', 46 | '隐藏键盘': 'HIDE_KEYBOARD', # iOS 专用 47 | 'HIDE_KEYBOARD': 'HIDE_KEYBOARD', 48 | '命名标签页': 'TAB_NAME', 49 | 'TAB_NAME': 'TAB_NAME', 50 | '重启': 'LAUNCH_APP', 51 | 'LAUNCH_APP': 'LAUNCH_APP', 52 | '锁屏状态': 'IS_LOCKED', 53 | 'IS_LOCKED': 'IS_LOCKED', 54 | '锁屏': 'LOCK', 55 | 'LOCK': 'LOCK', 56 | '解锁': 'UNLOCK', 57 | 'UNLOCK': 'UNLOCK', 58 | } -------------------------------------------------------------------------------- /sweet/modules/mobile/locator.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.support.ui import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as EC 5 | from sweet import log 6 | from sweet.config import element_wait_timeout 7 | 8 | 9 | def locating(driver, app, element, action=''): 10 | location = None 11 | try: 12 | el= element 13 | value = el['value'] 14 | except: 15 | log.exception(f'locating the element:{element} is failure, this element is not define') 16 | raise Exception(f'locating the element:{element} is failure, this element is not define') 17 | 18 | if not isinstance(el, dict): 19 | raise Exception(f'locating the element:{element} is failure, this element is not define') 20 | 21 | wait = WebDriverWait(driver, element_wait_timeout) 22 | 23 | if el['by'].lower() in ('title', 'url', 'current_url'): 24 | return None 25 | else: 26 | try: 27 | location = wait.until(EC.presence_of_element_located( 28 | (getattr(By, el['by'].upper()), value))) 29 | except: 30 | sleep(5) 31 | try: 32 | location = wait.until(EC.presence_of_element_located( 33 | (getattr(By, el['by'].upper()), value))) 34 | except : 35 | raise Exception(f'locating the element:{element} is failure: timeout') 36 | try: 37 | if driver.name in ('chrome', 'safari'): 38 | driver.execute_script( 39 | "arguments[0].scrollIntoViewIfNeeded(true)", location) 40 | else: 41 | driver.execute_script( 42 | "arguments[0].scrollIntoView(false)", location) 43 | except: 44 | pass 45 | 46 | try: 47 | if action == 'CLICK': 48 | location = wait.until(EC.element_to_be_clickable( 49 | (getattr(By, el['by'].upper()), value))) 50 | else: 51 | location = wait.until(EC.visibility_of_element_located( 52 | (getattr(By, el['by'].upper()), value))) 53 | except: 54 | pass 55 | 56 | return location 57 | 58 | 59 | def locatings(elements): 60 | locations = {} 61 | for el in elements: 62 | locations[el] = locating(el) 63 | return locations 64 | 65 | 66 | # def locating_data(keys): 67 | # data_location = {} 68 | # for key in keys: 69 | # data_location[key] = locating(key) 70 | # return data_location 71 | -------------------------------------------------------------------------------- /sweet/modules/mobile/window.py: -------------------------------------------------------------------------------- 1 | from sweet import log 2 | 3 | class Windows: 4 | 5 | def __init__(self): 6 | 7 | self.current_context = 'NATIVE_APP' 8 | 9 | def switch_context(self, context): 10 | if context.strip() == '': 11 | context = 'NATIVE_APP' 12 | if context != self.current_context: 13 | if context == '': 14 | context = None 15 | log.debug(f'switch context: {repr(context)}') 16 | self.driver.switch_to.context(context) 17 | self.current_context = context 18 | -------------------------------------------------------------------------------- /sweet/modules/web/__init__.py: -------------------------------------------------------------------------------- 1 | from sweet.modules.web.app import App 2 | 3 | -------------------------------------------------------------------------------- /sweet/modules/web/app.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.action_chains import ActionChains 3 | from selenium.common.exceptions import ElementClickInterceptedException 4 | from selenium.webdriver.support.select import Select 5 | from time import sleep 6 | import re 7 | 8 | from sweet import log, vars 9 | from sweet.utility import compare, replace, json2dict 10 | 11 | from sweet.modules.web.window import Windows 12 | from sweet.modules.web.locator import locating 13 | from sweet.modules.web.config import * 14 | 15 | 16 | class App: 17 | 18 | keywords = keywords 19 | 20 | def __init__(self, setting): 21 | browserName = setting.get('browserName', '') 22 | headless = setting.pop('headless', False) 23 | # snapshot = setting.pop('snapshot', False) 24 | executable_path = setting.pop('executable_path', False) 25 | # server_url = setting.pop('server_url', '') 26 | 27 | if browserName.lower() == 'ie': 28 | if executable_path: 29 | self.driver = webdriver.Ie(executable_path=executable_path) 30 | else: 31 | self.driver = webdriver.Ie() 32 | elif browserName.lower() == 'firefox': 33 | profile = webdriver.FirefoxProfile() 34 | profile.accept_untrusted_certs = True 35 | 36 | options = webdriver.FirefoxOptions() 37 | # 如果配置了 headless 模式 38 | if headless: 39 | options.set_headless() 40 | # options.add_argument('-headless') 41 | options.add_argument('--disable-gpu') 42 | options.add_argument("--no-sandbox") 43 | options.add_argument('window-size=1920x1080') 44 | 45 | if executable_path: 46 | self.driver = webdriver.Firefox( 47 | firefox_profile=profile, firefox_options=options, executable_path=executable_path) 48 | else: 49 | self.driver = webdriver.Firefox( 50 | firefox_profile=profile, firefox_options=options) 51 | self.driver.maximize_window() 52 | elif browserName.lower() == 'chrome': 53 | options = webdriver.ChromeOptions() 54 | 55 | # 如果配置了 headless 模式 56 | if headless: 57 | options.add_argument('--headless') 58 | options.add_argument('--disable-gpu') 59 | options.add_argument("--no-sandbox") 60 | options.add_argument('window-size=1920x1080') 61 | 62 | options.add_argument("--start-maximized") 63 | options.add_argument('--ignore-certificate-errors') 64 | # 指定浏览器分辨率,当"--start-maximized"无效时使用 65 | # options.add_argument('window-size=1920x1080') 66 | prefs = {} 67 | prefs["credentials_enable_service"] = False 68 | prefs["profile.password_manager_enabled"] = False 69 | options.add_experimental_option("prefs", prefs) 70 | options.add_argument('disable-infobars') 71 | options.add_experimental_option( 72 | "excludeSwitches", ['load-extension', 'enable-automation', 'enable-logging']) 73 | if executable_path: 74 | self.driver = webdriver.Chrome( 75 | options=options, executable_path=executable_path) 76 | else: 77 | self.driver = webdriver.Chrome(options=options) 78 | else: 79 | raise Exception( 80 | 'Error: this browser is not supported or mistake name:%s' % browserName) 81 | # 等待元素超时时间 82 | self.driver.implicitly_wait(element_wait_timeout) # seconds 83 | # 页面刷新超时时间 84 | self.driver.set_page_load_timeout(page_flash_timeout) # seconds 85 | self.w = Windows() 86 | self.w.driver = self.driver 87 | 88 | def _close(self): 89 | self.w.close() 90 | 91 | def _call(self, step): 92 | # 处理截图数据 93 | # snap = Snapshot() 94 | # snap.pre(step) 95 | 96 | name = step['data'].pop('#tab', '') 97 | if name: 98 | self.w.tab(name) 99 | else: 100 | self.w.switch() 101 | 102 | frame = replace(step.get('frame', '')) 103 | self.w.switch_frame(frame) 104 | 105 | # 根据关键字调用关键字实现 106 | element = getattr(self, step['keyword'].lower())(step) 107 | # snap.web_shot(step, element) 108 | 109 | 110 | def title(self, data, output): 111 | log.debug(f'DATA:{repr(data["text"])}') 112 | log.debug(f'REAL:{repr(self.driver.title)}') 113 | # try: 114 | if data['text'].startswith('*'): 115 | assert data['text'][1:] in self.driver.title 116 | else: 117 | assert data['text'] == self.driver.title 118 | 119 | for key in output: 120 | vars.put({key: self.driver.title}) 121 | 122 | 123 | def current_url(self, data, output): 124 | log.debug(f'DATA:{repr(data["text"])}') 125 | log.debug(f'REAL:{repr(self.driver.current_url)}') 126 | try: 127 | if data['text'].startswith('*'): 128 | assert data['text'][1:] in self.driver.current_url 129 | else: 130 | assert data['text'] == self.driver.current_url 131 | except: 132 | raise Exception( 133 | f'check failure, DATA:{data["text"]}, REAL:{self.driver.current_url}') 134 | # 只能获取到元素 url 135 | for key in output: 136 | vars.put({key: self.driver.current_url}) 137 | 138 | def locat(self, element, action=''): 139 | if not isinstance(element, dict): 140 | raise Exception(f'no this element:{element}') 141 | return locating(self.driver, element, action=action) 142 | 143 | def open(self, step): 144 | if isinstance(step['element'], dict): 145 | url = step['element']['value'] 146 | else: 147 | url = step['element'] 148 | 149 | if step['data'].get('#clear', ''): 150 | self.driver.delete_all_cookies() 151 | 152 | self.driver.get(url) 153 | 154 | cookie = step['data'].get('cookie', '') 155 | if cookie: 156 | self.driver.add_cookie(json2dict(cookie)) 157 | co = self.driver.get_cookie(json2dict(cookie).get('name', '')) 158 | log.debug(f'cookie is add: {co}') 159 | sleep(0.5) 160 | 161 | def check(self, step): 162 | data = step['data'] 163 | if not data: 164 | data = step['expected'] 165 | element = step['element'] 166 | by = element['by'] 167 | output = step['output'] 168 | 169 | location = '' 170 | if by in ('title', 'current_url'): 171 | getattr(self, by)(data, output) 172 | else: 173 | location = self.locat(element) 174 | for key in data: 175 | # 预期结果 176 | expected = data[key] 177 | # 切片操作处理 178 | s = re.findall(r'\[.*?\]', key) 179 | if s: 180 | s = s[0] 181 | key = key.replace(s, '') 182 | 183 | if key == 'text': 184 | real = location.text 185 | else: 186 | real = location.get_attribute(key) 187 | if s: 188 | real = eval('real' + s) 189 | 190 | log.debug(f'DATA:{repr(expected)}') 191 | log.debug('REAL:{repr(real)}') 192 | try: 193 | compare(expected, real) 194 | except: 195 | raise Exception( 196 | f'check failure, DATA:{repr(expected)}, REAL:{repr(real)}') 197 | 198 | # 获取元素其他属性 199 | for key in output: 200 | if output[key] == 'text': 201 | v = location.text 202 | vars.put({key: v}) 203 | elif output[key] in ('text…', 'text...'): 204 | if location.text.endswith('...'): 205 | v = location.text[:-3] 206 | vars.put({key: v}) 207 | else: 208 | v = location.text 209 | vars.put({key: v}) 210 | else: 211 | v = location.get_attribute(output[key]) 212 | vars.put({key: v}) 213 | 214 | return location 215 | 216 | 217 | def notcheck(self, step): 218 | try: 219 | self.check(step) 220 | raise Exception('check is success') 221 | except: 222 | pass 223 | 224 | def input(self, step): 225 | data = step['data'] 226 | location = self.locat(step['element']) 227 | 228 | if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': 229 | pass 230 | else: 231 | location.clear() 232 | 233 | for key in data: 234 | if key.startswith('text'): 235 | if isinstance(data[key], tuple): 236 | location.send_keys(*data[key]) 237 | elif location: 238 | location.send_keys(data[key]) 239 | sleep(0.5) 240 | if key == 'word': # 逐字输入 241 | for d in data[key]: 242 | location.send_keys(d) 243 | sleep(0.3) 244 | return location 245 | 246 | def click(self, step): 247 | data = step['data'] 248 | 249 | location = '' 250 | for element in step.get('elements'): 251 | # location = locating(self.driver, element, 'CLICK') 252 | location = self.locat(element, 'CLICK') 253 | try: 254 | location.click() 255 | except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 256 | sleep(1) 257 | if data.get('mode'): 258 | self.driver.execute_script( 259 | "arguments[0].click();", location) 260 | else: 261 | location.click() 262 | sleep(0.5) 263 | 264 | # 获取元素其他属性 265 | output = step['output'] 266 | for key in output: 267 | if output[key] == 'text': 268 | vars.put({key: location.text}) 269 | elif output[key] == 'tag_name': 270 | vars.put({key: location.tag_name}) 271 | elif output[key] in ('text…', 'text...'): 272 | if location.text.endswith('...'): 273 | vars.put({key: location.text[:-3]}) 274 | else: 275 | vars.put({key: location.text}) 276 | else: 277 | vars.put({key: location.get_attribute(output[key])}) 278 | 279 | return location 280 | 281 | def select(self, step): 282 | data = step['data'] 283 | 284 | location = self.locat(step['element']) 285 | for key in data: 286 | if key.startswith('index'): 287 | Select(location).select_by_index(data[key]) 288 | elif key.startswith('value'): 289 | Select(location).select_by_value(data[key]) 290 | elif key.startswith('text') or key.startswith('visible_text'): 291 | Select(location).select_by_visible_text(data[key]) 292 | 293 | def deselect(self, step): 294 | data = step['data'] 295 | location = self.locat(step['element']) 296 | for key in data: 297 | if key.startswith('all'): 298 | Select(location).deselect_all() 299 | elif key.startswith('index'): 300 | Select(location).deselect_by_index(data[key]) 301 | elif key.startswith('value'): 302 | Select(location).deselect_by_value(data[key]) 303 | elif key.startswith('text') or key.startswith('visible_text'): 304 | Select(location).deselect_by_visible_text(data[key]) 305 | 306 | def hover(self, step): 307 | actions = ActionChains(self.driver) 308 | location = self.locat(step['element']) 309 | actions.move_to_element(location) 310 | actions.perform() 311 | sleep(0.5) 312 | 313 | return location 314 | 315 | def context_click(self, step): 316 | actions = ActionChains(self.driver) 317 | location = self.locat(step['element']) 318 | actions.context_click(location) 319 | actions.perform() 320 | sleep(0.5) 321 | 322 | return location 323 | 324 | def double_click(self, step): 325 | actions = ActionChains(self.driver) 326 | location = self.locat(step['element']) 327 | actions.double_click(location) 328 | actions.perform() 329 | sleep(0.5) 330 | 331 | return location 332 | 333 | def drag_and_drop(self, step): 334 | actions = ActionChains(self.driver) 335 | elements = step['elements'] 336 | source = self.locat(elements[0]) 337 | target = self.locat(elements[1]) 338 | actions.drag_and_drop(source, target) 339 | actions.perform() 340 | sleep(0.5) 341 | 342 | def swipe(self, step): 343 | actions = ActionChains(self.driver) 344 | data = step['data'] 345 | location = self.locat(step['element']) 346 | x = data.get('x', 0) 347 | y = data.get('y', 0) 348 | actions.drag_and_drop_by_offset(location, x, y) 349 | actions.perform() 350 | sleep(0.5) 351 | 352 | def script(self, step): 353 | element = step['element'] 354 | self.driver.execute_script(element) 355 | 356 | def message(self, step): 357 | data = step['data'] 358 | text = data.get('text', '') 359 | value = step['element'] 360 | 361 | if value.lower() in ('确认', 'accept'): 362 | self.driver.switch_to_alert().accept() 363 | elif value.lower() in ('取消', '关闭', 'cancel', 'close'): 364 | self.driver.switch_to_alert().dismiss() 365 | elif value.lower() in ('输入', 'input'): 366 | self.driver.switch_to_alert().send_keys(text) 367 | self.driver.switch_to_alert().accept() 368 | log.debug('switch frame: Alert') 369 | self.w.frame = 'Alert' 370 | 371 | def upload(self, step): 372 | import win32com.client 373 | 374 | data = step['data'] 375 | location = self.locat(step['element']) 376 | file_path = data.get('text', '') or data.get('file', '') 377 | 378 | location.click() 379 | sleep(3) 380 | shell = win32com.client.Dispatch("WScript.Shell") 381 | shell.Sendkeys(file_path) 382 | sleep(2) 383 | shell.Sendkeys("{ENTER}") 384 | sleep(2) 385 | 386 | def navigate(self, step): 387 | element = step['element'] 388 | 389 | if element.lower() in ('刷新', 'refresh'): 390 | self.driver.refresh() 391 | elif element.lower() in ('前进', 'forward'): 392 | self.driver.forward() 393 | elif element.lower() in ('后退', 'back'): 394 | self.driver.back() 395 | 396 | def scroll(self, step): 397 | data = step['data'] 398 | x = data.get('x') 399 | y = data.get('y') or data.get('text') 400 | 401 | element = step['element'] 402 | if element == '': 403 | # if x is None: 404 | # x = '0' 405 | # self.driver.execute_script( 406 | # f"windoself.w.scrollTo({x},{y})") 407 | if y: 408 | self.driver.execute_script( 409 | f"document.documentElement.scrollTop={y}") 410 | if x: 411 | self.driver.execute_script( 412 | f"document.documentElement.scrollLeft={x}") 413 | else: 414 | location = self.locat(element) 415 | 416 | if y: 417 | self.driver.execute_script( 418 | f"arguments[0].scrollTop={y}", location) 419 | if x: 420 | self.driver.execute_script( 421 | f"arguments[0].scrollLeft={x}", location) 422 | -------------------------------------------------------------------------------- /sweet/modules/web/config.py: -------------------------------------------------------------------------------- 1 | element_wait_timeout = 10 # 等待元素出现超时时间,单位:秒 2 | page_flash_timeout = 90 # 页面刷新超时时间,单位:秒 3 | 4 | 5 | keywords = { 6 | '打开': 'OPEN', 7 | 'OPEN': 'OPEN', 8 | '检查': 'CHECK', 9 | 'CHECK': 'CHECK', 10 | '#检查': 'NOTCHECK', 11 | '#CHECK': 'NOTCHECK', 12 | '输入': 'INPUT', 13 | 'INPUT': 'INPUT', 14 | '点击': 'CLICK', 15 | 'CLICK': 'CLICK', 16 | '选择': 'SELECT', 17 | 'SELECT': 'SELECT', 18 | '取消选择': 'DESELECT', 19 | 'DESELECT': 'DESELECT', 20 | '移动到': 'HOVER', 21 | '悬停': 'HOVER', 22 | 'HOVER': 'HOVER', 23 | '右击': 'CONTEXT_CLICK', 24 | 'CONTEXT_CLICK': 'CONTEXT_CLICK', 25 | '双击': 'DOUBLE_CLICK', 26 | 'DOUBLE_CLICK': 'DOUBLE_CLICK', 27 | '拖拽': 'DRAG_AND_DROP', 28 | 'DRAG_AND_DROP': 'DRAG_AND_DROP', 29 | '滑动': 'SWIPE', 30 | 'SWIPE': 'SWIPE', 31 | '脚本': 'SCRIPT', 32 | 'SCRIPT': 'SCRIPT', 33 | '对话框': 'MESSAGE', 34 | 'MESSAGE': 'MESSAGE', 35 | '上传文件': 'UPLOAD', 36 | 'UPLOAD': 'UPLOAD', 37 | '导航': 'NAVIGATE', 38 | 'NAVIGATE': 'NAVIGATE', 39 | '滚动条': 'SCROLL', 40 | 'SCROLL': 'SCROLL' 41 | } -------------------------------------------------------------------------------- /sweet/modules/web/locator.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.support.ui import WebDriverWait 4 | from selenium.webdriver.support import expected_conditions as EC 5 | from sweet import log 6 | from sweet.modules.web.config import element_wait_timeout 7 | 8 | 9 | def locating(driver, element, action=''): 10 | location = None 11 | try: 12 | el= element 13 | value = el['value'] 14 | except: 15 | log.exception(f'locating the element:{element} is failure, this element is not define') 16 | raise Exception(f'locating the element:{element} is failure, this element is not define') 17 | 18 | if not isinstance(el, dict): 19 | raise Exception(f'locating the element:{element} is failure, this element is not define') 20 | 21 | wait = WebDriverWait(driver, element_wait_timeout) 22 | 23 | if el['by'].lower() in ('title', 'url', 'current_url'): 24 | return None 25 | else: 26 | try: 27 | location = wait.until(EC.presence_of_element_located( 28 | (getattr(By, el['by'].upper()), value))) 29 | except: 30 | sleep(5) 31 | try: 32 | location = wait.until(EC.presence_of_element_located( 33 | (getattr(By, el['by'].upper()), value))) 34 | except : 35 | raise Exception(f'locating the element:{element} is failure: timeout') 36 | try: 37 | if driver.name in ('chrome', 'safari'): 38 | driver.execute_script( 39 | "arguments[0].scrollIntoViewIfNeeded(true)", location) 40 | else: 41 | driver.execute_script( 42 | "arguments[0].scrollIntoView(false)", location) 43 | except: 44 | pass 45 | 46 | try: 47 | if action == 'CLICK': 48 | location = wait.until(EC.element_to_be_clickable( 49 | (getattr(By, el['by'].upper()), value))) 50 | else: 51 | location = wait.until(EC.visibility_of_element_located( 52 | (getattr(By, el['by'].upper()), value))) 53 | except: 54 | pass 55 | 56 | return location 57 | 58 | 59 | # def locations(elements): 60 | # locations = {} 61 | # for el in elements: 62 | # locations[el] = location(el) 63 | # return locations 64 | 65 | 66 | # def locating_data(keys): 67 | # data_location = {} 68 | # for key in keys: 69 | # data_location[key] = location(key) 70 | # return data_location 71 | -------------------------------------------------------------------------------- /sweet/modules/web/window.py: -------------------------------------------------------------------------------- 1 | from sweet import log 2 | 3 | 4 | class Windows: 5 | 6 | def __init__(self): 7 | self.current_window = '' 8 | self.windows = {} 9 | self.frame = 0 10 | 11 | def tab(self, name): 12 | current_handle = self.driver.current_window_handle 13 | if name in self.windows: 14 | if current_handle != self.windows[name]: 15 | self.driver.switch_to_window(self.windows[name]) 16 | log.debug(f'switch the windows: #tab:{name}, handle:{repr(self.windows[name])}') 17 | else: 18 | log.debug(f'current windows: #tab:{name}, handle:{repr(self.windows[name])}') 19 | 20 | else: 21 | all_handles = self.driver.window_handles 22 | for handle in all_handles: 23 | if handle not in self.windows.values(): 24 | self.windows[name] = handle 25 | if handle != current_handle: 26 | self.driver.switch_to_window(handle) 27 | log.debug(f'switch the windows: #tab:{name}, handle:{repr(handle)}') 28 | else: 29 | log.debug(f'current windows: #tab:{name}, handle:{repr(current_handle)}') 30 | 31 | self.clear() 32 | 33 | def clear(self): # 关闭未命名的 windows 34 | current_handle = self.driver.current_window_handle 35 | current_name = '' 36 | for name in self.windows: 37 | if current_handle == self.windows[name]: 38 | current_name = name 39 | 40 | all_handles = self.driver.window_handles 41 | for handle in all_handles: 42 | # 未命名的 handle 43 | if handle not in self.windows.values(): 44 | # 切换到每一个窗口,并关闭它 45 | self.driver.switch_to_window(handle) 46 | log.debug(f'switch the windows: #tab:, handle:{repr(handle)}') 47 | self.driver.close() 48 | log.debug(f'close the windows: #tab:, handle:{repr(handle)}') 49 | self.driver.switch_to_window(current_handle) 50 | log.debug(f'switch the windows: #tab:{current_name}, handle:{repr(current_handle)}') 51 | 52 | 53 | def switch(self): 54 | """ 55 | docstring 56 | """ 57 | current_handle = self.driver.current_window_handle 58 | use_handles = list(self.windows.values()) + [self.driver.current_window_handle] 59 | all_handles = self.driver.window_handles 60 | for handle in all_handles: 61 | # 未命名的 handle 62 | if handle not in self.windows.values(): 63 | # 切换到新窗口 64 | self.driver.switch_to_window(handle) 65 | log.debug(f'switch the windows: #tab:, handle:{repr(handle)}') 66 | 67 | 68 | def switch_frame(self, frame): 69 | if frame.strip(): 70 | frame = [x.strip() for x in frame.split('|')] 71 | if frame != self.frame: 72 | if self.frame != 0: 73 | self.driver.switch_to.default_content() 74 | for f in frame: 75 | log.debug(f'frame value: {repr(f)}') 76 | if f.startswith('#'): 77 | f = int(f[1:]) 78 | elif '#' in f: 79 | from sweet.testcase import elements_format 80 | from sweet.modules.web.locator import locating_element 81 | element = elements_format('public', f)[2] 82 | f = locating_element(element) 83 | log.debug(f' switch frame: {repr(f)}') 84 | self.driver.switch_to.frame(f) 85 | self.frame = frame 86 | else: 87 | if self.frame != 0: 88 | self.driver.switch_to.default_content() 89 | self.frame = 0 90 | 91 | 92 | def close(self): 93 | all_handles = self.driver.window_handles 94 | for handle in all_handles: 95 | # 切换到每一个窗口,并关闭它 96 | self.driver.switch_to_window(handle) 97 | self.driver.close() 98 | log.debug(f'close th windows: {repr(handle)}') 99 | --------------------------------------------------------------------------------