├── .gitignore ├── README.md ├── README_en.md ├── api-test └── __init__.py └── ui-test ├── README.md ├── __init__.py ├── base ├── __init__.py └── assembler.py ├── case ├── __init__.py ├── test_baidu_case.py ├── test_csdn_case.py └── test_other_case.py ├── common ├── __init__.py ├── browser_common.py └── page_common.py ├── data ├── __init__.py ├── baidu_main_data.py ├── baidu_result_data.py └── csdn_data.py ├── locator ├── __init__.py ├── baidu_main_locator.py ├── baidu_result_locator.py └── csdn_locator.py ├── page ├── __init__.py ├── baidu_main_page.py ├── baidu_result_page.py └── csdn_page.py ├── report ├── html │ └── UI测试报告.html ├── img │ └── force_test_1_TestCsdnCase.png └── log │ └── ui_log.log ├── requirements.txt ├── resource ├── __init__.py ├── config │ ├── __init__.py │ └── config.ini └── driver │ ├── IEDriverServer.exe │ ├── SafariDriver.safariextz │ ├── __init__.py │ ├── chromedriver.exe │ ├── geckodriver.exe │ ├── msedgedriver.exe │ └── operadriver.exe ├── suite ├── run_all.py └── run_all_mutithread.py └── util ├── __init__.py ├── config_reader.py ├── log_tool.py ├── mysql_tool.py ├── redis_pool.py ├── report_tool.py ├── screenshot_tool.py ├── text_tool.py └── thread_local_storage.py /.gitignore: -------------------------------------------------------------------------------- 1 | # 版本控制文件 2 | .idea/ 3 | 4 | # 虚拟环境文件夹 5 | venv/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[English](https://github.com/abcnull/python-ui-auto-test/blob/master/README_en.md) | [博客](https://blog.csdn.net/abcnull/article/details/103379143)** 2 | 3 | [TOC] 4 | # python-ui-auto-test 5 | 6 | python + selenium + unittest + PO + BeautifulReport + redis + mysql + ParamUnittest + 多线程 + 截图/日志 + 多浏览器支持 + RemoteWebDriver +文件读取 + 全参数化构建 7 | 8 | 欢迎大家 **Watch**,**Star** 和 **Fork**! 9 | 10 | - 框架作者:**abcnull** 11 | - csdn 博客:**https://blog.csdn.net/abcnull** 12 | - github:**https://github.com/abcnull** 13 | - e-mail:**abcnull@qq.com** 14 | 15 | ## 框架结构 16 | 17 | ``` 18 | python-ui-auto-test 19 | - api-test(api 测试包,未添加内容) 20 | - ui-test(ui 测试包) 21 | - base(与项目初始化配置相关) 22 | - case(测试用例脚本) 23 | - common(共用方法) 24 | - data(数据驱动) 25 | - locator(页面对应的元素定位) 26 | - page(页面类) 27 | - report(输出报告) 28 | - html(html 类型报告) 29 | - log(log 日志报告) 30 | - img(测试截图) 31 | - resource(资源文件夹) 32 | - config(配置文件) 33 | - driver(驱动) 34 | - util(工具类) 35 | - README.md(项目介绍 md) 36 | - requirements.txt(项目依赖清单) 37 | - run_all.py(类似于 testng 文件执行测试套,单线程) 38 | - run_all_mutithread.py(类似于 testng 文件执行测试套,多线程) 39 | - venv(虚拟环境的文件夹,github 拉下来后需要自己创虚拟环境) 40 | - .gitignore(git 忽略文件) 41 | External Libraries 42 | Scratches and Consoles 43 | ``` 44 | 45 | ![框架结构](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575304456217.png) 46 | 47 | 48 | ## 框架概述 49 | 50 | - 采用 PO 思想,将 PageObject 更细致化,使得页面的元素定位和页面对应的数据分别放在 locator 和 data 中,case 中存放测试用例 51 | 52 | - common 中存放了 PageCommon 和 BrowserCommon,分别是封装了页面操作和浏览器操作的代码 53 | 54 | - base 中存放的是初始化数据库和驱动的 Assembler 装配器,用来在测试用例之初创建初始化配置。其`__init__(self)`方法源码如下所示: 55 | 56 | ```python 57 | # 初始化所有工具 58 | def __init__(self): 59 | # 驱动装配 60 | self.assemble_driver() 61 | # redis 工具装配 62 | if ConfigReader().read("project")["redis_enable"].upper() == "Y": 63 | # 装配 redis 连接池工具 64 | self.assemble_redis() 65 | elif ConfigReader().read("project")["redis_enable"].upper() == "N": 66 | self.redis_pool_tool = None 67 | else: 68 | self.redis_pool_tool = None 69 | raise RuntimeError("配置文件中配置 redis 是否启用字段有误!请修改!") 70 | # mysql 工具装配 71 | if ConfigReader().read("project")["mysql_enable"].upper() == "Y": 72 | # 装配 mysql 工具 73 | self.assemble_mysql() 74 | elif ConfigReader().read("project")["mysql_enable"].upper() == "N": 75 | self.mysql_tool = None 76 | else: 77 | self.mysql_tool = None 78 | raise RuntimeError("配置文件中配置 mysql 是否启用字段有误!请修改!") 79 | # 将装配器保存到线程存储器中,键名为线程,键值为装配器对象 80 | ThreadLocalStorage.set(threading.current_thread(), self) 81 | ``` 82 | 83 | - report 中分别存放了 html 测试报告,log 日志信息和 img 截图。测试报告采用 BeautifulReport 模板,log 日志的输出可以通过 util/config/config.ini 进行全套参数化配置,截图这块受于 BeautifulReport 的限制本来是存放于项目下 img 中,但目前通过`../`的方式还是将截图放在这里。对于火狐驱动会产生 log 日志在 case 用例中,目前我也通过`../`将火狐的日志移动此 log 文件夹中。html 和 截图可以配置选择重名是否被覆盖,log 直接配置成最大轮转数为 5,重名是否被覆盖,拿 html 举例,可以判断重名获取新的报告名,采用递归思想,源码如下所示: 84 | 85 | ```python 86 | # 递归方法 87 | # 判断报告的名字在配置文件指定路径下是否有重复,并根据配置是否允许重复返回报告新的名字 88 | def get_html_name(self, filename: str = None, report_dir="."): 89 | """ 90 | 获取新的报告名 91 | :param filename: 报告名称 92 | :param report_dir: 报告路径 93 | :return: 通过配置文件判断报告是否可以被覆盖,返回新的报告名 94 | """ 95 | # 若允许被覆盖同名报告 96 | if ConfigReader().read("html")["cover_allowed"].upper() == "Y": 97 | # 返回报告名 98 | return filename 99 | # 若不支持覆盖同名报告 100 | elif ConfigReader().read("html")["cover_allowed"].upper() == "N": 101 | # 判断报告路径是否存在,若存在 102 | if os.path.exists(report_dir + filename + ".html"): 103 | # 如果名字不以 ")" 结尾 104 | if not filename.endswith(")"): 105 | filename = filename + "(2)" 106 | # 如果名字以 ")" 结尾 107 | else: 108 | file_num = filename[filename.index("(") + 1: -1] 109 | num = int(file_num) 110 | # 报告名称字段自增 111 | num += 1 112 | filename = filename[:filename.index("(")] + "(" + str(num) + ")" 113 | # 若报告不存在 114 | else: 115 | # 递归出口,运行测试并产出报告存放 116 | return filename 117 | # 递归:不断改变 filename 后报告是否还能找到 118 | return self.get_html_name(filename, report_dir) 119 | # 若配置中既不是 Y/y 也不是 N/n 就抛出异常 120 | else: 121 | raise RuntimeError("config.ini中[html]的cover_allowed字段配置错误,请检查!") 122 | ``` 123 | 124 | - 资源文件夹中 config 实现了项目完全依靠参数来配置运行,driver 文件夹中目前存放了所有主流驱动,版本信息请见 config.ini 中有介绍 125 | 126 | ![框架概述](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575305550583.png) 127 | 128 | - 工具类中包括配置文件读取器,log 日志工具,mysql 连接工具,redis 连接池工具,报告生成工具,截图工具,文本工具,线程本地存储工具 129 | - `run_all.py`可以单线程运行所有测试用例,`run_all_mutithread`则可以多线程运行测试用例,但是要注意的是多线程运行后测试报告还是无法汇总成一个报告,这个得对 BeautifulReport 进行二次开发才可以解决 130 | 131 | ## 层次结构思想 132 | 133 | 框架采用火热的 PageObject(PO)思想使得结构更加清晰,对于逻辑性代码可以在 page 文件夹中维护,业务流程组织可以在 case 中维护,data 数据改变或者 locator 元素定位改变也可以快速跟踪修改,base 中存放初始化的工具,common 存放共用方法。至于用例如何组织来运行,可见下面几种方式 134 | 135 | - case 文件中的一个测试类中分有多个 test 打头的方法,这些方法通过`setUp()`方法来初始化装配器(装配器中含有一个驱动),也就是一个 test 打头的方法为一个测试点,每开一个测试点相当于使用了一个新的装配器,其中含有驱动。测试类中一个 test 打头的方法可以是一个完整流程 136 | - 如若不希望写`setUp()`,自己可以专门写个方法放在某一个 test 打头的方法里头也行,这样不会每个 test 都跑这个方法了 137 | - 框架中有个非常好用的功能,比如测试电商系统类似淘宝,天猫的购物网站系统,分为商品页面,购物车页,结算页等,当我们分别按页面写好 page 之后,并且 case 中分别测试的是各个页面的功能点,如果我们需要把几个 case 串联起来,该如何去做呢,由于装配器中创建驱动之后会把驱动和对应的线程号保存到静态字典中,我们直接在另一个 case 中依据当前线程去取驱动即可接着上一个 case 跑了 138 | - 用例的其他组织方式,由于存在 ParamUnittest 工具,把 Assembler 装配器(内含驱动)专门放在测试类上的字典参数中,这样该类下每个测试方法都可以直接取用,这种方式也是可以的,也能组织跑一个类中完整的流程,还有多种方式,由于含有 ParamUnittest 外不传参,线程本地存储,config.ini 全参数化构建,用例的组织非常灵活,可以根据项目具体需求稍微加以修改增加功能 139 | 140 | ## Assembler 装配器 141 | 142 | 有几大点功能: 143 | 144 | - 各种类型驱动初始化 145 | - redis 装配 146 | - mysql 装配 147 | - 将该装配器对象和本地线程保存到一个静态字典中,方便在其他 case 中对驱动的取用 148 | 149 | ## ParamUnittest 外部传参 150 | 151 | 由于待测的环境可能是 SIT 或是 UAT 或是 PROD,所以可以通过 config.ini 修改参数的方式来往 case 中的类上参数中传值,进而传进类中的测试方法中。小伙伴们也可以考虑下二次开发直接在`run_all.py`中把参数传进具体的 case 里。还有一个参数是语言参数,对于多语言环境,可以修改此参数进而可以选择 data 中的指定数据。小伙伴们当然也可以根据项目需求自己增加自己需要的参数 152 | 153 | ![ParamUnittest 外部传参](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575369275406.png) 154 | 155 | ## config.ini 项目配置 156 | 157 | 其中包含 158 | 159 | - [project] 项目配置,如自定义的参数,哪种驱动,远程服务器 ip,redis/mysql 是否启用等参数 160 | - [driver] 几乎所有主流浏览器驱动路径 161 | - [redis] redis 各项配置 162 | - [mysql] mysql 各项配置 163 | - [screenshot] 截图路径以及截图格式和截图是否支持覆盖 164 | - [html] 包含报告名,报告存放路径以及报告是否支持覆盖 165 | - [log] 这个 log 日志的配置比较多,可以详细查看该文件 166 | 167 | 后续小伙伴可以添加 oracle,sqlserver,mongoDB 等的配置参数进去,记得同时要给 Assembler 装配器增添代码 168 | 169 | ![config.ini 项目配置](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575369314016.png) 170 | 171 | ## 工具类 172 | 173 | - ConfigReader 项目参数化构建的配置文件读取器 174 | 175 | - LogTool log 日志工具类 176 | 177 | - MysqlTool mysql 连接工具可以返回一个连接成功的 mysql 连接 178 | 179 | - RedisPool redis 连接池工具,其中包含连接池对象和连接池中的一个连接 180 | 181 | - ReportTool 报告生成工具,对 BeautifulReport 封装了一层外壳,可以依据配置文件进行同名文件覆盖或不覆盖 182 | 183 | - ScreenshotTool 截图工具 184 | 185 | - TextTool 文字工具,简单的生成一段文字信息 186 | 187 | - ThreadLocalStorage 用于将线程号和 Assembler 装配器通过键值对形式存进一个静态字典中,方便在不同 case 中取用装配器中的驱动 188 | 189 | ![工具类](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575369333647.png) 190 | 191 | ## 写在后头 192 | 193 | 项目仍有许多值得修改优化的地方,望 commit 宝贵意见,更好完善框架内容! 194 | 再次感谢! 195 | 196 | - 框架作者:**abcnull** 197 | - csdn 博客:**https://blog.csdn.net/abcnull** 198 | - github:**https://github.com/abcnull** 199 | - e-mail:**abcnull@qq.com** 200 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | **[中文](https://github.com/abcnull/python-ui-auto-test/blob/master/README.md) | [Blog](https://blog.csdn.net/abcnull/article/details/103379143)** 2 | 3 | [TOC] 4 | # python-ui-auto-test 5 | 6 | python + selenium + unittest + PageObject + BeautifulReport + redis + mysql + ParamUnittest + tomorrow + ThreadLocal + screenshot + log + multiple browser drivers + RemoteWebDriver + .ini file reader + configurable projects 7 | 8 | Thanks to Pengfei Li for his technical support during the construction process! If you have any further questions, please contact us through the following channels 9 | 10 | Welcome to **Watch**, **Star** and **Fork**! 11 | 12 | - Author: **abcnull** 13 | - Csdn Blog: **https://blog.csdn.net/abcnull** 14 | - GitHub: **https://github.com/abcnull** 15 | - E-Mail: **abcnull@qq.com** 16 | 17 | ## Hierarchy 18 | 19 | ``` 20 | python-ui-auto-test 21 | - api-test(api test package, to be continue...) 22 | - ui-test(ui test package) 23 | - base(related to the project initialization configuration) 24 | - case(test case) 25 | - common(common package) 26 | - data(data driving) 27 | - locator(element locator) 28 | - page(page object) 29 | - report(report package) 30 | - html(html report) 31 | - log(log report) 32 | - img(test screenshot) 33 | - resource(resouece package) 34 | - config(configuration of peoject) 35 | - driver(drivers) 36 | - util(utility package) 37 | - README.md(project brief) 38 | - requirements.txt(dependencies list) 39 | - run_all.py(runnable test suite, single thread) 40 | - run_all_mutithread.py(runnable test suite, muti thread) 41 | - venv(virtual environment) 42 | - .gitignore(git ignore list) 43 | External Libraries 44 | Scratches and Consoles 45 | ``` 46 | 47 | ![framework](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575304456217.png) 48 | 49 | 50 | ## Framework Overview 51 | 52 | - Using PO structure, and making it more detailed, i put element locator in locator package and put data-driven data in data package, case holding test case 53 | 54 | - Common contains PageCommon class and BrowserCommon class, and they are code that encapsulates page operations and browser operations, respectively 55 | 56 | - Base contains Assembler initial tool which is to initial database and driver, and others before the test suite runs. `__init__(self)`function source is as follows: 57 | 58 | ```python 59 | # init 60 | def __init__(self): 61 | # assemble driver 62 | self.assemble_driver() 63 | # assemble redis 64 | if ConfigReader().read("project")["redis_enable"].upper() == "Y": 65 | self.assemble_redis() 66 | elif ConfigReader().read("project")["redis_enable"].upper() == "N": 67 | self.redis_pool_tool = None 68 | else: 69 | self.redis_pool_tool = None 70 | raise RuntimeError("配置文件中配置 redis 是否启用字段有误!请修改!") 71 | # assemble mysql 72 | if ConfigReader().read("project")["mysql_enable"].upper() == "Y": 73 | self.assemble_mysql() 74 | elif ConfigReader().read("project")["mysql_enable"].upper() == "N": 75 | self.mysql_tool = None 76 | else: 77 | self.mysql_tool = None 78 | raise RuntimeError("配置文件中配置 mysql 是否启用字段有误!请修改!") 79 | # store thread and data in ThreadLocalStorage 80 | ThreadLocalStorage.set(threading.current_thread(), self) 81 | ``` 82 | 83 | - Report package contains html report, log report and test screenshot, respectively. Test report use BeautifulReport dependency. The generation of log report can be controlled by util/config/config.ini. Because of the flaw of the BeautifulReport, screenshot is supposed to be in project/img package, i want the screenshot in resource/img for i was used to java project structure. Up to now, i still use `../` to stitch path as `@BeautifulReport`. For the peculiarity of Firefox, which can generate a log report by himself in case package, i aslo use `../` to make the log move to resource/log. Html report and screenshot can be configured to enable or disable. Log have been configured that maximum rotation is 5 and repetition switch control file name repetition. Taking html for example, we can decide the report use a new name or the repective name just according to a Configuration. The function use a recursive method as follows: 84 | 85 | ```python 86 | # recursion 87 | # get a new name controlled by configuration 88 | def get_html_name(self, filename: str = None, report_dir="."): 89 | """ 90 | get new report name 91 | :param filename: old report name 92 | :param report_dir: report path 93 | :return: a new name controlled by name repetition enable switch in configuration file 94 | """ 95 | # if allow name repetition 96 | if ConfigReader().read("html")["cover_allowed"].upper() == "Y": 97 | # get new report name 98 | return filename 99 | # else if not allow name repetition 100 | elif ConfigReader().read("html")["cover_allowed"].upper() == "N": 101 | # if path right 102 | if os.path.exists(report_dir + filename + ".html"): 103 | # if the report name not end with ")" 104 | if not filename.endswith(")"): 105 | filename = filename + "(2)" 106 | # else the reprot name end with ")" 107 | else: 108 | file_num = filename[filename.index("(") + 1: -1] 109 | num = int(file_num) 110 | # num ++ 111 | num += 1 112 | filename = filename[:filename.index("(")] + "(" + str(num) + ")" 113 | # else name not exist 114 | else: 115 | # recursive export 116 | return filename 117 | # recursive: change filename constantly 118 | return self.get_html_name(filename, report_dir) 119 | # else it neither Y/y nor N/n, throw error 120 | else: 121 | raise RuntimeError("config.ini中[html]的cover_allowed字段配置错误,请检查!") 122 | ``` 123 | 124 | - In resource package, config realized that driver package contains all mainstream drivers, and all version info are in config.ini file 125 | 126 | ![框架概述](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575305550583.png) 127 | 128 | - Utiltiy package ccontains .ini file reader, log tool, mysql tool, redis tool, report generator, screenshot tool, text tool, local storage tool 129 | 130 | - `run_all.py`can run suite through a single thread, `run_all_mutithread` can run suite through muti thread. What should be paid attention to is when we run suite through muti thread, we can not get one report per thread. What can we do to solve this problem? Redevelop BeautifulReport can solve this 131 | 132 | ## Running Process 133 | 134 | Project use mainstream idea - PageObject. Logical code can be amended in page package. Business flow can be amended in case package. The changes of data or element locator can be quickly tracked and modified. Base Package contains initial tool. Common package contains common funtions. How do the test case run? Let's see following explanations: 135 | 136 | - Case package contains test class is made up of functions which starts with a word - test. These functions create an object of Assembler through `setUp()` function which contains a driver instance. When a test function run, it can create a new object of Assembler containing driver instance. A test function is a whole test process 137 | - If you don't want to write `setUp()`, you can write another function, and then you can call the function you just write in the first line of some functions that you need test, so that this test functions can run after what you write before 138 | - This fromework has a very nice feature. For example, like Taobao, Tianmao and other ebusiness system, they can be devided into good detail page, cart page, settlment page and others, so when we have created these page object, and we have written them in corresponding case, and we want to String a series cases as a whole process, we can take out the driver and other data in current thread.In Assembler's initialize funtion, we put the data in a static dictionary which storages the thread and data in the form of key-value pairs. 139 | - These test case alse have other organization form because of ParamUnittest dependency. If we move Assembler object in the outside params of ParamUnittest, the test functions below all can use the data in Assembler object. This way is also OK and there are a lot of ways. ParamUnittest, local storage, config.ini make case orgnization very flexible. How you organize test cases can accord to the requirment of your project and you can also add some idea in this framework depend on your need 140 | 141 | ## Assembler 142 | 143 | The features: 144 | 145 | - Initialization of mainstream drivers 146 | - Assembling redis 147 | - Assembling mysql 148 | - Storing all data and thread in a static dictionary 149 | 150 | ## ParamUnittest 151 | 152 | Environment could be SIT, UAT or PROD, so we can modify parameters in config.ini to control the running way of the suites and cases. You guys can also consider to write the parameters in`run_all.py` for the same purpose. There is another parameter besides enviroment parameter - language. It means that you can select specified language and to control data package using which data, if you amend code in data package a little bit. Except these parameters, you can also add other parameters unused 153 | 154 | ![ParamUnittest](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575369275406.png) 155 | 156 | ## config.ini 157 | 158 | Contains: 159 | 160 | - [project] project configuration, like customized parameters, some kinds of mainstream drivers, remote server ip, redis/mysql enable switch etc. 161 | - [driver] almost all of mainstream browsers’ drivers 162 | - [redis] redis configuration 163 | - [mysql] mysql configuration 164 | - [screenshot] screenshot path and screenshot format and name repetition enable switch 165 | - [html] html report name configuration, report path and name repetition enable switch 166 | - [log] there are lots of configurations here 167 | 168 | Follew-up user can add oracle, sqlserver, mongoDB and other configuration in .ini file to make theproject perfect.Don't forget to add code in Assembler! 169 | 170 | ![config.ini](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575369314016.png) 171 | 172 | ## Utility 173 | 174 | - ConfigReader: .ini file reader 175 | 176 | - LogTool: log generator 177 | 178 | - MysqlTool: mysql generator 179 | 180 | - RedisPool: redis pool generator 181 | 182 | - ReportTool: html report generator, just add a shell to BeautifulReport in order to control the generative process by configuration file 183 | 184 | - ScreenshotTool: screenshot generator 185 | 186 | - TextTool: just for a startup text 187 | 188 | - ThreadLocalStorage: store thread and Assembler object in a static dictionary through key-value pairs in order to take out the data in any test case 189 | 190 | ![工具类](https://github.com/abcnull/Image-Resources/blob/master/python-ui-auto-test/1575369333647.png) 191 | 192 | ## Bottom Line 193 | 194 | There is much about this project to optimized. I sincerely holp for your **Watch**, **Start**, **Fork**! Your commit will help us better improve the project! 195 | Thanks again! 196 | 197 | : ) 198 | 199 | - Author: **abcnull** 200 | - Csdn Blog: **https://blog.csdn.net/abcnull** 201 | - GitHub: **https://github.com/abcnull** 202 | - E-Mail: **abcnull@qq.com** 203 | -------------------------------------------------------------------------------- /api-test/__init__.py: -------------------------------------------------------------------------------- 1 | # python 接口测试框架 2 | -------------------------------------------------------------------------------- /ui-test/README.md: -------------------------------------------------------------------------------- 1 | [toc] 2 | 3 | python + selenium + unittest + PageObject(PO 思想) + BeautifulReport + redis + mysql + ParamUnittest(外部传参) + tomorrow(多线程) + 本地线程存储 + img 截图 + log 日志 + 多浏览器支持 + RemoteWebDriver + ini 文件读取 + 全参数化构建 4 | 5 | # 1.框架注意点-使用前必看! 6 | - 项目完全依靠参数化构建,见文件`ui-test/resource/config/config.ini` 7 | - `ui-test/case` 中存放测试用例,测试用例需要以 test 打头 8 | - `ui-test/page`中存放 PO 对象,PO 对象中的测试点方法名需要以 test 开头,并且通过下划线加数字形式进行执行排序 9 | - `ui-test/resource/driver`中的谷歌驱动对应浏览器 77 版本的,对于 78 及以上版本的浏览器还需要小伙伴们自行更新驱动文件 10 | - 此项目中已将`api-test`和`ui-test`设置成包文件夹,虽然`api-test`包中为空 11 | - 此项目在我电脑中才用虚拟环境,所以在`.gitignore`中把`venv`文件取消了跟踪 12 | - `ui-test`包中`/resource/config/config.ini`中的键名参数不能大写,因为读取`.ini`的键名必以小写读出! 13 | - `ui-test`包中`/util/log_tool.py`中最下方请不要取消注释,除非`log_tool.py`模块去单独调试 14 | - 项目名一定要是`python-ui-auto-test`,其下目录名或文件名不要使用同名,当 ci/cd 集成时其上目录名要不要使用此名,因为代码中根据此名寻找路径,同名文件寻找路径会出现异常 15 | - 各测试点方法上的报告截图装饰器的参数需要固定这么写,因为路径原因,我也很无奈啊╮(╯▽╰)╭ 16 | - 建议用谷歌和火狐驱动,ie 似乎元素定位不一样 17 | 18 | # 2.所需依赖 19 | 本人使用的是 python 3.6 20 | ``` 21 | BeautifulReport 0.1.2 22 | ParamUnittest 0.2 23 | PyMySQL 0.9.3 24 | futures 3.1.1 25 | pip 10.0.1 26 | redis 3.3.11 27 | selenium 3.141.0 28 | setuptools 39.1.0 29 | tomorrow 0.2.4 30 | urllib3 1.25.7 31 | ``` 32 | 33 | # 3.项目结构 34 | ``` 35 | python-ui-auto-test 36 | - api-test(api 测试包,未添加内容) 37 | - ui-test(ui 测试包) 38 | - base(与项目初始化配置相关) 39 | - case(测试用例脚本) 40 | - common(共用方法) 41 | - data(数据驱动) 42 | - locator(页面对应的元素定位) 43 | - page(页面类) 44 | - report(输出报告) 45 | - html(html 类型报告) 46 | - log(log 日志报告) 47 | - img(测试截图) 48 | - resource(资源文件夹) 49 | - config(配置文件) 50 | - driver(驱动) 51 | - util(工具类) 52 | - README.md(项目介绍 md) 53 | - requirements.txt(项目依赖清单) 54 | - run_all.py(类似于 testng 文件执行测试套,单线程) 55 | - run_all_mutithread.py(类似于 testng 文件执行测试套,多线程) 56 | - venv(虚拟环境的文件夹,github 拉下来后需要自己创虚拟环境) 57 | - .gitignore(git 忽略文件) 58 | External Libraries 59 | Scratches and Consoles 60 | ``` 61 | 62 | # 4.可以拓展补充的地方 63 | 1. 运行多线程`run_all_mutithread.py`,虽然确实实现了多线程,但报告中没有将多线程的用例联合起来,而是一个线程一个报告,应该可以通过对 BeautifulReport 进行二次开发,采用协程的方式可以解决 64 | 2. 可添加读取 csv,xls,yml,txt 等多种文件的工具类方便读取数据 65 | 3. 连接 oracle,sqlserver,mongoDB 等多种类型数据库的拓展实现 66 | 4. `log_tool.py`,`config_reader.py`,`screenshot_tool`,`assembler.py`中获取项目路径的方式不是很友好(因为如果改动项目文件结构有可能导致文件找不到的错误)。采用目前结构是正常实现,可以优化下路径获取方式 67 | 5. 对于邮件的发送工具类我建议小伙伴无需添加,可以自行将项目集成到 jenkins 等其他 ci/cd 平台自动构建,自动发送邮件 68 | 6. 可以考虑修改 config.ini 中的路径参数,不用每个路径后必添 "/",也可智能识别路径,同时注意使用到这几个路径参数的工具类代码也需要修改 69 | 7. 可以通过配置让火狐浏览器不自动产生 log 日志,目前只把火狐自动产生的日志放在项目 ui-log 日志生成的地方 70 | 8. 配置文件中可添加浏览器版本信息,目前是没有这项参数的。若添加了,之后再在 assembler 装载器中补全代码即可 71 | 9. 其他...... 72 | 73 | # 5.其他 74 | 搭建过程中非常感谢李鹏飞大侠的技术支持!后续有问题请在如下途径私聊联系! 75 | - 框架作者:**abcnull** 76 | - csdn 博客:**https://blog.csdn.net/abcnull** 77 | - github:**https://github.com/abcnull** 78 | - e-mail:**abcnull@qq.com** 79 | 80 | -------------------------------------------------------------------------------- /ui-test/__init__.py: -------------------------------------------------------------------------------- 1 | # python + selenium + unittest 的 WebUI 测试项目 2 | -------------------------------------------------------------------------------- /ui-test/base/__init__.py: -------------------------------------------------------------------------------- 1 | # 基类 2 | -------------------------------------------------------------------------------- /ui-test/base/assembler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import os 10 | import threading 11 | from selenium import webdriver 12 | from util.config_reader import ConfigReader 13 | from util.mysql_tool import MysqlTool 14 | from util.redis_pool import RedisPool 15 | from util.thread_local_storage import ThreadLocalStorage 16 | from selenium.webdriver.chrome.webdriver import RemoteWebDriver 17 | from selenium.webdriver.chrome.options import Options as ChromeOptions 18 | from selenium.webdriver.firefox.options import Options as FirefoxOptions 19 | from selenium.webdriver.ie.options import Options as IeOptions 20 | 21 | 22 | # 初始装配工具(一个线程分配一个驱动,一个 redis 连接,一个 mysql 连接) 23 | class Assembler: 24 | # 初始化所有工具 25 | def __init__(self): 26 | # 驱动装配 27 | self.assemble_driver() 28 | # redis 工具装配 29 | if ConfigReader().read("project")["redis_enable"].upper() == "Y": 30 | # 装配 redis 连接池工具 31 | self.assemble_redis() 32 | elif ConfigReader().read("project")["redis_enable"].upper() == "N": 33 | self.redis_pool_tool = None 34 | else: 35 | self.redis_pool_tool = None 36 | raise RuntimeError("配置文件中配置 redis 是否启用字段有误!请修改!") 37 | # mysql 工具装配 38 | if ConfigReader().read("project")["mysql_enable"].upper() == "Y": 39 | # 装配 mysql 工具 40 | self.assemble_mysql() 41 | elif ConfigReader().read("project")["mysql_enable"].upper() == "N": 42 | self.mysql_tool = None 43 | else: 44 | self.mysql_tool = None 45 | raise RuntimeError("配置文件中配置 mysql 是否启用字段有误!请修改!") 46 | # 将装配器保存到线程存储器中,键名为线程,键值为装配器对象 47 | ThreadLocalStorage.set(threading.current_thread(), self) 48 | 49 | # 卸下所有 50 | def disassemble_all(self): 51 | # 卸下驱动 52 | self.disassemble_driver() 53 | # 卸下 redis 连接池工具 54 | self.disassemble_redis() 55 | # 卸下 mysql 工具 56 | self.disassemble_mysql() 57 | # 删除当前线程存储的 {当前线程: 装配器} 键值对 58 | ThreadLocalStorage.clear_current_thread() 59 | 60 | ############################## 驱动 ############################## 61 | # 装配驱动 62 | def assemble_driver(self): 63 | # 若是谷歌驱动 64 | if ConfigReader().read("project")["driver"].lower() == "chrome": 65 | # chrome option 66 | chrome_options = ChromeOptions() 67 | # 服务端 root 用户不能直接运行 chrome,添加此参数可以运行 68 | chrome_options.add_argument('--no-sandbox') 69 | # # 下面参数可自行选择 70 | # chrome_options.add_argument('--user-data-dir') 71 | # chrome_options.add_argument('--dns-prefetch-disable') 72 | # chrome_options.add_argument('--lang=en-US') 73 | # chrome_options.add_argument('--disable-setuid-sandbox') 74 | # chrome_options.add_argument('--disable-gpu') 75 | 76 | # 驱动路径 77 | executable_path = os.path.abspath(os.path.dirname(__file__))[ 78 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 79 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("driver")[ 80 | "chrome_driver_path"] 81 | # 如果读取不到 remote_ip 或者 remote_port 就不用远端浏览器 82 | if ConfigReader().read("project")["remote_ip"] == "" or ConfigReader().read("project")["remote_port"] == "": 83 | self.driver = webdriver.Chrome(executable_path=executable_path, chrome_options=chrome_options) 84 | # 使用远端浏览器 85 | else: 86 | url = "http://" + ConfigReader().read("project")["remote_ip"] + ":" + ConfigReader().read("project")[ 87 | "remote_port"] + "/wd/hub" 88 | self.driver = RemoteWebDriver(command_executor=url, options=chrome_options) 89 | 90 | # 若是火狐驱动 91 | elif ConfigReader().read("project")["driver"].lower() == "firefox": 92 | # firefox option 93 | firefox_options = FirefoxOptions() 94 | # 服务端 root 用户不能直接运行 chrome,添加此参数可以运行 95 | firefox_options.add_argument('--no-sandbox') 96 | # 驱动路径 97 | executable_path = os.path.abspath(os.path.dirname(__file__))[ 98 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 99 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("driver")[ 100 | "firefox_driver_path"] 101 | # 获取驱动自己产出日志路径 102 | log_path = os.path.abspath(os.path.dirname(__file__))[ 103 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 104 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("log")["logfile_path"] 105 | self.driver = webdriver.Firefox(executable_path=executable_path, log_path=log_path + "geckodriver.log", 106 | firefox_options=firefox_options) 107 | 108 | # 若是 IE 驱动 109 | elif ConfigReader().read("project")["driver"].lower() == "ie": 110 | # ie option 111 | ie_options = IeOptions() 112 | # 服务端 root 用户不能直接运行 chrome,添加此参数可以运行 113 | ie_options.add_argument('--no-sandbox') 114 | # 驱动路径 115 | executable_path = os.path.abspath(os.path.dirname(__file__))[ 116 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 117 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("driver")["ie_driver_path"] 118 | self.driver = webdriver.Ie(executable_path=executable_path, ie_options=ie_options) 119 | 120 | # 若是 Edge 驱动 121 | elif ConfigReader().read("project")["driver"].lower() == "edge": 122 | executable_path = os.path.abspath(os.path.dirname(__file__))[ 123 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 124 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("driver")[ 125 | "edge_driver_path"] 126 | self.driver = webdriver.Edge(executable_path=executable_path) 127 | 128 | # 若是欧朋驱动 129 | elif ConfigReader().read("project")["driver"].lower() == "opera": 130 | executable_path = os.path.abspath(os.path.dirname(__file__))[ 131 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 132 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("driver")[ 133 | "opera_driver_path"] 134 | self.driver = webdriver.Opera(executable_path=executable_path) 135 | 136 | # 若是 Safari 驱动 137 | elif ConfigReader().read("project")["driver"].lower() == "safari": 138 | executable_path = os.path.abspath(os.path.dirname(__file__))[ 139 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 140 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("driver")[ 141 | "safari_driver_path"] 142 | self.driver = webdriver.Safari(executable_path=executable_path) 143 | 144 | # 不支持的浏览器类型 145 | else: 146 | self.driver = None 147 | raise RuntimeError("配置文件中配置了不支持的浏览器类型!请修改浏览器类型!") 148 | 149 | # 卸下驱动 150 | def disassemble_driver(self): 151 | if self.driver is not None: 152 | self.driver.quit() 153 | self.driver = None 154 | 155 | # 获取驱动 156 | def get_driver(self): 157 | return self.driver 158 | 159 | ############################## redis ############################## 160 | # 装配 redis 工具 161 | def assemble_redis(self): 162 | # redis 连接池工具 163 | self.redis_pool_tool = RedisPool() 164 | 165 | # 卸下 redis 工具 166 | def disassemble_redis(self): 167 | if self.redis_pool_tool is not None: 168 | # 关闭连接 169 | self.redis_pool_tool.release_redis_conn() 170 | # 关闭连接池 171 | self.redis_pool_tool.release_redis_pool() 172 | self.redis_pool_tool = None 173 | 174 | # 获取 redis 工具 175 | def get_redis(self): 176 | return self.redis_pool_tool 177 | 178 | ############################## mysql ############################## 179 | # 装配 mysql 工具 180 | def assemble_mysql(self): 181 | # mysql 连接 182 | self.mysql_tool = MysqlTool() 183 | 184 | # 卸下 mysql 工具 185 | def disassemble_mysql(self): 186 | if self.mysql_tool is not None: 187 | # 关闭 mysql 连接 188 | self.mysql_tool.release_mysql_conn() 189 | self.mysql_tool = None 190 | 191 | # 获取 mysql 工具 192 | def get_mysql(self): 193 | return self.mysql_tool 194 | -------------------------------------------------------------------------------- /ui-test/case/__init__.py: -------------------------------------------------------------------------------- 1 | # 用例 2 | -------------------------------------------------------------------------------- /ui-test/case/test_baidu_case.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import paramunittest 4 | from BeautifulReport import BeautifulReport 5 | from base.assembler import Assembler 6 | from data.csdn_data import CsdnData 7 | from page.baidu_main_page import BaiduMainPage 8 | from page.baidu_result_page import BaiduResultPage 9 | from util.config_reader import ConfigReader 10 | from util.log_tool import start_info, end_info, log 11 | from util.screenshot_tool import ScreenshotTool 12 | 13 | 14 | # 参数化构建参数 15 | @paramunittest.parametrized( 16 | # 参数{语言,环境} 17 | {"lan": ConfigReader().read("project")["lan"], "env": ConfigReader().read("project")["env"]} 18 | ) 19 | # 百度页流程用例测试 20 | class TestBaiduCase(unittest.TestCase): 21 | # 出错需要截图时此方法自动被调用 22 | def save_img(self, img_name): 23 | ScreenshotTool().save_img(self.driver, img_name) 24 | 25 | # 参数化构建方法 26 | def setParameters(self, lan, env): 27 | self.lan = lan 28 | self.env = env 29 | 30 | # @BeforeTest 31 | def setUp(self): 32 | # 开始的 log 信息 33 | start_info() 34 | # 装配器初始化 35 | self.assembler = Assembler() 36 | 37 | # 提取驱动 38 | self.driver = self.assembler.get_driver() 39 | 40 | # 提取 redis 连接池工具 41 | self.redis_tool = self.assembler.get_redis() 42 | # 从连接池工具中拿到一个连接 43 | self.redis_conn = self.redis_tool.get_redis_conn() 44 | 45 | # # 提取 mysql 工具 46 | # self.mysql_tool = self.assembler.get_mysql() 47 | # # 从 mysql 工具中拿到一个连接 48 | # self.mysql_conn = self.mysql_conn.get_mysql_conn() 49 | 50 | # @AfterTest 51 | def tearDown(self): 52 | # 结束的 log 信息 53 | end_info() 54 | # 装配器卸载 55 | self.assembler.disassemble_all() 56 | 57 | # 第一个测试点 58 | @BeautifulReport.add_test_img(ScreenshotTool().get_img_name("../../report/img/test_1_TestBaiduCase")) 59 | def test_1_TestBaiduCase(self): 60 | # log 信息 61 | log().info(f"百度测试第一个用例,环境" + self.env + "语言" + self.lan) 62 | # 初始化百度页面 63 | main_page = BaiduMainPage(self.driver) 64 | result_page = BaiduResultPage(self.driver) 65 | 66 | # 开启百度首页 67 | main_page.jump_to() 68 | # 首页搜索 69 | main_page.search() 70 | # 点击结果页的第一条链接 71 | result_page.click_first_link() 72 | 73 | # redis 存储一条数据 74 | self.redis_conn.set("param", "百度") 75 | 76 | # 切换窗口句柄 77 | result_page.switch_to_window_handle(CsdnData.handle_url) 78 | # 休眠 2 秒方便观察页面运行效果 79 | time.sleep(2) 80 | 81 | # 第二个测试点 82 | @BeautifulReport.add_test_img(ScreenshotTool().get_img_name("../../report/img/test_1_TestBaiduCase")) 83 | def test_2_TestBaiduCase(self): 84 | # log 信息 85 | log().info(f"百度测试第二个用例,环境" + self.env + "语言" + self.lan) 86 | 87 | # 从 redis 取出数据 88 | log().info(f"从 redis 中取出数据:" + self.redis_conn.get("param").decode()) 89 | 90 | # 休眠 2 秒方便观察页面运行效果 91 | time.sleep(2) 92 | 93 | 94 | # 当前用例程序入口 95 | if __name__ == "__main__": 96 | # 使用 unittest 依次执行当前模块中 test 打头的方法 97 | # verbosity=0 静默模式,仅仅获取总的测试用例数以及总的结果 98 | # verbosity=1 默认模式,在每个成功的用例前面有个’.’,每个失败的用例前面有个’F’ 99 | # verbosity=2 详细模式,测试结果会显示每个测试用例的所有相关信息 100 | unittest.main(verbosity=0) 101 | -------------------------------------------------------------------------------- /ui-test/case/test_csdn_case.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import paramunittest 4 | from BeautifulReport import BeautifulReport 5 | from base.assembler import Assembler 6 | from page.csdn_page import CsdnPage 7 | from util.log_tool import start_info, end_info, log 8 | from util.screenshot_tool import ScreenshotTool 9 | 10 | 11 | # 参数化构建参数 12 | @paramunittest.parametrized( 13 | # 参数{语言,环境} 14 | {"lan": "en_GB", "env": "UAT"} 15 | ) 16 | # csdn 流程用例测试 17 | class TestCsdnCase(unittest.TestCase): 18 | # 出错需要截图时此方法自动被调用 19 | def save_img(self, img_name): 20 | ScreenshotTool().save_img(self.driver, img_name) 21 | 22 | # 参数化构建方法 23 | def setParameters(self, lan, env): 24 | self.lan = lan 25 | self.env = env 26 | 27 | # 放在各个测试方法中首行执行 28 | @classmethod 29 | def before_setUp(self): 30 | # 开始的 log 信息 31 | start_info() 32 | # 装配器初始化并开启一个谷歌驱动 33 | self.assembler = Assembler() 34 | self.driver = self.assembler.get_driver() 35 | # 创建 csdn 页面类 36 | self.csdn_page = CsdnPage(self.driver) 37 | 38 | # 放在各个测试方法中末行执行 39 | @classmethod 40 | def after_tearDown(self): 41 | # 结束的 log 信息 42 | end_info() 43 | # 装配器卸载 44 | self.assembler.disassemble_all() 45 | 46 | # 第一个测试点 47 | @BeautifulReport.add_test_img(ScreenshotTool().get_img_name("../../report/img/test_1_TestCsdnCase")) 48 | def test_1_TestCsdnCase(self): 49 | # 初始化 50 | self.before_setUp() 51 | 52 | # log 信息 53 | log().info(f"csdn 测试第一个用例,环境" + self.env + "语言" + self.lan) 54 | 55 | # 开启 csdn 页面 56 | self.csdn_page.jump_to() 57 | # 休眠 2 秒方便看效果 58 | time.sleep(2) 59 | 60 | # 强行截图 61 | ScreenshotTool().save_img(self.driver, "force_test_1_TestCsdnCase") 62 | 63 | # 释放 64 | self.after_tearDown() 65 | 66 | 67 | # 当前用例程序入口 68 | if __name__ == "__main__": 69 | # 使用 unittest 依次执行当前模块中 test 打头的方法 70 | # verbosity=0 静默模式,仅仅获取总的测试用例数以及总的结果 71 | # verbosity=1 默认模式,在每个成功的用例前面有个’.’,每个失败的用例前面有个’F’ 72 | # verbosity=2 详细模式,测试结果会显示每个测试用例的所有相关信息 73 | unittest.main(verbosity=1) 74 | -------------------------------------------------------------------------------- /ui-test/case/test_other_case.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import paramunittest 4 | from BeautifulReport import BeautifulReport 5 | from base.assembler import Assembler 6 | from page.csdn_page import CsdnPage 7 | from util.log_tool import start_info, end_info, log 8 | from util.screenshot_tool import ScreenshotTool 9 | 10 | 11 | # 参数化构建参数 12 | @paramunittest.parametrized( 13 | # 参数{语言,环境} 14 | {"lan": "en_GB", "env": "UAT"} 15 | ) 16 | # 其他流程用例测试 17 | class TestOtherCase(unittest.TestCase): 18 | # 出错需要截图时此方法自动被调用 19 | def save_img(self, img_name): 20 | ScreenshotTool().save_img(self.driver, img_name) 21 | 22 | # 参数化构建方法 23 | def setParameters(self, lan, env): 24 | self.lan = lan 25 | self.env = env 26 | 27 | # @BeforeTest 28 | def setUp(self): 29 | # 开始的 log 信息 30 | start_info() 31 | # 装配器初始化并开启一个谷歌驱动 32 | self.assembler = Assembler() 33 | self.driver = self.assembler.get_driver() 34 | 35 | # @AfterTest 36 | def tearDown(self): 37 | # 结束的 log 信息 38 | end_info() 39 | # 装配器卸载 40 | self.assembler.disassemble_all() 41 | 42 | # 第一个测试点 43 | @BeautifulReport.add_test_img(ScreenshotTool().get_img_name("../../report/img/test_1_TestOtherCase")) 44 | def test_1_TestOtherCase(self): 45 | # log 信息 46 | log().info(f"其他测试第一个用例,环境" + self.env + "语言" + self.lan) 47 | 48 | # 创建 csdn 页面类 49 | csdn_page = CsdnPage(self.driver) 50 | # 开启 csdn 页面 51 | csdn_page.jump_to() 52 | # 休眠 2 秒方便观察页面运行效果 53 | time.sleep(2) 54 | 55 | # 手动断言错误 56 | assert False 57 | 58 | 59 | # 当前用例程序入口 60 | if __name__ == "__main__": 61 | # 使用 unittest 依次执行当前模块中 test 打头的方法 62 | # verbosity=0 静默模式,仅仅获取总的测试用例数以及总的结果 63 | # verbosity=1 默认模式,在每个成功的用例前面有个’.’,每个失败的用例前面有个’F’ 64 | # verbosity=2 详细模式,测试结果会显示每个测试用例的所有相关信息 65 | unittest.main(verbosity=2) 66 | -------------------------------------------------------------------------------- /ui-test/common/__init__.py: -------------------------------------------------------------------------------- 1 | # 公有层 2 | -------------------------------------------------------------------------------- /ui-test/common/browser_common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import time 10 | 11 | 12 | # 浏览器类封装浏览器操作 13 | class BrowserCommon(object): 14 | # 初始化驱动相关 15 | def __init__(self, driver=None): 16 | """ 17 | 传驱动 18 | :param driver: 驱动 19 | """ 20 | if driver: 21 | self.driver = driver 22 | else: 23 | raise RuntimeError("页面类初始化驱动必传!") 24 | # 窗口最大化 25 | self.driver.maximize_window() 26 | # 设置 20 s 隐式等待 27 | self.driver.implicitly_wait(20) 28 | 29 | ############################## 获取浏览器属性 ############################## 30 | 31 | # 获取当前页面窗口句柄 32 | def get_window_handle(self): 33 | return self.driver.current_window_handle 34 | 35 | # 获取当前所有页面窗口句柄 36 | def get_window_handles(self): 37 | return self.driver.window_handles 38 | 39 | # 获取当前页面标题 40 | def get_title(self): 41 | return self.driver.title 42 | 43 | # 获取当前页面网址 44 | def get_url(self): 45 | return self.driver.current_url 46 | 47 | # 返回当前驱动 48 | def get_driver(self): 49 | return self.driver 50 | 51 | ############################## 浏览器切换操作 ############################## 52 | 53 | # 切换 frame 54 | def switch_to_frame(self, param): 55 | self.driver.switch_to.frame(param) 56 | 57 | # 切换alter 58 | def switch_to_alert(self): 59 | return self.driver.switch_to.alert 60 | 61 | # 切换窗口句柄 62 | def switch_to_window_handle(self, url=""): 63 | """ 64 | 切换窗口 65 | 窗口数 == 1,提示只有个一个窗口 66 | 窗口数 == 2,切换另一个窗口 67 | 窗口数 >= 3,切换到指定窗口 68 | """ 69 | # 获取当前所有句柄 70 | window_handles = self.get_window_handles() 71 | # 窗口数 == 1 72 | if len(window_handles) == 1: 73 | pass 74 | elif len(window_handles) == 2: 75 | # 获取另一个窗口句柄 76 | the_other_handle = window_handles[1 - window_handles.index(self.get_window_handle)] 77 | # 切换句柄 78 | self.driver.switch_to.window(the_other_handle) 79 | else: 80 | for handle in window_handles: 81 | # 切换句柄 82 | self.driver.switch_to.window(handle) 83 | # url 匹配到指定句柄时停止循环 84 | if url in self.get_url(): 85 | break 86 | 87 | ############################## 浏览器其他操作 ############################## 88 | 89 | # 驱动退出 90 | def close(self): 91 | self.driver.quit() 92 | # self.undeploy_all() 93 | 94 | # 休眠一定时间 95 | def sleep(self, seconds=2): 96 | time.sleep(seconds) 97 | 98 | # 执行 JS 99 | def execute_script(self, js, *args): 100 | self.driver.execute_script(js, *args) 101 | -------------------------------------------------------------------------------- /ui-test/common/page_common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | from selenium.webdriver.support import expected_conditions 10 | from selenium.webdriver.support.wait import WebDriverWait 11 | from common.browser_common import BrowserCommon 12 | 13 | 14 | # Page 项目无关页面类封装基本页面操作 15 | class PageCommon(BrowserCommon): 16 | 17 | ############################## 基本方法再封装 ############################## 18 | # 依据 xpath 找到指定元素 19 | def find_element_by_xpath(self, xpath): 20 | """ 21 | 通过 xpath 找元素 22 | :param xpath: 元素定位 23 | :return: 原生通过 xpath 找元素方法 24 | """ 25 | return self.driver.find_element_by_xpath(xpath) 26 | 27 | # 找到指定元素 28 | def find_element(self, *args): 29 | """ 30 | 找元素 31 | :param args: 定位与通过什么定位 32 | :return: 原生找元素方法 33 | """ 34 | return self.driver.find_element(*args) 35 | 36 | # 依据 xpath 找到指定的一批元素 37 | def find_elements_by_xpath(self, xpath): 38 | """ 39 | 通过 xpath 找多元素 40 | :param xpath: 多元素定位 41 | :return: 原生通过 xpath 找多元素方法 42 | """ 43 | return self.driver.find_elements_by_xpath(xpath) 44 | 45 | # 找到指定的一批元素 46 | def find_elements(self, *args): 47 | """ 48 | 找多元素 49 | :param args: 定位与通过什么定位 50 | :return: 原生找多元素方法 51 | """ 52 | return self.driver.find_elements(*args) 53 | 54 | ############################## 单个元素操作 ############################## 55 | # 点击元素 56 | def click_element(self, xpath): 57 | """ 58 | 点击元素 59 | :param xpath: 元素定位 60 | :return: 返回原生点击事件 61 | """ 62 | # 显示等待元素可点击 63 | WebDriverWait(self.driver, 10, 0.1).until(expected_conditions.element_to_be_clickable(("xpath", xpath))) 64 | # 点击元素 65 | self.driver.find_element_by_xpath(xpath).click() 66 | 67 | # 输入框输入数据 68 | def input(self, xpath, value): 69 | """ 70 | 输入值 71 | :param xpath: 元素定位 72 | :param value: 输入值 73 | :return: 返回 send_keys 原生方法 74 | """ 75 | # 显示等待元素可点击 76 | WebDriverWait(self.driver, 10, 0.1).until(expected_conditions.element_to_be_clickable(("xpath", xpath))) 77 | # 输入框输入数据 78 | self.driver.find_element_by_xpath(xpath).send_keys(value) 79 | -------------------------------------------------------------------------------- /ui-test/data/__init__.py: -------------------------------------------------------------------------------- 1 | # 数据驱动层 2 | -------------------------------------------------------------------------------- /ui-test/data/baidu_main_data.py: -------------------------------------------------------------------------------- 1 | # 百度首页数据驱动 2 | class BaiduMainData: 3 | # 百度首页网址 4 | url = "http://www.baidu.com" 5 | 6 | # 搜索框搜索的数据 7 | data = "abcnull csdn" 8 | -------------------------------------------------------------------------------- /ui-test/data/baidu_result_data.py: -------------------------------------------------------------------------------- 1 | # 百度结果页数据驱动 2 | # class BaiduResultData: 3 | -------------------------------------------------------------------------------- /ui-test/data/csdn_data.py: -------------------------------------------------------------------------------- 1 | # csdn 元素数据驱动 2 | class CsdnData: 3 | # csdn home 网址 4 | url = "https://blog.csdn.net/abcnull" 5 | 6 | # csdn 句柄网址 7 | handle_url = "https://blog.csdn.net/abcnull" 8 | -------------------------------------------------------------------------------- /ui-test/locator/__init__.py: -------------------------------------------------------------------------------- 1 | # 对应页面类的元素定位 2 | -------------------------------------------------------------------------------- /ui-test/locator/baidu_main_locator.py: -------------------------------------------------------------------------------- 1 | # 百度首页的元素定位 2 | class BaiduMainLocator: 3 | # 首页的搜索 input 定位 4 | search_input = "//div[@id='wrapper']//form//input[@id='kw']" 5 | 6 | # 首页的搜索 btn 定位 7 | search_btn = "//div[@id='wrapper']//form//input[@id='su']" 8 | -------------------------------------------------------------------------------- /ui-test/locator/baidu_result_locator.py: -------------------------------------------------------------------------------- 1 | # 百度结果页的元素定位 2 | class BaiduResultLocator: 3 | # 结果页的第一行链接 4 | link_a = "//div[@id='content_left']/div[@id='1']//a" 5 | -------------------------------------------------------------------------------- /ui-test/locator/csdn_locator.py: -------------------------------------------------------------------------------- 1 | # csdn 页的元素定位 2 | # class CsdnLocator: 3 | -------------------------------------------------------------------------------- /ui-test/page/__init__.py: -------------------------------------------------------------------------------- 1 | # 页面类 PO 2 | -------------------------------------------------------------------------------- /ui-test/page/baidu_main_page.py: -------------------------------------------------------------------------------- 1 | from common.page_common import PageCommon 2 | from data.baidu_main_data import BaiduMainData 3 | from locator.baidu_main_locator import BaiduMainLocator 4 | 5 | 6 | # 百度首页页面类 7 | class BaiduMainPage(PageCommon): 8 | # 百度首页进入页面操作 9 | def jump_to(self): 10 | self.driver.get(BaiduMainData.url) 11 | 12 | # 搜索数据 13 | def search(self): 14 | self.input(BaiduMainLocator.search_input, BaiduMainData.data) 15 | self.click_element(BaiduMainLocator.search_btn) 16 | -------------------------------------------------------------------------------- /ui-test/page/baidu_result_page.py: -------------------------------------------------------------------------------- 1 | from common.page_common import PageCommon 2 | from locator.baidu_result_locator import BaiduResultLocator 3 | 4 | 5 | # 百度结果页页面类 6 | class BaiduResultPage(PageCommon): 7 | # 百度搜索结果页点击第一行链接操作 8 | def click_first_link(self): 9 | self.click_element(BaiduResultLocator.link_a) 10 | -------------------------------------------------------------------------------- /ui-test/page/csdn_page.py: -------------------------------------------------------------------------------- 1 | from common.page_common import PageCommon 2 | from data.csdn_data import CsdnData 3 | 4 | 5 | # csdn 页面类 6 | class CsdnPage(PageCommon): 7 | # csdn 进入页面操作 8 | def jump_to(self): 9 | self.driver.get(CsdnData.url) 10 | -------------------------------------------------------------------------------- /ui-test/report/img/force_test_1_TestCsdnCase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/report/img/force_test_1_TestCsdnCase.png -------------------------------------------------------------------------------- /ui-test/report/log/ui_log.log: -------------------------------------------------------------------------------- 1 | [2019-12-02 18:06:00,275][MainThread:10864][task_id:root][log_tool.py:112][INFO][<测试开始>] 2 | [2019-12-02 18:06:00,917][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session {"capabilities": {"firstMatch": [{}], "alwaysMatch": {"browserName": "chrome", "platformName": "any", "goog:chromeOptions": {"extensions": [], "args": ["--no-sandbox"]}}}, "desiredCapabilities": {"browserName": "chrome", "version": "", "platform": "ANY", "goog:chromeOptions": {"extensions": [], "args": ["--no-sandbox"]}}}] 3 | [2019-12-02 18:06:00,918][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:225][DEBUG][Starting new HTTP connection (1): 127.0.0.1:54525] 4 | [2019-12-02 18:06:02,915][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session HTTP/1.1" 200 683] 5 | [2019-12-02 18:06:02,916][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 6 | [2019-12-02 18:06:02,942][MainThread:10864][task_id:root][test_baidu_case.py:61][INFO][百度测试第一个用例,环境UAT语言en_GB] 7 | [2019-12-02 18:06:02,946][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/window/maximize {}] 8 | [2019-12-02 18:06:04,250][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session/704ae6a97352fc07f923ef6d23d64901/window/maximize HTTP/1.1" 200 51] 9 | [2019-12-02 18:06:04,250][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 10 | [2019-12-02 18:06:04,250][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/timeouts {"implicit": 20000}] 11 | [2019-12-02 18:06:04,256][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session/704ae6a97352fc07f923ef6d23d64901/timeouts HTTP/1.1" 200 14] 12 | [2019-12-02 18:06:04,257][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 13 | [2019-12-02 18:06:04,257][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/window/maximize {}] 14 | [2019-12-02 18:06:04,280][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session/704ae6a97352fc07f923ef6d23d64901/window/maximize HTTP/1.1" 200 51] 15 | [2019-12-02 18:06:04,281][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 16 | [2019-12-02 18:06:04,281][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/timeouts {"implicit": 20000}] 17 | [2019-12-02 18:06:04,283][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session/704ae6a97352fc07f923ef6d23d64901/timeouts HTTP/1.1" 200 14] 18 | [2019-12-02 18:06:04,283][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 19 | [2019-12-02 18:06:04,284][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/url {"url": "http://www.baidu.com"}] 20 | [2019-12-02 18:06:05,543][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session/704ae6a97352fc07f923ef6d23d64901/url HTTP/1.1" 200 14] 21 | [2019-12-02 18:06:05,544][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 22 | [2019-12-02 18:06:05,544][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/element {"using": "xpath", "value": "//div[@id='wrapper']//form//input[@id='kw']"}] 23 | [2019-12-02 18:06:05,581][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "POST /session/704ae6a97352fc07f923ef6d23d64901/element HTTP/1.1" 200 88] 24 | [2019-12-02 18:06:05,581][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 25 | [2019-12-02 18:06:05,582][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901/execute/sync {"script": "return (function(){return function(){var k=this;function l(a){return void 0!==a}function m(a){return\"string\"==typeof a}function aa(a,b){a=a.split(\".\");var c=k;a[0]in c||!c.execScript||c.execScript(\"var \"+a[0]);for(var d;a.length&&(d=a.shift());)!a.length&&l(b)?c[d]=b:c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}}\nfunction ba(a){var b=typeof a;if(\"object\"==b)if(a){if(a instanceof Array)return\"array\";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if(\"[object Window]\"==c)return\"object\";if(\"[object Array]\"==c||\"number\"==typeof a.length&&\"undefined\"!=typeof a.splice&&\"undefined\"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable(\"splice\"))return\"array\";if(\"[object Function]\"==c||\"undefined\"!=typeof a.call&&\"undefined\"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable(\"call\"))return\"function\"}else return\"null\";\nelse if(\"function\"==b&&\"undefined\"==typeof a.call)return\"object\";return b}function ca(a,b,c){return a.call.apply(a.bind,arguments)}function da(a,b,c){if(!a)throw Error();if(2b||a.indexOf(\"Error\",b)!=b)a+=\"Error\";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||\"\"}p(ga,Error);var ha=\"unknown error\",q={15:\"element not selectable\",11:\"element not visible\"};q[31]=ha;q[30]=ha;q[24]=\"invalid cookie domain\";q[29]=\"invalid element coordinates\";q[12]=\"invalid element state\";\nq[32]=\"invalid selector\";q[51]=\"invalid selector\";q[52]=\"invalid selector\";q[17]=\"javascript error\";q[405]=\"unsupported operation\";q[34]=\"move target out of bounds\";q[27]=\"no such alert\";q[7]=\"no such element\";q[8]=\"no such frame\";q[23]=\"no such window\";q[28]=\"script timeout\";q[33]=\"session not created\";q[10]=\"stale element reference\";q[21]=\"timeout\";q[25]=\"unable to set cookie\";q[26]=\"unexpected alert open\";q[13]=ha;q[9]=\"unknown command\";ga.prototype.toString=function(){return this.name+\": \"+this.message};var ia={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgreen:\"#006400\",\ndarkgrey:\"#a9a9a9\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",\nghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",grey:\"#808080\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgreen:\"#90ee90\",lightgrey:\"#d3d3d3\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",\nlightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",\nmoccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",\nseashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"};function ja(a,b){this.width=a;this.height=b}ja.prototype.toString=function(){return\"(\"+this.width+\" x \"+this.height+\")\"};ja.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};ja.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};ja.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function ka(a,b){var c=la;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var ma=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^[\\s\\xa0]+|[\\s\\xa0]+$/g,\"\")};function na(a,b){return ab?1:0}function oa(a){return String(a).replace(/\\-([a-z])/g,function(a,c){return c.toUpperCase()})};/*\n\n The MIT License\n\n Copyright (c) 2007 Cybozu Labs, Inc.\n Copyright (c) 2012 Google Inc.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to\n deal in the Software without restriction, including without limitation the\n rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n IN THE SOFTWARE.\n*/\nfunction pa(a,b,c){this.a=a;this.b=b||1;this.f=c||1};function qa(a){this.b=a;this.a=0}function ra(a){a=a.match(sa);for(var b=0;b]=|\\s+|./g,ta=/^\\s/;function t(a,b){return a.b[a.a+(b||0)]}function u(a){return a.b[a.a++]}function ua(a){return a.b.length<=a.a};var v;a:{var va=k.navigator;if(va){var wa=va.userAgent;if(wa){v=wa;break a}}v=\"\"}function x(a){return-1!=v.indexOf(a)};function y(a,b){this.h=a;this.c=l(b)?b:null;this.b=null;switch(a){case \"comment\":this.b=8;break;case \"text\":this.b=3;break;case \"processing-instruction\":this.b=7;break;case \"node\":break;default:throw Error(\"Unexpected argument\");}}function xa(a){return\"comment\"==a||\"text\"==a||\"processing-instruction\"==a||\"node\"==a}y.prototype.a=function(a){return null===this.b||this.b==a.nodeType};y.prototype.f=function(){return this.h};\ny.prototype.toString=function(){var a=\"Kind Test: \"+this.h;null===this.c||(a+=z(this.c));return a};function ya(a,b){this.j=a.toLowerCase();a=\"*\"==this.j?\"*\":\"http://www.w3.org/1999/xhtml\";this.c=b?b.toLowerCase():a}ya.prototype.a=function(a){var b=a.nodeType;if(1!=b&&2!=b)return!1;b=l(a.localName)?a.localName:a.nodeName;return\"*\"!=this.j&&this.j!=b.toLowerCase()?!1:\"*\"==this.c?!0:this.c==(a.namespaceURI?a.namespaceURI.toLowerCase():\"http://www.w3.org/1999/xhtml\")};ya.prototype.f=function(){return this.j};\nya.prototype.toString=function(){return\"Name Test: \"+(\"http://www.w3.org/1999/xhtml\"==this.c?\"\":this.c+\":\")+this.j};function za(a){switch(a.nodeType){case 1:return fa(Aa,a);case 9:return za(a.documentElement);case 11:case 10:case 6:case 12:return Ba;default:return a.parentNode?za(a.parentNode):Ba}}function Ba(){return null}function Aa(a,b){if(a.prefix==b)return a.namespaceURI||\"http://www.w3.org/1999/xhtml\";var c=a.getAttributeNode(\"xmlns:\"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?Aa(a.parentNode,b):null};function Ca(a,b){if(m(a))return m(b)&&1==b.length?a.indexOf(b,0):-1;for(var c=0;cb?null:m(a)?a.charAt(b):a[b]}function Ia(a){return Array.prototype.concat.apply([],arguments)}\nfunction Ja(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};function Ka(){return x(\"iPhone\")&&!x(\"iPod\")&&!x(\"iPad\")};var La=\"backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor\".split(\" \"),Ma=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,Na=/^#(?:[0-9a-f]{3}){1,2}$/i,Oa=/^(?:rgba)?\\((\\d{1,3}),\\s?(\\d{1,3}),\\s?(\\d{1,3}),\\s?(0|1|0\\.\\d*)\\)$/i,Pa=/^(?:rgb)?\\((0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2})\\)$/i;function Qa(){return(x(\"Chrome\")||x(\"CriOS\"))&&!x(\"Edge\")};function Ra(a,b){this.x=l(a)?a:0;this.y=l(b)?b:0}Ra.prototype.toString=function(){return\"(\"+this.x+\", \"+this.y+\")\"};Ra.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};Ra.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};Ra.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};var Sa=x(\"Opera\"),B=x(\"Trident\")||x(\"MSIE\"),Ta=x(\"Edge\"),Ua=x(\"Gecko\")&&!(-1!=v.toLowerCase().indexOf(\"webkit\")&&!x(\"Edge\"))&&!(x(\"Trident\")||x(\"MSIE\"))&&!x(\"Edge\"),Va=-1!=v.toLowerCase().indexOf(\"webkit\")&&!x(\"Edge\");function Wa(){var a=k.document;return a?a.documentMode:void 0}var Xa;\na:{var Ya=\"\",Za=function(){var a=v;if(Ua)return/rv\\:([^\\);]+)(\\)|;)/.exec(a);if(Ta)return/Edge\\/([\\d\\.]+)/.exec(a);if(B)return/\\b(?:MSIE|rv)[: ]([^\\);]+)(\\)|;)/.exec(a);if(Va)return/WebKit\\/(\\S+)/.exec(a);if(Sa)return/(?:Version)[ \\/]?(\\S+)/.exec(a)}();Za&&(Ya=Za?Za[1]:\"\");if(B){var $a=Wa();if(null!=$a&&$a>parseFloat(Ya)){Xa=String($a);break a}}Xa=Ya}var la={};\nfunction ab(a){return ka(a,function(){for(var b=0,c=ma(String(Xa)).split(\".\"),d=ma(String(a)).split(\".\"),e=Math.max(c.length,d.length),f=0;!b&&f\",4,2,function(a,b,c){return Pb(function(a,b){return a>b},a,b,c)});S(\"<=\",4,2,function(a,b,c){return Pb(function(a,b){return a<=b},a,b,c)});S(\">=\",4,2,function(a,b,c){return Pb(function(a,b){return a>=b},a,b,c)});var Ob=S(\"=\",3,2,function(a,b,c){return Pb(function(a,b){return a==b},a,b,c,!0)});S(\"!=\",3,2,function(a,b,c){return Pb(function(a,b){return a!=b},a,b,c,!0)});S(\"and\",2,2,function(a,b,c){return Mb(a,c)&&Mb(b,c)});S(\"or\",1,2,function(a,b,c){return Mb(a,c)||Mb(b,c)});function Sb(a,b){if(b.a.length&&4!=a.i)throw Error(\"Primary expression must evaluate to nodeset if filter has predicate(s).\");O.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}p(Sb,O);Sb.prototype.a=function(a){a=this.c.a(a);return Tb(this.h,a)};Sb.prototype.toString=function(){var a=\"Filter:\"+z(this.c);return a+=z(this.h)};function Ub(a,b){if(b.lengtha.A)throw Error(\"Function \"+a.j+\" expects at most \"+a.A+\" arguments, \"+b.length+\" given\");a.H&&A(b,function(b,d){if(4!=b.i)throw Error(\"Argument \"+d+\" to function \"+a.j+\" is not of type Nodeset: \"+b);});O.call(this,a.i);this.v=a;this.c=b;Kb(this,a.g||Fa(b,function(a){return a.g}));Lb(this,a.G&&!b.length||a.F&&!!b.length||Fa(b,function(a){return a.b}))}\np(Ub,O);Ub.prototype.a=function(a){return this.v.m.apply(null,Ia(a,this.c))};Ub.prototype.toString=function(){var a=\"Function: \"+this.v;if(this.c.length)var b=Ea(this.c,function(a,b){return a+z(b)},\"Arguments:\"),a=a+z(b);return a};function Vb(a,b,c,d,e,f,g,h,r){this.j=a;this.i=b;this.g=c;this.G=d;this.F=e;this.m=f;this.C=g;this.A=l(h)?h:g;this.H=!!r}Vb.prototype.toString=function(){return this.j};var Wb={};\nfunction T(a,b,c,d,e,f,g,h){if(Wb.hasOwnProperty(a))throw Error(\"Function already created: \"+a+\".\");Wb[a]=new Vb(a,b,c,d,!1,e,f,g,h)}T(\"boolean\",2,!1,!1,function(a,b){return Mb(b,a)},1);T(\"ceiling\",1,!1,!1,function(a,b){return Math.ceil(Q(b,a))},1);T(\"concat\",3,!1,!1,function(a,b){return Ea(Ja(arguments,1),function(b,d){return b+R(d,a)},\"\")},2,null);T(\"contains\",2,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);return-1!=b.indexOf(a)},2);T(\"count\",1,!1,!1,function(a,b){return b.a(a).l},1,1,!0);\nT(\"false\",2,!1,!1,function(){return!1},0);T(\"floor\",1,!1,!1,function(a,b){return Math.floor(Q(b,a))},1);T(\"id\",4,!1,!1,function(a,b){function c(a){if(D){var b=e.all[a];if(b){if(b.nodeType&&a==b.id)return b;if(b.length)return Ha(b,function(b){return a==b.id})}return null}return e.getElementById(a)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=R(b,a).split(/\\s+/);var f=[];A(a,function(a){a=c(a);!a||0<=Ca(f,a)||f.push(a)});f.sort(sb);var g=new I;A(f,function(a){J(g,a)});return g},1);\nT(\"lang\",2,!1,!1,function(){return!1},1);T(\"last\",1,!0,!1,function(a){if(1!=arguments.length)throw Error(\"Function last expects ()\");return a.f},0);T(\"local-name\",3,!1,!0,function(a,b){return(a=b?Hb(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():\"\"},0,1,!0);T(\"name\",3,!1,!0,function(a,b){return(a=b?Hb(b.a(a)):a.a)?a.nodeName.toLowerCase():\"\"},0,1,!0);T(\"namespace-uri\",3,!0,!1,function(){return\"\"},0,1,!0);\nT(\"normalize-space\",3,!1,!0,function(a,b){return(b?R(b,a):G(a.a)).replace(/[\\s\\xa0]+/g,\" \").replace(/^\\s+|\\s+$/g,\"\")},0,1);T(\"not\",2,!1,!1,function(a,b){return!Mb(b,a)},1);T(\"number\",1,!1,!0,function(a,b){return b?Q(b,a):+G(a.a)},0,1);T(\"position\",1,!0,!1,function(a){return a.b},0);T(\"round\",1,!1,!1,function(a,b){return Math.round(Q(b,a))},1);T(\"starts-with\",2,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);return!b.lastIndexOf(a,0)},2);T(\"string\",3,!1,!0,function(a,b){return b?R(b,a):G(a.a)},0,1);\nT(\"string-length\",1,!1,!0,function(a,b){return(b?R(b,a):G(a.a)).length},0,1);T(\"substring\",3,!1,!1,function(a,b,c,d){c=Q(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return\"\";d=d?Q(d,a):Infinity;if(isNaN(d)||-Infinity===d)return\"\";c=Math.round(c)-1;var e=Math.max(c,0);a=R(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);T(\"substring-after\",3,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);c=b.indexOf(a);return-1==c?\"\":b.substring(c+a.length)},2);\nT(\"substring-before\",3,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);a=b.indexOf(a);return-1==a?\"\":b.substring(0,a)},2);T(\"sum\",1,!1,!1,function(a,b){a=L(b.a(a));b=0;for(var c=N(a);c;c=N(a))b+=+G(c);return b},1,1,!0);T(\"translate\",3,!1,!1,function(a,b,c,d){b=R(b,a);c=R(c,a);var e=R(d,a);d={};for(var f=0;fa.length)throw Error(\"Unclosed literal string\");return new Xb(a)}\nfunction uc(a){var b=[];if(cc(t(a.a))){var c=u(a.a);var d=t(a.a);if(\"/\"==c&&(ua(a.a)||\".\"!=d&&\"..\"!=d&&\"@\"!=d&&\"*\"!=d&&!/(?![0-9])[\\w]/.test(d)))return new ac;d=new ac;W(a,\"Missing next location step.\");c=vc(a,c);b.push(c)}else{a:{c=t(a.a);d=c.charAt(0);switch(d){case \"$\":throw Error(\"Variable reference not allowed in HTML XPath\");case \"(\":u(a.a);c=pc(a);W(a,'unclosed \"(\"');rc(a,\")\");break;case '\"':case \"'\":c=tc(a);break;default:if(isNaN(+c))if(!xa(c)&&/(?![0-9])[\\w]/.test(d)&&\"(\"==t(a.a,1)){c=u(a.a);\nc=Wb[c]||null;u(a.a);for(d=[];\")\"!=t(a.a);){W(a,\"Missing function argument list.\");d.push(pc(a));if(\",\"!=t(a.a))break;u(a.a)}W(a,\"Unclosed function argument list.\");sc(a);c=new Ub(c,d)}else{c=null;break a}else c=new Yb(+u(a.a))}\"[\"==t(a.a)&&(d=new fc(wc(a)),c=new Sb(c,d))}if(c)if(cc(t(a.a)))d=c;else return c;else c=vc(a,\"/\"),d=new bc,b.push(c)}for(;cc(t(a.a));)c=u(a.a),W(a,\"Missing next location step.\"),c=vc(a,c),b.push(c);return new Zb(d,b)}\nfunction vc(a,b){if(\"/\"!=b&&\"//\"!=b)throw Error('Step op should be \"/\" or \"//\"');if(\".\"==t(a.a)){var c=new U(nc,new y(\"node\"));u(a.a);return c}if(\"..\"==t(a.a))return c=new U(mc,new y(\"node\")),u(a.a),c;if(\"@\"==t(a.a)){var d=$b;u(a.a);W(a,\"Missing attribute name\")}else if(\"::\"==t(a.a,1)){if(!/(?![0-9])[\\w]/.test(t(a.a).charAt(0)))throw Error(\"Bad token: \"+u(a.a));var e=u(a.a);d=lc[e]||null;if(!d)throw Error(\"No axis with name: \"+e);u(a.a);W(a,\"Missing node name\")}else d=ic;e=t(a.a);if(/(?![0-9])[\\w\\*]/.test(e.charAt(0)))if(\"(\"==\nt(a.a,1)){if(!xa(e))throw Error(\"Invalid node type: \"+e);e=u(a.a);if(!xa(e))throw Error(\"Invalid type name: \"+e);rc(a,\"(\");W(a,\"Bad nodetype\");var f=t(a.a).charAt(0),g=null;if('\"'==f||\"'\"==f)g=tc(a);W(a,\"Bad nodetype\");sc(a);e=new y(e,g)}else if(e=u(a.a),f=e.indexOf(\":\"),-1==f)e=new ya(e);else{var g=e.substring(0,f);if(\"*\"==g)var h=\"*\";else if(h=a.b(g),!h)throw Error(\"Namespace prefix not declared: \"+g);e=e.substr(f+1);e=new ya(e,h)}else throw Error(\"Bad token: \"+u(a.a));a=new fc(wc(a),d.s);return c||\nnew U(d,e,a,\"//\"==b)}function wc(a){for(var b=[];\"[\"==t(a.a);){u(a.a);W(a,\"Missing predicate expression.\");var c=pc(a);b.push(c);W(a,\"Unclosed predicate expression.\");rc(a,\"]\")}return b}function qc(a){if(\"-\"==t(a.a))return u(a.a),new gc(qc(a));var b=uc(a);if(\"|\"!=t(a.a))a=b;else{for(b=[b];\"|\"==u(a.a);)W(a,\"Missing next union location path.\"),b.push(uc(a));a.a.a--;a=new hc(b)}return a};function xc(a,b){if(!a.length)throw Error(\"Empty XPath expression.\");a=ra(a);if(ua(a))throw Error(\"Invalid XPath expression.\");b?\"function\"==ba(b)||(b=ea(b.lookupNamespaceURI,b)):b=function(){return null};var c=pc(new oc(a,b));if(!ua(a))throw Error(\"Bad token: \"+u(a));this.evaluate=function(a,b){a=c.a(new pa(a));return new X(a,b)}}\nfunction X(a,b){if(!b)if(a instanceof I)b=4;else if(\"string\"==typeof a)b=2;else if(\"number\"==typeof a)b=1;else if(\"boolean\"==typeof a)b=3;else throw Error(\"Unexpected evaluation result.\");if(2!=b&&1!=b&&3!=b&&!(a instanceof I))throw Error(\"value could not be converted to the specified type\");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof I?Ib(a):\"\"+a;break;case 1:this.numberValue=a instanceof I?+Ib(a):+a;break;case 3:this.booleanValue=a instanceof I?0=d.length?null:d[f++]};this.snapshotItem=function(a){if(6!=b&&7!=b)throw Error(\"snapshotItem called with wrong result type\");return a>=d.length||\n0>a?null:d[a]}}X.ANY_TYPE=0;X.NUMBER_TYPE=1;X.STRING_TYPE=2;X.BOOLEAN_TYPE=3;X.UNORDERED_NODE_ITERATOR_TYPE=4;X.ORDERED_NODE_ITERATOR_TYPE=5;X.UNORDERED_NODE_SNAPSHOT_TYPE=6;X.ORDERED_NODE_SNAPSHOT_TYPE=7;X.ANY_UNORDERED_NODE_TYPE=8;X.FIRST_ORDERED_NODE_TYPE=9;function yc(a){this.lookupNamespaceURI=za(a)}\nfunction zc(a,b){a=a||k;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=X,c.evaluate=function(a,b,c,g){return(new xc(a,c)).evaluate(b,g)},c.createExpression=function(a,b){return new xc(a,b)},c.createNSResolver=function(a){return new yc(a)}}aa(\"wgxpath.install\",zc);var Ac=function(){var a={M:\"http://www.w3.org/2000/svg\"};return function(b){return a[b]||null}}();\nfunction Bc(a,b){var c=F(a);if(!c.documentElement)return null;(B||hb)&&zc(c?c.parentWindow||c.defaultView:window);try{var d=c.createNSResolver?c.createNSResolver(c.documentElement):Ac;if(B&&!ab(7))return c.evaluate.call(c,b,a,d,9,null);if(!B||9<=Number(bb)){for(var e={},f=c.getElementsByTagName(\"*\"),g=0;g=b&&0<=c&&255>=c&&0<=d&&255>=d&&0<=e&&1>=e)){b=[b,c,d,e];break b}b=null}if(!b)b:{if(d=a.match(Pa))if(b=\nNumber(d[1]),c=Number(d[2]),d=Number(d[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d){b=[b,c,d,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=ia[b.toLowerCase()];if(!c&&(c=\"#\"==b.charAt(0)?b:\"#\"+b,4==c.length&&(c=c.replace(Ma,\"#$1$1$2$2$3$3\")),!Na.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?\"rgba(\"+b.join(\", \")+\")\":a}return a}\nfunction Fc(a,b){var c=a.currentStyle||a.style,d=c[b];!l(d)&&\"function\"==ba(c.getPropertyValue)&&(d=c.getPropertyValue(b));return\"inherit\"!=d?l(d)?d:null:(a=Ec(a))?Fc(a,b):null}\nfunction Gc(a,b,c){function d(a){var b=Hc(a);return 0=C.a+C.width;C=e.c>=C.b+C.height;if(M&&\"hidden\"==n.x||C&&\"hidden\"==n.y)return Z;if(M&&\"visible\"!=n.x||C&&\"visible\"!=n.y){if(w&&(n=d(a),e.f>=g.scrollWidth-n.x||e.a>=g.scrollHeight-n.y))return Z;e=Ic(a);return e==Z?Z:\"scroll\"}}}return\"none\"}\nfunction Hc(a){var b=Jc(a);if(b)return b.rect;if(K(a,\"HTML\"))return a=F(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a=\"CSS1Compat\"==a.compatMode?a.documentElement:a.body,a=new ja(a.clientWidth,a.clientHeight),new E(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(d){return new E(0,0,0,0)}b=new E(c.left,c.top,c.right-c.left,c.bottom-c.top);B&&a.ownerDocument.body&&(a=F(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);\nreturn b}function Jc(a){var b=K(a,\"MAP\");if(!b&&!K(a,\"AREA\"))return null;var c=b?a:K(a.parentNode,\"MAP\")?a.parentNode:null,d=null,e=null;c&&c.name&&(d=Cc('/descendant::*[@usemap = \"#'+c.name+'\"]',F(c)))&&(e=Hc(d),b||\"default\"==a.shape.toLowerCase()||(a=Mc(a),b=Math.min(Math.max(a.a,0),e.width),c=Math.min(Math.max(a.b,0),e.height),e=new E(b+e.a,c+e.b,Math.min(a.width,e.width-b),Math.min(a.height,e.height-c))));return{B:d,rect:e||new E(0,0,0,0)}}\nfunction Mc(a){var b=a.shape.toLowerCase();a=a.coords.split(\",\");if(\"rect\"==b&&4==a.length){var b=a[0],c=a[1];return new E(b,c,a[2]-b,a[3]-c)}if(\"circle\"==b&&3==a.length)return b=a[2],new E(a[0]-b,a[1]-b,2*b,2*b);if(\"poly\"==b&&2b||a.indexOf(\"Error\",b)!=b)a+=\"Error\";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||\"\"}p(ga,Error);var ha=\"unknown error\",q={15:\"element not selectable\",11:\"element not visible\"};q[31]=ha;q[30]=ha;q[24]=\"invalid cookie domain\";q[29]=\"invalid element coordinates\";q[12]=\"invalid element state\";\nq[32]=\"invalid selector\";q[51]=\"invalid selector\";q[52]=\"invalid selector\";q[17]=\"javascript error\";q[405]=\"unsupported operation\";q[34]=\"move target out of bounds\";q[27]=\"no such alert\";q[7]=\"no such element\";q[8]=\"no such frame\";q[23]=\"no such window\";q[28]=\"script timeout\";q[33]=\"session not created\";q[10]=\"stale element reference\";q[21]=\"timeout\";q[25]=\"unable to set cookie\";q[26]=\"unexpected alert open\";q[13]=ha;q[9]=\"unknown command\";ga.prototype.toString=function(){return this.name+\": \"+this.message};var ia={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgreen:\"#006400\",\ndarkgrey:\"#a9a9a9\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",\nghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",grey:\"#808080\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgreen:\"#90ee90\",lightgrey:\"#d3d3d3\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",\nlightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",\nmoccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",\nseashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"};function ja(a,b){this.width=a;this.height=b}ja.prototype.toString=function(){return\"(\"+this.width+\" x \"+this.height+\")\"};ja.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};ja.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};ja.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function ka(a,b){var c=la;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var ma=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^[\\s\\xa0]+|[\\s\\xa0]+$/g,\"\")};function na(a,b){return ab?1:0}function oa(a){return String(a).replace(/\\-([a-z])/g,function(a,c){return c.toUpperCase()})};/*\n\n The MIT License\n\n Copyright (c) 2007 Cybozu Labs, Inc.\n Copyright (c) 2012 Google Inc.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to\n deal in the Software without restriction, including without limitation the\n rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n IN THE SOFTWARE.\n*/\nfunction pa(a,b,c){this.a=a;this.b=b||1;this.f=c||1};function qa(a){this.b=a;this.a=0}function ra(a){a=a.match(sa);for(var b=0;b]=|\\s+|./g,ta=/^\\s/;function t(a,b){return a.b[a.a+(b||0)]}function u(a){return a.b[a.a++]}function ua(a){return a.b.length<=a.a};var v;a:{var va=k.navigator;if(va){var wa=va.userAgent;if(wa){v=wa;break a}}v=\"\"}function x(a){return-1!=v.indexOf(a)};function y(a,b){this.h=a;this.c=l(b)?b:null;this.b=null;switch(a){case \"comment\":this.b=8;break;case \"text\":this.b=3;break;case \"processing-instruction\":this.b=7;break;case \"node\":break;default:throw Error(\"Unexpected argument\");}}function xa(a){return\"comment\"==a||\"text\"==a||\"processing-instruction\"==a||\"node\"==a}y.prototype.a=function(a){return null===this.b||this.b==a.nodeType};y.prototype.f=function(){return this.h};\ny.prototype.toString=function(){var a=\"Kind Test: \"+this.h;null===this.c||(a+=z(this.c));return a};function ya(a,b){this.j=a.toLowerCase();a=\"*\"==this.j?\"*\":\"http://www.w3.org/1999/xhtml\";this.c=b?b.toLowerCase():a}ya.prototype.a=function(a){var b=a.nodeType;if(1!=b&&2!=b)return!1;b=l(a.localName)?a.localName:a.nodeName;return\"*\"!=this.j&&this.j!=b.toLowerCase()?!1:\"*\"==this.c?!0:this.c==(a.namespaceURI?a.namespaceURI.toLowerCase():\"http://www.w3.org/1999/xhtml\")};ya.prototype.f=function(){return this.j};\nya.prototype.toString=function(){return\"Name Test: \"+(\"http://www.w3.org/1999/xhtml\"==this.c?\"\":this.c+\":\")+this.j};function za(a){switch(a.nodeType){case 1:return fa(Aa,a);case 9:return za(a.documentElement);case 11:case 10:case 6:case 12:return Ba;default:return a.parentNode?za(a.parentNode):Ba}}function Ba(){return null}function Aa(a,b){if(a.prefix==b)return a.namespaceURI||\"http://www.w3.org/1999/xhtml\";var c=a.getAttributeNode(\"xmlns:\"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?Aa(a.parentNode,b):null};function Ca(a,b){if(m(a))return m(b)&&1==b.length?a.indexOf(b,0):-1;for(var c=0;cb?null:m(a)?a.charAt(b):a[b]}function Ia(a){return Array.prototype.concat.apply([],arguments)}\nfunction Ja(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};function Ka(){return x(\"iPhone\")&&!x(\"iPod\")&&!x(\"iPad\")};var La=\"backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor\".split(\" \"),Ma=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,Na=/^#(?:[0-9a-f]{3}){1,2}$/i,Oa=/^(?:rgba)?\\((\\d{1,3}),\\s?(\\d{1,3}),\\s?(\\d{1,3}),\\s?(0|1|0\\.\\d*)\\)$/i,Pa=/^(?:rgb)?\\((0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2})\\)$/i;function Qa(){return(x(\"Chrome\")||x(\"CriOS\"))&&!x(\"Edge\")};function Ra(a,b){this.x=l(a)?a:0;this.y=l(b)?b:0}Ra.prototype.toString=function(){return\"(\"+this.x+\", \"+this.y+\")\"};Ra.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};Ra.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};Ra.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};var Sa=x(\"Opera\"),B=x(\"Trident\")||x(\"MSIE\"),Ta=x(\"Edge\"),Ua=x(\"Gecko\")&&!(-1!=v.toLowerCase().indexOf(\"webkit\")&&!x(\"Edge\"))&&!(x(\"Trident\")||x(\"MSIE\"))&&!x(\"Edge\"),Va=-1!=v.toLowerCase().indexOf(\"webkit\")&&!x(\"Edge\");function Wa(){var a=k.document;return a?a.documentMode:void 0}var Xa;\na:{var Ya=\"\",Za=function(){var a=v;if(Ua)return/rv\\:([^\\);]+)(\\)|;)/.exec(a);if(Ta)return/Edge\\/([\\d\\.]+)/.exec(a);if(B)return/\\b(?:MSIE|rv)[: ]([^\\);]+)(\\)|;)/.exec(a);if(Va)return/WebKit\\/(\\S+)/.exec(a);if(Sa)return/(?:Version)[ \\/]?(\\S+)/.exec(a)}();Za&&(Ya=Za?Za[1]:\"\");if(B){var $a=Wa();if(null!=$a&&$a>parseFloat(Ya)){Xa=String($a);break a}}Xa=Ya}var la={};\nfunction ab(a){return ka(a,function(){for(var b=0,c=ma(String(Xa)).split(\".\"),d=ma(String(a)).split(\".\"),e=Math.max(c.length,d.length),f=0;!b&&f\",4,2,function(a,b,c){return Pb(function(a,b){return a>b},a,b,c)});S(\"<=\",4,2,function(a,b,c){return Pb(function(a,b){return a<=b},a,b,c)});S(\">=\",4,2,function(a,b,c){return Pb(function(a,b){return a>=b},a,b,c)});var Ob=S(\"=\",3,2,function(a,b,c){return Pb(function(a,b){return a==b},a,b,c,!0)});S(\"!=\",3,2,function(a,b,c){return Pb(function(a,b){return a!=b},a,b,c,!0)});S(\"and\",2,2,function(a,b,c){return Mb(a,c)&&Mb(b,c)});S(\"or\",1,2,function(a,b,c){return Mb(a,c)||Mb(b,c)});function Sb(a,b){if(b.a.length&&4!=a.i)throw Error(\"Primary expression must evaluate to nodeset if filter has predicate(s).\");O.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}p(Sb,O);Sb.prototype.a=function(a){a=this.c.a(a);return Tb(this.h,a)};Sb.prototype.toString=function(){var a=\"Filter:\"+z(this.c);return a+=z(this.h)};function Ub(a,b){if(b.lengtha.A)throw Error(\"Function \"+a.j+\" expects at most \"+a.A+\" arguments, \"+b.length+\" given\");a.H&&A(b,function(b,d){if(4!=b.i)throw Error(\"Argument \"+d+\" to function \"+a.j+\" is not of type Nodeset: \"+b);});O.call(this,a.i);this.v=a;this.c=b;Kb(this,a.g||Fa(b,function(a){return a.g}));Lb(this,a.G&&!b.length||a.F&&!!b.length||Fa(b,function(a){return a.b}))}\np(Ub,O);Ub.prototype.a=function(a){return this.v.m.apply(null,Ia(a,this.c))};Ub.prototype.toString=function(){var a=\"Function: \"+this.v;if(this.c.length)var b=Ea(this.c,function(a,b){return a+z(b)},\"Arguments:\"),a=a+z(b);return a};function Vb(a,b,c,d,e,f,g,h,r){this.j=a;this.i=b;this.g=c;this.G=d;this.F=e;this.m=f;this.C=g;this.A=l(h)?h:g;this.H=!!r}Vb.prototype.toString=function(){return this.j};var Wb={};\nfunction T(a,b,c,d,e,f,g,h){if(Wb.hasOwnProperty(a))throw Error(\"Function already created: \"+a+\".\");Wb[a]=new Vb(a,b,c,d,!1,e,f,g,h)}T(\"boolean\",2,!1,!1,function(a,b){return Mb(b,a)},1);T(\"ceiling\",1,!1,!1,function(a,b){return Math.ceil(Q(b,a))},1);T(\"concat\",3,!1,!1,function(a,b){return Ea(Ja(arguments,1),function(b,d){return b+R(d,a)},\"\")},2,null);T(\"contains\",2,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);return-1!=b.indexOf(a)},2);T(\"count\",1,!1,!1,function(a,b){return b.a(a).l},1,1,!0);\nT(\"false\",2,!1,!1,function(){return!1},0);T(\"floor\",1,!1,!1,function(a,b){return Math.floor(Q(b,a))},1);T(\"id\",4,!1,!1,function(a,b){function c(a){if(D){var b=e.all[a];if(b){if(b.nodeType&&a==b.id)return b;if(b.length)return Ha(b,function(b){return a==b.id})}return null}return e.getElementById(a)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=R(b,a).split(/\\s+/);var f=[];A(a,function(a){a=c(a);!a||0<=Ca(f,a)||f.push(a)});f.sort(sb);var g=new I;A(f,function(a){J(g,a)});return g},1);\nT(\"lang\",2,!1,!1,function(){return!1},1);T(\"last\",1,!0,!1,function(a){if(1!=arguments.length)throw Error(\"Function last expects ()\");return a.f},0);T(\"local-name\",3,!1,!0,function(a,b){return(a=b?Hb(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():\"\"},0,1,!0);T(\"name\",3,!1,!0,function(a,b){return(a=b?Hb(b.a(a)):a.a)?a.nodeName.toLowerCase():\"\"},0,1,!0);T(\"namespace-uri\",3,!0,!1,function(){return\"\"},0,1,!0);\nT(\"normalize-space\",3,!1,!0,function(a,b){return(b?R(b,a):G(a.a)).replace(/[\\s\\xa0]+/g,\" \").replace(/^\\s+|\\s+$/g,\"\")},0,1);T(\"not\",2,!1,!1,function(a,b){return!Mb(b,a)},1);T(\"number\",1,!1,!0,function(a,b){return b?Q(b,a):+G(a.a)},0,1);T(\"position\",1,!0,!1,function(a){return a.b},0);T(\"round\",1,!1,!1,function(a,b){return Math.round(Q(b,a))},1);T(\"starts-with\",2,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);return!b.lastIndexOf(a,0)},2);T(\"string\",3,!1,!0,function(a,b){return b?R(b,a):G(a.a)},0,1);\nT(\"string-length\",1,!1,!0,function(a,b){return(b?R(b,a):G(a.a)).length},0,1);T(\"substring\",3,!1,!1,function(a,b,c,d){c=Q(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return\"\";d=d?Q(d,a):Infinity;if(isNaN(d)||-Infinity===d)return\"\";c=Math.round(c)-1;var e=Math.max(c,0);a=R(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);T(\"substring-after\",3,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);c=b.indexOf(a);return-1==c?\"\":b.substring(c+a.length)},2);\nT(\"substring-before\",3,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);a=b.indexOf(a);return-1==a?\"\":b.substring(0,a)},2);T(\"sum\",1,!1,!1,function(a,b){a=L(b.a(a));b=0;for(var c=N(a);c;c=N(a))b+=+G(c);return b},1,1,!0);T(\"translate\",3,!1,!1,function(a,b,c,d){b=R(b,a);c=R(c,a);var e=R(d,a);d={};for(var f=0;fa.length)throw Error(\"Unclosed literal string\");return new Xb(a)}\nfunction uc(a){var b=[];if(cc(t(a.a))){var c=u(a.a);var d=t(a.a);if(\"/\"==c&&(ua(a.a)||\".\"!=d&&\"..\"!=d&&\"@\"!=d&&\"*\"!=d&&!/(?![0-9])[\\w]/.test(d)))return new ac;d=new ac;W(a,\"Missing next location step.\");c=vc(a,c);b.push(c)}else{a:{c=t(a.a);d=c.charAt(0);switch(d){case \"$\":throw Error(\"Variable reference not allowed in HTML XPath\");case \"(\":u(a.a);c=pc(a);W(a,'unclosed \"(\"');rc(a,\")\");break;case '\"':case \"'\":c=tc(a);break;default:if(isNaN(+c))if(!xa(c)&&/(?![0-9])[\\w]/.test(d)&&\"(\"==t(a.a,1)){c=u(a.a);\nc=Wb[c]||null;u(a.a);for(d=[];\")\"!=t(a.a);){W(a,\"Missing function argument list.\");d.push(pc(a));if(\",\"!=t(a.a))break;u(a.a)}W(a,\"Unclosed function argument list.\");sc(a);c=new Ub(c,d)}else{c=null;break a}else c=new Yb(+u(a.a))}\"[\"==t(a.a)&&(d=new fc(wc(a)),c=new Sb(c,d))}if(c)if(cc(t(a.a)))d=c;else return c;else c=vc(a,\"/\"),d=new bc,b.push(c)}for(;cc(t(a.a));)c=u(a.a),W(a,\"Missing next location step.\"),c=vc(a,c),b.push(c);return new Zb(d,b)}\nfunction vc(a,b){if(\"/\"!=b&&\"//\"!=b)throw Error('Step op should be \"/\" or \"//\"');if(\".\"==t(a.a)){var c=new U(nc,new y(\"node\"));u(a.a);return c}if(\"..\"==t(a.a))return c=new U(mc,new y(\"node\")),u(a.a),c;if(\"@\"==t(a.a)){var d=$b;u(a.a);W(a,\"Missing attribute name\")}else if(\"::\"==t(a.a,1)){if(!/(?![0-9])[\\w]/.test(t(a.a).charAt(0)))throw Error(\"Bad token: \"+u(a.a));var e=u(a.a);d=lc[e]||null;if(!d)throw Error(\"No axis with name: \"+e);u(a.a);W(a,\"Missing node name\")}else d=ic;e=t(a.a);if(/(?![0-9])[\\w\\*]/.test(e.charAt(0)))if(\"(\"==\nt(a.a,1)){if(!xa(e))throw Error(\"Invalid node type: \"+e);e=u(a.a);if(!xa(e))throw Error(\"Invalid type name: \"+e);rc(a,\"(\");W(a,\"Bad nodetype\");var f=t(a.a).charAt(0),g=null;if('\"'==f||\"'\"==f)g=tc(a);W(a,\"Bad nodetype\");sc(a);e=new y(e,g)}else if(e=u(a.a),f=e.indexOf(\":\"),-1==f)e=new ya(e);else{var g=e.substring(0,f);if(\"*\"==g)var h=\"*\";else if(h=a.b(g),!h)throw Error(\"Namespace prefix not declared: \"+g);e=e.substr(f+1);e=new ya(e,h)}else throw Error(\"Bad token: \"+u(a.a));a=new fc(wc(a),d.s);return c||\nnew U(d,e,a,\"//\"==b)}function wc(a){for(var b=[];\"[\"==t(a.a);){u(a.a);W(a,\"Missing predicate expression.\");var c=pc(a);b.push(c);W(a,\"Unclosed predicate expression.\");rc(a,\"]\")}return b}function qc(a){if(\"-\"==t(a.a))return u(a.a),new gc(qc(a));var b=uc(a);if(\"|\"!=t(a.a))a=b;else{for(b=[b];\"|\"==u(a.a);)W(a,\"Missing next union location path.\"),b.push(uc(a));a.a.a--;a=new hc(b)}return a};function xc(a,b){if(!a.length)throw Error(\"Empty XPath expression.\");a=ra(a);if(ua(a))throw Error(\"Invalid XPath expression.\");b?\"function\"==ba(b)||(b=ea(b.lookupNamespaceURI,b)):b=function(){return null};var c=pc(new oc(a,b));if(!ua(a))throw Error(\"Bad token: \"+u(a));this.evaluate=function(a,b){a=c.a(new pa(a));return new X(a,b)}}\nfunction X(a,b){if(!b)if(a instanceof I)b=4;else if(\"string\"==typeof a)b=2;else if(\"number\"==typeof a)b=1;else if(\"boolean\"==typeof a)b=3;else throw Error(\"Unexpected evaluation result.\");if(2!=b&&1!=b&&3!=b&&!(a instanceof I))throw Error(\"value could not be converted to the specified type\");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof I?Ib(a):\"\"+a;break;case 1:this.numberValue=a instanceof I?+Ib(a):+a;break;case 3:this.booleanValue=a instanceof I?0=d.length?null:d[f++]};this.snapshotItem=function(a){if(6!=b&&7!=b)throw Error(\"snapshotItem called with wrong result type\");return a>=d.length||\n0>a?null:d[a]}}X.ANY_TYPE=0;X.NUMBER_TYPE=1;X.STRING_TYPE=2;X.BOOLEAN_TYPE=3;X.UNORDERED_NODE_ITERATOR_TYPE=4;X.ORDERED_NODE_ITERATOR_TYPE=5;X.UNORDERED_NODE_SNAPSHOT_TYPE=6;X.ORDERED_NODE_SNAPSHOT_TYPE=7;X.ANY_UNORDERED_NODE_TYPE=8;X.FIRST_ORDERED_NODE_TYPE=9;function yc(a){this.lookupNamespaceURI=za(a)}\nfunction zc(a,b){a=a||k;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=X,c.evaluate=function(a,b,c,g){return(new xc(a,c)).evaluate(b,g)},c.createExpression=function(a,b){return new xc(a,b)},c.createNSResolver=function(a){return new yc(a)}}aa(\"wgxpath.install\",zc);var Ac=function(){var a={M:\"http://www.w3.org/2000/svg\"};return function(b){return a[b]||null}}();\nfunction Bc(a,b){var c=F(a);if(!c.documentElement)return null;(B||hb)&&zc(c?c.parentWindow||c.defaultView:window);try{var d=c.createNSResolver?c.createNSResolver(c.documentElement):Ac;if(B&&!ab(7))return c.evaluate.call(c,b,a,d,9,null);if(!B||9<=Number(bb)){for(var e={},f=c.getElementsByTagName(\"*\"),g=0;g=b&&0<=c&&255>=c&&0<=d&&255>=d&&0<=e&&1>=e)){b=[b,c,d,e];break b}b=null}if(!b)b:{if(d=a.match(Pa))if(b=\nNumber(d[1]),c=Number(d[2]),d=Number(d[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d){b=[b,c,d,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=ia[b.toLowerCase()];if(!c&&(c=\"#\"==b.charAt(0)?b:\"#\"+b,4==c.length&&(c=c.replace(Ma,\"#$1$1$2$2$3$3\")),!Na.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?\"rgba(\"+b.join(\", \")+\")\":a}return a}\nfunction Fc(a,b){var c=a.currentStyle||a.style,d=c[b];!l(d)&&\"function\"==ba(c.getPropertyValue)&&(d=c.getPropertyValue(b));return\"inherit\"!=d?l(d)?d:null:(a=Ec(a))?Fc(a,b):null}\nfunction Gc(a,b,c){function d(a){var b=Hc(a);return 0=C.a+C.width;C=e.c>=C.b+C.height;if(M&&\"hidden\"==n.x||C&&\"hidden\"==n.y)return Z;if(M&&\"visible\"!=n.x||C&&\"visible\"!=n.y){if(w&&(n=d(a),e.f>=g.scrollWidth-n.x||e.a>=g.scrollHeight-n.y))return Z;e=Ic(a);return e==Z?Z:\"scroll\"}}}return\"none\"}\nfunction Hc(a){var b=Jc(a);if(b)return b.rect;if(K(a,\"HTML\"))return a=F(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a=\"CSS1Compat\"==a.compatMode?a.documentElement:a.body,a=new ja(a.clientWidth,a.clientHeight),new E(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(d){return new E(0,0,0,0)}b=new E(c.left,c.top,c.right-c.left,c.bottom-c.top);B&&a.ownerDocument.body&&(a=F(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);\nreturn b}function Jc(a){var b=K(a,\"MAP\");if(!b&&!K(a,\"AREA\"))return null;var c=b?a:K(a.parentNode,\"MAP\")?a.parentNode:null,d=null,e=null;c&&c.name&&(d=Cc('/descendant::*[@usemap = \"#'+c.name+'\"]',F(c)))&&(e=Hc(d),b||\"default\"==a.shape.toLowerCase()||(a=Mc(a),b=Math.min(Math.max(a.a,0),e.width),c=Math.min(Math.max(a.b,0),e.height),e=new E(b+e.a,c+e.b,Math.min(a.width,e.width-b),Math.min(a.height,e.height-c))));return{B:d,rect:e||new E(0,0,0,0)}}\nfunction Mc(a){var b=a.shape.toLowerCase();a=a.coords.split(\",\");if(\"rect\"==b&&4==a.length){var b=a[0],c=a[1];return new E(b,c,a[2]-b,a[3]-c)}if(\"circle\"==b&&3==a.length)return b=a[2],new E(a[0]-b,a[1]-b,2*b,2*b);if(\"poly\"==b&&2b||a.indexOf(\"Error\",b)!=b)a+=\"Error\";this.name=a;a=Error(this.message);a.name=this.name;this.stack=a.stack||\"\"}p(ga,Error);var ha=\"unknown error\",q={15:\"element not selectable\",11:\"element not visible\"};q[31]=ha;q[30]=ha;q[24]=\"invalid cookie domain\";q[29]=\"invalid element coordinates\";q[12]=\"invalid element state\";\nq[32]=\"invalid selector\";q[51]=\"invalid selector\";q[52]=\"invalid selector\";q[17]=\"javascript error\";q[405]=\"unsupported operation\";q[34]=\"move target out of bounds\";q[27]=\"no such alert\";q[7]=\"no such element\";q[8]=\"no such frame\";q[23]=\"no such window\";q[28]=\"script timeout\";q[33]=\"session not created\";q[10]=\"stale element reference\";q[21]=\"timeout\";q[25]=\"unable to set cookie\";q[26]=\"unexpected alert open\";q[13]=ha;q[9]=\"unknown command\";ga.prototype.toString=function(){return this.name+\": \"+this.message};var ia={aliceblue:\"#f0f8ff\",antiquewhite:\"#faebd7\",aqua:\"#00ffff\",aquamarine:\"#7fffd4\",azure:\"#f0ffff\",beige:\"#f5f5dc\",bisque:\"#ffe4c4\",black:\"#000000\",blanchedalmond:\"#ffebcd\",blue:\"#0000ff\",blueviolet:\"#8a2be2\",brown:\"#a52a2a\",burlywood:\"#deb887\",cadetblue:\"#5f9ea0\",chartreuse:\"#7fff00\",chocolate:\"#d2691e\",coral:\"#ff7f50\",cornflowerblue:\"#6495ed\",cornsilk:\"#fff8dc\",crimson:\"#dc143c\",cyan:\"#00ffff\",darkblue:\"#00008b\",darkcyan:\"#008b8b\",darkgoldenrod:\"#b8860b\",darkgray:\"#a9a9a9\",darkgreen:\"#006400\",\ndarkgrey:\"#a9a9a9\",darkkhaki:\"#bdb76b\",darkmagenta:\"#8b008b\",darkolivegreen:\"#556b2f\",darkorange:\"#ff8c00\",darkorchid:\"#9932cc\",darkred:\"#8b0000\",darksalmon:\"#e9967a\",darkseagreen:\"#8fbc8f\",darkslateblue:\"#483d8b\",darkslategray:\"#2f4f4f\",darkslategrey:\"#2f4f4f\",darkturquoise:\"#00ced1\",darkviolet:\"#9400d3\",deeppink:\"#ff1493\",deepskyblue:\"#00bfff\",dimgray:\"#696969\",dimgrey:\"#696969\",dodgerblue:\"#1e90ff\",firebrick:\"#b22222\",floralwhite:\"#fffaf0\",forestgreen:\"#228b22\",fuchsia:\"#ff00ff\",gainsboro:\"#dcdcdc\",\nghostwhite:\"#f8f8ff\",gold:\"#ffd700\",goldenrod:\"#daa520\",gray:\"#808080\",green:\"#008000\",greenyellow:\"#adff2f\",grey:\"#808080\",honeydew:\"#f0fff0\",hotpink:\"#ff69b4\",indianred:\"#cd5c5c\",indigo:\"#4b0082\",ivory:\"#fffff0\",khaki:\"#f0e68c\",lavender:\"#e6e6fa\",lavenderblush:\"#fff0f5\",lawngreen:\"#7cfc00\",lemonchiffon:\"#fffacd\",lightblue:\"#add8e6\",lightcoral:\"#f08080\",lightcyan:\"#e0ffff\",lightgoldenrodyellow:\"#fafad2\",lightgray:\"#d3d3d3\",lightgreen:\"#90ee90\",lightgrey:\"#d3d3d3\",lightpink:\"#ffb6c1\",lightsalmon:\"#ffa07a\",\nlightseagreen:\"#20b2aa\",lightskyblue:\"#87cefa\",lightslategray:\"#778899\",lightslategrey:\"#778899\",lightsteelblue:\"#b0c4de\",lightyellow:\"#ffffe0\",lime:\"#00ff00\",limegreen:\"#32cd32\",linen:\"#faf0e6\",magenta:\"#ff00ff\",maroon:\"#800000\",mediumaquamarine:\"#66cdaa\",mediumblue:\"#0000cd\",mediumorchid:\"#ba55d3\",mediumpurple:\"#9370db\",mediumseagreen:\"#3cb371\",mediumslateblue:\"#7b68ee\",mediumspringgreen:\"#00fa9a\",mediumturquoise:\"#48d1cc\",mediumvioletred:\"#c71585\",midnightblue:\"#191970\",mintcream:\"#f5fffa\",mistyrose:\"#ffe4e1\",\nmoccasin:\"#ffe4b5\",navajowhite:\"#ffdead\",navy:\"#000080\",oldlace:\"#fdf5e6\",olive:\"#808000\",olivedrab:\"#6b8e23\",orange:\"#ffa500\",orangered:\"#ff4500\",orchid:\"#da70d6\",palegoldenrod:\"#eee8aa\",palegreen:\"#98fb98\",paleturquoise:\"#afeeee\",palevioletred:\"#db7093\",papayawhip:\"#ffefd5\",peachpuff:\"#ffdab9\",peru:\"#cd853f\",pink:\"#ffc0cb\",plum:\"#dda0dd\",powderblue:\"#b0e0e6\",purple:\"#800080\",red:\"#ff0000\",rosybrown:\"#bc8f8f\",royalblue:\"#4169e1\",saddlebrown:\"#8b4513\",salmon:\"#fa8072\",sandybrown:\"#f4a460\",seagreen:\"#2e8b57\",\nseashell:\"#fff5ee\",sienna:\"#a0522d\",silver:\"#c0c0c0\",skyblue:\"#87ceeb\",slateblue:\"#6a5acd\",slategray:\"#708090\",slategrey:\"#708090\",snow:\"#fffafa\",springgreen:\"#00ff7f\",steelblue:\"#4682b4\",tan:\"#d2b48c\",teal:\"#008080\",thistle:\"#d8bfd8\",tomato:\"#ff6347\",turquoise:\"#40e0d0\",violet:\"#ee82ee\",wheat:\"#f5deb3\",white:\"#ffffff\",whitesmoke:\"#f5f5f5\",yellow:\"#ffff00\",yellowgreen:\"#9acd32\"};function ja(a,b){this.width=a;this.height=b}ja.prototype.toString=function(){return\"(\"+this.width+\" x \"+this.height+\")\"};ja.prototype.ceil=function(){this.width=Math.ceil(this.width);this.height=Math.ceil(this.height);return this};ja.prototype.floor=function(){this.width=Math.floor(this.width);this.height=Math.floor(this.height);return this};ja.prototype.round=function(){this.width=Math.round(this.width);this.height=Math.round(this.height);return this};function ka(a,b){var c=la;return Object.prototype.hasOwnProperty.call(c,a)?c[a]:c[a]=b(a)};var ma=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^[\\s\\xa0]+|[\\s\\xa0]+$/g,\"\")};function na(a,b){return ab?1:0}function oa(a){return String(a).replace(/\\-([a-z])/g,function(a,c){return c.toUpperCase()})};/*\n\n The MIT License\n\n Copyright (c) 2007 Cybozu Labs, Inc.\n Copyright (c) 2012 Google Inc.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to\n deal in the Software without restriction, including without limitation the\n rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n IN THE SOFTWARE.\n*/\nfunction pa(a,b,c){this.a=a;this.b=b||1;this.f=c||1};function qa(a){this.b=a;this.a=0}function ra(a){a=a.match(sa);for(var b=0;b]=|\\s+|./g,ta=/^\\s/;function t(a,b){return a.b[a.a+(b||0)]}function u(a){return a.b[a.a++]}function ua(a){return a.b.length<=a.a};var v;a:{var va=k.navigator;if(va){var wa=va.userAgent;if(wa){v=wa;break a}}v=\"\"}function x(a){return-1!=v.indexOf(a)};function y(a,b){this.h=a;this.c=l(b)?b:null;this.b=null;switch(a){case \"comment\":this.b=8;break;case \"text\":this.b=3;break;case \"processing-instruction\":this.b=7;break;case \"node\":break;default:throw Error(\"Unexpected argument\");}}function xa(a){return\"comment\"==a||\"text\"==a||\"processing-instruction\"==a||\"node\"==a}y.prototype.a=function(a){return null===this.b||this.b==a.nodeType};y.prototype.f=function(){return this.h};\ny.prototype.toString=function(){var a=\"Kind Test: \"+this.h;null===this.c||(a+=z(this.c));return a};function ya(a,b){this.j=a.toLowerCase();a=\"*\"==this.j?\"*\":\"http://www.w3.org/1999/xhtml\";this.c=b?b.toLowerCase():a}ya.prototype.a=function(a){var b=a.nodeType;if(1!=b&&2!=b)return!1;b=l(a.localName)?a.localName:a.nodeName;return\"*\"!=this.j&&this.j!=b.toLowerCase()?!1:\"*\"==this.c?!0:this.c==(a.namespaceURI?a.namespaceURI.toLowerCase():\"http://www.w3.org/1999/xhtml\")};ya.prototype.f=function(){return this.j};\nya.prototype.toString=function(){return\"Name Test: \"+(\"http://www.w3.org/1999/xhtml\"==this.c?\"\":this.c+\":\")+this.j};function za(a){switch(a.nodeType){case 1:return fa(Aa,a);case 9:return za(a.documentElement);case 11:case 10:case 6:case 12:return Ba;default:return a.parentNode?za(a.parentNode):Ba}}function Ba(){return null}function Aa(a,b){if(a.prefix==b)return a.namespaceURI||\"http://www.w3.org/1999/xhtml\";var c=a.getAttributeNode(\"xmlns:\"+b);return c&&c.specified?c.value||null:a.parentNode&&9!=a.parentNode.nodeType?Aa(a.parentNode,b):null};function Ca(a,b){if(m(a))return m(b)&&1==b.length?a.indexOf(b,0):-1;for(var c=0;cb?null:m(a)?a.charAt(b):a[b]}function Ia(a){return Array.prototype.concat.apply([],arguments)}\nfunction Ja(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};function Ka(){return x(\"iPhone\")&&!x(\"iPod\")&&!x(\"iPad\")};var La=\"backgroundColor borderTopColor borderRightColor borderBottomColor borderLeftColor color outlineColor\".split(\" \"),Ma=/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])/,Na=/^#(?:[0-9a-f]{3}){1,2}$/i,Oa=/^(?:rgba)?\\((\\d{1,3}),\\s?(\\d{1,3}),\\s?(\\d{1,3}),\\s?(0|1|0\\.\\d*)\\)$/i,Pa=/^(?:rgb)?\\((0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2}),\\s?(0|[1-9]\\d{0,2})\\)$/i;function Qa(){return(x(\"Chrome\")||x(\"CriOS\"))&&!x(\"Edge\")};function Ra(a,b){this.x=l(a)?a:0;this.y=l(b)?b:0}Ra.prototype.toString=function(){return\"(\"+this.x+\", \"+this.y+\")\"};Ra.prototype.ceil=function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this};Ra.prototype.floor=function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this};Ra.prototype.round=function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this};var Sa=x(\"Opera\"),B=x(\"Trident\")||x(\"MSIE\"),Ta=x(\"Edge\"),Ua=x(\"Gecko\")&&!(-1!=v.toLowerCase().indexOf(\"webkit\")&&!x(\"Edge\"))&&!(x(\"Trident\")||x(\"MSIE\"))&&!x(\"Edge\"),Va=-1!=v.toLowerCase().indexOf(\"webkit\")&&!x(\"Edge\");function Wa(){var a=k.document;return a?a.documentMode:void 0}var Xa;\na:{var Ya=\"\",Za=function(){var a=v;if(Ua)return/rv\\:([^\\);]+)(\\)|;)/.exec(a);if(Ta)return/Edge\\/([\\d\\.]+)/.exec(a);if(B)return/\\b(?:MSIE|rv)[: ]([^\\);]+)(\\)|;)/.exec(a);if(Va)return/WebKit\\/(\\S+)/.exec(a);if(Sa)return/(?:Version)[ \\/]?(\\S+)/.exec(a)}();Za&&(Ya=Za?Za[1]:\"\");if(B){var $a=Wa();if(null!=$a&&$a>parseFloat(Ya)){Xa=String($a);break a}}Xa=Ya}var la={};\nfunction ab(a){return ka(a,function(){for(var b=0,c=ma(String(Xa)).split(\".\"),d=ma(String(a)).split(\".\"),e=Math.max(c.length,d.length),f=0;!b&&f\",4,2,function(a,b,c){return Pb(function(a,b){return a>b},a,b,c)});S(\"<=\",4,2,function(a,b,c){return Pb(function(a,b){return a<=b},a,b,c)});S(\">=\",4,2,function(a,b,c){return Pb(function(a,b){return a>=b},a,b,c)});var Ob=S(\"=\",3,2,function(a,b,c){return Pb(function(a,b){return a==b},a,b,c,!0)});S(\"!=\",3,2,function(a,b,c){return Pb(function(a,b){return a!=b},a,b,c,!0)});S(\"and\",2,2,function(a,b,c){return Mb(a,c)&&Mb(b,c)});S(\"or\",1,2,function(a,b,c){return Mb(a,c)||Mb(b,c)});function Sb(a,b){if(b.a.length&&4!=a.i)throw Error(\"Primary expression must evaluate to nodeset if filter has predicate(s).\");O.call(this,a.i);this.c=a;this.h=b;this.g=a.g;this.b=a.b}p(Sb,O);Sb.prototype.a=function(a){a=this.c.a(a);return Tb(this.h,a)};Sb.prototype.toString=function(){var a=\"Filter:\"+z(this.c);return a+=z(this.h)};function Ub(a,b){if(b.lengtha.A)throw Error(\"Function \"+a.j+\" expects at most \"+a.A+\" arguments, \"+b.length+\" given\");a.H&&A(b,function(b,d){if(4!=b.i)throw Error(\"Argument \"+d+\" to function \"+a.j+\" is not of type Nodeset: \"+b);});O.call(this,a.i);this.v=a;this.c=b;Kb(this,a.g||Fa(b,function(a){return a.g}));Lb(this,a.G&&!b.length||a.F&&!!b.length||Fa(b,function(a){return a.b}))}\np(Ub,O);Ub.prototype.a=function(a){return this.v.m.apply(null,Ia(a,this.c))};Ub.prototype.toString=function(){var a=\"Function: \"+this.v;if(this.c.length)var b=Ea(this.c,function(a,b){return a+z(b)},\"Arguments:\"),a=a+z(b);return a};function Vb(a,b,c,d,e,f,g,h,r){this.j=a;this.i=b;this.g=c;this.G=d;this.F=e;this.m=f;this.C=g;this.A=l(h)?h:g;this.H=!!r}Vb.prototype.toString=function(){return this.j};var Wb={};\nfunction T(a,b,c,d,e,f,g,h){if(Wb.hasOwnProperty(a))throw Error(\"Function already created: \"+a+\".\");Wb[a]=new Vb(a,b,c,d,!1,e,f,g,h)}T(\"boolean\",2,!1,!1,function(a,b){return Mb(b,a)},1);T(\"ceiling\",1,!1,!1,function(a,b){return Math.ceil(Q(b,a))},1);T(\"concat\",3,!1,!1,function(a,b){return Ea(Ja(arguments,1),function(b,d){return b+R(d,a)},\"\")},2,null);T(\"contains\",2,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);return-1!=b.indexOf(a)},2);T(\"count\",1,!1,!1,function(a,b){return b.a(a).l},1,1,!0);\nT(\"false\",2,!1,!1,function(){return!1},0);T(\"floor\",1,!1,!1,function(a,b){return Math.floor(Q(b,a))},1);T(\"id\",4,!1,!1,function(a,b){function c(a){if(D){var b=e.all[a];if(b){if(b.nodeType&&a==b.id)return b;if(b.length)return Ha(b,function(b){return a==b.id})}return null}return e.getElementById(a)}var d=a.a,e=9==d.nodeType?d:d.ownerDocument;a=R(b,a).split(/\\s+/);var f=[];A(a,function(a){a=c(a);!a||0<=Ca(f,a)||f.push(a)});f.sort(sb);var g=new I;A(f,function(a){J(g,a)});return g},1);\nT(\"lang\",2,!1,!1,function(){return!1},1);T(\"last\",1,!0,!1,function(a){if(1!=arguments.length)throw Error(\"Function last expects ()\");return a.f},0);T(\"local-name\",3,!1,!0,function(a,b){return(a=b?Hb(b.a(a)):a.a)?a.localName||a.nodeName.toLowerCase():\"\"},0,1,!0);T(\"name\",3,!1,!0,function(a,b){return(a=b?Hb(b.a(a)):a.a)?a.nodeName.toLowerCase():\"\"},0,1,!0);T(\"namespace-uri\",3,!0,!1,function(){return\"\"},0,1,!0);\nT(\"normalize-space\",3,!1,!0,function(a,b){return(b?R(b,a):G(a.a)).replace(/[\\s\\xa0]+/g,\" \").replace(/^\\s+|\\s+$/g,\"\")},0,1);T(\"not\",2,!1,!1,function(a,b){return!Mb(b,a)},1);T(\"number\",1,!1,!0,function(a,b){return b?Q(b,a):+G(a.a)},0,1);T(\"position\",1,!0,!1,function(a){return a.b},0);T(\"round\",1,!1,!1,function(a,b){return Math.round(Q(b,a))},1);T(\"starts-with\",2,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);return!b.lastIndexOf(a,0)},2);T(\"string\",3,!1,!0,function(a,b){return b?R(b,a):G(a.a)},0,1);\nT(\"string-length\",1,!1,!0,function(a,b){return(b?R(b,a):G(a.a)).length},0,1);T(\"substring\",3,!1,!1,function(a,b,c,d){c=Q(c,a);if(isNaN(c)||Infinity==c||-Infinity==c)return\"\";d=d?Q(d,a):Infinity;if(isNaN(d)||-Infinity===d)return\"\";c=Math.round(c)-1;var e=Math.max(c,0);a=R(b,a);return Infinity==d?a.substring(e):a.substring(e,c+Math.round(d))},2,3);T(\"substring-after\",3,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);c=b.indexOf(a);return-1==c?\"\":b.substring(c+a.length)},2);\nT(\"substring-before\",3,!1,!1,function(a,b,c){b=R(b,a);a=R(c,a);a=b.indexOf(a);return-1==a?\"\":b.substring(0,a)},2);T(\"sum\",1,!1,!1,function(a,b){a=L(b.a(a));b=0;for(var c=N(a);c;c=N(a))b+=+G(c);return b},1,1,!0);T(\"translate\",3,!1,!1,function(a,b,c,d){b=R(b,a);c=R(c,a);var e=R(d,a);d={};for(var f=0;fa.length)throw Error(\"Unclosed literal string\");return new Xb(a)}\nfunction uc(a){var b=[];if(cc(t(a.a))){var c=u(a.a);var d=t(a.a);if(\"/\"==c&&(ua(a.a)||\".\"!=d&&\"..\"!=d&&\"@\"!=d&&\"*\"!=d&&!/(?![0-9])[\\w]/.test(d)))return new ac;d=new ac;W(a,\"Missing next location step.\");c=vc(a,c);b.push(c)}else{a:{c=t(a.a);d=c.charAt(0);switch(d){case \"$\":throw Error(\"Variable reference not allowed in HTML XPath\");case \"(\":u(a.a);c=pc(a);W(a,'unclosed \"(\"');rc(a,\")\");break;case '\"':case \"'\":c=tc(a);break;default:if(isNaN(+c))if(!xa(c)&&/(?![0-9])[\\w]/.test(d)&&\"(\"==t(a.a,1)){c=u(a.a);\nc=Wb[c]||null;u(a.a);for(d=[];\")\"!=t(a.a);){W(a,\"Missing function argument list.\");d.push(pc(a));if(\",\"!=t(a.a))break;u(a.a)}W(a,\"Unclosed function argument list.\");sc(a);c=new Ub(c,d)}else{c=null;break a}else c=new Yb(+u(a.a))}\"[\"==t(a.a)&&(d=new fc(wc(a)),c=new Sb(c,d))}if(c)if(cc(t(a.a)))d=c;else return c;else c=vc(a,\"/\"),d=new bc,b.push(c)}for(;cc(t(a.a));)c=u(a.a),W(a,\"Missing next location step.\"),c=vc(a,c),b.push(c);return new Zb(d,b)}\nfunction vc(a,b){if(\"/\"!=b&&\"//\"!=b)throw Error('Step op should be \"/\" or \"//\"');if(\".\"==t(a.a)){var c=new U(nc,new y(\"node\"));u(a.a);return c}if(\"..\"==t(a.a))return c=new U(mc,new y(\"node\")),u(a.a),c;if(\"@\"==t(a.a)){var d=$b;u(a.a);W(a,\"Missing attribute name\")}else if(\"::\"==t(a.a,1)){if(!/(?![0-9])[\\w]/.test(t(a.a).charAt(0)))throw Error(\"Bad token: \"+u(a.a));var e=u(a.a);d=lc[e]||null;if(!d)throw Error(\"No axis with name: \"+e);u(a.a);W(a,\"Missing node name\")}else d=ic;e=t(a.a);if(/(?![0-9])[\\w\\*]/.test(e.charAt(0)))if(\"(\"==\nt(a.a,1)){if(!xa(e))throw Error(\"Invalid node type: \"+e);e=u(a.a);if(!xa(e))throw Error(\"Invalid type name: \"+e);rc(a,\"(\");W(a,\"Bad nodetype\");var f=t(a.a).charAt(0),g=null;if('\"'==f||\"'\"==f)g=tc(a);W(a,\"Bad nodetype\");sc(a);e=new y(e,g)}else if(e=u(a.a),f=e.indexOf(\":\"),-1==f)e=new ya(e);else{var g=e.substring(0,f);if(\"*\"==g)var h=\"*\";else if(h=a.b(g),!h)throw Error(\"Namespace prefix not declared: \"+g);e=e.substr(f+1);e=new ya(e,h)}else throw Error(\"Bad token: \"+u(a.a));a=new fc(wc(a),d.s);return c||\nnew U(d,e,a,\"//\"==b)}function wc(a){for(var b=[];\"[\"==t(a.a);){u(a.a);W(a,\"Missing predicate expression.\");var c=pc(a);b.push(c);W(a,\"Unclosed predicate expression.\");rc(a,\"]\")}return b}function qc(a){if(\"-\"==t(a.a))return u(a.a),new gc(qc(a));var b=uc(a);if(\"|\"!=t(a.a))a=b;else{for(b=[b];\"|\"==u(a.a);)W(a,\"Missing next union location path.\"),b.push(uc(a));a.a.a--;a=new hc(b)}return a};function xc(a,b){if(!a.length)throw Error(\"Empty XPath expression.\");a=ra(a);if(ua(a))throw Error(\"Invalid XPath expression.\");b?\"function\"==ba(b)||(b=ea(b.lookupNamespaceURI,b)):b=function(){return null};var c=pc(new oc(a,b));if(!ua(a))throw Error(\"Bad token: \"+u(a));this.evaluate=function(a,b){a=c.a(new pa(a));return new X(a,b)}}\nfunction X(a,b){if(!b)if(a instanceof I)b=4;else if(\"string\"==typeof a)b=2;else if(\"number\"==typeof a)b=1;else if(\"boolean\"==typeof a)b=3;else throw Error(\"Unexpected evaluation result.\");if(2!=b&&1!=b&&3!=b&&!(a instanceof I))throw Error(\"value could not be converted to the specified type\");this.resultType=b;switch(b){case 2:this.stringValue=a instanceof I?Ib(a):\"\"+a;break;case 1:this.numberValue=a instanceof I?+Ib(a):+a;break;case 3:this.booleanValue=a instanceof I?0=d.length?null:d[f++]};this.snapshotItem=function(a){if(6!=b&&7!=b)throw Error(\"snapshotItem called with wrong result type\");return a>=d.length||\n0>a?null:d[a]}}X.ANY_TYPE=0;X.NUMBER_TYPE=1;X.STRING_TYPE=2;X.BOOLEAN_TYPE=3;X.UNORDERED_NODE_ITERATOR_TYPE=4;X.ORDERED_NODE_ITERATOR_TYPE=5;X.UNORDERED_NODE_SNAPSHOT_TYPE=6;X.ORDERED_NODE_SNAPSHOT_TYPE=7;X.ANY_UNORDERED_NODE_TYPE=8;X.FIRST_ORDERED_NODE_TYPE=9;function yc(a){this.lookupNamespaceURI=za(a)}\nfunction zc(a,b){a=a||k;var c=a.Document&&a.Document.prototype||a.document;if(!c.evaluate||b)a.XPathResult=X,c.evaluate=function(a,b,c,g){return(new xc(a,c)).evaluate(b,g)},c.createExpression=function(a,b){return new xc(a,b)},c.createNSResolver=function(a){return new yc(a)}}aa(\"wgxpath.install\",zc);var Ac=function(){var a={M:\"http://www.w3.org/2000/svg\"};return function(b){return a[b]||null}}();\nfunction Bc(a,b){var c=F(a);if(!c.documentElement)return null;(B||hb)&&zc(c?c.parentWindow||c.defaultView:window);try{var d=c.createNSResolver?c.createNSResolver(c.documentElement):Ac;if(B&&!ab(7))return c.evaluate.call(c,b,a,d,9,null);if(!B||9<=Number(bb)){for(var e={},f=c.getElementsByTagName(\"*\"),g=0;g=b&&0<=c&&255>=c&&0<=d&&255>=d&&0<=e&&1>=e)){b=[b,c,d,e];break b}b=null}if(!b)b:{if(d=a.match(Pa))if(b=\nNumber(d[1]),c=Number(d[2]),d=Number(d[3]),0<=b&&255>=b&&0<=c&&255>=c&&0<=d&&255>=d){b=[b,c,d,1];break b}b=null}if(!b)b:{b=a.toLowerCase();c=ia[b.toLowerCase()];if(!c&&(c=\"#\"==b.charAt(0)?b:\"#\"+b,4==c.length&&(c=c.replace(Ma,\"#$1$1$2$2$3$3\")),!Na.test(c))){b=null;break b}b=[parseInt(c.substr(1,2),16),parseInt(c.substr(3,2),16),parseInt(c.substr(5,2),16),1]}a=b?\"rgba(\"+b.join(\", \")+\")\":a}return a}\nfunction Fc(a,b){var c=a.currentStyle||a.style,d=c[b];!l(d)&&\"function\"==ba(c.getPropertyValue)&&(d=c.getPropertyValue(b));return\"inherit\"!=d?l(d)?d:null:(a=Ec(a))?Fc(a,b):null}\nfunction Gc(a,b,c){function d(a){var b=Hc(a);return 0=C.a+C.width;C=e.c>=C.b+C.height;if(M&&\"hidden\"==n.x||C&&\"hidden\"==n.y)return Z;if(M&&\"visible\"!=n.x||C&&\"visible\"!=n.y){if(w&&(n=d(a),e.f>=g.scrollWidth-n.x||e.a>=g.scrollHeight-n.y))return Z;e=Ic(a);return e==Z?Z:\"scroll\"}}}return\"none\"}\nfunction Hc(a){var b=Jc(a);if(b)return b.rect;if(K(a,\"HTML\"))return a=F(a),a=((a?a.parentWindow||a.defaultView:window)||window).document,a=\"CSS1Compat\"==a.compatMode?a.documentElement:a.body,a=new ja(a.clientWidth,a.clientHeight),new E(0,0,a.width,a.height);try{var c=a.getBoundingClientRect()}catch(d){return new E(0,0,0,0)}b=new E(c.left,c.top,c.right-c.left,c.bottom-c.top);B&&a.ownerDocument.body&&(a=F(a),b.a-=a.documentElement.clientLeft+a.body.clientLeft,b.b-=a.documentElement.clientTop+a.body.clientTop);\nreturn b}function Jc(a){var b=K(a,\"MAP\");if(!b&&!K(a,\"AREA\"))return null;var c=b?a:K(a.parentNode,\"MAP\")?a.parentNode:null,d=null,e=null;c&&c.name&&(d=Cc('/descendant::*[@usemap = \"#'+c.name+'\"]',F(c)))&&(e=Hc(d),b||\"default\"==a.shape.toLowerCase()||(a=Mc(a),b=Math.min(Math.max(a.a,0),e.width),c=Math.min(Math.max(a.b,0),e.height),e=new E(b+e.a,c+e.b,Math.min(a.width,e.width-b),Math.min(a.height,e.height-c))));return{B:d,rect:e||new E(0,0,0,0)}}\nfunction Mc(a){var b=a.shape.toLowerCase();a=a.coords.split(\",\");if(\"rect\"==b&&4==a.length){var b=a[0],c=a[1];return new E(b,c,a[2]-b,a[3]-c)}if(\"circle\"==b&&3==a.length)return b=a[2],new E(a[0]-b,a[1]-b,2*b,2*b);if(\"poly\"==b&&2] 89 | [2019-12-02 18:06:12,558][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][DELETE http://127.0.0.1:54525/session/704ae6a97352fc07f923ef6d23d64901 {}] 90 | [2019-12-02 18:06:13,160][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54525 "DELETE /session/704ae6a97352fc07f923ef6d23d64901 HTTP/1.1" 200 14] 91 | [2019-12-02 18:06:13,160][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 92 | [2019-12-02 18:06:15,539][MainThread:10864][task_id:root][log_tool.py:112][INFO][<测试开始>] 93 | [2019-12-02 18:06:16,082][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54613/session {"capabilities": {"firstMatch": [{}], "alwaysMatch": {"browserName": "chrome", "platformName": "any", "goog:chromeOptions": {"extensions": [], "args": ["--no-sandbox"]}}}, "desiredCapabilities": {"browserName": "chrome", "version": "", "platform": "ANY", "goog:chromeOptions": {"extensions": [], "args": ["--no-sandbox"]}}}] 94 | [2019-12-02 18:06:16,083][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:225][DEBUG][Starting new HTTP connection (1): 127.0.0.1:54613] 95 | [2019-12-02 18:06:18,051][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54613 "POST /session HTTP/1.1" 200 681] 96 | [2019-12-02 18:06:18,052][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 97 | [2019-12-02 18:06:18,100][MainThread:10864][task_id:root][test_baidu_case.py:85][INFO][百度测试第二个用例,环境UAT语言en_GB] 98 | [2019-12-02 18:06:18,104][MainThread:10864][task_id:root][test_baidu_case.py:88][INFO][从 redis 中取出数据:百度] 99 | [2019-12-02 18:06:20,106][MainThread:10864][task_id:root][log_tool.py:117][INFO][<测试结束>] 100 | [2019-12-02 18:06:20,106][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][DELETE http://127.0.0.1:54613/session/f6399777c6d6c03560a9af37fd92f389 {}] 101 | [2019-12-02 18:06:20,391][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54613 "DELETE /session/f6399777c6d6c03560a9af37fd92f389 HTTP/1.1" 200 14] 102 | [2019-12-02 18:06:20,391][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 103 | [2019-12-02 18:06:22,520][MainThread:10864][task_id:root][log_tool.py:112][INFO][<测试开始>] 104 | [2019-12-02 18:06:23,076][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54634/session {"capabilities": {"firstMatch": [{}], "alwaysMatch": {"browserName": "chrome", "platformName": "any", "goog:chromeOptions": {"extensions": [], "args": ["--no-sandbox"]}}}, "desiredCapabilities": {"browserName": "chrome", "version": "", "platform": "ANY", "goog:chromeOptions": {"extensions": [], "args": ["--no-sandbox"]}}}] 105 | [2019-12-02 18:06:23,077][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:225][DEBUG][Starting new HTTP connection (1): 127.0.0.1:54634] 106 | [2019-12-02 18:06:24,835][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54634 "POST /session HTTP/1.1" 200 682] 107 | [2019-12-02 18:06:24,836][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 108 | [2019-12-02 18:06:24,849][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54634/session/541d64c3db6c168b754e297022e288b0/window/maximize {}] 109 | [2019-12-02 18:06:25,968][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54634 "POST /session/541d64c3db6c168b754e297022e288b0/window/maximize HTTP/1.1" 200 51] 110 | [2019-12-02 18:06:25,969][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 111 | [2019-12-02 18:06:25,969][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54634/session/541d64c3db6c168b754e297022e288b0/timeouts {"implicit": 20000}] 112 | [2019-12-02 18:06:25,971][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54634 "POST /session/541d64c3db6c168b754e297022e288b0/timeouts HTTP/1.1" 200 14] 113 | [2019-12-02 18:06:25,971][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 114 | [2019-12-02 18:06:25,997][MainThread:10864][task_id:root][test_csdn_case.py:53][INFO][csdn 测试第一个用例,环境UAT语言en_GB] 115 | [2019-12-02 18:06:25,997][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][POST http://127.0.0.1:54634/session/541d64c3db6c168b754e297022e288b0/url {"url": "https://blog.csdn.net/abcnull"}] 116 | [2019-12-02 18:06:28,751][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54634 "POST /session/541d64c3db6c168b754e297022e288b0/url HTTP/1.1" 200 14] 117 | [2019-12-02 18:06:28,752][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 118 | [2019-12-02 18:06:30,762][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][GET http://127.0.0.1:54634/session/541d64c3db6c168b754e297022e288b0/screenshot {}] 119 | [2019-12-02 18:06:31,188][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54634 "GET /session/541d64c3db6c168b754e297022e288b0/screenshot HTTP/1.1" 200 473508] 120 | [2019-12-02 18:06:31,190][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 121 | [2019-12-02 18:06:31,214][MainThread:10864][task_id:root][log_tool.py:117][INFO][<测试结束>] 122 | [2019-12-02 18:06:31,214][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:388][DEBUG][DELETE http://127.0.0.1:54634/session/541d64c3db6c168b754e297022e288b0 {}] 123 | [2019-12-02 18:06:31,651][MainThread:10864][task_id:urllib3.connectionpool][connectionpool.py:437][DEBUG][http://127.0.0.1:54634 "DELETE /session/541d64c3db6c168b754e297022e288b0 HTTP/1.1" 200 14] 124 | [2019-12-02 18:06:31,651][MainThread:10864][task_id:selenium.webdriver.remote.remote_connection][remote_connection.py:440][DEBUG][Finished Request] 125 | -------------------------------------------------------------------------------- /ui-test/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/requirements.txt -------------------------------------------------------------------------------- /ui-test/resource/__init__.py: -------------------------------------------------------------------------------- 1 | # UI 测试中的资源文件 2 | -------------------------------------------------------------------------------- /ui-test/resource/config/__init__.py: -------------------------------------------------------------------------------- 1 | # 配置文件存放 2 | -------------------------------------------------------------------------------- /ui-test/resource/config/config.ini: -------------------------------------------------------------------------------- 1 | [project] 2 | # 驱动选择 chrome/firefox/ie/edge/opera/safari 3 | driver = chrome 4 | # 远程运行服务器 ip 5 | remote_ip = 6 | # 远程运行端口 7 | remote_port = 8 | # redis 是否启用 [Y/y]启用 [N/n]不启用 9 | redis_enable = Y 10 | # mysql 是否启用 [Y/y]启用 [N/n]不启用 11 | mysql_enable = N 12 | 13 | # 测试的环境 14 | env = UAT 15 | # 语言选择 16 | lan = en_GB 17 | # 首页网址 18 | home_page = https://blog.csdn.net/abcnull 19 | 20 | [driver] 21 | # 谷歌浏览器驱动相对项目 ui-test 包的路径(项目中对应 77 的谷歌浏览器的驱动) 22 | chrome_driver_path = /resource/driver/chromedriver.exe 23 | # 火狐浏览器驱动路径(项目中是 v0.24.0 的火狐驱动) 24 | firefox_driver_path = /resource/driver/geckodriver.exe 25 | # IE 浏览器驱动路径 26 | ie_driver_path = /resource/driver/IEDriverServer.exe 27 | # Edge 浏览器驱动路径(项目中是 80 的 Edge 驱动) 28 | edge_driver_path = /resource/driver/msedgedriver.exe 29 | # 欧朋浏览器驱动路径(项目中是 78 的欧朋驱动) 30 | opera_driver_path = /resource/driver/operadriver.exe 31 | # safari 浏览器驱动路径 32 | safari_driver_path = /resource/driver/SafariDriver.safariextz 33 | 34 | [redis] 35 | # redis 服务器 ip 36 | redis_ip = localhost 37 | 38 | # redis 服务器端口号(默认 6379) 39 | redis_port = 6379 40 | 41 | # redis 连接密码 42 | redis_pwd = 43 | 44 | # 最大连接数 45 | max_connections = 1024 46 | 47 | [mysql] 48 | # mysql 服务器 ip 49 | mysql_ip = localhost 50 | 51 | # mysql 服务器端口号(默认 3306) 52 | mysql_port = 3306 53 | 54 | # 要连接的 database 名 55 | mysql_db = database1 56 | 57 | # mysql 连接用户名 58 | mysql_user = abcnull 59 | 60 | # mysql 连接密码 61 | mysql_pwd = 123456 62 | 63 | # mysql 编码格式 64 | mysql_charset = utf-8 65 | 66 | [screenshot] 67 | # 手动截图存放路径,相对于 ui-test 包的路径 68 | shotfile_path = /report/img/ 69 | 70 | # 截图格式 71 | shot_format = .png 72 | 73 | # 同名截图是否允许覆盖 [Y/y]允许 [N/n]不允许 74 | cover_allowed = N 75 | 76 | [html] 77 | # html 存放报告名 78 | htmlfile_name = UI测试报告 79 | 80 | # html 存放报告文件相对于项目 ui-test 包的路径 81 | htmlfile_path = /report/html/ 82 | 83 | # 同名报告是否允许覆盖 [Y/y]允许 [N/n]不允许 84 | cover_allowed = N 85 | 86 | [log] 87 | # log 存放日志名 88 | logfile_name = ui_log.log 89 | # log 存放日志文件相对于项目 ui-test 包的路径 90 | logfile_path = /report/log/ 91 | 92 | # 打到终端的日志级别和格式 93 | terminal_level = INFO 94 | terminal_formatter = simple 95 | 96 | # 打到文件的日志级别和格式 97 | file_level = DEBUG 98 | file_formatter = standard 99 | # 打到文件中日志大小字节数 1 M == 1048576 B 100 | max_bytes = 1048576 101 | # 轮转日志文件数量即最大保存日志数量 102 | backup_count = 5 103 | # 日志文件编码方式 104 | encoding = utf-8 105 | 106 | # 既打到终端又打到文件的日志级别 107 | all_level = DEBUG 108 | -------------------------------------------------------------------------------- /ui-test/resource/driver/IEDriverServer.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/resource/driver/IEDriverServer.exe -------------------------------------------------------------------------------- /ui-test/resource/driver/SafariDriver.safariextz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/resource/driver/SafariDriver.safariextz -------------------------------------------------------------------------------- /ui-test/resource/driver/__init__.py: -------------------------------------------------------------------------------- 1 | # 所需驱动 2 | -------------------------------------------------------------------------------- /ui-test/resource/driver/chromedriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/resource/driver/chromedriver.exe -------------------------------------------------------------------------------- /ui-test/resource/driver/geckodriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/resource/driver/geckodriver.exe -------------------------------------------------------------------------------- /ui-test/resource/driver/msedgedriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/resource/driver/msedgedriver.exe -------------------------------------------------------------------------------- /ui-test/resource/driver/operadriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcnull/python-ui-auto-test/85cdb3cf3be86f43f36ad756f758ecab50e90bc1/ui-test/resource/driver/operadriver.exe -------------------------------------------------------------------------------- /ui-test/suite/run_all.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from case import test_baidu_case, test_csdn_case, test_other_case 4 | from util.config_reader import ConfigReader 5 | from util.report_tool import ReportTool 6 | 7 | # 报告存放路径 8 | report_path = os.path.abspath(os.path.dirname(__file__))[ 9 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 10 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("html")["htmlfile_path"] 11 | # 报告名字 12 | report_name = ConfigReader().read("html")["htmlfile_name"] 13 | 14 | # 运行所有用例(单线程) 15 | if __name__ == "__main__": 16 | # 创建测试套 17 | suites = unittest.TestSuite() 18 | loader = unittest.TestLoader() 19 | 20 | # 百度测试流程添加到测试套 21 | suites.addTests(loader.loadTestsFromModule(test_baidu_case)) 22 | # csdn 测试流程添加到测试套 23 | suites.addTests(loader.loadTestsFromModule(test_csdn_case)) 24 | # 其他测试流程(这一行可以取消注释,可以查看断言错误的截图在报告中的效果) 25 | # suites.addTests(loader.loadTestsFromModule(test_other_case)) 26 | 27 | # 报告生成器,运行用例并生成报告,对 BeautifulReport 套了一层外壳 28 | ReportTool(suites).run(filename=report_name, description='demo', report_dir=report_path, theme="theme_cyan") 29 | -------------------------------------------------------------------------------- /ui-test/suite/run_all_mutithread.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from case import test_baidu_case, test_csdn_case, test_other_case 4 | from util.config_reader import ConfigReader 5 | from util.report_tool import ReportTool 6 | from tomorrow import threads 7 | 8 | # 报告存放路径 9 | report_path = os.path.abspath(os.path.dirname(__file__))[ 10 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 11 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("html")["htmlfile_path"] 12 | 13 | 14 | # 报告名字 15 | # report_name = ConfigReader().read("html")["htmlfile_name"] 16 | 17 | 18 | # 设置三线程 19 | @threads(3) 20 | def run_mutithread(test_suite): 21 | # 报告生成器,运行用例并生成报告,对 BeautifulReport 套了一层外壳 22 | ReportTool(test_suite).run(filename=new_report_name, description='demo', report_dir=report_path, theme="theme_cyan") 23 | 24 | 25 | # 运行所有用例(多线程,运行无问题,但是产出报告会被覆盖,未解决) 26 | if __name__ == "__main__": 27 | # 创建测试套 28 | suites = unittest.TestSuite() 29 | loader = unittest.TestLoader() 30 | 31 | # 百度测试流程添加到测试套 32 | suites.addTests(loader.loadTestsFromModule(test_baidu_case)) 33 | # csdn 测试流程添加到测试套 34 | suites.addTests(loader.loadTestsFromModule(test_csdn_case)) 35 | # 其他测试流程(这一行可以取消注释,可以查看断言错误的截图在报告中的效果) 36 | # suites.addTests(loader.loadTestsFromModule(test_other_case)) 37 | 38 | # 循环遍历 39 | n = 0 40 | for i in suites: 41 | # 循环次数 42 | n += 1 43 | # 获取多线程报告名字 44 | report_name = ConfigReader().read("html")["htmlfile_name"] + "-第" + str(n) + "个线程" 45 | new_report_name = ReportTool(suites).get_html_name(filename=report_name, report_dir=report_path) 46 | # 多线程运行 47 | run_mutithread(i) 48 | -------------------------------------------------------------------------------- /ui-test/util/__init__.py: -------------------------------------------------------------------------------- 1 | # 工具类 2 | -------------------------------------------------------------------------------- /ui-test/util/config_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import configparser 10 | import os 11 | 12 | 13 | # config 配置文件读取器 14 | class ConfigReader: 15 | # 依据 [module] 来读取 config 文件 16 | def read(self, module): 17 | """ 18 | 读取 ini 配置文件 19 | :param module: 配置文件的模块参数 20 | :return: 返回具体某个参数的值 21 | """ 22 | # 配置文件路径 23 | self.config_absolute_path = os.path.abspath(os.path.dirname(__file__))[ 24 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 25 | "python-ui-auto-test")] + '/ui-test/resource/config/config.ini' 26 | # 创建配置文件管理者 27 | self.config_manager = configparser.ConfigParser() 28 | # 以 utf-8 编码方式读取配置文件 29 | self.config_manager.read(self.config_absolute_path, encoding="utf-8") 30 | # 返回读取配置文件内容字典 31 | return dict(self.config_manager.items(module)) 32 | 33 | # 返回配置文件的绝对路径 34 | def get_config_absolute_path(self): 35 | return self.config_absolute_path 36 | 37 | 38 | # 检测 config 配置文件读取器 39 | if __name__ == "__main__": 40 | # 输出 [driver] 中的数据 41 | print(ConfigReader().read("driver")) 42 | -------------------------------------------------------------------------------- /ui-test/util/log_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import logging.config 10 | import os 11 | from util.config_reader import ConfigReader 12 | 13 | # 用户名字典 14 | dic = { 15 | # 用户名 16 | 'username': 'abcnull' 17 | } 18 | 19 | # 定义三种日志输出格式 20 | standard_format = '[%(asctime)s][%(threadName)s:%(thread)d][task_id:%(name)s][%(filename)s:%(lineno)d]' \ 21 | '[%(levelname)s][%(message)s]' 22 | simple_format = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s' 23 | id_simple_format = '[%(levelname)s][%(asctime)s] %(message)s' 24 | 25 | # 从配置文件取到的日志配置信息 26 | # 输出日志的名字和绝对路径 27 | logfile_name = ConfigReader().read('log')['logfile_name'] # log文件名 28 | logfile_path_staff = r'' + os.path.abspath(os.path.dirname(__file__))[ 29 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 30 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read('log')[ 31 | 'logfile_path'] + logfile_name 32 | 33 | # 打到终端的日志级别和格式 34 | terminal_level = ConfigReader().read('log')['terminal_level'] 35 | terminal_formatter = ConfigReader().read('log')['terminal_formatter'] 36 | # 打到文件的日志级别和格式 37 | file_level = ConfigReader().read('log')['file_level'] 38 | file_formatter = ConfigReader().read('log')['file_formatter'] 39 | # 既打到终端又打到文件的日志级别 40 | all_level = ConfigReader().read('log')['all_level'] 41 | # 日志文件存储字节数 42 | max_bytes = int(ConfigReader().read('log')['max_bytes']) 43 | # 日志文件轮转数 44 | backup_count = int(ConfigReader().read('log')['backup_count']) 45 | # 日志文件编码方式 46 | encoding = ConfigReader().read('log')['encoding'] 47 | 48 | # log 配置字典 49 | # logging_dic 第一层的所有的键不能改变 50 | logging_dic = { 51 | # 版本号 52 | 'version': 1, 53 | # 固定写法 54 | 'disable_existing_loggers': False, 55 | 'formatters': { 56 | 'standard': { 57 | 'format': standard_format 58 | }, 59 | 'simple': { 60 | 'format': simple_format 61 | }, 62 | }, 63 | 'filters': {}, 64 | 'handlers': { 65 | # 打印到终端的日志 66 | 'sh': { 67 | 'level': terminal_level, 68 | # 打印到屏幕 69 | 'class': 'logging.StreamHandler', 70 | 'formatter': terminal_formatter 71 | }, 72 | # 打印到文件的日志,收集info及以上的日志 73 | 'fh': { 74 | 'level': file_level, 75 | # 保存到文件 76 | 'class': 'logging.handlers.RotatingFileHandler', 77 | 'formatter': file_formatter, 78 | # 日志文件 79 | 'filename': logfile_path_staff, 80 | # 日志大小 300字节 81 | 'maxBytes': max_bytes, 82 | # 轮转文件的个数 83 | 'backupCount': backup_count, 84 | # 日志文件的编码 85 | 'encoding': encoding, 86 | }, 87 | }, 88 | 'loggers': { 89 | # logging.getLogger(__name__) 拿到的 logger 配置 90 | '': { 91 | # 这里把上面定义的两个handler都加上,即 log 数据既写入文件又打印到屏幕 92 | 'handlers': ['sh', 'fh'], 93 | 'level': all_level, 94 | # 向上(更高 level 的 logger)传递 95 | 'propagate': True, 96 | }, 97 | }, 98 | } 99 | 100 | 101 | # log 日志工具(已对其进行了封装) 102 | def log(): 103 | # 导入上面定义的 logging 配置 通过字典方式去配置这个日志 104 | logging.config.dictConfig(logging_dic) 105 | # 生成一个 log 实例,这里可以有参数传给 task_id 106 | logger = logging.getLogger() 107 | return logger 108 | 109 | 110 | # 用例开始 111 | def start_info(): 112 | log().info(f"<测试开始>") 113 | 114 | 115 | # 用例结束 116 | def end_info(): 117 | log().info(f"<测试结束>") 118 | 119 | 120 | # 用例登陆成功 121 | def login_info(): 122 | log().info(f"{dic['username']} 登陆成功") 123 | 124 | # 尝试(单独测试这里可以取消注释,之后这里请务必要注释掉!) 125 | # login_info() 126 | # start_info() 127 | # end_info() 128 | -------------------------------------------------------------------------------- /ui-test/util/mysql_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import pymysql as pymysql 10 | 11 | from util.config_reader import ConfigReader 12 | 13 | 14 | # mysql 连接工具 15 | class MysqlTool: 16 | # 初始化 mysql 连接 17 | def __init__(self): 18 | # mysql_ip 19 | self.host = ConfigReader().read("mysql")["mysql_ip"] 20 | # mysql_port 21 | self.port = int(ConfigReader().read("mysql")["mysql_port"]) 22 | # mysql_db 23 | self.db = ConfigReader().read("mysql")["mysql_db"] 24 | # mysql_user 25 | self.user = ConfigReader().read("mysql")["mysql_user"] 26 | # mysql_pwd 27 | self.passwd = ConfigReader().read("mysql")["mysql_pwd"] 28 | # mysql_charset 29 | self.charset = ConfigReader().read("mysql")["mysql_charset"] 30 | # mysql 连接 31 | self.mysql_conn = pymysql.connect(host=self.host, user=self.user, passwd=self.passwd, db=self.db, 32 | port=self.port, charset=self.charset) 33 | 34 | # execute 任何操作 35 | def execute(self, sql): 36 | """ 37 | 执行 sql 语句 38 | :param sql: sql 语句 39 | :return: select 语句返回 40 | """ 41 | # 从 mysql 连接中获取一个游标对象 42 | cursor = self.mysql_conn.cursor() 43 | # sql 语句执行返回值 44 | ret = None 45 | try: 46 | # 执行 sql 语句 47 | ret = cursor.execute(sql) 48 | # 提交 49 | self.mysql_conn.commit() 50 | except Exception as e: 51 | # 异常回滚数据 52 | self.mysql_conn.rollback() 53 | # 关闭游标 54 | cursor.close() 55 | # 返回 56 | return format(ret) 57 | 58 | # 获取 mysql 连接 59 | def get_mysql_conn(self): 60 | return self.mysql_conn 61 | 62 | # mysql 连接释放 63 | def release_mysql_conn(self): 64 | if self.mysql_conn is not None: 65 | self.mysql_conn.close() 66 | self.mysql_conn = None 67 | -------------------------------------------------------------------------------- /ui-test/util/redis_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import redis as redis 10 | from util.config_reader import ConfigReader 11 | 12 | 13 | # redis 连接池工具 14 | class RedisPool: 15 | # 初始化连接池并直接创建一个 redis 连接 16 | def __init__(self): 17 | # redis_ip 18 | self.host = ConfigReader().read("redis")["redis_ip"] 19 | # redis_port 20 | self.port = int(ConfigReader().read("redis")["redis_port"]) 21 | # redis_pwd 22 | self.password = ConfigReader().read("redis")["redis_pwd"] 23 | # 最大连接数 24 | self.max_connections = int(ConfigReader().read("redis")["max_connections"]) 25 | # redis 连接池 26 | self.pool = redis.ConnectionPool(host=self.host, port=self.port, password=self.password, 27 | max_connections=self.max_connections) 28 | # redis 的一个连接 29 | self.conn = redis.Redis(connection_pool=self.pool) 30 | 31 | # 获取 redis 连接池 32 | def get_redis_pool(self): 33 | return self.pool 34 | 35 | # 获取 redis 连接 36 | def get_redis_conn(self): 37 | return self.conn 38 | 39 | # 获取 redis 的一个新的连接池 40 | def get_new_redis_pool(self): 41 | self.pool = redis.ConnectionPool(host=self.host, port=self.port, password=self.password, 42 | max_connections=self.max_connections) 43 | self.conn = redis.Redis(connection_pool=self.pool) 44 | return self.pool 45 | 46 | # 获取 redis 的一个新的连接 47 | def get_new_redis_conn(self): 48 | self.conn = redis.Redis(connection_pool=self.pool) 49 | return self.conn 50 | 51 | # 关闭当前的连接 52 | def release_redis_conn(self): 53 | if self.conn is not None: 54 | self.conn.connection_pool.disconnect() 55 | self.conn = None 56 | 57 | # 关闭整个连接池 58 | def release_redis_pool(self): 59 | if self.pool is not None: 60 | self.pool.disconnect() 61 | self.pool = None 62 | -------------------------------------------------------------------------------- /ui-test/util/report_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import os 10 | from BeautifulReport import BeautifulReport 11 | from util.config_reader import ConfigReader 12 | from util.text_tool import TextTool 13 | 14 | 15 | # BeautifulReport 报告产生器,可控制文件是否支持覆盖 16 | class ReportTool: 17 | # 构造器 18 | def __init__(self, suites): 19 | """ 20 | 构造器 21 | :param suites: 测试套件 22 | """ 23 | self.suites = suites 24 | 25 | # 运行并产出报告方法 26 | def run(self, description, filename: str = None, report_dir=".", log_path=None, theme="theme_default"): 27 | """ 28 | 运行测试套并产生报告进行存放 29 | :param description: 见 BeautifulReport 的 report 方法参数 30 | :param filename: 见 BeautifulReport 的 report 方法参数 31 | :param report_dir: 见 BeautifulReport 的 report 方法参数 32 | :param log_path: 见 BeautifulReport 的 report 方法参数 33 | :param theme: 见 BeautifulReport 的 report 方法参数 34 | :return: 返回新的报告名称 35 | """ 36 | # 项目开始装配时输出文本到控制台 37 | TextTool().project_start() 38 | # 获取新的报告名 39 | new_filename = self.get_html_name(filename, report_dir) 40 | # 运行测试并产出报告存放 self.get_html_name(filename, report_dir) 41 | BeautifulReport(self.suites).report(filename=new_filename, description=description, report_dir=report_dir, 42 | theme=theme) 43 | # 由于可配置是否允许报告被覆盖,这里返回的是报告新名字 44 | return new_filename 45 | 46 | # 递归方法 47 | # 判断报告的名字在配置文件指定路径下是否有重复,并根据配置是否允许重复返回报告新的名字 48 | def get_html_name(self, filename: str = None, report_dir="."): 49 | """ 50 | 获取新的报告名 51 | :param filename: 报告名称 52 | :param report_dir: 报告路径 53 | :return: 通过配置文件判断报告是否可以被覆盖,返回新的报告名 54 | """ 55 | # 若允许被覆盖同名报告 56 | if ConfigReader().read("html")["cover_allowed"].upper() == "Y": 57 | # 返回报告名 58 | return filename 59 | # 若不支持覆盖同名报告 60 | elif ConfigReader().read("html")["cover_allowed"].upper() == "N": 61 | # 判断报告路径是否存在,若存在 62 | if os.path.exists(report_dir + filename + ".html"): 63 | # 如果名字不以 ")" 结尾 64 | if not filename.endswith(")"): 65 | filename = filename + "(2)" 66 | # 如果名字以 ")" 结尾 67 | else: 68 | file_num = filename[filename.index("(") + 1: -1] 69 | num = int(file_num) 70 | # 报告名称字段自增 71 | num += 1 72 | filename = filename[:filename.index("(")] + "(" + str(num) + ")" 73 | # 若报告不存在 74 | else: 75 | # 递归出口,运行测试并产出报告存放 76 | return filename 77 | # 递归:不断改变 filename 后报告是否还能找到 78 | return self.get_html_name(filename, report_dir) 79 | # 若配置中既不是 Y/y 也不是 N/n 就抛出异常 80 | else: 81 | raise RuntimeError("config.ini中[html]的cover_allowed字段配置错误,请检查!") 82 | -------------------------------------------------------------------------------- /ui-test/util/screenshot_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import os 10 | from util.config_reader import ConfigReader 11 | 12 | 13 | # 截图工具,可控制图片是否被覆盖 14 | class ScreenshotTool: 15 | # 错误截图方法 16 | def save_img(self, driver, img_name): 17 | """ 18 | 截图并保存进指定路径 19 | :param driver: 驱动 20 | :param img_name: 截图名 21 | :return: 新的截图名 22 | """ 23 | # 图片绝对路径 24 | img_path = os.path.abspath(os.path.dirname(__file__))[ 25 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 26 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("screenshot")["shotfile_path"] 27 | # 获取新的图片名 28 | new_img_name = self.get_img_name(img_name) 29 | # 根据配置文件的配置,截图智能存放 30 | driver.get_screenshot_as_file( 31 | ("{}/{}" + ConfigReader().read("screenshot")["shot_format"]).format(img_path, new_img_name)) 32 | # 由于可配置是否允许图片被覆盖,这里返回的是图片新名字 33 | return new_img_name 34 | 35 | # 递归方法 36 | # 判断图片的名字在配置文件指定路径下是否有重复,并根据配置是否允许重复返回图片新的名字 37 | def get_img_name(self, img_name): 38 | """ 39 | 获取新的截图名 40 | :param img_name: 截图名 41 | :return: 新的截图名 42 | """ 43 | # 图片绝对路径 44 | img_path = os.path.abspath(os.path.dirname(__file__))[ 45 | :os.path.abspath(os.path.dirname(__file__)).find("python-ui-auto-test") + len( 46 | "python-ui-auto-test")] + "/ui-test" + ConfigReader().read("screenshot")["shotfile_path"] 47 | # 判断同名截图是否支持覆盖,若支持覆盖同名图片 48 | if ConfigReader().read("screenshot")["cover_allowed"].upper() == "Y": 49 | # 返回截图名字 50 | return img_name 51 | # 若不支持覆盖同名图片 52 | elif ConfigReader().read("screenshot")["cover_allowed"].upper() == "N": 53 | # 判断图片路径是否存在,若存在 54 | if os.path.exists(img_path + img_name + ConfigReader().read("screenshot")["shot_format"]): 55 | # 如果名字不以 ")" 结尾 56 | if not img_name.endswith(")"): 57 | img_name = img_name + "(2)" 58 | # 如果名字以 ")" 结尾 59 | else: 60 | img_num = img_name[img_name.index("(") + 1: -1] 61 | num = int(img_num) 62 | # 图片名称字段自增 63 | num += 1 64 | img_name = img_name[:img_name.index("(")] + "(" + str(num) + ")" 65 | # 若图片不存在 66 | else: 67 | # 递归出口 68 | return img_name 69 | # 递归:不断改变 img_name 后图片是否还能找到 70 | return self.get_img_name(img_name) 71 | # 若配置中既不是 Y/y 也不是 N/n 就抛出异常 72 | else: 73 | raise RuntimeError("config.ini中[img]的cover_allowed 字段配置错误,请检查!") 74 | -------------------------------------------------------------------------------- /ui-test/util/text_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | # 文本产生器 10 | class TextTool: 11 | # 项目开始文本 12 | def project_start(cls): 13 | print("|| || |||||||| |||||||||| ||||||||| ||||||||| ||||||||||") 14 | print("|| || || || || || ||") 15 | print("|| || || || ||||||||| ||||||||| ||") 16 | print("|| || || || || || ||") 17 | print("|||||||||| |||||||| || ||||||||| ||||||||| ||") 18 | print(" ") 19 | -------------------------------------------------------------------------------- /ui-test/util/thread_local_storage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author : abcnull 4 | # @Time : 2019/12/2 17:37 5 | # @E-Mail : abcnull@qq.com 6 | # @CSDN : abcnull 7 | # @GitHub : abcnull 8 | 9 | import threading 10 | 11 | 12 | # 本地线程存储器 13 | class ThreadLocalStorage: 14 | # 类变量 15 | key_value = {} 16 | 17 | # 存储线程和对应线程的数据 18 | @classmethod 19 | def set(cls, thread, data): 20 | """ 21 | 类变量字典保存 {线程: 数据} 的键值对 22 | :param thread: 当前线程 23 | :param data: 需要保存的数据 24 | """ 25 | cls.key_value.update({thread: data}) 26 | 27 | # 通过键名取值 28 | @classmethod 29 | def get(cls, thread): 30 | """ 31 | 得到线程对应的存储数据 32 | :param thread: 线程 33 | :return: 返回对应线程的数据 34 | """ 35 | return cls.key_value[thread] 36 | 37 | # 清空当前线程存储的对应数据 38 | @classmethod 39 | def clear_current_thread(cls): 40 | del cls.key_value[threading.current_thread()] 41 | 42 | # 清空所有线程以及所有线程存储的对应数据 43 | @classmethod 44 | def clear_all_thread(cls): 45 | cls.key_value.clear() 46 | cls.key_value = {} 47 | --------------------------------------------------------------------------------