一:主要内容
- 框架功能、框架架构及测试报告效果
- airtest安装、环境搭建
- 框架搭建、框架运行说明
- 框架源码
二:框架功能及测试报告效果
1. 框架功能:
该框架笔者用来作为公司的项目的前端自动化,支持pc和app,本文的air脚本是针对app的,关于pc的脚本会专门在写一篇文章说明,该框架功能如下:
- 支持在安卓多台设备中批量运行所有后缀为air的测试脚本(因为ios的连接需要macOS,我是windows机所以暂时只连了安卓端的ios未做测试)
- 支持指定某个用例或某几个用例在某台设备或某几台设备中进行运行
- 支持控制测试用例执行顺序,默认会将登录用例排在第一,退出用例排在最后执行,如果想要自定义其他顺序,可以在run.py文件中修改sort_cases函数方法即可
- 支持多脚本多设备运行完成后,生成一份汇总的测试报告,且点击汇总测试报告中具体的某一个用例,还能查看该用例详细的airtest报告
2. 框架架构说明
3. 测试报告效果:
给大家看一下多设备、多脚本的测试报告效果:
点击详情效果:
三:airtest安装、环境搭建
1.python环境安装
这里不再赘述,安装并配置好环境变量后,执行python -V查看是否安装成功
2.airtestIDE安装
airtest安装很简单,安装airtestIDE,从官网下载:http://airtest.netease.com/
下载后解压缩到本地,我的本地位置为:G:AirtestIDE_2020-01-21_py3_win64AirtestIDE_2020-01-21_py3_win64AirtestIDE.exe,双击exe文件即为启动airtestIDE工具即可
3.包安装
需要安装如下包:
pip install airtest
pip install pocoui
如果执行不能安装成功,则可以使用如下命令:
pip install -i http://pypi.douban.com/simple --trusted-host pypi.douban.com airtest
pip install -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com pocoui
######如果想用airtest编写selenium即pc自动化脚本,则还需要安装如下包:
pip install selenium
pip install pynput
pip install airtest_selenium
关于这一步的安装也就是 pip install airtest_selenium,也可以从airtest安装目录下拷贝该文件夹到python目录下
我的python目录为:G:python3.6.5;
我的airtest安装目录为:G:AirtestIDE_2020-01-21_py3_win64AirtestIDE_2020-01-21_py3_win64,该路径下有个airtest_selenium文件夹;
可以拷贝airtest目录下的airtest_selenium文件夹到python目录下。
######如果想用airtest编写selenium即pc自动化脚本,除了安装上面的包,因为airtest-selenium自动化因为需要打开浏览器,所以我们还需要配置谷歌浏览器路径和下载匹配的谷歌驱动文件
- airtest设置谷歌启动路径:airtestIDE界面-点击选项-点击设置-点击chrome path-选择谷歌安装路径一直到chrome.exe文件
- 下载匹配的谷歌驱动文件:
可以使用该网站下载:https://npm.taobao.org/mirrors/chromedriver
下载后替换掉airtest根目录我的路径是G:AirtestIDE_2020-01-21_py3_win64AirtestIDE_2020-01-21_py3_win64下的chromedriver.exe文件即可
4.框架版本说明
该框架使用版本如下:
python 3.6.5
airtest 1.1.3
pocoui 1.0.79
pynput 1.6.8
airtestIDE 1.2.3
四:框架搭建、框架运行说明
1.框架搭建
该框架搭建很简单,就是一个python工程:
该工程根目录下开始时有一个result空文件夹、一个report_tpl.html模板文件、run.py启动脚本、docs文件夹是我自己放的一些项目描述文档可有可无,.air文件是自己通过airtestIDE编写的项目的自动化脚本
2.框架脚本文件说明
run.py #启动文件,python run.py即可
report_tpl.html #测试报告模板文件
report.html #自动生成的测试报告文件,会将汇总的执行结果的json数据即下面的summary数据格式与report_tpl.html结合,生成测试报告
result #文件夹,用于存放每个测试用例的执行json结果数据格式为下面的results数据格式
xxx.air #测试用例,所有以.air文件名称结尾的文件夹都是测试用例
xxx.air/log #每个测试用例的日志文件,以设备号区分,每个设备号下存放一份测试结果日志文件
log.html #每个测试用例在每个设备中运行的具体效果,即测试报告中点击具体测试用例右侧弹出的页面详情效果
log.txt #每个测试用例在每个设备中运行的json结果数据
3.框架运行编写建议
执行命令时可以用python run.py运行整个框架
但是写脚本或者调试脚本时,用airtestIDE来操作,即从airtestIDE中新建编辑.air脚本保存到该框架的根目录下,调试通过后再用run.py进行批量脚本、批量设备去执行。
这样就比较清晰
五:框架源码
1.run.py
1 # -*- encoding=utf-8 -*- 2 # Run Airtest in parallel on multi-device 3 import os 4 import traceback 5 import subprocess 6 import webbrowser 7 import time 8 import json 9 import shutil 10 from airtest.core.android.adb import ADB 11 from jinja2 import Environment, FileSystemLoader 12 13 14 def run(devices, airs): 15 """" 16 run_all 17 18 19 """ 20 try: 21 data_r=[] 22 global time_s 23 time_s = time.time() 24 for air in airs: 25 results = load_jdon_data(air) 26 tasks = run_on_multi_device(devices, air, results) 27 for task in tasks: 28 status = task['process'].wait() 29 results['tests'][task['dev']] = run_one_report(task['air'], task['dev']) 30 results['tests'][task['dev']]['status'] = status 31 name = air.split(".")[0] 32 json.dump(results, open(get_path("result")+os.sep+name+'_data.json', "w"), indent=4) 33 data_r.append(results) 34 run_summary(data_r) 35 except Exception as e: 36 traceback.print_exc() 37 38 39 def run_on_multi_device(devices, air, results): 40 """ 41 在多台设备上运行airtest脚本 42 Run airtest on multi-device 43 """ 44 tasks = [] 45 for dev in devices: 46 log_dir = get_path("log",dev,air) 47 #命令行执行:airtest run openOrder.air --device Android://127.0.0.1:5037/b7f0c036 --log F:airtest_codegood_store_projectlogopenOrder 48 cmd = [ 49 "airtest", 50 "run", 51 air, 52 "--device", 53 "Android:///" + dev, 54 "--log", 55 log_dir 56 ] 57 try: 58 tasks.append({ 59 'process': subprocess.Popen(cmd, cwd=os.getcwd()), 60 'dev': dev, 61 'air': air 62 }) 63 except Exception as e: 64 traceback.print_exc() 65 return tasks 66 67 #点击每个用例的详情页面 68 def run_one_report(air, dev): 69 """" 70 生成一个脚本的测试报告 71 Build one test report for one air script 72 """ 73 try: 74 log_dir = get_path("log",dev, air) 75 log = os.path.join(log_dir, 'log.txt') 76 if os.path.isfile(log): 77 #命令行执行:airtest report F:airtest_codegood_store_projectopenOrder.air --log_root F:airtest_codegood_store_projectlogopenOrder --outfile F:airtest_codegood_store_projectlogopenOrderopenOrder.html --lang zh 78 #如果是selenium,则最后要加上selenium插件 79 #airtest report F:airtest_codegood_store_projectopenOrder.air --log_root F:airtest_codegood_store_projectlogopenOrder --outfile F:airtest_codegood_store_projectlogopenOrderopenOrder.html --lang zh --plugins airtest_selenium.report 80 cmd = [ 81 "airtest", 82 "report", 83 air, 84 "--log_root", 85 log_dir, 86 "--outfile", 87 os.path.join(log_dir, 'log.html'), 88 "--lang", 89 "zh" 90 ] 91 ret = subprocess.call(cmd, shell=True, cwd=os.getcwd()) 92 return { 93 'status': ret, 94 'path': os.path.join(log_dir, 'log.html') 95 } 96 else: 97 print("Report build Failed. File not found in dir %s" % log) 98 except Exception as e: 99 traceback.print_exc() 100 return {'status': -1, 'device': dev, 'path': ''} 101 102 103 def run_summary(data): 104 """" 105 生成汇总的测试报告 106 Build sumary test report 107 """ 108 try: 109 for i in data: 110 c = get_json_value_by_key(i,"status") 111 112 summary = { 113 'time': "%.3f" % (time.time() - time_s), 114 'success': c.count(0), 115 'count': len(c) 116 } 117 summary['start_all'] = time.strftime("%Y-%m-%d %H:%M:%S", 118 time.localtime(time_s)) 119 summary["result"] = data 120 print("summary++++++++++",summary) 121 122 env = Environment(loader=FileSystemLoader(os.getcwd()), 123 trim_blocks=True) 124 html = env.get_template('report_tpl.html').render(data=summary) 125 with open("report.html", "w", encoding="utf-8") as f: 126 f.write(html) 127 webbrowser.open("report.html") 128 except Exception as e: 129 traceback.print_exc() 130 131 132 def load_jdon_data(air): 133 """" 134 加载进度 135 返回一个空的进度数据 136 """ 137 clear_log_dir(air) 138 return { 139 'start': time.time(), 140 'script': air, 141 'tests': {} 142 143 } 144 145 def clear_log_dir(air): 146 """" 147 清理log文件夹 openCard.air/log 148 Remove folder openCard.air/log 149 """ 150 log = os.path.join(os.getcwd(), air, 'log') 151 if os.path.exists(log): 152 shutil.rmtree(log) 153 154 #获取key为status的值 155 def get_json_value_by_key(in_json, target_key, results=[]): 156 for key,value in in_json.items(): # 循环获取key,value 157 if key == target_key: 158 results.append(value) 159 if isinstance(value, dict): 160 get_json_value_by_key(value,target_key) 161 return results 162 163 #获取路径 164 def get_path(content,device=None,air="openCard.air"): 165 root_path = os.getcwd() 166 path = os.getcwd() 167 if content=="result": 168 #返回测试报告路径 169 path = os.path.join(root_path,"result") 170 elif content == "log": 171 log_dir = os.path.join(root_path,air, 'log', device.replace(".", "_").replace(':', '_')) 172 #如果没有日志路径则创建一个 173 if not os.path.exists(log_dir): 174 os.makedirs(log_dir) 175 #返回日志路径 176 path = log_dir 177 elif content == "cases": 178 #返回测试用例路径 179 path = os.path.join(root_path,air) 180 else: 181 #返回根目录 182 path = root_path 183 return path 184 185 #获取路径下所有air的测试用例文件 186 def get_cases(path): 187 cases=[] 188 for name in os.listdir(get_path(path)): # 遍历当前路径下的文件夹和文件名称 189 if name.endswith(".air"): 190 cases.append(name) 191 return cases 192 193 def sort_cases(cases,loginAir,outAir): 194 #清除列表中的登录、退出登录,然后将其分别添加到列表的第一位和最后一位 195 cases.remove(loginAir) 196 cases.remove(outAir) 197 cases.insert(0, loginAir) 198 cases.insert(len(airs), outAir) 199 return cases 200 201 202 if __name__ == '__main__': 203 204 """ 205 初始化数据 206 Init variables here 207 """ 208 #获取所有已连接的设备列表 209 devices = [tmp[0] for tmp in ADB().devices()] 210 #设置指定设备执行测试用例 211 # devices = ["BTY4C16705003852","b7f0c036"] 212 #获取所有测试用例 213 airs = get_cases("root") 214 #将登录用例排在最前面执行,退出用例排在最后面执行 215 sort_airs = sort_cases(airs,"loginPro.air","loginOutPro.air") 216 #获取指定用例,按顺序执行 217 # sort_airs = ["openCardPro.air","openOrderPro.air","quickMoneyPro.air"] 218 """ 219 执行脚本 220 excute scripts 221 """ 222 # 运行所有脚本 223 run(devices, sort_airs)
2.report_tpl.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 5 <link rel="shortcut icon" type="image/png" href="http://airtest.netease.com/static/img/icon/favicon.ico"> 6 <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> 7 <meta name="viewport" content="width=device-width, initial-scale=1"> 8 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 9 <title>Airtest 多设备并行测试结果汇总</title> 10 </head> 11 <style type="text/css"> 12 *{ 13 margin: 0; 14 padding: 0; 15 } 16 body{ 17 background: #eeeeee 18 } 19 .container { 20 width: 75%; 21 min-width: 800px; 22 margin: auto 23 } 24 body.zh .en{ 25 display: none; 26 } 27 body.en .zh{ 28 display: none; 29 } 30 h1{ 31 margin-top: 50px; 32 text-align: center; 33 } 34 .center{ 35 text-align: center; 36 margin-top: 15px; 37 margin-bottom: 30px; 38 font-size: 14px; 39 position: relative; 40 } 41 .btn{ 42 border: solid 1px #c0c0c0; 43 padding: 5px 20px; 44 border-radius: 3px; 45 background: white; 46 cursor: context-menu; 47 } 48 .btn.lang:hover { 49 background: #5cb85c26; 50 border-color: #0a790a; 51 } 52 .btn.lang { 53 position: absolute; 54 top: 0; 55 } 56 .head { 57 margin: 20px 0 30px 0; 58 } 59 .head, .table{ 60 background: white; 61 border-radius: 5px; 62 box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); 63 padding: 30px 20px; 64 65 } 66 .head .progress{ 67 background: #dddddd; 68 color: white; 69 border-radius: 5px; 70 text-align: center; 71 margin-top: 12px; 72 } 73 .head .progress-bar-success{ 74 width: 0; 75 transition: all 0.5s ease; 76 background: #5cb85c; 77 border-radius: 5px; 78 } 79 .table-title { 80 text-align: center; 81 margin-bottom: 20px; 82 font-size: 18px; 83 font-weight: bold; 84 position: relative; 85 } 86 .table-row{ 87 border: solid 1px #e5e5e5; 88 margin-top: -1px; 89 cursor: context-menu; 90 } 91 .table-row:hover, .table-row.active{ 92 background: beige; 93 } 94 .table-head{ 95 background: aliceblue; 96 } 97 .table-head:hover{ 98 background: aliceblue; 99 } 100 .table-head .table-col{ 101 padding-top: 10px; 102 padding-bottom: 10px; 103 font-weight: bold; 104 text-align: center; 105 } 106 .table-col{ 107 display: inline-block; 108 width: 200px; 109 line-height: 30px; 110 padding: 5px 10px; 111 border-left: solid 1px #e5e5e5; 112 margin-top: -1px; 113 margin-right: -5px; 114 } 115 .table-col.short{ 116 width: 100px; 117 text-align: center; 118 } 119 .table-col.mid{ 120 width: 200px; 121 text-align: center; 122 } 123 .table-col:first-child{ 124 border: none; 125 } 126 .table-col.long{ 127 width: calc(100% - 700px); 128 } 129 .table-col.success{ 130 color: green; 131 } 132 .table-col.failed{ 133 color: red; 134 } 135 .detail{ 136 text-align: center; 137 font-size: 14px; 138 color: gray; 139 } 140 .iframe{ 141 position: fixed; 142 top: 0; 143 right: -100%; 144 width: 70%; 145 min-width: 800px; 146 height: 100%; 147 box-shadow: 0 5px 10px grey; 148 transition: right 0.5s ease; 149 background: white; 150 max-width: 1100px; 151 } 152 .iframe-tools{ 153 position: absolute; 154 top: 23px; 155 left: -34px; 156 background: white; 157 box-shadow: -2px 2px 5px grey; 158 border-radius: 7px; 159 } 160 .iframe-tools .close, .iframe-tools .open{ 161 width: 32px; 162 height: 50px; 163 color: gray; 164 cursor: context-menu; 165 display: block; 166 } 167 .iframe.show{ 168 right: 0; 169 } 170 iframe{ 171 width: 100%; 172 height: calc(100% - 70px); 173 border: none; 174 } 175 .iframe-head { 176 height: 60px; 177 line-height: 70px; 178 text-align: center; 179 border-bottom: solid 1px #ddd; 180 box-shadow: 2px 0 6px #999; 181 margin-bottom: 10px; 182 } 183 ::-webkit-scrollbar { 184 width: 10px; 185 height: 10px; 186 background-color: rgba(0,0,0,.34); 187 } 188 ::-webkit-scrollbar-thumb { 189 background-color: #8b8b8b; 190 border-radius: 10px; 191 } 192 ::-webkit-scrollbar-track { 193 background-color: #f5f5f5; 194 -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.22); 195 } 196 .iframe .close { 197 background: url('data:img/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgEAYAAAAj6qa3AAAABGdBTUEAALGPC/xhBQAAAAFzUkdC AK9OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dE AAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAA6JJREFUaN7tmc9LG0EUx99sE+xB/wBJ vQhCPLTojAr+Af6iKJSSVhD0EhNC1HgWxIt/gBA0MaQXqQoV2gb/Ag+Krs4KHlSMN38EFRGkIupm Xw/reEgIu9lsNi34vQTcie99P/NmZt8E4FWvKkpt6bZ0W/rdu0rnYVdektmBNEETNPHlS3Ytu5Zd Oz6mC3SBLgwMVNo4o4wy2tmp1qv1av3REVthK2xldNS2ANRN3dT99SvVqEa1pyfGGGMMkcpUprKq VgqEME436SbdvL8XebEAC7CAppkFQYyMwwM8wMP376SVtJJWlyt3HMYwhrFsFg7hEA6HhpRBZVAZ XFwst3GcwzmcS6VImIRJ+O3b/IHAgCFCB3RARyTCfdzHfdGoIQCxlkSpQwxiEKuqMkoMt3Ebt1UV kpCE5MCAElACSuDHD7uMN280bzRvdHcTiUhE+vWroPHcvJ4nCBuxERvfv9+t2a3ZrTk4EM/z9gC5 QW6QG05PyR7ZI3uitJ+ejAK9VEgTNEHT0pJdS0PMeLHGRQWQEAmR0OhorvGXvI3+T8tYy1jL2OfP uI7ruL68rP/V7TZL3urSMF3qBYwDBw48HOacc85jsULDDQE4DcIp40UDKDcIp41bBmA3CDJDZsjM 1ZXTxksG8JIHZZRRnw93cAd3lpYKHZd5IMSpIYMMsqqaNh6EIAQ1DeqgDuoCAd7De3jPt29W8y8Z gJDVijAtm2Y8V2/syu9863zrfOvgwOPz+Dy+/X04gRM4+fTpOYz1OGUybjsA20GU2biQ6WaoWGnV WrVW/ecPzuIszmazxX4f/ehHv6ZhBCMYub0tV5627QFCdIJO0ImuLuiDPuj7/dv05lYIRJl7DdsA 2G3cKRAlA7DapIjjTJR60cenTU2X5T1AzLjVJgXmYR7mR0akcWlcGu/v1x8633QVXQGWS91gV69U 02UaQLmM58ppEIYAnDJeKRCFr8QqZNxpEAWvxNRr9Vq9TqeLblLiEId4MMgVrnAlmbRqPI9riU0X 3uEd3n34kHszlPdqehY9i55Fb29re2t7a3svL4mXeIn340d9Rkl+xYgZX4VVWB0Z0Y0nEnYZF8pk MplMZn/fc+O58dyYf8UmLuIirqkppV1pV9p//sx7bhSYpmiKpoaHyQW5IBfxuH58SZJT7+qFZLg0 QhCC0OQk93M/909PlxxQgBC/B+j38KGQU4aNQOj5PD7qn5OTZQuoB/B6K238f8nrVf+6/gLOvYPg ZwC/JwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMy0wNlQyMDozMTo1NCswODowMMqAOUgAAAAl dEVYdGRhdGU6bW9kaWZ5ADIwMTktMDMtMDZUMjA6MzE6NTQrMDg6MDC73YH0AAAASHRFWHRzdmc6 YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9jOHk0dXZzNXd0 Zy9DbG9zZS5zdmfc199nAAAAAElFTkSuQmCC') no-repeat; 198 background-size: 20px; 199 background-position: center; 200 border-bottom: solid 2px #e5e5e5; 201 } 202 .iframe .open { 203 background: url("data:img/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgEAYAAAAj6qa3AAAABGdBTUEAALGPC/xhBQAAAAFzUkdC AK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dE AAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAABS5JREFUaN7tmGtIU28cx3+/s0tFGyEW SLfJ0F4Yk3YttNUY5prBoMsha1EUQoUZESW9KIXKYWiZQhR7UfYiRgmZkBaGLCuTaucsR6RiVxLp It1cLJjn/P4vxurf3785by3Rz5uxZ8/vec73s+fsec4Appjir0Qn08l0so0bDdcN1w3X1erxmoeJ d9DBwGqsxup586iYiqnY69WWa8u15SrVpBHwKwsXMipGxahu3TKyRtbIJiVNMgEAUAqlUJqaKr4Q X4gvGhuN3cZuY3di4uQR8AsajaAUlIKyoSEzMTMxM1GpnGQCANCKVrSaTCF3yB1y37yZXpZell42 c+akEfBDhAtd6MrIkG6Xbpdur61NqUqpSqmaNm3CCdDe197X3k9J0ev0Or2OZUkggYSsrJhF2NCG tlWrZvXO6p3V6/GspJW0kqTSoeqG7DDWGBIMCYaEzExSk5rUTicchINw0G6HAiiAguRkQEBAAKzE SqwcwQT1UA/1a9cGpUFpUFpdHWncujXyKooDxI1XUJZlWZaVSF6GXoZehjZvph20g3YcOgQlUAIl aWl/QjYAANRADdS43ZyaU3PqXbsijUTjJkC3RbdFt8VsBic4wXn2LB7Gw3h48eI/FngQyEY2slVU 8C7exbv27x8zAZF7Viaj1bSaVp84gb3Yi7379gEHHHA4bitsdBw7xnEcx3FFRSO+QI1Go9FoEhLk drldbq+rgyZogiazOd7RYoVUpCJVYeGwd4Eld5bcWXJnzhy5XC6Xy5ubJ1rwKLgMl+Gy5OSYBaRd SbuSdkWhYD4xn5hP9fWRVo0m3kFGxsWLXCFXyBUWFMQsYPrS6UunLz1zBo/iUTxqNMY7wrBZA2tg TW2twqfwKXx5eZFGURzyHGDYa9hr2Lt+Pa2jdbQuup9ONBobv8z+MvvL7E2bOOSQw/7+6CeSwUpM XaYuU9f8+aIgCqJw4wY8gAfwYMaMeEcZHl6v7JrsmuyawxHIDmQHsr9//2+PQVdAv7xf3i8vKcEq rMKqhIRRX4se9KAnimyPb95AGZRBWVcX8cQT//o1etCDnmCQLtAFuhAKQRIkQZJGg3a0oz0nJ+Z5 jsNxON7aGuoL9YX6HA5uAbeAWxAKDdZ9gIDIc/aiRUKP0CP0OJ3DzUnN1EzNT55AMRRDcUMDetGL 3qamcGo4NZza0hJwB9wB97dvYAUrWP9ngHRIh3QA/SX9Jf2l6IFlaAF0m27Tbb9fUAgKQZGT8xSf 4lMMBoeqGyBAPC+eF88XFWEd1mGdRPL78nCYrtJVuurxkItc5Dp3zq/wK/yK1tYBXXnggR+uzhgw gxnM7e2iX/SLfputbUXbirYVnz/HWv5DgDZDm6HNmDs3cqLLzY0IGKwsug0eOMCreBWv6ugYh2i/ h4CAnj8XH4oPxYdZWY9PPz79+PSHD8Md5ocAJp/JZ/Jzc2E37Ibd//rm8yEf8oNB2kk7aWdeHh/m w3z48uU/Hjiau5zKqby7W7AIFsGSldWGbdiGPT0jHe/nOeAUnIJTP+95ukt36W5np8iKrMiaTHEP XkEVVPHuHXOPucfciwZ/9Wq04zLRH73IW50usrQ6OiTbJNsk2ywWv9Kv9Cvb2+MWvJIqqfLjR9gA G2BDdrbviO+I70hn51iNLxUcgkNwWCy4HJfj8mfPoAVaoMVqfVTzqOZRzdu38QoOJ+EknPz6lelj +pg+u9333vfe9z4QGPN59KX6Un2p2x15rP3bzvbM+P9lp9uj26PbY7WOfqQpppiI/AOjmiKrfUvK NAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMy0wNlQyMDozMTo1NCswODowMMqAOUgAAAAldEVY dGRhdGU6bW9kaWZ5ADIwMTktMDMtMDZUMjA6MzE6NTQrMDg6MDC73YH0AAAASHRFWHRzdmc6YmFz ZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9jOHk0dXZzNXd0Zy9z aGFyZS5zdmftz7m3AAAAAElFTkSuQmCC") no-repeat; 204 background-size: 20px; 205 background-position: center; 206 } 207 select{height: 28px; line-height: auto; vertical-align: middle; height: 22px9; padding: 3px 09; box-sizing:content-box; font-size: 13px;} 208 :root select{padding: 0; height: 28px;} 209 </style> 210 <body class="zh"> 211 <div class="container-fluid" > 212 <div class="container"> 213 <div class="main"> 214 <div class="material"> 215 <h1>汇总报告</h1> 216 <div class="center"> 217 <div class="btn lang">Switch to English version</div> 218 <div class="time zh">开始时间:{{data['start_all']}},耗时 <b>{{data['time']}}</b> 秒</div> 219 <div class="time en">Started at:{{data['start_all']}},cost <b>{{data['time']}}</b> s</div> 220 </div> 221 <div class="head"> 222 <header class="zh"><span class="rate"></span>成功率:</span> {{data["success"]}}/{{data["count"]}}</header> 223 <header class="en"><span class="rate">Success rate:</span> {{data["success"]}}/{{data["count"]}}</header> 224 <div> 225 <div class="progress"> 226 <div class="progress-bar progress-bar-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" style=" {{data['success'] *100 / data['count']}}%"> 227 <span class="">{{'%0.2f' % (data["success"] *100 / data["count"])}}%</span> 228 </div> 229 </div> 230 </div> 231 </div> 232 <select name="" id="exit" style=" 100px;"> 233 <option class="zh" value="all">全部</option> 234 <option class="en" value="all">all</option> 235 <option class="en" value="成功">success</option> 236 <option class="zh" value="成功">成功</option> 237 <option class="en" value="失败">failed</option> 238 <option class="zh" value="失败">失败</option> 239 </select> 240 <div class="table" > 241 <div class="table-title"> 242 <span class="running_detail zh">用例列表</span> 243 <span class="running_detail en">Detail</span> 244 </div> 245 <div class="table-content" id="tab"> 246 <div class="table-row table-head"> 247 <div class="table-col short zh">序号</div> 248 <div class="table-col short zh">状态</div> 249 <div class="table-col mid zh">用例</div> 250 <div class="table-col long zh">设备</div> 251 <div class="table-col short en">id</div> 252 <div class="table-col short en">result</div> 253 <div class="table-col mid en">case</div> 254 <div class="table-col long en">device</div> 255 <div class="table-col ">--</div> 256 </div> 257 {% set ns = namespace(found=0) %} 258 {% for dat in data['result'] %} 259 {% for dev, item in dat['tests'].items() %} 260 <div class="table-row" path="{{item['path']}}" > 261 {% set ns.found = ns.found + 1 %} 262 <div class="table-col short">{{ns.found}}</div> 263 <div class="table-col short zh {{'success' if item['status']==0 else 'failed'}}">{{"成功" if item['status']==0 else "失败"}}</div> 264 <div class="table-col short en {{'success' if item['status']==0 else 'failed'}}">{{"success" if item['status']==0 else "failed"}}</div> 265 <div class="table-col mid">{{dat['script']}}</div> 266 <div class="table-col long">{{dev}}</div> 267 268 <div class="table-col detail zh">点击可查看详情</div> 269 <div class="table-col detail en">click to see detail</div> 270 </div> 271 {% endfor %} 272 {% endfor %} 273 </div> 274 </div> 275 </div> 276 </div> 277 278 <div class="iframe"> 279 <div class="iframe-head"></div> 280 <iframe src='.'></iframe> 281 <div class="iframe-tools"> 282 <div class="close"></div> 283 <a class="open" href='.' target='_blank'></a> 284 </div> 285 </div> 286 </div> 287 </body> 288 <script type="text/javascript"> 289 var Lang = 'zh' // or en 290 var rows = document.querySelectorAll('.table-row') 291 var iframe = document.querySelector('.iframe') 292 var iframeHead = document.querySelector('.iframe-head') 293 var open = document.querySelector('.open') 294 var close = document.querySelector('.iframe .close') 295 var langBtn = document.querySelector('.lang') 296 var body = document.body 297 var prevActiveRow = null 298 function init() { 299 for(i=0; i<rows.length; i++){ 300 addEvent(rows[i], 'click', function(e){ 301 path = this.getAttribute('path') 302 console.log(this) 303 if(path) { 304 showIframe(this) 305 } 306 }) 307 } 308 addEvent(close, 'click', function(e){ 309 iframe.className='iframe' 310 }) 311 addEvent(langBtn, 'click', function(e){ 312 if(Lang == 'zh'){ 313 Lang = 'en'; 314 this.innerText = '切换到中文版' 315 } else { 316 Lang = 'zh' 317 this.innerText = "Switch to English version" 318 } 319 document.body.className = Lang 320 if (iframe.className.indexOf('show')>=0) { 321 showIframe(prevActiveRow) 322 } 323 }) 324 document.body.className = Lang 325 } 326 function showIframe(obj){ 327 var num = obj.querySelector('.table-col.short').innerText 328 var device = obj.querySelector('.table-col.long').innerText 329 if(Lang =='en') { 330 num = ordinal_suffix_of(num) 331 iframeHead.innerHTML = "Test report running in the " + num + ' device "' + device + '"' 332 open.setAttribute('title', 'open in a new tab') 333 close.setAttribute('title', 'close') 334 } 335 else { 336 iframeHead.innerHTML = "第 " + num + " 台设备 【" + device + "】 的测试报告" 337 open.setAttribute('title', '在新标签页打开') 338 close.setAttribute('title', '关闭') 339 } 340 iframe.querySelector('iframe').setAttribute('src', path) 341 open.setAttribute('href', path) 342 iframe.className='iframe show' 343 if(prevActiveRow){ 344 prevActiveRow.className = "table-row" 345 } 346 obj.className = 'table-row active' 347 prevActiveRow = obj 348 } 349 function ordinal_suffix_of(i) { 350 i = Number(i) 351 var j = i % 10, 352 k = i % 100; 353 if (j == 1 && k != 11) { 354 return i + "st"; 355 } 356 if (j == 2 && k != 12) { 357 return i + "nd"; 358 } 359 if (j == 3 && k != 13) { 360 return i + "rd"; 361 } 362 return i + "th"; 363 } 364 function addEvent(obj,type,handle) { 365 try{// Chrome、FireFox、Opera、Safari、IE9.0 and above 366 obj.addEventListener(type,handle); 367 }catch(e){ 368 try{// IE8.0 and below 369 obj.attachEvent('on'+ type,handle); 370 }catch(e){// Browser in earlier vesion 371 obj['on'+ type]= handle; 372 } 373 } 374 } 375 init() 376 $(document).ready(function(){ 377 $('#exit').change(function(){ // 下拉框绑定change事件 378 var exit_code = $(this).children('option:selected').val(); // 获取下拉框选中值 379 $('#tab .table-row').each(function() { 380 var self = $(this).children().eq(1).text(); // 获取每行第二列的值 381 if(exit_code=='all'){ // 选中all时,数据全部显示 382 $(this).show(); 383 }else{ // 选中其他的值时,进一步判断 384 if(self!=exit_code){ // 列中的值和选中值不一致 385 $(this).hide(); // 该行不显示 386 $('#tab .table-head').show() 387 }else{ 388 $(this).show(); 389 } 390 } 391 }); 392 }) 393 }) 394 </script> 395 </html>