• airtest+poco多脚本、多设备批处理运行测试用例自动生成测试报告


    一:主要内容

    • 框架功能、框架架构及测试报告效果
    • 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>
  • 相关阅读:
    庆祝一下开通了第一条博客!
    查看树莓派温度
    Ubuntu Server for Raspberry Pi部署Jenkins
    acme.sh部署RSA、ECC双证书(使用阿里云API)
    Ubuntu Server 20.04换用阿里源
    ESP32使用NTP同步时间
    C语言的可变参数函数
    一种Θ(1)的计算32位整数二进制中1的个数的方法
    CentOS 8安装Docker
    CentOS 8搭建LNMP + WordPress(三)
  • 原文地址:https://www.cnblogs.com/miaomiaokaixin/p/12809632.html
Copyright © 2020-2023  润新知