diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index c2672dd..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: ['https://sweeter.io/#/sponsor'] diff --git a/FAQ.md b/FAQ.md deleted file mode 100644 index cf631cf..0000000 --- a/FAQ.md +++ /dev/null @@ -1,102 +0,0 @@ -# Sweetest 常见问题汇总(持续更新...) - - -## 安装配置 - - -### 1. 是否支持 Python2.7? - -答:不支持。 - -Sweetest 仅支持 Python3.6 或以上,原因如下: - -1) 框架中使用了有序字典等特性; -2) 人生苦短,我用新版 :) - - -### 2. 安装后,无法正常启动浏览器? - -答:请检查是否正确配置了 chromedriver: - -要求和 Chrome 版本匹配;并且把路径添加到环境变量的 path 中。 - - -### 3. 直接从 github 上下载源码能跑起来吗? - -答:不能。 - -因为没有安装依赖库;建议使用 pip 安装,pip 会自动把依赖库也一起安装。 - - -### 4. 是否支持 Mac、Linux? - -答:支持。 - -但是无法使用 sweetest 来创建示例文件夹,需要自行下载示例并解压; - -下载地址: - -另外,Mac 或 Linux 上的 chromedriver 在功能和稳定性上**可能**存在问题,建议还是在 Windows 上运行比较可靠。 - - -## 支持范围 - - -### 1. 除了 Chrome,是否支持 IE, Firefox, Safari 等浏览器? - -答:支持。 - -Sweetest 底层是 Selenium 接口,按如下操作即可: - -1) 配置好对应的浏览器驱动; -2) 在启动脚本里配置对应浏览器,如下: - -``` -desired_caps = {'platformName': 'Desktop', 'browserName': 'Ie'} -``` - - -### 2. 支持 Android App 测试吗? - -答:支持。 - -Sweetest 在移动端测试上底层使用的是 Appium,需要配置好 Appium 环境; - -目前,已经在 OPPO R9s 上测试通过。 - - -### 3. 支持 iOS App 测试吗? - -答:不支持。 - -虽然底层的 Appium 支持 iOS,但是经过我们在 iPhone 6p 上测试,响应速度非常慢,经常卡死; - -后续,我们会考虑使用其他框架作为底层来支持 iOS 测试,如 ATX。 - - -### 4. 支持小程序测试吗? - -答:支持 Android 上的小程序测试。 - -我们的在示例中有测试音乐台小程序的用例。 - - -### 5. 支持 http 接口测试吗? - -答:支持。 - -详情见示例中的用例。 - - -### 6. 支持数据库操作吗? - -答:支持。 - -详情见示例中的用例。 - - -## 使用及功能 - -### 1. 我写了一个 setup 用例,为什么没有执行? - -答:setup 是在普通用例之前执行的,如果没有普通用例就不会执行。 diff --git a/FUNDING.yml b/FUNDING.yml deleted file mode 100644 index d14e214..0000000 --- a/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: ['https://sweeter.io/docs/_media/sponsor.jpeg'] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a612ad9..0000000 --- a/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index 843cf19..defc957 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,6 @@ ![sweetest](https://sweeter.io/docs/_media/sweeter.png) -# sweetest -## 介绍 - -Sweetest 是一款小而美的自动化测试解决方案,同时支持 Web UI,Http 接口,DB 操作测试,Android/iOS App 测试,小程序测试,Windows GUI 测试,文件操作;由于开始只支持 Web UI 测试,名字取自 Selenium,Web UI,Excel,Element, Test 含义。 -特点: - -1. 简单快速,轻松上手 -2. 无需编码能力 -3. 在 Excel 中以文本编写测试用例 -4. 维护成本低 -5. 支持千、万级别的用例规模 -6. 拥抱变化,支持敏捷 - -## 安装 - -### 环境要求 - -- 系统要求:Windows -- Python 版本:**3.6+** -- 浏览器:Chrome -- Chrome 驱动: [chromedriver](https://npm.taobao.org/mirrors/chromedriver) (需和 Chrome 版本匹配,并配置环境变量,[参考这里配置](https://segmentfault.com/a/1190000013940356)) - -### 安装 sweetest - -```bash -pip install sweetest -``` - -### 升级 sweetest - -```bash -pip install -U sweetest -``` - -### 快速体验 - -打开 cmd 命令窗口,切换到某个目录,如:D:\\Autotest - -```bash -sweetest -cd sweetest_example -python start.py -``` - -![install](https://sweeter.io/docs/_snapshot/install.png) - -OK,如果一切顺利的话,sweetest 已经跑起来了 - -> 详细文档:https://sweeter.io/#/sweetest/ - -## 加入我们 - -QQ 交流群:**941761748** -> (验证码:python) 注意首字母小写 - -微信公众号:**喜文测试** - -![QQ2](https://sweeter.io/docs/_media/QQ.png)![WeChat](https://sweeter.io/docs/_media/WeChat.png) +Sweetest 已全面升级为 Sweet,请访问: +官网:https://sweeter.io +GitHub:https://github.com/sweeterio \ No newline at end of file diff --git a/example/report.zip b/example/report.zip deleted file mode 100644 index 65d7823..0000000 Binary files a/example/report.zip and /dev/null differ diff --git a/example/sweetest_example.zip b/example/sweetest_example.zip deleted file mode 100644 index 05b86ed..0000000 Binary files a/example/sweetest_example.zip and /dev/null differ diff --git a/sweetest/sweetest/lib/__init__.py b/sweet/lib/__init__.py similarity index 100% rename from sweetest/sweetest/lib/__init__.py rename to sweet/lib/__init__.py diff --git a/sweetest/sweetest/lib/c.py b/sweet/lib/c.py similarity index 100% rename from sweetest/sweetest/lib/c.py rename to sweet/lib/c.py diff --git a/sweetest/sweetest/lib/http_handle.py b/sweet/lib/http_handle.py similarity index 100% rename from sweetest/sweetest/lib/http_handle.py rename to sweet/lib/http_handle.py diff --git a/sweetest/sweetest/lib/u.py b/sweet/lib/u.py similarity index 100% rename from sweetest/sweetest/lib/u.py rename to sweet/lib/u.py diff --git a/sweet/modules/__init__.py b/sweet/modules/__init__.py new file mode 100644 index 0000000..4683a33 --- /dev/null +++ b/sweet/modules/__init__.py @@ -0,0 +1,8 @@ +from pathlib import Path + + +path = Path(__file__).resolve().parents[0] + +__all__ = [] +for p in path.iterdir(): + __all__.append(p.stem) \ No newline at end of file diff --git a/sweet/modules/db.py b/sweet/modules/db.py new file mode 100644 index 0000000..fded656 --- /dev/null +++ b/sweet/modules/db.py @@ -0,0 +1,214 @@ +from injson import check +from sweet import log, vars +from sweet.utility import compare, json2dict + + +keywords = { + 'SQL': 'SQL' +} + + +class App: + + keywords = keywords + + def __init__(self, setting): + # 获取连接参数 + self.db = DB(setting) + + def _close(self): + pass + + def _call(self, step): + # 根据关键字调用关键字实现 + getattr(self, step['keyword'].lower())(step) + + def sql(self, step): + + response = {} + + _sql = step['element'] + + log.debug(f'SQL: {repr(_sql)}') + + row = {} + if _sql.lower().startswith('select'): + row = self.db.fetchone(_sql) + log.debug(f'SQL response: {repr(row)}') + if not row: + raise Exception('*** Fetch None ***') + + elif _sql.lower().startswith('db.'): + _sql_ = _sql.split('.', 2) + collection = _sql_[1] + sql = _sql_[2] + response = self.db.mongo(collection, sql) + if response: + log.debug(f'find result: {repr(response)}') + else: + self.db.execute(_sql) + + if _sql.lower().startswith('select'): + text = _sql[6:].split('FROM')[0].split('from')[0].strip() + keys = dedup(text).split(',') + for i, k in enumerate(keys): + keys[i] = k.split(' ')[-1] + response = dict(zip(keys, row)) + log.debug(f'select result: {repr(response)}') + + expected = step['data'] + if not expected: + expected = step['expected'] + if 'json' in expected: + expected['json'] = json2dict(expected.get('json', '{}')) + result = check(expected.pop('json'), response['json']) + log.debug(f'json check result: {result}') + if result['code'] != 0: + raise Exception( + f'json | EXPECTED:{repr(expected["json"])}, REAL:{repr(response["json"])}, RESULT: {result}') + elif result['var']: + vars.put(result['var']) + log.debug(f'json var: {repr(result["var"])}') + + if expected: + for key in expected: + sv, pv = expected[key], response[key] + log.debug(f'key: {repr(key)}, expect: {repr(sv)}, real: { repr(pv)}') + + compare(sv, pv) + + output = step['output'] + if output: + + for k, v in output.items(): + if k == 'json': + sub = json2dict(output.get('json', '{}')) + result = check(sub, response['json']) + vars.put(result['var']) + log.debug(f'json var: {repr(result["var"])}') + else: + vars.put({k: response[v]}) + log.debug(f'output: {vars.output()}') + + +def dedup(text): + ''' + 去掉 text 中括号及其包含的字符 + ''' + _text = '' + n = 0 + + for s in text: + if s not in ('(', ')'): + if n <= 0: + _text += s + elif s == '(': + n += 1 + elif s == ')': + n -= 1 + return _text + + +class DB: + + def __init__(self, arg): + self.connect = '' + self.cursor = '' + self.db = '' + + try: + if arg['type'].lower() == 'mongodb': + import pymongo + host = arg.pop('host') if arg.get( + 'host') else 'localhost:27017' + host = host.split(',') if ',' in host else host + port = int(arg.pop('port')) if arg.get('port') else 27017 + if arg.get('user'): + arg['username'] = arg.pop('user') + # username = arg['user'] if arg.get('user') else '' + # password = arg['password'] if arg.get('password') else '' + # self.connect = pymongo.MongoClient('mongodb://' + username + password + arg['host'] + ':' + arg['port'] + '/') + self.connect = pymongo.MongoClient(host=host, port=port, **arg) + self.connect.server_info() + self.db = self.connect[arg['dbname']] + + return + + sql = '' + if arg['type'].lower() == 'mysql': + import pymysql as mysql + self.connect = mysql.connect( + host=arg['host'], port=int(arg['port']), user=arg['user'], password=arg['password'], database=arg['dbname'], charset=arg.get('charset', 'utf8')) + self.cursor = self.connect.cursor() + sql = 'select version()' + + elif arg['type'].lower() == 'oracle': + import os + import cx_Oracle as oracle + # Oracle查询出的数据,中文输出问题解决 + os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8' + self.connect = oracle.connect( + arg['user'] + '/' + arg['password'] + '@' + arg['host'] + '/' + arg['sid']) + self.cursor = self.connect.cursor() + sql = 'select * from v$version' + elif arg['type'].lower() == 'sqlserver': + import pymssql as sqlserver + self.connect = sqlserver.connect( + host=arg['host'], port=arg['port'], user=arg['user'], password=arg['password'], database=arg['dbname'], charset=arg.get('charset', 'utf8')) + self.cursor = self.connect.cursor() + sql = 'select @@version' + + self.cursor.execute(sql) + self.cursor.fetchone() + + except: + log.exception(f'*** {arg["type"]} connect is failure ***') + raise + + def fetchone(self, sql): + try: + self.cursor.execute(sql) + data = self.cursor.fetchone() + self.connect.commit() + return data + except: + log.exception('*** fetchone failure ***') + raise + + def fetchall(self, sql): + try: + self.cursor.execute(sql) + data = self.cursor.fetchall() + self.connect.commit() + return data + except: + log.exception('*** Fetchall failure ***') + raise + + def execute(self, sql): + try: + self.cursor.execute(sql) + self.connect.commit() + except: + log.exception('*** execute failure ***') + raise + + def mongo(self, collection, sql): + try: + cmd = 'self.db[\'' + collection + '\'].' + sql + result = eval(cmd) + if sql.startswith('find_one'): + return result + elif sql.startswith('find'): + for d in result: + return d + elif 'count' in sql: + return {'count': result} + else: + return {} + except: + log.exception('*** execute failure ***') + raise + + def __del__(self): + self.connect.close() diff --git a/sweet/modules/file.py b/sweet/modules/file.py new file mode 100644 index 0000000..d025600 --- /dev/null +++ b/sweet/modules/file.py @@ -0,0 +1,190 @@ +import os + + +keywords = { + '复制': 'COPY', + 'COPY': 'COPY', + '移动': 'MOVE', + 'MOVE': 'MOVE', + '删除文件': 'REMOVE', + 'REMOVE': 'REMOVE', + '删除目录': 'RMDIR', + 'RMDIR': 'RMDIR', + '创建目录': 'MKDIR', + 'MKDIR': 'MKDIR', + '路径存在': 'EXISTS', + 'EXISTS': 'EXISTS', + '路径不存在': 'NOT_EXISTS', + 'NOT_EXISTS': 'NOT_EXISTS', + '是文件': 'IS_FILE', + 'IS_FILE': 'IS_FILE', + '是目录': 'IS_DIR', + 'IS_DIR': 'IS_DIR', + '不是文件': 'NOT_FILE', + 'NOT_FILE': 'NOT_FILE', + '不是目录': 'NOT_DIR', + 'NOT_DIR': 'NOT_DIR' +} + + +class App: + + keywords = keywords + + def __init__(self, setting): + self.dir = '.' + if 'dir' in setting: + self.dir = setting['dir'] + os.chdir(self.dir) + + def _close(self): + pass + + def _call(self, step): + # 根据关键字调用关键字实现 + getattr(self, step['keyword'].lower())(step) + + + def copy(self, step): + source = step['element'] + data = step['data'] + destination = data['text'] + + if 'dir' in data: + os.chdir(data['dir']) + else: + os.chdir(self.dir) + + code = 0 + if os.name == 'nt': + code = os.system(f'COPY /Y {source} {destination}') + if os.name == 'posix': + code = os.system(f'cp -f -R {source} {destination}') + + if code != 0: + raise Exception( + f'COPY {source} {destination} is failure, code: {code}') + + def move(self, step): + source = step['element'] + data = step['data'] + destination = data['text'] + + if 'dir' in data: + os.chdir(data['dir']) + else: + os.chdir(self.dir) + + code = 0 + if os.name == 'nt': + code = os.system(f'MOVE /Y {source} {destination}') + if os.name == 'posix': + code = os.system(f'mv -f {source} {destination}') + + if code != 0: + raise Exception( + f'MOVE {source} {destination} is failure, code: {code}') + + def remove(self, step): + path = step['element'] + data = step['data'] + + if 'dir' in data: + os.chdir(data['dir']) + else: + os.chdir(self.dir) + + code = 0 + if os.name == 'nt': + code = os.system(f'del /S /Q {path}') + if os.name == 'posix': + code = os.system(f'rm -f {path}') + + if code != 0: + raise Exception(f'REMOVE {path} is failure, code: {code}') + + def rmdir(self, step): + path = step['element'] + data = step['data'] + + if 'dir' in data: + os.chdir(data['dir']) + else: + os.chdir(self.dir) + + code = 0 + if os.name == 'nt': + code = os.system(f'rd /S /Q {path}') + if os.name == 'posix': + code = os.system(f'rm -rf {path}') + + if code != 0: + raise Exception(f'RERMDIR {path} is failure, code: {code}') + + def mkdir(self, step): + path = step['element'] + data = step['data'] + + if 'dir' in data: + os.chdir(data['dir']) + else: + os.chdir(self.dir) + + code = 0 + if os.name == 'nt': + code = os.system(f'mkdir {path}') + if os.name == 'posix': + code = os.system(f'mkdir -p {path}') + + if code != 0: + raise Exception(f'MKDIR {path} is failure, code: {code}') + + def exists(self, step): + path = step['element'] + result = os.path.exists(path) + + if not result: + raise Exception(f'{path} is not exists') + + def not_exists(self, step): + try: + self.exists(step) + except: + pass + else: + path = step['element'] + raise Exception(f'{path} is a exists') + + def is_file(self, step): + path = step['element'] + + result = os.path.isfile(path) + + if not result: + raise Exception(f'{path} is not file') + + def not_file(self, step): + try: + self.is_file(step) + except: + pass + else: + path = step['element'] + raise Exception(f'{path} is a file') + + def is_dir(self, step): + path = step['element'] + + result = os.path.isdir(path) + + if not result: + raise Exception(f'{path} is not dir') + + def not_dir(self, step): + try: + self.is_dir(step) + except: + pass + else: + path = step['element'] + raise Exception(f'{path} is a dir') diff --git a/sweet/modules/http.py b/sweet/modules/http.py new file mode 100644 index 0000000..b81db2d --- /dev/null +++ b/sweet/modules/http.py @@ -0,0 +1,261 @@ +import requests +from pathlib import Path +from injson import check + +from sweet import log, vars +from sweet.utility import json2dict + + +path = Path('lib') / 'http_handle.py' +if path.is_file(): + from lib import http_handle +else: + from sweet.lib import http_handle + + +keywords = { + 'GET': 'GET', + 'POST': 'POST', + 'PUT': 'PUT', + 'PATCH': 'PATCH', + 'DELETE': 'DELETE', + 'OPTIONS': 'OPTIONS' +} + + +class App: + + keywords = keywords + + def __init__(self, setting): + # 获取 path + self.path = setting.get('path', '') + if self.path: + if not self.path.endswith('/'): + self.path += '/' + + self.r = requests.Session() + + # 获取 headers + self.headers = {} + for key in keywords: + if setting.get(key.lower()): + self.headers[key.upper()] = setting.get(key.lower()) + + + def _close(self): + self.r.close() + + def _call(self, step): + # 根据关键字调用关键字实现 + getattr(self, step['keyword'].lower())(step) + + + def get(self, step): + self.request('get', step) + + def post(self, step): + self.request('post', step) + + def put(self, step): + self.request('put', step) + + def patch(self, step): + self.request('patch', step) + + def delete(self, step): + self.request('delete', step) + + def options(self, step): + self.request('options', step) + + + def request(self, kw, step): + url = step['element'] + if url.startswith('/'): + url = url[1:] + + data = step['data'] + # 测试数据解析时,会默认添加一个 text 键,需要删除 + if 'text' in data and not data['text']: + data.pop('text') + + _data = {} + _data['headers'] = json2dict(data.pop('headers', '{}')) + if data.get('cookies'): + data['cookies'] = json2dict(data['cookies']) + if kw == 'get': + _data['params'] = json2dict( + data.pop('params', '{}')) or json2dict(data.pop('data', '{}')) + elif kw == 'post': + if data.get('text'): + _data['data'] = data.pop('text').encode('utf-8') + else: + _data['data'] = json2dict(data.pop('data', '{}')) + _data['json'] = json2dict(data.pop('json', '{}')) + _data['files'] = eval(data.pop('files', 'None')) + elif kw in ('put', 'patch'): + _data['data'] = json2dict(data.pop('data', '{}')) + + for k in data: + for s in ('{', '[', 'False', 'True'): + if s in data[k]: + try: + data[k] = eval(data[k]) + except: + log.warning(f'try eval data failure: {data[k]}') + break + expected = step['expected'] + expected['status_code'] = expected.get('status_code', None) + expected['text'] = expected.get('text', '').encode('utf-8') + expected['json'] = json2dict(expected.get('json', '{}')) + expected['cookies'] = json2dict(expected.get('cookies', '{}')) + expected['headers'] = json2dict(expected.get('headers', '{}')) + timeout = float(expected.get('timeout', 10)) + expected['time'] = float(expected.get('time', 0)) + + for key in keywords: + if kw.upper() == key.upper(): + self.r.headers.update(self.headers[key.upper()]) + + log.debug(f'URL: {self.path + url}') + + # 处理 before_send + before_send = data.pop('before_send', '') + if before_send: + _data, data = getattr(http_handle, before_send)(kw, _data, data) + else: + _data, data = getattr(http_handle, 'before_send')(kw, _data, data) + + if _data['headers']: + for k in [x for x in _data['headers']]: + if not _data['headers'][k]: + del self.r.headers[k] + del _data['headers'][k] + self.r.headers.update(_data['headers']) + + r = '' + if kw == 'get': + r = getattr(self.r, kw)(self.path + url, + params=_data['params'], timeout=timeout, **data) + if _data['params']: + log.debug(f'PARAMS: {_data["params"]}') + + elif kw == 'post': + r = getattr(self.r, kw)(self.path + url, + data=_data['data'], json=_data['json'], files=_data['files'], timeout=timeout, **data) + log.debug(f'BODY: {r.request.body}') + + elif kw in ('put', 'patch'): + r = getattr(self.r, kw)(self.path + url, + data=_data['data'], timeout=timeout, **data) + log.debug(f'BODY: {r.request.body}') + + elif kw in ('delete', 'options'): + r = getattr(self.r, kw)(self.path + url, timeout=timeout, **data) + + log.debug(f'status_code: {repr(r.status_code)}') + try: # json 响应 + log.debug(f'response json: {repr(r.json())}') + except: # 其他响应 + log.debug(f'response text: {repr(r.text)}') + + response = {'status_code': r.status_code, 'headers': r.headers, + '_cookies': r.cookies, 'content': r.content, 'text': r.text} + + try: + response['cookies'] = requests.utils.dict_from_cookiejar(r.cookies) + except: + response['cookies'] = r.cookies + + try: + j = r.json() + response['json'] = j + except: + response['json'] = {} + + # 处理 after_receive + after_receive = expected.pop('after_receive', '') + if after_receive: + response = getattr(http_handle, after_receive)(response) + else: + response = getattr(http_handle, 'after_receive')(response) + + if expected['status_code']: + if str(expected['status_code']) != str(response['status_code']): + raise Exception( + f'status_code | EXPECTED:{repr(expected["status_code"])}, REAL:{repr(response["status_code"])}') + + if expected['text']: + if expected['text'].startswith('*'): + if expected['text'][1:] not in response['text']: + raise Exception( + f'text | EXPECTED:{repr(expected["text"])}, REAL:{repr(response["text"])}') + else: + if expected['text'] == response['text']: + raise Exception( + f'text | EXPECTED:{repr(expected["text"])}, REAL:{repr(response["text"])}') + + if expected['headers']: + result = check(expected['headers'], response['headers']) + log.debug(f'headers check result: {result}') + if result['code'] != 0: + raise Exception( + f'headers | EXPECTED:{repr(expected["headers"])}, REAL:{repr(response["headers"])}, RESULT: {result}') + elif result['var']: + # var.update(result['var']) + vars.put(result['var']) + log.debug(f'headers var: {repr(result["var"])}') + + if expected['cookies']: + log.debug(f'response cookies: {response["cookies"]}') + result = check(expected['cookies'], response['cookies']) + log.debug(f'cookies check result: {result}') + if result['code'] != 0: + raise Exception( + f'cookies | EXPECTED:{repr(expected["cookies"])}, REAL:{repr(response["cookies"])}, RESULT: {result}') + elif result['var']: + # var.update(result['var']) + vars.put(result['var']) + log.debug(f'cookies var: {repr(result["var"])}') + + if expected['json']: + result = check(expected['json'], response['json']) + log.debug(f'json check result: {result}') + if result['code'] != 0: + raise Exception( + f'json | EXPECTED:{repr(expected["json"])}, REAL:{repr(response["json"])}, RESULT: {result}') + elif result['var']: + # var.update(result['var']) + vars.put(result['var']) + log.debug(f'json var: {repr(result["var"])}') + + if expected['time']: + if expected['time'] < r.elapsed.total_seconds(): + raise Exception( + f'time | EXPECTED:{repr(expected["time"])}, REAL:{repr(r.elapsed.total_seconds())}') + + output = step['output'] + # if output: + # log.debug('output: %s' % repr(output)) + + for k, v in output.items(): + if v == 'status_code': + status_code = response['status_code'] + vars.put({k: status_code}) + log.debug(f'{k}: {status_code}') + elif v == 'text': + text = response['text'] + vars.put({k: text}) + log.debug(f'{k}: {text}') + elif k == 'json': + sub = json2dict(output.get('json', '{}')) + result = check(sub, response['json']) + # var.update(result['var']) + vars.put(result['var']) + log.debug(f'json var: {repr(result["var"])}') + elif k == 'cookies': + sub = json2dict(output.get('cookies', '{}')) + result = check(sub, response['cookies']) + vars.put(result['var']) + log.debug(f'cookies var: {repr(result["var"])}') diff --git a/sweet/modules/mobile/__init__.py b/sweet/modules/mobile/__init__.py new file mode 100644 index 0000000..37db2eb --- /dev/null +++ b/sweet/modules/mobile/__init__.py @@ -0,0 +1,2 @@ +from sweet.modules.mobile.app import App + diff --git a/sweet/modules/mobile/app.py b/sweet/modules/mobile/app.py new file mode 100644 index 0000000..b87c880 --- /dev/null +++ b/sweet/modules/mobile/app.py @@ -0,0 +1,482 @@ +from selenium import webdriver +from selenium.common.exceptions import ElementClickInterceptedException +from appium.webdriver.common.touch_action import TouchAction +from time import sleep +import re + +from sweet import log, vars +from sweet.utility import compare, replace, json2dict + +from sweet.modules.mobile.window import Windows +from sweet.modules.web.locator import locating +from sweet.modules.web.config import * + + +class App: + + keywords = keywords + + def __init__(self, setting): + self.action = {} + platform = setting.get('platformName', '') + # snapshot = setting.pop('snapshot', False) + + if platform.lower() == 'ios': + from appium import webdriver as appdriver + self.driver = appdriver.Remote(self.server_url, self.desired_caps) + + elif platform.lower() == 'android': + from appium import webdriver as appdriver + self.driver = appdriver.Remote(self.server_url, self.desired_caps) + + # 等待元素超时时间 + self.driver.implicitly_wait(element_wait_timeout) # seconds + # 页面刷新超时时间 + self.driver.set_page_load_timeout(page_flash_timeout) # seconds + self.w = Windows() + self.w.driver = self.driver + + def _close(self): + pass + + def _call(self, step): + # 处理截图数据 + # snap = Snapshot() + # snap.pre(step) + + context = replace(step.get('frame', '')).strip() + self.w.switch_context(context) + + if self.w.current_context.startswith('WEBVIEW'): + # 切换标签页 + tab = step['data'].get('#tab') + if tab: + del step['data']['#tab'] + self.driver.switch_to_window(self.w.windows[tab]) + log.debug(f'current context: {repr(self.w.current_context)}') + + # 根据关键字调用关键字实现 + element = getattr(self, step['keyword'].lower())(step) + # snap.web_shot(step, element) + + + def title(self, data, output): + log.debug(f'DATA:{repr(data["text"])}') + log.debug(f'REAL:{repr(self.driver.title)}') + + if data['text'].startswith('*'): + assert data['text'][1:] in self.driver.title + else: + assert data['text'] == self.driver.title + # 只能获取到元素标题 + for key in output: + vars.put({key: self.driver.title}) + + + def current_url(self, data, output): + log.debug(f'DATA:{repr(data["text"])}') + log.debug(f'REAL:{repr(self.driver.current_url)}') + try: + if data['text'].startswith('*'): + assert data['text'][1:] in self.driver.current_url + else: + assert data['text'] == self.driver.current_url + except: + raise Exception( + f'check failure, DATA:{data["text"]}, REAL:{self.driver.current_url}') + # 只能获取到元素 url + for key in output: + vars.put({key: self.driver.current_url}) + return self.driver.current_url + + def locat(self, element, action=''): + if not isinstance(element, dict): + raise Exception(f'no this element:{element}') + + + def open(self, step): + url = step['element']['value'] + + if step['data'].get('#clear', ''): + self.driver.delete_all_cookies() + + self.driver.get(url) + + cookie = step['data'].get('cookie', '') + if cookie: + self.driver.add_cookie(json2dict(cookie)) + co = self.driver.get_cookie(json2dict(cookie).get('name', '')) + log.debug(f'cookie is add: {co}') + sleep(0.5) + + + def check(self, step): + data = step['data'] + if not data: + data = step['expected'] + + element = step['element'] + by = element['by'] + output = step['output'] + + if by in ('title', 'current_url'): + getattr(self, by)(data, output) + else: + location = self.locat(element) + for key in data: + # 预期结果 + expected = data[key] + # 切片操作处理 + s = re.findall(r'\[.*?\]', key) + if s: + s = s[0] + key = key.replace(s, '') + + if key == 'text': + real = location.text + else: + real = location.get_attribute(key) + if s: + real = eval('real' + s) + + log.debug(f'DATA:{repr(expected)}') + log.debug(f'REAL:{repr(real)}') + try: + compare(expected, real) + except: + raise Exception( + f'check failure, DATA:{repr(expected)}, REAL:{repr(real)}') + + # 获取元素其他属性 + for key in output: + if output[key] == 'text': + v = location.text + vars.put({key: v}) + elif output[key] in ('text…', 'text...'): + if location.text.endswith('...'): + v = location.text[:-3] + vars.put({key: v}) + else: + v = location.text + vars.put({key: v}) + else: + v = location.get_attribute(output[key]) + vars.put({key: v}) + + + def notcheck(self, step): + try: + self.check(step) + raise Exception('check is success') + except: + pass + + def input(self, step): + data = step['data'] + location = self.locat(step['element']) + + if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': + pass + else: + location.clear() + + for key in data: + if key.startswith('text'): + if isinstance(data[key], tuple): + location.send_keys(*data[key]) + elif location: + location.send_keys(data[key]) + sleep(0.5) + if key == 'word': # 逐字输入 + for d in data[key]: + location.send_keys(d) + sleep(0.3) + + def set_value(self, step): + data = step['data'] + location = self.locat(step['element']) + if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': + pass + else: + location.clear() + + for key in data: + if key.startswith('text'): + if isinstance(data[key], tuple): + location.set_value(*data[key]) + elif location: + location.set_value(data[key]) + sleep(0.5) + if key == 'word': # 逐字输入 + for d in data[key]: + location.set_value(d) + sleep(0.3) + + def click(self, step): + elements = step['elements'] # click 支持多个元素连续操作,需要转换为 list + # data = step['data'] + + location = '' + for element in elements: + location = self.locat(element, 'CLICK') + sleep(0.5) + try: + location.click() + except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 + sleep(1) + location.click() + sleep(0.5) + + # 获取元素其他属性 + output = step['output'] + for key in output: + if output[key] == 'text': + vars.put({key: location.text}) + elif output[key] == 'tag_name': + vars.put({key: location.tag_name}) + elif output[key] in ('text…', 'text...'): + if location.text.endswith('...'): + vars.put({key: location.text[:-3]}) + else: + vars.put({key: location.text}) + else: + vars.put({key: location.get_attribute(output[key])}) + + def tap(self, step): + action = TouchAction(self.driver) + + elements = step['elements'] # click 支持多个元素连续操作,需要转换为 list + # data = step['data'] + + location = '' + + for element in elements: + if ',' in element: + position = element.split(',') + x = int(position[0]) + y = int(position[1]) + position = (x, y) + self.driver.tap([position]) + sleep(0.5) + else: + location = self.locat(element, 'CLICK') + action.tap(location).perform() + sleep(0.5) + + # 获取元素其他属性 + output = step['output'] + for key in output: + if output[key] == 'text': + vars.put({key: location.text}) + elif output[key] == 'tag_name': + vars.put({key: location.tag_name}) + elif output[key] in ('text…', 'text...'): + if location.text.endswith('...'): + vars.put({key: location.text[:-3]}) + else: + vars.put({key: location.text}) + else: + vars.put({key: location.get_attribute(output[key])}) + + def press_keycode(self, step): + element = step['element'] + self.driver.press_keycode(int(element)) + + def swipe(self, step): + elements = step['elements'] + duration = step['data'].get('持续时间', 0.3) + assert isinstance(elements, list) and len( + elements) == 2, '坐标格式或数量不对,正确格式如:100,200|300,400' + + start = elements[0].replace(',', ',').split(',') + start_x = int(start[0]) + start_y = int(start[1]) + + end = elements[1].replace(',', ',').split(',') + end_x = int(end[0]) + end_y = int(end[1]) + + if duration: + self.driver.swipe(start_x, start_y, end_x, + end_y, sleep(float(duration))) + else: + self.driver.swipe(start_x, start_y, end_x, end_y) + + def line(self, step): + elements = step['elements'] + duration = float(step['data'].get('持续时间', 0.3)) + assert isinstance(elements, list) and len( + elements) > 1, '坐标格式或数量不对,正确格式如:258,756|540,1032' + postions = [] + for element in elements: + element = element.replace(',', ',') + p = element.split(',') + postions.append(p) + + action = TouchAction(self.driver) + action = action.press( + x=postions[0][0], y=postions[0][1]).wait(duration * 1000) + for i in range(1, len(postions)): + action.move_to(x=postions[i][0], y=postions[i] + [1]).wait(duration * 1000) + action.release().perform() + + def line_unlock(self, step): + elements = step['elements'] + duration = float(step['data'].get('持续时间', 0.3)) + assert isinstance(elements, list) and len( + elements) > 2, '坐标格式或数量不对,正确格式如:lock_pattern|1|4|7|8|9' + location = self.locat(elements[0]) + rect = location.rect + w = rect['width'] / 6 + h = rect['height'] / 6 + + key = {} + key['1'] = (rect['x'] + 1 * w, rect['y'] + 1 * h) + key['2'] = (rect['x'] + 3 * w, rect['y'] + 1 * h) + key['3'] = (rect['x'] + 5 * w, rect['y'] + 1 * h) + key['4'] = (rect['x'] + 1 * w, rect['y'] + 3 * h) + key['5'] = (rect['x'] + 3 * w, rect['y'] + 3 * h) + key['6'] = (rect['x'] + 5 * w, rect['y'] + 3 * h) + key['7'] = (rect['x'] + 1 * w, rect['y'] + 5 * h) + key['8'] = (rect['x'] + 3 * w, rect['y'] + 5 * h) + key['9'] = (rect['x'] + 5 * w, rect['y'] + 5 * h) + + action = TouchAction(self.driver) + for i in range(1, len(elements)): + k = elements[i] + if i == 1: + action = action.press( + x=key[k][0], y=key[k][1]).wait(duration * 1000) + action.move_to(x=key[k][0], y=key[k][1]).wait(duration * 1000) + action.release().perform() + + def rocker(self, step): + elements = step['elements'] + duration = float(step['data'].get('持续时间', 0.3)) + rocker_name = step['data'].get('摇杆', 'rocker') + release = step['data'].get('释放', False) + + # if isinstance(element, str): + # if element: + # element = [element] + # else: + # element = [] + + postions = [] + for element in elements: + element = element.replace(',', ',') + p = element.split(',') + postions.append(p) + + # 如果 action 中么有此摇杆名,则是新的遥感 + if not self.action.get(rocker_name): + self.action[rocker_name] = TouchAction(self.driver) + self.action[rocker_name].press( + x=postions[0][0], y=postions[0][1]).wait(duration * 1000) + # 新摇杆的第一个点已操作,需要删除 + postions.pop(0) + # 依次操作 + for i in range(len(postions)): + self.action[rocker_name].move_to( + x=postions[i][0], y=postions[i][1]).wait(duration * 1000) + + if release: + # 释放摇杆,并删除摇杆 + self.action[rocker_name].release().perform() + del self.action[rocker_name] + else: + self.action[rocker_name].perform() + + def scroll(self, step): + elements = step['elements'] + assert isinstance(elements, list) and len( + elements) == 2, '元素格式或数量不对,正确格式如:origin_el|destination_el' + origin = self.locat(elements[0]) + destination = self.locat(elements[1]) + self.driver.scroll(origin, destination) + + def flick_element(self, step): + elements = step['elements'] + speed = step['data'].get('持续时间', 10) + assert isinstance(elements, list) and len( + elements) == 2, '坐标格式或数量不对,正确格式如:elment|200,300' + location = self.locat(elements[0]) + + end = elements[1].replace(',', ',').split(',') + end_x = int(end[0]) + end_y = int(end[1]) + + if speed: + self.driver.flick_element(location, end_x, end_y, int(speed)) + + def flick(self, step): + elements = step['elements'] + assert isinstance(elements, list) and len( + elements) == 2, '坐标格式或数量不对,正确格式如:100,200|300,400' + + start = elements[0].replace(',', ',').split(',') + start_x = int(start[0]) + start_y = int(start[1]) + + end = elements[1].replace(',', ',').split(',') + end_x = int(end[0]) + end_y = int(end[1]) + + self.driver.flick(start_x, start_y, end_x, end_y) + + def drag_and_drop(self, step): + elements = step['elements'] + assert isinstance(elements, list) and len( + elements) == 2, '元素格式或数量不对,正确格式如:origin_el|destination_el' + origin = self.locat(elements[0]) + destination = self.locat(elements[1]) + self.driver.drag_and_drop(origin, destination) + + def long_press(self, step): + action = TouchAction(self.driver) + + element = step['element'] + duration = step['data'].get('持续时间', 1000) + if ',' in element or ',' in element: + position = element.replace(',', ',').split(',') + x = int(position[0]) + y = int(position[1]) + action.long_press(x=x, y=y, duration=duration).perform() + else: + location = self.locat(element) + action.long_press(location, duration=duration).perform() + sleep(0.5) + + def pinch(self, step): + element = step['element'] + location = self.locat(element) + percent = step['data'].get('百分比', 200) + steps = step['data'].get('步长', 50) + self.driver.pinch(location, percent, steps) + + def zoom(self, step): + element = step['element'] + location = self.locat(element) + percent = step['data'].get('百分比', 200) + steps = step['data'].get('步长', 50) + self.driver.zoom(location, percent, steps) + + def hide_keyboard(self, step): + self.driver.hide_keyboard() + + def shake(self, step): + self.driver.shake() + + def launch_app(self, step): + self.driver.launch_app() + + def is_locked(self, step): + status = self.driver.is_locked() + assert status, "it's not locked" + + def lock(self, step): + self.driver.lock() + + def unlock(self, step): + self.driver.unlock() \ No newline at end of file diff --git a/sweet/modules/mobile/config.py b/sweet/modules/mobile/config.py new file mode 100644 index 0000000..6bb76d7 --- /dev/null +++ b/sweet/modules/mobile/config.py @@ -0,0 +1,58 @@ + + +element_wait_timeout = 10 # 等待元素出现超时时间,单位:秒 +page_flash_timeout = 90 # 页面刷新超时时间,单位:秒 + + +keywords = { + '检查': 'CHECK', + 'CHECK': 'CHECK', + '#检查': 'NOTCHECK', + '#CHECK': 'NOTCHECK', + '输入': 'INPUT', + 'INPUT': 'INPUT', + '填写': 'SET_VALUE', + 'SET_VALUE': 'SET_VALUE', + '点击': 'CLICK', + 'CLICK': 'CLICK', + '轻点': 'TAP', + 'TAP': 'TAP', + '按键码': 'PRESS_KEYCODE', # Android 特有,常见代码 HOME:3, 菜单键:82,返回键:4 + 'PRESS_KEYCODE': 'PRESS_KEYCODE', + '滑动': 'SWIPE', + 'SWIPE': 'SWIPE', + '划线': 'LINE', + 'LINE': 'LINE', + '划线解锁': 'LINE_UNLOCK', + 'LINE_UNLOCK': 'LINE_UNLOCK', + '摇杆': 'ROCKER', + 'ROCKER': 'ROCKER', + '滚动': 'SCROLL', # iOS 专用 + 'SCROLL': 'SCROLL', + '拖拽': 'DRAG_AND_DROP', + 'DRAG_AND_DROP': 'DRAG_AND_DROP', + '摇晃': 'SHAKE', # 貌似 Android 上不可用 + 'SHAKE': 'SHAKE', + '快速滑动': 'FLICK', + 'FLICK': 'FLICK', + '滑动元素': 'FLICK_ELEMENT', + 'FLICK_ELEMENT': 'FLICK_ELEMENT', + '长按': 'LONG_PRESS', + 'LONG_PRESS': 'LONG_PRESS', + '缩小': 'PINCH', + 'PINCH': 'PINCH', + '放大': 'ZOOM', + 'ZOOM': 'ZOOM', + '隐藏键盘': 'HIDE_KEYBOARD', # iOS 专用 + 'HIDE_KEYBOARD': 'HIDE_KEYBOARD', + '命名标签页': 'TAB_NAME', + 'TAB_NAME': 'TAB_NAME', + '重启': 'LAUNCH_APP', + 'LAUNCH_APP': 'LAUNCH_APP', + '锁屏状态': 'IS_LOCKED', + 'IS_LOCKED': 'IS_LOCKED', + '锁屏': 'LOCK', + 'LOCK': 'LOCK', + '解锁': 'UNLOCK', + 'UNLOCK': 'UNLOCK', +} \ No newline at end of file diff --git a/sweet/modules/mobile/locator.py b/sweet/modules/mobile/locator.py new file mode 100644 index 0000000..0d5d10e --- /dev/null +++ b/sweet/modules/mobile/locator.py @@ -0,0 +1,70 @@ +from time import sleep +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from sweet import log +from sweet.config import element_wait_timeout + + +def locating(driver, app, element, action=''): + location = None + try: + el= element + value = el['value'] + except: + log.exception(f'locating the element:{element} is failure, this element is not define') + raise Exception(f'locating the element:{element} is failure, this element is not define') + + if not isinstance(el, dict): + raise Exception(f'locating the element:{element} is failure, this element is not define') + + wait = WebDriverWait(driver, element_wait_timeout) + + if el['by'].lower() in ('title', 'url', 'current_url'): + return None + else: + try: + location = wait.until(EC.presence_of_element_located( + (getattr(By, el['by'].upper()), value))) + except: + sleep(5) + try: + location = wait.until(EC.presence_of_element_located( + (getattr(By, el['by'].upper()), value))) + except : + raise Exception(f'locating the element:{element} is failure: timeout') + try: + if driver.name in ('chrome', 'safari'): + driver.execute_script( + "arguments[0].scrollIntoViewIfNeeded(true)", location) + else: + driver.execute_script( + "arguments[0].scrollIntoView(false)", location) + except: + pass + + try: + if action == 'CLICK': + location = wait.until(EC.element_to_be_clickable( + (getattr(By, el['by'].upper()), value))) + else: + location = wait.until(EC.visibility_of_element_located( + (getattr(By, el['by'].upper()), value))) + except: + pass + + return location + + +def locatings(elements): + locations = {} + for el in elements: + locations[el] = locating(el) + return locations + + +# def locating_data(keys): +# data_location = {} +# for key in keys: +# data_location[key] = locating(key) +# return data_location diff --git a/sweet/modules/mobile/window.py b/sweet/modules/mobile/window.py new file mode 100644 index 0000000..e852fa0 --- /dev/null +++ b/sweet/modules/mobile/window.py @@ -0,0 +1,17 @@ +from sweet import log + +class Windows: + + def __init__(self): + + self.current_context = 'NATIVE_APP' + + def switch_context(self, context): + if context.strip() == '': + context = 'NATIVE_APP' + if context != self.current_context: + if context == '': + context = None + log.debug(f'switch context: {repr(context)}') + self.driver.switch_to.context(context) + self.current_context = context diff --git a/sweet/modules/web/__init__.py b/sweet/modules/web/__init__.py new file mode 100644 index 0000000..a99e5b5 --- /dev/null +++ b/sweet/modules/web/__init__.py @@ -0,0 +1,2 @@ +from sweet.modules.web.app import App + diff --git a/sweet/modules/web/app.py b/sweet/modules/web/app.py new file mode 100644 index 0000000..bc134cb --- /dev/null +++ b/sweet/modules/web/app.py @@ -0,0 +1,421 @@ +from selenium import webdriver +from selenium.webdriver.common.action_chains import ActionChains +from selenium.common.exceptions import ElementClickInterceptedException +from selenium.webdriver.support.select import Select +from time import sleep +import re + +from sweet import log, vars +from sweet.utility import compare, replace, json2dict + +from sweet.modules.web.window import Windows +from sweet.modules.web.locator import locating +from sweet.modules.web.config import * + + +class App: + + keywords = keywords + + def __init__(self, setting): + browserName = setting.get('browserName', '') + headless = setting.pop('headless', False) + # snapshot = setting.pop('snapshot', False) + executable_path = setting.pop('executable_path', False) + # server_url = setting.pop('server_url', '') + + if browserName.lower() == 'ie': + if executable_path: + self.driver = webdriver.Ie(executable_path=executable_path) + else: + self.driver = webdriver.Ie() + elif browserName.lower() == 'firefox': + profile = webdriver.FirefoxProfile() + profile.accept_untrusted_certs = True + + options = webdriver.FirefoxOptions() + # 如果配置了 headless 模式 + if headless: + options.set_headless() + # options.add_argument('-headless') + options.add_argument('--disable-gpu') + options.add_argument("--no-sandbox") + options.add_argument('window-size=1920x1080') + + if executable_path: + self.driver = webdriver.Firefox( + firefox_profile=profile, firefox_options=options, executable_path=executable_path) + else: + self.driver = webdriver.Firefox( + firefox_profile=profile, firefox_options=options) + self.driver.maximize_window() + elif browserName.lower() == 'chrome': + options = webdriver.ChromeOptions() + + # 如果配置了 headless 模式 + if headless: + options.add_argument('--headless') + options.add_argument('--disable-gpu') + options.add_argument("--no-sandbox") + options.add_argument('window-size=1920x1080') + + options.add_argument("--start-maximized") + options.add_argument('--ignore-certificate-errors') + # 指定浏览器分辨率,当"--start-maximized"无效时使用 + # options.add_argument('window-size=1920x1080') + prefs = {} + prefs["credentials_enable_service"] = False + prefs["profile.password_manager_enabled"] = False + options.add_experimental_option("prefs", prefs) + options.add_argument('disable-infobars') + options.add_experimental_option( + "excludeSwitches", ['load-extension', 'enable-automation', 'enable-logging']) + if executable_path: + self.driver = webdriver.Chrome( + options=options, executable_path=executable_path) + else: + self.driver = webdriver.Chrome(options=options) + else: + raise Exception( + 'Error: this browser is not supported or mistake name:%s' % browserName) + # 等待元素超时时间 + self.driver.implicitly_wait(element_wait_timeout) # seconds + # 页面刷新超时时间 + self.driver.set_page_load_timeout(page_flash_timeout) # seconds + self.w = Windows() + self.w.driver = self.driver + + def _close(self): + self.w.close() + + def _call(self, step): + # 处理截图数据 + # snap = Snapshot() + # snap.pre(step) + + name = step['data'].pop('#tab', '') + if name: + self.w.tab(name) + else: + self.w.switch() + + frame = replace(step.get('frame', '')) + self.w.switch_frame(frame) + + # 根据关键字调用关键字实现 + element = getattr(self, step['keyword'].lower())(step) + # snap.web_shot(step, element) + + + def title(self, data, output): + log.debug(f'DATA:{repr(data["text"])}') + log.debug(f'REAL:{repr(self.driver.title)}') + # try: + if data['text'].startswith('*'): + assert data['text'][1:] in self.driver.title + else: + assert data['text'] == self.driver.title + + for key in output: + vars.put({key: self.driver.title}) + + + def current_url(self, data, output): + log.debug(f'DATA:{repr(data["text"])}') + log.debug(f'REAL:{repr(self.driver.current_url)}') + try: + if data['text'].startswith('*'): + assert data['text'][1:] in self.driver.current_url + else: + assert data['text'] == self.driver.current_url + except: + raise Exception( + f'check failure, DATA:{data["text"]}, REAL:{self.driver.current_url}') + # 只能获取到元素 url + for key in output: + vars.put({key: self.driver.current_url}) + + def locat(self, element, action=''): + if not isinstance(element, dict): + raise Exception(f'no this element:{element}') + return locating(self.driver, element, action=action) + + def open(self, step): + if isinstance(step['element'], dict): + url = step['element']['value'] + else: + url = step['element'] + + if step['data'].get('#clear', ''): + self.driver.delete_all_cookies() + + self.driver.get(url) + + cookie = step['data'].get('cookie', '') + if cookie: + self.driver.add_cookie(json2dict(cookie)) + co = self.driver.get_cookie(json2dict(cookie).get('name', '')) + log.debug(f'cookie is add: {co}') + sleep(0.5) + + def check(self, step): + data = step['data'] + if not data: + data = step['expected'] + element = step['element'] + by = element['by'] + output = step['output'] + + location = '' + if by in ('title', 'current_url'): + getattr(self, by)(data, output) + else: + location = self.locat(element) + for key in data: + # 预期结果 + expected = data[key] + # 切片操作处理 + s = re.findall(r'\[.*?\]', key) + if s: + s = s[0] + key = key.replace(s, '') + + if key == 'text': + real = location.text + else: + real = location.get_attribute(key) + if s: + real = eval('real' + s) + + log.debug(f'DATA:{repr(expected)}') + log.debug('REAL:{repr(real)}') + try: + compare(expected, real) + except: + raise Exception( + f'check failure, DATA:{repr(expected)}, REAL:{repr(real)}') + + # 获取元素其他属性 + for key in output: + if output[key] == 'text': + v = location.text + vars.put({key: v}) + elif output[key] in ('text…', 'text...'): + if location.text.endswith('...'): + v = location.text[:-3] + vars.put({key: v}) + else: + v = location.text + vars.put({key: v}) + else: + v = location.get_attribute(output[key]) + vars.put({key: v}) + + return location + + + def notcheck(self, step): + try: + self.check(step) + raise Exception('check is success') + except: + pass + + def input(self, step): + data = step['data'] + location = self.locat(step['element']) + + if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': + pass + else: + location.clear() + + for key in data: + if key.startswith('text'): + if isinstance(data[key], tuple): + location.send_keys(*data[key]) + elif location: + location.send_keys(data[key]) + sleep(0.5) + if key == 'word': # 逐字输入 + for d in data[key]: + location.send_keys(d) + sleep(0.3) + return location + + def click(self, step): + data = step['data'] + + location = '' + for element in step.get('elements'): + # location = locating(self.driver, element, 'CLICK') + location = self.locat(element, 'CLICK') + try: + location.click() + except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 + sleep(1) + if data.get('mode'): + self.driver.execute_script( + "arguments[0].click();", location) + else: + location.click() + sleep(0.5) + + # 获取元素其他属性 + output = step['output'] + for key in output: + if output[key] == 'text': + vars.put({key: location.text}) + elif output[key] == 'tag_name': + vars.put({key: location.tag_name}) + elif output[key] in ('text…', 'text...'): + if location.text.endswith('...'): + vars.put({key: location.text[:-3]}) + else: + vars.put({key: location.text}) + else: + vars.put({key: location.get_attribute(output[key])}) + + return location + + def select(self, step): + data = step['data'] + + location = self.locat(step['element']) + for key in data: + if key.startswith('index'): + Select(location).select_by_index(data[key]) + elif key.startswith('value'): + Select(location).select_by_value(data[key]) + elif key.startswith('text') or key.startswith('visible_text'): + Select(location).select_by_visible_text(data[key]) + + def deselect(self, step): + data = step['data'] + location = self.locat(step['element']) + for key in data: + if key.startswith('all'): + Select(location).deselect_all() + elif key.startswith('index'): + Select(location).deselect_by_index(data[key]) + elif key.startswith('value'): + Select(location).deselect_by_value(data[key]) + elif key.startswith('text') or key.startswith('visible_text'): + Select(location).deselect_by_visible_text(data[key]) + + def hover(self, step): + actions = ActionChains(self.driver) + location = self.locat(step['element']) + actions.move_to_element(location) + actions.perform() + sleep(0.5) + + return location + + def context_click(self, step): + actions = ActionChains(self.driver) + location = self.locat(step['element']) + actions.context_click(location) + actions.perform() + sleep(0.5) + + return location + + def double_click(self, step): + actions = ActionChains(self.driver) + location = self.locat(step['element']) + actions.double_click(location) + actions.perform() + sleep(0.5) + + return location + + def drag_and_drop(self, step): + actions = ActionChains(self.driver) + elements = step['elements'] + source = self.locat(elements[0]) + target = self.locat(elements[1]) + actions.drag_and_drop(source, target) + actions.perform() + sleep(0.5) + + def swipe(self, step): + actions = ActionChains(self.driver) + data = step['data'] + location = self.locat(step['element']) + x = data.get('x', 0) + y = data.get('y', 0) + actions.drag_and_drop_by_offset(location, x, y) + actions.perform() + sleep(0.5) + + def script(self, step): + element = step['element'] + self.driver.execute_script(element) + + def message(self, step): + data = step['data'] + text = data.get('text', '') + value = step['element'] + + if value.lower() in ('确认', 'accept'): + self.driver.switch_to_alert().accept() + elif value.lower() in ('取消', '关闭', 'cancel', 'close'): + self.driver.switch_to_alert().dismiss() + elif value.lower() in ('输入', 'input'): + self.driver.switch_to_alert().send_keys(text) + self.driver.switch_to_alert().accept() + log.debug('switch frame: Alert') + self.w.frame = 'Alert' + + def upload(self, step): + import win32com.client + + data = step['data'] + location = self.locat(step['element']) + file_path = data.get('text', '') or data.get('file', '') + + location.click() + sleep(3) + shell = win32com.client.Dispatch("WScript.Shell") + shell.Sendkeys(file_path) + sleep(2) + shell.Sendkeys("{ENTER}") + sleep(2) + + def navigate(self, step): + element = step['element'] + + if element.lower() in ('刷新', 'refresh'): + self.driver.refresh() + elif element.lower() in ('前进', 'forward'): + self.driver.forward() + elif element.lower() in ('后退', 'back'): + self.driver.back() + + def scroll(self, step): + data = step['data'] + x = data.get('x') + y = data.get('y') or data.get('text') + + element = step['element'] + if element == '': + # if x is None: + # x = '0' + # self.driver.execute_script( + # f"windoself.w.scrollTo({x},{y})") + if y: + self.driver.execute_script( + f"document.documentElement.scrollTop={y}") + if x: + self.driver.execute_script( + f"document.documentElement.scrollLeft={x}") + else: + location = self.locat(element) + + if y: + self.driver.execute_script( + f"arguments[0].scrollTop={y}", location) + if x: + self.driver.execute_script( + f"arguments[0].scrollLeft={x}", location) diff --git a/sweet/modules/web/config.py b/sweet/modules/web/config.py new file mode 100644 index 0000000..9426928 --- /dev/null +++ b/sweet/modules/web/config.py @@ -0,0 +1,41 @@ +element_wait_timeout = 10 # 等待元素出现超时时间,单位:秒 +page_flash_timeout = 90 # 页面刷新超时时间,单位:秒 + + +keywords = { + '打开': 'OPEN', + 'OPEN': 'OPEN', + '检查': 'CHECK', + 'CHECK': 'CHECK', + '#检查': 'NOTCHECK', + '#CHECK': 'NOTCHECK', + '输入': 'INPUT', + 'INPUT': 'INPUT', + '点击': 'CLICK', + 'CLICK': 'CLICK', + '选择': 'SELECT', + 'SELECT': 'SELECT', + '取消选择': 'DESELECT', + 'DESELECT': 'DESELECT', + '移动到': 'HOVER', + '悬停': 'HOVER', + 'HOVER': 'HOVER', + '右击': 'CONTEXT_CLICK', + 'CONTEXT_CLICK': 'CONTEXT_CLICK', + '双击': 'DOUBLE_CLICK', + 'DOUBLE_CLICK': 'DOUBLE_CLICK', + '拖拽': 'DRAG_AND_DROP', + 'DRAG_AND_DROP': 'DRAG_AND_DROP', + '滑动': 'SWIPE', + 'SWIPE': 'SWIPE', + '脚本': 'SCRIPT', + 'SCRIPT': 'SCRIPT', + '对话框': 'MESSAGE', + 'MESSAGE': 'MESSAGE', + '上传文件': 'UPLOAD', + 'UPLOAD': 'UPLOAD', + '导航': 'NAVIGATE', + 'NAVIGATE': 'NAVIGATE', + '滚动条': 'SCROLL', + 'SCROLL': 'SCROLL' +} \ No newline at end of file diff --git a/sweet/modules/web/locator.py b/sweet/modules/web/locator.py new file mode 100644 index 0000000..00634a5 --- /dev/null +++ b/sweet/modules/web/locator.py @@ -0,0 +1,70 @@ +from time import sleep +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from sweet import log +from sweet.modules.web.config import element_wait_timeout + + +def locating(driver, element, action=''): + location = None + try: + el= element + value = el['value'] + except: + log.exception(f'locating the element:{element} is failure, this element is not define') + raise Exception(f'locating the element:{element} is failure, this element is not define') + + if not isinstance(el, dict): + raise Exception(f'locating the element:{element} is failure, this element is not define') + + wait = WebDriverWait(driver, element_wait_timeout) + + if el['by'].lower() in ('title', 'url', 'current_url'): + return None + else: + try: + location = wait.until(EC.presence_of_element_located( + (getattr(By, el['by'].upper()), value))) + except: + sleep(5) + try: + location = wait.until(EC.presence_of_element_located( + (getattr(By, el['by'].upper()), value))) + except : + raise Exception(f'locating the element:{element} is failure: timeout') + try: + if driver.name in ('chrome', 'safari'): + driver.execute_script( + "arguments[0].scrollIntoViewIfNeeded(true)", location) + else: + driver.execute_script( + "arguments[0].scrollIntoView(false)", location) + except: + pass + + try: + if action == 'CLICK': + location = wait.until(EC.element_to_be_clickable( + (getattr(By, el['by'].upper()), value))) + else: + location = wait.until(EC.visibility_of_element_located( + (getattr(By, el['by'].upper()), value))) + except: + pass + + return location + + +# def locations(elements): +# locations = {} +# for el in elements: +# locations[el] = location(el) +# return locations + + +# def locating_data(keys): +# data_location = {} +# for key in keys: +# data_location[key] = location(key) +# return data_location diff --git a/sweet/modules/web/window.py b/sweet/modules/web/window.py new file mode 100644 index 0000000..39cd175 --- /dev/null +++ b/sweet/modules/web/window.py @@ -0,0 +1,98 @@ +from sweet import log + + +class Windows: + + def __init__(self): + self.current_window = '' + self.windows = {} + self.frame = 0 + + def tab(self, name): + current_handle = self.driver.current_window_handle + if name in self.windows: + if current_handle != self.windows[name]: + self.driver.switch_to_window(self.windows[name]) + log.debug(f'switch the windows: #tab:{name}, handle:{repr(self.windows[name])}') + else: + log.debug(f'current windows: #tab:{name}, handle:{repr(self.windows[name])}') + + else: + all_handles = self.driver.window_handles + for handle in all_handles: + if handle not in self.windows.values(): + self.windows[name] = handle + if handle != current_handle: + self.driver.switch_to_window(handle) + log.debug(f'switch the windows: #tab:{name}, handle:{repr(handle)}') + else: + log.debug(f'current windows: #tab:{name}, handle:{repr(current_handle)}') + + self.clear() + + def clear(self): # 关闭未命名的 windows + current_handle = self.driver.current_window_handle + current_name = '' + for name in self.windows: + if current_handle == self.windows[name]: + current_name = name + + all_handles = self.driver.window_handles + for handle in all_handles: + # 未命名的 handle + if handle not in self.windows.values(): + # 切换到每一个窗口,并关闭它 + self.driver.switch_to_window(handle) + log.debug(f'switch the windows: #tab:, handle:{repr(handle)}') + self.driver.close() + log.debug(f'close the windows: #tab:, handle:{repr(handle)}') + self.driver.switch_to_window(current_handle) + log.debug(f'switch the windows: #tab:{current_name}, handle:{repr(current_handle)}') + + + def switch(self): + """ + docstring + """ + current_handle = self.driver.current_window_handle + use_handles = list(self.windows.values()) + [self.driver.current_window_handle] + all_handles = self.driver.window_handles + for handle in all_handles: + # 未命名的 handle + if handle not in self.windows.values(): + # 切换到新窗口 + self.driver.switch_to_window(handle) + log.debug(f'switch the windows: #tab:, handle:{repr(handle)}') + + + def switch_frame(self, frame): + if frame.strip(): + frame = [x.strip() for x in frame.split('|')] + if frame != self.frame: + if self.frame != 0: + self.driver.switch_to.default_content() + for f in frame: + log.debug(f'frame value: {repr(f)}') + if f.startswith('#'): + f = int(f[1:]) + elif '#' in f: + from sweet.testcase import elements_format + from sweet.modules.web.locator import locating_element + element = elements_format('public', f)[2] + f = locating_element(element) + log.debug(f' switch frame: {repr(f)}') + self.driver.switch_to.frame(f) + self.frame = frame + else: + if self.frame != 0: + self.driver.switch_to.default_content() + self.frame = 0 + + + def close(self): + all_handles = self.driver.window_handles + for handle in all_handles: + # 切换到每一个窗口,并关闭它 + self.driver.switch_to_window(handle) + self.driver.close() + log.debug(f'close th windows: {repr(handle)}') diff --git a/sweetest/Junit/Baidu-Report.xml b/sweetest/Junit/Baidu-Report.xml deleted file mode 100644 index f6a9075..0000000 --- a/sweetest/Junit/Baidu-Report.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/sweetest/data/Baidu-baidu.csv b/sweetest/data/Baidu-baidu.csv deleted file mode 100644 index 1e5f4b1..0000000 --- a/sweetest/data/Baidu-baidu.csv +++ /dev/null @@ -1,3 +0,0 @@ -_keywords,_title,flag -segmentfault,SegmentFault ˼,Y -,, diff --git a/sweetest/element/Baidu-Elements.xlsx b/sweetest/element/Baidu-Elements.xlsx deleted file mode 100644 index ddfe198..0000000 Binary files a/sweetest/element/Baidu-Elements.xlsx and /dev/null differ diff --git a/sweetest/log/20180423.log b/sweetest/log/20180423.log deleted file mode 100644 index e69de29..0000000 diff --git a/sweetest/report/Baidu-Report@20180307_144216.xlsx b/sweetest/report/Baidu-Report@20180307_144216.xlsx deleted file mode 100644 index 0c07ca0..0000000 Binary files a/sweetest/report/Baidu-Report@20180307_144216.xlsx and /dev/null differ diff --git a/sweetest/start.py b/sweetest/start.py deleted file mode 100644 index 195eebb..0000000 --- a/sweetest/start.py +++ /dev/null @@ -1,47 +0,0 @@ -from sweetest import Autotest -import sys - - -# 项目名称,和测试用例、页面元素表文件名称中的项目名称必须一致 -plan_name = 'Baidu' - -# 单 sheet 页面模式 -sheet_name = 'baidu' - -# sheet 页面匹配模式,仅支持结尾带* -#sheet_name = 'TestCase*' - -# sheet 页面列表模式 -#sheet_name = ['TestCase', 'test'] - -# 环境配置信息 -# Chrome -desired_caps = {'platformName': 'Desktop', 'browserName': 'Chrome'} -# headless -#desired_caps = {'platformName': 'Desktop', 'browserName': 'Chrome', 'headless': True} -# 设置全局截图 -#desired_caps = {'platformName': 'Desktop', 'browserName': 'Chrome', 'snapshot': True} -# 指定 driver 路径 -#desired_caps = {'platformName': 'Desktop', 'browserName': 'Chrome', 'executable_path': 'D:\drivers\chromedriver.exe'} -server_url = '' - -# Windows GUI -# notepad start -#desired_caps = {'platformName': 'Windows', 'cmd_line': r'notepad.exe', 'timeout': 5, 'backend': 'uia'} -# notepad connect -#desired_caps = {'platformName': 'Windows', 'path': r'C:\Program Files\Microsoft Office\Office16\EXCEL.EXE'} - -# 初始化自动化实例 -sweet = Autotest(plan_name, sheet_name, desired_caps, server_url) - -# 按条件执行,支持筛选的属性有:'id', 'title', 'designer', 'priority' -# sweet.fliter(priority='H') - -# 执行自动化测试 -sweet.plan() - -# Web 测试报告 -sweet.report(r'D:\report') - -# 如果是集成到 CI/CD,则给出退出码 -#sys.exit(sweet.code) diff --git a/sweetest/sweetest/__init__.py b/sweetest/sweetest/__init__.py deleted file mode 100644 index aaee9a4..0000000 --- a/sweetest/sweetest/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys -import shutil -import zipfile -from pathlib import Path -from sweetest.autotest import Autotest -from sweetest.report import reporter - - -def extract(zfile, path): - f = zipfile.ZipFile(zfile, 'r') - for file in f.namelist(): - f.extract(file, path) - - -def sweetest(): - sweetest_dir = Path(__file__).resolve().parents[0] - example_dir = sweetest_dir / 'example' / 'sweetest_example.zip' - extract(str(example_dir), Path.cwd()) - - print('\n文档: https://sweeter.io\n公众号:喜文测试\nQ Q 群:941761748 (验证码:python)注意首字母小写') - print('\n\n生成 sweetest example 成功\n快速体验,请输入如下命令(进入示例目录,启动运行脚本):\n\ncd sweetest_example\npython start.py') - - -def report(): - sweetest_dir = Path(__file__).resolve().parents[0] - report_dir = sweetest_dir / 'example' / 'report.zip' - extract(str(report_dir), Path.cwd()) \ No newline at end of file diff --git a/sweetest/sweetest/autotest.py b/sweetest/sweetest/autotest.py deleted file mode 100644 index e84a194..0000000 --- a/sweetest/sweetest/autotest.py +++ /dev/null @@ -1,146 +0,0 @@ - -from pathlib import Path -import sys -import json -from sweetest.data import testsuite_format, testsuite2data, testsuite2report -from sweetest.parse import parse -from sweetest.elements import e -from sweetest.globals import g -from sweetest.windows import w -from sweetest.testsuite import TestSuite -from sweetest.utility import Excel, get_record, mkdir -from sweetest.log import logger, set_log -from sweetest.junit import JUnit -from sweetest.report import summary, markdown -from sweetest.config import _testcase, _elements, _report - - -class Autotest: - def __init__(self, file_name, sheet_name, desired_caps={}, server_url=''): - if desired_caps: - self.desired_caps = desired_caps - else: - self.desired_caps = { - 'platformName': 'Desktop', 'browserName': 'Chrome'} - self.server_url = server_url - self.conditions = {} - g.plan_name = file_name.split('-')[0] - g.init(self.desired_caps, self.server_url) - - plan_path = Path('snapshot') / g.plan_name - task_path = plan_path / g.start_time[1:] - - for p in ('JUnit', 'report', 'snapshot', plan_path, task_path, 'report/' + g.plan_name): - mkdir(p) - - g.plan_data['log'] = set_log(logger, task_path) - - self.testcase_file = str( - Path('testcase') / (file_name + '-' + _testcase + '.xlsx')) - self.elements_file = str( - Path('element') / (g.plan_name + '-' + _elements + '.xlsx')) - self.report_xml = str( - Path('JUnit') / (file_name + '-' + _report + g.start_time + '.xml')) - self.testcase_workbook = Excel(self.testcase_file, 'r') - self.sheet_names = self.testcase_workbook.get_sheet(sheet_name) - self.report_excel = str(Path( - 'report') / g.plan_name / (file_name + '-' + _report + g.start_time + '.xlsx')) - self.report_workbook = Excel(self.report_excel, 'w') - - self.report_data = {} # 测试报告详细数据 - - def fliter(self, **kwargs): - # 筛选要执行的测试用例 - self.conditions = kwargs - - def plan(self): - self.code = 0 # 返回码 - # 1.解析配置文件 - try: - e.get_elements(self.elements_file) - except: - logger.exception('*** Parse config file failure ***') - self.code = -1 - sys.exit(self.code) - - self.junit = JUnit() - self.junit_suite = {} - - # 2.逐个执行测试套件 - for sheet_name in self.sheet_names: - g.sheet_name = sheet_name - # xml 测试报告初始化 - self.junit_suite[sheet_name] = self.junit.create_suite( - g.plan_name, sheet_name) - self.junit_suite[sheet_name].start() - - self.run(sheet_name) - - self.plan_data = g.plan_end() - self.testsuite_data = g.testsuite_data - - summary_data = summary( - self.plan_data, self.testsuite_data, self.report_data, {}) - self.report_workbook.write(summary_data, '_Summary_') - self.report_workbook.close() - - with open(self.report_xml, 'w', encoding='utf-8') as f: - self.junit.write(f) - - def run(self, sheet_name): - # 1.从 Excel 获取测试用例集 - try: - data = self.testcase_workbook.read(sheet_name) - testsuite = testsuite_format(data) - # logger.info('Testsuite imported from Excel:\n' + - # json.dumps(testsuite, ensure_ascii=False, indent=4)) - logger.info('From Excel import testsuite success') - except: - logger.exception('*** From Excel import testsuite failure ***') - self.code = -1 - sys.exit(self.code) - - # 2.初始化全局对象 - try: - g.set_driver() - # 如果测试数据文件存在,则从该文件里读取数据,赋值到全局变量列表里 - data_file = Path('data') / (g.plan_name + - '-' + sheet_name + '.csv') - if data_file.is_file(): - g.test_data = get_record(str(data_file)) - w.init() - except: - logger.exception('*** Init global object failure ***') - self.code = -1 - sys.exit(self.code) - - # 3.解析测试用例集 - try: - parse(testsuite) - logger.debug('testsuite has been parsed:\n' + str(testsuite)) - except: - logger.exception('*** Parse testsuite failure ***') - self.code = -1 - sys.exit(self.code) - - # 4.执行测试套件 - g.ts = TestSuite(testsuite, sheet_name, - self.junit_suite[sheet_name], self.conditions) - g.ts.run() - - # 5.判断测试结果 - if self.junit_suite[sheet_name].high_errors + self.junit_suite[sheet_name].medium_errors + \ - self.junit_suite[sheet_name].high_failures + self.junit_suite[sheet_name].medium_failures: - self.code = -1 - - # 6.保存测试结果 - try: - data = testsuite2data(testsuite) - self.report_workbook.write(data, sheet_name) - self.report_data[sheet_name] = testsuite2report(testsuite) - except: - logger.exception('*** Save the report is failure ***') - - - def report(self, md_path): - markdown(self.plan_data, self.testsuite_data, self.report_data, md_path) \ No newline at end of file diff --git a/sweetest/sweetest/config.py b/sweetest/sweetest/config.py deleted file mode 100644 index 7cb819a..0000000 --- a/sweetest/sweetest/config.py +++ /dev/null @@ -1,210 +0,0 @@ - -web_keywords = { - '打开': 'OPEN', - 'OPEN': 'OPEN', - '检查': 'CHECK', - 'CHECK': 'CHECK', - '#检查': 'NOTCHECK', - '#CHECK': 'NOTCHECK', - '输入': 'INPUT', - 'INPUT': 'INPUT', - '点击': 'CLICK', - 'CLICK': 'CLICK', - '选择': 'SELECT', - 'SELECT': 'SELECT', - '取消选择': 'DESELECT', - 'DESELECT': 'DESELECT', - '移动到': 'HOVER', - '悬停': 'HOVER', - 'HOVER': 'HOVER', - '右击': 'CONTEXT_CLICK', - 'CONTEXT_CLICK': 'CONTEXT_CLICK', - '双击': 'DOUBLE_CLICK', - 'DOUBLE_CLICK': 'DOUBLE_CLICK', - '拖拽': 'DRAG_AND_DROP', - 'DRAG_AND_DROP': 'DRAG_AND_DROP', - '滑动': 'SWIPE', - 'SWIPE': 'SWIPE', - '脚本': 'SCRIPT', - 'SCRIPT': 'SCRIPT', - '对话框': 'MESSAGE', - 'MESSAGE': 'MESSAGE', - '上传文件': 'UPLOAD', - 'UPLOAD': 'UPLOAD', - '导航': 'NAVIGATE', - 'NAVIGATE': 'NAVIGATE', - '滚动条': 'SCROLL', - 'SCROLL': 'SCROLL' -} - -common_keywords = { - '执行': 'EXECUTE', - 'EXECUTE': 'EXECUTE', - 'SQL': 'SQL' -} - -http_keywords = { - 'GET': 'GET', - 'POST': 'POST', - 'PUT': 'PUT', - 'PATCH': 'PATCH', - 'DELETE': 'DELETE', - 'OPTIONS': 'OPTIONS' -} - -mobile_keywords = { - '检查': 'CHECK', - 'CHECK': 'CHECK', - '#检查': 'NOTCHECK', - '#CHECK': 'NOTCHECK', - '输入': 'INPUT', - 'INPUT': 'INPUT', - '填写': 'SET_VALUE', - 'SET_VALUE': 'SET_VALUE', - '点击': 'CLICK', - 'CLICK': 'CLICK', - '轻点': 'TAP', - 'TAP': 'TAP', - '按键码': 'PRESS_KEYCODE', # Android 特有,常见代码 HOME:3, 菜单键:82,返回键:4 - 'PRESS_KEYCODE': 'PRESS_KEYCODE', - '滑动': 'SWIPE', - 'SWIPE': 'SWIPE', - '划线': 'LINE', - 'LINE': 'LINE', - '划线解锁': 'LINE_UNLOCK', - 'LINE_UNLOCK': 'LINE_UNLOCK', - '摇杆': 'ROCKER', - 'ROCKER': 'ROCKER', - '滚动': 'SCROLL', # iOS 专用 - 'SCROLL': 'SCROLL', - '拖拽': 'DRAG_AND_DROP', - 'DRAG_AND_DROP': 'DRAG_AND_DROP', - '摇晃': 'SHAKE', # 貌似 Android 上不可用 - 'SHAKE': 'SHAKE', - '快速滑动': 'FLICK', - 'FLICK': 'FLICK', - '滑动元素': 'FLICK_ELEMENT', - 'FLICK_ELEMENT': 'FLICK_ELEMENT', - '长按': 'LONG_PRESS', - 'LONG_PRESS': 'LONG_PRESS', - '缩小': 'PINCH', - 'PINCH': 'PINCH', - '放大': 'ZOOM', - 'ZOOM': 'ZOOM', - '隐藏键盘': 'HIDE_KEYBOARD', # iOS 专用 - 'HIDE_KEYBOARD': 'HIDE_KEYBOARD', - '命名标签页': 'TAB_NAME', - 'TAB_NAME': 'TAB_NAME', - '重启': 'LAUNCH_APP', - 'LAUNCH_APP': 'LAUNCH_APP', - '锁屏状态': 'IS_LOCKED', - 'IS_LOCKED': 'IS_LOCKED', - '锁屏': 'LOCK', - 'LOCK': 'LOCK', - '解锁': 'UNLOCK', - 'UNLOCK': 'UNLOCK', - '启动页面': 'ACTIVITY', - 'ACTIVITY': 'ACTIVITY' -} - -windows_keywords = { - '检查': 'CHECK', - 'CHECK': 'CHECK', - '菜单': 'MENU_SELECT', - 'MENU_SELECT': 'MENU_SELECT', - '选择': 'SELECT', - 'SELECT': 'SELECT', - '点击': 'CLICK', - 'CLICK': 'CLICK', - '双击': 'DOUBLE_CLICK', - 'DOUBLE_CLICK': 'DOUBLE_CLICK', - '勾选': 'CHECK_OFF', - 'CHECK_OFF': 'CHECK_OFF', - '输入': 'INPUT', - 'INPUT': 'INPUT', - '填写': 'SET_TEXT', - 'SET_TEXT': 'SET_TEXT', - '按键': 'SEND_KEYS', - 'SEND_KEYS': 'SEND_KEYS', - '窗口': 'WINDOW', - 'WINDOW': 'WINDOW' -} - -files_keywords = { -'复制': 'COPY', -'COPY': 'COPY', -'移动': 'MOVE', -'MOVE': 'MOVE', -'删除文件': 'REMOVE', -'REMOVE': 'REMOVE', -'删除目录': 'RMDIR', -'RMDIR': 'RMDIR', -'创建目录': 'MKDIR', -'MKDIR': 'MKDIR', -'路径存在': 'EXISTS', -'EXISTS': 'EXISTS', -'路径不存在': 'NOT_EXISTS', -'NOT_EXISTS': 'NOT_EXISTS', -'是文件': 'IS_FILE', -'IS_FILE': 'IS_FILE', -'是目录': 'IS_DIR', -'IS_DIR': 'IS_DIR', -'不是文件': 'NOT_FILE', -'NOT_FILE': 'NOT_FILE', -'不是目录': 'NOT_DIR', -'NOT_DIR': 'NOT_DIR', -'命令行': 'COMMAND', -'COMMAND': 'COMMAND', -'SHELL': 'SHELL', -'CMD': 'CMD' -} - -all_keywords = {} -for keywords in (web_keywords, common_keywords, http_keywords, mobile_keywords, windows_keywords, files_keywords): - all_keywords = dict(all_keywords, **keywords) - - -zh_en = { - '等待时间': '#wait_time', - '标签页名': '#tab_name', - '打开方式': '#open_type', - '#ScreenShot': '#screen_shot', - '#ElementShot': '#element_shot', - '循环结束条件': '#break' -} - -# 文件名后缀 -_testcase = 'TestCase' # '测试用例' -_elements = 'Elements' # '页面元素表' -_report = 'Report' # '测试结果' - -# 特殊符号的转换别名 -comma_lower = '#$%^&' -comma_upper = '&^%$#' -equals = '%^$#&' -vertical = '&$&*^&A@' - -# header = ['用例编号', '用例标题', '前置条件', '测试步骤', '操作', '页面', '元素', -#'测试数据', '预期结果', '输出数据', '优先级', '设计者', '自动化标记', '测试结果', '备注'] - -header = { - '用例编号': 'id', - '用例标题': 'title', - '前置条件': 'condition', - '测试步骤': 'step', - '操作': 'keyword', - '页面': 'page', - '元素': 'element', - '测试数据': 'data', - '预期结果': 'expected', - '输出数据': 'output', - '优先级': 'priority', - '设计者': 'designer', - '自动化标记': 'flag', - '步骤结果': 'score', - '用例结果': 'result', - '备注': 'remark' -} - -element_wait_timeout = 10 # 等待元素出现超时时间,单位:秒 -page_flash_timeout = 90 # 页面刷新超时时间,单位:秒 diff --git a/sweetest/sweetest/data.py b/sweetest/sweetest/data.py deleted file mode 100644 index 3af0a99..0000000 --- a/sweetest/sweetest/data.py +++ /dev/null @@ -1,115 +0,0 @@ -import xlrd -from sweetest.utility import Excel, data2dict -from sweetest.config import header -from sweetest.globals import g - - -def testsuite_format(data): - ''' - 将元素为 dict 的 list,处理为 testcase 的 list - testcase 的格式: - { - 'id': 'Login_001', #用例编号 - 'title': 'Login OK', #用例标题 - 'condition': '', #前置条件 - 'designer': 'Leo', #设计者 - 'flag': '', #自动化标记 - 'result': '', #用例结果 - 'remark': '', #备注 - 'steps': - [ - { - 'no': 1, #测试步骤 - 'keyword': '输入', - 'page': '产品管系统登录页', - 'element': '用户名', - 'data': 'user1', #测试数据 - 'output': '', #输出数据 - 'score': '', #测试结果 - 'remark': '' #备注 - }, - {……} - …… - ] - } - ''' - testsuite = [] - testcase = {'testsuite': '', 'no': 0} - data = data2dict(data) - - for d in data: - # 如果用例编号不为空,则为新的用例 - if d['id'].strip(): - # 如果 testcase[id] 非空,则添加到 testsuite 里,并重新初始化 testcase - if testcase.get('id'): - testsuite.append(testcase) - testcase = {'testsuite': '', 'no': 0} - for key in ('id', 'title', 'condition', 'designer', 'flag', 'result', 'remark'): - testcase[key] = d[key] - if '#' in d['id']: - testcase['set'] = d['id'].split('#')[0] - testcase['flag'] = 'N' - testcase['priority'] = d['priority'] if d['priority'] else 'M' - testcase['steps'] = [] - # 如果测试步骤不为空,则为有效步骤,否则用例解析结束 - no = str(d['step']).strip() - if no: - step = {} - step['control'] = '' - if no[0] in ('^', '>', '<', '#'): - step['control'] = no[0] - step['no'] = no - else: - step['no'] = str(int(d['step'])) - for key in ('keyword', 'page', 'element', 'data', 'expected', 'output', 'score', 'remark'): - step[key] = d.get(key, '') - - # 仅作为测试结果输出时,保持原样 - step['_keyword'] = d['keyword'] - step['_element'] = d['element'] - step['_data'] = d['data'] - step['vdata'] = d.get('data', '') - step['_expected'] = d.get('expected', '') - step['_output'] = d.get('output', '') - testcase['steps'].append(step) - if testcase: - testsuite.append(testcase) - return testsuite - - -def testsuite_from_excel(file_name, sheet_name): - d = Excel(file_name) - return testsuite_format(data2dict(d.read(sheet_name))) - - -def testsuite2data(data): - # result = [list(header.values())] - result = [[g.header_custom[key.lower()] for key in header.values()]] - for d in data: - s = d['steps'][0] # 第一步和用例标题同一行 - testcase = [d['id'], d['title'], d['condition'], s['no'], s['_keyword'], s['page'], s['_element'], - s['vdata'], s['_output'], d['priority'], d['designer'], d['flag'], s['score'], d['result'], s['remark']] - if g.header_custom['expected']: - testcase.insert(8, s['_expected']) - result.append(testcase) - for s in d['steps'][1:]: - step = ['', '', '', s['no'], s['_keyword'], s['page'], s['_element'], - s['vdata'], s['_output'], '', '', '', s['score'], '', s['remark']] - if g.header_custom['expected']: - step.insert(8, s['_expected']) - result.append(step) - return result - - -def testsuite2report(data): - report = [] - for case in data: - if case['condition'] in ('BASE', 'SETUP') or case['flag'] != 'N': - for step in case['steps']: - step['keyword'] = step.pop('_keyword') - step['element'] = step.pop('_element') - step['data'] = str(step.pop('vdata')) - step['expected'] = step.pop('_expected') - step['output'] = step.pop('_output') - report.append(case) - return report diff --git a/sweetest/sweetest/database.py b/sweetest/sweetest/database.py deleted file mode 100644 index f86f498..0000000 --- a/sweetest/sweetest/database.py +++ /dev/null @@ -1,105 +0,0 @@ -from sweetest.log import logger - - -class DB: - - def __init__(self, arg): - self.connect = '' - self.cursor = '' - self.db = '' - - try: - if arg['type'].lower() == 'mongodb': - import pymongo - host = arg.pop('host') if arg.get('host') else 'localhost:27017' - host = host.split(',') if ',' in host else host - port = int(arg.pop('port')) if arg.get('port') else 27017 - if arg.get('user'): - arg['username'] = arg.pop('user') - # username = arg['user'] if arg.get('user') else '' - # password = arg['password'] if arg.get('password') else '' - # self.connect = pymongo.MongoClient('mongodb://' + username + password + arg['host'] + ':' + arg['port'] + '/') - self.connect = pymongo.MongoClient(host=host, port=port, **arg) - self.connect.server_info() - self.db = self.connect[arg['dbname']] - - return - - if arg['type'].lower() == 'mysql': - import pymysql as mysql - self.connect = mysql.connect( - host=arg['host'], port=int(arg['port']), user=arg['user'], password=arg['password'], database=arg['dbname'], charset=arg.get('charset', 'utf8')) - self.cursor = self.connect.cursor() - sql = 'select version()' - - elif arg['type'].lower() == 'oracle': - import os - import cx_Oracle as oracle - # Oracle查询出的数据,中文输出问题解决 - os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8' - self.connect = oracle.connect( - arg['user'] + '/' + arg['password'] + '@' + arg['host'] + '/' + arg['sid']) - self.cursor = self.connect.cursor() - sql = 'select * from v$version' - elif arg['type'].lower() == 'sqlserver': - import pymssql as sqlserver - self.connect = sqlserver.connect( - host=arg['host'], port=arg['port'], user=arg['user'], password=arg['password'], database=arg['dbname'], charset=arg.get('charset', 'utf8')) - self.cursor = self.connect.cursor() - sql = 'select @@version' - - self.cursor.execute(sql) - self.cursor.fetchone() - - except: - logger.exception('*** %s connect is failure ***' % arg['type']) - raise - - def fetchone(self, sql): - try: - self.cursor.execute(sql) - data = self.cursor.fetchone() - self.connect.commit() - return data - except: - logger.exception('*** Fetchone failure ***') - raise - - def fetchall(self, sql): - try: - self.cursor.execute(sql) - data = self.cursor.fetchall() - self.connect.commit() - return data - except: - logger.exception('*** Fetchall failure ***') - raise - - def execute(self, sql): - try: - self.cursor.execute(sql) - self.connect.commit() - except: - logger.exception('*** Execute failure ***') - raise - - - def mongo(self, collection, sql): - try: - cmd = 'self.db[\'' + collection + '\'].' + sql - result = eval(cmd) - if sql.startswith('find_one'): - return result - elif sql.startswith('find'): - for d in result: - return d - elif 'count' in sql: - return {'count': result} - else: - return {} - except: - logger.exception('*** Execute failure ***') - raise - - def __del__(self): - self.connect.close() diff --git a/sweetest/sweetest/elements.py b/sweetest/sweetest/elements.py deleted file mode 100644 index 7166556..0000000 --- a/sweetest/sweetest/elements.py +++ /dev/null @@ -1,81 +0,0 @@ -from sweetest.utility import Excel, data2dict, replace -from sweetest.log import logger - - -def elements_format(data): - elements = {} - page = '' - custom = '' - for d in data: - if d['page'].strip(): - page = d['page'] - custom = '' - else: - d['page'] = page - - if d.get('custom', '').strip(): - custom = d['custom'] - else: - d['custom'] = custom - - elements[d['page'] + '-' + d['element']] = d - return elements - - -class Elements: - def __init__(self): - pass - - def env(self): - pass - - def get_elements(self, elements_file): - d = Excel(elements_file) - self.elements = elements_format(data2dict(d.read('elements'))) - - def have(self, page, element): - ele = element.split('#') - - if len(ele) >= 2: - _el = ele[0] + '#' - else: - _el = element - # 如果有<>,则不判断了 - if '<' in _el: - return '', '通用' + '-' + element - # 在元素定位表中查询 - elem = page + '-' + _el - if self.elements.get(elem, ''): - return self.elements[elem]['custom'], page + '-' + element - else: - # 查不到就在通用里查,还是查不到,可能是不在 element.xlsx 中定义的元素 - elem = '通用' + '-' + _el - if self.elements.get(elem, ''): - return self.elements[elem]['custom'], '通用' + '-' + element - else: - logger.info('Page:%s element:%s' % (page, element)) - return '', element - - def get(self, element, flag=False): - ele = element.split('#') - # #号后面的值,即用户输入的变量 - _v = [] - # 支持多个变量替代,但是顺序要对应 - if len(ele) >= 2: - _el = ele[0] + '#' - _v = ele[1:] - else: - _el = element - el = self.elements.get(_el, '') - if not el: - if flag: - return _el, '' - return _el, element.split('#', 1)[-1] - value = el['value'] - for v in _v: - v = '#' if v=='^' else v # 当 value 中的 # 无需替换时,用例中的元素使用 ^ 表示 - value = value.replace('#', v, 1) - return el, replace(value) - - -e = Elements() diff --git a/sweetest/sweetest/globals.py b/sweetest/sweetest/globals.py deleted file mode 100644 index baf49ea..0000000 --- a/sweetest/sweetest/globals.py +++ /dev/null @@ -1,156 +0,0 @@ -import time -from selenium import webdriver -from sweetest.config import element_wait_timeout, page_flash_timeout - - -def now(): - t = time.time() - return time.strftime("@%Y%m%d_%H%M%S", time.localtime(t)) - - -def timestamp(): - # js 格式的时间戳 - return int(time.time() * 1000) - - -class Global: - def __init__(self): - self.start_time = now() - self.start_timestamp = timestamp() - self.plan_name = '' - self.sheet_name = '' - self.plan_data = {} - self.testsuite_data = {} - self.no = 1 - self.driver = '' - self.snippet = {} - self.caseset = {} - - - def init(self, desired_caps, server_url): - self.desired_caps = desired_caps - self.server_url = server_url - self.platform = desired_caps.get('platformName', '') - self.browserName = desired_caps.get('browserName', '') - self.headless = desired_caps.pop('headless', False) - self.snapshot = desired_caps.pop('snapshot', False) - self.executable_path = desired_caps.pop('executable_path', False) - - - def set_driver(self): - self.test_data = {'_last_': False} - self.var = {} - self.caseset = {} - self.casesets = [] # 用例组合执行容器 - self.current_page = '通用' - self.db = {} - self.http = {} - self.windows = {} - self.baseurl = {} - self.action = {} - self.wait_times = 0 - - if self.platform.lower() == 'desktop': - if self.browserName.lower() == 'ie': - #capabilities = webdriver.DesiredCapabilities().INTERNETEXPLORER - #capabilities['acceptInsecureCerts'] = True - if self.executable_path: - self.driver = webdriver.Ie(executable_path=self.executable_path) - else: - self.driver = webdriver.Ie() - elif self.browserName.lower() == 'firefox': - profile = webdriver.FirefoxProfile() - profile.accept_untrusted_certs = True - - options = webdriver.FirefoxOptions() - # 如果配置了 headless 模式 - if self.headless: - options.set_headless() - # options.add_argument('-headless') - options.add_argument('--disable-gpu') - options.add_argument("--no-sandbox") - options.add_argument('window-size=1920x1080') - - if self.executable_path: - self.driver = webdriver.Firefox( - firefox_profile=profile, firefox_options=options, executable_path=self.executable_path) - else: - self.driver = webdriver.Firefox( - firefox_profile=profile, firefox_options=options) - self.driver.maximize_window() - elif self.browserName.lower() == 'chrome': - options = webdriver.ChromeOptions() - - # 如果配置了 headless 模式 - if self.headless: - options.add_argument('--headless') - options.add_argument('--disable-gpu') - options.add_argument("--no-sandbox") - options.add_argument('window-size=1920x1080') - - options.add_argument("--start-maximized") - options.add_argument('--ignore-certificate-errors') - # 指定浏览器分辨率,当"--start-maximized"无效时使用 - # options.add_argument('window-size=1920x1080') - prefs = {} - prefs["credentials_enable_service"] = False - prefs["profile.password_manager_enabled"] = False - options.add_experimental_option("prefs", prefs) - options.add_argument('disable-infobars') - options.add_experimental_option("excludeSwitches", ['load-extension', 'enable-automation']) - if self.executable_path: - self.driver = webdriver.Chrome( - options=options, executable_path=self.executable_path) - else: - self.driver = webdriver.Chrome(options=options) - else: - raise Exception( - 'Error: this browser is not supported or mistake name:%s' % self.browserName) - # 等待元素超时时间 - self.driver.implicitly_wait(element_wait_timeout) # seconds - # 页面刷新超时时间 - self.driver.set_page_load_timeout(page_flash_timeout) # seconds - - elif self.platform.lower() == 'ios': - from appium import webdriver as appdriver - if not self.driver: - self.driver = appdriver.Remote(self.server_url, self.desired_caps) - - elif self.platform.lower() == 'android': - from appium import webdriver as appdriver - if not self.driver: - self.driver = appdriver.Remote(self.server_url, self.desired_caps) - - elif self.platform.lower() == 'windows': - from pywinauto.application import Application - from sweetest.keywords.windows import Windows - self.desired_caps.pop('platformName') - backend = self.desired_caps.pop('backend', 'win32') - _path = '' - if self.desired_caps.get('#path'): - _path = self.desired_caps.pop('#path') - _backend = self.desired_caps.pop('#backend') - - if self.desired_caps.get('cmd_line'): - app = Application(backend).start(**self.desired_caps) - elif self.desired_caps.get('path'): - app = Application(backend).connect(**self.desired_caps) - else: - raise Exception('Error: Windows GUI start/connect args error') - self.windows['default'] = Windows(app) - - if _path: - _app = Application(_backend).connect(path=_path) - self.windows['#'] = Windows(_app) - - - def plan_end(self): - self.plan_data['plan'] = self.plan_name - #self.plan_data['task'] = self.start_timestamp - self.plan_data['start_timestamp'] = self.start_timestamp - self.plan_data['end_timestamp'] = int(time.time() * 1000) - - return self.plan_data - - -g = Global() diff --git a/sweetest/sweetest/junit.py b/sweetest/sweetest/junit.py deleted file mode 100644 index 9d35bdf..0000000 --- a/sweetest/sweetest/junit.py +++ /dev/null @@ -1,184 +0,0 @@ -from datetime import datetime -from xml.dom.minidom import Document - - -class JUnit(): - - def __init__(self): - self.testsuites = [] - - def create_suite(self, name, hostname="localhost"): - suite = TestSuite(name, hostname) - self.testsuites.append(suite) - return suite - - def finish(self): - for suite in self.testsuites: - if suite.open == True: - suite.finish() - - def write(self, file): - self.finish() - doc = Document() - root = doc.createElement("testsuites") - doc.appendChild(root) - for suite in self.testsuites: - root.appendChild(suite.to_xml(doc)) - file.write(doc.toprettyxml()) - - -class TestSuite(): - def __init__(self, name, hostname): - self.properties = [] - self.name = name - self.hostname = hostname - self.open = False - self.cases = [] - self.systemout = None - self.systemerr = None - - def start(self): - self.open = True - self.time = datetime.now() - self.timestamp = datetime.isoformat(self.time) - return self - - def create_case(self, name, classname=""): - if self.open: - case = TestCase(name, classname) - self.cases.append(case) - return case - else: - raise Exception( - "This test suite cannot be modified in its current state") - - def append_property(self, name, value): - self.properties.append([name, value]) - - def finish(self, output=None, error=None): - if self.open == True: - self.open = False - # set the number of test cases, error cases, failed cases, and the amount of time taken in seconds. - td = datetime.now() - self.time - self.time = float( - (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)) / 10**6 - self.errors = 0 - self.high_errors = 0 - self.medium_errors = 0 - self.low_errors = 0 - self.failures = 0 - self.high_failures = 0 - self.medium_failures = 0 - self.low_failures = 0 - self.skipped = 0 - self.disabled = 0 - for case in self.cases: - if case.state == None: - case.error( - "XmlUnit Finished", "The test was forced to finish since the suite was finished.") - status = case.state.lower() - if status == "failure": - self.failures += 1 - if case.priority == 'H': - self.high_failures += 1 - if case.priority == 'M': - self.medium_failures += 1 - if case.priority == 'L': - self.low_failures += 1 - elif status == "error": - self.errors += 1 - if case.priority == 'H': - self.high_errors += 1 - if case.priority == 'M': - self.medium_errors += 1 - if case.priority == 'L': - self.low_errors += 1 - elif status == "skipped": - self.skipped += 1 - elif status == "blocked": - self.disabled += 1 - else: - pass - self.tests = len(self.cases) - self.output = output - self.error = error - return None - else: - raise Exception("This test suite is already finished.") - - def to_xml(self, doc): - node = doc.createElement("testsuite") - node.setAttribute("name", self.name) - node.setAttribute("hostname", self.hostname) - node.setAttribute("timestamp", self.timestamp) - node.setAttribute("tests", "%s" % self.tests) - node.setAttribute("failures", "%s" % self.failures) - node.setAttribute("failures_detail", "H:%s M:%s L:%s" % ( - self.high_failures, self.medium_failures, self.low_failures)) - node.setAttribute("errors", "%s" % (self.errors + self.skipped + self.disabled)) - node.setAttribute("errors_detail", "H:%s M:%s L:%s" % ( - self.high_errors, self.medium_errors, self.low_errors)) - node.setAttribute("time", "%s" % self.time) - node.setAttribute("skipped", "%s" % self.skipped) - node.setAttribute("disabled", "%s" % self.disabled) - for case in self.cases: - node.appendChild(case.to_xml(doc)) - - return node - - -class TestCase(): - - def __init__(self, name, classname): - self.state = None - self.name = name - self.classname = classname - self.priority = 'M' - return None - - def start(self): - self.time = datetime.now() - return self - - def custom(self, state, type, message): - if self.state != None: - raise Exception("This test case is already finished.") - self.state = state - self.message = message - self.type = type - td = datetime.now() - self.time - self.time = float( - (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)) / 10**6 - - def fail(self, type, message): - self.custom("failure", type, message) - - def skip(self, type, message): - self.custom("skipped", type, message) - - def error(self, type, message): - self.custom("error", type, message) - - def block(self, type, message): - self.custom("skipped", type, message) # Azure Devops 不识别 blocked,改为 skipped - - def succeed(self): - if self.state != None: - raise Exception("This test case is already finished.") - self.state = "success" - td = datetime.now() - self.time - self.time = float( - (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)) / 10**6 - - def to_xml(self, doc): - node = doc.createElement("testcase") - node.setAttribute("name", self.name) - node.setAttribute("classname", self.classname) - node.setAttribute("priority", self.priority) - node.setAttribute("time", "%s" % self.time) - if self.state != "success": - subnode = doc.createElement(self.state) - subnode.setAttribute("type", self.type) - subnode.setAttribute("message", self.message) - node.appendChild(subnode) - return node diff --git a/sweetest/sweetest/keywords/__init__.py b/sweetest/sweetest/keywords/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/sweetest/sweetest/keywords/common.py b/sweetest/sweetest/keywords/common.py deleted file mode 100644 index aa46bfe..0000000 --- a/sweetest/sweetest/keywords/common.py +++ /dev/null @@ -1,200 +0,0 @@ -from copy import deepcopy -from sweetest.globals import g -from sweetest.elements import e -from sweetest.log import logger -from sweetest.parse import data_format -from sweetest.database import DB -from sweetest.utility import replace_dict, compare -from injson import check -from sweetest.utility import json2dict - - -def execute(step): - # 先处理循环结束条件 - condition = '' - for k in ('循环结束条件', 'condition', '#break'): - if step['data'].get(k): - condition = step['data'].get(k) - del step['data'][k] - if condition.lower() in ('成功', 'success'): - condition = 'success' - elif condition.lower() in ('失败', 'failure'): - condition = 'failure' - - # 执行赋值操作 - data = step['data'] - for k, v in data.items(): - g.var[k] = v - - from sweetest.testcase import TestCase - element = step['element'] - times = 1 - _element = element.split('*') - - # snippet 执行失败是否退出标志 - flag = True - if element[-1] == '*': - flag = False - - # 循环次数为 N 标志 - n_flag = False - if len(_element) >= 2: - element = _element[0] - if _element[1].upper() == 'N': - times = 999 - n_flag = True - else: - times = int(_element[1]) - - # 初始化测试片段执行结果 - result = 'success' - steps = [] - testcase = {} - if step['page'] in ('用例片段', 'SNIPPET'): - g.var['_last_'] = False - for t in range(times): - if t > 0: - _data = data_format(str(step['_data'])) - replace_dict(_data) - for k, v in _data.items(): - g.var[k] = v - testcase = deepcopy(g.snippet[element]) - tc = TestCase(testcase) - tc.run() - for s in testcase['steps']: - s['no'] = str(step['no']) + '*' + \ - str(t + 1) + '-' + str(s['no']) - steps += testcase['steps'] - # 用例片段执行失败时 - if testcase['result'] != 'success': - result = testcase['result'] - # 循环退出条件为失败,则直接返回,返回结果是 success - if condition == 'failure': - return 'success', testcase['steps'] - # 如果没有结束条件,且直接退出标志位为真,则返回结果 - if not condition and flag: - return result, steps - - # 用例片段执行成功时 - else: - # 如果循环退出条件是成功,则直接返回,返回结果是 success - if condition == 'success': - return 'success', testcase['steps'] - - if n_flag and g.var['_last_']: # 只有循环次数为 N 时,才判断是否是变量最后一个值 - g.var['_last_'] = False - break - # 执行结束,还没有触发循环退出条件,则返回结果为 failure - if condition: - return 'failure', testcase['steps'] - elif step['page'] in ('用例组合', 'CASESET'): - caseset = element - for t in range(times): - if t > 0: - _data = data_format(str(step['_data'])) - replace_dict(_data) - for k, v in _data.items(): - g.var[k] = v - for testcase in g.caseset[caseset]: - testcase = deepcopy(testcase) - testcase['flag'] = '' - g.ts.run_testcase(testcase) - g.casesets.append(testcase) - # if testcase['result'] != 'success': - # result = testcase['result'] - return result, steps - - -def dedup(text): - ''' - 去掉 text 中括号及其包含的字符 - ''' - _text = '' - n = 0 - - for s in text: - if s not in ( '(', ')'): - if n <= 0: - _text += s - elif s == '(': - n += 1 - elif s == ')': - n -= 1 - return _text - - -def sql(step): - - response = {} - - element = step['element'] - _sql = e.get(element)[1] - - logger.info('SQL: %s' % repr(_sql)) - # 获取连接参数 - value = e.get(step['page'] + '-' + 'config')[1] - arg = data_format(value) - - if step['page'] not in g.db.keys(): - g.db[step['page']] = DB(arg) - if _sql.lower().startswith('select'): - row = g.db[step['page']].fetchone(_sql) - logger.info('SQL response: %s' % repr(row)) - if not row: - raise Exception('*** Fetch None ***') - - elif _sql.lower().startswith('db.'): - _sql_ = _sql.split('.', 2) - collection = _sql_[1] - sql = _sql_[2] - response = g.db[step['page']].mongo(collection, sql) - if response: - logger.info('find result: %s' % repr(response)) - else: - g.db[step['page']].execute(_sql) - - if _sql.lower().startswith('select'): - text = _sql[6:].split('FROM')[0].split('from')[0].strip() - keys = dedup(text).split(',') - for i, k in enumerate(keys): - keys[i] = k.split(' ')[-1] - response = dict(zip(keys, row)) - logger.info('select result: %s' % repr(response)) - - expected = step['data'] - if not expected: - expected = step['expected'] - if 'json' in expected: - expected['json'] = json2dict(expected.get('json', '{}')) - result = check(expected.pop('json'), response['json']) - logger.info('json check result: %s' % result) - if result['code'] != 0: - raise Exception(f'json | EXPECTED:{repr(expected["json"])}, REAL:{repr(response["json"])}, RESULT: {result}') - elif result['var']: - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('json var: %s' % (repr(result['var']))) - - if expected: - for key in expected: - sv, pv = expected[key], response[key] - logger.info('key: %s, expect: %s, real: %s' % - (repr(key), repr(sv), repr(pv))) - - compare(sv, pv) - - output = step['output'] - if output: - _output = {} - for k, v in output.items(): - if k == 'json': - sub = json2dict(output.get('json', '{}')) - result = check(sub, response['json']) - # logger.info('Compare json result: %s' % result) - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('json var: %s' % (repr(result['var']))) - else: - _output[k] = response[v] - g.var[k] = response[v] - logger.info('output: %s' % repr(_output)) \ No newline at end of file diff --git a/sweetest/sweetest/keywords/files.py b/sweetest/sweetest/keywords/files.py deleted file mode 100644 index 2eee943..0000000 --- a/sweetest/sweetest/keywords/files.py +++ /dev/null @@ -1,211 +0,0 @@ -import os -from sweetest.log import logger - - -def copy(step): - cwd = os.getcwd() - source = step['element'] - destination = step['data']['text'] - - if step['page']: - os.chdir(step['page']) - - code = 0 - if os.name == 'nt': - code = os.system(f'COPY /Y {source} {destination}') - if os.name == 'posix': - code = os.system(f'cp -f -R {source} {destination}') - - if step['page']: - os.chdir(cwd) - - if code != 0: - raise Exception(f'COPY {source} {destination} is failure, code: {code}') - - -def move(step): - cwd = os.getcwd() - source = step['element'] - destination = step['data']['text'] - - if step['page']: - os.chdir(step['page']) - - code = 0 - if os.name == 'nt': - code = os.system(f'MOVE /Y {source} {destination}') - if os.name == 'posix': - code = os.system(f'mv -f {source} {destination}') - - if step['page']: - os.chdir(cwd) - - if code != 0: - raise Exception(f'MOVE {source} {destination} is failure, code: {code}') - - -def remove(step): - cwd = os.getcwd() - path = step['element'] - - if step['page']: - os.chdir(step['page']) - - code = 0 - if os.name == 'nt': - code = os.system(f'del /S /Q {path}') - if os.name == 'posix': - code = os.system(f'rm -f {path}') - - if step['page']: - os.chdir(cwd) - - if code != 0: - raise Exception(f'REMOVE {path} is failure, code: {code}') - - -def rmdir(step): - cwd = os.getcwd() - path = step['element'] - - if step['page']: - os.chdir(step['page']) - - code = 0 - if os.name == 'nt': - code = os.system(f'rd /S /Q {path}') - if os.name == 'posix': - code = os.system(f'rm -rf {path}') - - if step['page']: - os.chdir(cwd) - - if code != 0: - raise Exception(f'RERMDIR {path} is failure, code: {code}') - - -def mkdir(step): - cwd = os.getcwd() - path = step['element'] - - if step['page']: - os.chdir(step['page']) - - code = 0 - if os.name == 'nt': - code = os.system(f'mkdir {path}') - if os.name == 'posix': - code = os.system(f'mkdir -p {path}') - - if step['page']: - os.chdir(cwd) - - if code != 0: - raise Exception(f'MKDIR {path} is failure, code: {code}') - - -def exists(step): - cwd = os.getcwd() - path = step['element'] - - if step['page']: - os.chdir(step['page']) - - result = os.path.exists(path) - - if step['page']: - os.chdir(cwd) - - if not result: - raise Exception(f'{path} is not exists') - - -def not_exists(step): - try: - exists(step) - except: - pass - else: - path = step['element'] - raise Exception(f'{path} is a exists') - - -def is_file(step): - cwd = os.getcwd() - path = step['element'] - - if step['page']: - os.chdir(step['page']) - - result = os.path.isfile(path) - - if step['page']: - os.chdir(cwd) - - if not result: - raise Exception(f'{path} is not file') - - -def not_file(step): - try: - is_file(step) - except: - pass - else: - path = step['element'] - raise Exception(f'{path} is a file') - - -def is_dir(step): - cwd = os.getcwd() - path = step['element'] - - if step['page']: - os.chdir(step['page']) - - result = os.path.isdir(path) - - if step['page']: - os.chdir(cwd) - - if not result: - raise Exception(f'{path} is not dir') - - -def not_dir(step): - try: - is_dir(step) - except: - pass - else: - path = step['element'] - raise Exception(f'{path} is a dir') - - -def command(step, name=None): - cwd = os.getcwd() - cmd = step['element'] - - if name and os.name != name: - logger.info(f'COMMAND: this OS is not {name}, "{cmd}" is skipped') - return - - - if step['page']: - os.chdir(step['page']) - - code = os.system(cmd) - - if step['page']: - os.chdir(cwd) - - if code != 0: - raise Exception(f'COMMAND: "{cmd}" is failure, code: {code}') - - -def shell(step): - command(step, 'posix') - - -def cmd(step): - command(step, 'nt') \ No newline at end of file diff --git a/sweetest/sweetest/keywords/http.py b/sweetest/sweetest/keywords/http.py deleted file mode 100644 index df03480..0000000 --- a/sweetest/sweetest/keywords/http.py +++ /dev/null @@ -1,255 +0,0 @@ -from copy import deepcopy -import requests -import json -from injson import check -from sweetest.globals import g -from sweetest.elements import e -from sweetest.log import logger -from sweetest.parse import data_format -from sweetest.utility import json2dict -from pathlib import Path - -path = Path('lib') / 'http_handle.py' -if path.is_file(): - from lib import http_handle -else: - from sweetest.lib import http_handle - - -class Http: - - def __init__(self, step): - # 获取 baseurl - baseurl = e.get(step['page'] + '-' + 'baseurl', True)[1] - if not baseurl: - self.baseurl = '' - else: - if not baseurl.endswith('/'): - baseurl += '/' - self.baseurl = baseurl - - self.r = requests.Session() - # 获取 headers - self.headers_get = e.get(step['page'] + '-' + 'headers_get', True)[1] - self.headers_post = e.get(step['page'] + '-' + 'headers_post', True)[1] - - -def get(step): - request('get', step) - - -def post(step): - request('post', step) - - -def put(step): - request('put', step) - - -def patch(step): - request('patch', step) - - -def delete(step): - request('delete', step) - - -def options(step): - request('options', step) - - -def request(kw, step): - element = step['element'] - url = e.get(element)[1] - if url.startswith('/'): - url = url[1:] - - data = step['data'] - # 测试数据解析时,会默认添加一个 text 键,需要删除 - if 'text' in data and not data['text']: - data.pop('text') - - _data = {} - _data['headers'] = json2dict(data.pop('headers', '{}')) - if data.get('cookies'): - data['cookies'] = json2dict(data['cookies']) - if kw == 'get': - _data['params'] = json2dict( - data.pop('params', '{}')) or json2dict(data.pop('data', '{}')) - elif kw == 'post': - if data.get('text'): - _data['data'] = data.pop('text').encode('utf-8') - else: - _data['data'] = json2dict(data.pop('data', '{}')) - _data['json'] = json2dict(data.pop('json', '{}')) - _data['files'] = eval(data.pop('files', 'None')) - elif kw in ('put', 'patch'): - _data['data'] = json2dict(data.pop('data', '{}')) - - - for k in data: - for s in ('{', '[', 'False', 'True'): - if s in data[k]: - try: - data[k] = eval(data[k]) - except: - logger.warning('Try eval data failure: %s' % data[k]) - break - expected = step['expected'] - expected['status_code'] = expected.get('status_code', None) - expected['text'] = expected.get('text', None) - expected['json'] = json2dict(expected.get('json', '{}')) - expected['cookies'] = json2dict(expected.get('cookies', '{}')) - expected['headers'] = json2dict(expected.get('headers', '{}')) - timeout = float(expected.get('timeout', 10)) - expected['time'] = float(expected.get('time', 0)) - - if not g.http.get(step['page']): - g.http[step['page']] = Http(step) - http = g.http[step['page']] - - if kw == 'post': - if http.headers_post: - http.r.headers.update(eval(http.headers_post)) - else: - if http.headers_get: - http.r.headers.update(eval(http.headers_get)) - - logger.info('URL: %s' % http.baseurl + url) - - # 处理 before_send - before_send = data.pop('before_send', '') - if before_send: - _data, data = getattr(http_handle, before_send)(kw, _data, data) - else: - _data, data = getattr(http_handle, 'before_send')(kw, _data, data) - - if _data['headers']: - for k in [x for x in _data['headers']]: - if not _data['headers'][k]: - del http.r.headers[k] - del _data['headers'][k] - http.r.headers.update(_data['headers']) - - if kw == 'get': - r = getattr(http.r, kw)(http.baseurl + url, - params=_data['params'], timeout=timeout, **data) - if _data['params']: - logger.info(f'PARAMS: {_data["params"]}') - - elif kw == 'post': - r = getattr(http.r, kw)(http.baseurl + url, - data=_data['data'], json=_data['json'], files=_data['files'], timeout=timeout, **data) - logger.info(f'BODY: {r.request.body}') - - elif kw in ('put', 'patch'): - r = getattr(http.r, kw)(http.baseurl + url, - data=_data['data'], timeout=timeout, **data) - logger.info(f'BODY: {r.request.body}') - - elif kw in ('delete', 'options'): - r = getattr(http.r, kw)(http.baseurl + url, timeout=timeout, **data) - - logger.info('status_code: %s' % repr(r.status_code)) - try: # json 响应 - logger.info('response json: %s' % repr(r.json())) - except: # 其他响应 - logger.info('response text: %s' % repr(r.text)) - - response = {'status_code': r.status_code, 'headers': r.headers, - '_cookies': r.cookies, 'content': r.content, 'text': r.text} - - try: - response['cookies'] = requests.utils.dict_from_cookiejar(r.cookies) - except: - response['cookies'] = r.cookies - - try: - j = r.json() - response['json'] = j - except: - response['json'] = {} - - # 处理 after_receive - after_receive = expected.pop('after_receive', '') - if after_receive: - response = getattr(http_handle, after_receive)(response) - else: - response = getattr(http_handle, 'after_receive')(response) - - var = {} # 存储所有输出变量 - - if expected['status_code']: - if str(expected['status_code']) != str(response['status_code']): - raise Exception(f'status_code | EXPECTED:{repr(expected["status_code"])}, REAL:{repr(response["status_code"])}') - - if expected['text']: - if expected['text'].startswith('*'): - if expected['text'][1:] not in response['text']: - raise Exception(f'text | EXPECTED:{repr(expected["text"])}, REAL:{repr(response["text"])}') - else: - if expected['text'] == response['text']: - raise Exception(f'text | EXPECTED:{repr(expected["text"])}, REAL:{repr(response["text"])}') - - if expected['headers']: - result = check(expected['headers'], response['headers']) - logger.info('headers check result: %s' % result) - if result['code'] != 0: - raise Exception(f'headers | EXPECTED:{repr(expected["headers"])}, REAL:{repr(response["headers"])}, RESULT: {result}') - elif result['var']: - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('headers var: %s' % (repr(result['var']))) - - if expected['cookies']: - logger.info('response cookies: %s' % response['cookies']) - result = check(expected['cookies'], response['cookies']) - logger.info('cookies check result: %s' % result) - if result['code'] != 0: - raise Exception(f'cookies | EXPECTED:{repr(expected["cookies"])}, REAL:{repr(response["cookies"])}, RESULT: {result}') - elif result['var']: - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('cookies var: %s' % (repr(result['var']))) - - if expected['json']: - result = check(expected['json'], response['json']) - logger.info('json check result: %s' % result) - if result['code'] != 0: - raise Exception(f'json | EXPECTED:{repr(expected["json"])}, REAL:{repr(response["json"])}, RESULT: {result}') - elif result['var']: - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('json var: %s' % (repr(result['var']))) - - if expected['time']: - if expected['time'] < r.elapsed.total_seconds(): - raise Exception(f'time | EXPECTED:{repr(expected["time"])}, REAL:{repr(r.elapsed.total_seconds())}') - - output = step['output'] - # if output: - # logger.info('output: %s' % repr(output)) - - for k, v in output.items(): - if v == 'status_code': - g.var[k] = response['status_code'] - logger.info('%s: %s' % (k, repr(g.var[k]))) - elif v == 'text': - g.var[k] = response['text'] - logger.info('%s: %s' % (k, repr(g.var[k]))) - elif k == 'json': - sub = json2dict(output.get('json', '{}')) - result = check(sub, response['json']) - # logger.info('Compare json result: %s' % result) - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('json var: %s' % (repr(result['var']))) - elif k == 'cookies': - sub = json2dict(output.get('cookies', '{}')) - result = check(sub, response['cookies']) - # logger.info('Compare json result: %s' % result) - var = dict(var, **result['var']) - g.var = dict(g.var, **result['var']) - logger.info('cookies var: %s' % (repr(result['var']))) - if var: - step['_output'] += '\n||output=' + str(var) \ No newline at end of file diff --git a/sweetest/sweetest/keywords/mobile.py b/sweetest/sweetest/keywords/mobile.py deleted file mode 100644 index 84dfb0e..0000000 --- a/sweetest/sweetest/keywords/mobile.py +++ /dev/null @@ -1,486 +0,0 @@ -from time import sleep -import re -from sweetest.globals import g -from sweetest.elements import e -from sweetest.windows import w -from sweetest.locator import locating_elements, locating_data, locating_element -from sweetest.log import logger -from sweetest.parse import data_format -from sweetest.utility import compare -from appium.webdriver.common.touch_action import TouchAction -from selenium.common.exceptions import ElementClickInterceptedException - - -class Common(): - @classmethod - def title(cls, data, output): - logger.info('DATA:%s' % repr(data['text'])) - logger.info('REAL:%s' % repr(g.driver.title)) - if data['text'].startswith('*'): - assert data['text'][1:] in g.driver.title - else: - assert data['text'] == g.driver.title - # 只能获取到元素标题 - for key in output: - g.var[key] = g.driver.title - - @classmethod - def current_url(cls, data, output): - logger.info('DATA:%s' % repr(data['text'])) - logger.info('REAL:%s' % repr(g.driver.current_url)) - if data['text'].startswith('*'): - assert data['text'][1:] in g.driver.current_url - else: - assert data['text'] == g.driver.current_url - # 只能获取到元素 url - for key in output: - g.var[key] = g.driver.current_url - - -def check(step): - data = step['data'] - if not data: - data = step['expected'] - - element = step['element'] - element_location = locating_element(element) - if '#' in element: - e_name = element.split('#')[0] + '#' - else: - e_name = element - by = e.elements[e_name]['by'] - output = step['output'] - - if by in ('title', 'current_url'): - getattr(Common, by)(data, output) - - else: - for key in data: - # 预期结果 - expected = data[key] - # 切片操作处理 - s = re.findall(r'\[.*?\]', key) - if s: - s = s[0] - key = key.replace(s, '') - - if key == 'text': - real = element_location.text - else: - real = element_location.get_attribute(key) - if s: - real = eval('real' + s) - - logger.info('DATA:%s' % repr(expected)) - logger.info('REAL:%s' % repr(real)) - compare(expected, real) - - # 获取元素其他属性 - for key in output: - if output[key] == 'text': - g.var[key] = element_location.text - elif output[key] in ('text…', 'text...'): - if element_location.text.endswith('...'): - g.var[key] = element_location.text[:-3] - else: - g.var[key] = element_location.text - else: - g.var[key] = element_location.get_attribute(output[key]) - - -def notcheck(step): - data = step['data'] - if not data: - data = step['expected'] - - element = step['element'] - # element_location = locating_element(element) - - if e.elements[element]['by'] == 'title': - assert data['text'] != g.driver.title - - -def input(step): - data = step['data'] - element = step['element'] - element_location = locating_element(element) - - if isinstance(data['text'], tuple): - element_location.send_keys(*data['text']) - elif element_location: - if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': - pass - else: - element_location.clear() - element_location.send_keys(data['text']) - - -def set_value(step): - data = step['data'] - element = step['element'] - element_location = locating_element(element) - - if isinstance(data['text'], tuple): - element_location.set_value(*data['text']) - elif element_location: - if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': - pass - else: - element_location.clear() - element_location.set_value(data['text']) - - -def click(step): - element = step['element'] - if isinstance(element, str): - #element_location = locating_element(element, 'CLICK') - element_location = locating_element(element) - try: - element_location.click() - except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 - sleep(1) - element_location.click() - elif isinstance(element, list): - for _e in element: - #element_location = locating_element(_e, 'CLICK') - element_location = locating_element(_e) - try: - element_location.click() - except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 - sleep(1) - element_location.click() - sleep(0.5) - sleep(0.5) - - # 获取元素其他属性 - output = step['output'] - for key in output: - if output[key] == 'text': - g.var[key] = element_location.text - elif output[key] == 'tag_name': - g.var[key] = element_location.tag_name - elif output[key] in ('text…', 'text...'): - if element_location.text.endswith('...'): - g.var[key] = element_location.text[:-3] - else: - g.var[key] = element_location.text - else: - g.var[key] = element_location.get_attribute(output[key]) - - # if w.current_context.startswith('WEBVIEW'): - # # 判断是否打开了新的窗口,并将新窗口添加到所有窗口列表里 - # all_handles = g.driver.window_handles - # for handle in all_handles: - # if handle not in w.windows.values(): - # w.register(step, handle) - - -def tap(step): - action = TouchAction(g.driver) - - element = step['element'] - if isinstance(element, str): - - if ',' in element: - position = element.split(',') - x = int(position[0]) - y = int(position[1]) - position = (x, y) - g.driver.tap([position]) - else: - element_location = locating_element(element, 'CLICK') - action.tap(element_location).perform() - elif isinstance(element, list): - if ',' in element[0]: - for el in element: - position = el.split(',') - x = int(position[0]) - y = int(position[1]) - position = (x, y) - g.driver.tap([position]) - sleep(0.5) - else: - for _e in element: - element_location = locating_element(_e, 'CLICK') - action.tap(element_location).perform() - sleep(0.5) - sleep(0.5) - - # 获取元素其他属性 - output = step['output'] - for key in output: - if output[key] == 'text': - g.var[key] = element_location.text - elif output[key] == 'tag_name': - g.var[key] = element_location.tag_name - elif output[key] in ('text…', 'text...'): - if element_location.text.endswith('...'): - g.var[key] = element_location.text[:-3] - else: - g.var[key] = element_location.text - else: - g.var[key] = element_location.get_attribute(output[key]) - - # if w.current_context.startswith('WEBVIEW'): - # # 判断是否打开了新的窗口,并将新窗口添加到所有窗口列表里 - # all_handles = g.driver.window_handles - # for handle in all_handles: - # if handle not in w.windows.values(): - # w.register(step, handle) - - -def press_keycode(step): - element = step['element'] - g.driver.press_keycode(int(element)) - - -def swipe(step): - element = step['element'] - duration = step['data'].get('持续时间', 0.3) - assert isinstance(element, list) and len( - element) == 2, '坐标格式或数量不对,正确格式如:100,200|300,400' - - start = element[0].replace(',', ',').split(',') - start_x = int(start[0]) - start_y = int(start[1]) - - end = element[1].replace(',', ',').split(',') - end_x = int(end[0]) - end_y = int(end[1]) - - if duration: - g.driver.swipe(start_x, start_y, end_x, end_y, sleep(float(duration))) - else: - g.driver.swipe(start_x, start_y, end_x, end_y) - - -def line(step): - element = step['element'] - duration = float(step['data'].get('持续时间', 0.3)) - assert isinstance(element, list) and len( - element) > 1, '坐标格式或数量不对,正确格式如:258,756|540,1032' - postions = [] - for _e in element: - _e = _e.replace(',', ',') - p = _e.split(',') - postions.append(p) - - action = TouchAction(g.driver) - action = action.press( - x=postions[0][0], y=postions[0][1]).wait(duration * 1000) - for i in range(1, len(postions)): - action.move_to(x=postions[i][0], y=postions[i] - [1]).wait(duration * 1000) - action.release().perform() - - -def line_unlock(step): - element = step['element'] - duration = float(step['data'].get('持续时间', 0.3)) - assert isinstance(element, list) and len( - element) > 2, '坐标格式或数量不对,正确格式如:lock_pattern|1|4|7|8|9' - _e = locating_element(element[0]) - rect = _e.rect - w = rect['width'] / 6 - h = rect['height'] / 6 - - key = {} - key['1'] = (rect['x'] + 1 * w, rect['y'] + 1 * h) - key['2'] = (rect['x'] + 3 * w, rect['y'] + 1 * h) - key['3'] = (rect['x'] + 5 * w, rect['y'] + 1 * h) - key['4'] = (rect['x'] + 1 * w, rect['y'] + 3 * h) - key['5'] = (rect['x'] + 3 * w, rect['y'] + 3 * h) - key['6'] = (rect['x'] + 5 * w, rect['y'] + 3 * h) - key['7'] = (rect['x'] + 1 * w, rect['y'] + 5 * h) - key['8'] = (rect['x'] + 3 * w, rect['y'] + 5 * h) - key['9'] = (rect['x'] + 5 * w, rect['y'] + 5 * h) - - action = TouchAction(g.driver) - for i in range(1, len(element)): - k = element[i] - if i == 1: - action = action.press( - x=key[k][0], y=key[k][1]).wait(duration * 1000) - action.move_to(x=key[k][0], y=key[k][1]).wait(duration * 1000) - action.release().perform() - - -def rocker(step): - element = step['element'] - duration = float(step['data'].get('持续时间', 0.3)) - rocker_name = step['data'].get('摇杆', 'rocker') - release = step['data'].get('释放', False) - - if isinstance(element, str): - if element: - element = [element] - else: - element = [] - - postions = [] - for _e in element: - _e = _e.replace(',', ',') - p = _e.split(',') - postions.append(p) - - # 如果 action 中么有此摇杆名,则是新的遥感 - if not g.action.get(rocker_name): - g.action[rocker_name] = TouchAction(g.driver) - g.action[rocker_name].press( - x=postions[0][0], y=postions[0][1]).wait(duration * 1000) - # 新摇杆的第一个点已操作,需要删除 - postions.pop(0) - # 依次操作 - for i in range(len(postions)): - g.action[rocker_name].move_to( - x=postions[i][0], y=postions[i][1]).wait(duration * 1000) - - if release: - # 释放摇杆,并删除摇杆 - g.action[rocker_name].release().perform() - del g.action[rocker_name] - else: - g.action[rocker_name].perform() - - -def scroll(step): - element = step['element'] - assert isinstance(element, list) and len( - element) == 2, '元素格式或数量不对,正确格式如:origin_el|destination_el' - origin = locating_element(element[0]) - destination = locating_element(element[1]) - g.driver.scroll(origin, destination) - - -def flick_element(step): - element = step['element'] - speed = step['data'].get('持续时间', 10) - assert isinstance(element, list) and len( - element) == 2, '坐标格式或数量不对,正确格式如:elment|200,300' - _e = eval(element[0]) - - end = element[1].replace(',', ',').split(',') - end_x = int(end[0]) - end_y = int(end[1]) - - if speed: - g.driver.flick_element(_e, end_x, end_y, int(speed)) - - -def flick(step): - element = step['element'] - assert isinstance(element, list) and len( - element) == 2, '坐标格式或数量不对,正确格式如:100,200|300,400' - - start = element[0].replace(',', ',').split(',') - start_x = int(start[0]) - start_y = int(start[1]) - - end = element[1].replace(',', ',').split(',') - end_x = int(end[0]) - end_y = int(end[1]) - - g.driver.flick(start_x, start_y, end_x, end_y) - - -def drag_and_drop(step): - element = step['element'] - assert isinstance(element, list) and len( - element) == 2, '元素格式或数量不对,正确格式如:origin_el|destination_el' - origin = locating_element(element[0]) - destination = locating_element(element[1]) - g.driver.drag_and_drop(origin, destination) - - -def long_press(step): - action = TouchAction(g.driver) - - element = step['element'] - duration = step['data'].get('持续时间', 1000) - if ',' in element or ',' in element: - position = element.replace(',', ',').split(',') - x = int(position[0]) - y = int(position[1]) - action.long_press(x=x, y=y, duration=duration).perform() - else: - element_location = locating_element(element) - action.long_press(element_location, duration=duration).perform() - sleep(0.5) - - -def pinch(step): - element = step['element'] - element_location = locating_element(element[0]) - percent = step['data'].get('百分比', 200) - steps = step['data'].get('步长', 50) - g.driver.pinch(element_location, percent, steps) - - -def zoom(step): - element = step['element'] - element_location = locating_element(element[0]) - percent = step['data'].get('百分比', 200) - steps = step['data'].get('步长', 50) - g.driver.zoom(element_location, percent, steps) - - -def hide_keyboard(step): - g.driver.hide_keyboard() - - -def shake(step): - g.driver.shake() - - -def launch_app(step): - g.driver.launch_app() - - -def is_locked(step): - status = g.driver.is_locked() - assert status, "it's not locked" - - -def lock(step): - g.driver.lock() - - -def unlock(step): - g.driver.unlock() - - -def tab_name(step): - element = step['element'] - name = step['data']['text'] - # 从所有窗口中查找给定元素,如果查询到就命名,否则报错 - all_handles = g.driver.window_handles - logger.info('All Handles: %s' % all_handles) - - flag = False - for handle in all_handles: - #logger.info('Page Source: %s \n%s' % (handle, g.driver.page_source)) - #logger.info('All Windows: %s' %w.windows) - if handle not in w.windows.values(): - # 切换至此窗口 - g.driver.switch_to_window(handle) - try: - # 成功定位到关键元素 - locating_element(element, 'CLICK') - # 添加到窗口资源池 g.windows - w.windows[name] = handle - # 把当前窗口名字改为新窗口名称 - w.current_window = name - flag = True - logger.info('Current Window: %s' % repr(name)) - logger.info('Current Handle: %s' % repr(handle)) - except: - pass - if not flag: - raise Exception( - 'Tab Name failure: the element:%s in all tab is not found' % element) - -def activity(step): - - appPackage = step['data']['appPackage'] - appActivity = step['data']['appActivity'] - g.driver.start_activity(appPackage, appActivity) \ No newline at end of file diff --git a/sweetest/sweetest/keywords/web.py b/sweetest/sweetest/keywords/web.py deleted file mode 100644 index 785ffc6..0000000 --- a/sweetest/sweetest/keywords/web.py +++ /dev/null @@ -1,382 +0,0 @@ -from selenium.webdriver.common.action_chains import ActionChains -from selenium.common.exceptions import ElementClickInterceptedException -from selenium.webdriver.support.select import Select -from time import sleep -import re -from sweetest.globals import g -from sweetest.elements import e -from sweetest.windows import w -from sweetest.locator import locating_elements, locating_data, locating_element -from sweetest.log import logger -from sweetest.parse import data_format -from sweetest.utility import compare, json2dict - - -class Common(): - @classmethod - def title(cls, data, output): - logger.info('DATA:%s' % repr(data['text'])) - logger.info('REAL:%s' % repr(g.driver.title)) - try: - if data['text'].startswith('*'): - assert data['text'][1:] in g.driver.title - else: - assert data['text'] == g.driver.title - except: - raise Exception(f'Check Failure, DATA:{data["text"]}, REAL:{g.driver.title}') - # 只能获取到元素标题 - for key in output: - g.var[key] = g.driver.title - return g.driver.title - - - @classmethod - def current_url(cls, data, output): - logger.info('DATA:%s' % repr(data['text'])) - logger.info('REAL:%s' % repr(g.driver.current_url)) - try: - if data['text'].startswith('*'): - assert data['text'][1:] in g.driver.current_url - else: - assert data['text'] == g.driver.current_url - except: - raise Exception(f'Check Failure, DATA:{data["text"]}, REAL:{g.driver.current_url}') - # 只能获取到元素 url - for key in output: - g.var[key] = g.driver.current_url - return g.driver.current_url - - -def open(step): - element = step['element'] - value = e.get(element)[1] - if step['data'].get('清理缓存', '') or step['data'].get('clear', ''): - g.driver.delete_all_cookies() - if step['data'].get('#open_type', '') in ('新标签页', 'tab'): - js = "window.open('%s')" % value - g.driver.execute_script(js) - # 判断是否打开了新的窗口,并将新窗口添加到所有窗口列表里 - all_handles = g.driver.window_handles - for handle in all_handles: - if handle not in w.windows.values(): - w.register(step, handle) - else: - if step['data'].get('#open_type', '') in ('新浏览器' , 'browser'): - w.close() - g.set_driver() - w.init() - g.driver.get(value) - w.open(step) - cookie = step['data'].get('cookie', '') - if cookie: - g.driver.add_cookie(json2dict(cookie)) - co = g.driver.get_cookie(json2dict(cookie).get('name', '')) - logger.info(f'cookie is add: {co}') - sleep(0.5) - - -def check(step): - data = step['data'] - if not data: - data = step['expected'] - - element = step['element'] - element_location = locating_element(element) - if '#' in element: - e_name = element.split('#')[0] + '#' - else: - e_name = element - by = e.elements[e_name]['by'] - output = step['output'] - var = {} - - if by in ('title', 'current_url'): - var[by] = getattr(Common, by)(data, output) - - else: - for key in data: - # 预期结果 - expected = data[key] - # 切片操作处理 - s = re.findall(r'\[.*?\]', key) - if s: - s = s[0] - key = key.replace(s, '') - - if key == 'text': - real = element_location.text - else: - real = element_location.get_attribute(key) - if s: - real = eval('real' + s) - - logger.info('DATA:%s' % repr(expected)) - logger.info('REAL:%s' % repr(real)) - try: - compare(expected, real) - except: - raise Exception(f'Check Failure, DATA:{repr(expected)}, REAL:{repr(real)}') - - # 获取元素其他属性 - for key in output: - if output[key] == 'text': - var[key] = g.var[key] = element_location.text - elif output[key] in ('text…', 'text...'): - if element_location.text.endswith('...'): - var[key] = g.var[key] = element_location.text[:-3] - else: - var[key] = g.var[key] = element_location.text - else: - var[key] = g.var[key] = element_location.get_attribute(output[key]) - if var: - step['_output'] += '\n||output=' + str(var) - return element_location - - -def notcheck(step): - data = step['data'] - if not data: - data = step['expected'] - - element = step['element'] - # element_location = locating_element(element) - - if e.elements[element]['by'] == 'title': - assert data['text'] != g.driver.title - - -def input(step): - data = step['data'] - element = step['element'] - element_location = locating_element(element) - - if step['data'].get('清除文本', '') == '否' or step['data'].get('clear', '').lower() == 'no': - pass - else: - element_location.clear() - - for key in data: - if key.startswith('text'): - if isinstance(data[key], tuple): - element_location.send_keys(*data[key]) - elif element_location: - element_location.send_keys(data[key]) - sleep(0.5) - if key == 'word': #逐字输入 - for d in data[key]: - element_location.send_keys(d) - sleep(0.3) - return element_location - - -def click(step): - element = step['element'] - data = step['data'] - if isinstance(element, str): - element_location = locating_element(element, 'CLICK') - if element_location: - try: - element_location.click() - except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 - sleep(1) - if data.get('mode'): - g.driver.execute_script("arguments[0].click();", element_location) - else: - element_location.click() - elif isinstance(element, list): - for _e in element: - element_location = locating_element(_e, 'CLICK') - try: - element_location.click() - except ElementClickInterceptedException: # 如果元素为不可点击状态,则等待1秒,再重试一次 - sleep(1) - if data.get('mode'): - g.driver.execute_script("arguments[0].click();", element_location) - else: - element_location.click() - sleep(0.5) - sleep(0.5) - - # 获取元素其他属性 - output = step['output'] - for key in output: - if output[key] == 'text': - g.var[key] = element_location.text - elif output[key] in ('text…', 'text...'): - if element_location.text.endswith('...'): - g.var[key] = element_location.text[:-3] - else: - g.var[key] = element_location.text - else: - g.var[key] = element_location.get_attribute(output[key]) - - # 判断是否打开了新的窗口,并将新窗口添加到所有窗口列表里 - all_handles = g.driver.window_handles - for handle in all_handles: - if handle not in w.windows.values(): - w.register(step, handle) - - return element_location - - -def select(step): - data = step['data'] - element = step['element'] - element_location = locating_element(element) - for key in data: - if key.startswith('index'): - Select(element_location).select_by_index(data[key]) - elif key.startswith('value'): - Select(element_location).select_by_value(data[key]) - elif key.startswith('text') or key.startswith('visible_text'): - Select(element_location).select_by_visible_text(data[key]) - - -def deselect(step): - data = step['data'] - element = step['element'] - element_location = locating_element(element) - for key in data: - if key.startswith('all'): - Select(element_location).deselect_all() - elif key.startswith('index'): - Select(element_location).deselect_by_index(data[key]) - elif key.startswith('value'): - Select(element_location).deselect_by_value(data[key]) - elif key.startswith('text') or key.startswith('visible_text'): - Select(element_location).deselect_by_visible_text(data[key]) - - -def hover(step): - actions = ActionChains(g.driver) - element = step['element'] - element_location = locating_element(element) - actions.move_to_element(element_location) - actions.perform() - sleep(0.5) - - return element_location - - -def context_click(step): - actions = ActionChains(g.driver) - element = step['element'] - element_location = locating_element(element) - actions.context_click(element_location) - actions.perform() - sleep(0.5) - - return element_location - - -def double_click(step): - actions = ActionChains(g.driver) - element = step['element'] - element_location = locating_element(element) - actions.double_click(element_location) - actions.perform() - sleep(0.5) - - return element_location - - -def drag_and_drop(step): - actions = ActionChains(g.driver) - element = step['element'] - source = locating_element(element[0]) - target = locating_element(element[1]) - actions.drag_and_drop(source, target) - actions.perform() - sleep(0.5) - - -def swipe(step): - actions = ActionChains(g.driver) - element = step['element'] - data = step['data'] - - source = locating_element(element) - x = data.get('x', 0) - y = data.get('y', 0) - actions.drag_and_drop_by_offset(source, x, y) - actions.perform() - sleep(0.5) - - -def script(step): - element = step['element'] - value = e.get(element)[1] - g.driver.execute_script(value) - - -def message(step): - data = step['data'] - text = data.get('text', '') - element = step['element'] - value = e.get(element)[1] - - if value.lower() in ('确认', 'accept'): - g.driver.switch_to_alert().accept() - elif value.lower() in ('取消', '关闭', 'cancel', 'close'): - g.driver.switch_to_alert().dismiss() - elif value.lower() in ('输入', 'input'): - g.driver.switch_to_alert().send_keys(text) - g.driver.switch_to_alert().accept() - logger.info('--- Switch Frame: Alert') - w.frame = 'Alert' - - -def upload(step): - import win32com.client - - data = step['data'] - element = step['element'] - element_location = locating_element(element) - file_path = data.get('text', '') or data.get('file', '') - - element_location.click() - sleep(3) - shell = win32com.client.Dispatch("WScript.Shell") - shell.Sendkeys(file_path) - sleep(2) - shell.Sendkeys("{ENTER}") - sleep(2) - - -def navigate(step): - element = step['element'] - - if element.lower() in ('刷新', 'refresh'): - g.driver.refresh() - elif element.lower() in ('前进', 'forward'): - g.driver.forward() - elif element.lower() in ('后退', 'back'): - g.driver.back() - - -def scroll(step): - data = step['data'] - x = data.get('x') - y = data.get('y') or data.get('text') - - element = step['element'] - if element == '': - # if x is None: - # x = '0' - # g.driver.execute_script( - # f"window.scrollTo({x},{y})") - if y: - g.driver.execute_script( - f"document.documentElement.scrollTop={y}") - if x: - g.driver.execute_script( - f"document.documentElement.scrollLeft={x}") - else: - element_location = locating_element(element) - - if y: - g.driver.execute_script( - f"arguments[0].scrollTop={y}", element_location) - if x: - g.driver.execute_script( - f"arguments[0].scrollLeft={x}", element_location) \ No newline at end of file diff --git a/sweetest/sweetest/keywords/windows.py b/sweetest/sweetest/keywords/windows.py deleted file mode 100644 index b49738c..0000000 --- a/sweetest/sweetest/keywords/windows.py +++ /dev/null @@ -1,222 +0,0 @@ -from pywinauto.application import Application -from pywinauto.keyboard import send_keys as sendkeys -import re -from sweetest.log import logger -from sweetest.globals import g -from sweetest.utility import compare - -class Windows(): - def __init__(self, app): - self.app = app - self.backend = app.backend.name - self.dialogs = [] - - def dialog(self, page): - if page == []: - if self.dialogs: - return self.dialogs[-1] - else: - raise Exception('Dialog: your page start with "", but there is no parent dialog') - elif page[0] == '<': - if len(self.dialogs) >= 2: - self.dialogs.pop() - return self.dialog(page[1:]) - else: - raise Exception('Dialog: your page start with "<", but the parent is less than 1 dialog') - elif page[0] == '>': - if self.dialogs: - if self.backend == 'win32': - current_dialog = self.app.window(best_match=page[1]) - elif self.backend == 'uia': - current_dialog = self.dialogs[-1].child_window(best_match=page[1]) - self.dialogs.append(current_dialog) - return self.dialog(page[2:]) - else: - raise Exception('Dialog: your page start with ">", but there is no parent dialog') - else: - current_dialog = self.app.window(best_match=page[0]) - self.dialogs = [current_dialog] - return self.dialog(page[1:]) - - -def menu_select(dialog, step): - element = step['element'] - try: - dialog.menu_select(element) - except: - for el in element.split('->'): - dialog.child_window(best_match=el).select() - - -def select(dialog, step): - element = step['element'] - if dialog.backend.name == 'win32': - dialog.window(best_match=element).select() - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).select() - - -def click(dialog, step): - element = step['element'] - if dialog.backend.name == 'win32': - dialog.window(best_match=element).click_input() - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).click_input() - - -def check_off(dialog, step): - element = step['element'] - if dialog.backend.name == 'win32': - dialog.window(best_match=element).check() - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).check() - - -def double_click(dialog,step): - element = step['element'] - if dialog.backend.name == 'win32': - dialog.window(best_match=element).double_click_input() - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).double_click_input() - - -def input(dialog, step): - element = step['element'] - value = step['data']['text'] - if dialog.backend.name == 'win32': - dialog.window(best_match=element).type_keys(value, with_spaces=True, with_newlines='\r\n') - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).type_keys(value, with_spaces=True, with_newlines='\r\n') - - -def set_text(dialog, step): - element = step['element'] - value = step['data']['text'] - if dialog.backend.name == 'win32': - dialog.window(best_match=element).set_edit_text(value) - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).set_edit_text(value) - - -def send_keys(dialog, step): - element = step['element'] - value = step['data'].get('text') - dialog.set_focus() - if element: - if dialog.backend.name == 'win32': - dialog.window(best_match=element).set_focus() - elif dialog.backend.name == 'uia': - dialog.child_window(best_match=element).set_focus() - sendkeys(value) - else: - sendkeys(value) - - -def check(dialog, step): - element = step['element'] - data = step['data'] - if not data: - data = step['expected'] - output = step['output'] - for key in data: - # 预期结果 - expected = data[key] - # 切片操作处理 - s = re.findall(r'\[.*?\]', key) - if s: - s = s[0] - key = key.replace(s, '') - - if key == 'text': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).texts()[0].replace('\r\n', '\n') - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).texts()[0].replace('\r\n', '\n') - elif key == 'value': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).text_block().replace('\r\n', '\n') - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).get_value().replace('\r\n', '\n') - if s: - real = eval('real' + s) - - if key == 'selected': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).is_selected() - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).is_selected() - elif key == 'checked': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).is_checked() - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).is_checked() - elif key == 'enabled': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).is_enabled() - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).is_enabled() - elif key == 'visible': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).is_visible() - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).is_visible() - elif key == 'focused': - if dialog.backend.name == 'win32': - real = dialog.window(best_match=element).is_focused() - elif dialog.backend.name == 'uia': - real = dialog.child_window(best_match=element).is_focused() - - logger.info('DATA:%s' % repr(expected)) - logger.info('REAL:%s' % repr(real)) - compare(expected, real) - - # 获取元素其他属性 - for key in output: - k = output[key] - if dialog.window(best_match=element).class_name() == 'Edit' and k == 'text': - k = 'value' - - if k == 'text': - if dialog.backend.name == 'win32': - g.var[key] = dialog.window(best_match=element).texts()[0].replace('\r\n', '\n') - elif dialog.backend.name == 'uia': - g.var[key] = dialog.child_window(best_match=element).texts()[0].replace('\r\n', '\n') - - if k == 'value': - if dialog.backend.name == 'win32': - g.var[key] = dialog.window(best_match=element).text_block().replace('\r\n', '\n') - elif dialog.backend.name == 'uia': - g.var[key] = dialog.child_window(best_match=element).get_value().replace('\r\n', '\n') - - -def window(dialog, step): - element = step['element'] - if element.lower() in ('最小化', 'minimize'): - dialog.minimize() - elif element.lower() in ('最大化', 'maximize'): - dialog.maximize() - elif element.lower() in ('恢复', 'restore'): - dialog.restore() - elif element.lower() in ('关闭', 'close'): - dialog.close() - elif element.lower() in ('前台', 'set_focus'): - dialog.set_focus() - elif element.lower() in ('重置', 'reset'): - for i in range(10): - if not dialog.has_focus(): - try: - # 如果弹出保存窗口 - save = dialog.get_active() - element = '(N)' - if dialog.backend.name == 'win32': - save.window(best_match=element).click_input() - elif dialog.backend.name == 'uia': - save.child_window(best_match=element).click_input() - except: - pass - #按 Alt+F4 关闭窗口 - sendkeys('%{F4}') - else: - break - else: - raise Exception(f'Reset the Windows is failure: try Alt+F4 for {i+1} times') \ No newline at end of file diff --git a/sweetest/sweetest/locator.py b/sweetest/sweetest/locator.py deleted file mode 100644 index 59e15d5..0000000 --- a/sweetest/sweetest/locator.py +++ /dev/null @@ -1,75 +0,0 @@ -from time import sleep -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from sweetest.elements import e -from sweetest.globals import g -from sweetest.windows import w -from sweetest.log import logger -from sweetest.config import element_wait_timeout - - -def locating_element(element, action=''): - el_location = None - try: - el, value = e.get(element) - except: - logger.exception( - 'Locating the element:%s is Failure, this element is not define' % element) - raise Exception( - 'Locating the element:%s is Failure, this element is not define' % element) - - if not isinstance(el, dict): - raise Exception( - 'Locating the element:%s is Failure, this element is not define' % element) - - wait = WebDriverWait(g.driver, element_wait_timeout) - - if el['by'].lower() in ('title', 'url', 'current_url'): - return None - else: - try: - el_location = wait.until(EC.presence_of_element_located( - (getattr(By, el['by'].upper()), value))) - except: - sleep(5) - try: - el_location = wait.until(EC.presence_of_element_located( - (getattr(By, el['by'].upper()), value))) - except : - raise Exception('Locating the element:%s is Failure: Timeout' % element) - try: - if g.driver.name in ('chrome', 'safari'): - g.driver.execute_script( - "arguments[0].scrollIntoViewIfNeeded(true)", el_location) - else: - g.driver.execute_script( - "arguments[0].scrollIntoView(false)", el_location) - except: - pass - - try: - if action == 'CLICK': - el_location = wait.until(EC.element_to_be_clickable( - (getattr(By, el['by'].upper()), value))) - else: - el_location = wait.until(EC.visibility_of_element_located( - (getattr(By, el['by'].upper()), value))) - except: - pass - - return el_location - - -def locating_elements(elements): - elements_location = {} - for el in elements: - elements_location[el] = locating_element(el) - return elements_location - - -def locating_data(keys): - data_location = {} - for key in keys: - data_location[key] = locating_element(key) - return data_location diff --git a/sweetest/sweetest/log.py b/sweetest/sweetest/log.py deleted file mode 100644 index 1615757..0000000 --- a/sweetest/sweetest/log.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging -import datetime -from pathlib import Path -import sys -from sweetest.utility import mkdir - - -def today(): - now = datetime.datetime.now() - return now.strftime('%Y%m%d') - - - -def set_log(logger, log_path): - mkdir('log') - - # 文件日志 - log_file = Path('log') / f'{today()}.log' - file_handler = logging.FileHandler(filename=log_file, encoding="utf-8") - file_handler.setFormatter(formatter) # 可以通过setFormatter指定输出格式 - - # 单次文件日志 - sweet_log = log_path / 'sweet.log' - try: - sweet_log.unlink() - except: - pass - sweet_handler = logging.FileHandler(filename=sweet_log, encoding="utf-8") - sweet_handler.setFormatter(formatter) # 可以通过setFormatter指定输出格式 - - # 控制台日志 - console_handler = logging.StreamHandler(sys.stdout) - console_handler.formatter = formatter # 也可以直接给formatter赋值 - - # 为logger添加的日志处理器 - logger.addHandler(file_handler) - logger.addHandler(sweet_handler) - logger.addHandler(console_handler) - - # 指定日志的最低输出级别,默认为WARN级别 - # DEBUG,INFO,WARNING,ERROR,CRITICAL - logger.setLevel(logging.INFO) - - return str(sweet_log) - -# 获取logger实例,如果参数为空则返回root logger -logger = logging.getLogger("sweetest") -# 指定logger输出格式 -formatter = logging.Formatter('%(asctime)s [%(levelname)s]: # %(message)s') \ No newline at end of file diff --git a/sweetest/sweetest/parse.py b/sweetest/sweetest/parse.py deleted file mode 100644 index efa2790..0000000 --- a/sweetest/sweetest/parse.py +++ /dev/null @@ -1,68 +0,0 @@ -from sweetest.log import logger -from sweetest.config import all_keywords, comma_lower, comma_upper, equals, vertical, zh_en -from sweetest.elements import e -from sweetest.globals import g - - -def escape(data): - # 先把转义字符替换掉 - # return data.replace('\\,', comma_lower).replace('\\,', comma_upper).replace('\\=', equals) - return data.replace('\\,', comma_lower) - - -def recover(data): - # 再把转义字符恢复 - # return data.replace(comma_lower, ',').replace(comma_upper, ',').replace(equals, '=') - return data.replace(comma_lower, ',') - - -def check_keyword(kw): - try: - keyword = all_keywords.get(kw) - return keyword - except: - logger.exception('Keyword:%s is not exist' % kw) - exit() - - -def data_format(data): - data = escape(data) - if ',,' in data: - data_list = data.split(',,') - else: - # data = data.replace(',', ',') # 中文逗号不再视为分隔符 - data_list = [] - if data: - data_list = data.split(',') - data_dict = {} - for data in data_list: - # 只需要分割第一个'='号 - d = data.split('=', 1) - d[-1] = recover(d[-1]) # 只有值需要转义恢复,<元素属性> or <变量名> 不应该出现转义字符 - if len(d) == 1: - # 如果没有=号分割,说明只有内容,默认赋值给 text - if not data_dict.get('text'): - data_dict['text'] = d[0] - elif len(d) == 2: - d[0] = d[0].strip() # 清除 <元素属性> 2边的空格,如果有的话 - d[0] = zh_en.get(d[0], d[0]) - data_dict[d[0]] = d[1] - else: - raise Exception( - 'Error: Testcase\'s Data is error, more "=" or less ","') - return data_dict - - -def parse(testsuit): - ''' - 将测试用例解析为可执行参数,如: - 打开首页,解析为:OPEN 127.0.0.1 - ''' - for testcase in testsuit: - for step in testcase['steps']: - step['keyword'] = check_keyword(step['keyword']) - # step['page'], step['custom'], step['element'] = elements_format( - # step['page'], step['element']) - step['data'] = data_format(str(step['data'])) - step['expected'] = data_format(str(step['expected'])) - step['output'] = data_format(step['output']) diff --git a/sweetest/sweetest/report.py b/sweetest/sweetest/report.py deleted file mode 100644 index 34ab058..0000000 --- a/sweetest/sweetest/report.py +++ /dev/null @@ -1,235 +0,0 @@ -import time -import arrow -from pathlib import Path -from sweetest.globals import g -from sweetest.utility import mkdir - - -def reporter(plan_data, testsuites_data, report_data, extra_data): - - extra_data['plan'] = plan_data['plan'] - extra_data['task'] = int(time.time() * 1000) - - testcases = [] - for key, ts in report_data.items(): - count = {'result': 'success', 'total': 0, - 'success': 0, 'failure': 0, 'blocked': 0} - no = 1 - for tc in ts: - tc['testsuite'] = key - tc['no'] = no - no += 1 - tc = {**extra_data, **tc} - - res = tc['result'].lower() - if tc['condition'].lower() in ('base', 'setup', 'snippet'): - pass - elif res in count: - count[res] += 1 - count['total'] += 1 - testcases.append(tc) - if count['failure'] + count['blocked']: - count['result'] = 'failure' - testsuites_data[key] = {**count, **testsuites_data[key]} - - testsuite = [] - count = {'total': 0, 'success': 0, - 'failure': 0, 'blocked': 0} - result = 'success' - for key, ts in testsuites_data.items(): - for k in count: - count[k] += ts[k] - if ts['result'] != 'success': - result = 'failure' - - ts = {**extra_data, **ts} - ts['testsuite'] = key - testsuite.append(ts) - count['result'] = result - - plan = {**extra_data, **count, **plan_data} - - return plan, testsuite, testcases - - -def local_time(timestamp): - import time - t = time.localtime(int(timestamp / 1000)) - return str(time.strftime("%Y/%m/%d %H:%M:%S", t)) - - -def cost_time(start, end): - return int((end - start) / 1000) - - -def summary(plan_data, testsuites_data, report_data, extra_data): - plan, testsuites, testcases = reporter( - plan_data, testsuites_data, report_data, extra_data) - - data = [['测试套件', '用例总数', '成功', '阻塞', '失败', '测试结果', '开始时间', '结束时间', '耗时(秒)']] - failures = [['测试套件', '用例编号', '用例标题', '用例结果', '失败步骤', '备注']] - - for suite in testsuites: - row = [suite['testsuite'], suite['total'], suite['success'], suite['blocked'], suite['failure'], - suite['result'], local_time(suite['start_timestamp']), local_time(suite['end_timestamp']), - cost_time(suite['start_timestamp'], suite['end_timestamp'])] - data.append(row) - - flag = False - for case in testcases: - suite_name = '' if flag else case['testsuite'] - row = [] - if case['result'] == 'blocked': - row = [suite_name, case['id'], case['title'], case['result']] - elif case['result'] == 'failure': - for step in case['steps']: - if step['score'] == 'NO': - desc = '|'.join([step[k] for k in ('no', 'keyword', 'page', 'element')]) - row = [suite_name, case['id'], case['title'], case['result'], desc, step['remark']] - break - if row: - flag = True - failures.append(row) - - data.append(['--------']) - total = ['总计', plan['total'], plan['success'], plan['blocked'], plan['failure'], - plan['result'], local_time(plan['start_timestamp']), local_time(plan['end_timestamp']), - cost_time(plan['start_timestamp'], plan['end_timestamp'])] - - data.append(total) - if len(failures) > 1: - data.append(['********']) - data += failures - return data - -def markdown(plan, testsuites, testcases, md_path='markdown'): - success = OK = '通过' - failure = NO = '失败' - blocked = '阻塞' - skipped = '-' - - md = '| 测试套件名称 | 开始时间 | 结束时间 | 耗时 | 成功个数 | 失败个数 | 阻塞个数 | 总个数 | 结果 |\n' - md +='| ----------- | ------- | ------- | ---- | ------- | ------- | -------- | ----- | ---- |\n' - - result = success - sc, fc, bc, tc = 0, 0, 0, 0 - for v in testsuites.values(): - sc += v['success'] - fc += v['failure'] - bc += v['blocked'] - tc += v['total'] - re = success - if v['result'] == 'failure': - re = failure - cost = round((v['end_timestamp'] - v['start_timestamp'])/1000, 1) - md += f'| {v["testsuite"]} | {tm(v["start_timestamp"])} | {tm(v["end_timestamp"])} | {cost} | ' - md += f'{v["success"]} | {v["failure"]} | {v["blocked"]} | {v["total"]} | {re} |\n' - if v['result'] == 'failure': - result = failure - cost = round((plan['end_timestamp'] - plan['start_timestamp'])/1000, 1) - md += f'| **共计** | {tm(plan["start_timestamp"])} | {tm(plan["end_timestamp"])} | {cost} | ' - md += f'{sc} | {fc} | {bc} | {tc} | {result} |\n' - title = f'# 「{plan["plan"]}」自动化测试执行报告 {result} #\n\n[历史记录](/{plan["plan"]}/)\n\n' - md = title + f'## 测试计划执行结果\n\n{md}\n\n## 测试套件执行结果\n\n' - - if result == success: - icon = '✔️' - else: - icon = '❌' - - message = f'- {icon} {tm(plan["start_timestamp"])} - {tm(plan["end_timestamp"])} 测试计划' - message +=f'「[{plan["plan"]}]({plan["plan"]}/{plan["plan"]}_{tm(plan["start_timestamp"], "_")})」执行完成,测试结果:{result},成功:{sc},失败:{fc},阻塞:{bc}\n\n' - - # 测试套件 - 测试用例结果 - txt = '' - for k,v in testcases.items(): - txt += f'\n- ### {k}\n\n' - txt += '| 用例id | 用例名称 | 前置条件 |开始时间 | 结束时间 | 耗时 | 结果 |\n' - txt += '| ------- | ------- | ----------- | -------------- | -------------- | ----- | ------- |\n' - for case in v: - if case['flag'] == 'N': - continue - cost = round((case['end_timestamp'] - case['start_timestamp'])/1000, 1) - result = eval(case['result']) - txt += f'| [{case["id"]}](#{case["id"]}) | {case["title"]} | {case["condition"]} | {tm(case["start_timestamp"])} | {tm(case["end_timestamp"])} | {cost} | {result} |\n' - - md += f'{txt}\n\n## 测试用例执行结果\n' - txt = '' - for k,v in testcases.items(): - txt += f'\n- ### {k}\n' - for case in v: - if case['flag'] == 'N': - continue - txt += f'\n#### {case["id"]}\n\n**{case["title"]}** | {case["condition"]} | {case["designer"]} | {eval(case["result"])}\n\n' - txt += '| 步骤 | 操作 | 页面 | 元素 | 测试数据 | 预期结果 | 输出数据 | 耗时 | 测试结果 | 备注 | 截图 |\n' - txt += '|------|-------|-------|------|-----------|---------|-----------|-----|---------|------|--------|\n' - for step in case['steps']: - cost = round((step.get('end_timestamp', 0) - step.get('start_timestamp', 0))/1000, 1) - if cost == 0: - cost = '-' - if not step['score']: - result = skipped - else: - result = eval(step['score']) - snapshot = '' - if 'snapshot' in step: - for k,v in step['snapshot'].items(): - snapshot += f"[{k}](/report/{v} ':ignore')\n" - txt += f'| {step["no"]} | {step["keyword"]} | {step["page"]} | {escape(step["element"])} | {escape(step["data"])} | {escape(step["expected"])} | {escape(step["output"])} | {cost} | {result} | {step["remark"]} | {escape(snapshot, "%23")} |\n' - result = eval(case['result']) - md += txt - - - p = Path(md_path) / 'report' - latest = p / 'latest' - report = p / g.plan_name - mkdir(p) - mkdir(report) - - - with open(p / 'README.md', 'r', encoding='UTF-8') as f: - txt = f.read() - if '恭喜你安装成功' in txt: - txt = '' - with open(p / f'README.md', 'w', encoding='UTF-8') as f: - f.write(message + txt) - with open(latest / f'{g.plan_name}.md','w', encoding='UTF-8') as f: - f.write(md) - readme = report / 'README.md' - if readme.is_file(): - with open(report / 'README.md', 'r', encoding='UTF-8') as f: - txt = f.read() - else: - txt = '' - with open(report / 'README.md','w', encoding='UTF-8') as f: - f.write(message + txt) - with open(report / f'{plan["plan"]}_{tm(plan["start_timestamp"], "_")}.md','w',encoding='UTF-8') as f: - f.write(md) - with open(p / '_sidebar.md', 'r', encoding='UTF-8') as f: - txt = f.read() - if f'[{g.plan_name}]' not in txt: - with open(p / '_sidebar.md', 'a',encoding='UTF-8') as f: - f.write(f'\n * [{g.plan_name}](latest/{g.plan_name})') - - files = [] - for f in report.iterdir(): - if f.stem not in ['_sidebar', 'README']: - files.append(f.stem) - files.sort(reverse=True) - with open(report / '_sidebar.md', 'w',encoding='UTF-8') as f: - txt = f'* 「{g.plan_name}」测试结果\n' - for stem in files: - txt +=f'\n * [{stem}]({g.plan_name}/{stem})' - f.write(txt) - - - - -def escape(data, well='#'): - return data.replace('|', '\|').replace('<', '\<').replace('>', '\>').replace('\n', '
').replace('#', well) - -def tm(stamp, dot=' '): - if dot == ' ': - return arrow.get(stamp/1000).to('local').format(f'YYYY-MM-DD{dot}HH:mm:ss').replace(':', ':') - elif dot == '_': - return arrow.get(stamp/1000).to('local').format(f'YYYYMMDD{dot}HH:mm:ss').replace(':', '') \ No newline at end of file diff --git a/sweetest/sweetest/snapshot.py b/sweetest/sweetest/snapshot.py deleted file mode 100644 index 9250a85..0000000 --- a/sweetest/sweetest/snapshot.py +++ /dev/null @@ -1,295 +0,0 @@ -from pathlib import Path -from PIL import Image -from PIL import ImageChops -import math -import operator -import time -from functools import reduce -from sweetest.globals import g, now -from sweetest.log import logger -from sweetest.utility import mkdir - - -# 解决打开图片大于 20M 的限制 -Image.MAX_IMAGE_PIXELS = None - -def crop(element, src, target): - location = element.location - size = element.size - im = Image.open(src) - left = location['x'] - top = location['y'] - right = location['x'] + size['width'] - bottom = location['y'] + size['height'] - im = im.crop((left, top, right, bottom)) - im.save(target) - - -def blank(src, boxs): - white = Image.new('RGB',(5000, 5000), 'white') - im = Image.open(src) - for box in boxs: - w = white.crop(box) - im.paste(w, box[:2]) - im.save(src) - - -def cut(src, target, box): - im = Image.open(src) - im = im.crop(box) - im.save(target) - - -def get_screenshot(file_path): - if g.headless: - width = g.driver.execute_script("return Math.max(document.body.scrollWidth, document.documentElement.clientWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth);") - height = g.driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);") - g.driver.set_window_size(width,height) - time.sleep(3) - g.driver.get_screenshot_as_file(file_path) - if g.headless: - g.driver.set_window_size(1920,1080) - - -class Snapshot: - def __init__(self): - snapshot_plan = Path('snapshot') / g.plan_name - self.snapshot_folder = snapshot_plan / g.start_time[1:] - snapshot_expected = Path('snapshot') / 'expected' - self.expected_folder = snapshot_expected / g.plan_name - for p in (snapshot_plan, self.snapshot_folder, snapshot_expected, self.expected_folder): - mkdir(p) - - def pre(self, step, label): - self.label = label - self.screen_flag = False - self.element_flag = False - - # 处理输出数据中的截图设置 - self.output = {} - for k, v in dict(step['output'].items()).items(): - if v in ('#screen_shot','#ScreenShot'): - self.output['#screen_shot'] = k - step['output'].pop(k) - self.screen_flag = True - if v == ('#element_shot', '#ElementShot'): - self.output['#element_shot'] = k - step['output'].pop(k) - self.element_flag = True - - # 处理图片比较 - self.expected = {} - for data in (step['data'], step['expected']): - if '#screen_shot' in data: - self.screen_flag = True - p = Path(data['#screen_shot']).stem - self.expected['#screen_name'] = '('+p.split('[')[1].split(']')[0]+')' if '[' in p else p - if Path(data['#screen_shot']).is_file(): - - step['snapshot']['expected_screen'] = data.pop('#screen_shot') - else: - step['snapshot']['expected_screen'] = str(self.expected_folder / data.pop('#screen_shot')) - - if '#element_shot' in data: - self.element_flag = True - p = Path(data['#element_shot']).stem - self.expected['#element_name'] = '('+p.split('[')[1].split(']')[0]+')' if '[' in p else p - if Path(data['#element_shot']).is_file(): - step['snapshot']['expected_element'] = data.pop('#element_shot') - else: - step['snapshot']['expected_element'] = str(self.expected_folder / data.pop('#element_shot')) - - if '#cut' in data: - self.expected['#cut'] = data.pop('#cut') - if '#blank' in data: - self.expected['#blank'] = data.pop('#blank') - - def web_screen(self, step, element): - # 截图 - screen_v = self.output.get('#screen_shot', '') - element_v = self.output.get('#element_shot', '') - - if g.snapshot or self.screen_flag or self.element_flag: - from selenium.webdriver.support import expected_conditions as EC - if not EC.alert_is_present()(g.driver): - if self.expected.get('#screen_name'): - screen_name = self.expected['#screen_name'] - elif screen_v: - screen_name = screen_v - else: - screen_name = '' - if screen_name: - file_name = self.label + now() + '#screen' + '[' + screen_name + ']' + '.png' - else: - file_name = self.label + now() + '#screen' + '.png' - step['snapshot']['real_screen'] = str( - Path(self.snapshot_folder) / file_name) - get_screenshot(step['snapshot']['real_screen']) - if screen_v: - g.var[screen_v] = step['snapshot']['real_screen'] - - if element_v: - file_name = self.label + now() + '#element' + '[' + element_v + ']' + '.png' - step['snapshot']['real_element'] = str(Path(self.snapshot_folder) / file_name) - crop(element, step['snapshot']['real_screen'], step['snapshot']['real_element']) - g.var[element_v] = step['snapshot']['real_element'] - - def web_check(self, step, element): - - def deep(src): - # 把不需要比较的部分贴白 - if self.expected.get('#blank'): - blank(src, eval(self.expected.get('#blank'))) - # 裁剪需要比较的部分 - if self.expected.get('#cut'): - cut(src, src, eval(self.expected.get('#cut'))) - - if Path(step['snapshot'].get('expected_screen', '')).is_file(): - # 屏幕截图比较 - image1 = Image.open(step['snapshot']['expected_screen']) - image2 = Image.open(step['snapshot']['real_screen']) - deep(step['snapshot']['real_screen']) - histogram1 = image1.histogram() - histogram2 = image2.histogram() - differ = math.sqrt(reduce(operator.add, list( - map(lambda a, b: (a - b)**2, histogram1, histogram2))) / len(histogram1)) - diff = ImageChops.difference(image1.convert('RGB'), image2.convert('RGB')) - if differ == 0.0: - # 图片间没有任何不同 - logger.info('SnapShot: screen_shot is the same') - else: - file_name = self.label + now() + 'diff_screen' + '.png' - step['snapshot']['diff_screen'] = str( - Path(self.snapshot_folder) / file_name) - diff.save(step['snapshot']['diff_screen']) - raise Exception('SnapShot: screen_shot is diff: %s' % differ) - elif step['snapshot'].get('expected_screen'): - get_screenshot(step['snapshot']['expected_screen']) - deep(step['snapshot']['expected_screen']) - - if Path(step['snapshot'].get('expected_element', '')).is_file(): - file_name = self.label + now() + '#element' + '[' + self.expected['#element_name'] + ']' + '.png' - step['snapshot']['real_element'] = str(Path(self.snapshot_folder) / file_name) - crop(element, step['snapshot']['real_screen'], step['snapshot']['real_element']) - deep(step['snapshot']['real_element']) - - # 屏幕截图比较 - image1 = Image.open(step['snapshot']['expected_element']) - image2 = Image.open(step['snapshot']['real_element']) - histogram1 = image1.histogram() - histogram2 = image2.histogram() - differ = math.sqrt(reduce(operator.add, list( - map(lambda a, b: (a - b)**2, histogram1, histogram2))) / len(histogram1)) - diff = ImageChops.difference(image1.convert('RGB'), image2.convert('RGB')) - if differ == 0.0: - logger.info('SnapShot: element_shot is the same') - else: - file_name = self.label + now() + 'diff_element' + '.png' - step['snapshot']['diff_element'] = str( - Path(self.snapshot_folder) / file_name) - diff.save(step['snapshot']['diff_element']) - raise Exception('SnapShot: element_shot is diff: %s' % differ) - elif step['snapshot'].get('expected_element'): - crop(element, step['snapshot']['real_screen'], step['snapshot']['expected_element']) - deep(step['snapshot']['expected_element']) - - def web_shot(self, step, element): - self.web_screen(step, element) - self.web_check(step, element) - - - def windwos_capture(self, dialog, step): - # 截图 - screen_v = self.output.get('#screen_shot', '') - element_v = self.output.get('#element_shot', '') - - if g.snapshot or self.screen_flag: - if self.expected.get('#screen_name'): - screen_name = self.expected['#screen_name'] - elif screen_v: - screen_name = screen_v - else: - screen_name = '' - if screen_name: - file_name = self.label + now() + '#screen' + '[' + screen_name + ']' + '.png' - else: - file_name = self.label + now() + '#screen' + '.png' - step['snapshot']['real_screen'] = str(Path(self.snapshot_folder) / file_name) - pic = dialog.capture_as_image() - pic.save(step['snapshot']['real_screen']) - if screen_v: - g.var[screen_v] = step['snapshot']['real_screen'] - - if element_v: - file_name = self.label + now() + '#element' + '[' + element_v + ']' + '.png' - step['snapshot']['real_element'] = str(Path(self.snapshot_folder) / file_name) - - element = step['element'] - if dialog.backend.name == 'win32': - pic = dialog.window(best_match=element).capture_as_image() - elif dialog.backend.name == 'uia': - pic = dialog.child_window(best_match=element).capture_as_image() - pic.save(step['snapshot']['real_screen']) - g.var[element_v] = step['snapshot']['real_element'] - - - def windwos_check(self, dialog, step): - element = step['element'] - if Path(step['snapshot'].get('expected_screen', '')).is_file(): - # 屏幕截图比较 - image1 = Image.open(step['snapshot']['expected_screen']) - image2 = Image.open(step['snapshot']['real_screen']) - histogram1 = image1.histogram() - histogram2 = image2.histogram() - differ = math.sqrt(reduce(operator.add, list( - map(lambda a, b: (a - b)**2, histogram1, histogram2))) / len(histogram1)) - diff = ImageChops.difference(image1.convert('RGB'), image2.convert('RGB')) - if differ == 0.0: - # 图片间没有任何不同 - logger.info('SnapShot: screen_shot is the same') - else: - file_name = self.label + now() + 'diff_screen' + '.png' - step['snapshot']['diff_screen'] = str( - Path(self.snapshot_folder) / file_name) - diff.save(step['snapshot']['diff_screen']) - raise Exception('SnapShot: screen_shot is diff: %s' % differ) - elif step['snapshot'].get('expected_screen'): - pic = dialog.capture_as_image() - pic.save(step['snapshot']['expected_screen']) - - if Path(step['snapshot'].get('expected_element', '')).is_file(): - file_name = self.label + now() + '#element' + '.png' - step['snapshot']['real_element'] = str(Path(self.snapshot_folder) / file_name) - if dialog.backend.name == 'win32': - pic = dialog.window(best_match=element).capture_as_image() - elif dialog.backend.name == 'uia': - pic = dialog.child_window(best_match=element).capture_as_image() - pic.save(step['snapshot']['real_element']) - - # 屏幕截图比较 - image1 = Image.open(step['snapshot']['expected_element']) - image2 = Image.open(step['snapshot']['real_element']) - histogram1 = image1.histogram() - histogram2 = image2.histogram() - differ = math.sqrt(reduce(operator.add, list( - map(lambda a, b: (a - b)**2, histogram1, histogram2))) / len(histogram1)) - diff = ImageChops.difference(image1.convert('RGB'), image2.convert('RGB')) - if differ == 0.0: - logger.info('SnapShot: element_shot is the same') - else: - file_name = self.label + now() + 'diff_element' + '.png' - step['snapshot']['diff_element'] = str( - Path(self.snapshot_folder) / file_name) - diff.save(step['snapshot']['diff_element']) - raise Exception('SnapShot: element_shot is diff: %s' % differ) - elif step['snapshot'].get('expected_element'): - if dialog.backend.name == 'win32': - pic = dialog.window(best_match=element).capture_as_image() - elif dialog.backend.name == 'uia': - pic = dialog.child_window(best_match=element).capture_as_image() - pic.save(step['snapshot']['expected_element']) - - - def windows_shot(self, dialog, step): - self.windwos_capture(dialog, step) - self.windwos_check(dialog, step) \ No newline at end of file diff --git a/sweetest/sweetest/testcase.py b/sweetest/sweetest/testcase.py deleted file mode 100644 index d91cfa5..0000000 --- a/sweetest/sweetest/testcase.py +++ /dev/null @@ -1,283 +0,0 @@ -from time import sleep -from pathlib import Path -import re -from sweetest.log import logger -from sweetest.globals import g, now, timestamp -from sweetest.elements import e -from sweetest.windows import w -from sweetest.snapshot import Snapshot -from sweetest.locator import locating_elements, locating_data, locating_element -from sweetest.keywords import web, common, mobile, http, files -from sweetest.config import web_keywords, common_keywords, mobile_keywords, http_keywords, windows_keywords, files_keywords -from sweetest.utility import replace_dict, replace - - -def elements_format(page, element): - if not page: - page = g.current_page - - if not element: - return page, '', element - - if page in ('SNIPPET', '用例片段') or element in ('变量赋值',): - return page, '', element - - elements = element.split('|') - if len(elements) == 1: - custom, el = e.have(page, element) - g.current_page = page - return page, custom, el - else: - els = [] - for _element in elements: - custom, el = e.have(page, _element.strip()) - els.append(el) - g.current_page = page - return page, custom, els - - -def v_data(d, _d): - data = '' - if ',,' in str(_d): - s = ',,' - else: - s = ',' - - for k, v in d.items(): - data += k + '=' + str(v) + s - - if s ==',,': - return data[:-2] - else: - return data[:-1] - - -def test_v_data(): - data = {'a': 1, 'b': 'b'} - _data = "{'a': 1,, 'b': 'b'}" - return v_data(data, _data) - - -class TestCase: - def __init__(self, testcase): - self.testcase = testcase - self.snippet_steps = {} - - def run(self): - logger.info('>>> Run the TestCase: %s|%s' % - (self.testcase['id'], self.testcase['title'])) - logger.info('-'*50) - self.testcase['result'] = 'success' - self.testcase['report'] = '' - if_result = '' - - for index, step in enumerate(self.testcase['steps']): - # 统计开始时间 - step['start_timestamp'] = timestamp() - step['snapshot'] = {} - - # if 为否,不执行 then 语句 - if step['control'] == '>' and not if_result: - step['score'] = '-' - step['end_timestamp'] = timestamp() - logger.info('Skipped the Step: %s|%s|%s|%s' %(step['no'], step['page'], step['keyword'], step['element'])) - continue - - # if 为真,不执行 else 语句 - if step['control'] == '<' and if_result: - step['score'] = '-' - step['end_timestamp'] = timestamp() - logger.info('Skipped the Step: %s|%s|%s|%s' %(step['no'], step['page'], step['keyword'], step['element'])) - continue - - if g.platform.lower() in ('windows',) and step['keyword'].upper() in windows_keywords: - pass - elif step['keyword'].upper() in files_keywords: - pass - else: - step['page'], step['custom'], step['element'] = elements_format( - step['page'], step['element']) - - label = g.sheet_name + '#' + \ - self.testcase['id'] + '#' + str(step['no']).replace('<', '(').replace('>', ')').replace('*', 'x') - - logger.info('Run the Step: %s|%s|%s|%s' % - (step['no'], step['page'], step['keyword'], step['element'])) - - snap = Snapshot() - try: - after_function = step['data'].pop('AFTER_FUNCTION', '') - - # 处理强制等待时间 - if '#wait_time' in step['data']: - t = step['data'].pop('#wait_time', 0) - sleep(float(t)) - else: - if '#wait_times' in step['data']: - g.wait_times = float(step['data'].pop('#wait_times', 0)) - if g.wait_times: - sleep(g.wait_times) - - if step['page']: - step['page'] = replace(step['page']) - - if isinstance(step['element'], str): - step['element'] = replace(step['element']) - step['_element'] = step['element'] - elif isinstance(step['element'], list): - for i in range(len(step['element'])): - step['element'][i] = replace(step['element'][i]) - step['_element'] = '|'.join(step['element']) - - # 变量替换 - replace_dict(step['data']) - replace_dict(step['expected']) - - step['data'].pop('BEFORE_FUNCTION', '') - - step['vdata'] = v_data(step['data'], step['_data']) - - if g.platform.lower() in ('desktop',) and step['keyword'].upper() in web_keywords: - # 处理截图数据 - snap.pre(step, label) - - if step['keyword'].upper() not in ('MESSAGE', '对话框'): - # 判断页面是否已和窗口做了关联,如果没有,就关联当前窗口,如果已关联,则判断是否需要切换 - w.switch_window(step['page']) - # 切换 frame 处理,支持变量替换 - frame = replace(step['custom']) - w.switch_frame(frame) - - # 根据关键字调用关键字实现 - element = getattr(web, step['keyword'].lower())(step) - snap.web_shot(step, element) - - elif g.platform.lower() in ('ios', 'android') and step['keyword'].upper() in mobile_keywords: - # 切換 context 處理 - context = replace(step['custom']).strip() - w.switch_context(context) - - if w.current_context.startswith('WEBVIEW'): - # 切换标签页 - tab = step['data'].get('标签页') - if tab: - del step['data']['标签页'] - g.driver.switch_to_window(w.windows[tab]) - logger.info('Current Context: %s' % - repr(w.current_context)) - - # 根据关键字调用关键字实现 - getattr(mobile, step['keyword'].lower())(step) - - elif g.platform.lower() in ('windows',) and step['keyword'].upper() in windows_keywords: - from sweetest.keywords import windows - _page = '' - if step['page'].startswith('#'): - _page = step['page'][1:] - page = [x for x in re.split(r'(<|>)', _page) if x !=''] - else: - page = [x for x in re.split(r'(<|>)', step['page']) if x !=''] - - if _page: - dialog = g.windows['#'].dialog(page) - else: - dialog = g.windows['default'].dialog(page) - #dialog.wait('ready') - - snap.pre(step, label) - - # 根据关键字调用关键字实现 - getattr(windows, step['keyword'].lower())(dialog, step) - snap.windows_shot(dialog, step) - - elif step['keyword'].upper() in http_keywords: - # 根据关键字调用关键字实现 - getattr(http, step['keyword'].lower())(step) - - elif step['keyword'].upper() in files_keywords: - # 根据关键字调用关键字实现 - getattr(files, step['keyword'].lower())(step) - - elif step['keyword'].lower() == 'execute': - result, steps = getattr( - common, step['keyword'].lower())(step) - if step['page'] in ('SNIPPET', '用例片段'): - self.snippet_steps[index + 1] = steps - if result != 'success': - self.testcase['result'] = result - step['end_timestamp'] = timestamp() - break - - # elif step['page'] in ('SCRIPT', '脚本'): - # # 判断页面是否已和窗口做了关联,如果没有,就关联当前窗口,如果已关联,则判断是否需要切换 - # w.switch_window(step['page']) - # # 切换 frame 处理,支持变量替换 - # frame = replace(step['custom']) - # w.switch_frame(frame) - # common.script(step) - - else: - # 根据关键字调用关键字实现 - getattr(common, step['keyword'].lower())(step) - logger.info('>>>>>>>>>>>>>>>>>>>> success <<<<<<<<<<<<<<<<<<<<') - step['score'] = 'OK' - - # if 语句结果赋值 - if step['control'] == '^': - if_result = True - - if after_function: - replace_dict({'after_function': after_function}) - # 操作后,等待0.2秒 - sleep(0.2) - except Exception as exception: - - if g.platform.lower() in ('desktop',) and step['keyword'].upper() in web_keywords: - file_name = label + now() + '#Failure' +'.png' - step['snapshot']['Failure'] = str(snap.snapshot_folder / file_name) - try: - if w.frame != 0: - g.driver.switch_to.default_content() - w.frame = 0 - g.driver.get_screenshot_as_file(step['snapshot']['Failure']) - except: - logger.exception( - '*** save the screenshot is failure ***') - - elif g.platform.lower() in ('ios', 'android') and step['keyword'].upper() in mobile_keywords: - file_name = label + now() + '#Failure' +'.png' - step['snapshot']['Failure'] = str(snap.snapshot_folder / file_name) - try: - g.driver.switch_to.context('NATIVE_APP') - w.current_context = 'NATIVE_APP' - g.driver.get_screenshot_as_file(u'%s' %step['snapshot']['Failure']) - except: - logger.exception('*** save the screenshot is failure ***') - - logger.exception('Exception:') - logger.error('>>>>>>>>>>>>>>>>>>>> failure <<<<<<<<<<<<<<<<<<<<') - step['score'] = 'NO' - - step['remark'] += str(exception) - step['end_timestamp'] = timestamp() - - # if 语句结果赋值 - if step['control'] == '^': - if_result = False - continue - - self.testcase['result'] = 'failure' - self.testcase['report'] = 'step-%s|%s|%s: %s' % ( - step['no'], step['keyword'], step['element'], exception) - break - - # 统计结束时间 - step['end_timestamp'] = timestamp() - - steps = [] - i = 0 - for k in self.snippet_steps: - steps += self.testcase['steps'][i:k] + self.snippet_steps[k] - i = k - steps += self.testcase['steps'][i:] - self.testcase['steps'] = steps diff --git a/sweetest/sweetest/testsuite.py b/sweetest/sweetest/testsuite.py deleted file mode 100644 index 0e258b3..0000000 --- a/sweetest/sweetest/testsuite.py +++ /dev/null @@ -1,232 +0,0 @@ -import time -from copy import deepcopy -from sweetest.globals import g, timestamp -from sweetest.windows import w -from sweetest.testcase import TestCase -from sweetest.log import logger -from sweetest.utility import replace - - -class TestSuite: - def __init__(self, testsuite, sheet_name, report, conditions={}): - self.testsuite = testsuite - self.sheet_name = sheet_name - self.report = report - self.conditions = conditions - self.result = {} - - # base 在整个测试套件中首先执行一次 - self.base_testcase = {} - # setup 在每个测试用例执行之前执行一次 - self.setup_testcase = {} - for testcase in self.testsuite: - if testcase['condition'].lower() == 'base': - self.base_testcase = testcase - elif testcase['condition'].lower() == 'setup': - self.setup_testcase = testcase - testcase['flag'] = 'N' # setup 用例只在执行其他普通用例之前执行 - elif testcase['condition'].lower() == 'snippet': - g.snippet[testcase['id']] = testcase - testcase['flag'] = 'N' - elif testcase.get('set'): - testcase['flag'] = 'N' - if testcase['set'] not in g.caseset: - g.caseset[testcase['set']] = [testcase] - else: - g.caseset[testcase['set']].append(testcase) - - def testsuite_start(self): - self.result['no'] = g.no - g.no += 1 - self.result['testsuite'] = self.sheet_name - self.result['start_timestamp'] = timestamp() - - def testsuite_end(self): - self.result['end_timestamp'] = timestamp() - g.testsuite_data[self.sheet_name] = self.result - - def setup(self, testcase, case): - if self.setup_testcase: - logger.info('*** SETUP testcase ↓ ***') - logger.info('-'*50) - else: - logger.info('...No SETUP testcase need to run...') - - def run_setup(testcase): - if testcase: - tc = TestCase(testcase) - tc.run() - if testcase['result'] == 'success': - flag = 'Y' - else: - flag = 'N' - else: - flag = 'O' - return flag - setup_flag = run_setup(deepcopy(self.setup_testcase)) - - if setup_flag == 'N': - base_flag = run_setup(deepcopy(self.base_testcase)) - if base_flag == 'Y': - setup_flag = run_setup(deepcopy(self.setup_testcase)) - if setup_flag == 'N': - testcase['result'] = 'blocked' - case.block('Blocked', 'SETUP is not success') - logger.info('-'*50) - logger.info(f'>>> Run the testcase: {testcase["id"]}|{testcase["title"]}') - logger.warn('>>>>>>>>>>>>>>>>>>>> blocked <<<<<<<<<<<<<<<<<<<< SETUP is not success') - return False - elif base_flag == 'O': - testcase['result'] = 'blocked' - case.block('Blocked', 'SETUP is not success') - logger.info('-'*50) - logger.info(f'>>> Run the testcase: {testcase["id"]}|{testcase["title"]}') - logger.warn('>>>>>>>>>>>>>>>>>>>> blocked <<<<<<<<<<<<<<<<<<<< SETUP is not success') - return False - - return True - - def run_testcase(self, testcase): - # 根据筛选条件,把不需要执行的测试用例跳过 - flag = False - for k, v in self.conditions.items(): - if not isinstance(v, list): - v = [v] - if testcase[k] not in v: - testcase['result'] = 'skipped' - flag = True - if flag: - return - - if testcase['condition'].lower() == 'base': - logger.info('*** BASE testcase ↓ ***') - - if testcase['condition'].lower() == 'setup': - return - - # 统计开始时间 - testcase['start_timestamp'] = timestamp() - # xml 测试报告-测试用例初始化 - if testcase['flag'] != 'N': - # 如果前置条件失败了,直接设置为阻塞 - if self.blocked_flag: - testcase['result'] = 'blocked' - testcase['end_timestamp'] = timestamp() - return - - case = self.report.create_case( - testcase['title'], testcase['id']) - case.start() - case.priority = testcase['priority'] - # 用例上下文 - self.previous = self.current - self.current = testcase - else: - testcase['result'] = 'skipped' - # case.skip('Skip', 'Autotest Flag is N') - # logger.info('Run the testcase: %s|%s skipped, because the flag=N or the condition=snippet' % ( - # testcase['id'], testcase['title'])) - # 统计结束时间 - testcase['end_timestamp'] = timestamp() - return - - if testcase['condition'].lower() not in ('base', 'setup'): - if testcase['condition'].lower() == 'sub': - if self.previous['result'] != 'success': - testcase['result'] = 'blocked' - case.block( - 'Blocked', 'Main or pre Sub testcase is not success') - logger.info('-'*50) - logger.info(f'>>> Run the testcase: {testcase["id"]}|{testcase["title"]}') - logger.warn('>>>>>>>>>>>>>>>>>>>> blocked <<<<<<<<<<<<<<<<<<<< Main or pre Sub TestCase is not success') - # 统计结束时间 - testcase['end_timestamp'] = timestamp() - return - # 如果前置条件为 skip,则此用例不执行前置条件 - elif testcase['condition'].lower() == 'skip': - pass - else: - result = self.setup(testcase, case) - # if result == 'N': - if not result: - # 统计结束时间 - testcase['end_timestamp'] = timestamp() - return - - try: - testcase['title'] = replace(testcase['title']) # 在多次跑用例集合时,可以在用例标题中使用变量区分 - testcase['id'] = replace(testcase['id']) # 在多次跑用例集合时,可以在用例 id 中使用变量区分 - tc = TestCase(testcase) - logger.info('-'*50) - tc.run() - - # 统计结束时间 - testcase['end_timestamp'] = timestamp() - - if testcase['result'] == 'success': - case.succeed() - elif testcase['result'] == 'failure': - case.fail('Failure', testcase['report']) - if testcase['condition'].lower() == 'base': - logger.warn('Run the testcase: %s|%s Failure, BASE is not success. Break the AutoTest' % ( - testcase['id'], testcase['title'])) - self.blocked_flag = True - return - if testcase['condition'].lower() == 'setup': - logger.warn('Run the testcase: %s|%s failure, SETUP is not success. Break the AutoTest' % ( - testcase['id'], testcase['title'])) - self.blocked_flag = True - return - except Exception as exception: - case.error('Error', 'Remark:%s |||Exception:%s' % - (testcase['remark'], exception)) - logger.exception('Run the testcase: %s|%s failure' % - (testcase['id'], testcase['title'])) - if testcase['condition'].lower() == 'base': - logger.warn('Run the testcase: %s|%s error, BASE is not success. Break the AutoTest' % ( - testcase['id'], testcase['title'])) - self.blocked_flag = True - return - if testcase['condition'].lower() == 'setup': - logger.warn('Run the testcase: %s|%s error, SETUP is not success. Break the AutoTest' % ( - testcase['id'], testcase['title'])) - self.blocked_flag = True - return - - - def run(self): - - self.testsuite_start() - - # 当前测试用例 - self.current = {'result': 'success'} - # 上一个测试用例 - self.previous = {} - - # 前置条件执行失败标志,即未执行用例阻塞标志 - self.blocked_flag = False - - for testcase in self.testsuite: - self.run_testcase(testcase) - - for key in g.db: - try: - g.db[key].connect.close() - except: - pass - - self.report.finish() - self.testsuite += g.casesets # 把用例组合执行结果添加到末尾 - - # 2.清理环境 - try: - if g.platform.lower() in ('desktop',): - # w.close() - g.driver.quit() - logger.info('--- Quit th Driver: %s' % g.browserName) - except: - logger.exception('Clear the env is fail') - - self.testsuite_end() - - diff --git a/sweetest/sweetest/utility.py b/sweetest/sweetest/utility.py deleted file mode 100644 index 7239c8b..0000000 --- a/sweetest/sweetest/utility.py +++ /dev/null @@ -1,492 +0,0 @@ -from selenium.webdriver.common.keys import Keys -from pathlib import Path -import xlrd -import xlsxwriter -from openpyxl import Workbook, load_workbook -import csv -import re -import json -import time -import random -from sweetest.config import header -from sweetest.globals import g - - -path = Path('lib') -if path.is_dir(): - from lib import * -else: - from sweetest.lib import * - - -class Excel: - def __init__(self, file_name, mode='r'): - if mode == 'r': - self.workbook = xlrd.open_workbook(file_name) - elif mode == 'w': - self.workbook = xlsxwriter.Workbook(file_name) - else: - raise Exception( - 'Error: init Excel class with error mode: %s' % mode) - - def get_sheet(self, sheet_name): - names = [] - if isinstance(sheet_name, str): - if sheet_name.endswith('*'): - for name in self.workbook.sheet_names(): - if sheet_name[:-1] in name: - names.append(name) - else: - names.append(sheet_name) - elif isinstance(sheet_name, list): - names = sheet_name - else: - raise Exception('Error: invalidity sheet_name: %s' % sheet_name) - - return names - - def read(self, sheet_name): - ''' - sheet_name:Excel 中标签页名称 - return:[[],[]……] - ''' - sheet = self.workbook.sheet_by_name(sheet_name) - nrows = sheet.nrows - data = [] - for i in range(nrows): - data.append(sheet.row_values(i)) - return data - - def write(self, data, sheet_name): - sheet = self.workbook.add_worksheet(sheet_name) - - red = self.workbook.add_format({'bg_color': 'red', 'color': 'white'}) - gray = self.workbook.add_format({'bg_color': 'gray', 'color': 'white'}) - green = self.workbook.add_format( - {'bg_color': 'green', 'color': 'white'}) - blue = self.workbook.add_format({'bg_color': 'blue', 'color': 'white'}) - orange = self.workbook.add_format( - {'bg_color': 'orange', 'color': 'white'}) - for i in range(len(data)): - for j in range(len(data[i])): - if str(data[i][j]) == 'failure': - sheet.write(i, j, str(data[i][j]), red) - elif str(data[i][j]) == 'NO': - sheet.write(i, j, str(data[i][j]), gray) - elif str(data[i][j]) == 'blocked': - sheet.write(i, j, str(data[i][j]), orange) - elif str(data[i][j]) == 'skipped': - sheet.write(i, j, str(data[i][j]), blue) - elif str(data[i][j]) == 'success': - sheet.write(i, j, str(data[i][j]), green) - else: - sheet.write(i, j, data[i][j]) - - def close(self): - self.workbook.close() - - -class Excel_01(): - def __init__(self, file_name, mode='r'): - if mode == 'r': - self.workbook = load_workbook(file_name) - elif mode == 'w': - self.workbook = Workbook() #.Workbook(file_name) - else: - raise Exception( - 'Error: init Excel class with error mode: %s' % mode) - - def get_sheet(self, sheet_name): - names = [] - if isinstance(sheet_name, str): - if sheet_name.endswith('*'): - for name in self.workbook.sheet_names(): - if sheet_name[:-1] in name: - names.append(name) - else: - names.append(sheet_name) - elif isinstance(sheet_name, list): - names = sheet_name - else: - raise Exception('Error: invalidity sheet_name: %s' % sheet_name) - - return names - - def read(self, sheet_name): - ''' - sheet_name:Excel 中标签页名称 - return:[[],[]……] - ''' - sheet = self.workbook.sheet_by_name(sheet_name) - nrows = sheet.nrows - data = [] - for i in range(nrows): - data.append(sheet.row_values(i)) - return data - - def write(self, data, sheet_name): - sheet = self.workbook.add_worksheet(sheet_name) - - red = self.workbook.add_format({'bg_color': 'red', 'color': 'white'}) - gray = self.workbook.add_format({'bg_color': 'gray', 'color': 'white'}) - green = self.workbook.add_format( - {'bg_color': 'green', 'color': 'white'}) - blue = self.workbook.add_format({'bg_color': 'blue', 'color': 'white'}) - orange = self.workbook.add_format( - {'bg_color': 'orange', 'color': 'white'}) - for i in range(len(data)): - for j in range(len(data[i])): - if str(data[i][j]) == 'failure': - sheet.write(i, j, str(data[i][j]), red) - elif str(data[i][j]) == 'NO': - sheet.write(i, j, str(data[i][j]), gray) - elif str(data[i][j]) == 'blocked': - sheet.write(i, j, str(data[i][j]), orange) - elif str(data[i][j]) == 'skipped': - sheet.write(i, j, str(data[i][j]), blue) - elif str(data[i][j]) == 'success': - sheet.write(i, j, str(data[i][j]), green) - else: - sheet.write(i, j, data[i][j]) - - def close(self): - self.workbook.close() - - -def data2dict(data): - # def list_list2list_dict(data): - ''' - 把带头标题的二维数组,转换成以标题为 key 的 dict 的 list - ''' - list_dict_data = [] - key = [] - g.header_custom = {} # 用户自定义的标题 - for d in data[0]: - k = d.strip().split('#')[0] - # 如果为中文,则替换成英文 - h = header.get(k, k).lower() - key.append(h) - g.header_custom[h] = d.strip() - - if not g.header_custom.get('expected'): - g.header_custom['expected'] = '' - - for d in data[1:]: - dict_data = {} - for i in range(len(key)): - if isinstance(d[i], str): - dict_data[key[i]] = str(d[i]).strip() - else: - dict_data[key[i]] = d[i] - list_dict_data.append(dict_data) - return list_dict_data - - -def replace_dict(data): - # 变量替换 - for key in data: - data[key] = replace(data[key]) - - -def replace_list(data): - # 变量替换 - for i in range(len(data)): - data[i] = replace(data[i]) - - -def replace_old(data): - # 正则匹配出 data 中所有 <> 中的变量,返回列表 - keys = re.findall(r'<(.*?)>', data) - for k in keys: - # 正则匹配出 k 中的 + - ** * // / % , ( ) 返回列表 - values = re.split(r'(\+|-|\*\*|\*|//|/|%|,|\(|\)|\'|\")', k) - for j, v in enumerate(values): - #切片操作处理,正则匹配出 [] 中内容 - s = v.split('[', 1) - index = '' - if len(s) == 2: - v = s[0] - index = '[' + s[1] - - if v in g.var: - # 如果在 g.var 中是 list - if isinstance(g.var[v], list): - if index: - # list 切片取值(值应该是动态赋值的变量,如自定义脚本的返回值) - values[j] = eval('g.var[v]' + index) - else: - if len(g.var[v]) == 1: - values[j] = g.var[v][0] - g.var['_last_'] = True - else: - values[j] = g.var[v].pop(0) - elif isinstance(g.var[v], dict) and index: - # 如果是 dict 取键值 - values[j] = eval('g.var[v]' + index) - else: - # 如果在 g.var 中是值,则直接赋值 - values[j] = g.var[v] - if index: - values[j] = eval('g.var[v]' + index) - # 如果值不在 g.var,且只有一个元素,则尝试 eval,比如,,<1>,<9.999> - elif len(values) == 1: - try: - values[j] = eval(values[j]) - except: - pass - - # 如果 values 长度大于 1,说明有算术运算符,则用 eval 运算 - # 注意,先要把元素中的数字变为字符串 - if len(values) > 1: - values = eval(''.join([str(x) for x in values])) - # 如果 values 长度为 1,则直接赋值,注意此值可能是数字 - else: - values = values[0] - # 如果 data 就是一个 <>,如 data = '',则直接赋值为 values,此值可能是数字 - if data == '<' + keys[0] + '>': - data = values - # 如果有键盘操作,则需要 eval 处理 - if isinstance(data, str) and 'Keys.' in data: - data = eval(data) - # 否则需要替换,此时变量强制转换为为字符串 - else: - data = data.replace('<' + k + '>', str(values)) - return data - - -def replace(data): - - if data.startswith("'''") and data.endswith("'''"): - return data[3:-3] - - left_angle = 'dsfw4rwfdfstg43' - right_angle = '3dsdtgsgt43trfdf' - left_delimiter = '<' - right_delimiter = '>' - data = data.replace(r'\<', left_angle).replace(r'\>', right_angle) - - if '<<' in data and '>>' in data: - left_delimiter = '<<' - right_delimiter = '>>' - - # 正则匹配出 data 中所有 <> 中的变量,返回列表 - keys = re.findall(r'%s' %(left_delimiter+'(.*?)'+right_delimiter), data) - _vars = {} - - for k in keys: - k = k.replace(left_angle, '<').replace(right_angle, '>') - # 正则匹配出 k 中的 + - ** * // / % , ( ) 返回列表 - values = re.split(r'(\+|-|\*\*|\*|//|/|%|,|\(|\))', k) - for v in values: - #切片操作处理,正则匹配出 [] 中内容 - s = v.split('[', 1) - index = '' - if len(s) == 2: - v = s[0] - index = '[' + s[1] - - if v not in _vars: - if v in g.var: - # 如果在 g.var 中是值,则直接赋值 - _vars[v] = g.var[v] - elif v in g.test_data: - # 是测试数据文件中的值 - if len(g.test_data[v]) == 0: # 为空 list 报错 - raise Exception('The key:%s is no value in data csv' %v) - elif len(g.test_data[v]) == 1: # 最后一个 list 元素,赋值 - _vars[v] = g.test_data[v][0] - g.var['_last_'] = True # 处理循环次数为 N 的情况,此标志标明循环到最后一个变量 - else: # 大于 1 个元素,pop 赋值 - _vars[v] = g.test_data[v].pop(0) - - try: - value = eval(k, globals(), _vars) - except NameError: - value = left_delimiter+ k + right_delimiter - - if data == left_delimiter+ keys[0] + right_delimiter: - data = value - # 否则需要替换,此时变量强制转换为为字符串 - else: - data = data.replace(left_delimiter + k + right_delimiter, str(value)) - - if isinstance(data, str): - data = data.replace(left_angle, '<').replace(right_angle, '>') - - return data - - -def test_replace(): - g.var = {'a': 1, 'b': 'B'} - for d in ('', '', 'abc', 'abc', '', ''): - print('data:%s' % d) - data = replace(d) - print(repr(data)) - - -def read_csv(csv_file, encoding=None): - data = [] - with open(csv_file, encoding=encoding) as f: - reader = csv.reader(f) - for line in reader: - data.append(line) - return data - - -def write_csv(csv_file, data, encoding=None): - with open(csv_file, 'w', encoding=encoding, newline='') as f: - writer = csv.writer(f) - writer.writerows(data) - - -def get_record(data_file): - encoding = None - try: - data = read_csv(data_file, encoding='utf-8') - encoding = 'utf-8' - except: - data = read_csv(data_file) - - def read_data(): - num = len(data[0])-1 if flag else len(data[0]) - for i in range(num): - if d[i]: - k = data[0][i] - if record.get(k, None): - # if isinstance(record[k], str): - # record[k] = [record[k]] - record[k].append(d[i]) - else: - record[k] = [d[i]] - if record[k][-1] == '"': # 空字符转换 - record[k][-1] = '' - - record = {} - flag = False - if data[0][-1].lower() == 'flag': - flag = True - - for d in data[1:]: - if not flag: - read_data() - elif d[-1] == 'N': - read_data() - elif d[-1] != 'Y': - read_data() - d[-1] = 'Y' - write_csv(data_file, data, encoding=encoding) - break - return record - - -def str2int(s): - s = str(s).replace(',', '').split('.', 1) - if len(s) == 2: - dot = s[-1] - assert int(dot) == 0 - return int(s[0]) - - -def zero(s): - if s and s[-1] == '0': - s = s[:-1] - s = zero(s) - return s - - -def str2float(s, n=None): - s = str(s).replace(',', '') - number = s.split('.', 1) - if n: - f = float(s) - return round(f, n), n - - dot = '0' - if len(number) == 2: - dot = number[-1] - dot = zero(dot) - f = float(number[0] + '.' + dot) - - return round(f, len(dot)), len(dot) - - -def f(v, e, n=2): - ''' - 判断2个 float 数值是否相同,类型可以是 str 或 float - v: 实际值,如:12.345, '1234.56', '1,234.5600' - e: 预期结果, 示例值同 v - n: 小数点精确位数 - ''' - v = str(v).replace(',', '') - e = str(e).replace(',', '') - _v = round(float(v), n) - _e = round(float(e), n) - assert round(_v, n) == round(_e, n) - - -def mkdir(p): - path = Path(p) - if not path.is_dir(): - path.mkdir() - - -def json2dict(s): - if isinstance(s, dict): - return s - s = str(s) - d = {} - try: - d = json.loads(s) - except: - try: - d = eval(s) - except: - s = s.replace('true', 'True').replace('false', 'False').replace( - 'null', 'None').replace('none', 'None') - d = eval(s) - return d - - -def compare(data, real): - if isinstance(data, str): - - if data.startswith('#'): - assert data[1:] != str(real) - return - elif data.startswith(':'): - exec('v=real;'+data[1:]) - return - - assert isinstance(real, str) - - if data.startswith('*'): - assert data[1:] in real - return - elif data.startswith('^'): - assert real.startswith(data[1:]) - return - elif data.startswith('$'): - assert real.endswith(data[1:]) - return - - elif data.startswith('\\'): - data = data[1:] - - assert data == real - - elif isinstance(data, int): - assert isinstance(real, int) - assert data == real - elif isinstance(data, float): - assert isinstance(real, float) - data, p1 = str2float(data) - real, p2 = str2float(real) - p = min(p1, p2) - assert round(data, p) == round(real, p) - else: - assert data == real - - -def timestamp(): - # js 格式的时间戳 - return int(time.time() * 1000) diff --git a/sweetest/sweetest/windows.py b/sweetest/sweetest/windows.py deleted file mode 100644 index 11d4f09..0000000 --- a/sweetest/sweetest/windows.py +++ /dev/null @@ -1,148 +0,0 @@ -from sweetest.globals import g -from sweetest.log import logger - - -class Windows: - def __init__(self): - self.init() - - def init(self): - # 当前窗口名字,如:'新版门户首页窗口', 'HOME' - self.current_window = '' - # 所有窗口名字--窗口handle映射表,如: - # {'新版门户首页窗口': 'CDwindow-3a12c86f-1986-4c02-ba7b-5a0ed94c5963', 'HOME': 'CDwindow-a3f0c44c-d269-4ff0-af38-c31ad70c69e3'} - self.windows = {} - # 当前frame名字 - self.frame = 0 - # 所有页面--窗口名字映射表,如:{'门户首页': '新版门户首页窗口'} - self.pages = {} - # 新开窗口标志 - self.new_window_flag = True - # App context - self.current_context = 'NATIVE_APP' - - def switch_window(self, page): - all_handles = g.driver.window_handles - for key in list(self.windows.keys()): - if self.windows[key] not in all_handles: - self.current_window = '' - self.windows.pop(key) - - if self.new_window_flag: - if page in list(self.pages): - page = '通用' - g.current_page = '通用' - self.new_window_flag = False - - if page != '通用': - if page not in list(self.pages): - # 如果当前页未注册,则需要先清除和当前窗口绑定的页面 - for k in list(self.pages): - if self.current_window == self.pages[k]: - self.pages.pop(k) - # 在把当前窗口捆定到当前页面 - self.pages[page] = self.current_window - - elif self.pages[page] != self.current_window: - # 如果当前窗口为 HOME,则关闭之 - if self.current_window == 'HOME': - g.driver.close() - self.windows.pop('HOME') - # 再切换到需要操作的窗口 - tw = self.windows[self.pages[page]] - logger.info('--- Switch Windows: %s' % repr(tw)) - g.driver.switch_to_window(tw) - self.current_window = self.pages[page] - logger.info('--- Current Windows: %s' % - repr(self.current_window)) - - def switch_frame(self, frame): - if frame.strip(): - frame = [x.strip() for x in frame.split('|')] - if frame != self.frame: - if self.frame != 0: - g.driver.switch_to.default_content() - for f in frame: - logger.info('--- Frame Value: %s' % repr(f)) - if f.startswith('#'): - f = int(f[1:]) - elif '#' in f: - from sweetest.testcase import elements_format - from sweetest.locator import locating_element - element = elements_format('通用', f)[2] - f = locating_element(element) - logger.info('--- Switch Frame: %s' % repr(f)) - g.driver.switch_to.frame(f) - self.frame = frame - else: - if self.frame != 0: - g.driver.switch_to.default_content() - self.frame = 0 - - def open(self, step): - # 查看当前窗口是否已经注册到 windows 映射表 - c = self.windows.get(self.current_window, '') - # 如果已经存在,则需要清除和当前窗口绑定的页面 - if c: - for k in list(self.pages): - if self.current_window == self.pages[k]: - self.pages.pop(k) - # 并从映射表里剔除 - self.windows.pop(self.current_window) - - # 获取当前窗口handle - handle = g.driver.current_window_handle - # 注册窗口名称和handle - self.register(step, handle) - - def register(self, step, handle): - # 如果有提供新窗口名字,则使用该名字,否则使用默认名字:HOME - #new_window = step['data'].get('新窗口', 'HOME') - # 新窗口 变为 标签页名,兼容原有格式 - new_window = 'HOME' - for k in ('新窗口', '标签页名', 'tabname', '#tab_name'): - if step['data'].get(k): - new_window = step['data'].get(k) - # 已存在同名的窗口,则 - if new_window in self.windows: - # 1. 清除和当前窗口同名的旧窗口绑定的页面 - for k in list(self.pages): - if new_window == self.pages[k]: - self.pages.pop(k) - - # 2. 切换到同名旧窗口去关闭它 - g.driver.switch_to_window(self.windows[new_window]) - g.driver.close() - # 3. 并从窗口资源池 g.windows 里剔除 - self.windows.pop(new_window) - # 然后切回当前窗口 - g.driver.switch_to_window(handle) - # 再添加到窗口资源池 g.windows - self.windows[new_window] = handle - # 把当前窗口名字改为新窗口名称 - self.current_window = new_window - # 新窗口标志置为是 - self.new_window_flag = True - - def close(self): - all_handles = g.driver.window_handles - for handle in all_handles: - # 切换到每一个窗口,并关闭它 - g.driver.switch_to_window(handle) - g.driver.close() - logger.info('--- Close th Windows: %s' % repr(handle)) - - def switch_context(self, context): - if context.strip() == '': - context = 'NATIVE_APP' - # logger.info('--- ALL Contexts:%s' % g.driver.contexts) - # logger.info('--- Input Context:%s' % repr(context)) - if context != self.current_context: - if context == '': - context = None - logger.info('--- Switch Context:%s' % repr(context)) - g.driver.switch_to.context(context) - self.current_context = context - - -w = Windows() diff --git a/sweetest/testcase/Baidu-TestCase.xlsx b/sweetest/testcase/Baidu-TestCase.xlsx deleted file mode 100644 index 69a4f34..0000000 Binary files a/sweetest/testcase/Baidu-TestCase.xlsx and /dev/null differ diff --git a/sweetest/testcase/Echo-TestCase.xlsx b/sweetest/testcase/Echo-TestCase.xlsx deleted file mode 100644 index 8885286..0000000 Binary files a/sweetest/testcase/Echo-TestCase.xlsx and /dev/null differ diff --git a/sweetest/testcase/Notepad-TestCase.xlsx b/sweetest/testcase/Notepad-TestCase.xlsx deleted file mode 100644 index e34898e..0000000 Binary files a/sweetest/testcase/Notepad-TestCase.xlsx and /dev/null differ